기술 스택
📱 Frontend
Next.js 15 (App Router)
TypeScript 5.3 · React 19 · Tailwind CSS 4 · Pretendard Variable
🔌 Backend
Vercel Serverless Functions
Edge Runtime 우선 · 한국 region: icn1 (서울)
🗄 Database
Supabase Postgres 15
Row Level Security · pg_cron · pgvector (Phase 3)
🔐 Auth
Supabase Auth
카카오·구글·애플 OAuth · JWT
📦 Storage
Supabase Storage
이미지 · 24h/90일 TTL · 1MB 압축
🤖 AI/LLM
Anthropic Claude SDK
Sonnet 4.7 (추천) · Haiku 4.5 (배치) · Vision (OCR)
⚡ Cache
Upstash Redis
recommendations 24h · ocr 영구 · session
📊 Analytics
Vercel Analytics + PostHog
코어 웹 바이탈 + 이벤트 추적
🚨 Monitoring
Sentry
에러 트래킹 · 성능 모니터링 · 알람
🔄 CI/CD
GitHub Actions + Vercel
PR preview · auto deploy · cron jobs
🧪 Testing
Vitest + Playwright
Unit · Integration · E2E + Visual regression
📦 Package
pnpm
Workspace 분리 (web · api · shared)
디렉토리 구조
mealfred/ ├── apps/ │ ├── web/ # Next.js 15 App Router │ │ ├── app/ │ │ │ ├── (auth)/ # 로그인·OAuth 콜백 │ │ │ ├── (app)/ # 인증 후 — 홈·도감·기록·레시피 │ │ │ │ ├── home/ │ │ │ │ ├── dex/ # 도감 │ │ │ │ ├── record/ # 식단 기록 │ │ │ │ └── recipes/ │ │ │ ├── eval/ # daycare-eval 진입점 │ │ │ ├── api/ # API Routes │ │ │ │ ├── eval/ │ │ │ │ ├── recommend/ │ │ │ │ ├── ocr/ │ │ │ │ └── webhook/ │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── components/ │ │ │ ├── ui/ # Button·Card·Modal 등 기본 │ │ │ ├── eval/ # SignalLight·GaugeBar 등 │ │ │ ├── recipe/ # RecipeCard·MealPlan │ │ │ └── record/ │ │ ├── lib/ │ │ │ ├── supabase/ # client·server·types │ │ │ ├── kdri/ # KDRI lookup·percentile │ │ │ └── utils/ │ │ ├── hooks/ # useChild·useMealLogs 등 │ │ └── styles/ │ └── admin/ # 운영자 대시보드 (Phase 2) ├── packages/ │ ├── shared/ # 공통 타입·상수 │ │ ├── types/ │ │ ├── kdri-rni.ts # KDRI 36 영양소 상수 │ │ └── ingredients-pool.ts # 147 식재료 풀 │ ├── eval-engine/ # 평가 엔진 (Pure functions) │ │ ├── axes/ │ │ ├── signal.ts │ │ └── bmi.ts │ ├── reco-engine/ # 추천 엔진 │ │ ├── phase0-profile.ts │ │ ├── phase1-rules.ts │ │ ├── phase2-llm.ts │ │ └── cache.ts │ └── ocr-engine/ # OCR 엔진 ├── supabase/ │ ├── migrations/ # SQL 마이그레이션 │ ├── functions/ # Edge Functions │ └── seed.sql # 초기 데이터 (KDRI·ingredients) ├── scripts/ │ ├── enrich-recipes.ts # 4,432 레시피 메타 enrich 배치 │ └── extract-ingredients.py # 식재료 추출 ├── .github/workflows/ # CI/CD ├── pnpm-workspace.yaml ├── package.json └── README.md
코딩 컨벤션
3-1. Naming
- 파일: kebab-case (
meal-log-card.tsx), 컴포넌트는 PascalCase (MealLogCard.tsx) 선택적 - 함수: camelCase 동사 시작 (
calculateBMI,fetchMealLogs) - 훅:
use접두사 (useChild,useEvalScore) - 상수: SCREAMING_SNAKE_CASE (
KDRI_36_NUTRIENTS,MAX_LOGS_PER_DAY) - 타입: PascalCase + 명사 (
MealLog,NutrientStatus) - enum: PascalCase (
MealType,TextureLevel) - DB 컬럼: snake_case (
child_id,eaten_at) — Supabase 관례
3-2. Comment 정책
기본: 주석 X. 잘 작성된 코드는 self-explanatory. 주석은 다음에만:
- 왜 (Why) 했는지가 비명확할 때 (How·What X)
- 외부 제약·버그 우회 (// Safari 17 bug — needs explicit type)
- 학술 출처 인용 (// HabEat 2014, Table 3)
- JSDoc은 공개 API 함수만
3-3. 파일 구성 패턴
// ✅ 좋은 예 — 도메인별 응집 export function calculateNutrientSignal(logs: MealLog[], ageBand: AgeBand) { return KDRI_36_NUTRIENTS.map(n => classifyFrequency(logs, n, ageBand)) } function classifyFrequency(logs: MealLog[], nutrient: Nutrient, band: AgeBand): NutrientSignal { // 빈도 → 색상 분류 const meals = countContributingMeals(logs, nutrient) const pct = (meals / WEEKLY_TARGET) * 100 return { nutrient, pct, status: pct >= 80 ? 'green' : pct >= 50 ? 'orange' : 'red' } }
API 엔드포인트 명세
4-1. 인증
POST/api/auth/oauth/[provider]
OAuth 콜백 (kakao·google·apple) → JWT 발급
POST/api/auth/signout
로그아웃 + 세션 만료
4-2. 아이 프로필
POST/api/children
아이 프로필 생성 (별명·생년월일·성별·알레르겐·돌봄)
GET/api/children/:id
프로필 조회
POST/api/children/:id/growth
키·몸무게 기록 → BMI percentile 자동 계산
4-3. 식단 기록
POST/api/meal-logs
끼니 기록 생성 (date·meal_type·photo·ingredients·texture·reaction)
GET/api/meal-logs?from=&to=
기록 조회 (날짜 범위)
PUT/api/meal-logs/:id
기록 수정 (소급 입력 지원)
4-4. 평가
GET/api/eval/score?child_id=&days=3
8축 진단 + 종합 점수 (ALG-EVAL-01)
GET/api/eval/signal?child_id=&days=7
36 영양소 신호등 (ALG-EVAL-02)
GET/api/eval/bmi?child_id=
BMI percentile + 탄·단·지 평가 (ALG-EVAL-03~04)
4-5. 추천
POST/api/recommend/recipes
4원칙 레시피 6-8개 (ALG-RECO-01~06) + cache hit/miss 헤더
POST/api/recommend/3day-plan
AI 3일 식단 (12끼) 생성
4-6. OCR
POST/api/ocr/menu
어린이집·유치원·가정 식단표 한 달 → 8축 평가 (ALG-OCR-01~05)
POST/api/ocr/food
음식 사진 → 식재료 자동 추출 (ALG-OCR-06)
4-7. 응답 표준 형식
{
"ok": true,
"data": { ... },
"meta": {
"cached": true,
"version": "1.0.0",
"latency_ms": 142
}
}
// 에러 시
{
"ok": false,
"error": { "code": "VALIDATION_ERROR", "message": "...", "field": "eaten_at" }
}
인증 흐름
// 1) OAuth 진입 const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'kakao', options: { redirectTo: '/auth/callback' } }) // 2) 콜백 — 세션 교환 // app/auth/callback/route.ts export async function GET(req: Request) { const code = new URL(req.url).searchParams.get('code') await supabase.auth.exchangeCodeForSession(code!) return Response.redirect('/home') } // 3) API 보호 — middleware export async function middleware(req: NextRequest) { const session = await getSession(req) if (!session) return NextResponse.redirect('/login') } // 4) RLS 자동 적용 (Supabase가 auth.uid()를 컨텍스트로) // children 테이블의 RLS 정책이 user_id = auth.uid() 자동 필터링
데이터 페칭 + 상태
- 서버 상태:
@tanstack/react-query v5— 캐싱·revalidation·optimistic updates - 클라이언트 상태:
zustand(전역 UI 상태) +useState(지역) - 폼:
react-hook-form+zod검증 - URL 상태: Next.js
searchParams(필터·날짜 등)
// 예: useChild hook export function useChild(childId: string) { return useQuery({ queryKey: ['child', childId], queryFn: () => fetchChild(childId), staleTime: 5 * 60 * 1000, // 5분 }) }
테스트 전략
| 레이어 | 도구 | 커버리지 목표 | 대상 |
|---|---|---|---|
| Unit | Vitest | ≥ 80% | eval-engine, reco-engine 순수 함수, utils |
| Integration | Vitest + Supabase 로컬 | ≥ 60% | API 라우트 + DB 통합 (RLS 정책 포함) |
| E2E | Playwright | 핵심 흐름 100% | 온보딩·식단기록·평가받기·레시피추천 |
| Visual | Playwright screenshots | 핵심 화면 | 홈·신호등 모달·결과 카드 |
| Load | k6 (PR 시 sample) | p95 < 500ms | /api/recommend, /api/ocr |
GitHub Actions + Vercel
# .github/workflows/ci.yml on: [pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v3 - run: pnpm install - run: pnpm typecheck - run: pnpm test:unit - run: pnpm test:integration - run: pnpm test:e2e - run: pnpm build - uses: codecov/codecov-action@v4 # Vercel PR Preview 자동 (브랜치 push 시) # main 머지 시 → 프로덕션 자동 배포 (icn1 region) # 배치 작업 cron # .github/workflows/cron-enrich.yml — 매일 03:00 KST # - 신규 식재료 운영 큐 검토 알림 # - 캐시 정리 # - LLM 사용량 리포트
환경변수 관리
# .env.local (개발) NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... SUPABASE_SERVICE_ROLE_KEY=eyJ... # server only ANTHROPIC_API_KEY=sk-ant-... UPSTASH_REDIS_REST_URL=... UPSTASH_REDIS_REST_TOKEN=... SENTRY_DSN=https://... NEXT_PUBLIC_POSTHOG_KEY=phc_... # 환경별 분리 .env.development → 개발 .env.preview → Vercel PR preview .env.production → 프로덕션 # Vercel에서 관리: # - SUPABASE_SERVICE_ROLE_KEY, ANTHROPIC_API_KEY 등 secret # - dev/preview/production 환경별 다른 값 가능
에러 처리 + 로깅
- 전역 에러 바운더리:
app/error.tsx+ Sentry 자동 캡쳐 - API 에러: 표준 코드 (VALIDATION_ERROR·UNAUTHORIZED·RATE_LIMIT·LLM_ERROR·OCR_FAILED)
- 재시도: LLM/OCR 호출은 exponential backoff (1s, 2s, 4s, max 3회)
- fallback: LLM 실패 시 Phase 1 룰 결과만 반환 (자연어 이유 제외)
- 사용자 메시지: 기술 용어 X → "이런, 한 번 더 시도해볼까요?"
성능 최적화
- 이미지: Next/Image + AVIF/WebP · lazy loading · responsive sizes
- 코드 분할: route-based + dynamic import (모달·차트)
- SSR/SSG: 정적 페이지(blog·proposal) SSG, 앱은 SSR + Streaming
- 캐싱: API 응답
Cache-Control+ Redis hot data + React Query - LLM 비용: Anthropic Prompt Caching (system prompt) → -50%
- DB 인덱스: 핵심 쿼리에 covering index (architecture-detailed §1-3)
- 모니터링 지표: LCP < 2.5s · FID < 100ms · CLS < 0.1
보안
⚠️ 영유아 데이터 — 특별 주의
아이 별명만, 실명 X. 생년월일 월·년만. 어린이집 식별 정보 미수집. 사진 24h/90일 자동 삭제.
- RLS: 모든 사용자 데이터 테이블에 적용 — 자기 데이터만 접근
- CSP:
Content-Security-Policy헤더 — 외부 스크립트 화이트리스트 - CORS: API 라우트는 same-origin only (앱 도메인에서만)
- Rate Limiting: Upstash Ratelimit — IP당 60 req/min
- SQL Injection: Supabase 클라이언트 (parameterized)
- XSS: React 자동 escape + dangerouslySetInnerHTML 금지
- 비밀: service_role_key는 서버 only, 클라이언트 노출 절대 X
- OAuth: PKCE flow · state 검증
- 이미지 검증: 업로드 시 magic byte 체크 + 크기 제한 10MB
- LLM 입력: prompt injection 방지 — user input은 항상 user role로 전달
모니터링 + 알람
13-1. 추적 지표
| 카테고리 | 지표 | 도구 | 알람 임계치 |
|---|---|---|---|
| 제품 | DAU/MAU, retention, conversion | PostHog | 전주 대비 -20% |
| 성능 | API p95, LCP, FCP, CLS | Vercel Analytics | p95 > 500ms |
| 에러 | Error rate, exception types | Sentry | 1% 이상 |
| 비용 | LLM 일일 호출/비용, OCR 호출 | Anthropic Console | 일일 한도 80% |
| 품질 | OCR 정확도, 추천 클릭률 | PostHog + custom | OCR < 85% |
13-2. 알람 채널
- Slack #alerts: 모든 에러 · 비용 임계 · 성능 저하
- 이메일 (긴급): 서비스 다운 · 보안 이슈
- 대시보드: Grafana (월 단위 + 실시간)
13-3. 운영자 대시보드 (Phase 2)
- 신규 식재료 검수 큐 (영양사 일 1회)
- OCR 실패 케이스 검토
- 사용자 피드백 모니터링
- 비용·사용량 트렌드