feat: PWA 기본 설정 (manifest, service worker, 앱 아이콘)

- manifest.json: 앱 이름 "청춘약국 마일리지", standalone 모드, 보라색 테마
- sw.js: 정적 자산 캐싱 (동적 페이지 제외)
- 앱 아이콘 192x192, 512x512 PNG 생성
- /sw.js 루트 라우트로 서비스 워커 scope 허용

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
thug0bin 2026-02-25 08:51:23 +09:00
parent 62632cb7b8
commit d1a5964bb7
4 changed files with 88 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,25 @@
{
"name": "청춘약국 마일리지",
"short_name": "청춘약국",
"description": "청춘약국 QR 마일리지 적립 서비스",
"start_url": "/my-page",
"display": "standalone",
"background_color": "#f5f7fa",
"theme_color": "#6366f1",
"orientation": "portrait",
"lang": "ko",
"icons": [
{
"src": "/static/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/static/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}

63
backend/static/sw.js Normal file
View File

@ -0,0 +1,63 @@
const CACHE_NAME = 'chungchun-pharmacy-v1';
const STATIC_ASSETS = [
'/static/js/lottie.min.js',
'/static/animations/ai-loading.json',
'/static/icons/icon-192.png',
'/static/icons/icon-512.png'
];
// Install: pre-cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS))
);
self.skipWaiting();
});
// Activate: clean old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
)
)
);
self.clients.claim();
});
// Fetch: cache-first for static, network-only for dynamic
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Skip non-GET requests
if (event.request.method !== 'GET') return;
// Skip dynamic routes entirely
if (url.pathname.startsWith('/api/') ||
url.pathname.startsWith('/admin') ||
url.pathname.startsWith('/claim') ||
url.pathname.startsWith('/my-page') ||
url.pathname === '/privacy' ||
url.pathname === '/logout' ||
url.pathname === '/') {
return;
}
// Cache-first for static assets and fonts
if (url.pathname.startsWith('/static/') ||
url.hostname === 'fonts.googleapis.com' ||
url.hostname === 'fonts.gstatic.com') {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request).then((response) => {
if (response.ok) {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
}
return response;
});
})
);
}
});