Spec

트렌드 리포트 PWA — 4주 MVP 구현 계획

홈 화면 아이콘 1개로 들어가는 전용 앱. Next.js 15 + Postgres + Vercel + Web Push로 4주 안에 첫 배포. 본인이 직접 만들 경우의 주차별 가이드와 외주 의뢰용 RFP 둘 다로 사용할 수 있게 정리했습니다.

2026-05-04 · Spec · 약 14분 · PWA 4주 계획서
한 줄 결론. 가능합니다. 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

Non-Goals (1단계 제외)

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)SvelteKitPWA·서버 액션·이미지 최적화·생태계
언어TypeScriptJavaScript유지보수 / LLM 코드 품질
스타일현재 디자인 시스템 CSS + Tailwindshadcn/ui이미 있는 토큰 그대로 사용
DBPostgres (Neon 무료 티어)Supabase / Turso무료 + Vercel 통합 매끄러움
ORMDrizzlePrisma가볍고 빠름. 타입 자동.
인증Auth.js (Email Magic Link)Clerk무료. 1인용에 충분.
스케줄러Vercel CronApps Script / GitHub Actions코드 한 곳·과금 없음
LLMClaude Haiku 4.5 (분류) + Sonnet 4.6 (리포트)Haiku로 비용 최소화
푸시Web Push API + web-push libOneSignal완전 자체 제어 / 무료
호스팅VercelCloudflare PagesCron + 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를 그대로 보내시면 되고, 후보 프리랜서 추천 기준이나 견적 검토용 체크리스트가 필요하면 따로 만들어 드릴게요.