가상 대기열(Virtual Queue) — RN 대기실 화면 FE 설계 (design-fe-app)
Background
근거 문서:
- PRD:
/Users/biuea/Desktop/dpdpdndn/프로젝트/스포츠앱/가상 대기열 트래픽 제어/20260709-가상대기열-prd.md(FR-6·FR-10, User Scenarios 1~7) - BE TDD:
/Users/biuea/Desktop/dpdpdndn/프로젝트/스포츠앱/가상 대기열 트래픽 제어/20260709-가상대기열-tdd.md“FE/외부 계약 — API 명세” (엔드포인트 5종·Request/Response 시그니처)
RN 앱(Expo Router)에 대기실 화면을 신설한다. 한정판 구매·티케팅 좌석 선택 두 진입점 앞단에 대기실을 얹어, 순번·앞선 대기 인원·예상 대기시간·진행바를 표시하고 입장 허용 시 구매 화면으로 자동 전환한다. BE는 폴링(SSE 미채택)으로 상태를 확정하며, 폴링이 heartbeat(60초 미갱신 시 이탈)를 겸한다.
이 설계는 RN 대기실 화면만 다룬다. BE 큐 로직·토큰 게이트는 BE TDD·BE 티켓(BE-01~11) 소관이다.
Overview
- 무엇을: 파라미터화된 단일 대기실 화면(
/queue/[type]/[targetId]) + 두 진입점 와이어업 + 구매 API에X-Entry-Token헤더 부착. - 왜: 판매 시작 폭주 시 사용자에게 “당신 차례가 언제인지”를 보여주며 유입을 관리하고, 입장 시 매끄럽게 구매로 전환한다(FR-10).
- 어떻게: 기존 레포 관례(Expo Router 파일 라우팅 · TanStack Query 폴링 훅
refetchInterval·useTheme()시맨틱 토큰 · 빌드타임 피처 플래그lib/feature-flags.ts·getBeClient()axios) 위에 얹는다. 신규 패턴·신규 테마 토큰 0개.
Terminology
| 용어 | 정의 |
|---|---|
| 대기실(Waiting Room) | 순번·앞선 인원·ETA·진행바를 표시하고 입장 시 구매로 전환하는 RN 화면 |
| Target(대상) | 대기열 단위. type ∈ limited-drop | ticketing-event, targetId = dropId | eventId |
| 폴링 heartbeat | GET .../entries/me 2~3초 주기 호출. 상태 갱신 + 생존 신호 겸용(60초 미갱신 → 이탈) |
| 입장 토큰(Entry Token) | ADMITTED/DIRECT_ADMITTED 시 발급되는 HMAC 문자열. 구매 API X-Entry-Token 헤더로 검증 |
| DIRECT_ADMITTED | 플래그 OFF(대기열 우회) 시 BE가 대기 없이 즉시 토큰을 발급하는 상태 |
| FE 플래그 | 빌드타임 virtual-queue.enabled(EXPO_PUBLIC_VIRTUAL_QUEUE_ENABLED). 진입점이 대기실을 경유할지 결정 |
Define Problem
AS-IS (실제 코드 근거)
| 지점 | 파일#심볼 | 현재 동작 |
|---|---|---|
| 라우팅 | Expo Router app/** 파일 라우팅, 경로 상수 lib/navigation.ts#ROUTES | /limited-drop/[id], /limited-drop/[id]/purchase, /event/[id], /event/[id]/order 존재. 대기열 경로 0건 |
| 한정판 진입점 CTA | app/limited-drop/[id]/index.tsx:99 | 상세 CTA가 router.push('/limited-drop/${dropId}/purchase')로 직접 이동 |
| 한정판 구매 API | api/limitedDrops.ts#purchaseLimitedDrop + lib/usePurchaseLimitedDrop.ts | POST /limited-drops/{dropId}/orders. X-User-Id·Idempotency-Key 헤더 부착. X-Entry-Token 없음. 4xx는 판별 유니온 반환, 5xx만 throw |
| 티케팅 좌석 진입점 | app/event/[id]/index.tsx:95 | 좌석 선택 후 router.push('/event/${eventId}/order?seatIds=...') |
| 티케팅 좌석 선점 API | api/ticketOrders.ts#selectSeats | POST /events/{eventId}/seats/select body {seatIds}. X-Entry-Token 없음. order.tsx#handleConfirm가 호출 |
| 폴링 패턴 | lib/useLimitedDrop.ts#getRefetchIntervalMs | TanStack Query refetchInterval: (query) => number | false. POLL_INTERVAL_MS = 3000 관례 존재 |
| 테마 | theme/tokens.ts(light/dark), theme/useTheme.ts#useTheme → { scheme, tokens } | 토스풍 시맨틱 토큰(accent #3182F6) 정착. 하드코딩 색은 tokens.ts에만 |
| 진행바 프리미티브 | components/limitedDrop/RemainingStockBar.tsx | track(tokens.border) + fill(tokens.accent, width %) 패턴 — 진행바 참조 원형 |
| 상태 컴포넌트 | components/ui/{LoadingView,ErrorView,EmptyState}.tsx | 로딩/에러(재시도)/빈 상태 프리미티브 존재 |
| 피처 플래그 | lib/feature-flags.ts#isFeatureEnabled | 빌드타임 EXPO_PUBLIC_*_ENABLED env. 화면이 이 함수로 게이팅하는 선례(FE-15 계승) |
| 세션 상태 | lib/auth.ts#useAuthStore (Zustand) | accessToken을 메모리 스토어로 화면 경계 넘겨 소비하는 선례 |
| userId | api/goods.ts#useCurrentUserId → number | X-User-Id 헤더용. dev 1 반환 |
| API 클라이언트 | api/be-client.ts#getBeClient | axios 싱글톤. useAuthStore.getState()를 React 밖에서 참조하는 선례 |
핵심 공백: 대기실 화면·큐 API 클라이언트·입장 토큰 보관·진입점 우회 로직이 전무하다.
TO-BE
flowchart LR subgraph Entry["진입점 (기존 화면)"] DropCta[한정판 상세 CTA] SeatCta[좌석 선택 예매 CTA] end subgraph Waiting["대기실 (신규)"] Screen[queue type targetId 화면] VM[useWaitingRoom 뷰모델] end subgraph Data["데이터 계층 (신규)"] EnterHook[useEnterQueue] StatusHook[useQueueStatus 폴링] TokenStore[entryTokenStore] QueueApi[api virtualQueue] end subgraph Buy["구매 (기존 + 토큰 헤더)"] Purchase[한정판 구매 / 좌석 선점] end DropCta -->|플래그 ON| Screen SeatCta -->|플래그 ON| Screen Screen --> VM VM --> EnterHook VM --> StatusHook VM --> TokenStore EnterHook --> QueueApi StatusHook --> QueueApi VM -->|ADMITTED| Purchase TokenStore -->|X-Entry-Token| Purchase
Architecture Benchmarking (의무)
디자인 입력이 없어 토스(Toss) 대기·진행 화면 패턴을 벤치마킹한다. 각 화면 설계에 참조 패턴을 1줄 명시한다.
| 제품/사례 | 해결 방식 | 참고할 패턴 | 미참고 사유 |
|---|---|---|---|
| 토스 송금·인증 대기 화면 | 한 화면 한 과업. 큰 숫자 1개(핵심 지표)를 중앙 강조, 보조 정보는 하위 위계, 단일 진행 인디케이터, accent 1곳 | 순번을 대형 숫자로 중앙 강조 · 앞선 인원/ETA는 보조 텍스트 · 진행바 1개 · CTA/링크 절제 | 토스의 실시간 애니메이션(스켈레톤 shimmer)은 RN 성능·범위 밖. 정적 진행바로 단순화 |
| 토스 로딩·처리중 상태 | 처리 중에는 스피너 + ”○○ 처리 중” 한 줄, 실패 시 명확한 사유 + 단일 재시도 | 대기실 진입 로딩·폴링 실패·포화 거부를 각각 한 문장 + 단일 액션으로 | 토스 고유 일러스트/로티는 자산 없음 → 텍스트 + 토큰 색으로 대체 |
| 인터파크 NOL 예매 대기 (PRD 벤치마킹) | 순번·예상 시간 표시 후 차례 도달 시 예매로 자동 진행, 이탈 시 순번 소실 안내 | 자동 전환 · 이탈/재진입 안내 문구 | 광고·부가 배너는 범위 밖 |
Detail Design
화면 목록
| ID | 화면 | 라우트 | 역할 |
|---|---|---|---|
| S1 | 대기실 | /queue/[type]/[targetId] | 순번·앞선 인원·ETA·진행바 표시, 폴링 heartbeat, 입장 시 구매로 자동 전환 |
진입점 2개(한정판 상세 CTA, 좌석 선택 예매 CTA)는 기존 화면이며 라우팅 분기만 추가한다(신규 화면 아님). 구매 화면(한정판 purchase, 티케팅 order)도 기존 화면이며 X-Entry-Token 헤더만 추가한다.
S1 대기실 — 텍스트 와이어프레임 (토스 벤치마킹)
참조 토스 패턴: 한 화면 한 과업 + 대형 숫자 중앙 강조 + 진행바 1개 + accent 1곳.
┌───────────────────────────────┐
│ (닫기 X) [나가기] │ ← 헤더: 좌 닫기, 우 '나가기'(DELETE 이탈) 텍스트 링크(secondary)
│ │
│ 잠시만 기다려 주세요 │ ← textPrimary, 한 과업 안내
│ │
│ 1,240 │ ← 순번: 대형 숫자(48pt, textPrimary) — 화면 중앙 강조
│ 내 순번 │ ← textTertiary 캡션
│ │
│ ▓▓▓▓▓▓▓▓░░░░░░░░░░ 62% │ ← 진행바: track(border)+fill(accent), 오른쪽 % (textSecondary)
│ │
│ 앞선 대기 1,239명 │ ← textSecondary
│ 예상 대기 약 4분 │ ← textSecondary (etaSeconds → '약 N분')
│ │
│ 접속이 많아 순서대로 │ ← textTertiary 안내 (accent 남용 금지)
│ 입장하고 있어요 │
└───────────────────────────────┘
- CTA/accent 절제: 대기 중에는 accent를 진행바 fill 1곳에만 쓴다(토스 “화면당 accent 1곳”). ‘나가기’는 텍스트 링크(secondary)로 절제.
- 입장 순간 자동 전환: 별도 버튼 없이
status === ADMITTED|DIRECT_ADMITTED가 되면 토큰을 저장하고 구매 화면으로router.replace(뒤로가기로 대기실 복귀 방지). - 진행바 비율: 최초 WAITING 응답의
aheadCount를 기준값으로 잡아ratio = 1 - aheadCount/initialAhead(뷰모델 계산, 컴포넌트는 ratio만 렌더). 기준값이 없거나 0이면 비율 미표시(순번/ETA만).
화면별 상태 표 (loading / empty / error / success 전부)
S1 대기실. 데이터 없음(empty)의 자연 대응은 “큐에 없음(404 · 이탈/미진입)“이다.
| 상태 | 트리거 | UI (토스풍) | 액션 |
|---|---|---|---|
| loading | 진입 직후 enter POST 진행 중, 첫 status 응답 전 | LoadingView(spinner, accent) + “대기열에 들어가는 중” | 없음 |
| success — WAITING | status 200 WAITING | 순번·앞선 인원·ETA·진행바(위 와이어프레임) | 3초 폴링 자동 |
| success — ADMITTED/DIRECT | status/enter 200 ADMITTED|DIRECT_ADMITTED (entryToken 존재) | 짧은 “입장! 이동 중” 스피너 후 구매 화면 router.replace | 토큰 저장 → 자동 전환 |
| empty — 큐에 없음 | status 404 (이탈·미진입) | EmptyState “대기열에서 나왔어요” + 사유(“자리를 비운 사이 순번이 정리됐어요”) | 단일 CTA “다시 대기”(재-enter) 또는 “돌아가기”(back) |
| error — 포화 | enter 429 QUEUE_FULL | EmptyState(warning 톤) “지금 대기 인원이 많아요” + “잠시 후 다시 시도해 주세요” | 단일 CTA “다시 시도”(재-enter), 자동 폴링 없음 (FR-7) |
| error — 폴링/네트워크 실패 | status/enter 5xx·타임아웃 | ErrorView “연결이 불안정해요” + 재시도 | ”다시 시도”(재-enter/refetch). 3회 연속 실패 시 자동 폴링 중단하고 수동 재시도만 |
| error — 토큰 만료(구매 단계) | 구매 API 403 QUEUE_BYPASS_DENIED | (구매 화면에서) “대기 시간이 지났어요” + “다시 대기하기” | 대기실로 router.replace 재진입 (FR-4 시나리오 3) |
해피 패스만 있는 설계는 미완성 — 위 7개 상태 전부를 S1(및 구매 단계 403)에서 처리한다.
시나리오 → 화면 상태 매핑 (PRD User Scenarios 1~7)
| # | PRD 시나리오 | 화면 처리 |
|---|---|---|
| 1 | 정상 입장·자동 전환 | WAITING 폴링 → ADMITTED → 토큰 저장 → 구매 화면 router.replace |
| 2 | 이탈 후 재진입 새 순번 | 백그라운드/60초 미폴링 → 복귀 시 status 404(empty) → “다시 대기” CTA → 재-enter로 새 순번 |
| 3 | 토큰 만료 안내·재대기 | 구매 API 403(토큰 만료) → “다시 대기하기” → 대기실 재진입 |
| 4 | 중복 진입 멱등 | 재진입 시 enter가 BE 멱등으로 기존 순번 반환 → 동일 순번 표시(새 순번 미발급) |
| 5 | 대기열 포화 | enter 429 QUEUE_FULL → “잠시 후 다시 시도” 상태(자동 폴링 없음) |
| 6 | 토큰 없이 구매 차단 | 대기실 미경유 직접 구매는 BE 403 → FE는 진입점이 플래그 ON 시 항상 대기실 경유(우회 불가) |
| 7 | 플래그 OFF 대기실 우회 | FE 플래그 OFF → 진입점이 대기실 없이 기존 구매로 직접 이동. (FE ON·BE OFF면 enter가 DIRECT_ADMITTED 즉시 반환 → 대기 없이 전환) |
테마 토큰 정의 표 (시맨틱 토큰 → 라이트/다크 매핑)
신규 토큰 0개. 대기실은 theme/tokens.ts의 기존 시맨틱 토큰만 재사용한다(라이트/다크 두 모드 자동 지원, theme/tokens.ts는 수정하지 않음 → Single Writer 충돌 회피). 아래는 대기실이 소비하는 토큰의 라이트/다크 값(SSOT = theme/tokens.ts).
| 시맨틱 토큰 | 용도(대기실) | Light | Dark |
|---|---|---|---|
background | 화면 배경 | #FFFFFF | #17171C |
surface | 카드/배지 배경(포화·empty 박스) | #F9FAFB | #202027 |
textPrimary | 순번 대형 숫자·제목 | #191F28 | #F2F4F6 |
textSecondary | 앞선 인원·ETA·% | #4E5968 | #B0B8C1 |
textTertiary | 캡션·보조 안내 | #8B95A1 | #6B7684 |
border | 진행바 track·구분선 | #E5E8EB | #2E2E36 |
accent | 진행바 fill·로딩 스피너 (화면당 1곳) | #3182F6 | #4E93FB |
accentText | (미사용 예정, CTA 버튼 재사용 시) | #FFFFFF | #FFFFFF |
warning | 포화(429) 경고 톤 | #FF9500 | #FF9F0A |
danger | 폴링/네트워크 오류 | #F04452 | #F76A78 |
disabled | 비활성 상태 | #C7C7CC | #48484A |
- 색은 전부
useTheme().tokens경유(하드코딩 색 0). 컴포넌트는 두 모드에서 각각 확인해야 완료(no-single-mode).
컴포넌트 트리 (컨테이너/프레젠테이션 분리)
app/queue/[type]/[targetId].tsx (컨테이너 — 라우트 파라미터 파싱, 뷰모델 호출, 상태별 분기 렌더만)
└─ useWaitingRoom(target) (뷰모델 훅 — 로직 전부: enter+poll 오케스트레이션, phase 결정, ratio 계산, 토큰 저장, 전환 사이드이펙트)
├─ useEnterQueue(target) (mutation 훅 — POST enter)
├─ useQueueStatus(target, enabled)(query 훅 — GET me 3초 폴링 heartbeat)
└─ useEntryTokenStore (Zustand — 토큰 저장/조회)
└─ 프레젠테이션 (순수, props만)
├─ QueuePositionCard {position} 순번 대형 숫자 + 캡션
├─ QueueProgressBar {ratio, percentLabel} 진행바 (RemainingStockBar 패턴 재사용)
├─ QueueWaitInfo {aheadCount, etaLabel} 앞선 인원·ETA 보조 텍스트
└─ (상태) LoadingView / ErrorView / EmptyState 기존 ui 프리미티브 재사용
- 재사용:
LoadingView·ErrorView·EmptyState·PrimaryButton·ThemedText·ThemedView는 기존 컴포넌트를 그대로 쓴다.QueueProgressBar는RemainingStockBar의 track/fill 패턴을 따른다. - no-logic-in-component: phase 결정·ratio 계산·전환 라우팅·토큰 저장은 전부
useWaitingRoom뷰모델에. 컴포넌트는 렌더만.
상태관리 설계 (서버/전역/지역 구분 + 채택 근거)
| 상태 | 종류 | 저장 | 근거 |
|---|---|---|---|
| 큐 상태(status/position/ahead/eta/entryToken) | 서버 상태 | TanStack Query 캐시(useQueueStatus) | 서버가 SSOT. 스토어 복사 금지(no-global-by-default). 폴링=heartbeat라 Query refetchInterval이 정확히 맞음 |
| 입장 토큰(구매 화면으로 전달) | 전역(세션·휘발) | Zustand entryTokenStore(target별) | 대기실 → 구매 API 호출 경계를 넘는 휘발성 자격증명. useAuthStore(accessToken) 선례와 동형. 서버 표시 캐시가 아니라 화면 경계 전달용이라 전역 승격 정당. 앱 재기동 시 소멸(디스크 저장 안 함) |
| 진행바 기준값(initialAhead) | 지역 | useWaitingRoom 내부 useRef | 세션 1회 한정 UI 파생값. 전역·서버 아님 |
| FE 플래그(virtual-queue.enabled) | 빌드타임 상수 | lib/feature-flags.ts | 기존 관례. 런타임 전환은 BE 플래그가 담당(FE는 진입점 경유 여부만) |
- 낙관적 업데이트·재시도: 대기실은 조회(폴링) 중심이라 낙관적 업데이트 없음. enter 429/5xx 재시도는 수동 CTA(자동 폭격 금지 — PRD 악순환 방지). 폴링 3회 연속 실패 시 자동 중단.
API 연동 표 (화면 → 엔드포인트 → 훅 → 에러 처리)
BE TDD “FE/외부 계약 — API 명세”와 필드·타입 1:1. 인증 X-User-Id 헤더(기존 관례). {type} ∈ limited-drop | ticketing-event.
| 화면/트리거 | 메서드·엔드포인트 | 훅/함수 | 요청 | 성공 | 실패 처리 |
|---|---|---|---|---|---|
| S1 진입 | POST /virtual-queues/{type}/{targetId}/entries | useEnterQueue → enterQueue() | X-User-Id, body 없음 | 200 QueueEntryResponse | 429 QUEUE_FULL→포화 상태, 5xx/네트워크→error |
| S1 폴링(heartbeat) | GET /virtual-queues/{type}/{targetId}/entries/me | useQueueStatus(refetchInterval 3s) → getQueueStatus() | X-User-Id | 200 QueueEntryResponse | 404→empty(재진입 유도), 5xx→error(3회 후 중단) |
| S1 이탈(나가기/언마운트) | DELETE /virtual-queues/{type}/{targetId}/entries/me | leaveQueue() (best-effort) | X-User-Id | 204 | 실패 무시(60초 heartbeat 미갱신으로 자동 방출) |
| 한정판 구매(기존) | POST /limited-drops/{dropId}/orders | usePurchaseLimitedDrop(헤더 추가) | 기존 + X-Entry-Token | 기존 | 403 QUEUE_BYPASS_DENIED→“다시 대기하기” |
| 티케팅 좌석(기존) | POST /events/{id}/seats/select | selectSeats(헤더 추가) | 기존 + X-Entry-Token | 기존 | 403 QUEUE_BYPASS_DENIED→“다시 대기하기” |
QueueEntryResponse 타입 (BE 계약 정합 — api/virtualQueue.ts에 정의):
type QueueEntryStatus = 'WAITING' | 'ADMITTED' | 'DIRECT_ADMITTED';
interface QueueEntryResponse {
status: QueueEntryStatus;
position: number | null; // 순번(1-based). ADMITTED/DIRECT는 null
aheadCount: number | null; // 앞선 대기 인원. WAITING만
etaSeconds: number | null; // 예상 대기 초. WAITING만
entryToken: string | null; // ADMITTED/DIRECT_ADMITTED만
tokenExpiresAt: string | null;// ISO-8601, entryToken 있을 때
}BE 계약이
position·aheadCount·etaSeconds를Long(정수)으로 명세 → FEnumber.entryToken/tokenExpiresAt은String?→string | null. 스키마 검증은 판별 유니온 매핑으로 좁힌다(no-loose-assertion). 429/404는 예외를 던지지 않고 판별 유니온(QueueEnterResult/QueueStatusResult)으로 반환 —api/limitedDrops.ts#mapPurchaseFailure선례와 동형. 5xx·네트워크만 throw.
라우팅·내비게이션 흐름 (Mermaid)
flowchart LR DropDetail[한정판 상세] -->|플래그 ON| Queue[대기실] DropDetail -->|플래그 OFF| Purchase[한정판 구매] SeatPick[좌석 선택] -->|플래그 ON| Queue SeatPick -->|플래그 OFF| Order[좌석 주문] Queue -->|ADMITTED limited-drop| Purchase Queue -->|ADMITTED ticketing| Order Queue -->|404 empty| Requeue[다시 대기] Requeue --> Queue Purchase -->|403 토큰만료| Queue Order -->|403 토큰만료| Queue
- 입장 후 전환은
router.replace(뒤로가기로 대기실 복귀 방지). 대기실 → 구매 시entryTokenStore에 토큰이 이미 저장돼 구매 API가 헤더로 읽는다. ROUTES(lib/navigation.ts)에queue.waiting(type, targetId)상수를 추가(FE-04) — 후행 티켓은 문자열 하드코딩 금지.
Testing Plan (implementer TDD 입력)
컴포넌트·훅별 테스트. 사용자 관점 동작 검증(보이는 텍스트·role·인터랙션), 구현 상세·snapshot 대용 금지. Testing Library(@testing-library/react-native) + Jest.
| 대상 | 레벨 | 케이스(해피·실패·엣지 최소 3) |
|---|---|---|
api/virtualQueue.ts | 단위(axios-mock-adapter) | 200 매핑, 429→QUEUE_FULL 판별, 404→NOT_IN_QUEUE 판별, 5xx→throw |
entryTokenStore | 단위 | target별 저장/조회, 다른 target 격리, clear |
useEnterQueue | 훅(renderHook) | 성공 200, 429 포화 결과, 5xx error |
useQueueStatus | 훅 | WAITING 폴링 재조회, 404 empty, refetchInterval이 3초·중단 조건 |
useWaitingRoom 뷰모델 | 훅 | WAITING phase, ADMITTED 시 토큰 저장+전환 호출, 429 포화, 404 empty, ratio 계산 |
QueueProgressBar | 컴포넌트 | ratio 0/0.5/1 width 반영, % 라벨, 라이트/다크 토큰 |
QueuePositionCard/QueueWaitInfo | 컴포넌트 | 순번·앞선 인원·ETA 라벨 렌더, null 처리 |
| S1 화면 | 컴포넌트(통합) | loading→waiting 렌더, admitted 시 전환, 포화 문구, 오류 재시도, 두 모드 렌더 |
| 진입점 분기 | 컴포넌트 | 플래그 ON→대기실 경로, OFF→직접 구매 경로 |
| 구매 토큰 헤더 | 단위 | 토큰 존재 시 X-Entry-Token 부착, 403→만료 결과 |
Release Scenario — 기능 플래그·점진 공개
- FE 플래그
virtual-queue.enabled(EXPO_PUBLIC_VIRTUAL_QUEUE_ENABLED, 기본false) — 기존lib/feature-flags.ts관례. 진입점이 대기실을 경유할지 결정. - BE 플래그와 lockstep: FE 플래그는 BE
virtual-queue.enabled와 함께 켠다. FE ON·BE OFF면 enter가DIRECT_ADMITTED를 즉시 반환해 대기 없이 통과(무해). FE OFF·BE ON은 BE 403을 유발하므로 금지 조합 — 배포 순서상 BE ON 이후 FE ON. - 단계: (1) 전 티켓 머지, 플래그 기본 OFF(기존 동작 무변화) → (2) BE 카나리 ON 확인 후 FE 플래그 ON → (3) 전량 ON. 롤백 = FE 플래그 OFF(대기실 우회, 기존 직접 구매 복귀).
- 제거 시점: 대기열 상시 운영이면 분기 유지(영구 안전장치). BE Release Scenario와 정합.
Open Questions
- 앱 백그라운드 시 TanStack Query
refetchInterval정지 동작(RN 타이머 스로틀)에 따라 heartbeat가 멎어 60초 후 이탈 판정 — 이는 PRD 시나리오 2(이탈=순번 소실)와 정합하므로 의도된 동작으로 둔다.AppState포그라운드 복귀 시 즉시 refetch(404면 empty 전환)로 처리. 별도 백그라운드 keep-alive는 범위 밖. - 진행바 기준값(initialAhead)이 폴링 사이에 서버 큐 축소로 aheadCount가 증가(뒤로 밀림, 재진입자 유입)하는 경우 ratio가 음수가 될 수 있어
clamp(0,1)처리 — 뷰모델에서 방어.
Document History
| 날짜 | 변경 내용 |
|---|---|
| 2026-07-09 | 최초 작성 — BE TDD “FE/외부 계약” 5종 소비. Expo Router 단일 대기실 화면 + 두 진입점 와이어업 + X-Entry-Token 헤더. 신규 테마 토큰 0·기존 폴링/스토어/플래그 관례 재사용. FE 티켓 9건 분해(별첨, wave 4,2,1,2) |