diff --git a/web/app/layout.tsx b/web/app/layout.tsx index cc39e89..25b0df8 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,11 +1,32 @@ import "./globals.css"; -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { AlertsToaster } from "../components/AlertsToaster"; +import { PwaRegister } from "../components/PwaRegister"; import { TopNav } from "../components/TopNav"; export const metadata: Metadata = { - title: "Stock Chart Site", - description: "개인용 주식 예측 차트", + title: { + default: "주식 차트 · 예측", + template: "%s · 주식 차트", + }, + description: "한국 주식 차트와 단기 예측을 보는 개인용 앱.", + manifest: "/manifest.webmanifest", + // SVG 아이콘 하나로 다 처리. Chrome/Safari/iOS 모두 SVG favicon + apple-touch 수용. + icons: { + icon: [{ url: "/icon.svg", type: "image/svg+xml" }], + apple: [{ url: "/icon.svg", type: "image/svg+xml" }], + }, + appleWebApp: { + capable: true, + statusBarStyle: "black-translucent", + title: "주식 차트", + }, +}; + +// Next 14: themeColor / colorScheme 는 viewport 로 분리해야 빌드 경고가 안 뜸. +export const viewport: Viewport = { + themeColor: "#09090b", + colorScheme: "dark", }; export default function RootLayout({ children }: { children: React.ReactNode }) { @@ -15,6 +36,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) {children} + ); diff --git a/web/components/PwaRegister.tsx b/web/components/PwaRegister.tsx new file mode 100644 index 0000000..210dd73 --- /dev/null +++ b/web/components/PwaRegister.tsx @@ -0,0 +1,21 @@ +"use client"; + +// 최소 서비스워커 등록. +// - production 빌드에서만 등록 — dev (next dev / HMR) 환경에선 SW 가 HMR 응답을 +// 이상하게 가로채는 케이스가 있어 의도적으로 우회. +// - 등록 실패는 silently 무시 (Safari private mode, http://0.0.0.0 같은 secure context +// 아닌 호스트, 브라우저 정책 거부 등). PWA install 자격만 못 갖출 뿐 앱 기능엔 영향 없음. + +import { useEffect } from "react"; + +export function PwaRegister() { + useEffect(() => { + if (typeof window === "undefined") return; + if (!("serviceWorker" in navigator)) return; + if (process.env.NODE_ENV !== "production") return; + navigator.serviceWorker.register("/sw.js").catch(() => { + /* noop — 등록 실패 시 PWA 기능만 비활성. */ + }); + }, []); + return null; +} diff --git a/web/public/icon.svg b/web/public/icon.svg new file mode 100644 index 0000000..56fd9a9 --- /dev/null +++ b/web/public/icon.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/manifest.webmanifest b/web/public/manifest.webmanifest new file mode 100644 index 0000000..dfa1c3a --- /dev/null +++ b/web/public/manifest.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "주식 차트 · 예측", + "short_name": "주식 차트", + "description": "한국 주식 차트와 단기 예측을 보는 개인용 앱.", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "portrait", + "background_color": "#09090b", + "theme_color": "#09090b", + "lang": "ko", + "categories": ["finance", "productivity"], + "icons": [ + { + "src": "/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ] +} diff --git a/web/public/sw.js b/web/public/sw.js new file mode 100644 index 0000000..144fb4f --- /dev/null +++ b/web/public/sw.js @@ -0,0 +1,13 @@ +// 최소 서비스워커 — PWA install 자격 (manifest + SW) 만 충족시킨다. +// fetch handler 가 없어 네트워크 요청을 가로채지 않음 → 정상 동작은 그대로, +// 캐시 stale 위험 없음. 향후 오프라인 셸/사진 캐싱이 필요하면 여기에 추가. + +self.addEventListener("install", () => { + // 이전 버전 SW 를 즉시 교체. 한 페이지 새로고침이면 새 SW 가 active 가 됨. + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + // 모든 열린 탭의 client 를 즉시 control 하도록. + event.waitUntil(self.clients.claim()); +});