목적
cody recreation engine MVP 의 gated surface 2종 구현:
- (+)Saved Looks (
app/cody/saved/page.tsx) — 로그인 필수, 유저가 저장한 룩 세트 보드
- Shop-this-look (
app/cody/looks/[slug]/page.tsx) — 공개 읽기, 룩 생성자 gated 공유 slug
이 두 화면은 wireframes.md §2 (+)surface + mvp-scope.md §2 보조 surface 에서 정의된 피처이며, core 5화면(①②③④⑤) 을 완성한 후 도달 가능한 저장·공유 엔드 포인트다.
범위
화면
| # |
경로 (packages/web/) |
역할 |
로그인 |
주요 컴포넌트 |
| Saved |
app/cody/saved/page.tsx |
유저의 저장된 룩 세트 보드 (gated) |
필수 |
보드 그리드 · 빈 상태 안내 |
| Shop-this-look |
app/cody/looks/[slug]/page.tsx |
공개 공유 페이지 (SEO, Growth L2) |
선택(뷰) / 필수(생성) |
룩 요약 · "내 버전 만들기" CTA · 메타 정보 |
auth 경계
- Saved:
- 뷰: 로그인 필수 (비로그인 → gated modal)
- DB: user-scoped (자신이 저장한 룩만)
- Shop-this-look:
- 뷰: 공개 (비로그인 허용, SEO 인입)
- 생성/수정: 로그인 필수 (자신이 만든 룩만 공유 가능, 저작권 deferred)
데이터 모델
// User-scoped 저장 (cody_matches 와 별도)
type SavedLook = {
id: string; // PK
user_id: string; // user-scoped
recommended_look_id: string; // ④ Recommended 의 look set 참조
title: string; // "가을 출근룩"
notes?: string; // 유저 추가 메모
slug: string; // Shop-this-look URL 용 (nullable — 공유 전까진 private)
created_at: timestamp;
updated_at: timestamp;
};
// Public-readable 공유 페이지
type PublicLookShare = {
slug: string; // PK (SEO-friendly URL)
creator_user_id: string; // who created
look_summary: { // composition of ④ 룩 세트
style_mood: string[]; // ["모던", "가을"]
items: CandidateSKU[]; // from cody-match-stage2 §2.2
total_price_krw: number;
similarity_score: number; // "유사도 88%"
original_cody_image_url: string; // reference
};
created_at: timestamp;
public_url: string; // /cody/looks/[slug]
};
중요: cody_matches(§7.2) 는 공용 서비스 캐시이고, saved_looks 는 user-scoped 저장소 — 별도 테이블 필요. 저작권은 deferred(§5), 현재는 "자신이 만든 룩만 공유" 제약으로 skin-deep 처리.
화면별 AC
(+)Saved Looks · P6 저장
┌────────────────────────────┐
│ 내 스타일 보드 │
│ ┌──────┐ ┌──────┐ │
│ │ 가을 │ │ 데일리│ … │
│ │ 출근룩│ │ 캐주얼│ │
│ │ ▦▦▦ │ │ ▦▦▦ │ │
│ └──────┘ └──────┘ │
│ ── empty ── │
│ 저장한 룩이 없어요 │
│ [ 룩 추천받기 → ① ] │
└────────────────────────────┘
Shop-this-look · 공유 slug
┌────────────────────────────┐
│ 분해된 아이템 요약 (미니) │ ← 원본 룩 맥락
│ [모던 가을 / 총 17만] │
│ │
│ ◇ 검색 결과 4건 │
│ ▸ ────────────────── ▸ │
│ [룩1] [룩2] [룩3] [룩4] │
│ │
│ [ 💡 내 예산으로 만들기 → ①] │ ← decoded 추천 동선
│ [ 공유하기 ] │
└────────────────────────────┘
Acceptance Criteria
핵심 파일
| 대상 |
경로 (packages/web/) |
소유 |
| Saved 페이지 |
app/cody/saved/page.tsx |
F9 신규 |
| Shop-this-look 페이지 |
app/cody/looks/[slug]/page.tsx |
F9 신규 |
| Saved 보드 컴포넌트 |
lib/components/cody/SavedBoard.tsx |
F9 신규 |
| Shop-this-look 컴포넌트 |
lib/components/cody/PublicLookShare.tsx |
F9 신규 |
| Cody store (F1 생성) |
lib/stores/codyStore.ts |
F1 기존, F9 extend (addSavedLook, removeSavedLook, getSavedLooks) |
| 단일 adapter (F1 생성) |
lib/api/adapters/cody-match.ts |
F1 기존, F9 import only (new entry 없음) |
DB migration |
별도 infra 이슈로 분리 |
saved_looks 테이블+FK는 type:infra + human checkpoint 필요(operating contract). F9는 테이블 존재를 가정하고 FE만 — migration은 F9 범위 아님 |
금지 경로:
- ❌
app/cody/screens, app/cody/detail, /recreate, app/(request), app/cody/[id]
- ❌ 새 adapter 파일 (
codyAnalysisAdapter.ts, useCodyMatch.ts, recommended/adapter.ts)
- ❌
lib/components/vton/, /api/v1/vton 임포트
mock·real 경계
Contract 출처
Saved Looks 저장:
- Input: F6(⑤ Item Detail) 에서 "룩 보드에 저장" 클릭
- Output: DB
saved_looks insert, 모달 "저장 완료" → Saved 보드로 이동 옵션
- Mock: saveLook stub은 cody-match adapter seam과 분리 —
lib/api/cody/saved.ts 별도 모듈에 둔다 (cody-match.ts/cody-match-mock.ts는 추천 contract 전용, 저장 로직 금지)
Shop-this-look 공유:
- Input: Saved 보드 카드 "공유" 클릭
- Output: slug 생성 + DB update(
saved_looks.slug) + URL 복사 모달
- Mock: slug 로컬 생성(
nanoid(6)) + URL 조립(window.location.origin + '/cody/looks/' + slug)
공개 페이지 로드:
- Input:
/cody/looks/[slug] 진입
- Output: DB
saved_looks lookup (slug PK) → PublicLookShare 구성 → 렌더링
- Mock: fixture
saved_looks[] 배열로 slug 검색
Type mirror (cody-match-stage2 §2.1 재확인):
CodyAnalysis — mvp-scope §3 기준, items[].category ∈ {top, bottom, footwear, outerwear, accessory}
CandidateSKU — §2.2 전 필드: sku_signature_hash, brand, product, color, category, price_krw, primary_image_url, product_url, source_domain, retailer_scope, confidence, reason, relaxation_level, image_verification
- affiliate_url은 catalog_items(§7.1) 소속, CandidateSKU 아님 — ⑤ Item Detail 에서 필요하면 catalog_items join
- fit 분리: ShopperPreference 에 fit 필드 없음. UI 핏(over/slim/any)은 client-only, 백엔드 미전송
의존성
선행 작업
- F1 (Upload + single adapter
cody-match.ts): Saved/Shop-this-look 에서 recommended_look_id 참조
- F6 (Item Detail): "룩 보드에 저장" CTA 가 Saved 진입 트리거
후행 작업
- Saved/Shop-this-look 실패상태(미가입 저장 모달 · 404 · slug 충돌)는 F9 자체 소유 — F8 통합 QA 범위는 ①~⑤ 5화면 실패 8종만이며 F9는 F8 deps가 아님
Cross-cutting
- Auth (기존 Kakao+Google+Apple): Saved gated 권한 체크
- Metadata extraction (og_extractor): Shop-this-look og:image 등
제약·주의
-
cody_matches 혼동 금지: cody_matches 는 raw_posts 당 서비스 캐시 (stage2 §7.2). 저장 은 별도 user-scoped saved_looks 테이블 에 만들 것.
-
권한 경계:
- Saved: 로그인 필수 → user context 확인 후
WHERE user_id = $1 쿼리
- Shop-this-look: 뷰는 공개, 생성/수정/삭제는
creator_user_id == current_user.id 체크
-
VTON 미포함: lib/components/cody/ 어디도 lib/components/vton/, /api/v1/vton, vtonStore import 금지.
-
Upload 재사용 금지: Saved → ① 로 갔을 때 기존 useImageUpload 쓰지 말 것 (requestStore 의존). ① 에서는 new cody upload 전용 hook 사용.
-
slug 생성 정책:
- 중복 방지: DB unique constraint
- 길이: nanoid(6) 또는 custom(recommended 4-8char, 하이픈·언더스코어 허용)
- 일회성 재생성 불가 (공유 링크 고정성 중요)
-
Meta 동적화: generateMetadata (Next.js) 사용해 og:title=룩 제목, og:description=스타일 무드, og:image=원본 또는 첫 아이템 이미지. 정적 meta tag 금지.
-
빈 결과 처리:
- Saved empty: "저장한 룩이 없어요" + "룩 추천받기" CTA(→ ①)
- Shop-this-look 404: "이 룩 페이지를 찾을 수 없습니다" + "룩 추천받기" CTA(→ ①)
-
성능:
- Saved 보드 pagination: limit 10–20 (무한스크롤 or 페이지 버튼)
- Shop-this-look 로드: 데이터 ≤ 3초 (이상 timeout → fallback)
- 검색 결과(선택): 캐시 재사용 (new API 콜 피하기)
참조
- [[wireframes.md]] §2(+)Saved·(+)Shop-this-look · §4 빈 상태 카탈로그
- [[mvp-scope.md]] §2(보조 surface) · §5(저작권 deferred)
- [[cody-match-stage2.md]] §2.1(ShopperPreference, fit 필드 없음) · §2.2(CandidateSKU 전 필드) · §7.1(affiliate_url 은 catalog_items 소속)
- [[CONTEXT.md]] Cody glossary — Style Formula · Analysis Quality · Shopper Preference(fit 아님) · Client-only Fit · VTON 분리
- 기존 코드: F1(codyStore) · F6(gated modal) 참조 구조
Owner: raf (persona gated 확정 6/11)
Status: Ready for implementation
Estimate: 8–12 story points (Saved board + Shop-this-look + DB + auth + meta)
목적
cody recreation engine MVP 의 gated surface 2종 구현:
app/cody/saved/page.tsx) — 로그인 필수, 유저가 저장한 룩 세트 보드app/cody/looks/[slug]/page.tsx) — 공개 읽기, 룩 생성자 gated 공유 slug이 두 화면은 wireframes.md §2 (+)surface + mvp-scope.md §2 보조 surface 에서 정의된 피처이며, core 5화면(①②③④⑤) 을 완성한 후 도달 가능한 저장·공유 엔드 포인트다.
범위
화면
app/cody/saved/page.tsxapp/cody/looks/[slug]/page.tsxauth 경계
데이터 모델
중요: cody_matches(§7.2) 는 공용 서비스 캐시이고, saved_looks 는 user-scoped 저장소 — 별도 테이블 필요. 저작권은 deferred(§5), 현재는 "자신이 만든 룩만 공유" 제약으로 skin-deep 처리.
화면별 AC
(+)Saved Looks · P6 저장
Shop-this-look · 공유 slug
/cody/looks/:slug(SEO-friendly, 정적 페이지 스타일)Acceptance Criteria
app/cody/saved비로그인 → gated modal(가입/로그인) + 모달 close 시 홈으로 이동saved_looks) user-scoped 조회, pagination 또는 limit 10slug = nanoid(6)또는 유사 생성 → Shop-this-look URL 복사 버튼saved_looks.slug(공유 전까지 NULL 가능)/cody/looks/[slug]공개(비로그인) · slug 유효성 검증 · 404 처리(deleted/invalid slug)<head>에 og:title/description/image · twitter:card, 동적 생성 (next/head 또는 generateMetadata)핵심 파일
app/cody/saved/page.tsxapp/cody/looks/[slug]/page.tsxlib/components/cody/SavedBoard.tsxlib/components/cody/PublicLookShare.tsxlib/stores/codyStore.tsaddSavedLook,removeSavedLook,getSavedLooks)lib/api/adapters/cody-match.tsDB migrationsaved_looks테이블+FK는type:infra+ human checkpoint 필요(operating contract). F9는 테이블 존재를 가정하고 FE만 — migration은 F9 범위 아님금지 경로:
app/cody/screens,app/cody/detail,/recreate,app/(request),app/cody/[id]codyAnalysisAdapter.ts,useCodyMatch.ts,recommended/adapter.ts)lib/components/vton/,/api/v1/vton임포트mock·real 경계
Contract 출처
Saved Looks 저장:
saved_looksinsert, 모달 "저장 완료" → Saved 보드로 이동 옵션lib/api/cody/saved.ts별도 모듈에 둔다 (cody-match.ts/cody-match-mock.ts는 추천 contract 전용, 저장 로직 금지)Shop-this-look 공유:
saved_looks.slug) + URL 복사 모달nanoid(6)) + URL 조립(window.location.origin + '/cody/looks/' + slug)공개 페이지 로드:
/cody/looks/[slug]진입saved_lookslookup (slug PK) → PublicLookShare 구성 → 렌더링saved_looks[]배열로 slug 검색Type mirror (cody-match-stage2 §2.1 재확인):
CodyAnalysis— mvp-scope §3 기준,items[].category∈ {top, bottom, footwear, outerwear, accessory}CandidateSKU— §2.2 전 필드:sku_signature_hash,brand,product,color,category,price_krw,primary_image_url,product_url,source_domain,retailer_scope,confidence,reason,relaxation_level,image_verification의존성
선행 작업
cody-match.ts): Saved/Shop-this-look 에서 recommended_look_id 참조후행 작업
Cross-cutting
제약·주의
cody_matches 혼동 금지:
cody_matches는 raw_posts 당 서비스 캐시 (stage2 §7.2). 저장 은 별도 user-scopedsaved_looks테이블 에 만들 것.권한 경계:
WHERE user_id = $1쿼리creator_user_id == current_user.id체크VTON 미포함:
lib/components/cody/어디도lib/components/vton/,/api/v1/vton,vtonStoreimport 금지.Upload 재사용 금지: Saved → ① 로 갔을 때 기존
useImageUpload쓰지 말 것 (requestStore 의존). ① 에서는 new cody upload 전용 hook 사용.slug 생성 정책:
Meta 동적화:
generateMetadata(Next.js) 사용해 og:title=룩 제목, og:description=스타일 무드, og:image=원본 또는 첫 아이템 이미지. 정적 meta tag 금지.빈 결과 처리:
성능:
참조
Owner: raf (persona gated 확정 6/11)
Status: Ready for implementation
Estimate: 8–12 story points (Saved board + Shop-this-look + DB + auth + meta)