트렌드 리포트 PWA — 4주 MVP 구현 계획
홈 화면 아이콘 1개로 들어가는 전용 앱. Next.js 15 + Postgres + Vercel + Web Push로 4주 안에 첫 배포. 본인이 직접 만들 경우의 주차별 가이드와 외주 의뢰용 RFP 둘 다로 사용할 수 있게 정리했습니다.
한 줄 결론. 가능합니다. 4주 안에 홈 화면 아이콘 → 푸시 → 검수 → 발행 → 월간/분기 리포트까지 한 사이클이 도는 PWA가 손에 들어옵니다. 본인이 직접 코딩하면 4주 풀타임 / 외주는 400~800만원 선.
01 · 목표와 비목표
02 · 화면 구조 (7개)
03 · 기술 스택
04 · 데이터 모델
05 · 디자인 시스템 이식
06 · 4주 주차별 계획
07 · 폴더 / 코드 골격
08 · 푸시 알림 / Cron
09 · 배포 / 도메인 / 보안
10 · 비용 · 일정 · 리스크
11 · 외주 RFP 템플릿
12 · 4주 후 완성 체크리스트
01
Goals & Non-Goals
목표와 비목표
Goals
- 홈 화면 아이콘 1개. 탭 → 즉시 콘텐츠. 로그인은 1회만.
- 매일 / 매월 1일 / 분기 첫날 자동 푸시. iOS·Android 모두 동작.
- 본인이 모바일에서 5~7건 검수 + 한 줄 코멘트 후 발행 가능.
- 아카이브 검색·카테고리 필터·북마크 기본 제공.
- SQNC 디자인 시스템(현재 올리브 톤) 그대로 이식 — 별도 디자인 작업 최소화.
Non-Goals (1단계 제외)
- 다중 사용자 / 회원가입 / 권한 분리 — 1인용으로만.
- iOS·Android 네이티브 스토어 등록 — PWA로 충분.
- 실시간 채팅·코멘트·커뮤니티 — 운영 부담 큼.
- 결제·구독·외부 공유 페이지 — 회사 자산화 단계에서.
02
Screens
화면 구조 (7개)
01
/ — 홈 (오늘의 큐레이션)
상단: 오늘 날짜 + 카테고리 필터 칩. 본문: 발행된 카드 5~10개. 카드 = 카테고리·제목·1줄 코멘트·원문 링크·북마크.
02
/inbox — 후보 검수
자동 수집된 candidate 카드 30~50개. 좌측 스와이프 = 발행, 우측 스와이프 = archive. 한 줄 코멘트 인풋.
03
/reports/[slug] — 월간 / 분기 리포트
긴 리포트 형태. 현재 SQNC 트렌드 리포트(과거·현재·미래)와 동일 골격. 자동 생성 초안 → 사용자 승인 후 공개.
04
/archive — 검색·필터
전체 published 카드. 카테고리 / 날짜 / 키워드 검색. 본인의 트렌드 아카이브가 누적됨.
05
/bookmarks — 북마크
"다시 본다" 컬렉션. 폴더 1단(예: SQNC 회의용 / 글로벌 / F&B).
06
/settings — 알림·계정
푸시 시간(기본 8시), 카테고리 ON/OFF, 무음 시간, 무음 요일(주말).
07
/login — 매직 링크 로그인
이메일 입력 → 메일로 6자리 코드. 1인용이므로 화이트리스트 1개 이메일만 허용.
03
Tech Stack
기술 스택 — 최종 결정
| 레이어 | 선택 | 대안 | 이유 |
|---|---|---|---|
| 프레임워크 | Next.js 15 (App Router) | SvelteKit | PWA·서버 액션·이미지 최적화·생태계 |
| 언어 | TypeScript | JavaScript | 유지보수 / LLM 코드 품질 |
| 스타일 | 현재 디자인 시스템 CSS + Tailwind | shadcn/ui | 이미 있는 토큰 그대로 사용 |
| DB | Postgres (Neon 무료 티어) | Supabase / Turso | 무료 + Vercel 통합 매끄러움 |
| ORM | Drizzle | Prisma | 가볍고 빠름. 타입 자동. |
| 인증 | Auth.js (Email Magic Link) | Clerk | 무료. 1인용에 충분. |
| 스케줄러 | Vercel Cron | Apps Script / GitHub Actions | 코드 한 곳·과금 없음 |
| LLM | Claude Haiku 4.5 (분류) + Sonnet 4.6 (리포트) | — | Haiku로 비용 최소화 |
| 푸시 | Web Push API + web-push lib | OneSignal | 완전 자체 제어 / 무료 |
| 호스팅 | Vercel | Cloudflare Pages | Cron + Edge + Free tier |
| 도메인 | Cloudflare 또는 가비아 | — | $10~15/년 |
04
Data Model
데이터 모델 (Drizzle 스키마)
db/schema.ts
import { pgTable, serial, text, integer, timestamp, boolean, jsonb } from 'drizzle-orm/pg-core'; export const sources = pgTable('sources', { id: serial('id').primaryKey(), name: text('name').notNull(), url: text('url').notNull(), active: boolean('active').default(true), }); export const articles = pgTable('articles', { id: serial('id').primaryKey(), title: text('title').notNull(), url: text('url').notNull().unique(), sourceId: integer('source_id'), category: text('category'), // 리트릿 / F&B / 공간 / 문화·체험 / 기타 score: integer('score'), // 1-10 summary: text('summary'), comment: text('comment'), // 사용자 한 줄 코멘트 status: text('status').default('candidate'), // candidate / published / archived fetchedAt: timestamp('fetched_at').defaultNow(), publishedAt: timestamp('published_at'), bookmarked: boolean('bookmarked').default(false), bookmarkFolder: text('bookmark_folder'), }); export const reports = pgTable('reports', { id: serial('id').primaryKey(), kind: text('kind').notNull(), // monthly / quarterly periodStart: timestamp('period_start'), periodEnd: timestamp('period_end'), draft: text('draft'), // LLM 초안 final: text('final'), // 사용자 승인본 status: text('status').default('draft'), publishedAt: timestamp('published_at'), }); export const subscriptions = pgTable('subscriptions', { id: serial('id').primaryKey(), endpoint: text('endpoint').notNull().unique(), keys: jsonb('keys').notNull(), // p256dh + auth createdAt: timestamp('created_at').defaultNow(), }); export const settings = pgTable('settings', { id: serial('id').primaryKey(), pushHour: integer('push_hour').default(8), pushOnWeekend: boolean('push_on_weekend').default(true), categories: jsonb('categories').default('["리트릿","F&B","공간","문화·체험"]'), scoreThreshold: integer('score_threshold').default(6), });
05
Design Transplant
디자인 시스템 이식 — 별도 디자인 작업 0
현재 /Users/midbar_/Desktop/Claude/디자인가이드라인/style.css의 토큰을 그대로 가져옵니다. 새로 디자인하지 않아도 SQNC 톤으로 통일된 PWA가 나옵니다.
컬러 토큰
--bg-primary·--accent·--text-* 그대로 globals.css에 복사타이포
Pretendard Variable + JetBrains Mono.
next/font로 셀프 호스팅컴포넌트
.callout·.stat·.feature-grid·.kv·.tag 그대로 React 컴포넌트로 래핑다크 모드
기존
theme.js 로직을 React 훅으로 포팅. localStorage 키 동일 유지PWA 매니페스트
올리브 액센트 컬러 + 그레인 페이퍼 배경의 아이콘 1세트만 새로 디자인 (Figma 30분)
06
4-Week Plan
주차별 실행 계획
Week 1 — 골격 + 데이터
D1
Next.js 15 프로젝트 생성 + Vercel 연결
create-next-app → GitHub 푸시 → Vercel 자동 배포. 이때 빈 페이지가 https로 떠야 함.D2
Neon Postgres 연결 + Drizzle 스키마 마이그레이션
위 schema.ts 그대로 사용.
drizzle-kit push로 테이블 생성.D3
디자인 시스템 이식 (globals.css + 5개 컴포넌트)
Card / Callout / StatGrid / TagChip / EyebrowLine을 첫 5개로.
D4
Auth.js 매직 링크 + 화이트리스트 1개
본인 이메일만 로그인 가능.
signIn callback에서 이메일 비교.D5
RSS 수집 API 라우트 + Vercel Cron 등록
/api/cron/fetch 매일 06:00 KST. Stage 1의 Apps Script 코드를 TS로 포팅.Week 2 — 화면 / 검수 흐름
D6-7
/ — 홈 화면 (오늘의 큐레이션)
서버 컴포넌트로 published 카드 fetch. 카테고리 필터 칩(클라이언트). 무한 스크롤.
D8-9
/inbox — 후보 검수 화면
스와이프 제스처 (Framer Motion). 좌 스와이프 = published, 우 = archived. 한 줄 코멘트 인풋.
D10
/archive + 검색 + 필터
Postgres FTS(
tsvector) 또는 ILIKE 단순 매치. 카테고리·날짜 필터.Week 3 — 푸시 + 리포트
D11-12
Web Push 셋업 (서비스 워커 + VAPID 키)
Service Worker 등록 → 구독 토큰을 subscriptions 테이블에 저장 → web-push로 발송.
D13
/api/cron/digest — 매일 8시 푸시
Score ≥ threshold 7건 묶어 한 푸시. 탭 → /로 진입.
D14-15
월간 리포트 자동 초안 + /reports/[slug]
매월 1일 09시. Sonnet 4.6에 한 달치 published 데이터 + 프롬프트 → reports.draft 저장. 사용자가 모바일에서 검토 → final 작성 → published.
Week 4 — 마감 + 폴리시
D16-17
/bookmarks + /settings + 다크모드 토글
북마크 폴더 1단. 푸시 시간 슬라이더. 무음 요일.
D18
PWA 매니페스트 + 아이콘 + 오프라인 폴백
manifest.json + 192·512 아이콘 + offline.html. iOS Add to Home Screen 검증.D19
분기 리포트 트리거 + 1일치 더미 데이터로 풀 리허설
new Date().getMonth() % 3 === 0 체크. Stage 1에서 모은 데이터가 있으면 시드.D20
실제 핸드폰에서 추가·푸시 수신·읽기 풀 테스트
iOS Safari · Android Chrome 모두에서. 푸시 수신 시간·탭 동작·오프라인 캐시 확인.
07
Folder Layout
폴더 구조
trend-pwa/ — folder layout
app/ layout.tsx # 글로벌 폰트 + 디자인 시스템 CSS page.tsx # / 홈 inbox/page.tsx # /inbox archive/page.tsx # /archive bookmarks/page.tsx # /bookmarks reports/[slug]/page.tsx # /reports/2026-05-monthly settings/page.tsx # /settings login/page.tsx api/ cron/ fetch/route.ts # 매일 06:00 RSS 수집 digest/route.ts # 매일 08:00 푸시 monthly/route.ts # 매월 1일 09:00 push/ subscribe/route.ts send/route.ts articles/[id]/route.ts # PATCH (status / comment / bookmark) db/ schema.ts index.ts # drizzle client lib/ rss.ts # RSS 파싱 claude.ts # Anthropic API push.ts # web-push wrapper components/ Card.tsx Callout.tsx StatGrid.tsx EyebrowLine.tsx TagChip.tsx CategoryFilter.tsx public/ manifest.json icon-192.png icon-512.png sw.js # service worker styles/ globals.css # 디자인가이드라인 토큰 이식 .env.local vercel.json # cron schedules
Vercel Cron 설정
vercel.json
{
"crons": [
{ "path": "/api/cron/fetch", "schedule": "0 21 * * *" },
{ "path": "/api/cron/digest", "schedule": "0 23 * * *" },
{ "path": "/api/cron/monthly", "schedule": "0 0 1 * *" }
]
}
※ Vercel Cron은 UTC 기준. 위 값은 KST 06·08·09로 환산 (UTC = KST − 9).
08
Push & Cron
푸시 알림 구현 핵심
Service Worker 등록
public/sw.js
self.addEventListener('push', (e) => { const data = e.data?.json() || {}; e.waitUntil(self.registration.showNotification(data.title || '트렌드', { body: data.body, icon: '/icon-192.png', badge: '/icon-192.png', data: { url: data.url || '/' }, })); }); self.addEventListener('notificationclick', (e) => { e.notification.close(); e.waitUntil(clients.openWindow(e.notification.data?.url || '/')); });
구독 등록 (클라이언트)
app/components/EnablePush.tsx
const reg = await navigator.serviceWorker.register('/sw.js'); const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: VAPID_PUBLIC_KEY, }); await fetch('/api/push/subscribe', { method: 'POST', body: JSON.stringify(sub), });
iOS 16.4+ 제약. iOS는 16.4 이상에서만 PWA 푸시가 동작하고, 반드시 홈 화면에 추가된 PWA에서만 가능합니다. 사파리 탭에서 직접 푸시 권한 요청은 거절됩니다. 사용자에게 "홈 화면 추가 → 그 안에서 알림 켜기" 안내가 필수.
09
Deploy
배포 / 도메인 / 보안
호스팅
Vercel Free → Pro($20/월)로 이전 권장 (Cron 한도 / 서버 리전 확장)
도메인
예:
trend.sqnc.global 서브도메인 또는 별도 tr.midbar.kr. CNAME만 추가환경 변수
DATABASE_URL · ANTHROPIC_API_KEY · VAPID_PUBLIC_KEY · VAPID_PRIVATE_KEY · AUTH_SECRET · ALLOWED_EMAIL
접근 제어
middleware.ts에서 비로그인 시 /login으로 리다이렉트. 로그인 이메일 화이트리스트 1개
백업
Neon은 자동 백업(7일). 추가로 매주 articles JSON dump를 R2/S3에 업로드 권장
10
Budget · Risks
비용 · 일정 · 리스크
4주
MVP 완성
$25
월 운영비 (Vercel + LLM)
$0
DB · Auth · Push (무료 티어)
600만원
외주 시 중간값
일정·리스크
① iOS PWA 푸시 함정. "홈 화면 추가 안 한 상태"에서는 푸시 권한이 요청되지 않습니다. Week 4 D20 테스트에서 이걸 가장 먼저 확인. 안내 화면(/welcome) 한 장이 꼭 필요.
② Vercel Cron 무료 한도. 무료 플랜은 일부 제약(invocations, region). MVP는 무료로 충분하지만 Pro($20/월)로 1주차에 올리는 게 디버깅·로그 면에서 편함.
③ RSS 신뢰성 + Claude 출력 깨짐. Stage 1에서 안 맞춰진 출처는 PWA에서도 깨집니다. Week 1 D5에 RSS 정규화 로직(JSON Feed/RSS/Atom 모두 처리)을 단단히 하고, Claude 출력은 항상 try/catch + 1회 재시도.
11
Outsourcing RFP
외주 의뢰 시 — RFP 1장 템플릿
RFP — Trend PWA
# 트렌드 리포트 PWA 개발 의뢰 (1인용 MVP)
[프로젝트 개요]
- 개인용 트렌드 리포트 PWA. 홈화면 아이콘 1개로 진입.
- 매일 / 매월 1일 / 분기 첫날 자동 푸시.
- 사용자 1명 (의뢰인 본인). 화이트리스트 매직 링크 로그인.
[필수 기능]
1. RSS 자동 수집(매일 새벽) + Claude API 분류·요약·점수
2. 후보 검수(스와이프 발행/폐기, 한 줄 코멘트)
3. 매일 8시 Web Push 다이제스트 (iOS 16.4+ / Android 모두)
4. 매월 1일 / 분기 첫날 LLM 자동 초안 → 사용자 검수 → 발행
5. 검색·카테고리 필터·북마크 폴더
6. 다크 모드 + 의뢰인 디자인 시스템 그대로 이식
[기술 스택 지정]
- Next.js 15 + TypeScript + Tailwind + Drizzle + Neon Postgres
- Auth.js (Email Magic Link), Vercel + Vercel Cron, web-push,
- Anthropic API (Haiku 4.5 + Sonnet 4.6)
[제공 자산]
- 디자인 시스템 CSS 토큰 일체 (의뢰인 보유)
- RSS 소스 리스트 10개
- 화면 와이어프레임 (이 문서의 02 섹션)
[기간·예산]
- 4주 MVP, 600~800만원 (논의 가능)
- 추가 변경 시 시간당 X원
[산출물]
- GitHub repo (private) + 배포된 URL + 환경변수 인계 + README
- 1주 무상 버그픽스
12
Done = ?
4주 후 완성 체크리스트
✓
홈 화면에 SQNC 트렌드 아이콘 1개
탭하면 1초 안에 오늘의 큐레이션이 뜬다.
✓
매일 08:00 푸시 도착
탭하면 / 홈으로 진입. 7건 카드.
✓
/inbox에서 모바일 검수 가능
스와이프로 발행/폐기. 한 줄 코멘트.
✓
매월 1일 09:00 월간 리포트 알림
탭 → 초안 검토 → 발행. 영구 아카이브.
✓
분기 첫날 09:30 분기 리포트 알림
"과거·현재·미래" 골격 자동 적용.
✓
아카이브 검색 / 북마크 / 다크 모드
핸드폰만으로 운영 가능.
다음 단계 제안
본인이 직접 만드신다면 Week 1 D1 시작 가이드(Next.js 생성·Vercel 연결·Neon DB 만들기 손 잡고 가는 30분짜리 체크리스트)를 만들어 드릴 수 있습니다. 외주로 가신다면 위 RFP를 그대로 보내시면 되고, 후보 프리랜서 추천 기준이나 견적 검토용 체크리스트가 필요하면 따로 만들어 드릴게요.