PWA Core - Manifest + Service Worker + 오프라인 + 레이아웃 + Install Prompt
ID: 0eef9d88-e13b-47b9-8ade-c9b2624ab698
## 목표
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
```
- 기존 주석 처리된 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
LogBible 앱 설치
홈 화면에 추가하세요
설치
닫기
```
### 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로 전체 테스트 통과 확인
첨부 이미지
이미지 추가 (Ctrl+V로 붙여넣기 또는 클릭)
JPEG, PNG, GIF, WebP / 최대 10MB
활동 로그
-
팀팀리드 상태 변경: 리뷰 → 완료
2026년 03월 02일 05:13:55
-
Ppwa-core-dev 상태 변경: 진행 중 → 리뷰
2026년 03월 02일 05:06:05
-
Ppwa-core-dev PWA Core 구현 완료
2026년 03월 02일 05:06:04
상세 보기
## 변경 사항
### 수정된 파일
- `app/views/pwa/manifest.json.erb` - LogBible manifest로 업데이트
- `app/views/pwa/service-worker.js` - Cache First + Network First 전략 구현
- `config/routes.rb` - PWA 라우트 3개 추가 (manifest, service-worker, offline)
- `app/views/layouts/application.html.erb` - theme-color 메타태그 + manifest 링크 + PWA Install Prompt 배너
- `app/views/layouts/devise.html.erb` - theme-color 메타태그 + manifest 링크### 새로 생성된 파일
- `app/controllers/pwa_controller.rb` - 오프라인 페이지 컨트롤러
- `app/views/pwa/offline.html.erb` - 오프라인 안내 페이지
- `app/javascript/controllers/pwa_install_controller.js` - Stimulus Install Prompt
- `public/icon-192x192.png` - 192x192 아이콘
- `test/controllers/pwa_controller_test.rb` - PWA 테스트 3개### 테스트 결과
- PWA 테스트 3개 모두 통과
- 기존 테스트 영향 없음 (기존 13개 Admin::QtThemesController 에러는 미구현 컨트롤러 관련) -
Ppwa-core-dev PWA Core 구현 시작
2026년 03월 02일 05:04:17
상세 보기
## 구현 항목
- Manifest 수정
- 아이콘 192x192 생성
- Service Worker 구현
- 오프라인 페이지 (컨트롤러 + 뷰)
- 라우트 추가
- 레이아웃 메타 태그 추가
- PWA Install Prompt (Stimulus)
- 테스트 작성 -
Ppwa-core-dev 티켓 클레임 완료
2026년 03월 02일 05:04:10