From 89f3a865af0ee23b37019a38b278437262cf1fa9 Mon Sep 17 00:00:00 2001 From: claude-owner Date: Thu, 28 May 2026 23:39:27 +0900 Subject: [PATCH] =?UTF-8?q?feat(pwa):=20installable=20PWA=20(manifest=20+?= =?UTF-8?q?=20=EB=B9=88=20SW=20+=20SVG=20=EC=95=84=EC=9D=B4=EC=BD=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 설치 가능한 PWA 자격을 갖추는 최소 셋: - web/public/manifest.webmanifest: 한국어 name/short_name, 다크 테마 컬러, standalone display, /icon.svg (any maskable, sizes any) - web/public/icon.svg: 다크 배경 + 한국 컬러 캔들(상승=#ef4444 / 하락=#3b82f6) + 우상향 추세선. maskable safe-zone (중심 80%) 보존 - web/public/sw.js: install/activate 만 처리 (fetch handler 없음). install 자격만 충족하고 네트워크는 그대로 통과 → 캐시 stale 위험 0 - web/components/PwaRegister.tsx: production 빌드에서만 SW 등록, 실패는 silently 무시 - web/app/layout.tsx: Metadata.manifest/icons/appleWebApp, viewport.themeColor/colorScheme 분리 (Next 14 권장) dev (next dev) 에서는 SW 등록을 우회 — HMR 응답 간섭 회피. --- web/app/layout.tsx | 28 +++++++++++++++++++++++++--- web/components/PwaRegister.tsx | 21 +++++++++++++++++++++ web/public/icon.svg | 28 ++++++++++++++++++++++++++++ web/public/manifest.webmanifest | 21 +++++++++++++++++++++ web/public/sw.js | 13 +++++++++++++ 5 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 web/components/PwaRegister.tsx create mode 100644 web/public/icon.svg create mode 100644 web/public/manifest.webmanifest create mode 100644 web/public/sw.js 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()); +});