백로그
0할 일
0진행 중
0리뷰
0완료 (전체)
2PWA Core - Manifest + Service Worker + 오프라인 + 레이아웃 + Install Prompt
## 목표 PWA 설치 가능(A2HS), Service Worker 캐싱, 오프라인 페이지 구현 ## 구현 항목 ### 1. Manifest 커스터마이징 (app/views/pwa/manifest.json.erb) 기존 파일 수정: ```json { "name": "LogBible - 묵상 기록", "short_name": "LogBible", "icons": [ { "src": "/icon.png", "type": "image/png", "sizes": "512x512" }, { "src": "/icon.png", "type": "image/png", "sizes": "512x512", "purpose": "maskable" }, { "src": "/icon-192x192.png", "type": "image/png", "sizes": "192x192" }, { "src": "/icon-192x192.png", "type": "image/png", "sizes": "192x192", "purpose": "maskable" } ], "start_url": "/", "display": "standalone", "scope": "/", "description": "크리스천 통독 묵상 기록 플랫폼", "theme_color": "#6366f1", "background_color": "#f8fafc", "orientation": "portrait", "categories": ["lifestyle", "education"], "lang": "ko" } ``` - theme_color: brand-primary (#6366f1 indigo-500 계열, 기존 디자인 시스템 확인) - 192x192 아이콘 생성 (icon.png에서 리사이즈 - ImageMagick 또는 간단한 복사) ### 2. 아이콘 생성 - public/icon-192x192.png: `convert public/icon.png -resize 192x192 public/icon-192x192.png` (ImageMagick) - ImageMagick 없으면 icon.png을 복사하여 icon-192x192.png으로 (브라우저가 리사이즈) ### 3. Service Worker (app/views/pwa/service-worker.js) 기존 주석 파일을 교체: ```javascript // LogBible Service Worker - Cache Strategy const CACHE_VERSION = "v1"; const CACHE_NAME = `logbible-${CACHE_VERSION}`; const OFFLINE_URL = "/offline"; // 캐시할 정적 리소스 const PRECACHE_URLS = [ "/offline", "/icon.png", "/icon-192x192.png" ]; // Install - 정적 리소스 프리캐시 self.addEventListener("install", (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll(PRECACHE_URLS); }) ); self.skipWaiting(); }); // Activate - 이전 캐시 정리 self.addEventListener("activate", (event) => { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames .filter((name) => name !== CACHE_NAME) .map((name) => caches.delete(name)) ); }) ); self.clients.claim(); }); // Fetch - Network First (HTML), Cache First (정적) self.addEventListener("fetch", (event) => { const { request } = event; const url = new URL(request.url); // 같은 origin만 처리 if (url.origin !== location.origin) return; // API/POST 요청은 패스 if (request.method !== "GET") return; // HTML 요청: Network First + 오프라인 폴백 if (request.headers.get("Accept")?.includes("text/html")) { event.respondWith( fetch(request) .catch(() => caches.match(OFFLINE_URL)) ); return; } // 정적 리소스 (JS/CSS/이미지): Cache First if (url.pathname.match(/\.(js|css|png|jpg|svg|ico|woff2?)$/)) { event.respondWith( caches.match(request).then((cached) => { return cached || fetch(request).then((response) => { if (response.ok) { const clone = response.clone(); caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)); } return response; }); }) ); return; } }); // Push Notification 처리 (향후 알림 시스템용) self.addEventListener("push", (event) => { const data = event.data ? event.data.json() : {}; const title = data.title || "LogBible"; const options = { body: data.body || "새 알림이 있습니다.", icon: data.icon || "/icon-192x192.png", badge: "/icon-192x192.png", data: { url: data.url || "/" } }; event.waitUntil(self.registration.showNotification(title, options)); }); self.addEventListener("notificationclick", (event) => { event.notification.close(); const url = event.notification.data?.url || "/"; event.waitUntil( clients.matchAll({ type: "window" }).then((clientList) => { for (const client of clientList) { if (new URL(client.url).pathname === url && "focus" in client) { return client.focus(); } } return clients.openWindow(url); }) ); }); ``` ### 4. 오프라인 페이지 - app/controllers/pwa_controller.rb 생성: ```ruby class PwaController < ApplicationController skip_before_action :authenticate_user!, raise: false layout false def offline render "pwa/offline" end end ``` - app/views/pwa/offline.html.erb 생성: - 심플한 오프라인 안내 페이지 (standalone HTML, Tailwind 인라인) - "인터넷 연결이 없습니다. 연결 후 다시 시도해주세요." 메시지 - 새로고침 버튼 ### 5. 라우트 (config/routes.rb) 기존 파일에 추가: ```ruby # PWA get "manifest" => "rails/pwa#manifest", as: :pwa_manifest get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker get "offline" => "pwa#offline" ``` - Rails 8에서 제공하는 `rails/pwa` 컨트롤러 활용 - get "up" 라우트 근처에 배치 ### 6. 레이아웃 메타 태그 (app/views/layouts/application.html.erb) ```erb <meta name="theme-color" content="#6366f1"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <link rel="manifest" href="/manifest.json"> ``` - 기존 주석 처리된 manifest 링크를 활성화 - theme-color 메타 태그 추가 - apple-mobile-web-app-status-bar-style 추가 - devise.html.erb에도 동일 적용 ### 7. PWA Install Prompt (Stimulus 컨트롤러) - app/javascript/controllers/pwa_install_controller.js 생성: ```javascript import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["banner"] connect() { this.deferredPrompt = null; window.addEventListener("beforeinstallprompt", this.handleInstallPrompt.bind(this)); window.addEventListener("appinstalled", this.handleInstalled.bind(this)); } handleInstallPrompt(event) { event.preventDefault(); this.deferredPrompt = event; if (this.hasBannerTarget) { this.bannerTarget.classList.remove("hidden"); } } async install() { if (!this.deferredPrompt) return; this.deferredPrompt.prompt(); const { outcome } = await this.deferredPrompt.userChoice; this.deferredPrompt = null; if (this.hasBannerTarget) { this.bannerTarget.classList.add("hidden"); } } dismiss() { if (this.hasBannerTarget) { this.bannerTarget.classList.add("hidden"); } } handleInstalled() { this.deferredPrompt = null; if (this.hasBannerTarget) { this.bannerTarget.classList.add("hidden"); } } disconnect() { window.removeEventListener("beforeinstallprompt", this.handleInstallPrompt); window.removeEventListener("appinstalled", this.handleInstalled); } } ``` - application.html.erb body에 install banner 추가: ```erb <div data-controller="pwa-install" class="fixed bottom-20 md:bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-80 z-50"> <div data-pwa-install-target="banner" class="hidden bg-brand-primary text-white rounded-lg shadow-lg p-4"> <div class="flex items-center justify-between"> <div> <p class="font-medium text-small">LogBible 앱 설치</p> <p class="text-caption opacity-80">홈 화면에 추가하세요</p> </div> <div class="flex gap-2"> <button data-action="pwa-install#install" class="bg-white text-brand-primary px-3 py-1 rounded text-small font-medium">설치</button> <button data-action="pwa-install#dismiss" class="text-white/80 hover:text-white text-small">닫기</button> </div> </div> </div> </div> ``` ### 8. 테스트 - test/controllers/pwa_controller_test.rb: - GET /offline 200 응답 - GET /manifest.json 200 응답 - GET /service-worker.js 200 응답 ## 기존 코드 참고 - app/views/pwa/manifest.json.erb 존재 (기본 템플릿) - app/views/pwa/service-worker.js 존재 (전부 주석) - application.html.erb 15줄: manifest 링크 주석 처리됨 - theme-color 등 PWA 메타 태그 누락 - Tailwind 디자인 시스템: brand-primary 사용 ## 주의사항 - 기존 테스트가 깨지지 않게 주의 - ERB 멀티라인 주석 사용 금지 - routes.rb 수정 시 기존 구조 유지 - skip_before_action으로 인증 우회 (offline, manifest, service-worker) - bin/rails test로 전체 테스트 통과 확인
Capacitor Setup - npm init + 설정 + Android/iOS 기본 구성
## 목표 Capacitor 프로젝트 초기화 및 Android/iOS 빌드 기반 구축 ## 구현 항목 ### 1. npm 프로젝트 초기화 ```bash cd /home/daniel/dev/logbile2.0.0 npm init -y ``` - package.json의 name을 "logbible"로 수정 - description, version 등 기본 정보 설정 ### 2. Capacitor 설치 ```bash npm install @capacitor/core @capacitor/cli npx cap init logbible co.kr.logbible --web-dir public ``` - web-dir: public (Rails의 정적 파일 디렉토리) ### 3. capacitor.config.ts 커스터마이징 ```typescript import type { CapacitorConfig } from '@capacitor/cli'; const config: CapacitorConfig = { appId: 'co.kr.logbible', appName: 'LogBible', webDir: 'public', server: { // 개발 중에는 Rails 서버로 프록시 url: process.env.CAPACITOR_SERVER_URL || undefined, cleartext: true }, plugins: { SplashScreen: { launchAutoHide: true, backgroundColor: '#6366f1', showSpinner: false, androidSplashResourceName: 'splash', splashFullScreen: false, splashImmersive: false, }, StatusBar: { style: 'DARK', backgroundColor: '#6366f1' } } }; export default config; ``` ### 4. Android 플랫폼 추가 ```bash npm install @capacitor/android npx cap add android ``` - android/ 디렉토리 생성됨 - 빌드 실패해도 설정 파일만 있으면 OK (Android SDK 없을 수 있음) ### 5. iOS 플랫폼 추가 (선택적) ```bash npm install @capacitor/ios npx cap add ios ``` - macOS가 아니면 실패할 수 있음 - 에러 무시 ### 6. Capacitor 플러그인 (기본) ```bash npm install @capacitor/splash-screen @capacitor/status-bar @capacitor/browser ``` ### 7. 빌드 스크립트 (package.json scripts) ```json { "scripts": { "cap:sync": "npx cap sync", "cap:open:android": "npx cap open android", "cap:open:ios": "npx cap open ios", "cap:build": "npx cap sync && echo 'Capacitor synced. Open Android Studio to build APK.'" } } ``` ### 8. .gitignore 업데이트 기존 .gitignore에 추가: ``` # Capacitor android/ ios/ node_modules/ ``` - android/, ios/ 디렉토리는 로컬에서 생성하므로 gitignore ### 9. 개발 가이드 (script/capacitor-setup.sh) ```bash #!/bin/bash # Capacitor 개발 환경 설정 스크립트 echo "=== LogBible Capacitor Setup ===" echo "" echo "1. npm install 실행..." npm install echo "" echo "2. Android 플랫폼 추가..." npx cap add android 2>/dev/null || echo "Android SDK가 필요합니다. https://developer.android.com/studio" echo "" echo "3. iOS 플랫폼 추가 (macOS only)..." npx cap add ios 2>/dev/null || echo "macOS + Xcode가 필요합니다." echo "" echo "=== 설정 완료 ===" echo "Android: npx cap open android (Android Studio 필요)" echo "iOS: npx cap open ios (Xcode 필요)" ``` ## 테스트 - `npm install`이 성공하는지 확인 - capacitor.config.ts 파일이 존재하는지 확인 - 기존 Rails 테스트에 영향 없는지 확인: bin/rails test ## 주의사항 - Rails 프로젝트 루트에서 npm 명령어 실행 - package.json은 이 에이전트만 관리 (충돌 방지) - node_modules/는 .gitignore에 추가 - android/, ios/도 .gitignore에 추가 (로컬 빌드 전용) - bin/rails test로 전체 테스트 통과 확인 (Capacitor가 Rails에 영향 없어야 함) - npx cap add 실패해도 OK (SDK 없을 수 있음) - config 파일만 있으면 성공