feat(install): /install 페이지 — 안드로이드/아이폰/PC 별 PWA 설치 가이드
Deploy momo-erp / deploy (push) Successful in 4m25s

노인 사용자(거래처 사장님 등) 도 따라할 수 있도록 큰 글씨 + 단계별 안내.
User-Agent 자동 감지로 해당 기기 가이드 우선 표시, 탭으로 다른 기기 전환 가능.

* 안드로이드: Chrome → 앱 설치 배너 → 4단계
* 아이폰: Safari → 공유 → 홈 화면에 추가 → 5단계 (사파리 필수 경고 강조)
* PC: QR 코드 (휴대폰 카메라로 즉시 안내 페이지 이동)

모바일 로그인 화면 하단에 "📱 휴대폰 홈 화면에 앱처럼 설치하는 방법" 링크 추가.
middleware publicPaths 에 /install 추가 (비로그인 접근 허용).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-05-20 13:58:36 +09:00
parent 7a712c164e
commit d95a736701
3 changed files with 270 additions and 0 deletions
+10
View File
@@ -218,6 +218,16 @@ export default function MobileLoginPage() {
</form>
</div>
{/* 앱 설치 안내 링크 */}
<div className="relative z-10 text-center pt-3">
<a
href="/install"
className="inline-flex items-center gap-1.5 text-xs text-emerald-50/90 hover:text-white underline underline-offset-4 decoration-emerald-300/50"
>
📱
</a>
</div>
{/* 푸터 */}
<div className="relative z-10 text-center pb-4 pt-2">
<p className="text-[10px] text-emerald-100/60 tracking-wide">
+259
View File
@@ -0,0 +1,259 @@
// 앱 설치 안내 페이지 — 노인 사용자도 따라할 수 있도록 큰 글씨 + 단계별 안내.
// User-Agent 로 안드로이드/아이폰/PC 감지해서 해당 가이드만 보여줌.
"use client";
import { useEffect, useState } from "react";
import { Smartphone, ChevronRight, Apple, Globe } from "lucide-react";
type Device = "android" | "ios" | "desktop";
export default function InstallGuidePage() {
const [device, setDevice] = useState<Device>("android");
const [autoDetected, setAutoDetected] = useState(false);
useEffect(() => {
const ua = navigator.userAgent.toLowerCase();
if (/iphone|ipad|ipod/.test(ua)) setDevice("ios");
else if (/android/.test(ua)) setDevice("android");
else setDevice("desktop");
setAutoDetected(true);
}, []);
return (
<main className="min-h-screen bg-gradient-to-b from-emerald-50 to-white px-4 py-6 sm:py-10">
<div className="max-w-md mx-auto">
{/* 헤더 */}
<div className="text-center mb-6 sm:mb-8">
<div className="w-20 h-20 bg-emerald-700 rounded-2xl mx-auto mb-3 flex items-center justify-center shadow-lg">
<span className="text-white text-3xl font-black">M</span>
</div>
<h1 className="text-2xl sm:text-3xl font-black text-slate-900">ERP </h1>
<p className="text-base text-slate-600 mt-2"> </p>
</div>
{/* 기종 선택 */}
<div className="bg-white border border-slate-200 rounded-2xl p-2 mb-6 shadow-sm">
<div className="grid grid-cols-3 gap-1">
<TabBtn active={device === "android"} onClick={() => setDevice("android")}>
<Smartphone size={20} />
<span></span>
</TabBtn>
<TabBtn active={device === "ios"} onClick={() => setDevice("ios")}>
<Apple size={20} />
<span></span>
</TabBtn>
<TabBtn active={device === "desktop"} onClick={() => setDevice("desktop")}>
<Globe size={20} />
<span></span>
</TabBtn>
</div>
{autoDetected && (
<p className="text-[11px] text-center text-slate-400 mt-2">
</p>
)}
</div>
{/* 가이드 본문 */}
{device === "android" && <AndroidGuide />}
{device === "ios" && <IosGuide />}
{device === "desktop" && <DesktopGuide />}
{/* 하단 — 로그인 바로가기 */}
<div className="mt-8 pt-6 border-t border-slate-200 text-center space-y-3">
<a
href="/m/login"
className="inline-flex items-center gap-2 px-6 h-12 rounded-xl bg-emerald-700 text-white text-base font-bold shadow-md active:translate-y-px"
>
<ChevronRight size={18} />
</a>
<p className="text-xs text-slate-500"> 010-6369-8443</p>
</div>
</div>
</main>
);
}
function TabBtn({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) {
return (
<button
onClick={onClick}
className={`flex flex-col items-center gap-1 py-3 rounded-xl text-xs font-bold transition ${
active
? "bg-emerald-700 text-white shadow"
: "bg-transparent text-slate-500 hover:bg-slate-50"
}`}
>
{children}
</button>
);
}
// ─────────────────────────────────────────
// 안드로이드 — Chrome 으로 PWA 설치
// ─────────────────────────────────────────
function AndroidGuide() {
return (
<div className="space-y-4">
<div className="bg-emerald-50 border border-emerald-200 rounded-2xl p-4 text-center">
<p className="text-base font-bold text-emerald-900">
📱
</p>
<p className="text-sm text-emerald-700 mt-1"> </p>
</div>
<Step n={1} title="크롬(Chrome) 앱 열기">
<p> <b className="text-emerald-700"></b> () .</p>
<div className="mt-2 flex items-center gap-2 text-sm text-slate-500">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500 via-red-500 to-yellow-400 flex items-center justify-center text-white font-black text-sm">C</div>
<span> </span>
</div>
</Step>
<Step n={2} title="momotogether.com 주소 입력">
<p> <b className="text-emerald-700 text-lg break-all">momotogether.com</b> .</p>
</Step>
<Step n={3} title='"앱 설치" 누르기'>
<p>
<b className="text-emerald-700">&quot; &quot;</b>
<b className="text-emerald-700"> &quot; &quot;</b> .
.
</p>
<p className="mt-2 text-sm text-slate-500">
, <b> </b> <b>&quot; &quot;</b> .
</p>
</Step>
<Step n={4} title='"설치" 한 번 더 누르기'>
<p> <b className="text-emerald-700">&quot;&quot;</b> .</p>
</Step>
<FinishBox>
<b>ERP</b> . .
</FinishBox>
</div>
);
}
// ─────────────────────────────────────────
// 아이폰 — Safari 로 PWA 설치
// ─────────────────────────────────────────
function IosGuide() {
return (
<div className="space-y-4">
<div className="bg-blue-50 border border-blue-200 rounded-2xl p-4 text-center">
<p className="text-base font-bold text-blue-900">
()
</p>
<p className="text-sm text-blue-700 mt-1"> (Safari) </p>
</div>
<div className="bg-amber-50 border-2 border-amber-300 rounded-2xl p-4">
<p className="text-base font-bold text-amber-900"> (Safari) </p>
<p className="text-sm text-amber-800 mt-1">· .</p>
</div>
<Step n={1} title="사파리(Safari) 앱 열기">
<p> <b className="text-blue-700"></b> .</p>
<div className="mt-2 flex items-center gap-2 text-sm text-slate-500">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-sky-400 to-blue-600 flex items-center justify-center text-white text-lg">🧭</div>
<span> </span>
</div>
</Step>
<Step n={2} title="momotogether.com 주소 입력">
<p> <b className="text-blue-700 text-lg break-all">momotogether.com</b> .</p>
</Step>
<Step n={3} title="공유 버튼 누르기">
<p> <b> </b> <b className="text-blue-700"> </b> .</p>
<div className="mt-2 flex items-center gap-2 text-sm text-slate-500">
<div className="w-10 h-10 rounded-xl border-2 border-blue-500 flex items-center justify-center text-blue-600 text-xl"></div>
<span> + </span>
</div>
</Step>
<Step n={4} title='"홈 화면에 추가" 누르기'>
<p>
<b> </b>
<b className="text-blue-700"> &quot; &quot;</b> . .
</p>
</Step>
<Step n={5} title='"추가" 한 번 더 누르기'>
<p> <b className="text-blue-700">&quot;&quot;</b> .</p>
</Step>
<FinishBox>
<b>ERP</b> . .
</FinishBox>
</div>
);
}
// ─────────────────────────────────────────
// PC — QR 안내 + 바로 사용 옵션
// ─────────────────────────────────────────
function DesktopGuide() {
return (
<div className="space-y-4">
<div className="bg-slate-50 border border-slate-200 rounded-2xl p-4 text-center">
<p className="text-base font-bold text-slate-900">💻 PC </p>
<p className="text-sm text-slate-600 mt-1"> , </p>
</div>
<Step n={1} title="휴대폰에서 설치하려면 — QR 코드">
<p> QR .</p>
<div className="mt-3 flex justify-center">
<img
src="https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=https%3A%2F%2Fmomotogether.com%2Finstall"
alt="설치 페이지 QR 코드"
className="rounded-xl border border-slate-200"
width={220}
height={220}
/>
</div>
</Step>
<Step n={2} title="컴퓨터로 바로 사용하기">
<p>· momotogether.com .</p>
<p className="mt-2 text-sm text-slate-500">
<b></b> <b>+</b> .
</p>
</Step>
<FinishBox>
PC .
</FinishBox>
</div>
);
}
// ─────────────────────────────────────────
function Step({ n, title, children }: { n: number; title: string; children: React.ReactNode }) {
return (
<div className="bg-white border border-slate-200 rounded-2xl p-4 shadow-sm">
<div className="flex items-start gap-3">
<div className="w-9 h-9 rounded-full bg-emerald-700 text-white flex items-center justify-center font-black text-base shrink-0">
{n}
</div>
<div className="flex-1 min-w-0">
<h3 className="text-base font-bold text-slate-900 mb-1">{title}</h3>
<div className="text-base text-slate-700 leading-relaxed">{children}</div>
</div>
</div>
</div>
);
}
function FinishBox({ children }: { children: React.ReactNode }) {
return (
<div className="bg-emerald-600 text-white rounded-2xl p-5 text-center shadow-md">
<p className="text-2xl mb-1">🎉</p>
<p className="text-base font-bold"> !</p>
<p className="text-sm mt-1 text-emerald-50">{children}</p>
</div>
);
}
+1
View File
@@ -11,6 +11,7 @@ export function middleware(request: NextRequest) {
"/signup",
"/privacy",
"/account-deletion",
"/install",
"/api/auth/login",
"/api/auth/signup",
"/api/auth/mobile-login",