가상 대기열(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(대상)대기열 단위. typelimited-drop | ticketing-event, targetId = dropId | eventId
폴링 heartbeatGET .../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건
한정판 진입점 CTAapp/limited-drop/[id]/index.tsx:99상세 CTA가 router.push('/limited-drop/${dropId}/purchase')로 직접 이동
한정판 구매 APIapi/limitedDrops.ts#purchaseLimitedDrop + lib/usePurchaseLimitedDrop.tsPOST /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=...')
티케팅 좌석 선점 APIapi/ticketOrders.ts#selectSeatsPOST /events/{eventId}/seats/select body {seatIds}. X-Entry-Token 없음. order.tsx#handleConfirm가 호출
폴링 패턴lib/useLimitedDrop.ts#getRefetchIntervalMsTanStack 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.tsxtrack(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을 메모리 스토어로 화면 경계 넘겨 소비하는 선례
userIdapi/goods.ts#useCurrentUserId → numberX-User-Id 헤더용. dev 1 반환
API 클라이언트api/be-client.ts#getBeClientaxios 싱글톤. 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 — WAITINGstatus 200 WAITING순번·앞선 인원·ETA·진행바(위 와이어프레임)3초 폴링 자동
success — ADMITTED/DIRECTstatus/enter 200 ADMITTED|DIRECT_ADMITTED (entryToken 존재)짧은 “입장! 이동 중” 스피너 후 구매 화면 router.replace토큰 저장 → 자동 전환
empty — 큐에 없음status 404 (이탈·미진입)EmptyState “대기열에서 나왔어요” + 사유(“자리를 비운 사이 순번이 정리됐어요”)단일 CTA “다시 대기”(재-enter) 또는 “돌아가기”(back)
error — 포화enter 429 QUEUE_FULLEmptyState(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).

시맨틱 토큰용도(대기실)LightDark
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는 기존 컴포넌트를 그대로 쓴다. QueueProgressBarRemainingStockBar의 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}/entriesuseEnterQueueenterQueue()X-User-Id, body 없음200 QueueEntryResponse429 QUEUE_FULL→포화 상태, 5xx/네트워크→error
S1 폴링(heartbeat)GET /virtual-queues/{type}/{targetId}/entries/meuseQueueStatus(refetchInterval 3s) → getQueueStatus()X-User-Id200 QueueEntryResponse404→empty(재진입 유도), 5xx→error(3회 후 중단)
S1 이탈(나가기/언마운트)DELETE /virtual-queues/{type}/{targetId}/entries/meleaveQueue() (best-effort)X-User-Id204실패 무시(60초 heartbeat 미갱신으로 자동 방출)
한정판 구매(기존)POST /limited-drops/{dropId}/ordersusePurchaseLimitedDrop(헤더 추가)기존 + X-Entry-Token기존403 QUEUE_BYPASS_DENIED→“다시 대기하기”
티케팅 좌석(기존)POST /events/{id}/seats/selectselectSeats(헤더 추가)기존 + 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·etaSecondsLong(정수)으로 명세 → FE number. entryToken/tokenExpiresAtString?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
useQueueStatusWAITING 폴링 재조회, 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)