상품·주문 공유 상위 컨텍스트 FE 설계 (앱 / React Native)
플랫폼 판정: 이 기능의 두 화면(통합 상품 검색·내 주문 통합 조회)은 최종 소비자(구매자/신청자) 대상이다. 레포 실측 결과
web/은 운영자·관리자 포털(BFF,app/portal/*·app/admin/*, 랜딩이 “운영자 포털(데모)“)이라 소비자 검색·주문내역 서피스가 없다. 소비자 앱은mobile/(Expo/React Native)이며 이미 product·event·facility·recruitment·limited-drop·order 화면이 존재한다. 따라서 이 기능은 앱 전용이고design-fe-web은 작성하지 않는다(근거는 “확인 필요” 및 상위 리포트의 PRD Non-Goals 상충 항목 참조).
Background
근거 PRD: 도메인 경계 재설계/20260708-상품주문-공유상위컨텍스트-prd.md (verdict PASS, v2).
근거 BE TDD: 도메인 경계 재설계/20260708-상품주문-공유상위컨텍스트-tdd.md — API 계약(GET /api/catalog·GET /api/orders)을 “인터페이스 시그니처”에서 확정했다. 이 문서는 그 계약을 입력으로 소비한다.
BE는 catalog(판매 대상 5종 통합 검색)·order(주문/신청 4종 통합 조회) 두 읽기 파사드 API를 신설한다. 사용자 관점의 목표는 “요가 매트도 사고 싶고 요가 클래스도 찾고 싶은” 사용자가 5개 화면 대신 한 검색 화면을 쓰고, 흩어진 결제/신청 이력을 한 주문내역 화면에서 보는 것이다. 이 문서는 그 두 화면을 앱에 설계·티켓화한다.
Overview
- 무엇을: 앱에 화면 2종을 추가한다 — ① 통합 상품 검색(
/catalog) ② 내 주문 통합 조회(/orders). 둘 다 BE 읽기 파사드 API를 TanStack Query로 소비하는 읽기 전용 조회 화면이다. - 왜: 판매 대상·주문이 5/4개 도메인에 분산돼 사용자가 통합 진입점 없이 개별 화면을 순회해야 한다(PRD Problem Definition).
- 어떻게: 레포에 이미 정착한 스택(Expo Router + TanStack Query + axios
be-client직접 호출 +useTheme()테마 토큰 +components/ui프리미티브 키트)을 그대로 따른다. 신규 라이브러리 도입 없음. 기존 화면((tabs)/search=시설 검색,app/order=goods 전용)은 불변으로 두고 신규 라우트로 추가한다(PRD: additive).
Terminology
| 용어 | 정의 |
|---|---|
| catalog 화면 | 판매 대상 5종(PRODUCT/LIMITED_DROP/TICKET/PROGRAM/RECRUITMENT) 통합 검색 화면 (app/catalog/index.tsx) |
| orders 화면 | 주문/신청 4종(BOOKING/TICKETING/GOODS/RECRUITMENT) 통합 조회 화면 (app/orders/index.tsx) |
| CatalogItem / OrderHistoryItem | BE가 정규화해 내려주는 통합 응답 항목 (FE는 그대로 렌더) |
| itemType / orderType | 항목의 원본 도메인 판별자. 배지·필터·상세 이동 분기의 기준 |
| sellerType | PRODUCT 항목의 판매자 유형(B2C 개인/중고, B2B 브랜드). PRODUCT에만 값, 그 외 null |
| failedDomains | 부분 실패 시 조회에 실패한 도메인 목록. FE는 안내 배너로 노출 |
| detailPath | 원본 도메인 상세 경로(예: /products/{id}). 항목 탭 시 이 경로로 이동 |
| 4상태 | loading / empty / error / success — 모든 데이터 화면이 처리해야 하는 상태(+ 부분 실패는 success의 하위 변형) |
Define Problem
AS-IS (레포 실측, HEAD 808d2101)
- 통합 검색 부재: 소비자가 판매 대상을 도메인별 화면에서만 찾는다 —
(tabs)/store.tsx(상품),app/event/*(티켓),(tabs)/search.tsx(시설 검색, 위치+날씨),app/recruitments/*(모집),app/limited-drop/*(한정판). 5종을 한 번에 검색하는 지점이 없다.(tabs)/search.tsx는 이름이 “검색”이지만 실제로는 시설+날씨 전용이라 이 통합 검색과 다르다(덮어쓰지 않는다). - 통합 주문내역 부재:
app/order/index.tsx는useMyGoodsOrdersQuery(GET /goods-orders/me)만 조회하는 goods 전용이다. 예약(Booking)·티켓(TicketOrder)·모임 신청(Application)은 각 화면에 흩어져 있어 “내 전체 구매/신청 이력”을 한 화면에서 볼 수 없다. - 정착된 FE 패턴(이 설계가 따를 것):
- 데이터 흐름:
api/*.ts(엔드포인트 함수,getBeClient()axios) →lib/use*.ts(TanStack Query 훅) →app/*화면. 컴포넌트는api/를 직접 호출하지 않는다(useProducts→getProducts예시,lib/useProducts.ts#useProducts). - 테마:
useTheme()→{ scheme, tokens }, 색은 전부 시맨틱 토큰(theme/tokens.ts가 SSOT, Toss 팔레트 이미 반영 — accent#3182F6). 신규 화면은 하드코딩 색 금지. - UI 키트:
components/ui에LoadingView(spinner/skeleton)·ErrorView(message+onRetry)·EmptyState(message+description)·SegmentedControl·Badge(숫자 전용)·Card·ListItem·ThemedText(variant primary/secondary/muted/accent/danger) 존재. 4상태 처리를 이 키트로 한다(app/recruitments/index.tsx가 표준 예시). - 라우트 상수:
lib/navigation.tsROUTES— 화면은 경로 문자열을 하드코딩하지 않는다. - 서버 상태만 Query, 전역 클라이언트 상태는 auth·theme(Zustand)만. 지역 상태는
useState.
- 데이터 흐름:
TO-BE
app/catalog/index.tsx(통합 검색) +app/orders/index.tsx(통합 주문내역) 신규. 둘 다 BE 파사드 API를 Query 훅으로 소비.- 재사용 프리미티브(
LoadingView/ErrorView/EmptyState/SegmentedControl)를 그대로 쓰고, 도메인 표현 컴포넌트(CatalogItemCard·OrderHistoryItemCard·SellerTypeBadge·PartialFailureBanner)만 신규. - 진입점: 마이 탭에 “내 주문 내역”(통합), 홈에 “통합 검색” 진입 추가(기존 goods 전용
/order·시설search탭은 유지).
Architecture Benchmarking (토스 벤치마킹)
디자인 입력이 없으므로 토스(Toss) 패턴을 기준으로 화면을 제안한다. 화면별 참고 패턴은 아래 와이어프레임에 1줄씩 명시한다.
| 사례 | 패턴 | 이 설계에 반영 | 미반영 |
|---|---|---|---|
| 토스 통합검색 | 단일 검색 필드 상단 고정 + 그 아래 세그먼트 카테고리 탭(전체/…)으로 결과 유형 필터. 결과는 절제된 카드 리스트, accent는 최소 | catalog 화면: 상단 검색 입력 1개 + itemType SegmentedControl + sellerType 보조 세그먼트. 결과는 CatalogItemCard 리스트 | 검색어 자동완성·추천어(랭킹 고도화는 PRD Non-Goal) |
| 토스 결제/주문 내역 | 시간 역순 리스트, 각 행에 상태 배지 + 절제된 메타(시각·금액), 한 화면 한 과업. 필터는 상단 세그먼트로 조용히 | orders 화면: createdAt desc 리스트, OrderHistoryItemCard에 orderType 배지 + status + 결제 연계. orderType/status 세그먼트 필터 | 명세·영수증 상세(원본 도메인 상세로 이관) |
| 토스 디자인 시스템(TDS) — 단일 CTA·절제된 색·충분한 여백 (TDS) | 화면당 accent 1곳, 위계 단순화, 여백으로 그룹핑 | accent는 활성 세그먼트·재시도 버튼에만. 배지는 채도 낮은 tint | — |
| 토스 오류/부분 상태 처리 | 실패를 화면 전체 에러로 막지 않고 상단 안내 배너 + 나머지 정상 노출 | PartialFailureBanner(failedDomains 있을 때만) — 조회된 결과는 그대로 보여주고 실패 도메인만 안내 | — |
Detail Design
화면 목록
| 화면 | 라우트 | 인증 | 소비 API | 참고 토스 패턴 |
|---|---|---|---|---|
| 통합 상품 검색 | /catalog (신규 스택 라우트) | 불필요(permitAll) | GET /api/catalog | 토스 통합검색(검색 필드 + 세그먼트 필터) |
| 내 주문 통합 조회 | /orders (신규 스택 라우트) | 필요(authenticated) | GET /api/orders | 토스 결제/주문 내역(시간 역순 + 상태 배지) |
라우트 명명: 기존
app/order/(단수, goods 전용)와 충돌을 피하려 통합 화면은app/orders/(복수, BE/api/orders와 일치). 통합 검색은 기존(tabs)/search(시설)와 구분해app/catalog/.
텍스트 와이어프레임 — ① 통합 상품 검색 (/catalog)
토스 통합검색 패턴: 상단 단일 검색 필드 → 세그먼트 카테고리 탭 → 절제된 결과 카드 리스트. accent는 활성 세그먼트에만.
┌─────────────────────────────────────┐
│ ← 통합 검색 │ ← 헤더(뒤로가기 + 타이틀)
├─────────────────────────────────────┤
│ 🔍 [ 요가 ✕ ] │ ← 검색 입력(디바운스 300ms, clear 버튼)
├─────────────────────────────────────┤
│ [전체][상품][한정판][티켓][클래스][모집]│ ← itemType SegmentedControl(가로 스크롤)
│ 판매자: [전체] [중고] [브랜드] │ ← sellerType 보조 세그먼트(itemType=상품일 때만 노출)
├─────────────────────────────────────┤
│ ⚠ 티켓 정보는 잠시 후 다시 시도해 주세요 │ ← PartialFailureBanner(failedDomains 있을 때만)
├─────────────────────────────────────┤
│ ┌───────────────────────────────┐ │
│ │ [상품·중고] 요가매트 프리미엄 │ │ ← CatalogItemCard: itemType 배지 + sellerType 배지
│ │ 32,000원 2일 전 │ │ 제목 / 가격(KRW) / createdAt
│ ├───────────────────────────────┤ │
│ │ [클래스] 아침 요가 클래스 │ │
│ │ 15,000원 1일 전 │ │
│ ├───────────────────────────────┤ │
│ │ [티켓] 요가 페스티벌 │ │ ← price=null → "가격 상세 확인"
│ │ 가격 상세 확인 방금 │ │
│ └───────────────────────────────┘ │
│ (탭 → 원본 detailPath로 이동) │
└─────────────────────────────────────┘
텍스트 와이어프레임 — ② 내 주문 통합 조회 (/orders)
토스 결제/주문 내역 패턴: 시간 역순 단일 리스트, 각 행 상태 배지 + 절제된 메타. 필터는 상단 세그먼트로 조용히.
┌─────────────────────────────────────┐
│ ← 내 주문 내역 │
├─────────────────────────────────────┤
│ 유형: [전체][예약][티켓][상품][모집] │ ← orderType SegmentedControl(가로 스크롤)
│ 상태: [전체][결제완료][대기][취소] │ ← status 보조 세그먼트(옵션)
├─────────────────────────────────────┤
│ ⚠ 일부 주문을 불러오지 못했어요 │ ← PartialFailureBanner(failedDomains 있을 때만)
├─────────────────────────────────────┤
│ ┌───────────────────────────────┐ │
│ │ [예약] 결제완료 ● │ │ ← OrderHistoryItemCard: orderType 배지 + status
│ │ 강남 풋살장 예약 │ │ (주 표시명 = item.title, BE 계약에 title 확정)
│ │ 결제 #4821 3일 전 │ │ ← paymentId 있으면 "결제 #id", 없으면 "미결제"
│ ├───────────────────────────────┤ │
│ │ [상품] 배송중 ● │ │
│ │ 요가매트 프리미엄 외 1건 │ │ (title 누락 시에만 "유형명 #sourceId" fallback)
│ │ 결제 #4790 5일 전 │ │
│ └───────────────────────────────┘ │
│ (탭 → 원본 detailPath로 이동) │
└─────────────────────────────────────┘
화면별 상태 표 (loading / empty / error / success + 부분 실패)
① 통합 상품 검색
| 상태 | 트리거 | UI |
|---|---|---|
| loading | isLoading(첫 조회·검색어 변경) | LoadingView variant="skeleton"(카드 3개 스켈레톤). 검색 입력·세그먼트는 계속 조작 가능 |
| empty | success && items.length===0 | EmptyState message="검색 결과가 없어요" description="다른 검색어나 필터를 시도해 보세요" |
| error | isError(전체 요청 실패·네트워크) | ErrorView message="검색 결과를 불러오지 못했어요" onRetry=refetch |
| success | items.length>0 | CatalogItemCard 리스트(createdAt desc, BE 정렬 그대로) |
| 부분 실패 | success && failedDomains.length>0 | 리스트 상단 PartialFailureBanner(실패 도메인 라벨) + 조회된 항목 정상 노출. 전체 에러로 승격하지 않음 |
② 내 주문 통합 조회
| 상태 | 트리거 | UI |
|---|---|---|
| loading | isLoading | LoadingView variant="skeleton" |
| empty | success && items.length===0 | EmptyState message="주문 내역이 없어요" description="예약·티켓·상품·모임 신청 내역이 여기에 모여요" |
| error(미인증 401) | isError && status 401 | ErrorView message="로그인이 필요해요" onRetry=()→router.replace('/(auth)/login')(재시도 라벨 대신 “로그인하기”). 401은 be-client 인터셉터가 refresh 시도 후에도 실패하면 로그인 이동 — 화면은 방어적으로 로그인 유도 표시 |
| error(그 외) | isError | ErrorView message="주문 내역을 불러오지 못했어요" onRetry=refetch |
| success | items.length>0 | OrderHistoryItemCard 리스트(createdAt desc) |
| 부분 실패 | success && failedDomains.length>0 | 상단 PartialFailureBanner + 조회된 항목 정상 노출 |
테마 토큰 정의 표 (시맨틱 토큰 → 라이트/다크, theme/tokens.ts SSOT)
신규 토큰 추가 없음. 두 화면·신규 컴포넌트는 기존 토큰만 사용한다(최소주의). 사용 토큰과 용도:
| 시맨틱 토큰 | 라이트 | 다크 | 이 기능에서의 용도 |
|---|---|---|---|
background | #FFFFFF | #17171C | 화면 배경 |
surface | #F9FAFB | #202027 | 세그먼트 트랙·스켈레톤·B2C 배지 배경 |
surfaceElevated | #FFFFFF | #26262E | 카드 배경·검색 입력·활성 세그먼트·배너 배경 |
textPrimary | #191F28 | #F2F4F6 | 항목 제목 |
textSecondary | #4E5968 | #B0B8C1 | 메타(가격·시각·B2C 배지 텍스트·배너 문구) |
textTertiary | #8B95A1 | #6B7684 | placeholder·비활성 세그먼트 |
border | #E5E8EB | #2E2E36 | 카드 구분선·입력 테두리·배너 테두리 |
accent | #3182F6 | #4E93FB | 활성 세그먼트·B2B(브랜드) 배지·재시도 버튼(화면당 1곳 원칙 준수) |
accentText | #FFFFFF | #FFFFFF | accent 위 텍스트(B2B 배지 글자) |
warning | #FF9500 | #FF9F0A | PartialFailureBanner 좌측 강조 바·아이콘(부분 실패 신호) |
danger | #F04452 | #F76A78 | ErrorView 메시지(키트 내부) |
success | #12B886 | #2AC29B | 주문 status “결제완료” 상태 점(dot) |
itemType 배지 색: 유형별 색을 새로 만들지 않는다 — 배지는
surface(배경)+textSecondary(글자) 중립 tint로 통일하고 라벨 텍스트로 유형을 구분한다(토스식 절제). 유일한 색 강조는 sellerType의 B2B(accent) 배지 하나뿐이라 “화면당 accent 1곳”과 정합.
컴포넌트 트리 (컨테이너/프레젠테이션 분리)
app/catalog/index.tsx [컨테이너] CatalogScreen
├─ useCatalogSearch() (훅: 서버 상태) ← lib/useCatalogSearch.ts
├─ useState(keyword, itemType, sellerType) (지역 상태)
├─ CatalogSearchControls [프레젠테이션] 검색 입력 + itemType/sellerType SegmentedControl
├─ PartialFailureBanner [프레젠테이션] failedDomains 라벨
├─ LoadingView / EmptyState / ErrorView (기존 키트)
└─ FlatList<CatalogItem>
└─ CatalogItemCard [프레젠테이션]
└─ SellerTypeBadge [프레젠테이션] (PRODUCT 항목만)
app/orders/index.tsx [컨테이너] OrderHistoryScreen
├─ useOrderHistory() (훅: 서버 상태) ← lib/useOrderHistory.ts
├─ useState(orderType, status) (지역 상태)
├─ SegmentedControl × 2 (기존 키트, orderType/status 필터 — inline)
├─ PartialFailureBanner [프레젠테이션]
├─ LoadingView / EmptyState / ErrorView (기존 키트)
└─ FlatList<OrderHistoryItem>
└─ OrderHistoryItemCard [프레젠테이션]
재사용 컴포넌트 식별
| 컴포넌트 | 신규/기존 | 프레젠테이션? | 재사용 범위 |
|---|---|---|---|
LoadingView·ErrorView·EmptyState·SegmentedControl·ThemedText·Card | 기존(components/ui) | ✓ | 그대로 사용 |
PartialFailureBanner | 신규(components/common) | ✓ | catalog·orders 양쪽 공유(제네릭: labels: string[]) |
CatalogSearchControls | 신규(components/catalog) | ✓ | catalog 전용 |
CatalogItemCard | 신규(components/catalog) | ✓ | catalog 전용 |
SellerTypeBadge | 신규(components/catalog) | ✓ | CatalogItemCard 내부(PRODUCT 항목) |
OrderHistoryItemCard | 신규(components/order) | ✓ | orders 전용 |
- 컨테이너(화면)는 훅 호출·지역 상태·상태 분기만. 데이터 가공(가격 포맷·상대 시각·detailPath 이동)은 프레젠테이션 컴포넌트 또는
lib/*-format.ts유틸로(컴포넌트 내 비즈니스 로직 금지,no-logic-in-component). - 상대 시각(“2일 전”) 포맷은 기존
lib포맷 유틸 관례를 따라 순수 함수로 분리(기존lib/*-format.ts패턴).
상태관리 설계 (서버/클라이언트 구분, 채택 근거)
| 상태 | 종류 | 저장소 | 근거 |
|---|---|---|---|
| catalog 검색 결과 | 서버 | TanStack Query (useCatalogSearch) | 서버 데이터는 Query 캐시가 SSOT. 스토어 복사 금지(no-global-by-default). queryKey에 keyword·itemType·sellerType·page 포함 → 파라미터별 캐시 |
| order 이력 | 서버 | TanStack Query (useOrderHistory) | 동일. queryKey에 orderType·status·page 포함 |
| keyword 입력값 | 지역 | useState + 300ms 디바운스 | 화면 밖으로 공유 안 됨 → 전역 승격 근거 없음 |
| itemType·sellerType·status 필터 | 지역 | useState | 동일 |
| 테마 | 전역 | 기존 Zustand themeStore(변경 없음) | 이미 전역 — 재사용만 |
| 인증(accessToken) | 전역 | 기존 Zustand authStore(변경 없음) | 이미 전역 — /orders가 소비만 |
- 낙관적 업데이트·재시도 커스텀 없음: 두 화면은 읽기 전용. 재요청은 Query의
refetch/staleTime(기존 30분) 기본값을 쓴다. 낙관적 업데이트는 쓰기가 없어 불필요(미채택). - 전역 스토어 신설 없음: 검색·필터 상태는 지역. “일단 전역”을 반려하고 지역 상태로 유지.
API 연동 표 (화면 → 엔드포인트 → 훅 → 에러 처리)
| 화면 | 엔드포인트(BE 계약) | api 함수 | Query 훅 | 파라미터 | 에러 처리 |
|---|---|---|---|---|---|
| catalog | GET /api/catalog?keyword=&itemType=&sellerType=&page=&size= → CatalogSearchResponse | api/catalog.ts#getCatalog | lib/useCatalogSearch.ts#useCatalogSearch | keyword?·itemType?·sellerType?·page(0)·size(20) | 전체 실패 → ErrorView+refetch. failedDomains(부분 실패) → PartialFailureBanner, 결과는 정상 노출 |
| orders | GET /api/orders?orderType=&status=&page=&size= (authenticated) → OrderHistoryResponse | api/orderHistory.ts#getOrderHistory | lib/useOrderHistory.ts#useOrderHistory | orderType?·status?·page(0)·size(20) | 401 → 로그인 유도(be-client 인터셉터가 refresh 선처리). 그 외 → ErrorView+refetch. failedDomains → 배너 |
응답 타입(FE, BE 계약과 필드·타입 일치 — api/catalog-types.ts·api/order-history-types.ts)
// api/catalog-types.ts
export type CatalogItemType = 'PRODUCT' | 'LIMITED_DROP' | 'TICKET' | 'PROGRAM' | 'RECRUITMENT';
export type SellerType = 'B2C' | 'B2B';
export interface CatalogItem {
itemType: CatalogItemType;
sourceId: number;
title: string;
price: number | null; // KRW. TICKET 등 대표가 없으면 null → "가격 상세 확인"
sellerType: SellerType | null; // PRODUCT만 값
status: string; // 원본 status enum name
detailPath: string; // 예: "/products/123"
createdAt: string; // ISO-8601
}
export interface CatalogSearchResponse {
items: CatalogItem[];
page: number;
size: number;
failedDomains: CatalogItemType[];
}
// api/order-history-types.ts
export type OrderType = 'BOOKING' | 'TICKETING' | 'GOODS' | 'RECRUITMENT';
export interface OrderHistoryItem {
orderType: OrderType;
sourceId: number;
title: string; // 항목 표시명 (senior-pm 승격 → BE 계약에 추가 확정)
status: string;
paymentId: number | null;
detailPath: string;
createdAt: string;
}
export interface OrderHistoryResponse {
items: OrderHistoryItem[];
page: number;
size: number;
failedDomains: OrderType[];
}BE
price는BigDecimal(KRW). FE는 JSON 수치(number)로 받되, 정밀도 이슈가 없는 원화 정수 판매가라number매핑이 안전하다. TICKET은null→ “가격 상세 확인” 렌더(BE TDD 가격 정규화 확정 반영).detailPath는 BE가 원본 상세 경로 문자열을 준다. 앱 라우트와 정확히 일치하지 않을 수 있어(웹 경로 형태일 가능성), 항목 탭 시detailPath의{도메인}/{id}를 파싱해ROUTES로 매핑하는 얇은 어댑터(lib/catalog-navigation.ts)를 둔다 — 파싱 실패 시 이동 무시(안전). itemType↔ROUTES 매핑은 이 유틸이 소유(컴포넌트에 분기 로직 금지).
라우팅·내비게이션 흐름
flowchart LR Home["(tabs)/index 홈"] Me["(tabs)/me 마이"] Catalog["/catalog 통합검색"] Orders["/orders 통합주문내역"] Login["/(auth)/login"] Product["/product/:id"] Event["/event/:id"] Facility["/facility/:id"] Recruitment["/recruitments/:id"] LimitedDrop["/limited-drop/:id"] Home -->|통합 검색 진입| Catalog Me -->|내 주문 내역 진입| Orders Orders -->|미인증 401| Login Catalog -->|항목 탭 detailPath 매핑| Product Catalog --> Event Catalog --> Facility Catalog --> Recruitment Catalog --> LimitedDrop Orders -->|항목 탭 detailPath 매핑| Product
lib/navigation.tsROUTES에catalog: '/catalog',orders: '/orders'추가(와이어업 티켓).- 진입점: 홈에 “통합 검색” 진입, 마이 탭에 “내 주문 내역”(통합) 진입. 기존 시설
search탭·goods/order는 유지(중복 아님 — 범위/도메인 다름).
Testing Plan (implementer TDD 입력 — 사용자 관점 동작 검증, @testing-library/react-native + Jest)
테스트명에 티켓ID 금지(동작 설명 이름만). 각 대상 해피·실패·엣지 최소 3개. 서버 상태는 axios-mock-adapter 또는 훅 모킹으로 주입.
| 대상 | 유형 | 테스트 케이스 |
|---|---|---|
useCatalogSearch | 훅(renderHook) | keyword·itemType·sellerType이 요청 파라미터로 전달된다 / 파라미터가 바뀌면 새 queryKey로 재조회한다 / 응답 failedDomains를 그대로 반환한다 |
useOrderHistory | 훅 | orderType·status가 요청 파라미터로 전달된다 / 인증 헤더 없이 401이면 에러 상태가 된다 / 빈 결과를 정상 반환한다 |
CatalogItemCard | 컴포넌트 | 제목·가격(KRW 포맷)을 렌더한다 / price=null이면 “가격 상세 확인”을 렌더한다 / PRODUCT면 sellerType 배지를, 그 외 유형이면 배지를 렌더하지 않는다 / 탭하면 detailPath 매핑 경로로 이동한다 |
SellerTypeBadge | 컴포넌트 | B2B면 “브랜드” 라벨을 accent 배지로 렌더한다 / B2C면 “중고” 라벨을 중립 배지로 렌더한다 / null이면 아무것도 렌더하지 않는다 |
OrderHistoryItemCard | 컴포넌트 | orderType 배지와 status를 렌더한다 / paymentId가 있으면 “결제 id”를, 없으면 “미결제”를 렌더한다 / 탭하면 detailPath 경로로 이동한다 |
PartialFailureBanner | 컴포넌트 | labels가 있으면 안내 문구와 실패 유형을 렌더한다 / labels가 빈 배열이면 아무것도 렌더하지 않는다 / 문구가 alert role로 노출된다 |
CatalogSearchControls | 컴포넌트 | 검색 입력 타이핑이 디바운스 후 onChange를 호출한다 / itemType 세그먼트 선택이 콜백을 호출한다 / itemType이 상품일 때만 sellerType 세그먼트가 보인다 |
CatalogScreen | 컴포넌트(통합) | 로딩 시 스켈레톤을 보여준다 / 결과 0건이면 empty 문구를 보여준다 / 요청 실패면 에러+재시도를 보여준다 / 성공 시 카드 리스트를 보여준다 / failedDomains가 있으면 배너와 결과를 함께 보여준다 |
OrderHistoryScreen | 컴포넌트(통합) | 성공 시 시간 역순 리스트를 보여준다 / 401이면 로그인 유도를 보여준다 / empty·부분 실패 상태를 처리한다 / orderType 필터 변경 시 재조회한다 |
| 라이트/다크 | 컴포넌트 | 각 신규 컴포넌트가 두 모드에서 하드코딩 색 없이 토큰으로 렌더된다(useTheme 모킹으로 scheme 전환 렌더 스냅 아닌 동작 확인) |
Release Scenario (기능 플래그·점진 공개)
- 엔드포인트 미노출 = 사실상 OFF: 두 화면은 신규 추가라 기존 트래픽 영향 0. BE 파사드 API(
/api/catalog·/api/orders)가 배포되기 전에는 진입점을 숨긴다. - 기능 플래그로 점진 공개: 레포에 이미
lib/feature-flags.ts(isFeatureEnabled, 탭 게이팅 선례 있음)가 있다. 진입점(홈 “통합 검색”·마이 “내 주문 내역”)을catalog.enabled·orders.unified.enabled플래그로 게이팅한다 — BE 준비 전엔 진입점 숨김, 준비 후 ON. 화면 라우트 자체는 배포하되 진입점만 토글(무중단, 재기동 불요). - 롤백: 플래그 OFF로 진입점 즉시 숨김. 라우트 파일은 남아도 진입 불가라 안전.
- 의존: BE M1(catalog API)·M2(order API) 배포 완료가 각 화면 ON의 선행 조건. FE 구현은 BE 계약(이 문서 소비)만으로 병렬 진행 가능(모킹으로 TDD).
Open Questions
detailPath형식이 앱 라우트와 어긋날 가능성 — BE가 웹 경로(/products/{id}) 문자열을 주므로, 앱은 itemType/orderType + sourceId로ROUTES매핑하는 어댑터를 두는 것이 더 견고할 수 있다. BE 실응답 확인 후lib/catalog-navigation.ts매핑 방식을 확정한다(1차: itemType 기준 매핑).- orders 화면의 항목 “표시명” — 확정: BE 계약에
title추가됨(senior-pm 검증으로 역제안 승격).OrderHistoryItemCard는item.title을 주 표시명으로 렌더하고, “유형명 + sourceId”는 title 누락 시의 최후 fallback으로만 남긴다. - 깊은 페이지네이션·무한 스크롤 — 1차는 첫 페이지(size=20)만. 무한 스크롤은 BE의 글로벌 페이지네이션 안정화(BE Open Question) 후 후속.
- 라이트/다크 외 시스템 폰트 스케일(접근성 큰 글씨) 대응은 기존 화면과 동일 수준으로 두고 별도 범위 밖.
Document History
| 날짜 | 변경 내용 |
|---|---|
| 2026-07-08 | 최초 작성 — PRD v2 + BE TDD 계약 기반. 플랫폼 판정(앱 전용, web은 운영자 포털이라 제외). 레포 정착 스택(Expo Router+TanStack Query+테마 토큰+UI 키트) 준수. 화면 2종·컴포넌트 4종 신규, 신규 토큰 0. 11개 티켓(wave 3/5/2/1)으로 분해 |