diff --git a/.agents/skills/api-contract-generator/SKILL.md b/.agents/skills/api-contract-generator/SKILL.md new file mode 100644 index 00000000..04dfec61 --- /dev/null +++ b/.agents/skills/api-contract-generator/SKILL.md @@ -0,0 +1,198 @@ +--- +name: api-contract-generator +description: Feature spec에서 OpenAPI 스타일 API 문서 생성. "API contract", "OpenAPI", "endpoint spec" 요청 시 자동 적용. +allowed-tools: Read, Write, Edit, Glob, Grep +--- + +# API Contract Generator + +## 개요 + +Feature spec과 화면 명세를 분석하여 API 계약 문서를 자동 생성합니다. +`specs/_shared/api-contracts.md` 형식을 준수합니다. + +## 트리거 조건 + +다음 키워드가 포함된 요청에서 자동 활성화: +- "API contract", "API 계약" +- "OpenAPI", "Swagger" +- "endpoint spec", "엔드포인트 명세" +- "API 문서 생성" + +## 생성 프로세스 + +### Step 1: 데이터 요구사항 추출 + +``` +[spec.md] → User Stories 분석 +[SCR-XXX-##] → 화면별 API 호출 식별 +[data-models.md] → 타입 참조 +``` + +### Step 2: 엔드포인트 도출 + +각 화면 명세에서: +1. "데이터 요구사항" 섹션 추출 +2. CRUD 작업 식별 +3. 필터/검색/페이지네이션 요구사항 파악 + +### Step 3: 문서 생성 + +## API 문서 구조 + +### 1. 개요 섹션 + +```markdown +# API Endpoints - {Feature Name} + +> {Feature} 기능에 필요한 API 엔드포인트 정의 + +**버전**: v1 +**기준 경로**: `/api/v1` +**인증 방식**: Bearer Token (Supabase Auth) +``` + +### 2. 엔드포인트 정의 + +```markdown +## {리소스} API + +### {HTTP Method} {Path} + +**설명**: {엔드포인트 목적} + +| 항목 | 내용 | +|------|------| +| Method | GET / POST / PATCH / DELETE | +| Path | `/api/v1/{resource}` | +| Auth | Required / Optional | +| 관련 화면 | SCR-XXX-## | +| 관련 User Story | U-## | + +#### Query Parameters (GET) + +| 파라미터 | 타입 | 필수 | 기본값 | 설명 | +|----------|------|:---:|-------|------| +| cursor | string | - | - | 페이지네이션 커서 | +| limit | number | - | 20 | 페이지 크기 | + +#### Request Body (POST/PATCH) + +```typescript +interface RequestBody { + field: string; +} +``` + +#### Response + +**성공 (200/201):** + +```typescript +interface SuccessResponse { + data: ResourceType; +} +``` + +**에러:** + +| 코드 | HTTP | 상황 | 메시지 | +|------|:---:|------|--------| +| VALIDATION_ERROR | 400 | 입력 검증 실패 | "Invalid input" | +| UNAUTHORIZED | 401 | 인증 필요 | "Authentication required" | +| FORBIDDEN | 403 | 권한 없음 | "Access denied" | +| NOT_FOUND | 404 | 리소스 없음 | "Resource not found" | +``` + +### 3. 화면-API 매핑 테이블 + +```markdown +## 화면-API 매핑 + +| 화면 ID | 기능 | API | 설명 | +|---------|------|-----|------| +| SCR-DISC-01 | 목록 로드 | GET /posts | 최신 Post 목록 | +| SCR-DISC-01 | 필터 조회 | GET /posts?category= | 필터 적용 | +``` + +## 출력 형식 + +### 파일 위치 + +``` +specs/{feature}/api-endpoints.md +``` + +### 문서 구조 + +```markdown +# API Endpoints - {Feature Name} + +## 목차 +1. [개요](#개요) +2. [{리소스1} API](#{리소스1}-api) +3. [{리소스2} API](#{리소스2}-api) +4. [화면-API 매핑](#화면-api-매핑) +5. [에러 코드](#에러-코드) + +--- + +## 개요 + +| 항목 | 내용 | +|------|------| +| 버전 | v1 | +| 기준 경로 | `/api/v1` | +| 인증 | Supabase Auth (Bearer Token) | +| 콘텐츠 타입 | application/json | + +--- + +## {리소스} API + +### GET /api/v1/{resource} + +{상세 정의...} + +--- + +## 화면-API 매핑 + +{매핑 테이블...} + +--- + +## 에러 코드 + +| 코드 | HTTP | 설명 | +|------|:---:|------| +| VALIDATION_ERROR | 400 | 입력값 검증 실패 | +| UNAUTHORIZED | 401 | 인증 필요 | +| FORBIDDEN | 403 | 권한 없음 | +| NOT_FOUND | 404 | 리소스 없음 | +| CONFLICT | 409 | 리소스 충돌 | +| INTERNAL_ERROR | 500 | 서버 오류 | +``` + +## 참조 파일 + +- `specs/_shared/api-contracts.md` - 기존 API 계약 패턴 +- `specs/_shared/data-models.md` - TypeScript 타입 참조 +- `docs/api/` - API 문서 디렉토리 + +## 검증 체크리스트 + +- [ ] 모든 화면 명세의 API 요구사항 충족 +- [ ] Request/Response 타입이 data-models.md와 일치 +- [ ] 인증 요구사항 명시 +- [ ] 페이지네이션 전략 정의 (cursor-based) +- [ ] 에러 코드 문서화 +- [ ] 화면-API 매핑 테이블 완성 + +## 사용 예시 + +``` +> specs/discovery/spec.md를 기반으로 API 계약 문서 생성해줘 +> 이 화면들에 필요한 API 엔드포인트를 정리해줘 +> OpenAPI 스타일로 API 명세 작성해줘 +``` diff --git a/.agents/skills/api-contract-generator/references/api-patterns.md b/.agents/skills/api-contract-generator/references/api-patterns.md new file mode 100644 index 00000000..7e85998c --- /dev/null +++ b/.agents/skills/api-contract-generator/references/api-patterns.md @@ -0,0 +1,218 @@ +# API 패턴 참조 + +> api-contract-generator 스킬에서 참조하는 API 설계 패턴입니다. + +## 참조 경로 + +| 문서 | 경로 | 설명 | +|------|------|------| +| 화면-API 매핑 | `specs/_shared/api-contracts.md` | 전체 API 계약 | +| Discovery API | `specs/discovery/api-endpoints.md` | 발견 기능 API | +| Detail View API | `specs/detail-view/api-endpoints.md` | 상세 보기 API | + +## URL 구조 패턴 + +### 기본 경로 + +``` +/api/v1/{resource} +``` + +### 계층 구조 + +``` +/api/v1/posts # 컬렉션 +/api/v1/posts/{post_id} # 단일 리소스 +/api/v1/posts/{post_id}/comments # 하위 리소스 +/api/v1/posts/{post_id}/spots # 관련 리소스 +``` + +### 특수 경로 + +``` +/api/v1/users/me # 현재 사용자 +/api/v1/search # 검색 +/api/v1/search/popular # 인기 검색어 +``` + +## HTTP 메서드 패턴 + +| 메서드 | 용도 | 멱등성 | 예시 | +|--------|------|:------:|------| +| GET | 조회 | ✅ | 리소스/컬렉션 조회 | +| POST | 생성 | ❌ | 새 리소스 생성 | +| PATCH | 부분 수정 | ✅ | 일부 필드 업데이트 | +| DELETE | 삭제 | ✅ | 리소스 삭제 | +| PUT | 전체 교체 | ✅ | (사용 자제) | + +## 요청 패턴 + +### Query Parameters + +```typescript +// 페이지네이션 +interface PaginationParams { + cursor?: string; // 커서 (이전 응답에서 받은 값) + limit?: number; // 페이지 크기 (기본: 20, 최대: 100) +} + +// 필터링 +interface FilterParams { + category?: string; + status?: string; + castId?: string; + mediaId?: string; +} + +// 정렬 +interface SortParams { + sort?: string; // 정렬 필드 + order?: 'asc' | 'desc'; +} + +// 검색 +interface SearchParams { + q?: string; // 검색어 +} +``` + +### Request Body + +```typescript +// 생성 요청 +interface CreateRequest { + // 필수 필드만 포함 + requiredField: string; + optionalField?: string; +} + +// 수정 요청 (모두 optional) +interface UpdateRequest { + requiredField?: string; + optionalField?: string; +} +``` + +## 응답 패턴 + +### 단일 리소스 + +```typescript +interface SingleResponse { + data: T; +} +``` + +### 컬렉션 (페이지네이션) + +```typescript +interface ListResponse { + data: T[]; + pagination: { + cursor: string | null; // 다음 페이지 커서 (없으면 null) + hasMore: boolean; // 다음 페이지 존재 여부 + total?: number; // 전체 개수 (비용이 높아 선택적) + }; +} +``` + +### 생성 응답 + +```typescript +// 201 Created +interface CreateResponse { + data: { + id: string; + // ... 생성된 리소스 전체 + }; +} +``` + +### 빈 응답 + +```typescript +// 204 No Content (DELETE 성공 시) +// body 없음 +``` + +## 에러 패턴 + +### 에러 응답 구조 + +```typescript +interface ErrorResponse { + error: { + code: string; // 에러 코드 (예: "VALIDATION_ERROR") + message: string; // 사용자 표시용 메시지 + details?: { // 상세 정보 (개발용) + field?: string; // 문제가 된 필드 + reason?: string; // 상세 원인 + [key: string]: unknown; + }; + }; +} +``` + +### 표준 에러 코드 + +| 코드 | HTTP | 상황 | +|------|:---:|------| +| VALIDATION_ERROR | 400 | 입력값 검증 실패 | +| INVALID_JSON | 400 | JSON 파싱 실패 | +| UNAUTHORIZED | 401 | 인증 필요 | +| TOKEN_EXPIRED | 401 | 토큰 만료 | +| FORBIDDEN | 403 | 권한 없음 | +| NOT_FOUND | 404 | 리소스 없음 | +| CONFLICT | 409 | 중복 또는 충돌 | +| RATE_LIMITED | 429 | 요청 한도 초과 | +| INTERNAL_ERROR | 500 | 서버 내부 오류 | + +## 인증 패턴 + +### Bearer Token + +``` +Authorization: Bearer {supabase_access_token} +``` + +### 인증 요구사항 + +| 엔드포인트 | 인증 | 비고 | +|------------|:----:|------| +| GET /posts | Optional | 비로그인도 조회 가능 | +| POST /posts | Required | 로그인 필수 | +| PATCH /posts/{id} | Required | 본인 리소스만 | +| DELETE /posts/{id} | Required | 본인 리소스만 | + +## 버전 관리 + +### URL 버저닝 + +``` +/api/v1/posts # v1 +/api/v2/posts # v2 (미래) +``` + +### 버전 정책 + +- Major 변경 시 새 버전 생성 +- Minor 변경은 하위 호환 유지 +- 구버전 최소 6개월 유지 + +## 화면별 API 매핑 예시 + +### Discovery + +| 화면 | 기능 | Method | Path | +|------|------|--------|------| +| SCR-DISC-01 | 홈 목록 | GET | /posts | +| SCR-DISC-02 | 필터 적용 | GET | /posts?category=X | +| SCR-DISC-03 | 검색 | GET | /search?q=X | + +### Detail View + +| 화면 | 기능 | Method | Path | +|------|------|--------|------| +| SCR-VIEW-01 | Post 상세 | GET | /posts/{id} | +| SCR-VIEW-02 | Spot 상세 | GET | /spots/{id} | +| SCR-VIEW-03 | 투표 | POST | /solutions/{id}/votes | diff --git a/.agents/skills/component-template-generator/SKILL.md b/.agents/skills/component-template-generator/SKILL.md new file mode 100644 index 00000000..7a288e60 --- /dev/null +++ b/.agents/skills/component-template-generator/SKILL.md @@ -0,0 +1,228 @@ +--- +name: component-template-generator +description: 화면 스펙에서 React 컴포넌트 보일러플레이트 생성. "scaffold component", "create component" 요청 시 자동 적용. +allowed-tools: Read, Write, Edit, Glob, Grep +--- + +# Component Template Generator + +## 개요 + +화면 명세(SCR-XXX-##)를 분석하여 React 컴포넌트 보일러플레이트를 자동 생성합니다. +프로젝트의 코딩 컨벤션과 디자인 시스템을 준수합니다. + +## 트리거 조건 + +다음 키워드가 포함된 요청에서 자동 활성화: +- "scaffold component", "컴포넌트 스캐폴드" +- "create component", "컴포넌트 생성" +- "generate component", "컴포넌트 템플릿" +- "보일러플레이트 생성" + +## 생성 프로세스 + +### Step 1: 화면 명세 분석 + +``` +[specs/{feature}/screens/SCR-XXX-##.md] + ↓ +UI 요소 추출 (IMG, TXT, BTN, INP...) + ↓ +상태 정의 추출 + ↓ +데이터 요구사항 추출 +``` + +### Step 2: 컴포넌트 구조 결정 + +``` +페이지 컴포넌트 (app/route/page.tsx) +├── 레이아웃 컴포넌트 +├── 섹션 컴포넌트 (lib/components/feature/) +│ ├── 프레젠테이셔널 컴포넌트 +│ └── 컨테이너 컴포넌트 +└── UI 컴포넌트 (lib/components/ui/) +``` + +### Step 3: 코드 생성 + +## 컴포넌트 템플릿 + +### 1. 페이지 컴포넌트 + +```tsx +// app/{route}/page.tsx +import { Suspense } from 'react'; +import { FeatureSection } from '@/lib/components/{feature}'; +import { FeatureSkeleton } from '@/lib/components/{feature}/skeleton'; + +export const metadata = { + title: '{페이지 제목}', + description: '{페이지 설명}', +}; + +export default function FeaturePage() { + return ( +
+ }> + + +
+ ); +} +``` + +### 2. 섹션 컴포넌트 (Server Component) + +```tsx +// lib/components/{feature}/FeatureSection.tsx +import { fetchFeatureData } from '@/lib/api/{feature}'; +import { FeatureClient } from './FeatureClient'; + +export async function FeatureSection() { + const data = await fetchFeatureData(); + + return ; +} +``` + +### 3. 클라이언트 컴포넌트 + +```tsx +// lib/components/{feature}/FeatureClient.tsx +'use client'; + +import { useState } from 'react'; +import type { FeatureData } from '@decoded/shared/types/{feature}'; + +interface FeatureClientProps { + initialData: FeatureData[]; +} + +export function FeatureClient({ initialData }: FeatureClientProps) { + const [data, setData] = useState(initialData); + + // 상태 처리 + if (!data.length) { + return ; + } + + return ( +
+ {/* UI 렌더링 */} +
+ ); +} +``` + +### 4. 스켈레톤 컴포넌트 + +```tsx +// lib/components/{feature}/skeleton.tsx +export function FeatureSkeleton() { + return ( +
+
+
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+
+ ); +} +``` + +### 5. UI 컴포넌트 + +```tsx +// lib/components/ui/{ComponentName}.tsx +import { cn } from '@/lib/utils'; + +interface ComponentNameProps extends React.HTMLAttributes { + variant?: 'default' | 'outline'; + size?: 'sm' | 'md' | 'lg'; +} + +export function ComponentName({ + className, + variant = 'default', + size = 'md', + children, + ...props +}: ComponentNameProps) { + return ( +
+ {children} +
+ ); +} +``` + +## 파일 구조 생성 + +``` +lib/components/{feature}/ +├── index.ts # barrel export +├── FeatureSection.tsx # Server component +├── FeatureClient.tsx # Client component +├── FeatureCard.tsx # 개별 아이템 +├── skeleton.tsx # 로딩 스켈레톤 +└── types.ts # 로컬 타입 (필요시) +``` + +### index.ts (Barrel Export) + +```tsx +export { FeatureSection } from './FeatureSection'; +export { FeatureClient } from './FeatureClient'; +export { FeatureSkeleton } from './skeleton'; +``` + +## 네이밍 컨벤션 + +| 유형 | 규칙 | 예시 | +|------|------|------| +| 파일명 | PascalCase | `FeatureCard.tsx` | +| 컴포넌트 | PascalCase | `FeatureCard` | +| 훅 | camelCase + use | `useFeatureData` | +| 유틸 | camelCase | `formatFeature` | +| 상수 | UPPER_SNAKE | `MAX_ITEMS` | + +## 참조 파일 + +### 화면 명세 +- `specs/{feature}/screens/` - 화면 스펙 +- `specs/_shared/templates/screen-template.md` - 템플릿 + +### 기존 컴포넌트 +- `lib/components/` - 컴포넌트 패턴 참조 +- `lib/components/ui/` - UI 컴포넌트 + +### 디자인 시스템 +- `docs/design-system/` - 디자인 토큰 + +## 생성 후 체크리스트 + +- [ ] TypeScript 타입 정의 +- [ ] Props interface 정의 +- [ ] 상태 처리 (로딩, 에러, 빈 상태) +- [ ] 반응형 스타일 +- [ ] 접근성 속성 (aria-*) +- [ ] barrel export 추가 + +## 사용 예시 + +``` +> SCR-DISC-01 화면의 컴포넌트 스캐폴드 생성해줘 +> PostGrid 컴포넌트 보일러플레이트 만들어줘 +> 이 화면 명세에 맞는 React 컴포넌트 생성해줘 +``` diff --git a/.agents/skills/component-template-generator/references/screen-template.md b/.agents/skills/component-template-generator/references/screen-template.md new file mode 100644 index 00000000..71d7c480 --- /dev/null +++ b/.agents/skills/component-template-generator/references/screen-template.md @@ -0,0 +1,214 @@ +# 화면 스펙 → 컴포넌트 매핑 가이드 + +> 화면 명세의 UI 요소를 React 컴포넌트로 변환하는 규칙입니다. + +## UI 요소 매핑 + +### 이미지 (IMG-##) + +```markdown +# 명세 +| UI ID | 구분 | 요소명 | 속성/상태 | +|:---:|:---:|:---|:---| +| **IMG-01** | 이미지 | 메인 이미지 | Aspect: 16:9, Fallback: placeholder | +``` + +```tsx +// 컴포넌트 +import Image from 'next/image'; + +
+ {alt} +
+``` + +### 텍스트 (TXT-##) + +```markdown +# 명세 +| **TXT-01** | 텍스트 | 제목 | Font: H1, Color: text-primary | +``` + +```tsx +// 컴포넌트 +

+ {title} +

+``` + +### 버튼 (BTN-##) + +```markdown +# 명세 +| **BTN-01** | 버튼 | 확인 버튼 | Style: Primary, Disabled: 조건 미충족 시 | +``` + +```tsx +// 컴포넌트 +import { Button } from '@/lib/components/ui/Button'; + + +``` + +### 입력 (INP-##) + +```markdown +# 명세 +| **INP-01** | 입력 | 검색 입력 | Type: text, Placeholder: "검색어 입력" | +``` + +```tsx +// 컴포넌트 +import { Input } from '@/lib/components/ui/Input'; + + setQuery(e.target.value)} +/> +``` + +## 상태별 컴포넌트 + +### 로딩 상태 + +```tsx +// 스켈레톤 패턴 +function LoadingSkeleton() { + return ( +
+
+
+
+
+ ); +} +``` + +### 빈 상태 + +```tsx +// Empty state 패턴 +function EmptyState({ message }: { message: string }) { + return ( +
+ +

{message}

+
+ ); +} +``` + +### 에러 상태 + +```tsx +// Error state 패턴 +function ErrorState({ + message, + onRetry +}: { + message: string; + onRetry: () => void; +}) { + return ( +
+ +

{message}

+ +
+ ); +} +``` + +## 반응형 레이아웃 + +### 그리드 패턴 + +```tsx +// 명세: 모바일 1열, 태블릿 2열, 데스크톱 3열 +
+ {items.map(item => ( + + ))} +
+``` + +### 스택 패턴 + +```tsx +// 명세: 세로 스택, 간격 16px +
+ {items.map(item => ( + + ))} +
+``` + +## 공통 패턴 + +### Props 타입 + +```tsx +// 항상 명시적 타입 정의 +interface CardProps { + /** 카드 데이터 */ + item: ItemType; + /** 클릭 핸들러 */ + onClick?: (id: string) => void; + /** 추가 스타일 */ + className?: string; +} +``` + +### className 합성 + +```tsx +import { cn } from '@/lib/utils'; + +function Component({ className, ...props }: Props) { + return ( +
+ ); +} +``` + +### 이벤트 핸들러 + +```tsx +// 명세의 인터랙션 → 이벤트 핸들러 +// "Click: API 호출" → onClick +// "Enter: 검색 실행" → onKeyDown +// "Change: 250ms debounce" → onChange + useDebouncedValue +``` + +## 접근성 (A11y) + +```tsx +// 이미지 +설명적인 대체 텍스트 + +// 버튼 + +
+ {/* Content */} +
+ + ); +} +``` + +### 2. 클라이언트 컴포넌트 + +```tsx +// lib/components/{feature}/{FeatureName}Client.tsx +'use client'; + +import { useState } from 'react'; +import { Card, CardContent } from '@/lib/design-system'; + +interface {FeatureName}ClientProps { + initialData: T[]; +} + +export function {FeatureName}Client({ initialData }: {FeatureName}ClientProps) { + const [data, setData] = useState(initialData); + + return ( +
+ {data.map((item) => ( + + {/* Item content */} + + ))} +
+ ); +} +``` + +### 3. 반응형 그리드 + +```tsx +// 모바일: 1-2열, 태블릿: 2-3열, 데스크톱: 3-4열 +
+ {children} +
+``` + +## Pencil Screen 이미지 분석 가이드 + +### Home Desktop (decoded_home_desktop.png) +주요 섹션: +- Header: 로고, 네비게이션, 검색, 프로필 +- Hero: 대형 배너 이미지 +- Decoded's Pick: 수평 스크롤 카드 +- Today's Decoded: 그리드 레이아웃 +- Artist Spotlight: 2열 레이아웃 +- What's New: 카드 그리드 +- Discover Items: 카테고리별 탭 +- Best Items / Weekly Best / Trending: 리스트/그리드 혼합 +- Footer: 4열 링크 그룹 + +### Home Mobile (decoded_home_mobile.png) +반응형 변경사항: +- 네비게이션 → 햄버거 메뉴 +- 그리드 2-4열 → 1-2열 +- 수평 스크롤 유지 +- 패딩 축소 (p-6 → p-4) + +### Post Detail Desktop (decoded_post_detail_desktop.png) +주요 섹션: +- Hero 이미지 (전체 너비) +- 제목 + 태그 +- 본문 텍스트 (dropcap) +- Detected Items 그리드 +- Gallery 섹션 +- Shop the Look 캐러셀 +- Related Looks 그리드 + +### Post Detail Mobile (decoded_post_detail_mobile.png) +반응형 변경사항: +- 이미지 전체 너비 +- 수직 스택 레이아웃 +- 캐러셀 → 수평 스크롤 + +## 재사용 컴포넌트 활용 + +### 기존 디자인 시스템 Import + +```tsx +import { + // Typography + Heading, Text, + // Cards + Card, CardHeader, CardContent, CardFooter, + ProductCard, GridCard, FeedCardBase, + // Inputs + Input, SearchInput, + // Headers + DesktopHeader, MobileHeader, DesktopFooter, + // Tokens + typography, colors, spacing +} from '@/lib/design-system'; +``` + +### 기존 컴포넌트 위치 + +| 컴포넌트 | 경로 | +|----------|------| +| Hero | `lib/components/main/HeroSection.tsx` | +| Product Card | `lib/design-system/cards/ProductCard.tsx` | +| Grid Card | `lib/design-system/cards/GridCard.tsx` | +| Feed Card | `lib/design-system/cards/FeedCardBase.tsx` | +| Section Header | `lib/components/main/index.ts` | + +## 출력 규칙 + +### 파일 위치 +- 페이지: `app/{route}/page.tsx` +- 섹션 컴포넌트: `lib/components/{feature}/` +- UI 컴포넌트: `lib/components/ui/` 또는 `lib/design-system/` + +### 네이밍 컨벤션 +| 유형 | 규칙 | 예시 | +|------|------|------| +| 파일명 | PascalCase | `HeroSection.tsx` | +| 컴포넌트 | PascalCase | `HeroSection` | +| CSS 클래스 | Tailwind | `bg-primary text-sm` | + +### 반응형 브레이크포인트 +- `sm`: 640px +- `md`: 768px +- `lg`: 1024px +- `xl`: 1280px + +## 검증 체크리스트 + +- [ ] Pencil Screen 이미지와 시각적 일치 +- [ ] decoded.pen 디자인 토큰 사용 +- [ ] 기존 디자인 시스템 컴포넌트 재사용 +- [ ] 반응형 레이아웃 (데스크톱/모바일) +- [ ] TypeScript 타입 정의 +- [ ] 접근성 속성 (aria-*) +- [ ] 다크 모드 지원 (CSS 변수 사용) + +## 사용 예시 + +``` +> pencil-screen의 home_desktop 참고해서 Hero 섹션 UI 구현해줘 +> decoded.pen 기반으로 ProductCard 컴포넌트 만들어줘 +> 모바일 포스트 상세 화면의 Gallery 섹션 코드 생성해줘 +> pencil-screen 디자인대로 Today's Decoded 섹션 구현해줘 +``` + +## 참조 문서 + +- `docs/pencil-screen/` - 디자인 스크린샷 +- `docs/design-system/decoded.pen` - Pencil 디자인 파일 +- `docs/design-system/README.md` - 디자인 시스템 문서 +- `.planning/codebase/CONVENTIONS.md` - 코딩 컨벤션 diff --git a/.agents/skills/pencil-screen-ui/references/design-tokens.md b/.agents/skills/pencil-screen-ui/references/design-tokens.md new file mode 100644 index 00000000..47ab9b98 --- /dev/null +++ b/.agents/skills/pencil-screen-ui/references/design-tokens.md @@ -0,0 +1,224 @@ +# Design Token Reference + +## decoded.pen 토큰 → Tailwind 매핑 + +### Colors + +| Pencil Variable | CSS Variable | Tailwind | 용도 | +|-----------------|--------------|----------|------| +| `$--primary` | `--primary` | `bg-primary`, `text-primary` | 주요 액션, CTA | +| `$--primary-foreground` | `--primary-foreground` | `text-primary-foreground` | primary 위 텍스트 | +| `$--secondary` | `--secondary` | `bg-secondary` | 보조 액션 | +| `$--background` | `--background` | `bg-background` | 페이지 배경 | +| `$--foreground` | `--foreground` | `text-foreground` | 기본 텍스트 | +| `$--muted` | `--muted` | `bg-muted`, `text-muted` | 비활성, 보조 텍스트 | +| `$--muted-foreground` | `--muted-foreground` | `text-muted-foreground` | muted 위 텍스트 | +| `$--accent` | `--accent` | `bg-accent` | 강조 | +| `$--border` | `--border` | `border-border` | 테두리 | +| `$--card` | `--card` | `bg-card` | 카드 배경 | +| `$--destructive` | `--destructive` | `bg-destructive` | 삭제, 에러 | + +### Spacing (4px 기반) + +| Pencil Value | Tailwind | 실제 값 | +|--------------|----------|--------| +| `4` | `1` | 4px | +| `8` | `2` | 8px | +| `12` | `3` | 12px | +| `16` | `4` | 16px | +| `20` | `5` | 20px | +| `24` | `6` | 24px | +| `32` | `8` | 32px | +| `40` | `10` | 40px | +| `48` | `12` | 48px | +| `64` | `16` | 64px | + +### Border Radius + +| Pencil Value | Tailwind | +|--------------|----------| +| `4` | `rounded` | +| `6` | `rounded-md` | +| `8` | `rounded-lg` | +| `12` | `rounded-xl` | +| `16` | `rounded-2xl` | +| `9999` | `rounded-full` | + +### Typography + +| Pencil Spec | Design System | Tailwind | +|-------------|---------------|----------| +| `fontSize: 32, fontWeight: 700` | `` | `text-3xl font-bold` | +| `fontSize: 24, fontWeight: 700` | `` | `text-2xl font-bold` | +| `fontSize: 18, fontWeight: 600` | `` | `text-lg font-semibold` | +| `fontSize: 16, fontWeight: 500` | `` | `text-base font-medium` | +| `fontSize: 14, fontWeight: 400` | `` | `text-sm` | +| `fontSize: 12, fontWeight: 400` | `` | `text-xs` | + +### Font Family + +| Pencil Font | CSS Variable | Tailwind | +|-------------|--------------|----------| +| `Inter` | `--font-sans` | `font-sans` | +| `JetBrains Mono` | `--font-mono` | `font-mono` | + +## 컴포넌트 스타일 매핑 + +### Button Variants (decoded.pen) + +```tsx +// Button/Default + + +// Button/Secondary + + +// Button/Outline + +``` + +### Card Variants + +```tsx +// Card (기본) +
+ {children} +
+ +// Card (elevated) +
+ {children} +
+ +// Card (interactive) +
+ {children} +
+``` + +### Input Variants + +```tsx +// Input (기본) + + +// Input (with icon) +
+ + +
+ +// Input (search) +
+ + +
+``` + +## 레이아웃 패턴 + +### Section Wrapper + +```tsx +// 기본 섹션 래퍼 +
+
+ {children} +
+
+ +// 배경색이 있는 섹션 +
+
+ {children} +
+
+``` + +### Grid Layouts + +```tsx +// 2열 그리드 +
+ {children} +
+ +// 3열 그리드 +
+ {children} +
+ +// 4열 그리드 +
+ {children} +
+ +// 6열 그리드 (아이템) +
+ {children} +
+``` + +### Horizontal Scroll + +```tsx +// 수평 스크롤 (모바일) +
+ {items.map(item => ( +
+ {/* Card content */} +
+ ))} +
+``` + +## 다크 모드 지원 + +CSS 변수를 사용하면 자동으로 다크 모드가 지원됩니다: + +```tsx +// 올바른 사용 (다크 모드 자동 지원) +
+
+
+ +// 피해야 할 사용 (하드코딩된 색상) +
// ❌ +
// ❌ +``` + +## Lucide Icons + +decoded.pen에서 사용하는 아이콘: + +```tsx +import { + Plus, + Search, + X, + ChevronRight, + ChevronLeft, + Heart, + Share2, + Bookmark, + MoreHorizontal, + ArrowLeft, + ArrowRight, + Menu, + User, + Settings, + LogOut, + Camera, + Image, + Upload +} from 'lucide-react'; +``` diff --git a/.agents/skills/pencil-screen-ui/references/screen-analysis.md b/.agents/skills/pencil-screen-ui/references/screen-analysis.md new file mode 100644 index 00000000..a895c99d --- /dev/null +++ b/.agents/skills/pencil-screen-ui/references/screen-analysis.md @@ -0,0 +1,242 @@ +# Pencil Screen 이미지 분석 + +## 1. Home Desktop (decoded_home_desktop.png) + +### 전체 레이아웃 +- 배경: 다크 테마 (`bg-background`) +- 최대 너비: `max-w-7xl` +- Header 높이: 64px + +### 섹션 상세 + +#### Header +``` +위치: 상단 고정 +구성: Logo | Navigation Links | Search Icon | Profile Avatar +스타일: bg-background/80 backdrop-blur +``` + +#### Hero Banner +``` +위치: Header 아래 +높이: ~400px +구성: 대형 이미지 + 오버레이 텍스트 +스타일: relative, image cover, gradient overlay +``` + +#### Decoded's Pick +``` +배경: bg-card +레이아웃: 수평 스크롤 캐러셀 +카드 크기: ~280px width +카드 스타일: rounded-lg, image + title + price +헤더: "Decoded's Pick" + "View All" 버튼 +``` + +#### Today's Decoded +``` +배경: bg-background +레이아웃: 3열 그리드 (lg) +카드 타입: 프로필 + 이미지 조합 +왼쪽: 프로필 정보 카드 +오른쪽: 2x2 이미지 그리드 +헤더: "Today's Decoded" + "View Solution" 버튼 +``` + +#### Artist Spotlight +``` +배경: bg-card +레이아웃: 2열 그리드 +카드: 대형 이미지 + 이름 + 부제목 +헤더: "Artist Spotlight" + "View All" +``` + +#### What's New +``` +배경: bg-background +레이아웃: 3열 그리드 +카드: 이미지 + 제목 + 날짜 +``` + +#### Discover Items +``` +배경: bg-card +레이아웃: 탭 + 그리드 +탭: Fashion | Beauty | Lifestyle | Accessories +그리드: 4열, 작은 아이템 카드 +``` + +#### Best/Weekly/Trending (3단 섹션) +``` +배경: bg-background +레이아웃: 3열 (각 섹션) +- Best Items: 순위 + 이미지 + 가격 +- Weekly Best: 큰 이미지 + 제목 +- Trending Now: 태그 버튼 그리드 +``` + +#### Footer +``` +배경: bg-card +레이아웃: 4열 링크 그룹 +구성: Brand | Company | Support | Connect +하단: Copyright + Social Links +``` + +--- + +## 2. Home Mobile (decoded_home_mobile.png) + +### 반응형 변경사항 + +| 섹션 | Desktop | Mobile | +|------|---------|--------| +| Navigation | 수평 링크 | 햄버거 메뉴 | +| Hero | 400px | 280px | +| Decoded's Pick | 수평 스크롤 | 유지 (작은 카드) | +| Today's Decoded | 3열 | 1열 스택 | +| Artist Spotlight | 2열 | 1열 | +| What's New | 3열 | 2열 | +| Discover Items | 4열 | 2열 | +| Best/Weekly/Trending | 3열 | 1열 스택 | +| Footer | 4열 | 1열 아코디언 | + +### 패딩 변경 +- `px-4` (모바일) vs `px-6 lg:px-8` (데스크톱) +- `py-8` (모바일) vs `py-10 md:py-16` (데스크톱) + +--- + +## 3. Post Detail Desktop (decoded_post_detail_desktop.png) + +### 섹션 상세 + +#### Hero Image +``` +높이: 60vh, max 600px +스타일: 전체 너비, object-cover +오버레이: 그라디언트 (아래에서 위로) +``` + +#### Tag Bar +``` +위치: Hero 아래 +구성: 카테고리 태그들 (pill buttons) +스타일: gap-2, bg-muted rounded-full px-3 py-1 +``` + +#### Content Section +``` +레이아웃: max-w-3xl mx-auto +Typography: +- 제목: text-4xl font-bold +- 본문: text-lg leading-relaxed +- Dropcap: float-left text-5xl +``` + +#### Detected Items +``` +배경: bg-card +헤더: "Detected Items" + "View All" +레이아웃: 리스트 (이미지 + 제목 + 가격) +아이템: flex items-center gap-4 +``` + +#### Gallery +``` +배경: bg-background +레이아웃: 3열 그리드 +이미지: aspect-square rounded-lg +호버: scale-105 transition +``` + +#### Shop the Look +``` +배경: bg-card +레이아웃: 수평 스크롤 캐러셀 +카드: ProductCard 컴포넌트 +``` + +#### Related Looks +``` +배경: bg-background +레이아웃: 4열 그리드 +카드: 이미지 + 오버레이 정보 +``` + +--- + +## 4. Post Detail Mobile (decoded_post_detail_mobile.png) + +### 반응형 변경사항 + +| 섹션 | Desktop | Mobile | +|------|---------|--------| +| Hero | 60vh | 426px (고정) | +| Tags | 수평 스크롤 | 유지 | +| Content | max-w-3xl | full width | +| Detected Items | 3열 | 2열 | +| Gallery | 3열 | 2열 | +| Shop the Look | 캐러셀 | 수평 스크롤 | +| Related | 4열 | 2열 | + +### 모바일 전용 UI +- 상단 뒤로가기 버튼 +- 하단 액션 바 (Like, Share, Bookmark) +- 스와이프 제스처 지원 + +--- + +## 5. 공통 컴포넌트 패턴 + +### Section Header +```tsx +
+ {title} + +
+``` + +### Card Grid +```tsx +
+ {items.map(item => ( + {/* ... */} + ))} +
+``` + +### Horizontal Scroll +```tsx +
+ {items.map(item => ( +
+ {/* ... */} +
+ ))} +
+``` + +### Section Wrapper +```tsx +
+
+ {/* Section Header */} + {/* Content */} +
+
+``` + +### Responsive Grid +```tsx +// 2열 (모바일 1열) +className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6" + +// 3열 (모바일 2열) +className="grid grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6" + +// 4열 (모바일 2열) +className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-6" +``` diff --git a/.agents/skills/screen-spec-generator/SKILL.md b/.agents/skills/screen-spec-generator/SKILL.md new file mode 100644 index 00000000..59a925ca --- /dev/null +++ b/.agents/skills/screen-spec-generator/SKILL.md @@ -0,0 +1,198 @@ +--- +name: screen-spec-generator +description: 디자인 설명에서 화면 스펙 문서 생성. "screen spec", "document screen" 요청 시 자동 적용. +allowed-tools: Read, Write, Edit, Glob, Grep +--- + +# Screen Spec Generator + +## 개요 + +디자인 설명이나 와이어프레임을 기반으로 화면 스펙 문서(SCR-XXX-##)를 자동 생성합니다. +`specs/_shared/templates/screen-template.md` 형식을 준수합니다. + +## 트리거 조건 + +다음 키워드가 포함된 요청에서 자동 활성화: +- "screen spec", "화면 스펙" +- "document screen", "화면 문서화" +- "화면 명세 작성" +- "SCR-XXX 생성" + +## 생성 프로세스 + +### Step 1: 입력 분석 + +``` +[디자인 설명] → 요소 추출 +[와이어프레임 이미지] → 레이아웃 파악 +[기능 요구사항] → 인터랙션 정의 +``` + +### Step 2: ID 할당 + +| 도메인 | 접두사 | 예시 | +|--------|--------|------| +| Discovery | DISC | SCR-DISC-01 | +| Detail View | VIEW | SCR-VIEW-01 | +| User | USER | SCR-USER-01 | +| Creation | CREA | SCR-CREA-01 | +| Admin | ADMN | SCR-ADMN-01 | + +### Step 3: 문서 생성 + +## 화면 스펙 구조 + +### 1. 헤더 + +```markdown +# [SCR-{도메인}-##] 화면명 (Screen Name) + +| 항목 | 내용 | +|:---|:---| +| **문서 ID** | SCR-{도메인}-## | +| **경로** | `/route/path` | +| **작성일** | YYYY-MM-DD | +| **버전** | v1.0 | +| **상태** | 초안 | +``` + +### 2. 화면 개요 + +```markdown +## 1. 화면 개요 + +- **목적**: 화면의 주요 목적을 한 문장으로 설명 +- **선행 조건**: 로그인 필요 여부, 권한 요구사항 +- **후속 화면**: 이동 가능한 화면 목록 +- **관련 기능 ID**: [U-##](../spec.md#u-##) +``` + +### 3. 와이어프레임 (ASCII) + +```markdown +## 2. UI 와이어프레임 + +### 2.1 데스크톱 (≥768px) + +┌────────────────────────────────────────────┐ +│ [ Header ] │ +├────────────────────────────────────────────┤ +│ │ +│ [IMG-01] 메인 이미지 영역 │ +│ │ +│ [TXT-01] 제목 │ +│ [TXT-02] 부제목 │ +│ │ +│ ┌────────────────────────────────────┐ │ +│ │ [BTN-01] CTA 버튼 │ │ +│ └────────────────────────────────────┘ │ +│ │ +├────────────────────────────────────────────┤ +│ [ Footer ] │ +└────────────────────────────────────────────┘ + +### 2.2 모바일 (<768px) + +┌──────────────────────┐ +│ [ Header ] │ +├──────────────────────┤ +│ [IMG-01] │ +│ (full width) │ +│ │ +│ [TXT-01] 제목 │ +│ [TXT-02] 부제목 │ +│ │ +│ [BTN-01] 버튼 │ +│ (full width) │ +├──────────────────────┤ +│ [ Footer ] │ +└──────────────────────┘ +``` + +### 4. UI 요소 정의 + +```markdown +## 3. UI 요소 정의 + +| UI ID | 구분 | 요소명 | 속성/상태 | 인터랙션/로직 | +|:---:|:---:|:---|:---|:---| +| **IMG-01** | 이미지 | 메인 이미지 | Aspect: 16:9, Fallback: placeholder | 클릭 시 확대 | +| **TXT-01** | 텍스트 | 제목 | Font: H1, Color: text-primary | - | +| **TXT-02** | 텍스트 | 부제목 | Font: Body, Color: text-muted | - | +| **BTN-01** | 버튼 | CTA 버튼 | Style: Primary, Disabled: 조건 미충족 | Click: 다음 화면 이동 | +``` + +### 5. 상태 정의 + +```markdown +## 4. 상태 정의 + +| 상태 | 조건 | UI 변화 | +|:---|:---|:---| +| **기본** | 데이터 로드 완료 | 정상 표시 | +| **로딩** | 데이터 요청 중 | 스켈레톤 UI | +| **빈 상태** | 데이터 없음 | Empty state 표시 | +| **에러** | API 실패 | 에러 메시지 + 재시도 | +``` + +### 6. 데이터 요구사항 + +```markdown +## 5. 데이터 요구사항 + +### 5.1 API 호출 + +| API | Method | Endpoint | 호출 시점 | 응답 | +|:---|:---:|:---|:---|:---| +| 데이터 조회 | GET | `/api/v1/resource` | 화면 진입 | `{ data: T[] }` | + +### 5.2 상태 관리 + +| 스토어 | 키 | 타입 | 설명 | +|:---|:---|:---|:---| +| Zustand | `filterStore.active` | `string` | 활성 필터 | +| React Query | `["resource", id]` | `QueryKey` | 캐시 키 | +``` + +### 7. 테스트 시나리오 + +```markdown +## 6. 테스트 시나리오 + +| ID | 시나리오 | 기대 결과 | 우선순위 | +|:---|:---|:---|:---:| +| T-01 | 정상 데이터 로드 | 목록 표시 | High | +| T-02 | 데이터 없음 | Empty state | High | +| T-03 | API 에러 | 에러 메시지 | High | +``` + +## 출력 위치 + +``` +specs/{feature}/screens/SCR-{도메인}-##.md +``` + +## 참조 파일 + +- `specs/_shared/templates/screen-template.md` - 전체 템플릿 +- `docs/design-system/` - 디자인 토큰 +- `specs/{feature}/spec.md` - Feature spec (User Story 참조) + +## 검증 체크리스트 + +- [ ] 문서 ID 형식 준수 (SCR-XXX-##) +- [ ] 와이어프레임 (데스크톱/모바일) 포함 +- [ ] 모든 UI 요소에 ID 부여 +- [ ] 상태 정의 (기본, 로딩, 빈 상태, 에러) +- [ ] API 호출 명세 +- [ ] User Story 연결 +- [ ] 테스트 시나리오 + +## 사용 예시 + +``` +> 홈 화면에 대한 screen spec 생성해줘 +> 이 디자인을 SCR-DISC-01 형식으로 문서화해줘 +> 검색 결과 화면의 화면 명세 작성해줘 +``` diff --git a/.agents/skills/screen-spec-generator/references/screen-template.md b/.agents/skills/screen-spec-generator/references/screen-template.md new file mode 100644 index 00000000..7dd067f3 --- /dev/null +++ b/.agents/skills/screen-spec-generator/references/screen-template.md @@ -0,0 +1,93 @@ +# 화면 스펙 템플릿 요약 + +> 전체 템플릿: `specs/_shared/templates/screen-template.md` + +## 필수 섹션 + +| 섹션 | 필수 | 설명 | +|------|:---:|------| +| 헤더 메타데이터 | ✅ | 문서 ID, 경로, 버전 | +| 화면 개요 | ✅ | 목적, 선행 조건, 후속 화면 | +| 와이어프레임 | ✅ | 데스크톱 + 모바일 | +| UI 요소 정의 | ✅ | 모든 요소 ID와 속성 | +| 상태 정의 | ✅ | 기본, 로딩, 빈 상태, 에러 | +| 데이터 요구사항 | ✅ | API, 상태 관리 | +| 테스트 시나리오 | ✅ | 주요 테스트 케이스 | + +## 선택 섹션 + +| 섹션 | 조건 | +|------|------| +| 애니메이션/전환 | 애니메이션 있는 화면 | +| 성능 최적화 | 리스트, 무한스크롤, 대용량 미디어 | +| i18n 고려사항 | 다국어 지원 화면 | + +## UI 요소 ID 규칙 + +| 접두사 | 유형 | 예시 | +|--------|------|------| +| IMG | 이미지 | IMG-01, IMG-02 | +| TXT | 텍스트 | TXT-01, TXT-02 | +| BTN | 버튼 | BTN-01, BTN-02 | +| INP | 입력 | INP-01, INP-02 | +| LST | 리스트 | LST-01, LST-02 | +| CRD | 카드 | CRD-01, CRD-02 | +| ICO | 아이콘 | ICO-01, ICO-02 | +| TAB | 탭 | TAB-01, TAB-02 | +| MDL | 모달 | MDL-01, MDL-02 | +| NAV | 네비게이션 | NAV-01, NAV-02 | + +## 상태 정의 템플릿 + +```markdown +| 상태 | 조건 | UI 변화 | +|:---|:---|:---| +| **기본** | 데이터 정상 로드 | 모든 요소 정상 표시 | +| **로딩** | 데이터 요청 중 | 스켈레톤 UI 또는 스피너 | +| **빈 상태** | 데이터 없음 | Empty state + 안내 메시지 | +| **에러** | API 실패 | 에러 메시지 + 재시도 버튼 | +| **부분 로딩** | 일부 데이터만 | 로드된 부분만 표시 + 더보기 | +``` + +## API 호출 템플릿 + +```markdown +| API | Method | Endpoint | 호출 시점 | 응답 타입 | +|:---|:---:|:---|:---|:---| +| 초기 로드 | GET | `/api/v1/{resource}` | 화면 진입 | `ListResponse` | +| 상세 조회 | GET | `/api/v1/{resource}/{id}` | 아이템 클릭 | `T` | +| 생성 | POST | `/api/v1/{resource}` | 폼 제출 | `{ id: string }` | +| 수정 | PATCH | `/api/v1/{resource}/{id}` | 저장 클릭 | `T` | +| 삭제 | DELETE | `/api/v1/{resource}/{id}` | 삭제 확인 | `void` | +``` + +## 테스트 시나리오 템플릿 + +```markdown +| ID | 시나리오 | 기대 결과 | 우선순위 | +|:---|:---|:---|:---:| +| T-01 | 정상 데이터 로드 | 목록 정상 표시 | High | +| T-02 | 데이터 없음 | Empty state 표시 | High | +| T-03 | API 타임아웃 | 에러 + 재시도 버튼 | High | +| T-04 | 인증 만료 | 로그인 페이지 이동 | High | +| T-05 | 스크롤 페이지네이션 | 추가 데이터 로드 | Medium | +| T-06 | 오프라인 상태 | 캐시된 데이터 표시 | Medium | +``` + +## 디자인 토큰 참조 + +| 용도 | 토큰 | 참조 | +|------|------|------| +| 배경색 | `--background` | colors.md | +| 텍스트 | `--foreground` | colors.md | +| 제목 | `text-2xl` | typography.md | +| 본문 | `text-base` | typography.md | +| 간격 | `gap-4`, `p-6` | spacing.md | + +## 반응형 브레이크포인트 + +| 뷰포트 | 너비 | 레이아웃 | +|--------|------|----------| +| 모바일 | <768px | 단일 컬럼, 스택 | +| 태블릿 | 768-1023px | 2컬럼 그리드 | +| 데스크톱 | ≥1024px | 3+ 컬럼, 사이드바 | diff --git a/.agents/skills/supabase-migration-generator/SKILL.md b/.agents/skills/supabase-migration-generator/SKILL.md new file mode 100644 index 00000000..5359e4bd --- /dev/null +++ b/.agents/skills/supabase-migration-generator/SKILL.md @@ -0,0 +1,233 @@ +--- +name: supabase-migration-generator +description: data-model.md에서 SQL 마이그레이션 생성. "migration", "database schema" 요청 시 자동 적용. +allowed-tools: Read, Write, Edit, Glob, Grep, Bash +--- + +# Supabase Migration Generator + +## 개요 + +데이터 모델 정의에서 Supabase/PostgreSQL 마이그레이션 SQL을 자동 생성합니다. +프로젝트의 기존 스키마 패턴과 네이밍 규칙을 준수합니다. + +## 트리거 조건 + +다음 키워드가 포함된 요청에서 자동 활성화: +- "migration", "마이그레이션" +- "database schema", "DB 스키마" +- "테이블 생성", "create table" +- "컬럼 추가", "add column" + +## 생성 프로세스 + +### Step 1: 소스 분석 + +``` +[specs/_shared/data-models.md] → TypeScript 인터페이스 파싱 +[specs/{feature}/data-model.md] → 기능별 모델 파싱 +[supabase/migrations/] → 기존 스키마 확인 +``` + +### Step 2: 변경 사항 도출 + +``` +현재 스키마 ─┬─ 새 모델 정의 + │ + ↓ + 변경 사항 계산 + │ + ├── 새 테이블 + ├── 새 컬럼 + ├── 타입 변경 + └── 관계 추가 +``` + +### Step 3: SQL 생성 + +## 마이그레이션 패턴 + +### 1. 새 테이블 생성 + +```sql +-- Migration: create_{table_name}_table +-- Description: {테이블 설명} + +CREATE TABLE {table_name} ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- 필수 필드 + required_field VARCHAR(255) NOT NULL, + + -- 선택 필드 + optional_field TEXT, + + -- 관계 (FK) + related_id UUID REFERENCES related_table(id) ON DELETE SET NULL, + + -- JSONB + metadata JSONB DEFAULT '{}', + + -- 타임스탬프 + created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL +); + +-- 인덱스 +CREATE INDEX idx_{table_name}_{column} ON {table_name}({column}); + +-- RLS 정책 +ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "{table_name}_select_policy" ON {table_name} + FOR SELECT USING (true); + +CREATE POLICY "{table_name}_insert_policy" ON {table_name} + FOR INSERT WITH CHECK (auth.uid() IS NOT NULL); + +-- updated_at 자동 갱신 트리거 +CREATE TRIGGER update_{table_name}_updated_at + BEFORE UPDATE ON {table_name} + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); +``` + +### 2. ENUM 타입 생성 + +```sql +-- ENUM 대신 CHECK 제약 조건 권장 (유연성) +ALTER TABLE {table_name} +ADD CONSTRAINT {table_name}_{column}_check +CHECK ({column} IN ('value1', 'value2', 'value3')); + +-- 또는 참조 테이블 사용 +CREATE TABLE {type_name}_enum ( + value VARCHAR(50) PRIMARY KEY, + label VARCHAR(100) NOT NULL, + sort_order INTEGER DEFAULT 0 +); +``` + +### 3. 컬럼 추가 + +```sql +-- Migration: add_{column}_to_{table} +-- Description: {변경 설명} + +ALTER TABLE {table_name} +ADD COLUMN {column_name} {TYPE} {CONSTRAINTS}; + +-- 기존 데이터 마이그레이션 (필요시) +UPDATE {table_name} +SET {column_name} = {default_value} +WHERE {column_name} IS NULL; +``` + +### 4. 관계 테이블 (Junction) + +```sql +-- Migration: create_{table1}_{table2}_junction +-- Description: M:N 관계 테이블 + +CREATE TABLE {table1}_{table2} ( + {table1}_id UUID REFERENCES {table1}(id) ON DELETE CASCADE, + {table2}_id UUID REFERENCES {table2}(id) ON DELETE CASCADE, + + -- 추가 메타데이터 + created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + + PRIMARY KEY ({table1}_id, {table2}_id) +); + +CREATE INDEX idx_{table1}_{table2}_{table1}_id ON {table1}_{table2}({table1}_id); +CREATE INDEX idx_{table1}_{table2}_{table2}_id ON {table1}_{table2}({table2}_id); +``` + +### 5. 인덱스 추가 + +```sql +-- Migration: add_indexes_to_{table} +-- Description: 성능 최적화 인덱스 + +-- 단일 컬럼 인덱스 +CREATE INDEX idx_{table}_{column} ON {table}({column}); + +-- 복합 인덱스 +CREATE INDEX idx_{table}_{col1}_{col2} ON {table}({col1}, {col2}); + +-- 부분 인덱스 +CREATE INDEX idx_{table}_{column}_active +ON {table}({column}) +WHERE status = 'active'; +``` + +## TypeScript → PostgreSQL 매핑 + +| TypeScript | PostgreSQL | 비고 | +|------------|------------|------| +| `string` | `VARCHAR(255)` / `TEXT` | 길이에 따라 | +| `number` | `INTEGER` / `DECIMAL(10,2)` | 정수/소수 | +| `boolean` | `BOOLEAN` | | +| `Date` | `TIMESTAMPTZ` | 타임존 포함 | +| `string \| null` | `{TYPE}` (nullable) | NULL 허용 | +| `field?: string` | `{TYPE} DEFAULT NULL` | Optional | +| Union type | CHECK 제약 | ENUM 대체 | +| `object` | `JSONB` | | + +## 파일 규칙 + +### 파일명 + +``` +{timestamp}_{action}_{target}.sql + +예시: +20240115120000_create_post_table.sql +20240115120100_add_status_to_post.sql +20240115120200_create_post_cast_junction.sql +``` + +### 파일 위치 + +``` +supabase/migrations/{파일명}.sql +``` + +## 참조 파일 + +### 데이터 모델 +- `specs/_shared/data-models.md` - 마스터 데이터 모델 +- `specs/{feature}/data-model.md` - 기능별 모델 + +### 기존 스키마 +- `supabase/migrations/` - 기존 마이그레이션 +- `docs/database/` - DB 문서 + +## 검증 체크리스트 + +- [ ] 테이블명 snake_case 준수 +- [ ] 컬럼명 snake_case 준수 +- [ ] 적절한 타입 선택 +- [ ] NOT NULL 제약 조건 설정 +- [ ] FK 관계 정의 +- [ ] 인덱스 설계 +- [ ] RLS 정책 설정 +- [ ] updated_at 트리거 설정 + +## MCP 연동 + +생성된 SQL은 Supabase MCP를 통해 적용: + +``` +mcp__supabase__apply_migration + name: "{migration_name}" + query: "{sql_content}" +``` + +## 사용 예시 + +``` +> Post 인터페이스에 대한 마이그레이션 생성해줘 +> 새로운 Badge 테이블 SQL 만들어줘 +> media와 cast 간의 관계 테이블 마이그레이션 생성해줘 +``` diff --git a/.agents/skills/supabase-migration-generator/references/migration-patterns.md b/.agents/skills/supabase-migration-generator/references/migration-patterns.md new file mode 100644 index 00000000..0e4869ed --- /dev/null +++ b/.agents/skills/supabase-migration-generator/references/migration-patterns.md @@ -0,0 +1,223 @@ +# 마이그레이션 패턴 참조 + +> Supabase/PostgreSQL 마이그레이션 작성 시 참조하는 패턴입니다. + +## 기존 마이그레이션 참조 + +| 패턴 | 예시 파일 | +|------|----------| +| 테이블 생성 | `supabase/migrations/` 내 최신 파일 참조 | +| ENUM 타입 | `specs/_shared/data-models.md` SQL 섹션 | +| 관계 테이블 | `media_cast`, `post_cast` 패턴 | + +## RLS (Row Level Security) 패턴 + +### 공개 읽기 + +```sql +-- 모든 사용자가 읽기 가능 +CREATE POLICY "public_read" ON {table} + FOR SELECT USING (true); +``` + +### 인증된 사용자만 읽기 + +```sql +CREATE POLICY "authenticated_read" ON {table} + FOR SELECT USING (auth.role() = 'authenticated'); +``` + +### 본인 데이터만 접근 + +```sql +-- 본인 데이터만 조회 +CREATE POLICY "own_data_select" ON {table} + FOR SELECT USING (auth.uid() = user_id); + +-- 본인 데이터만 수정 +CREATE POLICY "own_data_update" ON {table} + FOR UPDATE USING (auth.uid() = user_id); + +-- 본인 데이터만 삭제 +CREATE POLICY "own_data_delete" ON {table} + FOR DELETE USING (auth.uid() = user_id); +``` + +### 관리자 전체 접근 + +```sql +CREATE POLICY "admin_all" ON {table} + FOR ALL USING ( + EXISTS ( + SELECT 1 FROM "user" + WHERE id = auth.uid() AND role = 'admin' + ) + ); +``` + +## 함수 및 트리거 + +### updated_at 자동 갱신 + +```sql +-- 함수 (한 번만 생성) +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- 트리거 (테이블마다 생성) +CREATE TRIGGER update_{table}_updated_at + BEFORE UPDATE ON {table} + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); +``` + +### Soft Delete + +```sql +-- 테이블에 deleted_at 컬럼 추가 +ALTER TABLE {table} ADD COLUMN deleted_at TIMESTAMPTZ; + +-- 삭제 대신 soft delete +CREATE OR REPLACE FUNCTION soft_delete_{table}() +RETURNS TRIGGER AS $$ +BEGIN + UPDATE {table} SET deleted_at = NOW() WHERE id = OLD.id; + RETURN NULL; -- 실제 삭제 방지 +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER soft_delete_{table}_trigger + BEFORE DELETE ON {table} + FOR EACH ROW + EXECUTE FUNCTION soft_delete_{table}(); + +-- 조회 시 deleted_at 필터 +CREATE POLICY "exclude_deleted" ON {table} + FOR SELECT USING (deleted_at IS NULL); +``` + +## 인덱스 전략 + +### 일반 인덱스 + +```sql +-- 자주 필터링되는 컬럼 +CREATE INDEX idx_{table}_{column} ON {table}({column}); +``` + +### 복합 인덱스 + +```sql +-- 자주 함께 조회되는 컬럼 +CREATE INDEX idx_{table}_user_created +ON {table}(user_id, created_at DESC); +``` + +### 부분 인덱스 + +```sql +-- 특정 조건의 데이터만 인덱싱 +CREATE INDEX idx_{table}_active +ON {table}(created_at) +WHERE status = 'active'; +``` + +### GIN 인덱스 (JSONB) + +```sql +-- JSONB 필드 검색용 +CREATE INDEX idx_{table}_metadata_gin +ON {table} USING GIN (metadata); +``` + +### 텍스트 검색 인덱스 + +```sql +-- Full-text search +CREATE INDEX idx_{table}_search +ON {table} USING GIN (to_tsvector('korean', content)); +``` + +## 데이터 마이그레이션 + +### 기본값으로 채우기 + +```sql +-- NOT NULL 컬럼 추가 시 +ALTER TABLE {table} ADD COLUMN new_column VARCHAR(100); +UPDATE {table} SET new_column = 'default_value'; +ALTER TABLE {table} ALTER COLUMN new_column SET NOT NULL; +``` + +### 데이터 변환 + +```sql +-- 타입 변경 + 데이터 변환 +ALTER TABLE {table} +ALTER COLUMN {column} +TYPE INTEGER USING {column}::INTEGER; +``` + +### 컬럼 리네이밍 + +```sql +ALTER TABLE {table} RENAME COLUMN old_name TO new_name; +``` + +## 롤백 패턴 + +```sql +-- 마이그레이션 롤백용 SQL +-- 파일: {timestamp}_rollback_{action}.sql + +-- 테이블 삭제 +DROP TABLE IF EXISTS {table} CASCADE; + +-- 컬럼 삭제 +ALTER TABLE {table} DROP COLUMN IF EXISTS {column}; + +-- 인덱스 삭제 +DROP INDEX IF EXISTS idx_{table}_{column}; +``` + +## 주의사항 + +### 1. 파괴적 변경 + +```sql +-- ❌ 위험: 데이터 손실 가능 +DROP COLUMN +DROP TABLE +ALTER TYPE (축소) + +-- ✅ 안전: 새 컬럼/테이블 추가 후 마이그레이션 +ADD COLUMN +CREATE TABLE +``` + +### 2. 잠금 최소화 + +```sql +-- ❌ 테이블 잠금 발생 +ALTER TABLE {table} ADD COLUMN col INT DEFAULT 0; + +-- ✅ 잠금 없이 추가 후 별도 업데이트 +ALTER TABLE {table} ADD COLUMN col INT; +UPDATE {table} SET col = 0 WHERE col IS NULL; +ALTER TABLE {table} ALTER COLUMN col SET DEFAULT 0; +``` + +### 3. 트랜잭션 + +```sql +BEGIN; +-- 여러 변경 사항 +COMMIT; +-- 또는 +ROLLBACK; +``` diff --git a/.claude/commands/grill-docs.md b/.claude/commands/grill-docs.md new file mode 100644 index 00000000..c0a9da6b --- /dev/null +++ b/.claude/commands/grill-docs.md @@ -0,0 +1,11 @@ +--- +description: Matt Pocock의 grill-with-docs — plan을 도메인 모델·ADR과 함께 grill (CONTEXT.md/ADR inline 업데이트) +--- + +Read `~/.agents/skills/grill-with-docs/SKILL.md` and follow its instructions exactly. Treat the loaded SKILL.md content as executable instructions, not reference material. + +This variant grills against the project's existing domain language (CONTEXT.md) and architectural decisions (docs/adr/). It updates docs inline as decisions crystallize. + +Note for decoded-monorepo: existing canonical docs live at `docs/agent/`, `docs/architecture/`, `docs/adr/`, and `.planning/codebase/`. The skill assumes `CONTEXT.md` and `docs/adr/` — you may need to map to the actual decoded paths or run `/setup-matt-skills` first. + +The user's topic: $ARGUMENTS diff --git a/.claude/commands/grill.md b/.claude/commands/grill.md new file mode 100644 index 00000000..93825750 --- /dev/null +++ b/.claude/commands/grill.md @@ -0,0 +1,9 @@ +--- +description: Matt Pocock의 grill-me — plan/design을 결정 분기 다 풀릴 때까지 인터뷰 +--- + +Read `~/.agents/skills/grill-me/SKILL.md` and follow its instructions exactly. Treat the loaded SKILL.md content as executable instructions, not reference material. Ask questions one at a time. Provide your recommended answer for each. Explore the codebase when a question can be answered that way. + +The user's topic to grill: $ARGUMENTS + +If `$ARGUMENTS` is empty, ask the user what plan or design they want grilled, then proceed. diff --git a/.claude/commands/improve-codebase-architecture.md b/.claude/commands/improve-codebase-architecture.md new file mode 100644 index 00000000..cfb69c76 --- /dev/null +++ b/.claude/commands/improve-codebase-architecture.md @@ -0,0 +1,11 @@ +--- +description: Matt Pocock의 improve-codebase-architecture — 코드베이스 deepening opportunities 발견, 도메인 언어·ADR 기반 refactoring 제안 +--- + +Read `~/.agents/skills/improve-codebase-architecture/SKILL.md` and follow its instructions exactly. Treat the loaded SKILL.md content as executable instructions, not reference material. + +Before exploring, read the domain docs as listed in [`docs/agents/domain.md`](docs/agents/domain.md) and any relevant ADRs in `docs/adr/`. Use `subagent_type=Explore` for codebase walks. + +The user's target area to improve: $ARGUMENTS + +If `$ARGUMENTS` is empty, ask which area/package to scope (e.g. `packages/web/lib/supabase`, `packages/api-server`, design-system) before exploring. Don't run on the entire monorepo without scope. diff --git a/.claude/commands/setup-matt-skills.md b/.claude/commands/setup-matt-skills.md new file mode 100644 index 00000000..49e65422 --- /dev/null +++ b/.claude/commands/setup-matt-skills.md @@ -0,0 +1,15 @@ +--- +description: Matt Pocock skills 일회성 setup — issue tracker / triage labels / domain docs 위치를 AGENTS.md|CLAUDE.md에 박음 +--- + +Read `~/.agents/skills/setup-matt-pocock-skills/SKILL.md` and follow its instructions exactly. Treat the loaded SKILL.md content as executable instructions. + +The skill's `disable-model-invocation: true` flag means it cannot auto-trigger — this slash command is the explicit invocation path. + +Important context for decoded-monorepo: +- Issue tracker: GitHub (`decodedcorp/decoded`) +- Existing docs layout: `docs/agent/` (agent reference), `docs/architecture/`, `docs/adr/`, `.planning/codebase/` +- Conventional Commits enforced +- Run this BEFORE first use of `/to-issues`, `/to-prd`, or any matt-pocock engineering skill that depends on issue tracker / triage label config + +The skill is prompt-driven (not deterministic). Explore, present what you found, confirm with the user before writing. diff --git a/.claude/commands/to-issues.md b/.claude/commands/to-issues.md new file mode 100644 index 00000000..14b291cd --- /dev/null +++ b/.claude/commands/to-issues.md @@ -0,0 +1,11 @@ +--- +description: Matt Pocock의 to-issues — plan/PRD를 vertical slice GitHub Issues로 분해 +--- + +Read `~/.agents/skills/to-issues/SKILL.md` and follow its instructions exactly. Treat the loaded SKILL.md content as executable instructions. + +**Prerequisite:** `/setup-matt-skills` must have been run once for this repo so the skill knows the issue tracker (GitHub) and triage label vocabulary. If setup was not done, the skill should refuse and ask you to run it first. + +Source plan/PRD path or topic: $ARGUMENTS + +If `$ARGUMENTS` is empty, the skill should grab the most recent plan in `.planning/`, `~/.gstack/projects/decodedcorp-decoded/`, or the current conversation context. diff --git a/.claude/commands/to-prd.md b/.claude/commands/to-prd.md new file mode 100644 index 00000000..82c2dc3c --- /dev/null +++ b/.claude/commands/to-prd.md @@ -0,0 +1,11 @@ +--- +description: Matt Pocock의 to-prd — 현재 대화 맥락을 PRD 문서로 응축해 issue tracker에 publish +--- + +Read `~/.agents/skills/to-prd/SKILL.md` and follow its instructions exactly. Treat the loaded SKILL.md content as executable instructions. + +**Prerequisite:** `/setup-matt-skills` must have been run once. PRD destination depends on the issue tracker config from setup. + +Optional override of PRD topic/title: $ARGUMENTS + +If `$ARGUMENTS` is empty, derive the PRD subject from the current conversation context. diff --git a/.claude/commands/zoom-out.md b/.claude/commands/zoom-out.md new file mode 100644 index 00000000..92e33096 --- /dev/null +++ b/.claude/commands/zoom-out.md @@ -0,0 +1,9 @@ +--- +description: Matt Pocock의 zoom-out — 현재 코드/맥락을 더 넓은 시야로 다시 보기 +--- + +Read `~/.agents/skills/zoom-out/SKILL.md` and follow its instructions exactly. Treat the loaded SKILL.md content as executable instructions. + +Use when the immediate work is making narrow assumptions and the broader codebase context (or product/system fit) needs to be re-grounded. + +The user's focus area: $ARGUMENTS diff --git a/.cursor/rules/api-routes.mdc b/.cursor/rules/api-routes.mdc index 476935ad..b1e0922e 100644 --- a/.cursor/rules/api-routes.mdc +++ b/.cursor/rules/api-routes.mdc @@ -3,6 +3,8 @@ description: Next.js API route conventions globs: "packages/web/app/api/**/*.ts" --- +> Cross-tool 공통 진입 규칙은 [`AGENTS.md`](mdc:AGENTS.md)를 먼저 읽는다. 본 파일은 Cursor 네이티브 규칙(globs · description)과 repo 특이사항만 유지. + # API Route Conventions ## Handler Format diff --git a/.cursor/rules/monorepo.mdc b/.cursor/rules/monorepo.mdc index b9f69f99..3e377382 100644 --- a/.cursor/rules/monorepo.mdc +++ b/.cursor/rules/monorepo.mdc @@ -3,6 +3,8 @@ description: decoded-monorepo conventions and architecture globs: "**/*" --- +> Cross-tool 공통 진입 규칙은 [`AGENTS.md`](mdc:AGENTS.md)를 먼저 읽는다. 본 파일은 Cursor 네이티브 규칙(globs · description)과 repo 특이사항만 유지. + # decoded-monorepo ## Structure diff --git a/.cursor/rules/react-components.mdc b/.cursor/rules/react-components.mdc index e54855e0..36838936 100644 --- a/.cursor/rules/react-components.mdc +++ b/.cursor/rules/react-components.mdc @@ -3,6 +3,8 @@ description: React component conventions for decoded web package globs: "packages/web/**/*.tsx" --- +> Cross-tool 공통 진입 규칙은 [`AGENTS.md`](mdc:AGENTS.md)를 먼저 읽는다. 본 파일은 Cursor 네이티브 규칙(globs · description)과 repo 특이사항만 유지. + # React Component Conventions ## Server vs Client Components diff --git a/.cursor/rules/rust-api.mdc b/.cursor/rules/rust-api.mdc index 29565913..68d13f96 100644 --- a/.cursor/rules/rust-api.mdc +++ b/.cursor/rules/rust-api.mdc @@ -3,6 +3,8 @@ description: Rust API server conventions (Axum + SeaORM) globs: "packages/api-server/**/*.rs" --- +> Cross-tool 공통 진입 규칙은 [`AGENTS.md`](mdc:AGENTS.md)를 먼저 읽는다. 본 파일은 Cursor 네이티브 규칙(globs · description)과 repo 특이사항만 유지. + # Rust API Server Conventions ## Stack diff --git a/.env.backend.example b/.env.backend.example index 16a3a476..bd1d4242 100644 --- a/.env.backend.example +++ b/.env.backend.example @@ -122,3 +122,13 @@ BATCH_SIZE=10 MAX_CONCURRENT_REQUESTS=5 REQUEST_TIMEOUT=30 MAX_RETRIES=3 + +# raw_posts R2 (#258) +RAW_POSTS_R2_ACCOUNT_ID= +RAW_POSTS_R2_ACCESS_KEY_ID= +RAW_POSTS_R2_SECRET_ACCESS_KEY= +RAW_POSTS_R2_PUBLIC_URL= + +# Instagram (#259, #495) — session dir mounted in docker-compose.prod.yml +INSTAGRAM_SESSION_USERNAME= +INSTAGRAM_SYNC_SINCE= diff --git a/.github/workflows/api-server-invariants.yml b/.github/workflows/api-server-invariants.yml index 04ad1346..e21e2a9b 100644 --- a/.github/workflows/api-server-invariants.yml +++ b/.github/workflows/api-server-invariants.yml @@ -22,7 +22,7 @@ jobs: invariants: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # OPERATION_DATABASE_URL is the SOT operation DB env var (#369). # prod / staging / dev infra all set OPERATION_DATABASE_URL only; diff --git a/.github/workflows/backend-release.yml b/.github/workflows/backend-release.yml index ba697099..3ee1937e 100644 --- a/.github/workflows/backend-release.yml +++ b/.github/workflows/backend-release.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/daily-digest.yml b/.github/workflows/daily-digest.yml index b079da75..3d02f914 100644 --- a/.github/workflows/daily-digest.yml +++ b/.github/workflows/daily-digest.yml @@ -29,7 +29,7 @@ jobs: digest: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/db-drift-check.yml b/.github/workflows/db-drift-check.yml index 5308c352..74fddb97 100644 --- a/.github/workflows/db-drift-check.yml +++ b/.github/workflows/db-drift-check.yml @@ -31,7 +31,7 @@ jobs: PRD_DATABASE_URL: ${{ secrets.PRD_DATABASE_URL }} LOCAL_DATABASE_URL: postgresql://postgres:testpass@localhost:5432/drift_test steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install postgresql-client 17 run: | diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index 57dac6a8..0b2f77e2 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -26,7 +26,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Copy env file run: cp /Users/decoded/dev/decoded/.env.backend.prod .env.backend.prod diff --git a/.github/workflows/dev-merge-close-issues.yml b/.github/workflows/dev-merge-close-issues.yml new file mode 100644 index 00000000..689a95c9 --- /dev/null +++ b/.github/workflows/dev-merge-close-issues.yml @@ -0,0 +1,97 @@ +name: Close issues on dev merge + +on: + pull_request: + types: [closed] + +permissions: + contents: read + issues: write + pull-requests: read + repository-projects: write + +jobs: + close-linked-issues: + if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev' + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.DECODED_GITHUB_TOKEN || github.token }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_URL: ${{ github.event.pull_request.html_url }} + PROJECT_ID: PVT_kwDOCf1dEc4BUHn- + PROJECT_NUMBER: "3" + STATUS_FIELD_ID: PVTSSF_lADOCf1dEc4BUHn-zhBR8Fk + STATUS_DONE_OPTION_ID: "98236657" + steps: + - name: Extract closing issue references + id: refs + shell: bash + run: | + set -euo pipefail + body=$(jq -r '.pull_request.body // ""' "$GITHUB_EVENT_PATH") + issues=$( + printf '%s\n' "$body" | + perl -0777 -ne 'while (/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:https:\/\/github\.com\/decodedcorp\/decoded\/issues\/)?#?([0-9]+)/ig) { print "$1\n" }' | + sort -n -u + ) + + if [ -z "$issues" ]; then + echo "No closing issue references found in PR body." + echo "issues=" >> "$GITHUB_OUTPUT" + exit 0 + fi + + { + echo "issues<> "$GITHUB_OUTPUT" + + - name: Close issues and mark project Done + if: steps.refs.outputs.issues != '' + shell: bash + run: | + set -euo pipefail + owner="${REPO%%/*}" + repo="${REPO#*/}" + + while IFS= read -r issue; do + [ -z "$issue" ] && continue + + state=$(gh issue view "$issue" --repo "$REPO" --json state --jq '.state') + if [ "$state" = "OPEN" ]; then + gh issue close "$issue" \ + --repo "$REPO" \ + --comment "Closed automatically because PR #${PR_NUMBER} was merged into \`dev\`: ${PR_URL}" + else + echo "Issue #${issue} is already ${state}; skipping close." + fi + + item_id=$( + gh api graphql \ + -f query='query($owner:String!, $repo:String!, $number:Int!) { repository(owner:$owner, name:$repo) { issue(number:$number) { projectItems(first:20) { nodes { id project { ... on ProjectV2 { number } } } } } } }' \ + -f owner="$owner" \ + -f repo="$repo" \ + -F number="$issue" \ + --jq ".data.repository.issue.projectItems.nodes[] | select(.project.number == ${PROJECT_NUMBER}) | .id" | + head -n 1 + ) || true + + if [ -z "$item_id" ]; then + echo "Issue #${issue} is not in project #${PROJECT_NUMBER}; skipping project status update." + continue + fi + + if ! gh api graphql \ + -f query='mutation($project:ID!, $item:ID!, $field:ID!, $option:String!) { updateProjectV2ItemFieldValue(input:{projectId:$project, itemId:$item, fieldId:$field, value:{singleSelectOptionId:$option}}) { projectV2Item { id } } }' \ + -f project="$PROJECT_ID" \ + -f item="$item_id" \ + -f field="$STATUS_FIELD_ID" \ + -f option="$STATUS_DONE_OPTION_ID" >/dev/null; then + echo "::warning::Issue #${issue} was closed, but project status update failed." + continue + fi + + echo "Issue #${issue} closed and project status set to Done." + done <<< "${{ steps.refs.outputs.issues }}" diff --git a/.github/workflows/health-check.yml b/.github/workflows/health-check.yml index 296b6fed..1e47bdfd 100644 --- a/.github/workflows/health-check.yml +++ b/.github/workflows/health-check.yml @@ -12,7 +12,7 @@ jobs: health-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check API health and notify env: diff --git a/.github/workflows/project-pr-status.yml b/.github/workflows/project-pr-status.yml new file mode 100644 index 00000000..04e1f7f9 --- /dev/null +++ b/.github/workflows/project-pr-status.yml @@ -0,0 +1,79 @@ +name: Track PR status in project + +on: + pull_request: + types: [opened, reopened, ready_for_review, converted_to_draft, closed] + +permissions: + contents: read + pull-requests: read + repository-projects: write + +jobs: + update-pr-status: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.DECODED_GITHUB_TOKEN || github.token }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PROJECT_ID: PVT_kwDOCf1dEc4BUHn- + PROJECT_NUMBER: "3" + STATUS_FIELD_ID: PVTSSF_lADOCf1dEc4BUHn-zhBR8Fk + STATUS_IN_PROGRESS_OPTION_ID: "47fc9ee4" + STATUS_DONE_OPTION_ID: "98236657" + steps: + - name: Set PR project status + shell: bash + run: | + set -euo pipefail + owner="${REPO%%/*}" + repo="${REPO#*/}" + + if [ "${{ github.event.action }}" = "closed" ]; then + status_option="$STATUS_DONE_OPTION_ID" + status_name="Done" + else + status_option="$STATUS_IN_PROGRESS_OPTION_ID" + status_name="In Progress" + fi + + pr_data=$( + gh api graphql \ + -f query='query($owner:String!, $repo:String!, $number:Int!) { repository(owner:$owner, name:$repo) { pullRequest(number:$number) { id projectItems(first:20) { nodes { id project { ... on ProjectV2 { number } } } } } } }' \ + -f owner="$owner" \ + -f repo="$repo" \ + -F number="$PR_NUMBER" + ) + + pr_id=$(jq -r '.data.repository.pullRequest.id' <<<"$pr_data") + item_id=$( + jq -r ".data.repository.pullRequest.projectItems.nodes[] | select(.project.number == ${PROJECT_NUMBER}) | .id" <<<"$pr_data" | + head -n 1 + ) + + if [ -z "$item_id" ]; then + item_id=$( + gh api graphql \ + -f query='mutation($project:ID!, $content:ID!) { addProjectV2ItemById(input:{projectId:$project, contentId:$content}) { item { id } } }' \ + -f project="$PROJECT_ID" \ + -f content="$pr_id" \ + --jq '.data.addProjectV2ItemById.item.id' + ) || true + fi + + if [ -z "$item_id" ]; then + echo "::warning::PR #${PR_NUMBER} project item was not found or created; skipping status update." + exit 0 + fi + + if ! gh api graphql \ + -f query='mutation($project:ID!, $item:ID!, $field:ID!, $option:String!) { updateProjectV2ItemFieldValue(input:{projectId:$project, itemId:$item, fieldId:$field, value:{singleSelectOptionId:$option}}) { projectV2Item { id } } }' \ + -f project="$PROJECT_ID" \ + -f item="$item_id" \ + -f field="$STATUS_FIELD_ID" \ + -f option="$status_option" >/dev/null; then + echo "::warning::PR #${PR_NUMBER} project status update failed." + exit 0 + fi + + echo "PR #${PR_NUMBER} project status set to ${status_name}." diff --git a/.github/workflows/vault-dispatch.yml b/.github/workflows/vault-dispatch.yml index 22e4a428..3cdba522 100644 --- a/.github/workflows/vault-dispatch.yml +++ b/.github/workflows/vault-dispatch.yml @@ -4,6 +4,14 @@ on: branches: [main, dev] pull_request: types: [opened, closed] + pull_request_review: + types: [submitted] + issues: + types: [opened, closed, reopened, labeled, assigned] + issue_comment: + types: [created] + release: + types: [published] deployment_status: permissions: @@ -43,6 +51,43 @@ jobs: fi BODY="**PR #${NUM}** ${TITLE} (${TYPE} by ${ACTOR})" ;; + pull_request_review) + NUM=$(jq -r '.pull_request.number' "$GITHUB_EVENT_PATH") + TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH") + STATE=$(jq -r '.review.state' "$GITHUB_EVENT_PATH") + TYPE="pr-review-${STATE}" + BODY="**PR #${NUM}** ${TITLE} (review-${STATE} by ${ACTOR})" + ;; + issues) + NUM=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH") + TITLE=$(jq -r '.issue.title' "$GITHUB_EVENT_PATH") + EXTRA="" + case "$EVENT_ACTION" in + labeled) + LABEL=$(jq -r '.label.name // ""' "$GITHUB_EVENT_PATH") + EXTRA=" [+${LABEL}]" + ;; + assigned) + ASSIGNEE=$(jq -r '.assignee.login // ""' "$GITHUB_EVENT_PATH") + EXTRA=" → ${ASSIGNEE}" + ;; + esac + TYPE="issue-${EVENT_ACTION}" + BODY="**Issue #${NUM}** ${TITLE}${EXTRA} (${TYPE} by ${ACTOR})" + ;; + issue_comment) + NUM=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH") + TITLE=$(jq -r '.issue.title' "$GITHUB_EVENT_PATH") + SNIPPET=$(jq -r '.comment.body // ""' "$GITHUB_EVENT_PATH" | head -1 | head -c 120) + TYPE="issue-comment" + BODY="**Issue #${NUM}** ${TITLE} — comment by ${ACTOR}: ${SNIPPET}" + ;; + release) + TAG=$(jq -r '.release.tag_name // ""' "$GITHUB_EVENT_PATH") + NAME=$(jq -r '.release.name // ""' "$GITHUB_EVENT_PATH") + TYPE="release-${EVENT_ACTION}" + BODY="**Release ${TAG}** ${NAME} (${TYPE} by ${ACTOR})" + ;; deployment_status) STATE=$(jq -r '.deployment_status.state' "$GITHUB_EVENT_PATH") ENV_NAME=$(jq -r '.deployment_status.environment // "unknown"' "$GITHUB_EVENT_PATH") diff --git a/.github/workflows/wiki-lint.yml b/.github/workflows/wiki-lint.yml index 7da0d07f..9726db91 100644 --- a/.github/workflows/wiki-lint.yml +++ b/.github/workflows/wiki-lint.yml @@ -15,7 +15,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: diff --git a/.gitignore b/.gitignore index f971d556..bd281c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,15 @@ packages/ai-server/searxng/settings.yml.new !.omc/ !.omc/project-memory.json +# OMC per-package artifacts (root .omc 정책은 위에서 처리) +packages/*/.omc/ + +# Next.js build backups +packages/*/.next.bak/ + +# Local scratch directory +.scratch/ + # Hybrid harness local artifacts packages/web/.tasks/ packages/web/.handoff/ diff --git a/AGENT.md b/AGENT.md index 97f969e3..5c40c3d5 100644 --- a/AGENT.md +++ b/AGENT.md @@ -1,37 +1,5 @@ -# decoded-app AI 에이전트 맵 +# AGENT.md — 한국어 진입점 (redirect) -> **Purpose**: AI 에이전트가 이 모노레포에서 작업할 때의 **한국어 진입점**입니다. 장문·표는 [`docs/agent/`](docs/agent/)와 [`.planning/codebase/`](.planning/codebase/)에 두었습니다. +본 파일은 `AGENTS.md`로 합쳐졌다. 한국어 작업 메모를 포함한 cross-tool 진입 규칙은 **[`AGENTS.md`](AGENTS.md)** 를 읽는다. -## 필수 진입 문서 - -| 문서 | 역할 | -|------|------| -| **[CLAUDE.md](CLAUDE.md)** | 영문 **맵** (항상 읽기 쉬운 요약, 규칙, `docs/agent` 인덱스) | -| **[docs/agent/README.md](docs/agent/README.md)** | 표·인벤토리 목차 (라우트, API, 훅, 디자인 시스템) | -| **[.planning/codebase/](.planning/codebase/)** | 아키텍처, 스택, 컨벤션, 테스트, 연동 | - -## 작업 유형별로 열 파일 - -| 작업 | 문서 | -|------|------| -| 명령어·패키지 구조·로컬 deps | [docs/agent/monorepo.md](docs/agent/monorepo.md) | -| 웹 라우트·기능 영역 | [docs/agent/web-routes-and-features.md](docs/agent/web-routes-and-features.md) | -| Next.js `app/api/v1` | [docs/agent/api-v1-routes.md](docs/agent/api-v1-routes.md) | -| 훅·스토어·주요 경로 | [docs/agent/web-hooks-and-stores.md](docs/agent/web-hooks-and-stores.md) | -| 디자인 시스템 import·컴포넌트 목록 | [docs/agent/design-system-llm.md](docs/agent/design-system-llm.md) | -| Warehouse 스키마 (ETL·Seed) | [docs/agent/warehouse-schema.md](docs/agent/warehouse-schema.md) | -| Rust API 서버 (`api-server`) | [packages/api-server/AGENT.md](packages/api-server/AGENT.md) | - -## 반드시 지킬 것 - -1. **패키지 매니저**: **bun** (`bun run`, `bun add`). yarn/npm 아님. -2. **상세 표의 SSOT**: `docs/agent/` — CLAUDE.md에는 링크만 있음. -3. **디자인 시스템**: 새 UI는 [docs/design-system/](docs/design-system/) 및 `docs/agent/design-system-llm.md` 확인. -4. **Supabase 쿼리**: 웹은 `packages/web/lib/supabase/queries/` 패턴 유지. - -## 기타 - -- 디자인 시스템 토큰·UI 가이드: [docs/design-system/README.md](docs/design-system/README.md) -- 문서 인덱스: [docs/README.md](docs/README.md) - -**마지막 업데이트**: 2026-04-02 +(파일 자체는 외부 link 호환을 위해 유지.) diff --git a/AGENTS.md b/AGENTS.md index bab9ebf4..5305cef7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,85 @@ -# Agents +# AGENTS.md — Cross-tool agent entry point -This repository uses **[CLAUDE.md](CLAUDE.md)** as the short entry map (rules and links). +이 파일은 monorepo의 **canonical agent 진입점**이다. 모든 LLM 도구(Codex / Cursor / Warp / Claude Code / Gemini CLI)는 작업 시작 시 이 파일을 **가장 먼저** 읽는다. -- **Inventories** (routes, `/api/v1/*`, hooks, design system): [`docs/agent/README.md`](docs/agent/README.md) -- **Korean agent map**: [`AGENT.md`](AGENT.md) +각 도구별 overlay는 본 파일을 보강만 한다 — 본 파일과 충돌하면 본 파일이 우선한다. + +## 도구별 overlay 매핑 + +| 도구 | Overlay 파일 | 역할 | +| ----------------- | ---------------------- | --------------------------------------------------------------- | +| Claude Code | `CLAUDE.md` | Claude Code 전용 routing 규칙(skill, OMC, GSD, Superpowers) | +| Warp Terminal | `WARP.md` | Warp 워크플로우 명령·자동화 | +| Cursor | `.cursor/rules/*.mdc` | Cursor 네이티브 규칙(globs · description) | +| Codex CLI | `~/.codex/config.toml` | Hybrid harness Layer 2/2.5 (codex exec --profile fast / strict) | +| 한국어 진입(공통) | `AGENT.md` | `AGENTS.md` 1줄 redirect | + +## 읽기 우선순위 + +1. **AGENTS.md** (이 파일) — cross-tool 공통 +2. **`docs/wiki/schema/ownership-matrix.md`** — 정보 카테고리별 정본/pointer 매핑 SSOT +3. **`docs/agent/README.md`** — agent 참조 인벤토리 목차 (라우트, API, 훅, 디자인 시스템) +4. **`.planning/codebase/*`** — 스택·아키텍처·컨벤션·테스트 +5. 도구별 overlay (`CLAUDE.md`, `WARP.md`, `.cursor/rules`, ...) + +## Routing SSOT + +정보 카테고리별로 어디를 정본으로 보아야 하는지는 **`docs/wiki/schema/ownership-matrix.md`** 하나가 결정한다. 다른 파일은 그 행을 가리키기만 한다 — 표를 복제하지 않는다. + +대표 항목: + +- Stack/버전 → `.planning/codebase/STACK.md` +- 코딩 컨벤션 → `docs/wiki/schema/conventions.md` +- Skills inventory → **`docs/agent/skills.md`** +- ADR → vault `Architecture/adr/` (monorepo 측 stub: `docs/adr/index.md`) +- Harness wiki → `docs/wiki/wiki/harness/*` +- Cross-tool agent entry → 이 파일 + +전체 매트릭스: [`docs/wiki/schema/ownership-matrix.md`](docs/wiki/schema/ownership-matrix.md) + +## Documentation 위치 (decoded vs decoded-docs) + +| 종류 | 위치 | +| ----------------------------------------- | --------------------------------------------------------------------------------- | +| 코드 · LLM 라우팅 · agent 인벤토리 · spec | **이 레포** (`docs/agent/`, `docs/superpowers/`, `AGENTS.md`, 등) | +| 회의 · 결정 · 기획 · 아키텍처 · 회고 | **[decoded-docs vault](https://github.com/decodedcorp/decoded-docs)** (별도 레포) | + +전체 sync policy: [decoded-docs/Guides/sync-policy.md](https://github.com/decodedcorp/decoded-docs/blob/main/Guides/sync-policy.md) + +회사 지식 질의(예: "지난주 결정 뭐였어?")는 Telegram 매니저 agent에게 — vault 기반 답변. + +## 작업 유형별 진입 파일 + +| 작업 | 문서 | +| ------------------------------ | --------------------------------------- | +| 명령어·패키지 구조·로컬 deps | `docs/agent/monorepo.md` | +| 웹 라우트·기능 영역 | `docs/agent/web-routes-and-features.md` | +| Next.js `app/api/v1` | `docs/agent/api-v1-routes.md` | +| 훅·스토어 | `docs/agent/web-hooks-and-stores.md` | +| 디자인 시스템 import·컴포넌트 | `docs/agent/design-system-llm.md` | +| Skills · slash commands | `docs/agent/skills.md` | +| Setup (issue tracker · triage) | `docs/agent/setup/` | +| Rust API crate | `packages/api-server/AGENT.md` | +| 아키텍처·컨벤션·스택 심층 | `.planning/codebase/` | +| Harness 세션 규율 | `docs/wiki/wiki/harness/*` | + +## 반드시 지킬 것 + +1. **패키지 매니저**: **bun** (`bun run`, `bun add`). yarn/npm 아님. +2. **상세 표의 SSOT**: `docs/agent/` + `ownership-matrix.md` — overlay 파일에는 링크만. +3. **Git workflow**: `feature/*` → `dev` → `main`. `main` 직접 push 금지. 상세는 `docs/GIT-WORKFLOW.md`. +4. **Conventional Commits** 준수. +5. **Skill 라우팅**: 사용자 요청이 skill과 매치되면 Skill tool을 FIRST action으로. 직접 답하지 않는다. 상세는 `docs/agent/skills.md`. +6. **Generated 코드 금지**: `packages/web/lib/api/generated/`는 Orval 자동 생성, 수동 편집 금지. +7. **Vault 결정**: 회의·결정·아키텍처·회고는 monorepo에 두지 않는다 — 항상 vault. + +## Harness Workflow + +| Tool | Role | When | +| ----------- | -------------------- | ------------------- | +| gstack | 기획/리뷰/QA/배포 | Think → Plan → Ship | +| Superpowers | TDD, 코드 품질 강제 | Build (구현) | +| OMC | Claude + Gemini 듀얼 | 대규모 작업 보조 | +| GSD quick | 원자적 단발 패치 | 유지보수 | + + diff --git a/CLAUDE.md b/CLAUDE.md index be1c8813..b8babc78 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,201 +1,55 @@ -# decoded-monorepo Development Guidelines +# CLAUDE.md — Claude Code overlay -짧은 **맵**입니다. 라우트/API/훅/디자인 시스템 **표·인벤토리**는 [`docs/agent/`](docs/agent/)에 두었습니다. 해당 작업을 할 때는 항상 해당 파일을 연 뒤 진행합니다. +> 본 파일은 **Claude Code 전용 overlay**다. 공통 cross-tool 진입 규칙은 **`AGENTS.md`를 먼저 읽고**, 본 파일은 Claude Code-specific 보강만 담는다. -## Documentation 위치 (decoded vs decoded-docs) +## Read order -| 종류 | 위치 | -|------|------| -| 코드 · LLM 라우팅 · agent 인벤토리 · spec | **이 레포** (`docs/agent/`, `docs/superpowers/`, `CLAUDE.md`, 등) | -| 회의 · 결정 · 기획 · 아키텍처 · 회고 | **[decoded-docs vault](https://github.com/decodedcorp/decoded-docs)** (별도 레포) | +1. `AGENTS.md` (canonical, cross-tool) +2. `docs/wiki/schema/ownership-matrix.md` (routing SSOT) +3. 본 파일 (Claude Code overlay) +4. `docs/agent/README.md` (참조 인벤토리) -전체 sync policy: [decoded-docs/Guides/sync-policy.md](https://github.com/decodedcorp/decoded-docs/blob/main/Guides/sync-policy.md) +## Documentation 위치 -회사 지식 질의 (예: "지난주 결정 뭐였어?", "에디토리얼 매거진 방향성") 는 Telegram의 매니저 agent 에게 물어보면 vault 기반으로 답변합니다. +코드/LLM 라우팅/agent 인벤토리/spec은 monorepo. 회의/결정/아키텍처/회고는 vault — 자세한 사항은 `AGENTS.md` 참조. -## Overview +## Skill routing (Claude Code) -Monorepo for the decoded platform — image/item discovery and curation with behavioral intelligence, editorial magazine system (news-referenced), virtual try-on (VTON), admin dashboard (seed pipeline, entity management, monitoring), SEO infrastructure, and design system (v2.0). AI-powered item detection (Ollama vision auto-tagging), social actions (like/save/comment/follow), personalization, rankings, collection/studio. +사용자 요청이 가용 skill과 매치되면 Skill tool을 **FIRST action**으로 호출한다. 표/세부 매핑은 **`docs/agent/skills.md`**가 정본. -## Monorepo (summary) +핵심 라우팅 키워드: -- **Root**: bun workspaces + Turborepo (`package.json`, `turbo.json`, `bunfig.toml`) -- **`packages/web`**: Next.js 16 app (main frontend) -- **`packages/shared`**: Shared types, hooks, Supabase queries -- **`packages/mobile`**: Expo app -- **`packages/api-server`**: Rust/Axum (Cargo; not a bun workspace member) -- **`packages/ai-server`**: Python AI / gRPC (`uv`) +- "is this worth building" / brainstorming → `office-hours` +- bug / 500 → `investigate` +- ship / deploy / PR → `ship` +- QA → `qa` +- code review → `review` +- design system → `design-consultation` +- save progress → `checkpoint` -**상세 트리·명령어·로컬 deps·포트**: [`docs/agent/monorepo.md`](docs/agent/monorepo.md) -**기술 스택·버전**: [`.planning/codebase/STACK.md`](.planning/codebase/STACK.md) +## Commit discipline (Claude Code) -## Agent reference (`docs/agent/`) +- 작업 완료 시 검증 후 관련 파일만 선별해 커밋. 커밋 금지는 push/merge/PR 병합 금지와 구분 — 로컬 커밋은 기본 완료 조건. +- 더러운 워크트리에서는 unrelated 변경 건드리지 말고 `git add `만. +- review-only / plan-only 모드, human checkpoint 필요 작업이면 커밋하지 않고 이유 남김. -### Topic routing (1순위: `*-summary.md` → canonical) +## Worktree -Topic 질의(아키텍처 / API / DB / 디자인 시스템 / AI playbook)는 **먼저 summary**를 읽고, summary가 가리키는 canonical(`.planning/codebase/*`, `docs/architecture/`, `docs/api/`, `docs/database/`, `docs/ai-playbook/`) 으로 내려간다. canonical을 먼저 열면 설계 의도와 스냅샷을 혼동한다. +세션 격리는 `superpowers:using-git-worktrees` 사용. `.claude/worktrees/`는 `.gitignore`됨. -| Topic | Summary (먼저 읽기) | -| ------------- | ---------------------------------------------------------------------------- | -| architecture | [`docs/agent/architecture-summary.md`](docs/agent/architecture-summary.md) | -| api | [`docs/agent/api-summary.md`](docs/agent/api-summary.md) | -| database | [`docs/agent/database-summary.md`](docs/agent/database-summary.md) | -| design-system | [`docs/agent/design-system-summary.md`](docs/agent/design-system-summary.md) | -| ai-playbook | [`docs/agent/ai-playbook-summary.md`](docs/agent/ai-playbook-summary.md) | - -### 참조 인벤토리 (표·라우트·훅) - -| 문서 | 용도 | -| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -| [`docs/agent/README.md`](docs/agent/README.md) | 목차·언제 무엇을 읽을지 | -| [`docs/database/operating-model.md`](docs/database/operating-model.md) | **DB 운영 모델 단일 진입점** — 영역/시스템 매트릭스, "어디 추가하나" 결정 트리, drift 회피 | -| [`docs/agent/environments.md`](docs/agent/environments.md) | **env matrix (dev=local / prod=Cloud Supabase)** | -| [`docs/DATABASE-MIGRATIONS.md`](docs/DATABASE-MIGRATIONS.md) | **DB 마이그레이션 SOT / 워크플로우** | -| [`docs/agent/staging.md`](docs/agent/staging.md) | staging 정의 (현재 없음) | -| [`docs/agent/web-routes-and-features.md`](docs/agent/web-routes-and-features.md) | 웹 라우트·기능 영역 | -| [`docs/agent/api-v1-routes.md`](docs/agent/api-v1-routes.md) | Next.js `/api/v1/*` 표 | -| [`docs/agent/web-hooks-and-stores.md`](docs/agent/web-hooks-and-stores.md) | 훅·스토어·주요 경로 | -| [`docs/agent/design-system-llm.md`](docs/agent/design-system-llm.md) | 디자인 시스템 import·컴포넌트 목록 | -| [`docs/architecture/assets-project.md`](docs/architecture/assets-project.md) | **assets Supabase 프로젝트 (#333)** — 파이프라인 스테이징 + verify 플로우 | -| [`docs/agent/verify-flow-qa.md`](docs/agent/verify-flow-qa.md) | verify 엔드포인트 수동 QA 체크리스트 | -| [`docs/agent/version-harness.md`](docs/agent/version-harness.md) | **Backend 버전 lockstep harness** — PR `bump:*` 라벨 + main 머지 시 자동 bump/tag | -| [`docs/agent/warehouse-schema.md`](docs/agent/warehouse-schema.md) | (DEPRECATED, #333) historical warehouse 스키마 reference | -| [`packages/api-server/AGENT.md`](packages/api-server/AGENT.md) | Rust API 크레이트 전용 | - -### Harness knowledge (세션 규율) - -- [`docs/wiki/wiki/harness/claude-code.md`](docs/wiki/wiki/harness/claude-code.md) — Claude Code 세션·worktree·Draft PR·Auto Mode -- [`docs/wiki/wiki/harness/session-discipline.md`](docs/wiki/wiki/harness/session-discipline.md) — 세션 분리, 상태 격리, cwd 리셋 -- [`docs/wiki/wiki/harness/commit-protocol.md`](docs/wiki/wiki/harness/commit-protocol.md) — Conventional Commits, fmt/check, PR 범위 -- [`docs/wiki/wiki/harness/review-flow.md`](docs/wiki/wiki/harness/review-flow.md) — 리뷰/검증 레인 분리, self-approve 금지 - -## Conventions (SSOT) - -상세 컨벤션은 [docs/wiki/schema/conventions.md](docs/wiki/schema/conventions.md)를 참조한다. 이 파일은 agent routing과 docs 맵만 담는다. - -주요 규칙 요약: - -- bun + Turborepo -- Conventional Commits -- Next.js 16은 `proxy.ts` 사용 -- `packages/web/lib/api/generated/`는 자동 생성, 수동 편집 금지 - -## Git workflow - -요약: `feature/*` → `dev` → `main` 플로우. `main` 직접 push 금지, `dev`→`main` PR 머지만 허용. 긴급 시 `hotfix/*`→`main` 예외. 상세는 **[docs/GIT-WORKFLOW.md](docs/GIT-WORKFLOW.md)**. - -## Commit discipline - -- 사용자가 코드/문서 수정을 요청하고 작업이 완료되면, 검증 후 관련 파일만 선별해 커밋한다. -- 커밋 금지는 `push`, `merge`, PR 병합 금지와 구분한다. 로컬 커밋은 기본 완료 조건이다. -- 더러운 워크트리에서는 unrelated 변경을 건드리지 말고, 이번 작업 파일만 `git add `로 스테이징한다. -- 커밋하지 않아야 하는 명시 요청, review-only/plan-only 모드, 또는 human checkpoint가 필요한 위험 작업이면 커밋하지 않고 이유를 남긴다. - -## Codebase documentation - -| 문서 | 내용 | -| ----------------------------------------------------- | ----------------------------- | -| [STACK.md](.planning/codebase/STACK.md) | 기술 스택, 의존성, 설정 | -| [ARCHITECTURE.md](.planning/codebase/ARCHITECTURE.md) | 아키텍처, 레이어, 데이터 흐름 | -| [STRUCTURE.md](.planning/codebase/STRUCTURE.md) | 디렉토리 구조 | -| [CONVENTIONS.md](.planning/codebase/CONVENTIONS.md) | 코딩 컨벤션 | -| [TESTING.md](.planning/codebase/TESTING.md) | 테스트 | -| [INTEGRATIONS.md](.planning/codebase/INTEGRATIONS.md) | 외부 연동 | -| [CONCERNS.md](.planning/codebase/CONCERNS.md) | 기술 부채 | - -## GSD Workflow +## GSD Workflow (quick reference) ```bash +/gsd:quick # 빠른 작업 /gsd:progress # 전체 진행 상황 -/gsd:discuss-phase N # 페이즈 N 논의 -/gsd:plan-phase N # 페이즈 N 계획 -/gsd:execute-phase N # 페이즈 N 실행 -/gsd:verify-work # 작업 검증 /gsd:help # 명령어 목록 -/gsd:quick # 빠른 작업 ``` -## Harness Workflow - -| Tool | Role | When | -| ----------- | -------------------- | ------------------- | -| gstack | 기획/리뷰/QA/배포 | Think → Plan → Ship | -| Superpowers | TDD, 코드 품질 강제 | Build (구현) | -| OMC | Claude + Gemini 듀얼 | 대규모 작업 보조 | -| GSD quick | 원자적 단발 패치 | 유지보수 | - -## Documentation - -- [docs/README.md](docs/README.md) — 문서 인덱스 -- [docs/GIT-WORKFLOW.md](docs/GIT-WORKFLOW.md) — 브랜치, 커밋, PR -- [docs/agent/](docs/agent/) — 에이전트용 참조 (표·인벤토리) -- [docs/design-system/](docs/design-system/) — 디자인 토큰 -- [.planning/](.planning/) — GSD 아티팩트 -- docs/adr/, docs/api/, docs/ai-playbook/ - -## gstack (Software Factory) - -Use gstack slash commands for the sprint workflow: **Think → Plan → Build → Review → Test → Ship → Reflect**. - -### Available Skills - -| Phase | Command | Role | -| ------- | -------------------------------------------- | ----------------------------------------- | -| Think | `/office-hours` | YC Office Hours — reframe the product | -| Plan | `/plan-ceo-review` | CEO — rethink scope | -| Plan | `/plan-eng-review` | Eng Manager — lock architecture | -| Plan | `/plan-design-review` | Designer — rate & improve design | -| Plan | `/design-consultation` | Design Partner — build design system | -| Plan | `/autoplan` | Auto-review pipeline: CEO → design → eng | -| Build | `/browse` | Browser automation (Playwright) | -| Review | `/review` | Staff Engineer — find production bugs | -| Review | `/design-review` | Designer Who Codes — audit + fix | -| Review | `/cso` | Security Officer — OWASP + STRIDE audit | -| Test | `/qa` | QA Lead — real browser testing + auto-fix | -| Test | `/qa-only` | QA report only (no fixes) | -| Ship | `/ship` | Release Engineer — test, PR, ship | -| Ship | `/land-and-deploy` | Merge → deploy → canary verify | -| Monitor | `/canary` | Post-deploy monitoring | -| Monitor | `/benchmark` | Performance regression detection | -| Debug | `/investigate` | Systematic root-cause debugging | -| Reflect | `/retro` | Sprint retrospective | -| Docs | `/document-release` | Post-ship doc updates | -| Safety | `/careful`, `/freeze`, `/guard`, `/unfreeze` | Destructive op protection | -| Setup | `/setup-deploy`, `/setup-browser-cookies` | One-time config | -| Utility | `/gstack-upgrade` | Update gstack | -| Utility | `/codex` | Multi-AI second opinion | -| Utility | `/connect-chrome` | Connect Chrome for browsing | - -### Rules - -- Use `/browse` for all web browsing — never use `mcp__claude-in-chrome__*` tools -- If gstack skills aren't working, run `cd ~/.claude/skills/gstack && ./setup` -- Follow the sprint order: Think → Plan → Build → Review → Test → Ship → Reflect - - - -## Skill routing - -When the user's request matches an available skill, ALWAYS invoke it using the Skill -tool as your FIRST action. Do NOT answer directly, do NOT use other tools first. -The skill has specialized workflows that produce better results than ad-hoc answers. - -Key routing rules: - -- Product ideas, "is this worth building", brainstorming → invoke office-hours -- Bugs, errors, "why is this broken", 500 errors → invoke investigate -- Ship, deploy, push, create PR → invoke ship -- QA, test the site, find bugs → invoke qa -- Code review, check my diff → invoke review -- Update docs after shipping → invoke document-release -- Weekly retro → invoke retro -- Design system, brand → invoke design-consultation -- Visual audit, design polish → invoke design-review -- Architecture review → invoke plan-eng-review -- Save progress, checkpoint, resume → invoke checkpoint -- Code quality, health check → invoke health +자세한 GSD/gstack/Superpowers 경계는 `AGENTS.md`의 Harness Workflow 표 참조. + + -- [Antigravity Rules](file:///Users/kiyeol/development/decoded/decoded-app/.antigravity/rules.md) - Autonomous execution policy and language preferences. +- [Antigravity Rules](file:///Users/kiyeol/development/decoded/decoded-app/.antigravity/rules.md) — Autonomous execution policy and language preferences. diff --git a/WARP.md b/WARP.md deleted file mode 120000 index 681311eb..00000000 --- a/WARP.md +++ /dev/null @@ -1 +0,0 @@ -CLAUDE.md \ No newline at end of file diff --git a/WARP.md b/WARP.md new file mode 100644 index 00000000..3d920002 --- /dev/null +++ b/WARP.md @@ -0,0 +1,42 @@ +# WARP.md — Warp Terminal overlay + +> 본 파일은 **Warp Terminal 전용 overlay**다. 공통 cross-tool 진입 규칙은 **`AGENTS.md`를 먼저 읽고**, 본 파일은 Warp-specific 보강(워크플로우 명령·자동화)만 담는다. +> +> Read order: `AGENTS.md` → `docs/wiki/schema/ownership-matrix.md` → 본 파일. + +## Documentation 위치 + +→ `AGENTS.md` 참조. + +## Harness Workflow + +→ `AGENTS.md` 참조. + +## Skill routing + +→ `AGENTS.md` + `docs/agent/skills.md` 참조. + +## Warp-specific 보강 + +Warp Terminal에서 본 monorepo를 운영할 때만 적용되는 가이드. + +### Workflow 명령 (Warp Workflows) + +Warp의 [Workflows](https://docs.warp.dev/features/workflows) 기능으로 자주 쓰는 monorepo 명령을 등록해 두면 효율적이다. + +| Workflow 이름 | 명령 | 용도 | +| ------------------ | ------------------------ | ------------------------ | +| `decoded:dev` | `bun run dev` | 로컬 dev 서버 기동 | +| `decoded:gen-api` | `bun run generate:api` | Orval+Zod 타입 재생성 | +| `decoded:db-types` | `bun run gen:types` | Supabase types.ts 재생성 | +| `decoded:turbo` | `bun run build` | Turborepo 빌드 | +| `decoded:wiki` | `/oh-my-claudecode:wiki` | LLM Wiki 진입 | + +### Terminal automation tip + +- Warp의 AI Command 검색은 `#` 트리거로 호출. monorepo 컨벤션상 `bun`/`turbo`/`cargo`/`uv` 4종이 자주 등장하므로 별칭 등록 권장. +- `git worktree` 사용 시 Warp 탭별로 다른 worktree를 띄우면 세션 격리에 유리하다. 세션 규율은 `docs/wiki/wiki/harness/session-discipline.md` 참조. + +### 자동화 스크립트 + +monorepo 루트의 `scripts/`에 정의된 자동화는 Warp Workflows로 wrap해도 무방하다. 단, 위험 작업(DB 변경, 대량 삭제)은 워크플로우로 등록해도 confirmation prompt를 유지할 것. diff --git a/bun.lock b/bun.lock index 5a8579b8..9b1bdf95 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "decoded-monorepo", @@ -79,6 +78,7 @@ "@chenglou/pretext": "^0.0.4", "@decoded/shared": "workspace:*", "@gsap/react": "^2.1.2", + "@next/third-parties": "^16.2.1", "@radix-ui/react-slot": "^1.2.4", "@sentry/nextjs": "^10.47.0", "@splinetool/react-spline": "^4.1.0", @@ -649,6 +649,8 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg=="], + "@next/third-parties": ["@next/third-parties@16.2.6", "", { "dependencies": { "third-party-capital": "1.0.20" }, "peerDependencies": { "next": "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0-beta.0", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0" } }, "sha512-PDPIPVj1NX6Taxsl8OJteAUJ7iwR+QrokwWig68eh0cOmuNjC6MBL+ZzBjO8Bv0n/HOSqjGArZpM5KMSUxm+MQ=="], + "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], @@ -2941,6 +2943,8 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "third-party-capital": ["third-party-capital@1.0.20", "", {}, "sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA=="], + "three": ["three@0.183.2", "", {}, "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ=="], "throat": ["throat@5.0.0", "", {}, "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA=="], diff --git a/docs/GIT-WORKFLOW.md b/docs/GIT-WORKFLOW.md index 6e21bd4a..2ecc5abb 100644 --- a/docs/GIT-WORKFLOW.md +++ b/docs/GIT-WORKFLOW.md @@ -124,10 +124,12 @@ hotfix/* ──PR──▶ main (긴급 시에만) 1. `dev`에서 작업 브랜치 생성: `git checkout -b feat/-xxx dev` 2. **즉시 Draft PR 생성** — 프로젝트 보드가 자동으로 **In Progress** 전환 3. 작업 완료 → Draft 해제 → 리뷰 요청 -4. 리뷰 통과 후 `dev`에 머지 → 프로젝트 보드 자동 **Done** + 이슈 close +4. 리뷰 통과 후 `dev`에 머지 → `.github/workflows/dev-merge-close-issues.yml` 이 연결 이슈 close + 프로젝트 보드 **Done** 처리 5. 릴리스 준비 시 `dev` → `main` PR 생성 6. CI 체크 통과 + 리뷰 후 `main`에 머지 → Vercel 자동 배포 +완료 기준은 `dev` 머지다. `main`은 GitHub default branch라 `Closes #N` native auto-close 대상이지만, 팀 운영에서는 `dev` 머지 이후 `main` 반영이 필수 흐름이므로 feature/bug/docs 이슈는 `dev` 머지 시 닫는다. + ## 프로젝트 보드 자동 추적 [decoded-monorepo 프로젝트 #3](https://github.com/orgs/decodedcorp/projects/3)의 활성 자동화: @@ -136,13 +138,32 @@ hotfix/* ──PR──▶ main (긴급 시에만) |--------|------| | 신규 이슈/PR 생성 | Todo로 자동 추가 | | **PR이 이슈에 링크됨** (`Closes #N`) | **In Progress** | -| PR 머지 | Done + 이슈 자동 close | +| PR opened/reopened/ready_for_review | `.github/workflows/project-pr-status.yml` 이 PR item을 **In Progress** 보정 | +| PR closed | `.github/workflows/project-pr-status.yml` 이 PR item을 **Done** 보정 | +| `dev` 대상 PR 머지 | `.github/workflows/dev-merge-close-issues.yml` 이 `Closes #N` 연결 이슈 close + Done 보정 | ### 중요 - **브랜치 생성만으로는 전환 안 됨** — Draft PR 필요 - 브랜치 이름에 이슈 번호 포함 권장: `feat/27-follow-api` - 리뷰 전이라도 Draft PR을 먼저 올려 진행 가시화 +- `decoded`의 default branch는 `main`이므로 GitHub native `Closes #N`만으로는 `dev` 머지 시 이슈가 자동 close되지 않는다. `dev` 머지 close는 `.github/workflows/dev-merge-close-issues.yml` 이 담당한다. +- Project v2 상태 보정까지 동작하려면 repository secret `DECODED_GITHUB_TOKEN`이 필요하다. 이 토큰은 `decoded` issue/PR 읽기, issue close, org Project #3 item/status 쓰기 권한을 가져야 한다. secret이 없으면 workflow는 `GITHUB_TOKEN`으로 시도하지만 org Project 업데이트는 실패할 수 있다. +- GitHub Actions workflow 활성화 기준은 default branch(`main`) 반영이다. 이 변경은 팀 흐름대로 `dev`에 먼저 머지하되, 자동화가 실제 기준선으로 안정 동작하는 시점은 `dev` 이후 `main`까지 반영된 뒤다. +- 자동화 도입 이전에 `dev`로 머지된 PR은 수동 백필 대상이다. `Closes/Fixes/Resolves #N`가 있는 merged PR을 감사해 열린 이슈가 남아 있으면 PR 번호를 남기고 close한다. + +### Dev merge close 감사 + +정기적으로 다음 조건을 확인한다. + +1. 대상: `base=dev`, `state=merged` PR +2. PR 본문에 `Closes #N`, `Fixes #N`, `Resolves #N`가 있음 +3. 연결 이슈가 아직 `OPEN` +4. 실제 완료 기준이 `dev` 머지라면 이슈에 머지 PR 번호를 코멘트하고 close + +`refs #N`는 단순 참조이므로 close 대상이 아니다. + +2026-05-21 백필 결과: 자동화 도입 전 누락된 15개 이슈를 닫았고, 재검사에서 closing reference가 남긴 열린 이슈는 0건이다. `refs #518`는 문서 마이그레이션 장기 트래킹이라 열린 상태로 유지한다. ### 숏컷: `scripts/start-issue.sh` diff --git a/docs/_archive/README.md b/docs/_archive/README.md new file mode 100644 index 00000000..3ee62feb --- /dev/null +++ b/docs/_archive/README.md @@ -0,0 +1,35 @@ +--- +title: Archived docs +owner: human +status: approved +updated: 2026-05-21 +tags: [archive] +--- + +# `docs/_archive/` + +Deprecated 또는 superseded된 문서의 보관 디렉터리. 외부 link 호환을 위해 삭제하지 않고 여기로 이동한다. + +## 정책 + +- 새로 archive되는 파일의 frontmatter `status: archived`를 명시한다. +- archive된 파일은 `docs/agent/README.md` 인벤토리에서 제거한다. +- 본 디렉터리는 build/lint/index에서 제외된다(필요 시 `.gitignore` 또는 lint exclude 추가). + +## 현재 archived 항목 + +| 파일 | 사유 | Archive 일자 | +| -------------------------------------- | ----------------------------------------------------------------------------------- | ------------ | +| `warehouse-schema.md` | DEPRECATED (#333 — assets project로 대체) | 2026-05-21 | +| `post-centric-refactoring-summary.md` | 2025-12-18 완료된 마이그레이션 회고 snapshot (#561) | 2026-05-21 | +| `home-sections-backend-feasibility.md` | MVP 런칭 단계 feasibility 분석 — 이후 home 리뉴얼 진행으로 superseded (#561) | 2026-05-21 | +| `backend-frontend-status.md` | 2026-02-08 시점 상태 snapshot — `.planning/codebase/`와 `docs/agent/`로 대체 (#561) | 2026-05-21 | +| `qa-screenshots-2026-Q1/` | 2026 Q1 시각 QA 출력 (40 PNG + DIFFERENCES.md + README, `visual-qa.spec.ts`) | 2026-05-21 | + +## QA screenshots 분기 정책 + +`packages/web/tests/visual-qa.spec.ts`는 `docs/qa-screenshots/`로 출력한다. 분기 종료 시 현재 PNG와 비교 산출물(`DIFFERENCES.md`, `README.md`)을 `docs/_archive/qa-screenshots-YYYY-QN/`로 이동한다. + +- 디렉토리 명: `qa-screenshots-YYYY-QN` (예: `qa-screenshots-2026-Q1`, `qa-screenshots-2026-Q2`) +- `docs/qa-screenshots/`는 비워두지 않고 다음 분기 시작과 함께 새 README + 새 출력으로 다시 채워진다. +- 분기 cutoff: 매 분기 마지막 주 (Q1=3월, Q2=6월, Q3=9월, Q4=12월). diff --git a/docs/_archive/backend-frontend-status.md b/docs/_archive/backend-frontend-status.md new file mode 100644 index 00000000..0a39b927 --- /dev/null +++ b/docs/_archive/backend-frontend-status.md @@ -0,0 +1,298 @@ +--- +title: DECODED 백엔드-프론트엔드 구현 현황 (archived) +owner: human +status: archived +updated: 2026-05-21 +tags: [archive] +related: + - docs/_archive/README.md +--- + +# DECODED 백엔드-프론트엔드 구현 현황 및 UI 기능 가능 여부 + +> **Archived 2026-05-21** (#561). 2026-02-08 시점 상태 snapshot — `.planning/codebase/`와 `docs/agent/`로 대체. + +**작성일:** 2026-02-08 +**분석 대상:** `decoded-api/` (Rust/Axum 백엔드) ↔ `decoded-app/` (Next.js 프론트엔드) + +--- + +## 요약 + +| 구분 | 수치 | +| ----------------------------------------- | ------------------- | +| 백엔드 총 API 엔드포인트 | **78개** | +| 프론트엔드에서 연동 완료 | **26개** (33%) | +| 프론트엔드 API 클라이언트 존재하나 미사용 | **2개** | +| 프론트엔드 미연동 (백엔드만 존재) | **50개** (64%) | +| Supabase 직접 조회 (백엔드 우회) | 홈/이미지 읽기 계열 | + +--- + +## 1. 완전 연동 (백엔드 API + 프론트엔드 UI 모두 구현) + +### 1.1 Posts (게시물) + +| 상태 | 백엔드 엔드포인트 | 프론트엔드 위치 | UI 기능 | +| :----------------: | ----------------------------------- | --------------------------------------------- | ------------------------------- | +| :white_check_mark: | `GET /api/v1/posts` | `lib/api/posts.ts` → `useInfinitePosts` | Explore 그리드, Feed 무한스크롤 | +| :white_check_mark: | `GET /api/v1/posts/{post_id}` | `app/api/v1/posts/[postId]/route.ts` | Post 상세 페이지 | +| :white_check_mark: | `POST /api/v1/posts` | `lib/api/posts.ts` → `createPost` | 게시물 생성 (Request 플로우) | +| :white_check_mark: | `POST /api/v1/posts/with-solutions` | `lib/api/posts.ts` → `createPostWithSolution` | 솔루션 포함 게시물 생성 | +| :white_check_mark: | `POST /api/v1/posts/upload` | `lib/api/posts.ts` → `uploadImage` | 이미지 업로드 (DropZone) | +| :white_check_mark: | `POST /api/v1/posts/analyze` | `lib/api/posts.ts` → `analyzeImage` | AI 이미지 분석 | +| :white_check_mark: | `PATCH /api/v1/posts/{post_id}` | `lib/api/posts.ts` → `updatePost` | 게시물 수정 | +| :white_check_mark: | `DELETE /api/v1/posts/{post_id}` | `lib/api/posts.ts` → `deletePost` | 게시물 삭제 | + +### 1.2 Users (사용자) + +| 상태 | 백엔드 엔드포인트 | 프론트엔드 위치 | UI 기능 | +| :----------------: | --------------------------------- | ---------------------------------------- | ----------------------------------------- | +| :white_check_mark: | `GET /api/v1/users/me` | `lib/api/users.ts` → `useMe` | 프로필 페이지 내 정보 표시 | +| :white_check_mark: | `PATCH /api/v1/users/me` | `lib/api/users.ts` → `useUpdateProfile` | 프로필 수정 모달 | +| :white_check_mark: | `GET /api/v1/users/me/stats` | `lib/api/users.ts` → `useUserStats` | 프로필 통계 카드 | +| :white_check_mark: | `GET /api/v1/users/me/activities` | `lib/api/users.ts` → `useUserActivities` | 프로필 활동 탭 (Hook 존재, **UI 미완성**) | +| :white_check_mark: | `GET /api/v1/users/{user_id}` | `lib/api/users.ts` → `useUser` | 타 사용자 프로필 조회 | + +### 1.3 Categories (카테고리) + +| 상태 | 백엔드 엔드포인트 | 프론트엔드 위치 | UI 기능 | +| :----------------: | ------------------------ | ----------------------------------------- | --------------- | +| :white_check_mark: | `GET /api/v1/categories` | `lib/api/categories.ts` → `getCategories` | 카테고리 필터링 | + +### 1.4 Spots (스팟) + +| 상태 | 백엔드 엔드포인트 | 프론트엔드 위치 | UI 기능 | +| :----------------: | ------------------------------------ | --------------------------------- | ----------------------------- | +| :white_check_mark: | `GET /api/v1/posts/{post_id}/spots` | `lib/api/spots.ts` → `fetchSpots` | Post 상세 내 아이템 스팟 표시 | +| :white_check_mark: | `POST /api/v1/posts/{post_id}/spots` | `lib/api/spots.ts` → `createSpot` | 스팟 생성 | +| :white_check_mark: | `PATCH /api/v1/spots/{spot_id}` | `lib/api/spots.ts` → `updateSpot` | 스팟 수정 | +| :white_check_mark: | `DELETE /api/v1/spots/{spot_id}` | `lib/api/spots.ts` → `deleteSpot` | 스팟 삭제 | + +### 1.5 Solutions (솔루션) + +| 상태 | 백엔드 엔드포인트 | 프론트엔드 위치 | UI 기능 | +| :----------------: | ------------------------------------------ | -------------------------------------------------- | ------------------------- | +| :white_check_mark: | `GET /api/v1/spots/{spot_id}/solutions` | `lib/api/solutions.ts` → `fetchSolutions` | 스팟 내 솔루션 목록 | +| :white_check_mark: | `POST /api/v1/spots/{spot_id}/solutions` | `lib/api/solutions.ts` → `createSolution` | 솔루션 등록 | +| :white_check_mark: | `PATCH /api/v1/solutions/{solution_id}` | `lib/api/solutions.ts` → `updateSolution` | 솔루션 수정 | +| :white_check_mark: | `DELETE /api/v1/solutions/{solution_id}` | `lib/api/solutions.ts` → `deleteSolution` | 솔루션 삭제 | +| :white_check_mark: | `POST /api/v1/solutions/extract-metadata` | `lib/api/solutions.ts` → `extractSolutionMetadata` | 상품 링크 메타데이터 추출 | +| :white_check_mark: | `POST /api/v1/solutions/convert-affiliate` | `lib/api/solutions.ts` → `convertAffiliate` | 어필리에이트 링크 변환 | + +--- + +## 2. 부분 연동 (연동 코드 존재하나 UI 미완성 또는 Mock 사용) + +### 2.1 Search (검색) + +| 상태 | 백엔드 엔드포인트 | 프론트엔드 위치 | 비고 | +| :--------------------: | ----------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------- | +| :large_orange_diamond: | `GET /api/v1/search` | `shared/api/search.ts` → `useUnifiedSearch` | **연동 코드 구현 완료**, Mock/실제 전환 가능 (`NEXT_PUBLIC_USE_MOCK_SEARCH`) | +| :large_orange_diamond: | `GET /api/v1/search/popular` | `shared/api/search.ts` → `usePopularSearches` | **연동 코드 구현 완료**, Mock/실제 전환 가능 | +| :large_orange_diamond: | `GET /api/v1/search/recent` | `shared/api/search.ts` → `fetchRecentSearches` | **함수 구현 완료**, Hook에서 아직 미사용 | +| :large_orange_diamond: | `DELETE /api/v1/search/recent/{id}` | `shared/api/search.ts` → `deleteRecentSearches` | **함수 구현 완료**, Hook에서 아직 미사용 | + +> **참고:** Search UI 컴포넌트는 풍부하게 구현됨 (SearchOverlay, SearchTabs, SearchResults, RecentSearches 등 12개 컴포넌트). 하지만 Next.js API Route 프록시가 없어서 `/api/v1/search` 경로로 직접 호출. 환경변수 설정으로 Mock ↔ 실제 API 전환 가능. + +### 2.2 Profile (프로필 내 활동 탭) + +| 상태 | 연동 상태 | 비고 | +| :--------------------: | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| :large_orange_diamond: | `useUserActivities` Hook 구현 완료 | ProfileClient에서 `TODO: Connect to useUserActivities hook` 상태. 탭 UI는 존재하나 데이터가 항상 `EmptyState` 표시 | +| :large_orange_diamond: | Badge/Ranking 데이터 | API 호출 없이 **Mock 데이터** 사용 중 (`MOCK_BADGES`, `MOCK_RANKINGS`) | + +--- + +## 3. 미연동 (백엔드 구현 완료, 프론트엔드 미구현) + +### 3.1 Feed (피드) - 전용 API 미연동 + +| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | +| --------------------------------- | ---------------- | ------------------------------------------------------ | +| `GET /api/v1/feed` | 홈 피드 (개인화) | **미사용** — Feed 페이지는 `/api/v1/posts`를 직접 사용 | +| `GET /api/v1/feed/trending` | 트렌딩 피드 | **미사용** — 트렌딩 전용 피드 UI 없음 | +| `GET /api/v1/feed/curations` | 큐레이션 목록 | **미사용** | +| `GET /api/v1/feed/curations/{id}` | 큐레이션 상세 | **미사용** | + +> **현황:** Feed 페이지(`/feed`)는 `useInfinitePosts`로 `/api/v1/posts` 엔드포인트만 사용. 백엔드의 개인화 피드, 트렌딩, 큐레이션 API는 프론트엔드에서 아직 호출하지 않음. + +### 3.2 Rankings (랭킹) + +| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | +| --------------------------------- | --------------- | ----------------------------------------- | +| `GET /api/v1/rankings` | 전체 랭킹 | **미연동** — Profile에서 Mock 랭킹만 표시 | +| `GET /api/v1/rankings/{category}` | 카테고리별 랭킹 | **미연동** | +| `GET /api/v1/rankings/me` | 나의 랭킹 상세 | **미연동** | + +### 3.3 Badges (배지) + +| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | +| ------------------------------- | --------- | ------------------------------------------- | +| `GET /api/v1/badges` | 배지 목록 | **미연동** — Profile에서 `MOCK_BADGES` 사용 | +| `GET /api/v1/badges/me` | 내 배지 | **미연동** | +| `GET /api/v1/badges/{badge_id}` | 배지 상세 | **미연동** | + +> **참고:** 홈 페이지에서 `fetchAllBadgesServer()`로 **Supabase 직접 조회**하여 배지 표시 중. 백엔드 API 연동은 없음. + +### 3.4 Comments (댓글) + +| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | +| --------------------------------------- | --------- | -------------------------------- | +| `POST /api/v1/posts/{post_id}/comments` | 댓글 작성 | **미연동** — API 클라이언트 없음 | +| `GET /api/v1/posts/{post_id}/comments` | 댓글 목록 | **미연동** | +| `PATCH /api/v1/comments/{comment_id}` | 댓글 수정 | **미연동** | +| `DELETE /api/v1/comments/{comment_id}` | 댓글 삭제 | **미연동** | + +### 3.5 Votes (투표) + +| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | +| ------------------------------------- | ----------- | -------------------------------- | +| `POST /api/v1/solutions/{id}/votes` | 솔루션 투표 | **미연동** — API 클라이언트 없음 | +| `DELETE /api/v1/solutions/{id}/votes` | 투표 취소 | **미연동** | +| `GET /api/v1/solutions/{id}/votes` | 투표 통계 | **미연동** | +| `POST /api/v1/solutions/{id}/adopt` | 솔루션 채택 | **미연동** | +| `DELETE /api/v1/solutions/{id}/adopt` | 채택 취소 | **미연동** | + +### 3.6 Earnings (수익) + +| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | 백엔드 상태 | +| ------------------------------------------------ | --------- | --------------- | ----------------------- | +| `POST /api/v1/earnings/clicks` | 클릭 기록 | **미연동** | 구현 완료 | +| `GET /api/v1/earnings/clicks/stats` | 클릭 통계 | **미연동** | 구현 완료 | +| `GET /api/v1/earnings/earnings` | 수익 현황 | **미연동** | _임시 구현 (빈 데이터)_ | +| `GET /api/v1/earnings/settlements` | 정산 내역 | **미연동** | _임시 구현 (빈 배열)_ | +| `POST /api/v1/earnings/settlements/withdraw` | 출금 요청 | **미연동** | _임시 구현 (400 에러)_ | +| `GET /api/v1/earnings/settlements/withdraw/{id}` | 출금 상태 | **미연동** | _임시 구현 (404 에러)_ | + +### 3.7 Subcategories (하위 카테고리) + +| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | +| ----------------------------------------- | ------------------ | --------------- | +| `GET /api/v1/subcategories` | 전체 하위 카테고리 | **미연동** | +| `GET /api/v1/subcategories/{category_id}` | 카테고리별 하위 | **미연동** | + +### 3.8 Spots - 개별 조회 + +| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | +| ----------------------------- | -------------- | ----------------------------- | +| `GET /api/v1/spots/{spot_id}` | 스팟 단건 조회 | **미연동** (목록 조회만 사용) | + +### 3.9 Solutions - 개별 조회 + +| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | +| ------------------------------------- | ---------------- | ----------------------------- | +| `GET /api/v1/solutions/{solution_id}` | 솔루션 단건 조회 | **미연동** (목록 조회만 사용) | + +### 3.10 Admin (관리자) - 전체 미연동 + +| 백엔드 엔드포인트 그룹 | 엔드포인트 수 | 프론트엔드 현황 | +| ---------------------- | :-----------: | ------------------------------- | +| Admin - Posts | 2 | **미연동** — 관리자 페이지 없음 | +| Admin - Solutions | 2 | **미연동** | +| Admin - Categories | 4 | **미연동** | +| Admin - Synonyms | 5 | **미연동** | +| Admin - Curations | 4 | **미연동** | +| Admin - Dashboard | 3 | **미연동** | +| Admin - Badges | 4 | **미연동** | +| **합계** | **24** | | + +--- + +## 4. Supabase 직접 조회 (백엔드 API 우회) + +아래 데이터는 백엔드 API를 거치지 않고 Supabase에서 직접 조회합니다. + +| 사용 위치 | Supabase 쿼리 | 관련 백엔드 API | +| ---------------------------- | ------------------------------------ | ---------------------------------------------- | +| 홈 페이지 (`/`) | `fetchWeeklyBestImagesServer()` | `GET /api/v1/feed` (미사용) | +| 홈 페이지 | `fetchFeaturedImageServer()` | `GET /api/v1/feed` (미사용) | +| 홈 페이지 | `fetchWhatsNewStylesServer()` | `GET /api/v1/feed` (미사용) | +| 홈 페이지 | `fetchDecodedPickServer()` | `GET /api/v1/feed/curations` (미사용) | +| 홈 페이지 | `fetchArtistSpotlightStylesServer()` | — | +| 홈 페이지 | `fetchTrendingKeywordsServer()` | `GET /api/v1/search/popular` (미사용) | +| 홈 페이지 | `fetchAllBadgesServer()` | `GET /api/v1/badges` (미사용) | +| 이미지 목록 (`/images`) | `fetchUnifiedImages()` | `GET /api/v1/posts` (대안 존재) | +| 이미지 상세 (`/images/[id]`) | `fetchImageByIdServer()` | `GET /api/v1/posts/{id}` (대안 존재) | +| 아이템/솔루션 | `fetchSolutionsBySpotId()` 등 | `GET /api/v1/spots/{id}/solutions` (대안 존재) | + +--- + +## 5. 페이지별 UI 기능 가능 여부 종합 + +### :white_check_mark: 완전 동작 가능 + +| 페이지 | 데이터 소스 | 비고 | +| ----------------- | ---------------------------------- | ------------------------------------------- | +| `/` (홈) | Supabase 직접 조회 | 백엔드 API 없이도 SSR로 완전 동작 | +| `/explore` | 백엔드 API (`GET /posts`) | 무한 스크롤 + 카테고리 필터 동작 | +| `/feed` | 백엔드 API (`GET /posts`) | 수직 피드 동작 (단, 개인화/트렌딩은 미적용) | +| `/images` | Supabase 직접 조회 | 이미지 그리드 + 무한 스크롤 동작 | +| `/images/[id]` | Supabase 직접 조회 | 이미지 상세, Lightbox, 관련 이미지 동작 | +| `/posts/[id]` | 백엔드 API | Post 상세 동작 | +| `/request/upload` | 백엔드 API (`POST /posts/upload`) | 이미지 업로드 동작 | +| `/request/detect` | 백엔드 API (`POST /posts/analyze`) | AI 분석 동작 | +| `/login` | Supabase Auth | OAuth(Kakao, Google, Apple) 동작 | + +### :large_orange_diamond: 부분 동작 (일부 기능 제한) + +| 페이지 | 동작하는 기능 | 미동작/제한 기능 | +| ---------- | ------------------------------------ | ---------------------------------------------------------------------------------------------- | +| `/profile` | 내 정보 표시, 통계 카드, 프로필 수정 | **배지:** Mock 데이터, **랭킹:** Mock 데이터, **활동 탭:** 항상 Empty State | +| `/search` | Search UI 전체, Mock 검색 결과 | **실제 검색:** env 설정 필요 (`NEXT_PUBLIC_USE_MOCK_SEARCH=false`), **최근 검색:** Hook 미연결 | + +### :x: UI 없음 (백엔드만 구현) + +| 기능 | 필요한 UI | 우선순위 제안 | +| ------------------ | -------------------------------------- | ------------------------------------- | +| 댓글 시스템 | Post 상세 내 댓글 섹션 | **높음** — 사용자 인터랙션 핵심 | +| 투표/채택 시스템 | Solution 카드 내 투표 버튼 + 채택 표시 | **높음** — 솔루션 품질 관리 핵심 | +| 개인화 피드 | 피드 탭 전환 (홈/트렌딩/큐레이션) | **중간** — 현재 일반 피드로 대체 가능 | +| 큐레이션 | 큐레이션 목록/상세 페이지 | **중간** | +| 랭킹 | 랭킹 보드 페이지 또는 섹션 | **중간** | +| 배지 (실제 데이터) | 배지 API 연동 + Mock 제거 | **낮음** — Mock으로 UI는 동작 | +| 수익/정산 | 수익 대시보드 페이지 | **낮음** — 백엔드도 임시 구현 | +| 하위 카테고리 | 카테고리 드릴다운 필터 | **낮음** | +| 관리자 패널 | 관리자 전용 대시보드 (별도 앱 권장) | **별도** | + +--- + +## 6. 아키텍처 특이사항 + +### 듀얼 데이터 소스 패턴 + +``` +[프론트엔드] + ├── Supabase 직접 조회 (읽기 전용, SSR) + │ └── 홈 페이지, 이미지 목록/상세 + ├── 백엔드 REST API (CRUD, AI 기능) + │ └── 게시물/스팟/솔루션 CUD, 사용자, 카테고리 + └── Next.js API Route 프록시 (/app/api/v1/) + └── 백엔드 API를 프록시하여 CORS 해결 +``` + +### 주의사항 + +1. **홈 페이지 데이터 이중화:** Supabase 직접 조회와 백엔드 Feed API가 겹침. 장기적으로 백엔드 API로 통일 권장. +2. **Search API Route 프록시 부재:** Search는 `shared/api/search.ts`에서 직접 호출. Next.js API Route 프록시 없음. +3. **Profile Mock 의존성:** 배지/랭킹이 Mock 데이터에 의존. API 연동 시 스토어 구조 변경 필요. +4. **Affiliate 링크:** 백엔드에서 `https://affiliate.example.com/{url}` 형태의 **Mock URL** 반환 중 (TODO). + +--- + +## 7. 권장 연동 우선순위 + +| 순위 | 작업 | 이유 | +| :--: | ------------------------ | ----------------------------------------------------------------- | +| 1 | **댓글 시스템 연동** | Post 상세 페이지의 핵심 인터랙션. 백엔드 완전 구현됨. | +| 2 | **투표/채택 연동** | 솔루션 품질 관리 핵심. 백엔드 완전 구현됨. | +| 3 | **Search 실제 API 전환** | UI 이미 완성됨. 환경변수 전환 + API Route 프록시 추가만 필요. | +| 4 | **Profile 활동 탭 연동** | Hook 이미 구현됨 (`useUserActivities`). UI 바인딩만 필요. | +| 5 | **Feed API 전환** | 현재 Posts API로 우회 중. Feed API로 전환하면 개인화/트렌딩 가능. | +| 6 | **배지/랭킹 API 연동** | Mock 제거하고 실제 데이터 사용. UI 이미 존재. | +| 7 | **클릭 기록 연동** | Earnings 수익 시스템의 기초. 어필리에이트 클릭 추적. | +| 8 | **관리자 패널** | 24개 Admin API 활용. 별도 앱 또는 라우트 그룹으로 구현 권장. | + +--- + +_이 문서는 `decoded-api/src/domains/` 및 `decoded-app/packages/web/` 코드를 기반으로 자동 분석되었습니다._ diff --git a/docs/_archive/home-sections-backend-feasibility.md b/docs/_archive/home-sections-backend-feasibility.md new file mode 100644 index 00000000..dcac8c07 --- /dev/null +++ b/docs/_archive/home-sections-backend-feasibility.md @@ -0,0 +1,173 @@ +--- +title: 홈 화면 간소화 계획 (MVP Launch) (archived) +owner: human +status: archived +updated: 2026-05-21 +tags: [archive] +related: + - docs/_archive/README.md +--- + +# 홈 화면 간소화 계획 (MVP Launch) + +> **Archived 2026-05-21** (#561). MVP 런칭 단계 feasibility 분석 — 이후 home 리뉴얼 진행으로 superseded. + +**목표:** 초기 플랫폼 데이터 한계를 인정하고, 실데이터로 즉시 동작 가능한 섹션만으로 구성하여 빠르게 런칭 → 이터레이션 + +--- + +## 왜 간소화해야 하는가? + +| 문제 | 설명 | +| ----------------- | ------------------------------------------------------------------------- | +| **데이터 부족** | 초기 런칭 시 게시물 수, 유저 상호작용, 검색 이력 등이 절대적으로 부족 | +| **빈 섹션 문제** | 11개 섹션 중 대다수가 하드코딩 fallback에 의존 → 실사용자에게 신뢰도 저하 | +| **유지보수 부담** | 섹션마다 별도 Supabase 쿼리 + fallback 로직 + 백엔드 매핑 필요 | +| **사용자 집중도** | 너무 많은 섹션은 핵심 가치("셀럽 패션 디코딩")를 희석 | + +--- + +## 섹션 분류: 유지 vs 제거 + +### 유지 (4개 섹션) + +| # | 섹션 | 유지 근거 | 데이터 요건 | +| :-: | -------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| 1 | **Hero** | 플랫폼 첫인상. 캐러셀/슬라이드로 여러 게시물 노출 | `posts` 5개 (view_count DESC) | +| 2 | **Artist Spotlight** | 핵심 가치 전달 ("셀럽 패션"). artist_name이 있는 게시물이면 충분 | `posts` WHERE `artist_name IS NOT NULL` (2~4개) | +| 3 | **What's New** | 신규 콘텐츠를 두 가지로 구분 노출 | **(3a)** 솔루션 있는 post: 최신순. **(3b)** 솔루션 없는 post: 최신순 + "이 spot에 솔루션 제안하기" 유도 | +| 4 | **Weekly Best** | 인기 콘텐츠 갤러리. view_count 기반이라 게시물만 있으면 동작 | `posts` 4~8개 (view_count DESC) | + +### 제거 (7개 섹션) + +| # | 섹션 | 제거 근거 | +| :-: | --------------------- | -------------------------------------------------------------------- | +| 2 | **Decoded's Pick** | 에디터 큐레이션 로직 없음. 초기에는 큐레이션할 콘텐츠 풀 자체가 부족 | +| 3 | **Today's Decoded** | 100% 하드코딩. `stylingTip` 백엔드 미지원. 완전 신규 개발 필요 | +| 6 | **Badge Grid** | 배지 시스템은 유저 활동 데이터가 쌓인 후에 의미. 홈 핵심 가치와 무관 | +| 7 | **Discover Items** | API가 항상 빈 배열 반환. 아티스트별 아이템 전용 API 없음 | +| 8 | **Discover Products** | click_count 초기 0. 카테고리 필터링 불가. 의미 있는 데이터 불가 | +| 9 | **Best Item** | Discover Products와 동일 데이터 재사용. 중복 | +| 11 | **Trending Now** | 검색 이력/아티스트 빈도 모두 초기에 의미 없는 데이터 | + +--- + +## 간소화된 홈 화면 구성 + +``` +┌─────────────────────────────────────────────┐ +│ Header │ +│ DECODED Home Explore [+] 🔍 👤 │ +├─────────────────────────────────────────────┤ +│ ── Hero (캐러셀, 5개 포스트) ── │ +│ ●───●───●───●───● view_count DESC │ +│ Section 1: Hero │ +├─────────────────────────────────────────────┤ +│ ── Global Perspectives ── │ +│ Artist Spotlight DISCOVER ALL → │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ StyleCard │ │ StyleCard │ │ View More│ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ Section 2: Artist Spotlight │ +├─────────────────────────────────────────────┤ +│ ── What's New ── EXPLORE FEED → │ +│ (3a) 솔루션과 함께 등록된 post │ +│ posts (spot+solution 존재) 최신순, 스타일+아이템 │ +│ (3b) 솔루션 없이 등록된 post │ +│ posts (spot만 있음) 최신순 + CTA: "이 스팟 │ +│ 디코딩하기" → 유저가 솔루션 제안하도록 유도 │ +│ Section 3: What's New │ +├─────────────────────────────────────────────┤ +│ ── Editor's Weekly Roll ── │ +│ Weekly Best ●───●───●───● │ +│ posts 4~8개 (view_count DESC) │ +│ Section 4: Weekly Best │ +├─────────────────────────────────────────────┤ +│ Footer │ +└─────────────────────────────────────────────┘ +``` + +**Header 네비:** Home, Explore(탭), **[+]** Request(포스트 생성), **🔍** 검색(magnifying glass), **👤** 프로필(person 아이콘). Feed 제거. + +--- + +## 섹션별 구현 계획 + +### Section 1: Hero + +| 항목 | 내용 | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| **데이터 소스** | `posts` **5개** 조회 (view_count DESC). 기존 `fetchFeaturedPostServer()` → `fetchFeaturedPostsServer(5)` 또는 limit 5로 확장 | +| **Fallback** | 게시물 0개일 때 `defaultHeroData` 하드코딩 | +| **필요 작업** | Hero UI가 캐러셀/슬라이드인 경우 5개 데이터 전달. 단일 배너면 1개만 사용 가능 | + +### Section 2: Artist Spotlight + +| 항목 | 내용 | +| --------------- | -------------------------------------------------------- | +| **데이터 소스** | `fetchArtistSpotlightStylesServer()` (Supabase SSR) 유지 | +| **Fallback** | 아티스트 게시물 부족 시 `sampleSpotlightData` 하드코딩 | + +### Section 3: What's New (두 하위 섹션) + +What's New를 **두 블록**으로 나눈다. + +| 하위 | 제목 | 데이터 | UX 목표 | +| ------ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| **3a** | 솔루션과 함께 등록된 post | `posts` 중 **최소 1개 spot + solution** 있는 것만, `created_at` DESC. 기존 StyleCard + ItemCard 형태 (최신 2개 + solutions 4개 등) | 방금 디코딩된 콘텐츠 노출 | +| **3b** | 솔루션 없이 등록된 post | `posts` 중 **spot은 있으나 solution이 0개**인 것, `created_at` DESC | 다른 유저가 해당 **spot에 솔루션을 제안**하도록 유도. CTA 예: "이 스팟 디코딩하기" → 상세/편집 플로우로 연결 (문구는 추후 확정) | + +| 항목 | 내용 | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **데이터 소스** | **(3a)** `fetchWhatsNewWithSolutionsServer()` (기존 fetchWhatsNewStylesServer + fetchWhatsNewItemsServer 유사). **(3b)** `fetchPostsWithoutSolutionsServer()` 신규: post–spots JOIN 후 solution 개수 0인 post만 필터, 최신순 | +| **Fallback** | (3a)(3b) 각각 데이터 없으면 해당 블록만 숨김. 둘 다 없으면 What's New 섹션 전체 `return null` | +| **필요 작업** | (3b) 상세/포스트 페이지에서 "이 스팟 디코딩하기" 버튼 또는 스팟 클릭 시 솔루션 제안 UI 연결. CTA 문구는 추후 확정 | + +### Section 4: Weekly Best + +| 항목 | 내용 | +| --------------- | ---------------------------------------------------- | +| **데이터 소스** | `fetchWeeklyBestImagesServer(8)` (Supabase SSR) 유지 | +| **Fallback** | `sampleWeeklyStyles` 하드코딩 | + +--- + +## 코드 변경 사항 + +### `page.tsx` + +- **제거할 쿼리:** + `fetchDecodedPickStyleServer`, `fetchTrendingKeywordsServer`, `fetchBestItemsServer`, `fetchItemsByAccountServer`, `fetchAllBadgesServer` +- **유지·수정할 쿼리:** + - **Hero:** `fetchFeaturedPostServer()` → **`fetchFeaturedPostsServer(5)`** (또는 동일 함수에 limit 5 적용) + - `fetchArtistSpotlightStylesServer()` + - **What's New (3a):** `fetchWhatsNewWithSolutionsServer()` (기존 스타일+아이템 쿼리) + - **What's New (3b):** `fetchPostsWithoutSolutionsServer()` 신규 — spot은 있으나 solution 0개인 post 최신순 + - `fetchWeeklyBestImagesServer()` + +### `HomeAnimatedContent.tsx` + +- **제거:** DecodedPickSection, TodayDecodedSection, BadgeGridSection, DiscoverItemsSection, DiscoverProductsSection, BestItemSection, TrendingNowSection +- **유지:** HeroSection(5개 데이터), ArtistSpotlightSection, **WhatsNewSection(3a 데이터 + 3b 데이터)**, WeeklyBestSection + +--- + +## 성능 개선 효과 + +| 지표 | Before (11 섹션) | After (4 섹션) | +| --------------------- | :----------------: | :--------------------------------: | +| **SSR 쿼리 수** | 11개 Promise.all | 5개 Promise.all | +| **번들 크기** | 11개 섹션 컴포넌트 | 4개 섹션 컴포넌트 | +| **하드코딩 fallback** | 8개 섹션 | 3개 섹션 (Hero, Spotlight, Weekly) | +| **빈 데이터 리스크** | 높음 | 낮음 (What's New만 자동 숨김) | + +--- + +## 이터레이션 로드맵 + +| Phase | 내용 | +| --------------------- | ----------------------------------------------------------------------------- | +| **1. MVP Launch** | 4개 섹션 간소화, Supabase SSR 유지, Hero 5개 포스트 반영 | +| **2. 백엔드 전환** | Supabase → 백엔드 API, `GET /api/v1/feed` 등 활용 | +| **3. 데이터 축적 후** | Trending Now, Decoded's Pick, Today's Decoded, Discover Products 등 복원 검토 | +| **4. 개인화** | 유저별 맞춤 피드, 팔로우 아티스트 기반 섹션 | diff --git a/docs/post-centric-refactoring-summary.md b/docs/_archive/post-centric-refactoring-summary.md similarity index 97% rename from docs/post-centric-refactoring-summary.md rename to docs/_archive/post-centric-refactoring-summary.md index 003a55b9..3e8d57b7 100644 --- a/docs/post-centric-refactoring-summary.md +++ b/docs/_archive/post-centric-refactoring-summary.md @@ -1,5 +1,17 @@ +--- +title: Post-Centric Refactoring — Implementation Summary (archived) +owner: human +status: archived +updated: 2026-05-21 +tags: [archive] +related: + - docs/_archive/README.md +--- + # Post-Centric Refactoring - Implementation Summary +> **Archived 2026-05-21** (#561). 2025-12-18 완료된 마이그레이션 회고 snapshot. + **Date**: 2025-12-18 **Status**: ✅ **COMPLETED** **Approach**: B (Adapter Layer with Zero Data Loss) diff --git a/docs/qa-screenshots/DIFFERENCES.md b/docs/_archive/qa-screenshots-2026-Q1/DIFFERENCES.md similarity index 100% rename from docs/qa-screenshots/DIFFERENCES.md rename to docs/_archive/qa-screenshots-2026-Q1/DIFFERENCES.md diff --git a/docs/_archive/qa-screenshots-2026-Q1/README.md b/docs/_archive/qa-screenshots-2026-Q1/README.md new file mode 100644 index 00000000..fd8ad3d6 --- /dev/null +++ b/docs/_archive/qa-screenshots-2026-Q1/README.md @@ -0,0 +1,120 @@ +# Visual QA Screenshots + +**Generated:** 2026-02-12 +**Reference:** docs/design-system/decoded.pen +**Test Automation:** packages/web/tests/visual-qa.spec.ts + +이 문서는 v2.0 디자인 시스템 구현이 decoded.pen 디자인 참조와 일치하는지 검증하기 위한 시각적 QA 스크린샷을 포함합니다. + +**Total Screenshots:** 40 (4 viewports × 10 pages) + +## Breakpoints Tested + +| Name | Width | Height | Device | +|------|-------|--------|--------| +| Mobile | 375px | 812px | iPhone 13 Pro | +| Tablet | 768px | 1024px | iPad | +| Desktop | 1280px | 800px | Standard Desktop | +| Desktop LG | 1440px | 900px | Large Desktop | + +## Pages Captured + +| Page | Mobile | Tablet | Desktop | Desktop LG | +|------|--------|--------|---------|------------| +| Home | [mobile-home.png](./mobile-home.png) | [tablet-home.png](./tablet-home.png) | [desktop-home.png](./desktop-home.png) | [desktop-lg-home.png](./desktop-lg-home.png) | +| Explore | [mobile-explore.png](./mobile-explore.png) | [tablet-explore.png](./tablet-explore.png) | [desktop-explore.png](./desktop-explore.png) | [desktop-lg-explore.png](./desktop-lg-explore.png) | +| Feed | [mobile-feed.png](./mobile-feed.png) | [tablet-feed.png](./tablet-feed.png) | [desktop-feed.png](./desktop-feed.png) | [desktop-lg-feed.png](./desktop-lg-feed.png) | +| Search | [mobile-search.png](./mobile-search.png) | [tablet-search.png](./tablet-search.png) | [desktop-search.png](./desktop-search.png) | [desktop-lg-search.png](./desktop-lg-search.png) | +| Profile | [mobile-profile.png](./mobile-profile.png) | [tablet-profile.png](./tablet-profile.png) | [desktop-profile.png](./desktop-profile.png) | [desktop-lg-profile.png](./desktop-lg-profile.png) | +| Login | [mobile-login.png](./mobile-login.png) | [tablet-login.png](./tablet-login.png) | [desktop-login.png](./desktop-login.png) | [desktop-lg-login.png](./desktop-lg-login.png) | +| Request Upload | [mobile-request-upload.png](./mobile-request-upload.png) | [tablet-request-upload.png](./tablet-request-upload.png) | [desktop-request-upload.png](./desktop-request-upload.png) | [desktop-lg-request-upload.png](./desktop-lg-request-upload.png) | +| Images | [mobile-images.png](./mobile-images.png) | [tablet-images.png](./tablet-images.png) | [desktop-images.png](./desktop-images.png) | [desktop-lg-images.png](./desktop-lg-images.png) | +| Request | [mobile-request.png](./mobile-request.png) | [tablet-request.png](./tablet-request.png) | [desktop-request.png](./desktop-request.png) | [desktop-lg-request.png](./desktop-lg-request.png) | +| Request Detect | [mobile-request-detect.png](./mobile-request-detect.png) | [tablet-request-detect.png](./tablet-request-detect.png) | [desktop-request-detect.png](./desktop-request-detect.png) | [desktop-lg-request-detect.png](./desktop-lg-request-detect.png) | + +## Findings + +### Review Completed: 2026-02-12 + +**Status:** Approved with caveats +**Reviewer:** Human (orchestrator) +**Reviewed:** 40 screenshots (4 viewports × 10 pages) + +**Context:** API was down during screenshot capture, preventing full pixel-perfect comparison against decoded.pen design reference. Pages that successfully loaded (explore, login, request-upload) showed correct responsive layouts. + +#### Issues Identified + +##### 1. Images Page - Raw JSON Error Exposure (Major - UX/Security) +- **Issue**: Images page (`/images`) exposes raw Supabase/PostgREST JSON error details (PGRST205, table names, hint messages) to users instead of a friendly error message. +- **Severity**: Major +- **Category**: API error handling (not design/CSS) +- **Status**: Deferred to separate quick task +- **Reason**: This is an API error handling issue, not a visual design issue. The v2-09-03 plan focuses on CSS/layout visual QA. +- **Files Affected**: TBD in quick task (likely `app/images/page.tsx` or API client error handling) +- **Recommendation**: Implement user-friendly error messages for API failures (e.g., "Unable to load images. Please try again later.") + +##### 2. Next.js Dev Overlay Badge (Minor - Dev Only) +- **Issue**: "14 Issues" badge visible on images page in dev mode +- **Severity**: Minor +- **Category**: Development artifact +- **Status**: Not applicable (dev mode only, not production) + +##### 3. Mobile Bottom Nav Loading Text (Minor - Dev Only) +- **Issue**: "Compiling..." text visible on mobile bottom navigation +- **Severity**: Minor +- **Category**: Development artifact +- **Status**: Not applicable (dev mode only, not production) + +#### Visual QA Results + +**Responsive Layouts:** ✓ Correct on pages that loaded (explore, login, request-upload) +- Mobile (375px): Layouts correctly adapt +- Tablet (768px): Grid columns adjust appropriately +- Desktop (1280px, 1440px): Full desktop layouts render properly + +**Design System Compliance:** ✓ Observed on loaded pages +- Typography sizing and hierarchy consistent +- Spacing and padding follow design tokens +- Color usage matches design system + +**Deferred:** +- Full pixel-perfect comparison against decoded.pen (API down prevented complete page loading) +- Comprehensive cross-page consistency check (API dependency blocked several pages) + +### Next Steps + +1. **Quick Task**: Fix images page raw JSON error exposure (API error handling) +2. **Future QA**: Re-run visual QA when API is available for complete verification + +## Test Automation + +스크린샷은 Playwright로 자동화되어 있으며, 언제든지 재생성할 수 있습니다. + +### Prerequisites +```bash +# Playwright 브라우저 설치 (최초 1회) +cd packages/web +yarn playwright install chromium +``` + +### Regeneration +```bash +# 개발 서버 시작 +yarn dev:web + +# 다른 터미널에서 테스트 실행 +cd packages/web +yarn playwright test tests/visual-qa.spec.ts +``` + +### Test Configuration +- **Config:** packages/web/playwright.config.ts +- **Test:** packages/web/tests/visual-qa.spec.ts +- **Output:** docs/qa-screenshots/ + +## Notes + +- 모든 스크린샷은 full-page 캡처로 생성됩니다 (전체 페이지 스크롤 포함) +- 각 페이지는 networkidle 상태를 기다린 후 500ms 추가 대기하여 애니메이션이 완료되도록 합니다 +- Search 페이지는 `?q=dress` 쿼리 파라미터로 검색 결과 상태를 캡처합니다 +- 스크린샷 파일명 형식: `{viewport}-{page}.png` diff --git a/docs/qa-screenshots/desktop-explore.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-explore.png similarity index 100% rename from docs/qa-screenshots/desktop-explore.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-explore.png diff --git a/docs/qa-screenshots/desktop-feed.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-feed.png similarity index 100% rename from docs/qa-screenshots/desktop-feed.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-feed.png diff --git a/docs/qa-screenshots/desktop-home.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-home.png similarity index 100% rename from docs/qa-screenshots/desktop-home.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-home.png diff --git a/docs/qa-screenshots/desktop-images.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-images.png similarity index 100% rename from docs/qa-screenshots/desktop-images.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-images.png diff --git a/docs/qa-screenshots/desktop-lg-explore.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-lg-explore.png similarity index 100% rename from docs/qa-screenshots/desktop-lg-explore.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-lg-explore.png diff --git a/docs/qa-screenshots/desktop-lg-feed.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-lg-feed.png similarity index 100% rename from docs/qa-screenshots/desktop-lg-feed.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-lg-feed.png diff --git a/docs/qa-screenshots/desktop-lg-home.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-lg-home.png similarity index 100% rename from docs/qa-screenshots/desktop-lg-home.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-lg-home.png diff --git a/docs/qa-screenshots/desktop-lg-images.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-lg-images.png similarity index 100% rename from docs/qa-screenshots/desktop-lg-images.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-lg-images.png diff --git a/docs/qa-screenshots/desktop-lg-login.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-lg-login.png similarity index 100% rename from docs/qa-screenshots/desktop-lg-login.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-lg-login.png diff --git a/docs/qa-screenshots/desktop-lg-profile.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-lg-profile.png similarity index 100% rename from docs/qa-screenshots/desktop-lg-profile.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-lg-profile.png diff --git a/docs/qa-screenshots/desktop-lg-request-detect.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-lg-request-detect.png similarity index 100% rename from docs/qa-screenshots/desktop-lg-request-detect.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-lg-request-detect.png diff --git a/docs/qa-screenshots/desktop-lg-request-upload.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-lg-request-upload.png similarity index 100% rename from docs/qa-screenshots/desktop-lg-request-upload.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-lg-request-upload.png diff --git a/docs/qa-screenshots/desktop-lg-request.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-lg-request.png similarity index 100% rename from docs/qa-screenshots/desktop-lg-request.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-lg-request.png diff --git a/docs/qa-screenshots/desktop-lg-search.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-lg-search.png similarity index 100% rename from docs/qa-screenshots/desktop-lg-search.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-lg-search.png diff --git a/docs/qa-screenshots/desktop-login.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-login.png similarity index 100% rename from docs/qa-screenshots/desktop-login.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-login.png diff --git a/docs/qa-screenshots/desktop-profile.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-profile.png similarity index 100% rename from docs/qa-screenshots/desktop-profile.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-profile.png diff --git a/docs/qa-screenshots/desktop-request-detect.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-request-detect.png similarity index 100% rename from docs/qa-screenshots/desktop-request-detect.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-request-detect.png diff --git a/docs/qa-screenshots/desktop-request-upload.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-request-upload.png similarity index 100% rename from docs/qa-screenshots/desktop-request-upload.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-request-upload.png diff --git a/docs/qa-screenshots/desktop-request.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-request.png similarity index 100% rename from docs/qa-screenshots/desktop-request.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-request.png diff --git a/docs/qa-screenshots/desktop-search.png b/docs/_archive/qa-screenshots-2026-Q1/desktop-search.png similarity index 100% rename from docs/qa-screenshots/desktop-search.png rename to docs/_archive/qa-screenshots-2026-Q1/desktop-search.png diff --git a/docs/qa-screenshots/mobile-explore.png b/docs/_archive/qa-screenshots-2026-Q1/mobile-explore.png similarity index 100% rename from docs/qa-screenshots/mobile-explore.png rename to docs/_archive/qa-screenshots-2026-Q1/mobile-explore.png diff --git a/docs/qa-screenshots/mobile-feed.png b/docs/_archive/qa-screenshots-2026-Q1/mobile-feed.png similarity index 100% rename from docs/qa-screenshots/mobile-feed.png rename to docs/_archive/qa-screenshots-2026-Q1/mobile-feed.png diff --git a/docs/qa-screenshots/mobile-home.png b/docs/_archive/qa-screenshots-2026-Q1/mobile-home.png similarity index 100% rename from docs/qa-screenshots/mobile-home.png rename to docs/_archive/qa-screenshots-2026-Q1/mobile-home.png diff --git a/docs/qa-screenshots/mobile-images.png b/docs/_archive/qa-screenshots-2026-Q1/mobile-images.png similarity index 100% rename from docs/qa-screenshots/mobile-images.png rename to docs/_archive/qa-screenshots-2026-Q1/mobile-images.png diff --git a/docs/qa-screenshots/mobile-login.png b/docs/_archive/qa-screenshots-2026-Q1/mobile-login.png similarity index 100% rename from docs/qa-screenshots/mobile-login.png rename to docs/_archive/qa-screenshots-2026-Q1/mobile-login.png diff --git a/docs/qa-screenshots/mobile-profile.png b/docs/_archive/qa-screenshots-2026-Q1/mobile-profile.png similarity index 100% rename from docs/qa-screenshots/mobile-profile.png rename to docs/_archive/qa-screenshots-2026-Q1/mobile-profile.png diff --git a/docs/qa-screenshots/mobile-request-detect.png b/docs/_archive/qa-screenshots-2026-Q1/mobile-request-detect.png similarity index 100% rename from docs/qa-screenshots/mobile-request-detect.png rename to docs/_archive/qa-screenshots-2026-Q1/mobile-request-detect.png diff --git a/docs/qa-screenshots/mobile-request-upload.png b/docs/_archive/qa-screenshots-2026-Q1/mobile-request-upload.png similarity index 100% rename from docs/qa-screenshots/mobile-request-upload.png rename to docs/_archive/qa-screenshots-2026-Q1/mobile-request-upload.png diff --git a/docs/qa-screenshots/mobile-request.png b/docs/_archive/qa-screenshots-2026-Q1/mobile-request.png similarity index 100% rename from docs/qa-screenshots/mobile-request.png rename to docs/_archive/qa-screenshots-2026-Q1/mobile-request.png diff --git a/docs/qa-screenshots/mobile-search.png b/docs/_archive/qa-screenshots-2026-Q1/mobile-search.png similarity index 100% rename from docs/qa-screenshots/mobile-search.png rename to docs/_archive/qa-screenshots-2026-Q1/mobile-search.png diff --git a/docs/qa-screenshots/tablet-explore.png b/docs/_archive/qa-screenshots-2026-Q1/tablet-explore.png similarity index 100% rename from docs/qa-screenshots/tablet-explore.png rename to docs/_archive/qa-screenshots-2026-Q1/tablet-explore.png diff --git a/docs/qa-screenshots/tablet-feed.png b/docs/_archive/qa-screenshots-2026-Q1/tablet-feed.png similarity index 100% rename from docs/qa-screenshots/tablet-feed.png rename to docs/_archive/qa-screenshots-2026-Q1/tablet-feed.png diff --git a/docs/qa-screenshots/tablet-home.png b/docs/_archive/qa-screenshots-2026-Q1/tablet-home.png similarity index 100% rename from docs/qa-screenshots/tablet-home.png rename to docs/_archive/qa-screenshots-2026-Q1/tablet-home.png diff --git a/docs/qa-screenshots/tablet-images.png b/docs/_archive/qa-screenshots-2026-Q1/tablet-images.png similarity index 100% rename from docs/qa-screenshots/tablet-images.png rename to docs/_archive/qa-screenshots-2026-Q1/tablet-images.png diff --git a/docs/qa-screenshots/tablet-login.png b/docs/_archive/qa-screenshots-2026-Q1/tablet-login.png similarity index 100% rename from docs/qa-screenshots/tablet-login.png rename to docs/_archive/qa-screenshots-2026-Q1/tablet-login.png diff --git a/docs/qa-screenshots/tablet-profile.png b/docs/_archive/qa-screenshots-2026-Q1/tablet-profile.png similarity index 100% rename from docs/qa-screenshots/tablet-profile.png rename to docs/_archive/qa-screenshots-2026-Q1/tablet-profile.png diff --git a/docs/qa-screenshots/tablet-request-detect.png b/docs/_archive/qa-screenshots-2026-Q1/tablet-request-detect.png similarity index 100% rename from docs/qa-screenshots/tablet-request-detect.png rename to docs/_archive/qa-screenshots-2026-Q1/tablet-request-detect.png diff --git a/docs/qa-screenshots/tablet-request-upload.png b/docs/_archive/qa-screenshots-2026-Q1/tablet-request-upload.png similarity index 100% rename from docs/qa-screenshots/tablet-request-upload.png rename to docs/_archive/qa-screenshots-2026-Q1/tablet-request-upload.png diff --git a/docs/qa-screenshots/tablet-request.png b/docs/_archive/qa-screenshots-2026-Q1/tablet-request.png similarity index 100% rename from docs/qa-screenshots/tablet-request.png rename to docs/_archive/qa-screenshots-2026-Q1/tablet-request.png diff --git a/docs/qa-screenshots/tablet-search.png b/docs/_archive/qa-screenshots-2026-Q1/tablet-search.png similarity index 100% rename from docs/qa-screenshots/tablet-search.png rename to docs/_archive/qa-screenshots-2026-Q1/tablet-search.png diff --git a/docs/agent/warehouse-schema.md b/docs/_archive/warehouse-schema.md similarity index 99% rename from docs/agent/warehouse-schema.md rename to docs/_archive/warehouse-schema.md index e6ca6cd7..e340aefb 100644 --- a/docs/agent/warehouse-schema.md +++ b/docs/_archive/warehouse-schema.md @@ -1,9 +1,10 @@ --- title: Warehouse Schema — DEPRECATED (#333) owner: human -status: deprecated +status: archived updated: 2026-04-25 -tags: [agent, db] +archived: 2026-05-21 +tags: [archive, db] --- # Warehouse Schema — DEPRECATED (#333) @@ -16,6 +17,7 @@ tags: [agent, db] > 신규 **assets** Supabase 프로젝트로 이동(또는 drop) 됐습니다. > > **새 진입점**: +> > - 두 프로젝트 구조: [`docs/agent/database-summary.md`](database-summary.md) > - assets 프로젝트 설계: [`docs/architecture/assets-project.md`](../architecture/assets-project.md) > - env 매트릭스: [`docs/agent/environments.md`](environments.md) diff --git a/docs/adr/ADR-0001-ai-dev-boilerplate.md b/docs/adr/ADR-0001-ai-dev-boilerplate.md index 8940d4a7..830a61fe 100644 --- a/docs/adr/ADR-0001-ai-dev-boilerplate.md +++ b/docs/adr/ADR-0001-ai-dev-boilerplate.md @@ -1,181 +1,15 @@ --- -title: "ADR-0001: Multi-AI Development Boilerplate" +title: ADR-0001 — AI dev boilerplate owner: human -status: approved -updated: 2026-04-17 +status: deprecated +updated: 2026-05-21 tags: [harness, agent] +related: + - docs/adr/index.md --- -# ADR-0001: Multi-AI Development Boilerplate +# ADR-0001 — AI dev boilerplate -## Status +> **Redirect to vault.** Canonical 위치는 vault: [`decoded-docs/Architecture/adr/ADR-0001-ai-dev-boilerplate.md`](https://github.com/decodedcorp/decoded-docs/blob/main/Architecture/adr/ADR-0001-ai-dev-boilerplate.md) -Accepted (v1.0) - -## Context - -We use multiple AI tools in our development workflow: - -- **Claude** (via gstack/Superpowers): For refactors, code analysis, and TDD-driven implementation -- **Cursor**: Main coding assistant for feature implementation -- **Gemini**: Documentation generation -- **Codex**: Spec templates and checklists - -Each tool has different strengths and should be used intentionally. Without clear guidelines, developers may: - -- Use the wrong tool for a task -- Waste time figuring out which tool to use -- Create inconsistent workflows -- Duplicate effort across tools - -## Decision - -We will create a dedicated AI playbook structure with: - -1. **Shared principles** (`docs/ai-playbook/01-principles.md`): - - Language conventions (Korean conversation, English code) - - Code quality standards - - Safety guidelines - - Workflow principles - -2. **Tool-specific profiles** (`docs/ai-playbook/*-profile.md`): - - Clear role definition for each tool - - Usage guidelines and examples - - Integration points with other tools - - Do's and don'ts - -3. **Workflow overview** (`docs/ai-playbook/02-workflow-overview.md`): - - How tools work together - - Typical workflows for common tasks - - Integration with existing infrastructure - -4. **Configuration files**: - - `.cursor/rules/`: Cursor-specific rules (JSONC format) - - `.claude/settings.json`: Claude Code harness configuration - - `.codex/config.json`: Codex CLI configuration - -5. **Prompt templates** (`docs/prompts/`): - - Templates for Gemini documentation generation - - Templates for Codex spec generation - -6. **Integration with existing infrastructure**: - - Integrate with `.claude/` harness workflow (gstack, Superpowers, GSD) - - Preserve `.cursor/rules/` for Cursor users - -## Consequences - -### Positive - -- **Clear mental model**: Developers know which tool to use when -- **Faster onboarding**: New team members understand the workflow quickly -- **Consistency**: Standardized approach across the team -- **Better collaboration**: Clear handoffs between tools -- **Reduced confusion**: Less time spent deciding which tool to use - -### Negative - -- **Additional maintenance**: More documentation files to keep updated -- **Learning curve**: Team needs to learn the new structure -- **Potential rigidity**: May feel restrictive if not balanced with flexibility - -### Risks - -- **Documentation drift**: Docs may become outdated if not maintained -- **Over-engineering**: Too much structure can slow down simple tasks -- **Tool version changes**: Tool updates may require doc updates - -### Mitigation - -- **Version tracking**: Each profile includes "Last verified with" date -- **Kill-switch**: If setup time exceeds work time, simplify to core principles -- **Regular review**: Update docs when tools or workflows change -- **Flexibility**: Structure is advisory, not mandatory - -## Alternatives Considered - -### A: Single Source of Truth (Single File) - -**Approach**: One `docs/ai-playbook/ai-roles.md` file with all rules - -**Rejected because**: - -- Less optimized for each tool's format -- Harder to maintain tool-specific guidance -- Doesn't leverage tool-specific configuration formats - -### B: Tool-Specific Profiles + Shared Principles (Selected) - -**Approach**: Separate profiles with shared principles - -**Selected because**: - -- Best balance of consistency and tool optimization -- Easier to maintain tool-specific guidance -- Supports tool-specific configuration formats - -### C: Task Pipeline Approach - -**Approach**: Process-based documentation (e.g., "small-refactor-claude.md") - -**Rejected because**: - -- Harder to maintain when tools change -- Less reusable across different tasks -- Doesn't provide clear tool roles - -## Implementation - -### Phase 1: Structure Creation (v1.0) - -- Create directory structure -- Write core principles and tool profiles -- Create configuration files -- Set up prompt templates - -### Phase 2: Integration (v1.0) - -- Integrate with existing `.specify/` infrastructure -- Update workflow overview with integration points -- Create ADR document - -### Phase 3: Experimentation (v1.1) - -- Track usage in `docs/ai-playbook/usage-log.md` -- Gather feedback from team -- Refine based on actual usage - -### Phase 4: Refinement (v1.1+) - -- Update profiles based on learnings -- Simplify if needed (kill-switch criteria) -- Maintain version information - -## Success Criteria - -- **Time to first commit**: Reduced by 50%+ for new tasks -- **Tool switching frequency**: Reduced confusion about which tool to use -- **Confusion score**: "Which AI to use when?" score improves (1-5 scale) -- **Workflow clarity**: Team feels workflow is clear and helpful - -## Version History - -- **v1.0** (2025-01-27): Initial boilerplate structure -- **v1.1** (TBD): Updates based on Week 1 usage and feedback - -## References - -- `docs/ai-playbook/01-principles.md`: Core principles -- `docs/ai-playbook/02-workflow-overview.md`: Workflow integration -- `.specify/memory/constitution.md`: Project constitution -- `.cursor/rules/`: Cursor configuration - -## Notes - -This ADR documents the decision to create a multi-AI development boilerplate. The structure is designed to be: - -- **Flexible**: Can adapt to tool changes -- **Maintainable**: Clear version tracking and update process -- **Practical**: Based on actual workflow needs -- **Evolvable**: Can be simplified if needed (kill-switch criteria) - -The boilerplate complements, rather than replaces, existing infrastructure like `.specify/` and `.claude/commands/`. +monorepo 측 본 파일은 외부 link 호환용 stub. vault 측이 정본. diff --git a/docs/adr/ADR-0002-llm-wiki-foundation.md b/docs/adr/ADR-0002-llm-wiki-foundation.md index 57c24e93..72be2d54 100644 --- a/docs/adr/ADR-0002-llm-wiki-foundation.md +++ b/docs/adr/ADR-0002-llm-wiki-foundation.md @@ -1,64 +1,15 @@ --- -title: "ADR-0002: LLM Wiki Foundation" +title: ADR-0002 — LLM Wiki foundation owner: human -status: approved -updated: 2026-04-17 -tags: [architecture, harness, agent] +status: deprecated +updated: 2026-05-21 +tags: [harness, agent] related: - - docs/superpowers/specs/2026-04-17-llm-wiki-foundation-design.md - - docs/wiki/schema/README.md + - docs/adr/index.md --- -# ADR-0002: LLM Wiki Foundation +# ADR-0002 — LLM Wiki foundation -- **Status:** Accepted (2026-04-17) -- **Supersedes:** — -- **Related:** Issue #153, PR #223, Spec `docs/superpowers/specs/2026-04-17-llm-wiki-foundation-design.md` +> **Redirect to vault.** Canonical 위치는 vault: [`decoded-docs/Architecture/adr/ADR-0002-llm-wiki-foundation.md`](https://github.com/decodedcorp/decoded-docs/blob/main/Architecture/adr/ADR-0002-llm-wiki-foundation.md) -## Context - -decoded-monorepo는 현재 4개 이상의 문서 소스가 공존한다: 루트 `CLAUDE.md`, `.cursor/rules/*.mdc`, `docs/agent/`, `docs/ai-playbook/`, `.planning/codebase/`. 각각 부분적으로 중복된 컨벤션·아키텍처·에이전트 규칙을 담고 있으며 정본(SSOT) 규칙이 없다. 이슈 #153은 이 상태를 정비하면서 Karpathy LLM wiki 컨셉(LLM이 markdown 위키를 자동 ingest/lint/update하는 구조)에서 영감을 얻어 에이전트 하네스 프로그래밍을 업그레이드한다. - -## Decision - -`docs/` 하위에 LLM wiki 2계층 구조(`docs/wiki/wiki/` + `docs/wiki/schema/`)를 도입한다. Karpathy의 3계층(source/wiki/schema) 중 `source`는 "source = code"인 이 repo 특성상 생략한다. 기존 `docs/agent/`는 topic summary 허브로 확장하고, 기존 topic 폴더(`docs/architecture/` 등)에 distributed `agent.md`를 두지 않는다. 컨벤션·하네스 규칙은 `docs/wiki/schema/`로 정본화하고 `CLAUDE.md`·`.cursor/rules/`는 pointer로 축소한다. - -## Alternatives Considered - -1. **Do nothing** — 현 상태 유지. 드리프트 지속. -2. **Flatten everything into `docs/agent/`** — Karpathy 컨셉 포기, 성장 경로 상실. -3. **Full Karpathy 3계층(source/wiki/schema)** — `source` 계층 억지 적용. -4. **Consolidation only** (schema 도입 없이 중복만 줄임) — SSOT 구조화 기회 상실. - -## Rationale - -에이전트 작업 지식의 누적을 허용(hybrid D 기반)하면서, OMC critic·architect 리뷰가 지적한 3가지 위험(distributed `agent.md`, `source` 계층 억지, 자동화 지연)을 동시 회피한다. - -## Consequences - -### Positive - -- 컨벤션 조회 시 정본 1곳으로 수렴 (현재 4+). -- LLM·Cursor·Claude가 공통 규약 파일을 참조. -- 에이전트 지식 누적을 받아내는 전용 공간(`wiki/wiki/`) 확보. - -### Negative - -- 새 디렉토리 도입으로 초기 learning curve. -- Phase 1 산출물이 Sub-3 자동화 없이 수동 유지. Sub-3 지연 시 drift 위험. -- 기존 파일 프론트매터 추가·pointer 축소 migration 비용. - -## Reversibility - -Phase 1 후 2~3개월 내 이 구조가 overhead로 드러나면: - -1. 이 ADR을 `Superseded`로 변경하고 사유 기록. -2. `docs/wiki/schema/**` 삭제 또는 `/archive/` 이동. -3. PR-C에서 축소한 `CLAUDE.md`·`.cursor/rules/` 컨벤션 섹션을 `git revert`로 복원. -4. `docs/agent/*-summary.md` 파일은 유지 여부를 파일별로 재평가. -5. `.planning/codebase/`와 기존 `docs/agent/` 인벤토리는 변경 없이 잔존. - -## Implementation - -- Plan: `docs/superpowers/plans/2026-04-17-llm-wiki-foundation.md` -- Follow-ups: Sub-3 (자동화), Sub-4 (CLAUDE.md·.cursor/rules 전면 리팩토링) +monorepo 측 본 파일은 외부 link 호환용 stub. vault 측이 정본. diff --git a/docs/adr/index.md b/docs/adr/index.md new file mode 100644 index 00000000..7153a23c --- /dev/null +++ b/docs/adr/index.md @@ -0,0 +1,29 @@ +--- +title: Architectural Decision Records (ADR) +owner: human +status: approved +updated: 2026-05-21 +tags: [harness, agent] +related: + - docs/wiki/schema/ownership-matrix.md +--- + +# Architectural Decision Records (ADR) + +**Canonical 위치는 vault**: [`decoded-docs/Architecture/adr/`](https://github.com/decodedcorp/decoded-docs/tree/main/Architecture/adr) + +monorepo의 `docs/adr/` 폴더는 외부 link 호환을 위한 redirect stub만 유지한다. + +## 최근 ADR + +| ID | Title | Status | +| -------- | ----------------------------------------------------------------------------------------------- | -------- | +| ADR-0001 | AI dev boilerplate (vault) | approved | +| ADR-0002 | LLM Wiki foundation (vault) | approved | +| ADR-0003 | Cross-tool agent entry policy (#561) — 본 ADR이 cross-tool entry/ownership-matrix 정책을 본문화 | proposed | + +## 정책 + +- 새 ADR은 vault `Architecture/adr/ADR-XXXX-.md`에 작성한다. +- monorepo의 `docs/adr/ADR-XXXX-*.md`는 vault link 1줄만 가진 stub로 유지한다 (외부 link rot 방지). +- 자세한 vault sync policy는 [`Guides/sync-policy.md`](https://github.com/decodedcorp/decoded-docs/blob/main/Guides/sync-policy.md). diff --git a/docs/agent/README.md b/docs/agent/README.md index d91a5a0b..c2cd43dd 100644 --- a/docs/agent/README.md +++ b/docs/agent/README.md @@ -2,7 +2,7 @@ title: Agent reference (`docs/agent/`) owner: human status: approved -updated: 2026-04-21 +updated: 2026-05-21 tags: [agent, harness] --- @@ -10,30 +10,42 @@ tags: [agent, harness] 버전 관리되는 **에이전트·LLM용 참조** 모음입니다. 루트 [`CLAUDE.md`](../../CLAUDE.md)는 짧은 **맵**만 두고, 표·인벤토리·긴 트리는 이 폴더에 둡니다. +> **회의 · 결정 · 기획 · 아키텍처 · 회고는 별도 [decoded-docs vault](https://github.com/decodedcorp/decoded-docs)에 있습니다.** 이 폴더(`docs/agent/`)는 코드/LLM 라우팅/agent 인벤토리/spec 전용입니다. 전체 동기화 정책: [decoded-docs/Guides/sync-policy.md](https://github.com/decodedcorp/decoded-docs/blob/main/Guides/sync-policy.md) + ## When to read what -| Task | Document | -| ---------------------------------------------- | -------------------------------------------------------- | -| **env matrix (dev/prod) + env 파일 매핑** | [**environments.md**](environments.md) | -| **DB 마이그레이션 SOT / 워크플로우** | [**../DATABASE-MIGRATIONS.md**](../DATABASE-MIGRATIONS.md) | -| staging 정의 (현재 없음) | [staging.md](staging.md) | -| `docs/wiki/` 진입 가이드 (에이전트 워크플로우) | [wiki-entry.md](wiki-entry.md) | -| 아키텍처 summary (설계 의도 vs 스냅샷) | [architecture-summary.md](architecture-summary.md) | -| API summary (Next.js + Rust 레이어) | [api-summary.md](api-summary.md) | -| DB summary (prod / assets 두 프로젝트, #333) | [database-summary.md](database-summary.md) | +| Task | Document | +| ---------------------------------------------- | ------------------------------------------------------------------------ | +| **env matrix (dev/prod) + env 파일 매핑** | [**environments.md**](environments.md) | +| **DB 마이그레이션 SOT / 워크플로우** | [**../DATABASE-MIGRATIONS.md**](../DATABASE-MIGRATIONS.md) | +| staging 정의 (현재 없음) | [staging.md](staging.md) | +| `docs/wiki/` 진입 가이드 (에이전트 워크플로우) | [wiki-entry.md](wiki-entry.md) | +| 아키텍처 summary (설계 의도 vs 스냅샷) | [architecture-summary.md](architecture-summary.md) | +| API summary (Next.js + Rust 레이어) | [api-summary.md](api-summary.md) | +| DB summary (prod / assets 두 프로젝트, #333) | [database-summary.md](database-summary.md) | | **Assets Supabase 프로젝트 설계 (#333)** | [`../architecture/assets-project.md`](../architecture/assets-project.md) | -| Verify 플로우 수동 QA 체크리스트 | [verify-flow-qa.md](verify-flow-qa.md) | -| 디자인 시스템 summary | [design-system-summary.md](design-system-summary.md) | -| AI playbook summary (에이전트별 프로필) | [ai-playbook-summary.md](ai-playbook-summary.md) | -| 패키지 구조, 명령어, 로컬 deps / 포트 | [monorepo.md](monorepo.md) | -| 웹 라우트·기능 영역 | [web-routes-and-features.md](web-routes-and-features.md) | -| Next.js `app/api/v1/*` 라우트 표 | [api-v1-routes.md](api-v1-routes.md) | -| 훅 목록, 스토어·주요 경로 | [web-hooks-and-stores.md](web-hooks-and-stores.md) | -| 디자인 시스템 import·컴포넌트 인벤토리 | [design-system-llm.md](design-system-llm.md) | -| Warehouse 스키마 (DEPRECATED — historical reference, #333) | [warehouse-schema.md](warehouse-schema.md) | -| E2E 테스트 인프라·검증 항목 | [e2e-testing.md](e2e-testing.md) | -| 아키텍처·컨벤션·스택 심층 | [`.planning/codebase/`](../../.planning/codebase/) | -| 디자인 토큰·UI 상세 | [`docs/design-system/`](../design-system/) | +| Verify 플로우 수동 QA 체크리스트 | [verify-flow-qa.md](verify-flow-qa.md) | +| 디자인 시스템 summary | [design-system-summary.md](design-system-summary.md) | +| AI playbook summary (에이전트별 프로필) | [ai-playbook-summary.md](ai-playbook-summary.md) | +| 패키지 구조, 명령어, 로컬 deps / 포트 | [monorepo.md](monorepo.md) | +| 웹 라우트·기능 영역 | [web-routes-and-features.md](web-routes-and-features.md) | +| Next.js `app/api/v1/*` 라우트 표 | [api-v1-routes.md](api-v1-routes.md) | +| 훅 목록, 스토어·주요 경로 | [web-hooks-and-stores.md](web-hooks-and-stores.md) | +| 디자인 시스템 import·컴포넌트 인벤토리 | [design-system-llm.md](design-system-llm.md) | +| E2E 테스트 인프라·검증 항목 | [e2e-testing.md](e2e-testing.md) | +| 아키텍처·컨벤션·스택 심층 | [`.planning/codebase/`](../../.planning/codebase/) | +| 디자인 토큰·UI 상세 | [`docs/design-system/`](../design-system/) | + +> Deprecated 문서는 [`docs/_archive/`](../_archive/)에 보관한다. + +## Setup (1회성) + +| 항목 | 문서 | +| ---------------- | ------------------------------------------------ | +| Issue tracker | [setup/issue-tracker.md](setup/issue-tracker.md) | +| Triage labels | [setup/triage-labels.md](setup/triage-labels.md) | +| Domain docs 위치 | [setup/domain.md](setup/domain.md) | +| Skills inventory | [skills.md](skills.md) | ## Rust API crate diff --git a/docs/agent/database-summary.md b/docs/agent/database-summary.md index 02dd62e8..ddef88b4 100644 --- a/docs/agent/database-summary.md +++ b/docs/agent/database-summary.md @@ -2,11 +2,12 @@ title: Database — Agent Summary owner: llm status: draft -updated: 2026-04-30 +updated: 2026-05-11 tags: [db, agent] related: - docs/database/operating-model.md - docs/database/01-schema-usage.md + - docs/database/entity-enrichment-pipeline.md - docs/architecture/assets-project.md - docs/database/04-supabase-cli-setup.md --- @@ -29,6 +30,7 @@ prod 는 assets 의 존재를 모르고, assets 도 prod 를 모른다 (cross-pr ## Canonical sources - **운영 모델 (먼저 읽기)**: [`docs/database/operating-model.md`](../database/operating-model.md) — 영역/시스템 매트릭스, "어디 추가하나" 결정 트리, drift 회피, dev 시드 절차 +- Entity enrichment RFC: [`docs/database/entity-enrichment-pipeline.md`](../database/entity-enrichment-pipeline.md) — assets Instagram tags → prod `instagram_accounts` → `artists`/`brands`/`groups` - 스키마 사용법: [`docs/database/01-schema-usage.md`](../database/01-schema-usage.md) - 데이터 흐름: [`docs/database/03-data-flow.md`](../database/03-data-flow.md) - 업데이트 체크리스트: [`docs/database/02-update-checklist.md`](../database/02-update-checklist.md) @@ -40,6 +42,7 @@ prod 는 assets 의 존재를 모르고, assets 도 prod 를 모른다 (cross-pr - **prod public schema**: 앱 데이터 (posts, items, users, solutions, social 등) + 엔티티 카탈로그 (artists/groups/brands, #333) - **assets public schema**: 파이프라인 스테이징 (raw_post_sources, raw_posts, pipeline_events) + 5-state 상태머신 +- **entity enrichment**: assets `raw_posts.platform_metadata.tagged_usernames` 는 후보 증거이고, scheduler state/usage 는 assets `pipeline_settings` / `gemini_usage_events` 가 소유한다. reviewed account/entity state 는 prod `public.instagram_accounts` / `artists` / `brands` / `groups` 가 소유한다. - Migration 전략: - prod: SeaORM (테이블·컬럼) + Supabase CLI SQL (RLS·함수, `supabase/migrations` 가 SOT, #282) - assets: Supabase CLI SQL only (`supabase-assets/migrations/`). SeaORM 은 assets 를 건드리지 않음. @@ -57,3 +60,4 @@ prod 는 assets 의 존재를 모르고, assets 도 prod 를 모른다 (cross-pr - 2026-04-30: 운영 모델 단일 진입점 (`operating-model.md`) 추가 (#371). PRD ref 갱신 (`womgfy...` → `tdchmitwczlwydzkyczu`). - 2026-04-25: warehouse 스키마 드롭 + assets 프로젝트 분리 반영 (#333 / #335) - 2026-04-17: 초기 작성 (Phase 1) +- 2026-05-11: Instagram tagged account → entity catalog enrichment RFC 추가. diff --git a/docs/agent/setup/domain.md b/docs/agent/setup/domain.md new file mode 100644 index 00000000..3ea329f6 --- /dev/null +++ b/docs/agent/setup/domain.md @@ -0,0 +1,46 @@ +# Domain Docs + +How the engineering skills should consume this repo's domain documentation when exploring the codebase. + +## Layout: multi-context (monorepo) + +이 repo는 monorepo로, 각 패키지가 자체 컨텍스트를 가집니다. `CONTEXT-MAP.md`가 아직 root에 없으면, 대신 다음을 1차 진입점으로 사용: + +| 위치 | 역할 | +| ------------------------------------ | ---------------------------------- | +| `CLAUDE.md` | 전체 맵 (코드/LLM 라우팅) | +| `.planning/codebase/STACK.md` | 기술 스택, 의존성 | +| `.planning/codebase/ARCHITECTURE.md` | 아키텍처, 레이어, 데이터 흐름 | +| `.planning/codebase/CONVENTIONS.md` | 코딩 컨벤션 | +| `docs/agent/` | 표·인벤토리·라우트 (에이전트 참조) | +| `docs/adr/` | 시스템 전반 아키텍처 결정 | +| `packages/api-server/AGENT.md` | Rust API 크레이트 전용 가이드 | + +## Before exploring, read these + +- **`CLAUDE.md`** at the repo root — short map pointing to subsystem docs +- **`docs/adr/`** — read ADRs that touch the area you're about to work in +- **Package-specific guides** when touching a specific package: + - `packages/web/` — Next.js 16, see `docs/agent/web-routes-and-features.md` + - `packages/api-server/` — Rust/Axum, see `packages/api-server/AGENT.md` + - `packages/shared/` — Shared types/hooks/queries + - `packages/mobile/` — Expo 54 + - `packages/ai-server/` — Python/uv gRPC + +If `CONTEXT.md` or per-package `CONTEXT.md` files don't exist yet, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The producer skill (`/grill-docs`) creates them lazily when terms or decisions actually get resolved. + +## Use the existing vocabulary + +When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use terms as they appear in `CLAUDE.md`, `.planning/codebase/ARCHITECTURE.md`, and existing ADRs/code. Don't drift to synonyms. + +If the concept you need isn't documented yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/grill-docs`). + +## Flag ADR conflicts + +If your output contradicts an existing ADR, surface it explicitly rather than silently overriding: + +> _Contradicts ADR-NNNN (title) — but worth reopening because…_ + +## Decoded-docs vault + +회의 · 결정 · 기획 · 아키텍처 · 회고 등 비코드 도메인 지식은 별도 [decoded-docs vault](https://github.com/decodedcorp/decoded-docs)에 있습니다. 코드 관련 ADR/CONTEXT만 이 repo에서 관리합니다. diff --git a/docs/agent/setup/issue-tracker.md b/docs/agent/setup/issue-tracker.md new file mode 100644 index 00000000..1391e7b4 --- /dev/null +++ b/docs/agent/setup/issue-tracker.md @@ -0,0 +1,26 @@ +# Issue tracker: GitHub + +Issues and PRDs for this repo live as GitHub issues at [decodedcorp/decoded](https://github.com/decodedcorp/decoded). Use the `gh` CLI for all operations. + +## Conventions + +- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies. +- **Read an issue**: `gh issue view --comments`, filtering comments by `jq` and also fetching labels. +- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters. +- **Comment on an issue**: `gh issue comment --body "..."` +- **Apply / remove labels**: `gh issue edit --add-label "..."` / `--remove-label "..."` +- **Close**: `gh issue close --comment "..."` + +Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone. + +## Project board + +이 repo는 `decoded-monorepo` GitHub Project를 사용. issue를 만들면 GitHub Actions가 자동으로 project board에 등록합니다 (`.github/workflows/issue-project-sync.yml`). + +## When a skill says "publish to the issue tracker" + +Create a GitHub issue. Default assignee는 `thxforall` (현재 단독 유지보수). label 컨벤션: `documentation`, `tech-debt`, `bump:*` (audit/SemVer 트래킹용). + +## When a skill says "fetch the relevant ticket" + +Run `gh issue view --comments`. diff --git a/docs/agent/setup/triage-labels.md b/docs/agent/setup/triage-labels.md new file mode 100644 index 00000000..aa83e98b --- /dev/null +++ b/docs/agent/setup/triage-labels.md @@ -0,0 +1,17 @@ +# Triage Labels + +The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker. + +| Label in mattpocock/skills | Label in our tracker | Meaning | +| -------------------------- | -------------------- | ---------------------------------------- | +| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue | +| `needs-info` | `needs-info` | Waiting on reporter for more information | +| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent | +| `ready-for-human` | `ready-for-human` | Requires human implementation | +| `wontfix` | `wontfix` | Will not be actioned | + +When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table. + +## Status (2026-05-21) + +triage 전용 라벨은 아직 GitHub repo에 생성되어 있지 않습니다. 첫 사용 시점에 `gh label create needs-triage --color "..." --description "..."` 형태로 생성하거나, 기존 라벨(`documentation`, `tech-debt`, `bump:*`)과 별도 카테고리로 추가하세요. canonical 이름 그대로 사용해도 충돌 없음. diff --git a/docs/agent/skills.md b/docs/agent/skills.md new file mode 100644 index 00000000..30ac640e --- /dev/null +++ b/docs/agent/skills.md @@ -0,0 +1,95 @@ +--- +title: Skills inventory +owner: human +status: approved +updated: 2026-05-21 +tags: [agent, harness, skills] +related: + - AGENTS.md + - docs/wiki/schema/ownership-matrix.md +--- + +# Skills inventory + +monorepo에서 사용 가능한 slash command / skill의 정본. `CLAUDE.md`, `AGENTS.md`, `WARP.md`는 이 문서를 가리키기만 한다. + +## gstack (Software Factory) + +Sprint workflow: **Think → Plan → Build → Review → Test → Ship → Reflect**. + +| Phase | Command | Role | +| ------- | -------------------------------------------- | ----------------------------------------- | +| Think | `/office-hours` | YC Office Hours — reframe the product | +| Plan | `/plan-ceo-review` | CEO — rethink scope | +| Plan | `/plan-eng-review` | Eng Manager — lock architecture | +| Plan | `/plan-design-review` | Designer — rate & improve design | +| Plan | `/design-consultation` | Design Partner — build design system | +| Plan | `/autoplan` | Auto-review pipeline: CEO → design → eng | +| Build | `/browse` | Browser automation (Playwright) | +| Review | `/review` | Staff Engineer — find production bugs | +| Review | `/design-review` | Designer Who Codes — audit + fix | +| Review | `/cso` | Security Officer — OWASP + STRIDE audit | +| Test | `/qa` | QA Lead — real browser testing + auto-fix | +| Test | `/qa-only` | QA report only (no fixes) | +| Ship | `/ship` | Release Engineer — test, PR, ship | +| Ship | `/land-and-deploy` | Merge → deploy → canary verify | +| Monitor | `/canary` | Post-deploy monitoring | +| Monitor | `/benchmark` | Performance regression detection | +| Debug | `/investigate` | Systematic root-cause debugging | +| Reflect | `/retro` | Sprint retrospective | +| Docs | `/document-release` | Post-ship doc updates | +| Safety | `/careful`, `/freeze`, `/guard`, `/unfreeze` | Destructive op protection | +| Setup | `/setup-deploy`, `/setup-browser-cookies` | One-time config | +| Utility | `/gstack-upgrade` | Update gstack | +| Utility | `/codex` | Multi-AI second opinion | +| Utility | `/connect-chrome` | Connect Chrome for browsing | + +### Rules + +- 웹 브라우징은 항상 `/browse` 사용 — `mcp__claude-in-chrome__*` 금지 +- gstack 스킬이 동작하지 않으면 `cd ~/.claude/skills/gstack && ./setup` +- Sprint 순서 준수: Think → Plan → Build → Review → Test → Ship → Reflect + +## Matt Pocock slash commands + +| Command | 용도 | +| -------------------- | ------------------------------------------------------------------------------------------- | +| `/grill` | plan/design을 인터뷰로 결정 분기 풀기 | +| `/grill-docs` | plan을 도메인 모델·ADR과 함께 grill | +| `/to-prd` | 대화 맥락을 PRD로 응축해 issue tracker에 publish | +| `/to-issues` | plan/PRD를 vertical slice GitHub Issues로 분해 | +| `/zoom-out` | 현재 코드/맥락을 더 넓은 시야로 다시 보기 | +| `/setup-matt-skills` | issue tracker / triage labels / domain docs 위치를 AGENTS.md·CLAUDE.md에 등록 (1회성 setup) | + +## Spec / Generator skills + +| Trigger 키워드 | Skill | +| --------------------------------------------------------- | ---------------------------- | +| "screen spec", "화면 명세", "document screen" | screen-spec-generator | +| "data model", "generate types", "create interface" | data-model-generator | +| "API contract", "OpenAPI", "endpoint spec" | api-contract-generator | +| "scaffold component", "create component", "컴포넌트 생성" | component-template-generator | +| "migration", "database schema", "테이블 생성" | supabase-migration-generator | +| "excalidraw", "다이어그램", "flowchart", "ERD" | excalidraw-generator | +| "pencil ui", "screen ui", "디자인 구현", "UI 구현" | pencil-screen-ui | +| "meeting prep", "weekly update", "주간 보고" | meeting-prep | +| "브랜치 이름", "commit convention", "PR 준비" | git-workflow | + +## Skill routing rules + +사용자의 요청이 가용 skill과 매치되면 **Skill tool을 FIRST action으로** 호출. 직접 답하지 않는다. + +핵심 라우팅: + +- Product ideas, "is this worth building", brainstorming → `office-hours` +- Bugs, errors, "why is this broken", 500 errors → `investigate` +- Ship, deploy, push, create PR → `ship` +- QA, test the site, find bugs → `qa` +- Code review, check my diff → `review` +- Update docs after shipping → `document-release` +- Weekly retro → `retro` +- Design system, brand → `design-consultation` +- Visual audit, design polish → `design-review` +- Architecture review → `plan-eng-review` +- Save progress, checkpoint, resume → `checkpoint` +- Code quality, health check → `health` diff --git a/docs/agent/web-routes-and-features.md b/docs/agent/web-routes-and-features.md index 73d79144..f7a59539 100644 --- a/docs/agent/web-routes-and-features.md +++ b/docs/agent/web-routes-and-features.md @@ -2,7 +2,7 @@ title: Web Routes & Features — Agent Reference owner: human status: approved -updated: 2026-04-17 +updated: 2026-05-11 tags: [agent, ui] --- @@ -40,9 +40,10 @@ App Router 기준 (`packages/web/app/`). 작업 시 이 표와 실제 `app/` 트 | `/admin/seed/post-spots` | 시드 포스트 스팟 | | `/admin/entities/artists` | 아티스트 관리 (CRUD, paginated, searchable) | | `/admin/entities/brands` | 브랜드 관리 (CRUD) | -| `/admin/entities/group-members` | 그룹 멤버 관리 | +| `/admin/entities/group-members` | 그룹 멤버 관리 — group별 artist membership 조회·추가·수정·삭제 | | `/admin/raw-post-sources` | 수집 소스 등록/관리 (Pinterest 등 — #327) | | `/admin/raw-posts` | **검증 큐** (#333) — assets 의 raw_posts 를 status 탭(COMPLETED/IN_PROGRESS/ERROR/VERIFIED) 으로 필터링, "검증" 버튼으로 prod posts 반영 | +| `/admin/data-pipeline/instagram-accounts` | Instagram tagged account enrichment queue, Gemini grounding quota, scheduler controls, manual entity review. See [`docs/database/entity-enrichment-pipeline.md`](../database/entity-enrichment-pipeline.md). | | `/request/upload` | Image upload with DropZone | | `/request/detect` | AI detection results with item spotting | | `/request/try` | Try 포스트 업로드 페이지 | diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 8424bc57..aa1f8b31 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,500 +1,17 @@ -# System Architecture - -> Version: 1.0.0 -> Last Updated: 2026-01-14 -> Purpose: 시스템 아키텍처 개요 및 컴포넌트 의존성 - ---- - -## Overview - -Decoded는 K-콘텐츠 패션 발견 플랫폼으로, 모노레포 구조의 Next.js 웹 앱과 Expo 모바일 앱으로 구성됩니다. - ---- - -## 1. System Architecture Diagram - -![System Architecture](../diagrams/system-architecture.excalidraw.png) - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ CLIENT LAYER │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ packages/web │ │ -│ │ │ │ -│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ -│ │ │ Next.js │ │ React │ │ TypeScript │ │ │ -│ │ │ 16.0.7 │ │ 18.3.1 │ │ 5.9.3 │ │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ │ • App Router │ │ • Components │ │ • Type-safe │ │ │ -│ │ │ • SSR/SSG │ │ • Hooks │ │ • Interfaces │ │ │ -│ │ │ • API Routes │ │ • Context │ │ • Generics │ │ │ -│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ -│ │ │ │ -│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ -│ │ │ Tailwind │ │ Zustand │ │ React Query │ │ │ -│ │ │ 3.4.18 │ │ 4.5.7 │ │ 5.90.11 │ │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ │ • Utility │ │ • 전역 상태 │ │ • 서버 상태 │ │ │ -│ │ │ • Design │ │ • Filter │ │ • Caching │ │ │ -│ │ │ System │ │ • Search │ │ • Mutations │ │ │ -│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ -│ │ │ │ -│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ -│ │ │ GSAP │ │ Motion │ │ Lenis │ │ │ -│ │ │ 3.13.0 │ │ 12.23.12 │ │ 1.3.15 │ │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ │ • FLIP │ │ • Gestures │ │ • Smooth │ │ │ -│ │ │ • ScrollTrig │ │ • Transitions│ │ Scroll │ │ │ -│ │ │ • Timeline │ │ • Spring │ │ • Virtual │ │ │ -│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ packages/shared │ │ -│ │ │ │ -│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ -│ │ │ Hooks │ │ Stores │ │ Queries │ │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ │ • useImages │ │ • filter │ │ • images │ │ │ -│ │ │ • useDebounce│ │ • search │ │ • items │ │ │ -│ │ │ │ │ • hierarchic │ │ • adapter │ │ │ -│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ packages/mobile │ │ -│ │ │ │ -│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ -│ │ │ Expo │ │ React Native │ │ Reanimated │ │ │ -│ │ │ SDK 54 │ │ 0.81 │ │ 4 │ │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ │ • Router 6 │ │ • iOS/Andro │ │ • 60fps │ │ │ -│ │ │ • Notif │ │ • Native UI │ │ • Gestures │ │ │ -│ │ │ • ImagePick │ │ │ │ │ │ │ -│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ - │ - │ HTTPS - ▼ -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ SUPABASE BACKEND │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────┐ ┌──────────────────────┐ ┌────────────────────┐ │ -│ │ PostgreSQL │ │ Storage │ │ Auth │ │ -│ │ │ │ │ │ │ │ -│ │ Tables: │ │ Buckets: │ │ Providers: │ │ -│ │ • image │ │ • uploads │ │ • Kakao │ │ -│ │ • post │ │ • cropped │ │ • Google │ │ -│ │ • item │ │ │ │ • Apple │ │ -│ │ • post_image │ │ │ │ │ │ -│ │ • (future) user │ │ │ │ Sessions: │ │ -│ │ • (future) vote │ │ │ │ • JWT tokens │ │ -│ │ • (future) comment │ │ │ │ • Refresh tokens │ │ -│ │ │ │ │ │ │ │ -│ └──────────────────────┘ └──────────────────────┘ └────────────────────┘ │ -│ │ -│ ┌──────────────────────┐ ┌──────────────────────┐ │ -│ │ Edge Functions │ │ Realtime │ │ -│ │ (Future) │ │ (Future) │ │ -│ │ │ │ │ │ -│ │ • AI Detection │ │ • Live updates │ │ -│ │ • Scraper │ │ • Notifications │ │ -│ │ • Click tracking │ │ │ │ -│ │ │ │ │ │ -│ └──────────────────────┘ └──────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ - │ - │ (Future Integration) - ▼ -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ EXTERNAL SERVICES │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────┐ ┌──────────────────────┐ ┌────────────────────┐ │ -│ │ Vision API │ │ Scraper Engine │ │ Affiliate APIs │ │ -│ │ (TBD) │ │ │ │ │ │ -│ │ │ │ • Product info │ │ • Musinsa │ │ -│ │ • Object detection │ │ • Price extraction │ │ • 29CM │ │ -│ │ • Brand recognition │ │ • Image scraping │ │ • Farfetch │ │ -│ │ • Fashion tagging │ │ │ │ • SSENSE │ │ -│ │ │ │ │ │ │ │ -│ └──────────────────────┘ └──────────────────────┘ └────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 2. Monorepo Structure - -``` -decoded-app/ -├── packages/ -│ ├── web/ # Next.js 웹 앱 -│ │ ├── app/ # App Router pages -│ │ │ ├── page.tsx # Home -│ │ │ ├── layout.tsx # Root layout -│ │ │ ├── images/[id]/ # Image detail -│ │ │ ├── @modal/ # Parallel route (modal) -│ │ │ └── lab/ # Experimental -│ │ │ -│ │ ├── lib/ # App-specific code -│ │ │ ├── components/ # React components -│ │ │ │ ├── detail/ # Detail view components -│ │ │ │ ├── filter/ # Filter components -│ │ │ │ ├── grid/ # Grid components -│ │ │ │ └── ui/ # Base UI components -│ │ │ │ -│ │ │ ├── hooks/ # Custom hooks -│ │ │ ├── stores/ # Zustand stores -│ │ │ ├── supabase/ # Supabase client -│ │ │ └── utils/ # Utilities -│ │ │ -│ │ └── public/ # Static assets -│ │ -│ ├── shared/ # 공유 코드 (web + mobile) -│ │ ├── hooks/ # Shared hooks -│ │ ├── stores/ # Shared stores -│ │ ├── supabase/ # Supabase queries -│ │ │ └── queries/ # Query functions -│ │ ├── types/ # Shared types -│ │ └── data/ # Mock data -│ │ -│ └── mobile/ # Expo 모바일 앱 (초기 구조) -│ ├── app/ # Expo Router -│ └── components/ # Mobile components -│ -├── docs/ # 문서 -│ ├── architecture/ # 아키텍처 문서 -│ ├── database/ # DB 스키마 문서 -│ ├── design-system/ # 디자인 시스템 -│ ├── testing/ # 테스트 문서 -│ ├── performance/ # 성능 가이드 -│ ├── adr/ # 아키텍처 결정 기록 -│ └── ai-playbook/ # AI 도구 가이드 -│ -├── specs/ # 기능 명세 -│ ├── feature-spec/ # 기능별 명세서 -│ └── 001-scroll-animation/ # Feature spec -│ -├── __tests__/ # 테스트 파일 -│ -├── package.json # Yarn workspaces root -├── yarn.lock # Yarn 4 lock file -└── .yarnrc.yml # Yarn 설정 (node-modules linker) -``` - --- - -## 3. Component Dependency Graph - -![Navigation Flow](../diagrams/navigation-flow.excalidraw.png) - -### 3.1 Home Page Dependencies - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ HOME PAGE DEPENDENCY GRAPH │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ app/page.tsx (SSR) │ -│ │ │ -│ ├──▶ fetchLatestImages() [Server] │ -│ │ │ -│ └──▶ HomeClient.tsx (Client) │ -│ │ │ -│ ├──▶ useInfiniteFilteredImages() │ -│ │ │ │ -│ │ └──▶ fetchUnifiedImages() │ -│ │ │ │ -│ │ └──▶ Supabase │ -│ │ │ -│ ├──▶ Header.tsx │ -│ │ │ │ -│ │ ├──▶ FilterTabs.tsx │ -│ │ │ │ │ -│ │ │ └──▶ filterStore (Zustand) │ -│ │ │ │ -│ │ ├──▶ SearchInput.tsx │ -│ │ │ │ │ -│ │ │ └──▶ searchStore (Zustand) │ -│ │ │ │ -│ │ └──▶ ThemeToggle.tsx │ -│ │ │ │ -│ │ └──▶ next-themes │ -│ │ │ -│ └──▶ ThiingsGrid.tsx │ -│ │ │ -│ ├──▶ CardCell.tsx │ -│ │ │ │ -│ │ └──▶ transitionStore (FLIP) │ -│ │ │ -│ └──▶ useScrollAnimation() │ -│ │ │ -│ └──▶ IntersectionObserver │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - -### 3.2 Detail Page Dependencies - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ DETAIL PAGE DEPENDENCY GRAPH │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Desktop (Modal): │ -│ app/@modal/(.)images/[id]/page.tsx │ -│ │ │ -│ └──▶ ImageDetailModal.tsx │ -│ │ │ -│ ├──▶ transitionStore (FLIP state) │ -│ │ │ -│ └──▶ ImageDetailContent.tsx ◀───┐ │ -│ │ │ -│ Mobile/Direct: │ (공유) │ -│ app/images/[id]/page.tsx │ │ -│ │ │ │ -│ └──▶ ImageDetailPage.tsx ─────────────────┘ │ -│ │ -│ ImageDetailContent.tsx │ -│ │ │ -│ ├──▶ useImageById() │ -│ │ │ │ -│ │ └──▶ fetchImageById() → Supabase │ -│ │ │ -│ ├──▶ HeroSection.tsx │ -│ │ │ │ -│ │ └──▶ GSAP (Ken Burns, Parallax) │ -│ │ │ -│ ├──▶ InteractiveShowcase.tsx │ -│ │ │ │ -│ │ ├──▶ useNormalizedItems() │ -│ │ │ │ -│ │ └──▶ ItemDetailCard.tsx │ -│ │ │ -│ ├──▶ ShopGrid.tsx │ -│ │ │ │ -│ │ └──▶ 수평 캐러셀 │ -│ │ │ -│ └──▶ RelatedImages.tsx │ -│ │ │ -│ └──▶ useRelatedImagesByAccount() │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - +title: System Architecture +owner: llm +status: deprecated +updated: 2026-05-21 +tags: [architecture, obsidian, deprecated] --- -## 4. Module Responsibility Matrix - -| Module | Responsibility | Key Files | Dependencies | -|--------|---------------|-----------|--------------| -| **App Router** | 라우팅, SSR, 레이아웃 | `app/**/*.tsx` | Next.js | -| **Components** | UI 렌더링, 인터랙션 | `lib/components/**` | React, Tailwind | -| **Hooks** | 재사용 로직, 데이터 페칭 | `lib/hooks/**` | React Query | -| **Stores** | 전역 상태 관리 | `lib/stores/**` | Zustand | -| **Queries** | Supabase 데이터 액세스 | `shared/supabase/queries/**` | Supabase | -| **Utils** | 헬퍼 함수 | `lib/utils/**` | - | -| **Types** | 타입 정의 | `shared/types/**` | TypeScript | - ---- - -## 5. Data Flow Architecture - -### 5.1 Read Flow (Query) - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ READ DATA FLOW │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Component │ -│ │ │ -│ │ useInfiniteFilteredImages({ filter, search, limit }) │ -│ ▼ │ -│ React Query │ -│ │ │ -│ │ queryKey: ["images", "infinite", { filter, search, limit }] │ -│ │ │ -│ │ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ │ Cache Check │ │ -│ │ │ │ │ -│ │ │ staleTime > 0? ──YES──▶ Return cached data │ │ -│ │ │ │ │ │ -│ │ │ NO │ │ -│ │ │ │ │ │ -│ │ │ ▼ │ │ -│ │ │ Background refetch (if not fresh) │ │ -│ │ └─────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ │ queryFn: fetchUnifiedImages() │ -│ ▼ │ -│ Supabase Client │ -│ │ │ -│ │ SELECT * FROM post_image │ -│ │ JOIN image ON ... │ -│ │ JOIN post ON ... │ -│ │ WHERE account = filter │ -│ │ ORDER BY created_at DESC │ -│ │ LIMIT 50 │ -│ ▼ │ -│ PostgreSQL │ -│ │ │ -│ │ Query execution │ -│ ▼ │ -│ Response │ -│ │ │ -│ │ Transform: normalizeImage() │ -│ ▼ │ -│ Component Update │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - -### 5.2 State Update Flow - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ STATE UPDATE FLOW │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ User Action (Filter Click) │ -│ │ │ -│ ▼ │ -│ FilterTabs.tsx │ -│ │ │ -│ │ onClick={() => setFilter('blackpinkk.style')} │ -│ ▼ │ -│ filterStore (Zustand) │ -│ │ │ -│ │ state.activeFilter = 'blackpinkk.style' │ -│ │ │ -│ │ Subscribers notified │ -│ ▼ │ -│ useInfiniteFilteredImages() │ -│ │ │ -│ │ queryKey changed: ["images", "infinite", { filter: "blackpinkk..." }] │ -│ │ │ -│ │ React Query detects key change │ -│ ▼ │ -│ Automatic Refetch │ -│ │ │ -│ │ fetchUnifiedImages({ filter: 'blackpinkk.style' }) │ -│ ▼ │ -│ ThiingsGrid re-render │ -│ │ │ -│ │ New data displayed │ -│ ▼ │ -│ UI Updated │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 6. Technology Stack Detail - -### 6.1 Frontend Technologies - -| Category | Technology | Version | Purpose | -|----------|------------|---------|---------| -| Framework | Next.js | 16.0.7 | App Router, SSR, API Routes | -| UI Library | React | 18.3.1 | Component rendering | -| Language | TypeScript | 5.9.3 | Type safety | -| Styling | Tailwind CSS | 3.4.18 | Utility-first CSS | -| State (Client) | Zustand | 4.5.7 | Global state | -| State (Server) | React Query | 5.90.11 | Server state, caching | -| Animation | GSAP | 3.13.0 | Complex animations | -| Animation | Motion | 12.23.12 | Declarative animations | -| Scroll | Lenis | 1.3.15 | Smooth scroll | -| Theme | next-themes | 0.4.6 | Dark mode | - -### 6.2 Backend Technologies - -| Category | Technology | Purpose | -|----------|------------|---------| -| Database | Supabase (PostgreSQL) | Primary data store | -| Auth | Supabase Auth | OAuth providers | -| Storage | Supabase Storage | Image uploads | -| Hosting | Vercel (TBD) | Web deployment | - -### 6.3 Development Tools - -| Category | Technology | Version | Purpose | -|----------|------------|---------|---------| -| Package Manager | Yarn | 4.9.2 | Monorepo workspaces | -| Linting | ESLint | 9.39.1 | Code quality | -| Formatting | Prettier | 3.6.2 | Code formatting | -| Testing | Playwright | - | E2E testing | - ---- - -## 7. Security Architecture - -### 7.1 Authentication Flow - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ OAUTH AUTHENTICATION FLOW │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Client Supabase Auth OAuth Provider │ -│ │ │ │ │ -│ │ 1. signInWithOAuth() │ │ │ -│ │ ─────────────────────▶ │ │ │ -│ │ │ │ │ -│ │ 2. Redirect URL │ │ │ -│ │ ◀───────────────────── │ │ │ -│ │ │ │ │ -│ │ 3. Redirect to Provider │ │ -│ │ ───────────────────────────────────────────────────▶│ │ -│ │ │ │ │ -│ │ 4. User authenticates │ │ -│ │ │ │ │ -│ │ 5. Callback with code │ │ -│ │ ◀───────────────────────────────────────────────────│ │ -│ │ │ │ │ -│ │ 6. Exchange code │ │ │ -│ │ ─────────────────────▶ │ ─────────────────────────▶│ │ -│ │ │ │ │ -│ │ │ 7. Tokens │ │ -│ │ │ ◀─────────────────────────│ │ -│ │ │ │ │ -│ │ 8. Session created │ │ │ -│ │ ◀───────────────────── │ │ │ -│ │ │ │ │ -│ │ 9. JWT stored in cookie│ │ │ -│ │ │ │ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - -### 7.2 Security Measures - -| Area | Measure | Implementation | -|------|---------|----------------| -| Authentication | OAuth 2.0 | Supabase Auth (Kakao, Google, Apple) | -| Authorization | RLS | Supabase Row Level Security | -| Data Validation | Server-side | API Route validation | -| XSS Prevention | React | Automatic escaping | -| CSRF | Next.js | SameSite cookies | - ---- - -## Related Documents +# System Architecture -- [data-pipeline.md](./data-pipeline.md) - 데이터 파이프라인 -- [state-management.md](./state-management.md) - 상태 관리 -- [../database/01-schema-usage.md](../database/01-schema-usage.md) - DB 스키마 -- [../specs/feature-spec/README.md](../../specs/feature-spec/README.md) - 기능 명세 +> **이 문서는 [decoded-docs vault](https://github.com/decodedcorp/decoded-docs)로 이전되었습니다.** +> +> 본문: [`Architecture/_README.md`](https://github.com/decodedcorp/decoded-docs/blob/main/Architecture/_README.md) +> +> Obsidian path: `Architecture/_README` +> +> Sync policy: [Guides/sync-policy](https://github.com/decodedcorp/decoded-docs/blob/main/Guides/sync-policy.md) diff --git a/docs/architecture/assets-project.md b/docs/architecture/assets-project.md index 24b93895..6999f903 100644 --- a/docs/architecture/assets-project.md +++ b/docs/architecture/assets-project.md @@ -1,163 +1,17 @@ --- title: Assets Supabase Project -owner: human -status: approved -updated: 2026-04-25 -tags: [architecture, db, agent] +owner: llm +status: deprecated +updated: 2026-05-21 +tags: [architecture, db, obsidian, deprecated] --- -# Assets Supabase Project (#333) +# Assets Supabase Project -**한 줄 요약**: Pinterest/Instagram 등 외부 플랫폼에서 수집·파싱한 raw_posts 와 파이프라인 중간 상태를 별도 Supabase 프로젝트(`assets`)에 격리. prod 는 admin 이 검증 완료한 데이터만 보유. - -## 왜 분리했나 - -PR #258 의 raw_posts 파이프라인이 prod Supabase 의 `warehouse.raw_post*` 테이블을 직접 사용하면서 두 가지 문제가 누적됐다: - -1. **운영 경계 모호** — 검증된 prod posts 와 unverified raw 데이터가 같은 프로젝트에 섞여 있어 백업/복원, RLS, 권한 분리, 모니터링 모두 한꺼번에 다뤄야 했다. -2. **prod 스키마 오염** — 파이프라인 상태머신, dispatch 로그, parse 결과 등 운영성 메타데이터가 prod 의 schema diff 를 키웠다. - -**원칙**: 두 프로젝트는 cross-ID 참조 없이 완전 독립. prod 는 assets 의 존재를 모르고, assets 도 prod 를 모른다. - -## 구조 - -``` -┌──────────────────────────────────────────────┐ -│ Cloud Supabase: ASSETS (파이프라인 스테이징) │ -│ public.raw_post_sources │ -│ public.raw_posts │ -│ └ status pipeline_status │ -│ └ verified_at / verified_by │ -│ public.pipeline_events │ -│ + RLS: service-role only │ -└──────────────▲──────────────┬─────────────────┘ - │ write │ read - │ │ - ┌───────────┴────┐ ┌───────┴──────────────┐ - │ ai-server │ │ api-server │ ──► R2 (raw bucket, 공유) - │ ARQ pipeline │ │ raw_posts domain │ - │ ↓ │ │ verify endpoint │ - │ status=COMPLETED ──────────► │ - └────────────────┘ └────────┬─────────────┘ - │ on verify ✓ - ▼ INSERT -┌──────────────────────────────────────────────┐ -│ Cloud Supabase: PROD (검증본 — 실서비스) │ -│ public.posts (검증된 raw_post 의 복사본) │ -│ public.users / public.solutions / ... │ -│ public.artists / public.groups / public.brands│ -│ (#335 warehouse → public 이관) │ -└──────────────────────────────────────────────┘ -``` - -## 핵심 설계 결정 - -### 1. 검증(verify) 이 최종 액션 — "승격" 개념 없음 - -admin 이 COMPLETED raw_post 를 검증하면 **한 번의 액션**으로: - -- assets `status = COMPLETED → VERIFIED` (production 환경에서만) -- prod `public.posts` 에 새 row INSERT - -별도의 "promote" 단계나 후처리 없음. `verified_at`/`verified_by` 가 운영적 진실의 기준. - -### 2. 중복 방지는 admin 운영 책임 - -prod `public.posts` 에 **DB UNIQUE 제약을 걸지 않는다**. admin UI 가 같은 raw_post 를 두 번 검증할 가능성을 시각적으로 막고, 실제로 중복이 발생하면 admin 이 수동 정리한다. 이유: - -- 동일한 이미지가 여러 platform/external_id 로 들어올 수 있어 DB 레벨 dedupe 가 false positive 다발 -- VERIFIED 가 cross-project ID(`source_raw_post_id`) 로 prod 에 새는 걸 막아야 함 - -### 3. 5-state 파이프라인 상태머신 - -``` -NOT_STARTED → IN_PROGRESS → COMPLETED ──(admin verify)──► VERIFIED - ↘ ERROR -``` - -| 상태 | 의미 | 누가 전이시키는가 | -|---|---|---| -| `NOT_STARTED` | row 가 막 INSERT 됨 (default) | DB default | -| `IN_PROGRESS` | fetch / parse 진행 중 | ai-server (현재 architecture 에선 거의 사용되지 않음 — 단일 트랜잭션) | -| `COMPLETED` | 자동 처리 완료 (R2 업로드 + 기본 메타) | ai-server `upsert_raw_posts` | -| `VERIFIED` | admin 검증 완료, prod 에 반영됨 | api-server `verify_raw_post` | -| `ERROR` | 어느 단계든 실패 | ai-server `mark_raw_post_error` | - -전환 시 `pipeline_events` 에 감사 row 가 동일 트랜잭션으로 INSERT 된다. - -### 4. APP_ENV 분기로 cloud assets 보호 - -로컬 개발자가 cloud assets(공유) 데이터를 오염시키지 않도록 `APP_ENV=local` 일 때 verify 엔드포인트는 **prod INSERT 만** 수행하고 assets status write 를 스킵한다. production 배포에서만 status=VERIFIED 가 기록된다. - -## verify 시퀀스 - -``` -[Admin] - │ /admin/raw-posts → COMPLETED 탭에서 행 클릭 → "검증" 버튼 - ▼ -[web Next.js] - │ POST /api/admin/raw-posts/items/{id}/verify - ▼ -[api-server proxy] - │ Bearer 위임 - ▼ -[api-server raw_posts::verify_raw_post] - ├─ assets_db.find_by_id(id) ← assets pool - │ status == COMPLETED ? → 아니면 400 - │ - ├─ posts::create_post_from_raw(prod_db, admin_id, raw, dto) ← prod pool - │ INSERT INTO public.posts ... RETURNING * - │ - └─ if APP_ENV == Production: - assets txn: - UPDATE public.raw_posts SET status='VERIFIED' WHERE id=$1 - INSERT INTO public.pipeline_events (raw_post_id, from_status, - to_status, actor) VALUES (...) - commit - (실패 시 prod 는 이미 INSERT 된 상태 — admin 이 시각적 dedupe) -``` - -## 실패 모드 - -| 시나리오 | 영향 | 완화 | -|---|---|---| -| cloud assets 장애 | `/api/v1/raw-posts/*` 503, posts CRUD 정상 | pool 분리. 장애 메시지에서 명시 | -| verify step 2 ✓ + step 3 ✗ | prod 에 새 row, assets 는 여전히 COMPLETED → 다음 클릭에서 중복 INSERT 가능 | admin 시각적 dedupe (설계 결정 #2). loud error log | -| assets URL stale (로컬) | DATABASE_URL fallback + WARN | `APP_ENV=production` 에서는 panic | -| 파이프라인 ERROR 재시도 | `mark_raw_post_error` 가 VERIFIED 는 보존 | `WHERE status <> 'VERIFIED'` 가드 | - -## 마이그레이션 (#335) - -prod 에서 `warehouse` 스키마를 완전 드롭하고 살아남는 엔티티 테이블(artists/groups/brands/group_members/admin_audit_log/instagram_accounts) 을 `public` 스키마로 SET SCHEMA. 자세한 절차는 [`docs/DATABASE-MIGRATIONS.md`](../DATABASE-MIGRATIONS.md) 와 `supabase/migrations/20260425000001_drop_warehouse_and_promote_entities.sql`. - -## 관련 파일 - -- 스키마: `supabase-assets/migrations/20260424120000_initial.sql` (초기) + `20260426130000_drop_r2_columns.sql` (#347 r2_url/r2_key 드롭) -- api-server: `packages/api-server/src/domains/raw_posts/{handlers,service,dto}.rs` -- api-server entity: `packages/api-server/src/entities/{assets_raw_posts,assets_raw_post_sources}.rs` -- api-server config: `packages/api-server/src/config.rs::AppEnv` / `AssetsDatabaseConfig` -- api-server state: `packages/api-server/src/app_state.rs::AppState::assets_db` -- ai-server: `packages/ai-server/src/services/raw_posts/repository.py` -- ai-server pool: `packages/ai-server/src/managers/database/pool.py::DatabaseManager._resolve_dsn` -- web admin: `packages/web/app/admin/raw-posts/page.tsx` -- web hook: `packages/web/lib/api/admin/raw-posts.ts` -- 환경: [`docs/agent/environments.md`](../agent/environments.md) - -## 컬럼 의미 (raw_posts 본체) - -| 컬럼 | 의미 | 케이스별 | -|---|---|---| -| `external_url` | 외부 출처 페이지 URL (Pinterest 핀 페이지 등) | Pinterest/IG: 핀/포스트 URL — 합성: NULL | -| `image_url` | **이미지 위치 URL — 실질적으로 R2 퍼블릭 URL**. ai-server 가 R2 업로드 후 채움 (#347) | 모든 케이스 동일 | -| `caption` | 텍스트 (Pinterest description, IG caption 등) | 합성 케이스: NULL 또는 prompt | -| `author_name` | 저자/소스 명 (Pinterest pinner 등) | — | -| `platform_metadata` | 플랫폼별 자유 메타 (saves, board_id, hashtags 등) | 합성 케이스: 보통 NULL | -| `parse_result` | 비전파싱 결과 (아이템 bbox, 브랜드 후보 등) | 모든 케이스 동일 — Vision 결과 | -| `dispatch_id` | ai-server scheduler 의 1회 dispatch 추적 키 | — | - -> #347 이전: `image_url`(외부 CDN) + `r2_url`(R2 복사본) + `r2_key`(R2 path) 3개 컬럼이었으나 단일화. 외부 CDN URL 은 운영상 거의 미사용이라 드롭, R2 URL 은 `image_url` 한 컬럼으로 통합. - -## 변경 이력 - -- 2026-04-26: r2_url/r2_key 컬럼 드롭, image_url 단일화 (#347) -- 2026-04-25: 초기 작성 — 두 프로젝트 분리, 5-state 상태머신, verify 플로우 (#333) +> **이 문서는 [decoded-docs vault](https://github.com/decodedcorp/decoded-docs)로 이전되었습니다.** +> +> 본문: [`Architecture/assets-project.md`](https://github.com/decodedcorp/decoded-docs/blob/main/Architecture/assets-project.md) +> +> Obsidian path: `Architecture/assets-project` +> +> Sync policy: [Guides/sync-policy](https://github.com/decodedcorp/decoded-docs/blob/main/Guides/sync-policy.md) diff --git a/docs/architecture/data-pipeline.md b/docs/architecture/data-pipeline.md index 5c336210..e759900f 100644 --- a/docs/architecture/data-pipeline.md +++ b/docs/architecture/data-pipeline.md @@ -1,523 +1,17 @@ -# Data Pipeline - -> Version: 1.0.0 -> Last Updated: 2026-01-14 -> Purpose: 데이터 수집, 변환, 캐싱 파이프라인 문서화 - ---- - -## Overview - -이 문서는 Decoded 앱의 데이터 흐름을 설명합니다. 외부 소스에서 데이터 수집, DB 저장, 프론트엔드 표시까지의 전체 파이프라인을 다룹니다. - ---- - -## 1. Data Collection Pipeline - -### 1.1 Ingestion Flow - -![AI Creation Pipeline](../diagrams/ai-creation-pipeline.excalidraw.png) - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ DATA INGESTION PIPELINE │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────┐ │ -│ │ Instagram │ │ -│ │ Posts │ │ -│ └────────┬────────┘ │ -│ │ │ -│ │ Scraper (Backend Service) │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ IMAGE STORAGE │ │ -│ │ │ │ -│ │ S3 / Supabase Storage │ │ -│ │ └── Original images saved │ │ -│ │ └── image_url generated │ │ -│ │ │ │ -│ └─────────────────────────────────┬───────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ AI DETECTION PIPELINE │ │ -│ │ │ │ -│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ -│ │ │ Object Detection │ │ Brand Detection │ │ Price Extraction│ │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ │ • Fashion items │ │ • Brand names │ │ • Price values │ │ │ -│ │ │ • Bounding boxes│ │ • Logos │ │ • Currency │ │ │ -│ │ │ • Categories │ │ • Confidence │ │ • Links │ │ │ -│ │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │ -│ │ │ │ │ │ │ -│ │ └────────────────────┼────────────────────┘ │ │ -│ │ │ │ │ -│ │ ▼ │ │ -│ │ Cropped Images Generated │ │ -│ │ └── cropped_image_path │ │ -│ │ │ │ -│ └─────────────────────────────────┬───────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ SUPABASE DATABASE │ │ -│ │ │ │ -│ │ Tables: │ │ -│ │ • image - 원본 이미지 메타데이터 │ │ -│ │ • post - 소셜 미디어 포스트 정보 │ │ -│ │ • item - 감지된 아이템 정보 │ │ -│ │ • post_image - 포스트-이미지 연결 │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - -### 1.2 Database Schema Relationships - -![Entity Relationship Diagram](../diagrams/entity-relationship.excalidraw.png) - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ ENTITY RELATIONSHIP DIAGRAM │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────┐ ┌──────────────────────┐ │ -│ │ post │ │ image │ │ -│ ├──────────────────────┤ ├──────────────────────┤ │ -│ │ id (PK) │ │ id (PK) │ │ -│ │ account │ ┌───▶│ image_hash │ │ -│ │ article │ │ │ image_url │ │ -│ │ metadata[] │ │ │ with_items │ │ -│ │ ts │ │ │ status │ │ -│ │ created_at │ │ │ created_at │ │ -│ └──────────┬───────────┘ │ └──────────┬───────────┘ │ -│ │ │ │ │ -│ │ 1:N │ │ 1:N │ -│ ▼ │ ▼ │ -│ ┌──────────────────────┐ │ ┌──────────────────────┐ │ -│ │ post_image │────┘ │ item │ │ -│ ├──────────────────────┤ ├──────────────────────┤ │ -│ │ post_id (FK) ────────┼─────────│ id (PK) │ │ -│ │ image_id (FK) ───────┤ │ image_id (FK) ───────┤ │ -│ │ item_locations (JSON)│ │ product_name │ │ -│ │ curated_item_ids │ │ brand │ │ -│ │ created_at │ │ price │ │ -│ └──────────────────────┘ │ center (JSON) │ │ -│ │ bboxes (JSON) │ │ -│ │ citations[] │ │ -│ │ metadata[] │ │ -│ │ status │ │ -│ │ created_at │ │ -│ └──────────────────────┘ │ -│ │ -│ 관계: │ -│ • post 1:N post_image (한 포스트에 여러 이미지) │ -│ • image 1:N post_image (한 이미지가 여러 포스트에) │ -│ • image 1:N item (한 이미지에 여러 아이템) │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 2. Data Transformation Pipeline - -### 2.1 Transformation Functions - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ DATA TRANSFORMATION PIPELINE │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Supabase Query Result │ -│ │ │ -│ │ Raw database rows │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ normalizeImage() │ │ -│ │ File: shared/supabase/queries/images-adapter.ts │ │ -│ │ │ │ -│ │ Input: DbImageRow │ │ -│ │ Output: ImageWithPostId │ │ -│ │ │ │ -│ │ Transformations: │ │ -│ │ • id → id │ │ -│ │ • image_url → imageUrl │ │ -│ │ • with_items → withItems │ │ -│ │ • created_at → createdAt │ │ -│ │ • (join) post.account → postAccount │ │ -│ │ • (join) post_image.created_at → postImageCreatedAt │ │ -│ │ │ │ -│ └─────────────────────────────────┬───────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ normalizeItem() │ │ -│ │ File: packages/web/lib/hooks/useNormalizedItems.ts │ │ -│ │ │ │ -│ │ Input: DbItemRow + post_image.item_locations │ │ -│ │ Output: UiItem │ │ -│ │ │ │ -│ │ Transformations: │ │ -│ │ • item.center OR item_locations[id] → position │ │ -│ │ • product_name → productName │ │ -│ │ • cropped_image_path → croppedImageUrl │ │ -│ │ • bboxes → boundingBoxes │ │ -│ │ • citations → purchaseUrls │ │ -│ │ │ │ -│ └─────────────────────────────────┬───────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ React Component │ -│ │ │ -│ │ Normalized data ready for rendering │ -│ ▼ │ -│ UI Display │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - -### 2.2 Type Definitions - -```typescript -// Database Types (from Supabase) -interface DbImageRow { - id: string; - image_hash: string; - image_url: string | null; - with_items: boolean; - status: 'pending' | 'extracted' | 'skipped' | 'extracted_metadata'; - created_at: string; -} - -interface DbItemRow { - id: number; - image_id: string; - product_name: string | null; - brand: string | null; - price: string | null; - center: [number, number] | null; - bboxes: number[][] | null; - citations: string[] | null; - metadata: string[] | null; - status: string | null; - created_at: string; -} - -// Normalized Types (for UI) -interface ImageWithPostId { - id: string; - imageUrl: string; - withItems: boolean; - status: string; - createdAt: string; - postId: string; - postSource: 'post' | 'legacy'; - postAccount: string; - postImageCreatedAt: string; - postCreatedAt: string; -} - -interface UiItem { - id: number; - imageId: string; - productName: string; - brand: string; - price: string; - position: { x: number; y: number }; - boundingBoxes: BoundingBox[]; - purchaseUrls: string[]; - croppedImageUrl: string; -} -``` - ---- - -## 3. Query Functions - -### 3.1 Core Query Functions - -| Function | File | Input | Output | Purpose | -|----------|------|-------|--------|---------| -| `fetchLatestImages` | `images.ts` | limit | ImageRow[] | 최신 이미지 조회 (SSR) | -| `fetchImageById` | `images.ts` | id | ImageDetail | 단일 이미지 + 관계 조회 | -| `fetchUnifiedImages` | `images-adapter.ts` | options | ImagePage | 통합 이미지 조회 (필터/검색) | -| `fetchOrphanImages` | `images-orphan.ts` | options | ImageRow[] | 고아 이미지 조회 | -| `fetchRelatedImagesByAccount` | `images.ts` | account, excludeId | ImageRow[] | 관련 이미지 조회 | - -### 3.2 fetchUnifiedImages 상세 - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ fetchUnifiedImages() FLOW │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Input Parameters: │ -│ { │ -│ filter: 'all' | 'newjeanscloset' | 'blackpinkk.style', │ -│ search: string, │ -│ limit: 50, │ -│ cursor: string | null, │ -│ deduplicateByImageId: boolean │ -│ } │ -│ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ Step 1: Query post_image with filters │ │ -│ │ │ │ -│ │ SELECT pi.*, p.account, p.created_at as post_created_at, │ │ -│ │ i.* FROM post_image pi │ │ -│ │ JOIN image i ON pi.image_id = i.id │ │ -│ │ JOIN post p ON pi.post_id = p.id │ │ -│ │ WHERE p.account = :filter (if not 'all') │ │ -│ │ ORDER BY pi.created_at DESC │ │ -│ │ LIMIT :limit │ │ -│ │ │ │ -│ └─────────────────────────────────┬───────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ Step 2: Fetch orphan images (if filter is 'all') │ │ -│ │ │ │ -│ │ SELECT * FROM image │ │ -│ │ WHERE id NOT IN (SELECT image_id FROM post_image) │ │ -│ │ ORDER BY created_at DESC │ │ -│ │ │ │ -│ └─────────────────────────────────┬───────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ Step 3: Merge and deduplicate │ │ -│ │ │ │ -│ │ • Combine post_image results with orphan images │ │ -│ │ • If deduplicateByImageId: remove duplicate image_ids │ │ -│ │ • Sort by created_at │ │ -│ │ │ │ -│ └─────────────────────────────────┬───────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ Output: │ -│ { │ -│ items: ImageWithPostId[], │ -│ nextCursor: string | null, │ -│ hasMore: boolean, │ -│ stats: { fromPostImage: number, fromOrphans: number } │ -│ } │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 4. React Query Caching Strategy - -![Filter Data Flow](../diagrams/filter-data-flow.excalidraw.png) - -### 4.1 Cache Configuration - -```typescript -// lib/react-query/client.ts - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 60 * 1000, // 1분 동안 fresh - gcTime: 5 * 60 * 1000, // 5분 후 garbage collection - retry: 1, // 1회 재시도 - refetchOnWindowFocus: false, // 포커스 시 refetch 안함 - }, - }, -}); -``` - -### 4.2 Query Key Structure - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ QUERY KEY HIERARCHY │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ["images"] │ -│ │ │ -│ ├── ["images", "infinite", { filter, search, limit }] │ -│ │ │ │ -│ │ └── 무한 스크롤 이미지 목록 │ -│ │ • useInfiniteFilteredImages() │ -│ │ • fetchUnifiedImages() │ -│ │ │ -│ ├── ["images", "latest", { limit }] │ -│ │ │ │ -│ │ └── 최신 이미지 (SSR 초기 데이터) │ -│ │ • fetchLatestImages() │ -│ │ │ -│ └── ["image", id] │ -│ │ │ -│ └── 단일 이미지 상세 │ -│ • useImageById() │ -│ • fetchImageById() │ -│ │ -│ ["related", account, excludeId] │ -│ │ │ -│ └── 관련 이미지 │ -│ • useRelatedImagesByAccount() │ -│ • fetchRelatedImagesByAccount() │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - -### 4.3 Cache Invalidation Patterns - -| Trigger | Invalidated Keys | Action | -|---------|------------------|--------| -| Filter 변경 | `["images", "infinite", *]` | 자동 refetch | -| Search 입력 | `["images", "infinite", *]` | debounced refetch | -| 새 이미지 업로드 | `["images"]` | 전체 invalidate | -| 이미지 수정 | `["image", id]` | 해당 키만 invalidate | - --- - -## 5. Data Flow Hooks - -### 5.1 useInfiniteFilteredImages - -```typescript -// lib/hooks/useImages.ts - -export function useInfiniteFilteredImages(options: { - filter: FilterType; - search: string; - limit?: number; - initialData?: ImagePage; -}) { - const { filter, search, limit = 50, initialData } = options; - - return useInfiniteQuery({ - queryKey: ['images', 'infinite', { filter, search, limit }], - queryFn: ({ pageParam }) => - fetchUnifiedImages({ - filter, - search, - limit, - cursor: pageParam, - deduplicateByImageId: true, - }), - initialPageParam: null, - getNextPageParam: (lastPage) => - lastPage.hasMore ? lastPage.nextCursor : undefined, - initialData: initialData - ? { pages: [initialData], pageParams: [null] } - : undefined, - staleTime: 60 * 1000, - }); -} -``` - -### 5.2 Hook → Store → Query 연결 - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ HOOK-STORE-QUERY CONNECTION │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Component │ -│ │ │ -│ │ const filter = useFilterStore(state => state.activeFilter) │ -│ │ const search = useSearchStore(state => state.debouncedQuery) │ -│ │ │ -│ │ const { data, fetchNextPage } = useInfiniteFilteredImages({ │ -│ │ filter, │ -│ │ search, │ -│ │ }); │ -│ │ │ -│ │ // Store 변경 시 자동으로 queryKey 변경 → refetch │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ filterStore.setFilter('blackpinkk.style') │ │ -│ │ │ │ │ -│ │ │ Zustand state update │ │ -│ │ ▼ │ │ -│ │ Component re-render (filter value changed) │ │ -│ │ │ │ │ -│ │ │ useInfiniteFilteredImages called with new filter │ │ -│ │ ▼ │ │ -│ │ React Query detects queryKey change │ │ -│ │ │ │ │ -│ │ │ ["images", "infinite", { filter: "blackpinkk.style", ... }] │ │ -│ │ ▼ │ │ -│ │ Cache miss → fetchUnifiedImages() │ │ -│ │ │ │ │ -│ │ │ New data fetched │ │ -│ │ ▼ │ │ -│ │ Component re-render with new data │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - +title: Data Pipeline +owner: llm +status: deprecated +updated: 2026-05-21 +tags: [architecture, db, obsidian, deprecated] --- -## 6. Error Handling - -### 6.1 Error Types - -| Error Type | HTTP Code | Handling | UI Response | -|------------|-----------|----------|-------------| -| Network Error | - | Retry 1회 | "연결 확인" + 재시도 버튼 | -| Not Found | 404 | No retry | EmptyState 표시 | -| Server Error | 500 | Retry 1회 | "잠시 후 다시" 메시지 | -| Auth Error | 401 | No retry | 로그인 리다이렉트 | - -### 6.2 Error Boundary - -```typescript -// Error handling in useInfiniteFilteredImages -const { data, error, isError } = useInfiniteFilteredImages({...}); - -if (isError) { - // ErrorState component 표시 - return ; -} -``` - ---- - -## 7. Performance Optimizations - -### 7.1 Implemented Optimizations - -| Optimization | Description | File | -|--------------|-------------|------| -| Cursor Pagination | Offset 대신 cursor 사용 | `images-adapter.ts` | -| Deduplication | 중복 이미지 제거 옵션 | `images-adapter.ts` | -| SSR Initial Data | 서버에서 초기 데이터 로드 | `app/page.tsx` | -| Stale Time | 1분간 캐시 유지 | `react-query/client.ts` | -| Keep Previous | 필터 변경 시 이전 데이터 유지 | `useImages.ts` | - -### 7.2 Query Optimization Tips - -```typescript -// 불필요한 refetch 방지 -staleTime: 60 * 1000, -refetchOnWindowFocus: false, - -// 이전 데이터 유지 (깜빡임 방지) -placeholderData: keepPreviousData, - -// 선택적 필드 로드 -select: (data) => data.pages.flatMap(page => page.items), -``` - ---- - -## Related Documents +# Data Pipeline -- [README.md](./README.md) - 시스템 아키텍처 -- [state-management.md](./state-management.md) - 상태 관리 -- [../database/01-schema-usage.md](../database/01-schema-usage.md) - DB 스키마 -- [../database/03-data-flow.md](../database/03-data-flow.md) - DB 데이터 흐름 +> **이 문서는 [decoded-docs vault](https://github.com/decodedcorp/decoded-docs)로 이전되었습니다.** +> +> 본문: [`Architecture/data-pipeline.md`](https://github.com/decodedcorp/decoded-docs/blob/main/Architecture/data-pipeline.md) +> +> Obsidian path: `Architecture/data-pipeline` +> +> Sync policy: [Guides/sync-policy](https://github.com/decodedcorp/decoded-docs/blob/main/Guides/sync-policy.md) diff --git a/docs/architecture/state-management.md b/docs/architecture/state-management.md index f8d06ada..760c837d 100644 --- a/docs/architecture/state-management.md +++ b/docs/architecture/state-management.md @@ -1,597 +1,17 @@ -# State Management - -> Version: 1.0.0 -> Last Updated: 2026-01-14 -> Purpose: Zustand + React Query 상태 관리 패턴 문서화 - ---- - -## Overview - -Decoded 앱은 두 가지 상태 관리 도구를 사용합니다: -- **Zustand**: 클라이언트 전역 상태 (필터, 검색, UI 상태) -- **React Query**: 서버 상태 (데이터 페칭, 캐싱, 동기화) - ---- - -## 1. State Architecture - -### 1.1 State Layer Diagram - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ STATE ARCHITECTURE │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ COMPONENT LAYER │ │ -│ │ │ │ -│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ -│ │ │ Local │ │ Local │ │ Local │ │ │ -│ │ │ State │ │ State │ │ State │ │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ │ • useState │ │ • useRef │ │ • useReducer│ │ │ -│ │ │ • UI toggle │ │ • DOM refs │ │ • Complex │ │ │ -│ │ │ • Form data │ │ • Timers │ │ local │ │ │ -│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ │ Props / Context │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ GLOBAL CLIENT STATE │ │ -│ │ (Zustand Stores) │ │ -│ │ │ │ -│ │ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ │ -│ │ │ filterStore │ │ searchStore │ │transitionStore │ │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ │ • activeFilter │ │ • query │ │ • selectedId │ │ │ -│ │ │ • category │ │ • debouncedQ │ │ • originState │ │ │ -│ │ │ • mediaId │ │ │ │ • rect │ │ │ -│ │ │ • castId │ │ │ │ │ │ │ -│ │ └────────────────┘ └────────────────┘ └────────────────┘ │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ │ Query Keys │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ SERVER STATE │ │ -│ │ (React Query) │ │ -│ │ │ │ -│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ -│ │ │ Query Cache │ │ │ -│ │ │ │ │ │ -│ │ │ ["images", "infinite", {...}] → ImagePage[] │ │ │ -│ │ │ ["image", id] → ImageDetail │ │ │ -│ │ │ ["related", account, id] → ImageRow[] │ │ │ -│ │ │ │ │ │ -│ │ └─────────────────────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ -│ │ │ Mutation Cache │ │ │ -│ │ │ │ │ │ -│ │ │ (Future) vote, comment, upload mutations │ │ │ -│ │ │ │ │ │ -│ │ └─────────────────────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ │ Supabase Client │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────────┐ │ -│ │ REMOTE DATA SOURCE │ │ -│ │ (Supabase) │ │ -│ │ │ │ -│ │ PostgreSQL Database │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 2. Zustand Stores - -### 2.1 filterStore - -**File**: `lib/stores/filterStore.ts` / `shared/stores/filterStore.ts` - -```typescript -// 현재 구현 -interface FilterState { - activeFilter: 'all' | 'newjeanscloset' | 'blackpinkk.style'; - setFilter: (filter: FilterType) => void; -} - -const useFilterStore = create((set) => ({ - activeFilter: 'all', - setFilter: (filter) => set({ activeFilter: filter }), -})); - -// 사용 예시 -function FilterTabs() { - const { activeFilter, setFilter } = useFilterStore(); - - return ( -
- - {/* ... */} -
- ); -} -``` - -**Future 확장 (D-02 계층형 필터)**: -```typescript -interface HierarchicalFilterState { - // 현재 선택값 - category: CategoryType | null; - mediaId: string | null; - castId: string | null; - contextType: ContextType | null; - - // Breadcrumb - breadcrumb: FilterBreadcrumb[]; - - // Actions - setCategory: (cat: CategoryType | null) => void; - setMedia: (id: string | null) => void; - setCast: (id: string | null) => void; - setContext: (ctx: ContextType | null) => void; - clearAll: () => void; - navigateToBreadcrumb: (level: number) => void; -} -``` - -### 2.2 searchStore - -**File**: `lib/stores/searchStore.ts` / `shared/stores/searchStore.ts` - -```typescript -interface SearchState { - query: string; - debouncedQuery: string; - setQuery: (query: string) => void; - setDebouncedQuery: (query: string) => void; -} - -const useSearchStore = create((set) => ({ - query: '', - debouncedQuery: '', - setQuery: (query) => set({ query }), - setDebouncedQuery: (debouncedQuery) => set({ debouncedQuery }), -})); - -// SearchInput 컴포넌트에서 사용 -function SearchInput() { - const { query, setQuery, setDebouncedQuery } = useSearchStore(); - - // 250ms debounce - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedQuery(query); - }, 250); - return () => clearTimeout(timer); - }, [query]); - - return ( - setQuery(e.target.value)} - /> - ); -} -``` - -### 2.3 transitionStore - -**File**: `lib/stores/transitionStore.ts` - -```typescript -interface TransitionState { - selectedId: string | null; - originState: Flip.FlipState | null; - rect: DOMRect | null; - - setSelectedId: (id: string | null) => void; - setOriginState: (state: Flip.FlipState | null) => void; - setRect: (rect: DOMRect | null) => void; - clear: () => void; -} - -const useTransitionStore = create((set) => ({ - selectedId: null, - originState: null, - rect: null, - - setSelectedId: (id) => set({ selectedId: id }), - setOriginState: (state) => set({ originState: state }), - setRect: (rect) => set({ rect }), - clear: () => set({ selectedId: null, originState: null, rect: null }), -})); - -// CardCell에서 클릭 시 상태 저장 -function CardCell({ image }) { - const { setSelectedId, setOriginState } = useTransitionStore(); - - const handleClick = (e) => { - const element = e.currentTarget; - const state = Flip.getState(element); - setSelectedId(image.id); - setOriginState(state); - }; - - return
...
; -} - -// Detail Modal에서 애니메이션 실행 -function ImageDetailModal() { - const { originState, clear } = useTransitionStore(); - - useEffect(() => { - if (originState) { - Flip.from(originState, { - duration: 0.5, - ease: 'power2.inOut', - }); - } - return () => clear(); - }, []); -} -``` - ---- - -## 3. React Query Patterns - -### 3.1 Query Client Configuration - -**File**: `lib/react-query/client.ts` - -```typescript -import { QueryClient } from '@tanstack/react-query'; - -export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 60 * 1000, // 1분 - gcTime: 5 * 60 * 1000, // 5분 - retry: 1, - refetchOnWindowFocus: false, - refetchOnReconnect: true, - }, - mutations: { - retry: 1, - }, - }, -}); -``` - -### 3.2 Query Hooks - -**File**: `lib/hooks/useImages.ts` - -```typescript -// 무한 스크롤 이미지 조회 -export function useInfiniteFilteredImages(options: { - filter: FilterType; - search: string; - limit?: number; - initialData?: ImagePage; -}) { - return useInfiniteQuery({ - queryKey: ['images', 'infinite', { - filter: options.filter, - search: options.search, - limit: options.limit - }], - queryFn: ({ pageParam }) => fetchUnifiedImages({ - filter: options.filter, - search: options.search, - limit: options.limit ?? 50, - cursor: pageParam, - }), - initialPageParam: null, - getNextPageParam: (lastPage) => - lastPage.hasMore ? lastPage.nextCursor : undefined, - initialData: options.initialData - ? { pages: [options.initialData], pageParams: [null] } - : undefined, - staleTime: 60 * 1000, - placeholderData: keepPreviousData, // 필터 변경 시 이전 데이터 유지 - }); -} - -// 단일 이미지 조회 -export function useImageById(id: string) { - return useQuery({ - queryKey: ['image', id], - queryFn: () => fetchImageById(id), - enabled: !!id, - staleTime: 60 * 1000, - }); -} - -// 관련 이미지 조회 -export function useRelatedImagesByAccount( - account: string, - excludeId: string -) { - return useQuery({ - queryKey: ['related', account, excludeId], - queryFn: () => fetchRelatedImagesByAccount(account, excludeId), - enabled: !!account && !!excludeId, - staleTime: 60 * 1000, - }); -} -``` - -### 3.3 Query Key Factory Pattern - -```typescript -// lib/react-query/keys.ts - -export const imageKeys = { - all: ['images'] as const, - lists: () => [...imageKeys.all, 'list'] as const, - list: (filters: ImageFilters) => [...imageKeys.lists(), filters] as const, - infinite: (filters: ImageFilters) => [...imageKeys.all, 'infinite', filters] as const, - details: () => [...imageKeys.all, 'detail'] as const, - detail: (id: string) => [...imageKeys.details(), id] as const, -}; - -export const relatedKeys = { - all: ['related'] as const, - byAccount: (account: string, excludeId: string) => - [...relatedKeys.all, account, excludeId] as const, -}; - -// 사용 -useInfiniteQuery({ - queryKey: imageKeys.infinite({ filter, search, limit }), - // ... -}); -``` - --- - -## 4. Store ↔ Query Integration - -### 4.1 State Sync Flow - -``` -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ STORE ↔ QUERY SYNC FLOW │ -├─────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ User Action │ -│ (Filter Click) │ -│ │ │ -│ ▼ │ -│ ┌────────────────────────────────────────────────────────────────────────┐ │ -│ │ FilterTabs.tsx │ │ -│ │ │ │ -│ │ onClick={() => setFilter('blackpinkk.style')} │ │ -│ └────────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌────────────────────────────────────────────────────────────────────────┐ │ -│ │ filterStore (Zustand) │ │ -│ │ │ │ -│ │ state.activeFilter = 'blackpinkk.style' │ │ -│ │ │ │ -│ │ → All subscribers notified │ │ -│ └────────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌────────────────────────────────────────────────────────────────────────┐ │ -│ │ HomeClient.tsx (subscriber) │ │ -│ │ │ │ -│ │ const filter = useFilterStore(s => s.activeFilter); │ │ -│ │ // filter 값이 변경됨 → 컴포넌트 re-render │ │ -│ │ │ │ -│ │ const { data } = useInfiniteFilteredImages({ filter, search }); │ │ -│ │ // queryKey가 변경됨 │ │ -│ └────────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌────────────────────────────────────────────────────────────────────────┐ │ -│ │ React Query │ │ -│ │ │ │ -│ │ queryKey: ["images", "infinite", { filter: "blackpinkk.style", ... }] │ │ -│ │ │ │ -│ │ Cache lookup: │ │ -│ │ • Cache miss → fetchUnifiedImages() 호출 │ │ -│ │ • Cache hit + fresh → 캐시 데이터 반환 │ │ -│ │ • Cache hit + stale → 캐시 반환 + background refetch │ │ -│ └────────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌────────────────────────────────────────────────────────────────────────┐ │ -│ │ ThiingsGrid re-render │ │ -│ │ │ │ -│ │ 새로운 데이터로 그리드 업데이트 │ │ -│ └────────────────────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────┘ -``` - -### 4.2 Component Usage Pattern - -```typescript -// HomeClient.tsx - -function HomeClient({ initialData }: { initialData: ImagePage }) { - // 1. Zustand stores에서 필터/검색 상태 구독 - const filter = useFilterStore((state) => state.activeFilter); - const search = useSearchStore((state) => state.debouncedQuery); - - // 2. React Query로 데이터 페칭 (store 값이 queryKey에 포함) - const { - data, - fetchNextPage, - hasNextPage, - isFetchingNextPage - } = useInfiniteFilteredImages({ - filter, - search, - limit: 50, - initialData, - }); - - // 3. 데이터 가공 - const images = useMemo(() => - data?.pages.flatMap(page => page.items) ?? [], - [data] - ); - - // 4. 렌더링 - return ( - - ); -} -``` - +title: State Management +owner: llm +status: deprecated +updated: 2026-05-21 +tags: [architecture, ui, obsidian, deprecated] --- -## 5. State Persistence - -### 5.1 Current Persistence - -| Store | Persistence | Location | -|-------|------------|----------| -| filterStore | URL params | `?filter=xxx` | -| searchStore | URL params | `?q=xxx` | -| transitionStore | Memory only | - | -| React Query | Memory (gcTime) | 5분 캐시 | - -### 5.2 Future Persistence (사용자 설정) - -```typescript -// 로그인 사용자 설정 저장 예시 -const useUserPreferences = create( - persist( - (set) => ({ - theme: 'system', - language: 'ko', - setTheme: (theme) => set({ theme }), - setLanguage: (language) => set({ language }), - }), - { - name: 'user-preferences', - storage: createJSONStorage(() => localStorage), - } - ) -); -``` - ---- - -## 6. DevTools - -### 6.1 React Query DevTools - -**File**: `app/providers.tsx` - -```typescript -import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; - -export function AppProviders({ children }: { children: React.ReactNode }) { - return ( - - {children} - - - ); -} -``` - -### 6.2 Zustand DevTools - -```typescript -import { devtools } from 'zustand/middleware'; - -const useFilterStore = create( - devtools( - (set) => ({ - activeFilter: 'all', - setFilter: (filter) => set({ activeFilter: filter }), - }), - { name: 'filterStore' } - ) -); -``` - ---- - -## 7. Best Practices - -### 7.1 When to Use What - -| Use Case | Solution | Example | -|----------|----------|---------| -| UI 상태 (토글, 열림/닫힘) | Local useState | Modal open state | -| 전역 클라이언트 상태 | Zustand | Filter, Search | -| 서버 데이터 | React Query | Images, Items | -| 애니메이션 상태 | Zustand | FLIP transition state | -| 폼 상태 | Local useState + Zustand | Create post form | - -### 7.2 Performance Tips - -```typescript -// 1. Zustand selector로 필요한 값만 구독 -// BAD: 전체 store 구독 -const store = useFilterStore(); - -// GOOD: 필요한 값만 구독 -const filter = useFilterStore((s) => s.activeFilter); - -// 2. React Query select로 데이터 변환 -const { data } = useInfiniteFilteredImages({...}, { - select: (data) => data.pages.flatMap(p => p.items), -}); - -// 3. 불필요한 refetch 방지 -{ - staleTime: 60 * 1000, - refetchOnWindowFocus: false, -} - -// 4. 낙관적 업데이트 (mutations) -useMutation({ - mutationFn: voteItem, - onMutate: async (newVote) => { - await queryClient.cancelQueries(['item', newVote.itemId]); - const previous = queryClient.getQueryData(['item', newVote.itemId]); - queryClient.setQueryData(['item', newVote.itemId], (old) => ({ - ...old, - votes: old.votes + 1, - })); - return { previous }; - }, - onError: (err, newVote, context) => { - queryClient.setQueryData(['item', newVote.itemId], context.previous); - }, -}); -``` - ---- - -## Related Documents +# State Management -- [README.md](./README.md) - 시스템 아키텍처 -- [data-pipeline.md](./data-pipeline.md) - 데이터 파이프라인 -- [../../specs/feature-spec/workflows.md](../../specs/feature-spec/workflows.md) - 워크플로우 +> **이 문서는 [decoded-docs vault](https://github.com/decodedcorp/decoded-docs)로 이전되었습니다.** +> +> 본문: [`Architecture/state-management.md`](https://github.com/decodedcorp/decoded-docs/blob/main/Architecture/state-management.md) +> +> Obsidian path: `Architecture/state-management` +> +> Sync policy: [Guides/sync-policy](https://github.com/decodedcorp/decoded-docs/blob/main/Guides/sync-policy.md) diff --git a/docs/backend-frontend-status.md b/docs/backend-frontend-status.md deleted file mode 100644 index 6b020130..00000000 --- a/docs/backend-frontend-status.md +++ /dev/null @@ -1,286 +0,0 @@ -# DECODED 백엔드-프론트엔드 구현 현황 및 UI 기능 가능 여부 - -**작성일:** 2026-02-08 -**분석 대상:** `decoded-api/` (Rust/Axum 백엔드) ↔ `decoded-app/` (Next.js 프론트엔드) - ---- - -## 요약 - -| 구분 | 수치 | -|------|------| -| 백엔드 총 API 엔드포인트 | **78개** | -| 프론트엔드에서 연동 완료 | **26개** (33%) | -| 프론트엔드 API 클라이언트 존재하나 미사용 | **2개** | -| 프론트엔드 미연동 (백엔드만 존재) | **50개** (64%) | -| Supabase 직접 조회 (백엔드 우회) | 홈/이미지 읽기 계열 | - ---- - -## 1. 완전 연동 (백엔드 API + 프론트엔드 UI 모두 구현) - -### 1.1 Posts (게시물) - -| 상태 | 백엔드 엔드포인트 | 프론트엔드 위치 | UI 기능 | -|:----:|-------------------|-----------------|---------| -| :white_check_mark: | `GET /api/v1/posts` | `lib/api/posts.ts` → `useInfinitePosts` | Explore 그리드, Feed 무한스크롤 | -| :white_check_mark: | `GET /api/v1/posts/{post_id}` | `app/api/v1/posts/[postId]/route.ts` | Post 상세 페이지 | -| :white_check_mark: | `POST /api/v1/posts` | `lib/api/posts.ts` → `createPost` | 게시물 생성 (Request 플로우) | -| :white_check_mark: | `POST /api/v1/posts/with-solutions` | `lib/api/posts.ts` → `createPostWithSolution` | 솔루션 포함 게시물 생성 | -| :white_check_mark: | `POST /api/v1/posts/upload` | `lib/api/posts.ts` → `uploadImage` | 이미지 업로드 (DropZone) | -| :white_check_mark: | `POST /api/v1/posts/analyze` | `lib/api/posts.ts` → `analyzeImage` | AI 이미지 분석 | -| :white_check_mark: | `PATCH /api/v1/posts/{post_id}` | `lib/api/posts.ts` → `updatePost` | 게시물 수정 | -| :white_check_mark: | `DELETE /api/v1/posts/{post_id}` | `lib/api/posts.ts` → `deletePost` | 게시물 삭제 | - -### 1.2 Users (사용자) - -| 상태 | 백엔드 엔드포인트 | 프론트엔드 위치 | UI 기능 | -|:----:|-------------------|-----------------|---------| -| :white_check_mark: | `GET /api/v1/users/me` | `lib/api/users.ts` → `useMe` | 프로필 페이지 내 정보 표시 | -| :white_check_mark: | `PATCH /api/v1/users/me` | `lib/api/users.ts` → `useUpdateProfile` | 프로필 수정 모달 | -| :white_check_mark: | `GET /api/v1/users/me/stats` | `lib/api/users.ts` → `useUserStats` | 프로필 통계 카드 | -| :white_check_mark: | `GET /api/v1/users/me/activities` | `lib/api/users.ts` → `useUserActivities` | 프로필 활동 탭 (Hook 존재, **UI 미완성**) | -| :white_check_mark: | `GET /api/v1/users/{user_id}` | `lib/api/users.ts` → `useUser` | 타 사용자 프로필 조회 | - -### 1.3 Categories (카테고리) - -| 상태 | 백엔드 엔드포인트 | 프론트엔드 위치 | UI 기능 | -|:----:|-------------------|-----------------|---------| -| :white_check_mark: | `GET /api/v1/categories` | `lib/api/categories.ts` → `getCategories` | 카테고리 필터링 | - -### 1.4 Spots (스팟) - -| 상태 | 백엔드 엔드포인트 | 프론트엔드 위치 | UI 기능 | -|:----:|-------------------|-----------------|---------| -| :white_check_mark: | `GET /api/v1/posts/{post_id}/spots` | `lib/api/spots.ts` → `fetchSpots` | Post 상세 내 아이템 스팟 표시 | -| :white_check_mark: | `POST /api/v1/posts/{post_id}/spots` | `lib/api/spots.ts` → `createSpot` | 스팟 생성 | -| :white_check_mark: | `PATCH /api/v1/spots/{spot_id}` | `lib/api/spots.ts` → `updateSpot` | 스팟 수정 | -| :white_check_mark: | `DELETE /api/v1/spots/{spot_id}` | `lib/api/spots.ts` → `deleteSpot` | 스팟 삭제 | - -### 1.5 Solutions (솔루션) - -| 상태 | 백엔드 엔드포인트 | 프론트엔드 위치 | UI 기능 | -|:----:|-------------------|-----------------|---------| -| :white_check_mark: | `GET /api/v1/spots/{spot_id}/solutions` | `lib/api/solutions.ts` → `fetchSolutions` | 스팟 내 솔루션 목록 | -| :white_check_mark: | `POST /api/v1/spots/{spot_id}/solutions` | `lib/api/solutions.ts` → `createSolution` | 솔루션 등록 | -| :white_check_mark: | `PATCH /api/v1/solutions/{solution_id}` | `lib/api/solutions.ts` → `updateSolution` | 솔루션 수정 | -| :white_check_mark: | `DELETE /api/v1/solutions/{solution_id}` | `lib/api/solutions.ts` → `deleteSolution` | 솔루션 삭제 | -| :white_check_mark: | `POST /api/v1/solutions/extract-metadata` | `lib/api/solutions.ts` → `extractSolutionMetadata` | 상품 링크 메타데이터 추출 | -| :white_check_mark: | `POST /api/v1/solutions/convert-affiliate` | `lib/api/solutions.ts` → `convertAffiliate` | 어필리에이트 링크 변환 | - ---- - -## 2. 부분 연동 (연동 코드 존재하나 UI 미완성 또는 Mock 사용) - -### 2.1 Search (검색) - -| 상태 | 백엔드 엔드포인트 | 프론트엔드 위치 | 비고 | -|:----:|-------------------|-----------------|------| -| :large_orange_diamond: | `GET /api/v1/search` | `shared/api/search.ts` → `useUnifiedSearch` | **연동 코드 구현 완료**, Mock/실제 전환 가능 (`NEXT_PUBLIC_USE_MOCK_SEARCH`) | -| :large_orange_diamond: | `GET /api/v1/search/popular` | `shared/api/search.ts` → `usePopularSearches` | **연동 코드 구현 완료**, Mock/실제 전환 가능 | -| :large_orange_diamond: | `GET /api/v1/search/recent` | `shared/api/search.ts` → `fetchRecentSearches` | **함수 구현 완료**, Hook에서 아직 미사용 | -| :large_orange_diamond: | `DELETE /api/v1/search/recent/{id}` | `shared/api/search.ts` → `deleteRecentSearches` | **함수 구현 완료**, Hook에서 아직 미사용 | - -> **참고:** Search UI 컴포넌트는 풍부하게 구현됨 (SearchOverlay, SearchTabs, SearchResults, RecentSearches 등 12개 컴포넌트). 하지만 Next.js API Route 프록시가 없어서 `/api/v1/search` 경로로 직접 호출. 환경변수 설정으로 Mock ↔ 실제 API 전환 가능. - -### 2.2 Profile (프로필 내 활동 탭) - -| 상태 | 연동 상태 | 비고 | -|:----:|----------|------| -| :large_orange_diamond: | `useUserActivities` Hook 구현 완료 | ProfileClient에서 `TODO: Connect to useUserActivities hook` 상태. 탭 UI는 존재하나 데이터가 항상 `EmptyState` 표시 | -| :large_orange_diamond: | Badge/Ranking 데이터 | API 호출 없이 **Mock 데이터** 사용 중 (`MOCK_BADGES`, `MOCK_RANKINGS`) | - ---- - -## 3. 미연동 (백엔드 구현 완료, 프론트엔드 미구현) - -### 3.1 Feed (피드) - 전용 API 미연동 - -| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | -|-------------------|------|----------------| -| `GET /api/v1/feed` | 홈 피드 (개인화) | **미사용** — Feed 페이지는 `/api/v1/posts`를 직접 사용 | -| `GET /api/v1/feed/trending` | 트렌딩 피드 | **미사용** — 트렌딩 전용 피드 UI 없음 | -| `GET /api/v1/feed/curations` | 큐레이션 목록 | **미사용** | -| `GET /api/v1/feed/curations/{id}` | 큐레이션 상세 | **미사용** | - -> **현황:** Feed 페이지(`/feed`)는 `useInfinitePosts`로 `/api/v1/posts` 엔드포인트만 사용. 백엔드의 개인화 피드, 트렌딩, 큐레이션 API는 프론트엔드에서 아직 호출하지 않음. - -### 3.2 Rankings (랭킹) - -| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | -|-------------------|------|----------------| -| `GET /api/v1/rankings` | 전체 랭킹 | **미연동** — Profile에서 Mock 랭킹만 표시 | -| `GET /api/v1/rankings/{category}` | 카테고리별 랭킹 | **미연동** | -| `GET /api/v1/rankings/me` | 나의 랭킹 상세 | **미연동** | - -### 3.3 Badges (배지) - -| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | -|-------------------|------|----------------| -| `GET /api/v1/badges` | 배지 목록 | **미연동** — Profile에서 `MOCK_BADGES` 사용 | -| `GET /api/v1/badges/me` | 내 배지 | **미연동** | -| `GET /api/v1/badges/{badge_id}` | 배지 상세 | **미연동** | - -> **참고:** 홈 페이지에서 `fetchAllBadgesServer()`로 **Supabase 직접 조회**하여 배지 표시 중. 백엔드 API 연동은 없음. - -### 3.4 Comments (댓글) - -| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | -|-------------------|------|----------------| -| `POST /api/v1/posts/{post_id}/comments` | 댓글 작성 | **미연동** — API 클라이언트 없음 | -| `GET /api/v1/posts/{post_id}/comments` | 댓글 목록 | **미연동** | -| `PATCH /api/v1/comments/{comment_id}` | 댓글 수정 | **미연동** | -| `DELETE /api/v1/comments/{comment_id}` | 댓글 삭제 | **미연동** | - -### 3.5 Votes (투표) - -| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | -|-------------------|------|----------------| -| `POST /api/v1/solutions/{id}/votes` | 솔루션 투표 | **미연동** — API 클라이언트 없음 | -| `DELETE /api/v1/solutions/{id}/votes` | 투표 취소 | **미연동** | -| `GET /api/v1/solutions/{id}/votes` | 투표 통계 | **미연동** | -| `POST /api/v1/solutions/{id}/adopt` | 솔루션 채택 | **미연동** | -| `DELETE /api/v1/solutions/{id}/adopt` | 채택 취소 | **미연동** | - -### 3.6 Earnings (수익) - -| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | 백엔드 상태 | -|-------------------|------|----------------|-------------| -| `POST /api/v1/earnings/clicks` | 클릭 기록 | **미연동** | 구현 완료 | -| `GET /api/v1/earnings/clicks/stats` | 클릭 통계 | **미연동** | 구현 완료 | -| `GET /api/v1/earnings/earnings` | 수익 현황 | **미연동** | *임시 구현 (빈 데이터)* | -| `GET /api/v1/earnings/settlements` | 정산 내역 | **미연동** | *임시 구현 (빈 배열)* | -| `POST /api/v1/earnings/settlements/withdraw` | 출금 요청 | **미연동** | *임시 구현 (400 에러)* | -| `GET /api/v1/earnings/settlements/withdraw/{id}` | 출금 상태 | **미연동** | *임시 구현 (404 에러)* | - -### 3.7 Subcategories (하위 카테고리) - -| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | -|-------------------|------|----------------| -| `GET /api/v1/subcategories` | 전체 하위 카테고리 | **미연동** | -| `GET /api/v1/subcategories/{category_id}` | 카테고리별 하위 | **미연동** | - -### 3.8 Spots - 개별 조회 - -| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | -|-------------------|------|----------------| -| `GET /api/v1/spots/{spot_id}` | 스팟 단건 조회 | **미연동** (목록 조회만 사용) | - -### 3.9 Solutions - 개별 조회 - -| 백엔드 엔드포인트 | 기능 | 프론트엔드 현황 | -|-------------------|------|----------------| -| `GET /api/v1/solutions/{solution_id}` | 솔루션 단건 조회 | **미연동** (목록 조회만 사용) | - -### 3.10 Admin (관리자) - 전체 미연동 - -| 백엔드 엔드포인트 그룹 | 엔드포인트 수 | 프론트엔드 현황 | -|-----------------------|:---:|----------------| -| Admin - Posts | 2 | **미연동** — 관리자 페이지 없음 | -| Admin - Solutions | 2 | **미연동** | -| Admin - Categories | 4 | **미연동** | -| Admin - Synonyms | 5 | **미연동** | -| Admin - Curations | 4 | **미연동** | -| Admin - Dashboard | 3 | **미연동** | -| Admin - Badges | 4 | **미연동** | -| **합계** | **24** | | - ---- - -## 4. Supabase 직접 조회 (백엔드 API 우회) - -아래 데이터는 백엔드 API를 거치지 않고 Supabase에서 직접 조회합니다. - -| 사용 위치 | Supabase 쿼리 | 관련 백엔드 API | -|-----------|--------------|----------------| -| 홈 페이지 (`/`) | `fetchWeeklyBestImagesServer()` | `GET /api/v1/feed` (미사용) | -| 홈 페이지 | `fetchFeaturedImageServer()` | `GET /api/v1/feed` (미사용) | -| 홈 페이지 | `fetchWhatsNewStylesServer()` | `GET /api/v1/feed` (미사용) | -| 홈 페이지 | `fetchDecodedPickServer()` | `GET /api/v1/feed/curations` (미사용) | -| 홈 페이지 | `fetchArtistSpotlightStylesServer()` | — | -| 홈 페이지 | `fetchTrendingKeywordsServer()` | `GET /api/v1/search/popular` (미사용) | -| 홈 페이지 | `fetchAllBadgesServer()` | `GET /api/v1/badges` (미사용) | -| 이미지 목록 (`/images`) | `fetchUnifiedImages()` | `GET /api/v1/posts` (대안 존재) | -| 이미지 상세 (`/images/[id]`) | `fetchImageByIdServer()` | `GET /api/v1/posts/{id}` (대안 존재) | -| 아이템/솔루션 | `fetchSolutionsBySpotId()` 등 | `GET /api/v1/spots/{id}/solutions` (대안 존재) | - ---- - -## 5. 페이지별 UI 기능 가능 여부 종합 - -### :white_check_mark: 완전 동작 가능 - -| 페이지 | 데이터 소스 | 비고 | -|--------|-----------|------| -| `/` (홈) | Supabase 직접 조회 | 백엔드 API 없이도 SSR로 완전 동작 | -| `/explore` | 백엔드 API (`GET /posts`) | 무한 스크롤 + 카테고리 필터 동작 | -| `/feed` | 백엔드 API (`GET /posts`) | 수직 피드 동작 (단, 개인화/트렌딩은 미적용) | -| `/images` | Supabase 직접 조회 | 이미지 그리드 + 무한 스크롤 동작 | -| `/images/[id]` | Supabase 직접 조회 | 이미지 상세, Lightbox, 관련 이미지 동작 | -| `/posts/[id]` | 백엔드 API | Post 상세 동작 | -| `/request/upload` | 백엔드 API (`POST /posts/upload`) | 이미지 업로드 동작 | -| `/request/detect` | 백엔드 API (`POST /posts/analyze`) | AI 분석 동작 | -| `/login` | Supabase Auth | OAuth(Kakao, Google, Apple) 동작 | - -### :large_orange_diamond: 부분 동작 (일부 기능 제한) - -| 페이지 | 동작하는 기능 | 미동작/제한 기능 | -|--------|-------------|-----------------| -| `/profile` | 내 정보 표시, 통계 카드, 프로필 수정 | **배지:** Mock 데이터, **랭킹:** Mock 데이터, **활동 탭:** 항상 Empty State | -| `/search` | Search UI 전체, Mock 검색 결과 | **실제 검색:** env 설정 필요 (`NEXT_PUBLIC_USE_MOCK_SEARCH=false`), **최근 검색:** Hook 미연결 | - -### :x: UI 없음 (백엔드만 구현) - -| 기능 | 필요한 UI | 우선순위 제안 | -|------|----------|-------------| -| 댓글 시스템 | Post 상세 내 댓글 섹션 | **높음** — 사용자 인터랙션 핵심 | -| 투표/채택 시스템 | Solution 카드 내 투표 버튼 + 채택 표시 | **높음** — 솔루션 품질 관리 핵심 | -| 개인화 피드 | 피드 탭 전환 (홈/트렌딩/큐레이션) | **중간** — 현재 일반 피드로 대체 가능 | -| 큐레이션 | 큐레이션 목록/상세 페이지 | **중간** | -| 랭킹 | 랭킹 보드 페이지 또는 섹션 | **중간** | -| 배지 (실제 데이터) | 배지 API 연동 + Mock 제거 | **낮음** — Mock으로 UI는 동작 | -| 수익/정산 | 수익 대시보드 페이지 | **낮음** — 백엔드도 임시 구현 | -| 하위 카테고리 | 카테고리 드릴다운 필터 | **낮음** | -| 관리자 패널 | 관리자 전용 대시보드 (별도 앱 권장) | **별도** | - ---- - -## 6. 아키텍처 특이사항 - -### 듀얼 데이터 소스 패턴 - -``` -[프론트엔드] - ├── Supabase 직접 조회 (읽기 전용, SSR) - │ └── 홈 페이지, 이미지 목록/상세 - ├── 백엔드 REST API (CRUD, AI 기능) - │ └── 게시물/스팟/솔루션 CUD, 사용자, 카테고리 - └── Next.js API Route 프록시 (/app/api/v1/) - └── 백엔드 API를 프록시하여 CORS 해결 -``` - -### 주의사항 - -1. **홈 페이지 데이터 이중화:** Supabase 직접 조회와 백엔드 Feed API가 겹침. 장기적으로 백엔드 API로 통일 권장. -2. **Search API Route 프록시 부재:** Search는 `shared/api/search.ts`에서 직접 호출. Next.js API Route 프록시 없음. -3. **Profile Mock 의존성:** 배지/랭킹이 Mock 데이터에 의존. API 연동 시 스토어 구조 변경 필요. -4. **Affiliate 링크:** 백엔드에서 `https://affiliate.example.com/{url}` 형태의 **Mock URL** 반환 중 (TODO). - ---- - -## 7. 권장 연동 우선순위 - -| 순위 | 작업 | 이유 | -|:----:|------|------| -| 1 | **댓글 시스템 연동** | Post 상세 페이지의 핵심 인터랙션. 백엔드 완전 구현됨. | -| 2 | **투표/채택 연동** | 솔루션 품질 관리 핵심. 백엔드 완전 구현됨. | -| 3 | **Search 실제 API 전환** | UI 이미 완성됨. 환경변수 전환 + API Route 프록시 추가만 필요. | -| 4 | **Profile 활동 탭 연동** | Hook 이미 구현됨 (`useUserActivities`). UI 바인딩만 필요. | -| 5 | **Feed API 전환** | 현재 Posts API로 우회 중. Feed API로 전환하면 개인화/트렌딩 가능. | -| 6 | **배지/랭킹 API 연동** | Mock 제거하고 실제 데이터 사용. UI 이미 존재. | -| 7 | **클릭 기록 연동** | Earnings 수익 시스템의 기초. 어필리에이트 클릭 추적. | -| 8 | **관리자 패널** | 24개 Admin API 활용. 별도 앱 또는 라우트 그룹으로 구현 권장. | - ---- - -*이 문서는 `decoded-api/src/domains/` 및 `decoded-app/packages/web/` 코드를 기반으로 자동 분석되었습니다.* diff --git a/docs/database/entity-enrichment-pipeline.md b/docs/database/entity-enrichment-pipeline.md new file mode 100644 index 00000000..bfc1ea30 --- /dev/null +++ b/docs/database/entity-enrichment-pipeline.md @@ -0,0 +1,352 @@ +--- +title: Entity Enrichment Pipeline +owner: llm +status: draft +updated: 2026-05-11 +tags: [database, assets, operation, instagram, entity-enrichment, admin] +related: + - docs/database/operating-model.md + - docs/architecture/assets-project.md + - docs/agent/database-summary.md + - docs/agent/web-routes-and-features.md +--- + +# Entity Enrichment Pipeline + +This document is the working RFC and GitHub issue draft for turning Instagram +tagged accounts into reviewed `artists`, `brands`, `groups`, `group_members`, +and `instagram_accounts` rows in the operation Supabase project. + +## Goal + +Build a Data Pipeline workflow that: + +- extracts Instagram tagged usernames from the assets project, +- upserts candidates into operation `public.instagram_accounts`, +- enriches missing names/profile metadata with Gemini 2.5 Flash + Google Search + grounding and Instaloader profile lookup, +- records Gemini grounded-search daily usage against the 1,500 RPD free quota, +- lets admins review account type and role from a Data Pipeline page, +- promotes approved primary accounts into `artists`, `brands`, or `groups`. + +## Current State + +### Operation Supabase + +The operation project owns the entity catalog: + +- `public.artists` +- `public.brands` +- `public.groups` +- `public.group_members` +- `public.instagram_accounts` + +Observed on 2026-05-11: + +- `artists`: 68 rows +- `brands`: 416 rows +- `groups`: 47 rows +- `instagram_accounts`: 1,518 rows +- `group_members`: 0 rows +- `artists.profile_image_url`, `brands.logo_image_url`, and + `groups.profile_image_url` are currently empty across all existing rows. + +`instagram_accounts` already has `username`, `name_en`, `name_ko`, +`display_name`, `bio`, `profile_image_url`, `wikidata_status`, `wikidata_id`, +`needs_review`, `artist_id`, `brand_id`, `group_id`, and +`entity_region_code`. + +Missing for this workflow: + +- explicit account type (`artist`, `brand`, `group`, `other`, `unknown`), +- explicit account role (`primary`, `secondary`, `regional`, `unknown`), +- brand `country_of_origin` (`NA` if unknown), +- Gemini review status/model/confidence/reason in metadata, +- grounded-search usage accounting in the assets Gemini cost tables. + +### Assets Supabase + +The assets project owns raw pipeline staging: + +- `public.raw_post_sources` +- `public.raw_posts` +- `public.pipeline_events` +- `public.pipeline_settings` +- `public.gemini_usage_events` +- `public.gemini_spend_daily` + +Instagram tagged accounts are currently stored in +`raw_posts.platform_metadata.tagged_usernames` as a comma-separated string. The +source is `packages/ai-server/src/services/raw_posts/adapters/instagram.py`, +which reads `edge_media_to_tagged_user.edges[*].node.user.username` from the +Instaloader GraphQL payload. + +The first version of this pipeline must handle both: + +- existing operation `instagram_accounts` that need enrichment, and +- new candidates discovered from assets `raw_posts`. + +## Data Flow + +```mermaid +flowchart TD + assetRawPosts["Assets raw_posts"] --> extractTags["Extract tagged_usernames"] + extractTags --> upsertAccounts["Upsert operation instagram_accounts"] + upsertAccounts --> profileBackfill["Instaloader profile backfill"] + upsertAccounts --> geminiReview["Gemini 2.5 Flash grounded review"] + geminiReview --> usageLog["Assets gemini_usage_events"] + profileBackfill --> reviewQueue["Admin Data Pipeline review queue"] + geminiReview --> reviewQueue + reviewQueue --> promoteEntities["Promote approved primary accounts"] + promoteEntities --> artists["artists"] + promoteEntities --> brands["brands"] + promoteEntities --> groups["groups"] + promoteEntities --> groupMembers["group_members"] +``` + +## Proposed Operation Schema + +Add only durable classification columns to `public.instagram_accounts`. +Operational review/profile state stays in `metadata` to avoid widening the +table for pipeline-internal details. + +| Column | Type | Purpose | +| --- | --- | --- | +| `account_type` | `text` | Gemini/admin classification: `artist`, fashion-only `brand`, `group`, `other`, `unknown` | +| `entity_ig_role` | `text` | Entity account role: `primary`, `secondary`, `regional`, `unknown` | + +Gemini review provenance and profile backfill state should stay in +`instagram_accounts.metadata`, not in dedicated columns: + +```json +{ + "entity_enrichment": { + "gemini_review": { + "status": "reviewed", + "reviewed_at": "2026-05-14T00:00:00Z", + "model": "gemini-2.5-flash", + "confidence": 0.92, + "reason": "...", + "payload": {} + }, + "profile_backfill": { + "status": "completed", + "completed_at": "2026-05-14T00:00:00Z", + "source": "instaloader" + } + } +} +``` + +Add `public.brands.country_of_origin text not null default 'NA'`. + +| Column | Type | Purpose | +| --- | --- | --- | +| `country_of_origin` | `text` | Country where the brand was founded; `NA` if unknown. | + +Recommended check constraints: + +- `account_type in ('artist', 'brand', 'group', 'other', 'unknown')` +- `entity_ig_role in ('primary', 'secondary', 'regional', 'unknown')` +- `num_nonnulls(artist_id, brand_id, group_id) <= 1` + +`entity_region_code` already exists and should be used for regional accounts +such as `JP` or `KR`. + +### Gemini Usage Accounting + +Use the existing assets DB cost tables instead of creating an operation DB usage +table: + +- `public.gemini_usage_events` records each grounded review call with + `pipeline = 'entity_enrichment'`, `step = 'instagram_account_review'`, and + `grounding_queries = 1`. +- `public.gemini_spend_daily` remains the admin read-side for spend and usage + aggregation. +- The scheduler checks today's KST `grounding_queries` for + `pipeline = 'entity_enrichment'` before sending another grounded prompt. If + the configured daily cap has been reached, it pauses without another Gemini + call. + +Runtime scheduler state also belongs in assets DB: + +- `public.pipeline_settings.platform = 'entity_enrichment'` +- the existing `processing_*` columns store enabled/running/progress/last error +- `pipeline_settings.metadata.entity_enrichment` stores config such as + `daily_grounding_cap` and `auto_promote_confidence_min` + +Model selection is intentionally isolated from the shared `GEMINI_MODEL` env var: +entity enrichment must use `ENTITY_ENRICHMENT_GEMINI_MODEL=gemini-2.5-flash`. +Do not point this pipeline at preview/3.x Gemini models, because Google Search +grounding limits and pricing may differ. + +## Scheduler Scope + +The enrichment scheduler belongs under the Data Pipeline concept, not inside the +entity CRUD screens. + +Responsibilities: + +1. Read assets `raw_posts` where `platform = 'instagram'` and + `platform_metadata.tagged_usernames` exists. +2. Normalize usernames with the same rules as the Instagram adapter: trim, + remove leading `@`, lowercase. +3. Upsert operation `instagram_accounts` by unique `username`. +4. Select operation accounts that are missing required data or have + `needs_review = true`. +5. Run Instaloader profile lookup to populate `display_name`, `bio`, and + `profile_image_url`. +6. Run Gemini 2.5 Flash with Google Search grounding to classify: + `name_en`, `name_ko`, `account_type`, `entity_ig_role`, region, confidence, + and evidence. +7. Store structured review output in operation metadata and quota usage in + assets `gemini_usage_events`. +8. Mark uncertain results as `needs_human` / `needs_review = true`. + +Non-responsibilities: + +- It should not auto-publish uncertain Gemini decisions. +- It should not replace the existing raw posts review queue. +- It should not create duplicate primary entities for secondary/regional + accounts. + +Recommended implementation home: + +- candidate extraction and profile/Gemini enrichment: ai-server scheduler or + worker, because `instagram.py`, Instaloader, Gemini usage, and existing + scheduler patterns already live there; +- admin control and review: Next.js admin APIs/pages under `packages/web`; +- operation writes: service-role server-side only, with audit/provenance. + +## Admin Dashboard Scope + +Add a Data Pipeline page, tentatively: + +- `/admin/data-pipeline/instagram-accounts` + +The page should show: + +- queue counts derived from `metadata.entity_enrichment.*.status`, + `account_type`, and `entity_ig_role`, +- Gemini grounded-search usage for today: used, remaining, quota, cap reached, +- scheduler controls from assets `pipeline_settings`: enabled, paused, batch + size, daily cap, last run, last success, last error, +- account table: username, current entity link, Gemini suggestion, confidence, + profile image, bio/display name, source count, last tagged source, +- review actions: approve suggestion, edit type/role/region, link existing + entity, create primary entity, mark unknown, skip, retry. + +Promotion behavior: + +- `entity_ig_role = 'primary'` can create/link one of `artists`, `brands`, or + `groups`, then set that entity's `primary_instagram_account_id`. +- `entity_ig_role = 'secondary'` links to the correct entity but does not + replace `primary_instagram_account_id`. +- `entity_ig_role = 'regional'` links to the correct entity and must set + `entity_region_code`. +- `account_type = 'other'` means the account is known but not eligible for the + fashion catalog, e.g. beauty/cosmetics, magazines/media like Vogue, technology + companies, character/IP owners like Hello Kitty/Sanrio, venues, restaurants, or + platforms. +- `account_type = 'unknown'` means there is insufficient evidence. Both `other` + and `unknown` remain in `instagram_accounts` and stay visible for review or + exclusion. + +## Storage Decision + +Use the existing Cloudflare R2 public URL strategy used by raw posts. Do not add +operation Supabase Storage buckets for this pipeline. + +MVP behavior: + +- Instaloader profile lookup resolves the source Instagram profile image URL. +- ai-server downloads that image and uploads it to R2 under + `entity-profiles/instagram/{username}.{ext}`. +- `instagram_accounts.profile_image_url` stores the R2 public URL when R2 is + configured. +- If R2 is not configured in a local environment, the scheduler may keep the + source profile image URL and log a warning, but production should be configured + with R2 credentials. + +## GitHub Issue Draft + +Title: + +```text +Build Instagram account enrichment pipeline for artist/brand/group catalog +``` + +Body: + +```markdown +## Summary +Build a Data Pipeline workflow that turns Instagram tagged accounts into reviewed artist/brand/group catalog entries. Operation DB owns durable entity state; assets DB owns pipeline runtime state and Gemini usage accounting. + +## Context +We currently collect Instagram tagged accounts from Instaloader into assets `raw_posts.platform_metadata.tagged_usernames`. Operation DB already has `instagram_accounts`, `artists`, `brands`, `groups`, and `group_members`, but the old n8n warehouse workflow no longer matches the current public schema. Existing entity dashboards can edit artists/brands, but there is no first-class Instagram account enrichment/review queue. + +We want to use Gemini 2.5 Flash with Google Search grounding to enrich missing `name_en`, `name_ko`, account type, account role, regional classification, and brand country of origin. Grounding has a 1,500 RPD free quota in the paid tier, so usage must be tracked in the existing assets Gemini cost tables and visible in admin before the scheduler can run safely. + +## Scope +- Add only durable classification columns to operation `instagram_accounts`: `account_type` and `entity_ig_role`. +- Add `brands.country_of_origin text not null default 'NA'`. +- Store Gemini/profile enrichment details under `instagram_accounts.metadata.entity_enrichment` instead of adding dedicated audit/status columns. +- Store scheduler runtime state in assets `pipeline_settings.platform = 'entity_enrichment'`. +- Record Gemini usage in existing assets `gemini_usage_events` with `pipeline = 'entity_enrichment'` and `step = 'instagram_account_review'`. +- Upsert tagged usernames from assets raw posts into operation `instagram_accounts`. +- Backfill Instagram profile data: `display_name`, `bio`, `profile_image_url`, + mirroring profile images to R2. +- Use Gemini 2.5 Flash with Google Search grounding to enrich missing names and classify account type/role/region/country of origin. +- Add a Data Pipeline dashboard page for queue health, scheduler controls, quota usage, and enrichment monitoring. +- Auto-promote high-confidence primary accounts into `artists`, `brands`, or `groups`; keep secondary/regional accounts in `instagram_accounts` for reference and later manual linking if needed. +- Use the existing artist/brand/group admin screens for manual corrections after enrichment. +- Document the pipeline and operation/assets DB ownership. + +## Non-goals +- Fully replacing the existing raw posts review queue. +- Autonomously publishing uncertain Gemini decisions. +- Reworking Pinterest/raw post pipelines beyond consuming existing tagged metadata. +- Creating duplicate primary entities for regional or secondary Instagram accounts. + +## Acceptance Criteria +- Admin can see an Instagram account enrichment queue under Data Pipeline. +- Admin can run/pause the enrichment scheduler from Data Pipeline using the assets `pipeline_settings` row for `entity_enrichment`. +- Admin can see today's Gemini grounded-search usage, remaining quota, configured cap, and cap-reached state. +- Scheduler respects the daily Gemini grounding cap using assets `gemini_usage_events` and records every grounded request through the existing Gemini cost tracking path. +- Scheduler resets stale component-level `error` / `processing` statuses back to `pending` after a cooldown so missing Gemini review and missing profile backfill can be retried independently. +- Tagged usernames from assets raw posts are deduplicated and upserted into operation `instagram_accounts` by normalized username. +- Gemini output is stored as structured review data under operation `instagram_accounts.metadata.entity_enrichment.gemini_review`, not only opaque text. +- Profile backfill status/provenance is stored under operation `instagram_accounts.metadata.entity_enrichment.profile_backfill`, not as dedicated columns. +- Admin can inspect each candidate account with Gemini evidence and profile metadata. +- Account type is visible as `artist`, fashion-only `brand`, `group`, `other`, or `unknown`. +- Account role is visible as `primary`, `secondary`, `regional`, or `unknown`. +- Regional accounts store `entity_region_code`, e.g. `JP` or `KR`, without creating duplicate primary entities. +- Brand primary entities store `country_of_origin`, using `NA` when unknown. +- High-confidence primary accounts create or link the correct `artists`, `brands`, or `groups` row and set `primary_instagram_account_id`. +- Manual corrections happen in the existing artist/brand/group admin screens, not in the pipeline queue. +- Secondary/regional accounts remain in `instagram_accounts` with explicit role and region. +- Entity image/logo URL fields can be populated from R2-backed profile image URLs. +- Automated writes preserve provenance through metadata or audit logs. +- Docs are updated with schema, data flow, quota policy, and manual review rules. + +## Implementation Notes +- Prefer a Data Pipeline page over adding this first to the generic entity CRUD screens. +- Candidate extraction reads from assets; catalog writes go to operation; scheduler runtime and Gemini usage stay in assets. There are no cross-project FKs. +- Do not create operation DB tables for entity enrichment scheduler settings or Gemini daily usage. Reuse assets `pipeline_settings`, `gemini_usage_events`, and `gemini_spend_daily`. +- First release should process both new tagged accounts and existing operation `instagram_accounts` that are missing names/images/review state. +- Reuse the existing R2 public URL strategy for profile/logo images; do not add Supabase Storage for this flow. +- Review operation RLS posture before exposing more audit/admin data. Current MCP advisory reports RLS disabled on `seaql_migrations`, `admin_audit_log`, and `post_magazine_events`. +``` + +## Documentation Checklist + +- Update `docs/agent/database-summary.md` with this pipeline as an operation + entity catalog workflow fed by assets raw data, with runtime/usage state in + assets. +- Update `docs/database/operating-model.md` with the cross-project rule: + assets owns raw tagged evidence plus pipeline runtime/usage state; operation + owns reviewed account/entity state. +- Update `docs/architecture/assets-project.md` with tagged account extraction as + a downstream consumer of `raw_posts.platform_metadata`. +- Data Pipeline route: `packages/web/app/admin/data-pipeline/instagram-accounts/page.tsx`. +- Keep this document as the canonical RFC until the issue is split into PRs. diff --git a/docs/database/operating-model.md b/docs/database/operating-model.md index f8e6e1c7..aca718ab 100644 --- a/docs/database/operating-model.md +++ b/docs/database/operating-model.md @@ -1,12 +1,13 @@ --- title: DB 운영 모델 — 단일 진입점 -date: 2026-04-30 +date: 2026-05-11 status: approved owner: human related: - docs/agent/environments.md - docs/DATABASE-MIGRATIONS.md - docs/agent/database-summary.md + - docs/database/entity-enrichment-pipeline.md - docs/architecture/assets-project.md tags: [database, migration, supabase, seaorm, operating-model] --- @@ -42,7 +43,7 @@ decoded 는 **3개의 PostgreSQL 영역** (dev/prod/assets) 과 **3개의 사실 └────────────────────────────────────────────────────────────────────┘ ``` -prod ↔ assets 사이에 cross-project FK 는 **없다**. assets 는 검증되어야 prod 로 흘러간다 (`/api/v1/.../verify` 엔드포인트). +prod ↔ assets 사이에 cross-project FK 는 **없다**. assets 는 검증되어야 prod 로 흘러간다 (`/api/v1/.../verify` 엔드포인트). Instagram 계정 보강처럼 assets raw evidence 를 prod entity catalog 로 반영하는 흐름도 같은 원칙을 따른다: assets 는 `raw_posts.platform_metadata` 증거만 소유하고, reviewed account/entity state 는 prod `public.instagram_accounts` / `artists` / `brands` / `groups` 가 소유한다. ## 마이그레이션 / 스키마 시스템 @@ -82,6 +83,11 @@ prod ↔ assets 사이에 cross-project FK 는 **없다**. assets 는 검증되 ├─ assets 스키마 변경 ───────────► supabase-assets/migrations/_.sql │ (별도 link → supabase db push) │ + ├─ assets raw 데이터 → prod catalog 파이프라인 + │ ───────► prod schema 변경은 supabase/migrations/* + │ assets schema 변경은 supabase-assets/migrations/* + │ cross-project FK 금지, app/worker 가 idempotent write + │ └─ SeaORM 변경 ─────────────────► ❌ 추가하지 말 것 (B.3 #374 까지) 기존 SeaORM 마이그레이션도 손대지 말 것 ``` @@ -113,6 +119,15 @@ prod ↔ assets 사이에 cross-project FK 는 **없다**. assets 는 검증되 2. `cd supabase-assets && supabase db push` 3. ai-server / api-server 의 status 머신 코드 동기 갱신 +**(e) Instagram tagged account 를 entity catalog 로 보강** + +1. assets `raw_posts.platform_metadata.tagged_usernames` 는 후보 증거로만 취급한다. +2. prod `public.instagram_accounts` / `artists` / `brands` / `groups` 변경은 `supabase/migrations/_entity_enrichment_*.sql` 로 추가한다. +3. Gemini grounding 사용량과 scheduler running/progress 같은 pipeline runtime 상태는 assets `pipeline_settings` / `gemini_usage_events` 가 소유한다. +4. admin 이 보는 quota/status 는 assets 에서 읽고, 검증된 entity 결과만 prod 에 쓴다. +5. assets 와 prod 사이 FK 는 만들지 않는다. worker/admin API 가 username 기준으로 idempotent upsert 한다. +6. 상세 RFC: [`docs/database/entity-enrichment-pipeline.md`](entity-enrichment-pipeline.md). + ## drift 발생 패턴 + 회피 | 패턴 | 사례 / 회피책 | @@ -157,6 +172,7 @@ unset PRD_DB_URL | 마이그레이션 SOT 정책, `SKIP_DB_MIGRATIONS` | [`docs/DATABASE-MIGRATIONS.md`](../DATABASE-MIGRATIONS.md) | | Supabase CLI 사용법 (link, push, gen types) | [`docs/database/04-supabase-cli-setup.md`](04-supabase-cli-setup.md) | | nightly drift CI 운영 (#373) | [`docs/database/drift-check.md`](drift-check.md) | +| Entity enrichment RFC | [`docs/database/entity-enrichment-pipeline.md`](entity-enrichment-pipeline.md) | | PRD → dev 시드 자동화 스크립트 | [`scripts/seed-from-prod.sh`](../../scripts/seed-from-prod.sh) | | assets 프로젝트 설계 (#333) | [`docs/architecture/assets-project.md`](../architecture/assets-project.md) | | agent 짧은 요약 | [`docs/agent/database-summary.md`](../agent/database-summary.md) | @@ -167,3 +183,4 @@ unset PRD_DB_URL - **2026-04-30** — 초기 작성 (#371). PRD→dev 시드 작업 중 발견된 `post_magazines.status='failed'` drift (#372) 를 계기로 운영 모델을 1페이지로 정리. 기존에 흩어져 있던 `agent/environments.md`, `DATABASE-MIGRATIONS.md`, `agent/database-summary.md`, `database/04-supabase-cli-setup.md` 의 진입점 역할. - **2026-04-30** — 후속: dev→prod 시드 절차를 `just seed-from-prod` (#377) 한 줄로 단축. drift 패턴 표에 `post_magazines.status='failed'` (#372) 사례를 inline 인용. nightly drift CI (#373, #378) 와 시드 스크립트를 관련 문서에 등록. +- **2026-05-11** — assets Instagram tagged accounts 를 prod entity catalog 로 보강하는 cross-project pipeline 원칙과 RFC 링크 추가. diff --git a/docs/home-sections-backend-feasibility.md b/docs/home-sections-backend-feasibility.md deleted file mode 100644 index 1aa97479..00000000 --- a/docs/home-sections-backend-feasibility.md +++ /dev/null @@ -1,161 +0,0 @@ -# 홈 화면 간소화 계획 (MVP Launch) - -**목표:** 초기 플랫폼 데이터 한계를 인정하고, 실데이터로 즉시 동작 가능한 섹션만으로 구성하여 빠르게 런칭 → 이터레이션 - ---- - -## 왜 간소화해야 하는가? - -| 문제 | 설명 | -|------|------| -| **데이터 부족** | 초기 런칭 시 게시물 수, 유저 상호작용, 검색 이력 등이 절대적으로 부족 | -| **빈 섹션 문제** | 11개 섹션 중 대다수가 하드코딩 fallback에 의존 → 실사용자에게 신뢰도 저하 | -| **유지보수 부담** | 섹션마다 별도 Supabase 쿼리 + fallback 로직 + 백엔드 매핑 필요 | -| **사용자 집중도** | 너무 많은 섹션은 핵심 가치("셀럽 패션 디코딩")를 희석 | - ---- - -## 섹션 분류: 유지 vs 제거 - -### 유지 (4개 섹션) - -| # | 섹션 | 유지 근거 | 데이터 요건 | -|:-:|------|----------|------------| -| 1 | **Hero** | 플랫폼 첫인상. 캐러셀/슬라이드로 여러 게시물 노출 | `posts` 5개 (view_count DESC) | -| 2 | **Artist Spotlight** | 핵심 가치 전달 ("셀럽 패션"). artist_name이 있는 게시물이면 충분 | `posts` WHERE `artist_name IS NOT NULL` (2~4개) | -| 3 | **What's New** | 신규 콘텐츠를 두 가지로 구분 노출 | **(3a)** 솔루션 있는 post: 최신순. **(3b)** 솔루션 없는 post: 최신순 + "이 spot에 솔루션 제안하기" 유도 | -| 4 | **Weekly Best** | 인기 콘텐츠 갤러리. view_count 기반이라 게시물만 있으면 동작 | `posts` 4~8개 (view_count DESC) | - -### 제거 (7개 섹션) - -| # | 섹션 | 제거 근거 | -|:-:|------|----------| -| 2 | **Decoded's Pick** | 에디터 큐레이션 로직 없음. 초기에는 큐레이션할 콘텐츠 풀 자체가 부족 | -| 3 | **Today's Decoded** | 100% 하드코딩. `stylingTip` 백엔드 미지원. 완전 신규 개발 필요 | -| 6 | **Badge Grid** | 배지 시스템은 유저 활동 데이터가 쌓인 후에 의미. 홈 핵심 가치와 무관 | -| 7 | **Discover Items** | API가 항상 빈 배열 반환. 아티스트별 아이템 전용 API 없음 | -| 8 | **Discover Products** | click_count 초기 0. 카테고리 필터링 불가. 의미 있는 데이터 불가 | -| 9 | **Best Item** | Discover Products와 동일 데이터 재사용. 중복 | -| 11 | **Trending Now** | 검색 이력/아티스트 빈도 모두 초기에 의미 없는 데이터 | - ---- - -## 간소화된 홈 화면 구성 - -``` -┌─────────────────────────────────────────────┐ -│ Header │ -│ DECODED Home Explore [+] 🔍 👤 │ -├─────────────────────────────────────────────┤ -│ ── Hero (캐러셀, 5개 포스트) ── │ -│ ●───●───●───●───● view_count DESC │ -│ Section 1: Hero │ -├─────────────────────────────────────────────┤ -│ ── Global Perspectives ── │ -│ Artist Spotlight DISCOVER ALL → │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ StyleCard │ │ StyleCard │ │ View More│ │ -│ └──────────┘ └──────────┘ └──────────┘ │ -│ Section 2: Artist Spotlight │ -├─────────────────────────────────────────────┤ -│ ── What's New ── EXPLORE FEED → │ -│ (3a) 솔루션과 함께 등록된 post │ -│ posts (spot+solution 존재) 최신순, 스타일+아이템 │ -│ (3b) 솔루션 없이 등록된 post │ -│ posts (spot만 있음) 최신순 + CTA: "이 스팟 │ -│ 디코딩하기" → 유저가 솔루션 제안하도록 유도 │ -│ Section 3: What's New │ -├─────────────────────────────────────────────┤ -│ ── Editor's Weekly Roll ── │ -│ Weekly Best ●───●───●───● │ -│ posts 4~8개 (view_count DESC) │ -│ Section 4: Weekly Best │ -├─────────────────────────────────────────────┤ -│ Footer │ -└─────────────────────────────────────────────┘ -``` - -**Header 네비:** Home, Explore(탭), **[+]** Request(포스트 생성), **🔍** 검색(magnifying glass), **👤** 프로필(person 아이콘). Feed 제거. - ---- - -## 섹션별 구현 계획 - -### Section 1: Hero - -| 항목 | 내용 | -|------|------| -| **데이터 소스** | `posts` **5개** 조회 (view_count DESC). 기존 `fetchFeaturedPostServer()` → `fetchFeaturedPostsServer(5)` 또는 limit 5로 확장 | -| **Fallback** | 게시물 0개일 때 `defaultHeroData` 하드코딩 | -| **필요 작업** | Hero UI가 캐러셀/슬라이드인 경우 5개 데이터 전달. 단일 배너면 1개만 사용 가능 | - -### Section 2: Artist Spotlight - -| 항목 | 내용 | -|------|------| -| **데이터 소스** | `fetchArtistSpotlightStylesServer()` (Supabase SSR) 유지 | -| **Fallback** | 아티스트 게시물 부족 시 `sampleSpotlightData` 하드코딩 | - -### Section 3: What's New (두 하위 섹션) - -What's New를 **두 블록**으로 나눈다. - -| 하위 | 제목 | 데이터 | UX 목표 | -|------|------|--------|--------| -| **3a** | 솔루션과 함께 등록된 post | `posts` 중 **최소 1개 spot + solution** 있는 것만, `created_at` DESC. 기존 StyleCard + ItemCard 형태 (최신 2개 + solutions 4개 등) | 방금 디코딩된 콘텐츠 노출 | -| **3b** | 솔루션 없이 등록된 post | `posts` 중 **spot은 있으나 solution이 0개**인 것, `created_at` DESC | 다른 유저가 해당 **spot에 솔루션을 제안**하도록 유도. CTA 예: "이 스팟 디코딩하기" → 상세/편집 플로우로 연결 (문구는 추후 확정) | - -| 항목 | 내용 | -|------|------| -| **데이터 소스** | **(3a)** `fetchWhatsNewWithSolutionsServer()` (기존 fetchWhatsNewStylesServer + fetchWhatsNewItemsServer 유사). **(3b)** `fetchPostsWithoutSolutionsServer()` 신규: post–spots JOIN 후 solution 개수 0인 post만 필터, 최신순 | -| **Fallback** | (3a)(3b) 각각 데이터 없으면 해당 블록만 숨김. 둘 다 없으면 What's New 섹션 전체 `return null` | -| **필요 작업** | (3b) 상세/포스트 페이지에서 "이 스팟 디코딩하기" 버튼 또는 스팟 클릭 시 솔루션 제안 UI 연결. CTA 문구는 추후 확정 | - -### Section 4: Weekly Best - -| 항목 | 내용 | -|------|------| -| **데이터 소스** | `fetchWeeklyBestImagesServer(8)` (Supabase SSR) 유지 | -| **Fallback** | `sampleWeeklyStyles` 하드코딩 | - ---- - -## 코드 변경 사항 - -### `page.tsx` - -- **제거할 쿼리:** - `fetchDecodedPickStyleServer`, `fetchTrendingKeywordsServer`, `fetchBestItemsServer`, `fetchItemsByAccountServer`, `fetchAllBadgesServer` -- **유지·수정할 쿼리:** - - **Hero:** `fetchFeaturedPostServer()` → **`fetchFeaturedPostsServer(5)`** (또는 동일 함수에 limit 5 적용) - - `fetchArtistSpotlightStylesServer()` - - **What's New (3a):** `fetchWhatsNewWithSolutionsServer()` (기존 스타일+아이템 쿼리) - - **What's New (3b):** `fetchPostsWithoutSolutionsServer()` 신규 — spot은 있으나 solution 0개인 post 최신순 - - `fetchWeeklyBestImagesServer()` - -### `HomeAnimatedContent.tsx` - -- **제거:** DecodedPickSection, TodayDecodedSection, BadgeGridSection, DiscoverItemsSection, DiscoverProductsSection, BestItemSection, TrendingNowSection -- **유지:** HeroSection(5개 데이터), ArtistSpotlightSection, **WhatsNewSection(3a 데이터 + 3b 데이터)**, WeeklyBestSection - ---- - -## 성능 개선 효과 - -| 지표 | Before (11 섹션) | After (4 섹션) | -|------|:----------------:|:---------------:| -| **SSR 쿼리 수** | 11개 Promise.all | 5개 Promise.all | -| **번들 크기** | 11개 섹션 컴포넌트 | 4개 섹션 컴포넌트 | -| **하드코딩 fallback** | 8개 섹션 | 3개 섹션 (Hero, Spotlight, Weekly) | -| **빈 데이터 리스크** | 높음 | 낮음 (What's New만 자동 숨김) | - ---- - -## 이터레이션 로드맵 - -| Phase | 내용 | -|-------|------| -| **1. MVP Launch** | 4개 섹션 간소화, Supabase SSR 유지, Hero 5개 포스트 반영 | -| **2. 백엔드 전환** | Supabase → 백엔드 API, `GET /api/v1/feed` 등 활용 | -| **3. 데이터 축적 후** | Trending Now, Decoded's Pick, Today's Decoded, Discover Products 등 복원 검토 | -| **4. 개인화** | 유저별 맞춤 피드, 팔로우 아티스트 기반 섹션 | diff --git a/docs/qa-screenshots/README.md b/docs/qa-screenshots/README.md index fd8ad3d6..9b856189 100644 --- a/docs/qa-screenshots/README.md +++ b/docs/qa-screenshots/README.md @@ -1,120 +1,21 @@ -# Visual QA Screenshots - -**Generated:** 2026-02-12 -**Reference:** docs/design-system/decoded.pen -**Test Automation:** packages/web/tests/visual-qa.spec.ts - -이 문서는 v2.0 디자인 시스템 구현이 decoded.pen 디자인 참조와 일치하는지 검증하기 위한 시각적 QA 스크린샷을 포함합니다. - -**Total Screenshots:** 40 (4 viewports × 10 pages) - -## Breakpoints Tested - -| Name | Width | Height | Device | -|------|-------|--------|--------| -| Mobile | 375px | 812px | iPhone 13 Pro | -| Tablet | 768px | 1024px | iPad | -| Desktop | 1280px | 800px | Standard Desktop | -| Desktop LG | 1440px | 900px | Large Desktop | - -## Pages Captured - -| Page | Mobile | Tablet | Desktop | Desktop LG | -|------|--------|--------|---------|------------| -| Home | [mobile-home.png](./mobile-home.png) | [tablet-home.png](./tablet-home.png) | [desktop-home.png](./desktop-home.png) | [desktop-lg-home.png](./desktop-lg-home.png) | -| Explore | [mobile-explore.png](./mobile-explore.png) | [tablet-explore.png](./tablet-explore.png) | [desktop-explore.png](./desktop-explore.png) | [desktop-lg-explore.png](./desktop-lg-explore.png) | -| Feed | [mobile-feed.png](./mobile-feed.png) | [tablet-feed.png](./tablet-feed.png) | [desktop-feed.png](./desktop-feed.png) | [desktop-lg-feed.png](./desktop-lg-feed.png) | -| Search | [mobile-search.png](./mobile-search.png) | [tablet-search.png](./tablet-search.png) | [desktop-search.png](./desktop-search.png) | [desktop-lg-search.png](./desktop-lg-search.png) | -| Profile | [mobile-profile.png](./mobile-profile.png) | [tablet-profile.png](./tablet-profile.png) | [desktop-profile.png](./desktop-profile.png) | [desktop-lg-profile.png](./desktop-lg-profile.png) | -| Login | [mobile-login.png](./mobile-login.png) | [tablet-login.png](./tablet-login.png) | [desktop-login.png](./desktop-login.png) | [desktop-lg-login.png](./desktop-lg-login.png) | -| Request Upload | [mobile-request-upload.png](./mobile-request-upload.png) | [tablet-request-upload.png](./tablet-request-upload.png) | [desktop-request-upload.png](./desktop-request-upload.png) | [desktop-lg-request-upload.png](./desktop-lg-request-upload.png) | -| Images | [mobile-images.png](./mobile-images.png) | [tablet-images.png](./tablet-images.png) | [desktop-images.png](./desktop-images.png) | [desktop-lg-images.png](./desktop-lg-images.png) | -| Request | [mobile-request.png](./mobile-request.png) | [tablet-request.png](./tablet-request.png) | [desktop-request.png](./desktop-request.png) | [desktop-lg-request.png](./desktop-lg-request.png) | -| Request Detect | [mobile-request-detect.png](./mobile-request-detect.png) | [tablet-request-detect.png](./tablet-request-detect.png) | [desktop-request-detect.png](./desktop-request-detect.png) | [desktop-lg-request-detect.png](./desktop-lg-request-detect.png) | - -## Findings - -### Review Completed: 2026-02-12 - -**Status:** Approved with caveats -**Reviewer:** Human (orchestrator) -**Reviewed:** 40 screenshots (4 viewports × 10 pages) - -**Context:** API was down during screenshot capture, preventing full pixel-perfect comparison against decoded.pen design reference. Pages that successfully loaded (explore, login, request-upload) showed correct responsive layouts. - -#### Issues Identified +--- +title: Visual QA Screenshots (current quarter) +owner: human +status: approved +updated: 2026-05-21 +tags: [qa, ops] +related: + - docs/_archive/README.md +--- -##### 1. Images Page - Raw JSON Error Exposure (Major - UX/Security) -- **Issue**: Images page (`/images`) exposes raw Supabase/PostgREST JSON error details (PGRST205, table names, hint messages) to users instead of a friendly error message. -- **Severity**: Major -- **Category**: API error handling (not design/CSS) -- **Status**: Deferred to separate quick task -- **Reason**: This is an API error handling issue, not a visual design issue. The v2-09-03 plan focuses on CSS/layout visual QA. -- **Files Affected**: TBD in quick task (likely `app/images/page.tsx` or API client error handling) -- **Recommendation**: Implement user-friendly error messages for API failures (e.g., "Unable to load images. Please try again later.") - -##### 2. Next.js Dev Overlay Badge (Minor - Dev Only) -- **Issue**: "14 Issues" badge visible on images page in dev mode -- **Severity**: Minor -- **Category**: Development artifact -- **Status**: Not applicable (dev mode only, not production) - -##### 3. Mobile Bottom Nav Loading Text (Minor - Dev Only) -- **Issue**: "Compiling..." text visible on mobile bottom navigation -- **Severity**: Minor -- **Category**: Development artifact -- **Status**: Not applicable (dev mode only, not production) - -#### Visual QA Results - -**Responsive Layouts:** ✓ Correct on pages that loaded (explore, login, request-upload) -- Mobile (375px): Layouts correctly adapt -- Tablet (768px): Grid columns adjust appropriately -- Desktop (1280px, 1440px): Full desktop layouts render properly - -**Design System Compliance:** ✓ Observed on loaded pages -- Typography sizing and hierarchy consistent -- Spacing and padding follow design tokens -- Color usage matches design system - -**Deferred:** -- Full pixel-perfect comparison against decoded.pen (API down prevented complete page loading) -- Comprehensive cross-page consistency check (API dependency blocked several pages) - -### Next Steps - -1. **Quick Task**: Fix images page raw JSON error exposure (API error handling) -2. **Future QA**: Re-run visual QA when API is available for complete verification - -## Test Automation - -스크린샷은 Playwright로 자동화되어 있으며, 언제든지 재생성할 수 있습니다. - -### Prerequisites -```bash -# Playwright 브라우저 설치 (최초 1회) -cd packages/web -yarn playwright install chromium -``` - -### Regeneration -```bash -# 개발 서버 시작 -yarn dev:web +# Visual QA Screenshots -# 다른 터미널에서 테스트 실행 -cd packages/web -yarn playwright test tests/visual-qa.spec.ts -``` +이 디렉토리는 `packages/web/tests/visual-qa.spec.ts`의 **최신 분기 출력 경로**다. 새 스크린샷은 항상 여기로 기록된다. -### Test Configuration -- **Config:** packages/web/playwright.config.ts -- **Test:** packages/web/tests/visual-qa.spec.ts -- **Output:** docs/qa-screenshots/ +## Quarterly archive policy -## Notes +분기 종료 시 현재 PNG와 비교 산출물(`DIFFERENCES.md`)을 `docs/_archive/qa-screenshots-YYYY-QN/`로 이동한다. 이 디렉토리는 항상 "지금 분기" 출력만 보유한다. -- 모든 스크린샷은 full-page 캡처로 생성됩니다 (전체 페이지 스크롤 포함) -- 각 페이지는 networkidle 상태를 기다린 후 500ms 추가 대기하여 애니메이션이 완료되도록 합니다 -- Search 페이지는 `?q=dress` 쿼리 파라미터로 검색 결과 상태를 캡처합니다 -- 스크린샷 파일명 형식: `{viewport}-{page}.png` +- 직전 분기: [`docs/_archive/qa-screenshots-2026-Q1/`](../_archive/qa-screenshots-2026-Q1/) +- 정책: [`docs/_archive/README.md`](../_archive/README.md) +- 테스트: `packages/web/tests/visual-qa.spec.ts` diff --git a/docs/superpowers/plans/2026-04-09-harness-memory-docs-update.md b/docs/superpowers/plans/2026-04-09-harness-memory-docs-update.md index 9b46da59..6c7d018f 100644 --- a/docs/superpowers/plans/2026-04-09-harness-memory-docs-update.md +++ b/docs/superpowers/plans/2026-04-09-harness-memory-docs-update.md @@ -154,7 +154,7 @@ type: project - Modify: `docs/agent/web-routes-and-features.md` - Modify: `docs/agent/api-v1-routes.md` - Modify: `docs/agent/web-hooks-and-stores.md` -- Modify: `docs/agent/warehouse-schema.md` +- Modify: `docs/_archive/warehouse-schema.md` - [ ] **Step 1: web-routes-and-features.md 업데이트** diff --git a/docs/superpowers/plans/2026-04-17-llm-wiki-foundation.md b/docs/superpowers/plans/2026-04-17-llm-wiki-foundation.md index a3c5520e..f377ddca 100644 --- a/docs/superpowers/plans/2026-04-17-llm-wiki-foundation.md +++ b/docs/superpowers/plans/2026-04-17-llm-wiki-foundation.md @@ -303,7 +303,7 @@ related: | 아키텍처 스냅샷 (자동 분석) | `.planning/codebase/ARCHITECTURE.md` | `docs/agent/architecture-summary.md` | | API 라우트 | `docs/agent/api-v1-routes.md` | `docs/agent/api-summary.md` | | DB 사용법 | `docs/database/01-schema-usage.md` 등 | `docs/agent/database-summary.md` | -| Warehouse 스키마 | `docs/agent/warehouse-schema.md` | `docs/agent/database-summary.md` | +| Warehouse 스키마 | `docs/_archive/warehouse-schema.md` | `docs/agent/database-summary.md` | | 디자인 시스템 | `docs/design-system/**` + `docs/agent/design-system-llm.md` | `docs/agent/design-system-summary.md` | | 에이전트 운영·Gotchas | `docs/wiki/wiki/**` | `docs/agent/README.md` | | 에이전트별 프로필 | `docs/ai-playbook/*.md` | `docs/agent/ai-playbook-summary.md` | @@ -766,7 +766,7 @@ updated: 2026-04-17 tags: [db, agent] related: - docs/database/01-schema-usage.md - - docs/agent/warehouse-schema.md + - docs/_archive/warehouse-schema.md - docs/database/04-supabase-cli-setup.md --- @@ -781,7 +781,7 @@ Supabase 기반 이중 스키마 (public / warehouse) 구조의 진입점. 앱 - 스키마 사용법: [`docs/database/01-schema-usage.md`](../database/01-schema-usage.md) - 데이터 흐름: [`docs/database/03-data-flow.md`](../database/03-data-flow.md) - 업데이트 체크리스트: [`docs/database/02-update-checklist.md`](../database/02-update-checklist.md) -- Warehouse 스키마 인벤토리: [`docs/agent/warehouse-schema.md`](warehouse-schema.md) +- Warehouse 스키마 인벤토리: [`docs/_archive/warehouse-schema.md`](warehouse-schema.md) - Supabase CLI setup: [`docs/database/04-supabase-cli-setup.md`](../database/04-supabase-cli-setup.md) ## Key files / concepts @@ -978,7 +978,7 @@ git push origin chore/153-docs-harness-wiki - Modify: `docs/agent/monorepo.md` (프론트매터 추가) - Modify: `docs/agent/api-v1-routes.md` (프론트매터) -- Modify: `docs/agent/warehouse-schema.md` (프론트매터) +- Modify: `docs/_archive/warehouse-schema.md` (프론트매터) - Modify: `docs/agent/web-hooks-and-stores.md` (프론트매터) - Modify: `docs/agent/web-routes-and-features.md` (프론트매터) - Modify: `docs/agent/design-system-llm.md` (프론트매터) diff --git a/docs/superpowers/plans/2026-05-07-profile-tries-detail-modal.md b/docs/superpowers/plans/2026-05-07-profile-tries-detail-modal.md new file mode 100644 index 00000000..5ec91693 --- /dev/null +++ b/docs/superpowers/plans/2026-05-07-profile-tries-detail-modal.md @@ -0,0 +1,1293 @@ +# Profile Tries Detail Modal Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Clicking a saved try-on in `/profile` -> `Tries` opens an in-place detail modal backed by save-time provenance snapshots and safe legacy fallbacks. + +**Architecture:** Keep the existing `user_tryon_history` table shape. Expand the POST save payload into `style_combination`, normalize that JSON in the profile tries route, regenerate the generated web API model from the OpenAPI source, and keep the detail UI local to the profile grid. + +**Tech Stack:** Next.js App Router route handlers, Supabase JS, React 19, TanStack Query, Vitest, Testing Library, Orval generated API models. + +--- + +## Spec Review Gate + +Verdict: proceed to implementation planning. + +Required planning adjustment: the spec's file list must include the OpenAPI source path because `TriesGrid` imports `TryItem` from `packages/web/lib/api/generated/models`. Do not hand-edit `packages/web/lib/api/generated/*`; update the source schema in `packages/api-server/src/domains/users/dto.rs`, regenerate `packages/api-server/openapi.json` through the repo's API-server flow if needed, then run `bun run generate:api` from `packages/web`. + +No migration is part of this pass. Storage migration remains a follow-up. + +## File Structure + +- Modify `packages/web/app/api/v1/tries/route.ts` + - Validate expanded save payload fields. + - Store `person_original_image`, `source_post_snapshot`, and `selected_items_snapshot` inside `style_combination`. +- Modify `packages/web/app/api/v1/tries/__tests__/route.test.ts` + - Cover snapshot save and legacy ID-only save. +- Modify `packages/web/app/api/v1/users/me/tries/route.ts` + - Select `style_combination`. + - Normalize malformed, null, legacy, and snapshot rows. + - Resolve current post and solution rows only for legacy rows with IDs and no snapshots. +- Modify `packages/web/app/api/v1/users/me/tries/__tests__/route.test.ts` + - Cover snapshot response and legacy fallback response. +- Modify `packages/api-server/src/domains/users/dto.rs` + - Expand `TryItem` schema with nullable source post, selected item, and original image fields. +- Regenerate `packages/api-server/openapi.json` if this repo's API-server command is available. +- Regenerate `packages/web/lib/api/generated/models/tryItem.ts` and related generated files via `bun run generate:api`. +- Modify `packages/web/lib/hooks/useVtonTryOn.ts` + - Accept source post snapshot and include snapshots in save request. +- Modify `packages/web/lib/components/vton/VtonModal.tsx` + - Derive `sourcePostSnapshot` from `posts` and pass it to the hook. +- Create `packages/web/lib/components/profile/TryDetailModal.tsx` + - Present detail-ready try item content. +- Modify `packages/web/lib/components/profile/TriesGrid.tsx` + - Own selected try state and render `TryDetailModal`. +- Create `packages/web/lib/components/profile/__tests__/TryDetailModal.test.tsx` + - Verify full metadata and fallback rendering. +- Create `packages/web/lib/components/profile/__tests__/TriesGrid.test.tsx` + - Verify card click opens the modal. + +## Task 1: Save Expanded Snapshot Contract + +**Files:** +- Modify: `packages/web/app/api/v1/tries/route.ts` +- Modify: `packages/web/app/api/v1/tries/__tests__/route.test.ts` + +- [ ] **Step 1: Add failing POST snapshot test** + +Add this test case to `describe("POST /api/v1/tries", ...)` in `packages/web/app/api/v1/tries/__tests__/route.test.ts`. + +```ts +it("stores detail snapshots in style_combination", async () => { + getUserMock.mockResolvedValue({ data: { user: { id: "user-1" } } }); + singleMock.mockResolvedValue({ + data: { + id: "try-1", + image_url: "data:image/png;base64,result", + created_at: "2026-05-07T00:00:00Z", + }, + error: null, + }); + + const { POST } = await import("../route"); + const res = await POST( + makeRequest({ + result_image: "data:image/png;base64,result", + person_original_image: "data:image/png;base64,person", + source_post_id: "post-1", + selected_item_ids: ["item-1", "item-2"], + source_post_snapshot: { + id: "post-1", + title: "Source Look", + image_url: "https://example.com/post.jpg", + artist_name: "Artist", + group_name: null, + context: "Airport look", + }, + selected_items_snapshot: [ + { + id: "item-1", + title: "Jacket", + thumbnail_url: "https://example.com/jacket.jpg", + description: "Black jacket", + keywords: ["outerwear"], + }, + ], + }) + ); + + expect(res.status).toBe(201); + expect(insertMock).toHaveBeenCalledWith("user_tryon_history", { + user_id: "user-1", + image_url: "data:image/png;base64,result", + style_combination: { + source_post_id: "post-1", + selected_item_ids: ["item-1", "item-2"], + person_original_image: "data:image/png;base64,person", + source_post_snapshot: { + id: "post-1", + title: "Source Look", + image_url: "https://example.com/post.jpg", + artist_name: "Artist", + group_name: null, + context: "Airport look", + }, + selected_items_snapshot: [ + { + id: "item-1", + title: "Jacket", + thumbnail_url: "https://example.com/jacket.jpg", + description: "Black jacket", + keywords: ["outerwear"], + }, + ], + }, + }); +}); +``` + +- [ ] **Step 2: Run the failing POST test** + +Run: + +```bash +cd packages/web +bun run test:unit app/api/v1/tries/__tests__/route.test.ts +``` + +Expected: the new test fails because `person_original_image`, `source_post_snapshot`, and `selected_items_snapshot` are not stored. + +- [ ] **Step 3: Implement payload normalization** + +In `packages/web/app/api/v1/tries/route.ts`, replace `SaveTryOnBody` and add the helpers near `toStringArray`. + +```ts +interface SourcePostSnapshot { + id: string; + title: string | null; + image_url: string | null; + artist_name?: string | null; + group_name?: string | null; + context?: string | null; +} + +interface SelectedItemSnapshot { + id: string; + title: string; + thumbnail_url: string; + description?: string | null; + keywords?: string[] | null; +} + +interface SaveTryOnBody { + result_image?: unknown; + image_url?: unknown; + person_original_image?: unknown; + source_post_id?: unknown; + selected_item_ids?: unknown; + source_post_snapshot?: unknown; + selected_items_snapshot?: unknown; +} + +function optionalString(value: unknown): string | null { + return typeof value === "string" && value.length > 0 ? value : null; +} + +function nullableString(value: unknown): string | null { + return typeof value === "string" ? value : null; +} + +function toSourcePostSnapshot(value: unknown): SourcePostSnapshot | null { + if (!value || typeof value !== "object") return null; + const input = value as Record; + const id = optionalString(input.id); + if (!id) return null; + return { + id, + title: nullableString(input.title), + image_url: nullableString(input.image_url), + artist_name: nullableString(input.artist_name), + group_name: nullableString(input.group_name), + context: nullableString(input.context), + }; +} + +function toSelectedItemSnapshots(value: unknown): SelectedItemSnapshot[] { + if (!Array.isArray(value)) return []; + return value.flatMap((entry) => { + if (!entry || typeof entry !== "object") return []; + const input = entry as Record; + const id = optionalString(input.id); + const title = optionalString(input.title); + const thumbnailUrl = optionalString(input.thumbnail_url); + if (!id || !title || !thumbnailUrl) return []; + return [ + { + id, + title, + thumbnail_url: thumbnailUrl, + description: nullableString(input.description), + keywords: Array.isArray(input.keywords) + ? input.keywords.filter( + (keyword): keyword is string => typeof keyword === "string" + ) + : null, + }, + ]; + }); +} +``` + +Then update the `style_combination` insert object. + +```ts +const styleCombination = { + source_post_id: + typeof body.source_post_id === "string" ? body.source_post_id : null, + selected_item_ids: toStringArray(body.selected_item_ids), + person_original_image: optionalString(body.person_original_image), + source_post_snapshot: toSourcePostSnapshot(body.source_post_snapshot), + selected_items_snapshot: toSelectedItemSnapshots( + body.selected_items_snapshot + ), +}; +``` + +Use `style_combination: styleCombination` in the insert. + +- [ ] **Step 4: Keep legacy test green** + +Update existing expectations in `route.test.ts` for legacy payloads to include the newly persisted nullable fields. + +```ts +style_combination: { + source_post_id: "post-1", + selected_item_ids: ["item-1", "item-2"], + person_original_image: null, + source_post_snapshot: null, + selected_items_snapshot: [], +}, +``` + +- [ ] **Step 5: Verify POST route tests** + +Run: + +```bash +cd packages/web +bun run test:unit app/api/v1/tries/__tests__/route.test.ts +``` + +Expected: all tests in `route.test.ts` pass. + +## Task 2: Return Detail-Ready Try Items + +**Files:** +- Modify: `packages/web/app/api/v1/users/me/tries/route.ts` +- Modify: `packages/web/app/api/v1/users/me/tries/__tests__/route.test.ts` + +- [ ] **Step 1: Add failing snapshot response test** + +Extend the mock chain in `route.test.ts` so `.from("user_tryon_history")` can return the existing chain, then add this test. + +```ts +it("returns expanded fields from saved snapshots", async () => { + getUserMock.mockResolvedValue({ data: { user: { id: "user-1" } } }); + rangeMock.mockResolvedValue({ + data: [ + { + id: "try-1", + image_url: "https://example.com/result.png", + created_at: "2026-05-07T00:00:00Z", + style_combination: { + source_post_id: "post-1", + selected_item_ids: ["item-1"], + person_original_image: "https://example.com/person.png", + source_post_snapshot: { + id: "post-1", + title: "Source Look", + image_url: "https://example.com/post.png", + artist_name: "Artist", + group_name: null, + context: "Stage look", + }, + selected_items_snapshot: [ + { + id: "item-1", + title: "Jacket", + thumbnail_url: "https://example.com/jacket.png", + description: "Black jacket", + keywords: ["outerwear"], + }, + ], + }, + }, + ], + error: null, + count: 1, + }); + + const { GET } = await import("../route"); + const res = await GET(makeRequest()); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(selectMock).toHaveBeenCalledWith( + "user_tryon_history", + "id, image_url, created_at, style_combination", + { count: "exact" } + ); + expect(json.data[0]).toEqual({ + id: "try-1", + image_url: "https://example.com/result.png", + created_at: "2026-05-07T00:00:00Z", + source_post_id: "post-1", + selected_item_ids: ["item-1"], + person_original_image: "https://example.com/person.png", + source_post: { + id: "post-1", + title: "Source Look", + image_url: "https://example.com/post.png", + artist_name: "Artist", + group_name: null, + context: "Stage look", + }, + selected_items: [ + { + id: "item-1", + title: "Jacket", + thumbnail_url: "https://example.com/jacket.png", + description: "Black jacket", + keywords: ["outerwear"], + }, + ], + }); +}); +``` + +- [ ] **Step 2: Add failing legacy fallback test** + +Add a test where `style_combination` is null. It should return detail fields with nulls and empty arrays. + +```ts +it("returns fallback detail fields for null style_combination", async () => { + getUserMock.mockResolvedValue({ data: { user: { id: "user-1" } } }); + rangeMock.mockResolvedValue({ + data: [ + { + id: "try-legacy", + image_url: "https://example.com/legacy.png", + created_at: "2026-05-07T00:00:00Z", + style_combination: null, + }, + ], + error: null, + count: 1, + }); + + const { GET } = await import("../route"); + const res = await GET(makeRequest()); + const json = await res.json(); + + expect(json.data[0]).toMatchObject({ + id: "try-legacy", + source_post_id: null, + selected_item_ids: [], + person_original_image: null, + source_post: null, + selected_items: [], + }); +}); +``` + +- [ ] **Step 3: Run the failing GET route tests** + +Run: + +```bash +cd packages/web +bun run test:unit app/api/v1/users/me/tries/__tests__/route.test.ts +``` + +Expected: tests fail because the route does not select or map `style_combination`. + +- [ ] **Step 4: Implement normalizers** + +Add these types and helpers in `packages/web/app/api/v1/users/me/tries/route.ts`. + +```ts +type TrySourcePost = { + id: string; + title: string | null; + image_url: string | null; + artist_name?: string | null; + group_name?: string | null; + context?: string | null; +}; + +type TrySelectedItem = { + id: string; + title: string; + thumbnail_url: string; + description?: string | null; + keywords?: string[] | null; +}; + +type StyleCombination = { + source_post_id: string | null; + selected_item_ids: string[]; + person_original_image: string | null; + source_post_snapshot: TrySourcePost | null; + selected_items_snapshot: TrySelectedItem[]; +}; + +function stringOrNull(value: unknown): string | null { + return typeof value === "string" && value.length > 0 ? value : null; +} + +function stringArray(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return value.filter((item): item is string => typeof item === "string"); +} + +function normalizeSourcePost(value: unknown): TrySourcePost | null { + if (!value || typeof value !== "object") return null; + const input = value as Record; + const id = stringOrNull(input.id); + if (!id) return null; + return { + id, + title: stringOrNull(input.title), + image_url: stringOrNull(input.image_url), + artist_name: stringOrNull(input.artist_name), + group_name: stringOrNull(input.group_name), + context: stringOrNull(input.context), + }; +} + +function normalizeSelectedItems(value: unknown): TrySelectedItem[] { + if (!Array.isArray(value)) return []; + return value.flatMap((entry) => { + if (!entry || typeof entry !== "object") return []; + const input = entry as Record; + const id = stringOrNull(input.id); + const title = stringOrNull(input.title); + const thumbnailUrl = stringOrNull(input.thumbnail_url); + if (!id || !title || !thumbnailUrl) return []; + return [ + { + id, + title, + thumbnail_url: thumbnailUrl, + description: stringOrNull(input.description), + keywords: Array.isArray(input.keywords) + ? input.keywords.filter( + (keyword): keyword is string => typeof keyword === "string" + ) + : null, + }, + ]; + }); +} + +function normalizeStyleCombination(value: unknown): StyleCombination { + if (!value || typeof value !== "object") { + return { + source_post_id: null, + selected_item_ids: [], + person_original_image: null, + source_post_snapshot: null, + selected_items_snapshot: [], + }; + } + const input = value as Record; + return { + source_post_id: stringOrNull(input.source_post_id), + selected_item_ids: stringArray(input.selected_item_ids), + person_original_image: stringOrNull(input.person_original_image), + source_post_snapshot: normalizeSourcePost(input.source_post_snapshot), + selected_items_snapshot: normalizeSelectedItems( + input.selected_items_snapshot + ), + }; +} +``` + +- [ ] **Step 5: Select and map detail fields** + +Change the Supabase select and response map. + +```ts +.select("id, image_url, created_at, style_combination", { count: "exact" }) +``` + +Then map rows before returning. + +```ts +const tries = (data ?? []).map((row) => { + const style = normalizeStyleCombination( + (row as { style_combination?: unknown }).style_combination + ); + return { + id: row.id, + image_url: row.image_url, + created_at: row.created_at, + source_post_id: style.source_post_id, + selected_item_ids: style.selected_item_ids, + person_original_image: style.person_original_image, + source_post: style.source_post_snapshot, + selected_items: style.selected_items_snapshot, + }; +}); +``` + +Use `data: tries` in `NextResponse.json`. + +- [ ] **Step 6: Add best-effort ID lookup for legacy rows** + +After the snapshot-only tests pass, add one more test for a legacy row with `source_post_id` and `selected_item_ids` but no snapshots. Mock additional `from("posts")` and `from("solutions")` chains that return current rows. Implement lookup only for missing snapshots, using these select shapes: + +```ts +const { data: postRows } = await supabase + .from("posts") + .select("id, title, image_url, artist_name, group_name, context") + .in("id", sourcePostIds); + +const { data: solutionRows } = await supabase + .from("solutions") + .select("id, title, thumbnail_url, description, keywords") + .in("id", selectedItemIds); +``` + +Use saved snapshots first, then maps from current rows, then null or `[]`. + +- [ ] **Step 7: Verify GET route tests** + +Run: + +```bash +cd packages/web +bun run test:unit app/api/v1/users/me/tries/__tests__/route.test.ts +``` + +Expected: all tests pass. + +## Task 3: Regenerate TryItem API Model + +**Files:** +- Modify: `packages/api-server/src/domains/users/dto.rs` +- Regenerate: `packages/api-server/openapi.json` +- Regenerate: `packages/web/lib/api/generated/models/tryItem.ts` +- Regenerate: related generated files under `packages/web/lib/api/generated/` + +- [ ] **Step 1: Expand OpenAPI DTO source** + +In `packages/api-server/src/domains/users/dto.rs`, add nested DTOs near `TryItem` and replace `TryItem`. + +```rust +/// VTON 히스토리 원본 포스트 스냅샷 +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct TrySourcePost { + pub id: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub image_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub artist_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub group_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, +} + +/// VTON 히스토리 선택 아이템 스냅샷 +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct TrySelectedItem { + pub id: Uuid, + pub title: String, + pub thumbnail_url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub keywords: Option>, +} + +/// VTON 히스토리 아이템 +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct TryItem { + pub id: Uuid, + pub image_url: String, + pub created_at: DateTime, + #[serde(skip_serializing_if = "Option::is_none")] + pub source_post_id: Option, + pub selected_item_ids: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub person_original_image: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub source_post: Option, + pub selected_items: Vec, +} +``` + +- [ ] **Step 2: Keep API-server compile errors explicit** + +Run from repo root: + +```bash +cargo check -p api-server +``` + +Expected: if `users/service.rs` still constructs the old three-field `TryItem`, Rust reports missing fields. + +- [ ] **Step 3: Update API-server service construction** + +In `packages/api-server/src/domains/users/service.rs`, update the existing `TryItem` construction to compile with empty detail fields. The Next route owns the detail implementation for this pass. + +```rust +TryItem { + id: r.id, + image_url: r.image_url, + created_at: r.created_at.into(), + source_post_id: None, + selected_item_ids: Vec::new(), + person_original_image: None, + source_post: None, + selected_items: Vec::new(), +} +``` + +- [ ] **Step 4: Regenerate OpenAPI and web client** + +Use the repo's existing OpenAPI dump command if available. If no package script exists, run the local binary command used by this repository. + +```bash +cd packages/api-server +cargo run --bin dump_openapi +cd ../web +bun run generate:api +``` + +Expected: `packages/web/lib/api/generated/models/tryItem.ts` includes `source_post_id`, `selected_item_ids`, `person_original_image`, `source_post`, and `selected_items`. + +- [ ] **Step 5: Verify generated model shape** + +Run: + +```bash +rg -n "source_post_id|selected_items|person_original_image" packages/web/lib/api/generated/models/tryItem.ts +``` + +Expected: all three fields are present. + +## Task 4: Send Save-Time Snapshots from VTON + +**Files:** +- Modify: `packages/web/lib/hooks/useVtonTryOn.ts` +- Modify: `packages/web/lib/components/vton/VtonModal.tsx` + +- [ ] **Step 1: Extend hook options** + +In `useVtonTryOn.ts`, import `VtonPostData` and add snapshot fields. + +```ts +import type { VtonPostData } from "@/lib/hooks/useVtonPostFetch"; + +interface UseVtonTryOnOptions { + personImage: string | null; + personPreview: string | null; + selectedItems: ItemData[]; + sourcePostId: string | null; + sourcePostSnapshot: Omit | null; + displayResultImage: string | null; + abortControllerRef: React.RefObject; + onTryOnStart: () => void; + onTryOnComplete: (resultDataUrl: string, latencyMs: number | null) => void; + onTryOnError: (message: string) => void; + onTryOnFinally: () => void; + startBackgroundJob: ( + personPreview: string, + personImageBase64: string, + selectedItems: ItemData[] + ) => string; +} +``` + +- [ ] **Step 2: Add snapshot payload to save request** + +Include these fields in the `JSON.stringify` body inside `handleSaveToProfile`. + +```ts +body: JSON.stringify({ + result_image: displayResultImage, + person_original_image: personPreview, + source_post_id: sourcePostId, + selected_item_ids: selectedItems.map((i) => i.id), + source_post_snapshot: sourcePostSnapshot, + selected_items_snapshot: selectedItems.map((item) => ({ + id: item.id, + title: item.title, + thumbnail_url: item.thumbnail_url, + description: item.description, + keywords: item.keywords, + })), +}), +``` + +Add `personPreview` and `sourcePostSnapshot` to the callback dependency list. + +- [ ] **Step 3: Derive source post snapshot in VtonModal** + +In `VtonModal.tsx`, derive a snapshot after `posts` and `sourcePostId` are available. + +```ts +const sourcePostSnapshot = useMemo(() => { + if (!sourcePostId) return null; + const post = posts.find((entry) => entry.id === sourcePostId); + if (!post) return null; + return { + id: post.id, + title: post.title, + image_url: post.image_url, + artist_name: post.artist_name, + group_name: post.group_name, + context: post.context, + }; +}, [posts, sourcePostId]); +``` + +Pass it to `useVtonTryOn`. + +```ts +sourcePostSnapshot, +``` + +- [ ] **Step 4: Verify TypeScript for VTON files** + +Run: + +```bash +cd packages/web +bun run typecheck +``` + +Expected: no type errors from `useVtonTryOn.ts` or `VtonModal.tsx`. + +## Task 5: Build Try Detail Modal + +**Files:** +- Create: `packages/web/lib/components/profile/TryDetailModal.tsx` +- Create: `packages/web/lib/components/profile/__tests__/TryDetailModal.test.tsx` + +- [ ] **Step 1: Write full metadata modal test** + +Create `TryDetailModal.test.tsx`. + +```tsx +/** + * @vitest-environment jsdom + */ +import React from "react"; +import { describe, expect, it, vi } from "vitest"; +import { fireEvent, render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { TryDetailModal } from "../TryDetailModal"; +import type { TryItem } from "@/lib/api/generated/models"; + +vi.mock("next/image", () => ({ + default: (props: React.ImgHTMLAttributes) => ( + + ), +})); + +const fullTry: TryItem = { + id: "try-1", + image_url: "https://example.com/result.png", + created_at: "2026-05-07T00:00:00Z", + source_post_id: "post-1", + selected_item_ids: ["item-1"], + person_original_image: "https://example.com/person.png", + source_post: { + id: "post-1", + title: "Source Look", + image_url: "https://example.com/post.png", + artist_name: "Artist", + group_name: null, + context: "Stage look", + }, + selected_items: [ + { + id: "item-1", + title: "Jacket", + thumbnail_url: "https://example.com/jacket.png", + description: "Black jacket", + keywords: ["outerwear"], + }, + ], +}; + +describe("TryDetailModal", () => { + it("renders result, original, source post, and selected items", () => { + render( {}} />); + + expect(screen.getByRole("dialog")).toBeInTheDocument(); + expect(screen.getByAltText("Try-on result")).toBeInTheDocument(); + expect(screen.getByAltText("Uploaded original")).toBeInTheDocument(); + expect(screen.getByText("Source Look")).toBeInTheDocument(); + expect(screen.getByText("Jacket")).toBeInTheDocument(); + expect(screen.getByText("outerwear")).toBeInTheDocument(); + }); + + it("calls onClose from the close button", () => { + const onClose = vi.fn(); + render(); + fireEvent.click(screen.getByRole("button", { name: /close/i })); + expect(onClose).toHaveBeenCalled(); + }); +}); +``` + +- [ ] **Step 2: Add fallback modal test** + +Add this case to the same test file. + +```tsx +it("renders fallback text when metadata is missing", () => { + render( + {}} + /> + ); + + expect(screen.getByText("Original image unavailable")).toBeInTheDocument(); + expect(screen.getByText("Source post unavailable")).toBeInTheDocument(); + expect(screen.getByText("Items unavailable")).toBeInTheDocument(); +}); +``` + +- [ ] **Step 3: Run failing modal tests** + +Run: + +```bash +cd packages/web +bun run test:unit lib/components/profile/__tests__/TryDetailModal.test.tsx +``` + +Expected: module not found until `TryDetailModal.tsx` is created. + +- [ ] **Step 4: Implement TryDetailModal** + +Create `packages/web/lib/components/profile/TryDetailModal.tsx`. + +```tsx +"use client"; + +import { useEffect } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { X } from "lucide-react"; +import type { TryItem } from "@/lib/api/generated/models"; + +interface TryDetailModalProps { + tryItem: TryItem; + onClose: () => void; +} + +export function TryDetailModal({ tryItem, onClose }: TryDetailModalProps) { + useEffect(() => { + function onKeyDown(event: KeyboardEvent) { + if (event.key === "Escape") onClose(); + } + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [onClose]); + + const savedDate = new Date(tryItem.created_at).toLocaleDateString("ko-KR", { + year: "numeric", + month: "short", + day: "numeric", + }); + + return ( +
{ + if (event.target === event.currentTarget) onClose(); + }} + > +
+ + +
+
+
+ Try-on result +
+
+ Result +
+
+ + {tryItem.person_original_image ? ( +
+
+ Uploaded original +
+
+ Original +
+
+ ) : ( +
+ Original image unavailable +
+ )} + + {tryItem.source_post?.image_url ? ( +
+
+ Source post +
+
+ Source post +
+
+ ) : ( +
+ Source post unavailable +
+ )} +
+ + +
+
+ ); +} +``` + +- [ ] **Step 5: Verify modal tests** + +Run: + +```bash +cd packages/web +bun run test:unit lib/components/profile/__tests__/TryDetailModal.test.tsx +``` + +Expected: all modal tests pass. + +## Task 6: Wire Modal into TriesGrid + +**Files:** +- Modify: `packages/web/lib/components/profile/TriesGrid.tsx` +- Create: `packages/web/lib/components/profile/__tests__/TriesGrid.test.tsx` + +- [ ] **Step 1: Add failing grid interaction test** + +Create `TriesGrid.test.tsx`. + +```tsx +/** + * @vitest-environment jsdom + */ +import React from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { describe, expect, it, vi } from "vitest"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { TriesGrid } from "../TriesGrid"; + +vi.mock("next/image", () => ({ + default: (props: React.ImgHTMLAttributes) => ( + + ), +})); + +vi.mock("@/lib/api/generated/users/users", () => ({ + getMyTries: vi.fn(async () => ({ + data: [ + { + id: "try-1", + image_url: "https://example.com/result.png", + created_at: "2026-05-07T00:00:00Z", + source_post_id: null, + selected_item_ids: [], + person_original_image: null, + source_post: null, + selected_items: [], + }, + ], + pagination: { + current_page: 1, + per_page: 20, + total: 1, + total_pages: 1, + }, + })), +})); + +function renderGrid() { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return render( + + + + ); +} + +describe("TriesGrid", () => { + it("opens the detail modal when a try card is clicked", async () => { + renderGrid(); + + await waitFor(() => + expect(screen.getByAltText("Try-on result")).toBeInTheDocument() + ); + fireEvent.click(screen.getByRole("button", { name: /open try-on/i })); + + expect(screen.getByRole("dialog")).toBeInTheDocument(); + expect(screen.getByText("Try-on details")).toBeInTheDocument(); + }); +}); +``` + +- [ ] **Step 2: Run failing grid test** + +Run: + +```bash +cd packages/web +bun run test:unit lib/components/profile/__tests__/TriesGrid.test.tsx +``` + +Expected: the test fails because cards have no accessible name and no modal state. + +- [ ] **Step 3: Implement selected try state** + +In `TriesGrid.tsx`, import `useState` and `TryDetailModal`. + +```ts +import { useRef, useEffect, useState } from "react"; +import { TryDetailModal } from "./TryDetailModal"; +``` + +Inside `TriesGrid`, add: + +```ts +const [selectedTry, setSelectedTry] = useState(null); +``` + +Update the card button. + +```tsx + +
+ +
+ {loading ? ( +
+ +
+ ) : results.length === 0 ? ( +
+ No posts found +
+ ) : ( +
+ {results.map((post) => ( + + ))} +
+ )} +
+ +
+ {showManualInput ? ( +
{ + e.preventDefault(); + if (manualId.trim()) { + onSelect(manualId.trim()); + onClose(); + } + }} + className="flex gap-2" + > + setManualId(e.target.value)} + placeholder="Enter post ID or UUID" + className="flex-1 rounded-md border border-input bg-background px-3 py-1.5 text-sm outline-none" + /> + +
+ ) : ( + + )} +
+
+ + ); +} +``` + +**주의**: `search()` 함수의 실제 파라미터 형식과 응답 타입은 `packages/web/lib/api/generated/search/search.ts`와 `SearchParams`/`SearchResponse` 타입을 확인하세요. 위 코드의 `{ q, limit }` 파라미터와 `res.data` 응답 매핑은 실제 API 스펙에 맞게 조정이 필요할 수 있습니다. `createBrowserClient`는 `@/lib/supabase/client`에서 import합니다 — 실제 경로를 확인하세요. + +- [ ] **Step 2: 타입 체크** + +Run: `cd packages/web && npx tsc --noEmit --pretty 2>&1 | grep "PostPickerModal"` +Expected: 0 errors (import 전이므로 에러 없음) + +- [ ] **Step 3: 커밋** + +```bash +git add packages/web/app/admin/content-studio/PostPickerModal.tsx +git commit -m "feat(content-studio): add PostPickerModal component + +Meilisearch search with 300ms debounce + Supabase popular posts fallback +(view_count descending, top 20). Shows thumbnail, title, artist/group, +view/like counts. Manual ID input via fallback link at bottom." +``` + +--- + +## Task 7: page.tsx 통합 — PostPickerModal + Research 제거 + 썸네일 UI + +**Files:** + +- Modify: `packages/web/app/admin/content-studio/page.tsx` + +- [ ] **Step 1: page.tsx에서 research 상태 및 ResearchPanel 제거** + +`packages/web/app/admin/content-studio/page.tsx`에서: + +1. import에서 `ResearchPanel`, `ResearchRun` 제거 +2. import에 `PostPickerModal` 추가 +3. state에서 `researchRun`, `researchWarning`, `useResearchInCopy` 삭제 +4. `handleCreatePacket`에서 research state 초기화 제거 (`setResearchRun(null)`, `setResearchWarning(null)`, `setUseResearchInCopy(false)`) +5. `handleOpenPacket`에서 동일 research state 제거 +6. `handleGenerateVariants`에서 `researchContext`, `useResearchInCopy` API body 제거 +7. JSX에서 `` 전체 제거 +8. ``에서 `researchRun={researchRun}`, `useResearchInCopy={useResearchInCopy}` props 제거 +9. ``에서 동일 props 제거 + +- [ ] **Step 2: PostPickerModal 통합 + post ID 텍스트 입력 교체** + +```tsx +// 추가 state: +const [pickerOpen, setPickerOpen] = useState(false); +const [keywords, setKeywords] = useState([]); +const [imagePrompts, setImagePrompts] = useState<{ + youtube: string; + instagram_feed: string; + instagram_story: string; +} | null>(null); +const [thumbnails, setThumbnails] = useState>({}); +const [thumbnailLoading, setThumbnailLoading] = useState(false); + +// handleSelectPost: PostPickerModal에서 선택 시 자동 packet 생성 +async function handleSelectPost(selectedPostId: string) { + setPostId(selectedPostId); + setState("loading"); + setError(null); + setGovernance(null); + setGenerationWarning(null); + setVariants([]); + setKeywords([]); + setImagePrompts(null); + setThumbnails({}); + + try { + const data = await postJson<{ packet: ContentPacket }>( + "/api/v1/content/packets", + { postId: selectedPostId }, + ); + setPacket(data.packet); + await loadRecentPackets(); + setState("idle"); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to create packet"); + setState("error"); + } +} +``` + +기존 form을 교체: + +```tsx +
+
+
+ + Post + + +
+
+ + +
+
+ + {error && ( +
+ {error} +
+ )} + {generationWarning && ( +
+ {generationWarning} +
+ )} +
+ + setPickerOpen(false)} + onSelect={handleSelectPost} +/> +``` + +- [ ] **Step 3: 썸네일 생성 UI 추가** + +variants 아래, GovernancePanel 위에 썸네일 섹션 추가: + +```tsx +{ + imagePrompts && ( +
+

+ Thumbnail Generation +

+
+ {(["youtube", "instagram_feed", "instagram_story"] as const).map( + (channel) => ( +