feat(pwa): installable PWA (manifest + 빈 SW + SVG 아이콘)
설치 가능한 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 응답 간섭 회피.
This commit is contained in:
@@ -1,11 +1,32 @@
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import { AlertsToaster } from "../components/AlertsToaster";
|
import { AlertsToaster } from "../components/AlertsToaster";
|
||||||
|
import { PwaRegister } from "../components/PwaRegister";
|
||||||
import { TopNav } from "../components/TopNav";
|
import { TopNav } from "../components/TopNav";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Stock Chart Site",
|
title: {
|
||||||
description: "개인용 주식 예측 차트",
|
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 }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
@@ -15,6 +36,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
<TopNav />
|
<TopNav />
|
||||||
{children}
|
{children}
|
||||||
<AlertsToaster />
|
<AlertsToaster />
|
||||||
|
<PwaRegister />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
21
web/components/PwaRegister.tsx
Normal file
21
web/components/PwaRegister.tsx
Normal file
@@ -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;
|
||||||
|
}
|
||||||
28
web/public/icon.svg
Normal file
28
web/public/icon.svg
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="주식 차트">
|
||||||
|
<!-- maskable safe-zone: 중심 80% (102.4 ~ 409.6) 안에 핵심 요소를 둠. -->
|
||||||
|
<rect width="512" height="512" rx="96" fill="#09090b"/>
|
||||||
|
<!-- 격자 가이드 (옅게) -->
|
||||||
|
<g stroke="#27272a" stroke-width="2">
|
||||||
|
<line x1="64" y1="160" x2="448" y2="160"/>
|
||||||
|
<line x1="64" y1="256" x2="448" y2="256"/>
|
||||||
|
<line x1="64" y1="352" x2="448" y2="352"/>
|
||||||
|
</g>
|
||||||
|
<!-- 캔들 (한국 컬러: 상승=빨강 / 하락=파랑) -->
|
||||||
|
<g>
|
||||||
|
<!-- 하락 -->
|
||||||
|
<line x1="144" y1="120" x2="144" y2="300" stroke="#3b82f6" stroke-width="6"/>
|
||||||
|
<rect x="124" y="180" width="40" height="100" fill="#3b82f6"/>
|
||||||
|
<!-- 상승 -->
|
||||||
|
<line x1="224" y1="160" x2="224" y2="360" stroke="#ef4444" stroke-width="6"/>
|
||||||
|
<rect x="204" y="200" width="40" height="140" fill="#ef4444"/>
|
||||||
|
<!-- 하락 -->
|
||||||
|
<line x1="304" y1="200" x2="304" y2="380" stroke="#3b82f6" stroke-width="6"/>
|
||||||
|
<rect x="284" y="240" width="40" height="120" fill="#3b82f6"/>
|
||||||
|
<!-- 상승 (가장 큼 — 우상향 인상) -->
|
||||||
|
<line x1="384" y1="100" x2="384" y2="340" stroke="#ef4444" stroke-width="6"/>
|
||||||
|
<rect x="364" y="140" width="40" height="180" fill="#ef4444"/>
|
||||||
|
</g>
|
||||||
|
<!-- 추세선 (사선 우상향) -->
|
||||||
|
<polyline points="80,380 160,330 240,280 320,260 400,180 448,140"
|
||||||
|
fill="none" stroke="#fbbf24" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
21
web/public/manifest.webmanifest
Normal file
21
web/public/manifest.webmanifest
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
13
web/public/sw.js
Normal file
13
web/public/sw.js
Normal file
@@ -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());
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user