모집·시설상품·소모임예약·모임게시글 연동 — FE 설계 (앱 / React Native · Expo)

Background

근거 PRD:

  • 스포츠앱/모임·커뮤니티/20260706-post-community-연동-prd.md (모임 게시글·종목별 게시글)
  • 스포츠앱/모임·커뮤니티/20260706-모집-시설상품-소모임예약연동-prd.md (모집·시설상품·소모임 예약)

근거 BE TDD (API 계약 소비):

  • 20260707-post-community-연동-tdd.md “API 계약” 섹션 (posts additive 확장 + GET /communities/{communityId}/posts)
  • 20260707-모집-시설상품-소모임예약연동-tdd.md “REST API 계약” 표 (recruitment 7 + facility 6 + community 2 신규 엔드포인트)

이 문서는 앱(소비자용, mobile/ — Expo Router + React Native) 의 FE 기술 설계다. 운영자용 기능(운영시간 등록·휴무 관리·시설상품 등록·슬롯 수동 open/close)은 웹 운영 포털이 담당하며 20260707-...-design-fe-web.md가 별도로 다룬다. 플랫폼 경계는 §“플랫폼 경계 · 공유 경계” 표에 확정한다.

Overview

앱이 담당하는 화면재사용 기반
모집(recruitment)목록·상세·개설·신청(결제)·내 신청·신청 취소(단계 수수료 고지)·신청자 목록·개설자 취소신규 api/recruitment.ts·lib/useRecruitment*, 결제는 기존 app/payment/new.tsx(MO-08)에 OrderType.RECRUITMENT 추가
시설상품(program)시설 상세 내 program 목록·운영시간/휴무 표시·program 회차 예약예약은 기존 app/booking/new.tsx(MO-04) 재사용(slot programId 필터). facility 상세 확장
모임 게시글(community post)모임 상세 내 게시판(목록·작성·상세·댓글)·종목별 게시글 필터기존 api/post.ts·lib/usePosts.ts 확장. 모임 상세 communities/[id].tsx(S5)에 게시판 섹션 추가
소모임 예약(community↔booking)모임 내 연결 예약 목록(멤버 열람)·예약 연결(방장)신규 api/communityBooking.ts. slot 선택은 booking slot 목록 재사용

핵심 원칙: 소비자 앱은 새 예약·결제 메커니즘을 만들지 않는다. program·소모임 예약은 기존 booking slot + OrderType.BOOKING 결제를, 모집 신청 결제는 기존 결제 WebView 흐름 + OrderType.RECRUITMENT를 재사용한다.

Terminology

용어정의
Recruitment모집글 — 정원·참가비·활동일시·신청마감·상태(OPEN/CLOSED/CANCELLED)
Application모집 신청 — 상태(PENDING/CONFIRMED/CANCELLED/REFUNDED)·결제 참조
단계 수수료신청 취소 시 마감 잔여기간별 수수료율 (7일 초과 0% / 3~7일 5% / 3일 이내 10%)
Program시설상품 (PT·클래스) — 이름·가격·정원·소요시간. facility 산하
모임 게시글communityId non-null 게시글. 소속 모임 가시성·멤버십으로 열람/작성 인가
소모임 예약방장이 booking slot을 모임 활동으로 연결한 링크(CommunityBooking)

Define Problem

AS-IS (실제 코드 근거)

사실근거
예약/슬롯 선택 + 결제 흐름이 이미 존재app/booking/new.tsx(MO-04) — useSlots(facilityId) 슬롯 목록 → 슬롯·결제수단 선택 → useCreateBookingrouter.push('/payment?orderType=BOOKING&orderId=...&amount=...')
결제 WebView 흐름이 orderType를 라우트 파라미터로 수용app/payment/new.tsx(MO-08) — orderType/orderId/amount 수신 → preparePayment(POST /payments/prepare) → Linking.openURL(checkoutUrl)getPayment 폴링(최대 3회). OrderType = 'BOOKING' | 'TICKETING' | 'GOODS' (api/payment.ts:11)
게시글 API·훅 존재 (전역 전용)api/post.ts = searchPosts/getPost/createPost. lib/usePosts.ts = usePosts/usePost/useCreatePost. CreatePostRequestcommunityId·sportCategory 없음. 댓글 작성/목록 함수 0건 (PostDetailResponse.comments만 존재)
커뮤니티 컨텍스트 완비api/community.ts(9 함수)·lib/useCommunity.ts(useCommunity/useCommunityMembers/멤버십 mutation 등) · api/community-types.ts(CommunityVisibility·MemberRole·MembershipStatus·SportCategory 12종·CommunityResponse). 모임 상세 화면 communities/[id].tsx(S5) 존재
시설 상세·슬롯 조회 존재api/facility.ts·lib/useFacility.ts(useFacilityDetail). SlotResponseprogramId·status·price 없음(api/types.ts:104)
모집·시설상품·소모임예약 UI·API 전무api/recruitment.ts·program·communityBooking 0건
테마·상태·재사용 컴포넌트 정착theme/tokens.ts(시맨틱 토큰 light/dark, accent #3182F6 = 토스 블루) + theme/useTheme.ts + theme/themeStore.ts(Zustand, MMKV 영속). components/ui = Card·EmptyState·ErrorView·LoadingView·ThemedText. components/PaymentButton.tsx. 서버상태 TanStack Query, 전역상태 Zustand, TS strict
종목·가시성 표시 헬퍼 존재lib/community-format.ts = getSportCategoryDisplay·getVisibilityLabel
기능 플래그 게이트 존재lib/feature-flags.ts = isFeatureEnabled(flag) (환경변수 소스, 채팅 플래그만 정의)

TO-BE

  • 모집 4화면 신규 + 결제 흐름에 RECRUITMENT 추가.
  • facility 상세에 program 목록·운영시간/휴무 표시 섹션 추가, program 예약은 booking/new 재사용.
  • 모임 상세(S5)에 게시판 섹션 + 게시글 작성/상세/댓글 화면, 전역 게시글 목록에 종목 필터.
  • 모임 상세에 소모임 예약(연결 목록·연결하기) 섹션.
  • 테마 토큰 신규 0개 — 기존 theme/tokens.ts 재사용(수수료 경고=warning, 무료·전액환불=success, 마감후 불가=danger).

Architecture Benchmarking (토스 벤치마킹)

디자인 입력이 없으므로 화면 구성은 토스(Toss)를 벤치마킹한다. 기존 앱 accent가 이미 토스 블루(#3182F6)로, 토스 패턴이 정착돼 있다.

화면참고 토스 패턴
모집 개설 폼토스 송금·상품등록 — 한 화면 한 과업, 항목별 단독 입력 스텝(정원·참가비·일시), 하단 고정 단일 CTA
모집 상세토스 결제/청구 상세 — 상단 핵심 요약 카드(정원·참가비·마감), 본문, 하단 고정 단일 CTA(“신청하기 · 5,000원”)
신청 취소 수수료 고지토스 해지·환불 확인 바텀시트 — 환불액을 큰 숫자로, 수수료를 회색 보조 텍스트로, 되돌릴 수 없음을 명확히
모임 게시판토스 피드 — 절제된 카드 리스트, 작성 플로팅 단일 CTA, 공지는 상단 배지
종목 필터토스 카테고리 칩 — 가로 스크롤 세그먼트, 선택 시 accent 1곳만
시설상품 목록토스 상품 리스트 — 가격을 위계 최상단, 정원·소요시간은 보조 메타

Detail Design

플랫폼 경계 · 공유 경계 (웹/앱 · 필독)

mobile/(앱, 소비자)와 web/(운영 포털)은 별도 앱이다 — RN vs React 웹, 데이터 패칭 패러다임도 다르다(앱=TanStack Query, 웹=useState/useEffect+fetch+Next API BFF). 따라서:

공유 대상공유 여부근거
BE API 계약공유(SSOT)두 앱의 타입이 BE TDD 계약과 필드·타입 일치해야 함
타입 정의비공유(각자 정의)api/types.ts·api/community-types.ts·(신규)api/recruitment.ts / 웹 lib/portal/types.ts. 코드 패키지 공유 없음 → 각자 계약을 미러링
API 클라이언트비공유api/be-client.ts(axios) / 웹 lib/server/be-client.ts + Next /api/portal/* BFF
query 훅비공유lib/use*.ts(TanStack) / 웹 lib/portal/use*.ts(fetch)
컴포넌트비공유(플랫폼별)RN StyleSheet / 웹 shadcn+Tailwind. 컴포넌트 공유 강제 안 함

결론: 이 두 design-fe 문서 간 유일한 공유 계약은 BE API 계약 필드다. 아래 §“기능별 담당 플랫폼” 표로 화면 중복을 방지한다.

기능별 담당 플랫폼 (중복 방지)

기능앱(소비자)웹(운영자)
모집 개설·목록·상세·신청·취소·내 신청·신청자 목록✅ 전부
시설상품(program) 등록·수정
시설상품 목록 열람·회차 예약
운영시간·휴무 등록
운영시간·휴무 표시(시설 상세)✅ (읽기)✅ (편집)
슬롯 수동 open/close
모임 게시글 작성/열람/댓글·종목 필터
소모임 예약 연결(방장)·열람(멤버)

화면 목록 (앱)

ID화면라우트 (expo-router)신규/확장
A-R1모집 목록app/recruitments/index.tsx신규
A-R2모집 상세app/recruitments/[id].tsx신규
A-R3모집 개설 폼app/recruitments/new.tsx신규
A-R4신청자 목록(개설자)app/recruitments/[id]/applications.tsx신규
A-R5내 신청 목록app/recruitments/me.tsx신규
A-R6신청 취소 수수료 바텀시트components/recruitment/CancelApplicationSheet.tsx신규(컴포넌트)
A-F1시설 상세 확장(program 목록 + 운영시간/휴무 표시)app/facility/[id]/index.tsx확장
A-F2program 회차 예약app/booking/new.tsx 재사용(?programId=)확장
A-P1모임 게시판 섹션(모임 상세 내)app/communities/[id].tsx(S5)확장
A-P2모임 게시글 목록app/communities/[id].tsx 내 게시판 섹션확장
A-P3게시글 작성app/community/new.tsx(기존, ?communityId= optional)확장
A-P4게시글 상세 + 댓글app/community/[id].tsx(기존, 하드코딩 색 토큰 리트로핏 + 댓글 추가)확장
A-P5종목별 게시글 필터(전역 목록)app/(tabs)/community.tsx(기존 전역 게시글 탭)확장
A-B1소모임 예약 목록(모임 상세 내)app/communities/[id].tsx 섹션확장
A-B2소모임 예약 연결(방장)app/communities/[id]/bookings/new.tsx신규

app/community/[id].tsx(구, 하드코딩 색 #fff/#007AFF)는 stale 잔재 — 사용/확장하지 않는다. 모임 상세는 app/communities/[id].tsx(S5)가 정본.

텍스트 와이어프레임 (토스 패턴)

A-R1 모집 목록 — 토스 피드

┌───────────────────────────┐
│ 모집                        │  ← 헤더
│ [전체][우리 모임] 세그먼트    │  ← communityId 필터(옵션)
├───────────────────────────┤
│ ⚽ 주말 축구 3명 모집         │  ← Card
│ 정원 3 · 5,000원 · D-2 마감  │  ← 보조 메타(textSecondary)
│ [OPEN 배지]                 │
├───────────────────────────┤
│ 🏀 농구 픽업 게임            │
│ 정원 6 · 무료 · D-5 마감     │
└───────────────────────────┘
        ( + )  ← 플로팅 단일 CTA(개설, accent)

A-R2 모집 상세 — 토스 결제 상세 (상단 요약 카드 + 하단 고정 CTA)

┌───────────────────────────┐
│ 주말 축구 3명 모집           │  ← 제목(header)
│ ┌─ 요약 카드 ──────────┐    │
│ │ 참가비   5,000원      │    │  ← 가격 위계 최상단
│ │ 정원     2/3명        │    │
│ │ 활동일   7/12(토) 14시 │    │
│ │ 마감     7/10(목) 23시 │    │
│ └───────────────────────┘    │
│ 설명 본문...                 │
│ (개설자 뷰) [신청자 보기]     │  ← 개설자만
│ (개설자 뷰) [모집 취소]       │  ← 개설자만, danger 텍스트
├───────────────────────────┤
│ [ 신청하기 · 5,000원 ]       │  ← 하단 고정 단일 CTA(accent)
└───────────────────────────┘
   정원마감 시 → [ 모집 마감됨 ] disabled

A-R6 신청 취소 수수료 바텀시트 — 토스 환불 확인

┌───────────────────────────┐
│ 신청을 취소할까요?           │
│                            │
│   환불 예정   4,750원        │  ← 큰 숫자(textPrimary)
│   수수료 5% (250원) 공제     │  ← 보조(textSecondary/warning)
│   마감까지 5일 남음           │
│                            │
│ 취소 후에는 되돌릴 수 없어요.  │  ← 경고(danger 톤)
│ [ 취소하기 ]  [ 닫기 ]        │
└───────────────────────────┘
  * 마감 후 진입 시: "마감되어 취소할 수 없어요"만 표시, CTA disabled
  * 무료 구간(7일 초과): "전액 환불 · 수수료 없음"(success)

A-P1/A-P2 모임 게시판 — 토스 피드 (모임 상세 탭)

[소개] [멤버] [게시판] [활동]   ← 모임 상세 세그먼트 탭
┌───────────────────────────┐
│ 📌 [공지] 이번 달 회비 안내   │  ← NOTICE = 상단 배지
│ 홍길동 · 2일 전              │
├───────────────────────────┤
│ 토요일 경기 후기             │
│ 김철수 · 5일 전 · 댓글 3      │
└───────────────────────────┘
        ( + 글쓰기 )  ← ACTIVE 멤버만 노출, 비멤버는 숨김
  * PRIVATE 비멤버 → 게시판 탭 자체가 잠금 상태(자물쇠 + "멤버만 볼 수 있어요")

A-P5 종목 필터 — 토스 카테고리 칩 (전역 목록 상단 가로 스크롤)

[전체][⚽축구][🏀농구][🏃러닝][🏸배드민턴]...  ← 선택 칩만 accent

A-F1 시설 상세 확장 — program 목록 + 운영시간

(기존 시설 정보 · 대기질 카드)
┌ 운영시간 ──────────────────┐
│ 평일 06:00~22:00 (12~13 휴게)│  ← 읽기 전용 표시
│ 토·일 08:00~20:00           │
│ 7/15(화) 휴무               │
└───────────────────────────┘
┌ 시설상품 ──────────────────┐
│ PT 1:1 · 50,000원 · 60분     │  ← 가격 위계 최상단
│ 정원 1명            [예약]   │
├───────────────────────────┤
│ 필라테스 그룹 · 30,000원 · 50분│
│ 정원 6명            [예약]   │
└───────────────────────────┘

A-B2 소모임 예약 연결(방장) — 토스 슬롯 선택 (booking slot 재사용)

┌───────────────────────────┐
│ 활동 예약 연결               │
│ 시설: [○○ 체육관 선택]       │
│ ┌ 예약 가능 회차 ─────────┐ │  ← useSlots 재사용, CLOSED 슬롯 제외
│ │ ○ 7/12(토) 14:00 (2/8)  │ │
│ │ ○ 7/13(일) 10:00 (0/8)  │ │
│ └───────────────────────┘ │
│ [ 이 회차로 연결 ]           │  ← 방장만, POST /communities/{id}/bookings
└───────────────────────────┘

화면별 4상태 표 (loading / empty / error / success)

화면loadingemptyerrorsuccess
A-R1 모집 목록LoadingView 스켈레톤EmptyState “아직 모집이 없어요” + 개설 유도ErrorView + 재시도카드 리스트
A-R2 모집 상세LoadingView해당 없음(단건)ErrorView; 404→“삭제되었거나 없는 모집”요약 카드 + CTA. 정원마감→CTA disabled
A-R3 개설 폼제출 중 CTA 스피너해당 없음필드 인라인 검증 + 제출 실패 토스트성공 시 상세로 이동
A-R4 신청자 목록LoadingViewEmptyState “아직 신청자가 없어요”(정상)403→“개설자만 볼 수 있어요”신청자 리스트
A-R5 내 신청 목록LoadingViewEmptyState “신청한 모집이 없어요”ErrorView신청 카드(상태 배지 + 취소 CTA)
A-R6 취소 시트취소 중 CTA 스피너422 마감후→“취소할 수 없어요”; PG오류→“환불 처리 실패, 잠시 후 다시”환불액 표시 후 목록 갱신
A-F1 시설 상세섹션별 LoadingViewprogram 0건→“등록된 상품이 없어요”; 휴무/운영시간 미등록→섹션 숨김섹션별 ErrorView(다른 섹션 영향 없음)목록 + 운영시간
A-F2 program 예약슬롯 LoadingView슬롯 0건(휴무일 포함)→“예약 가능한 회차가 없어요”ErrorView슬롯 선택 → 결제 이동
A-P1/P2 게시판LoadingViewEmptyState “첫 글을 남겨보세요”403 PRIVATE 비멤버→잠금 상태 UI(에러 아님)게시글 리스트
A-P3 작성제출 중 스피너403→“작성 권한이 없어요”(NOTICE 비host 포함)성공→목록 갱신 후 뒤로
A-P4 상세+댓글LoadingView댓글 0건→“첫 댓글을 남겨보세요”403→잠금; 404→“없는 게시글”본문 + 댓글 리스트 + 입력
A-P5 종목 필터목록 LoadingView해당 종목 0건→“이 종목 글이 아직 없어요”(정상)ErrorView필터된 리스트
A-B1 소모임 예약 목록LoadingViewEmptyState “연결된 예약이 없어요”403→잠금예약 카드(시설·일시·정원)
A-B2 예약 연결슬롯 LoadingView슬롯 0건→“예약 가능한 회차가 없어요”403→“방장만 연결할 수 있어요”연결 성공→목록 갱신

컴포넌트 트리 (컨테이너/프레젠테이션 분리)

app/recruitments/index.tsx (컨테이너: useRecruitments)
  └─ RecruitmentList (프레젠테이션)
       └─ RecruitmentCard  (components/recruitment/)
app/recruitments/[id].tsx (컨테이너: useRecruitment)
  ├─ RecruitmentSummaryCard (프레젠테이션)
  ├─ ApplyButton → /payment 이동 (useApplyRecruitment)
  └─ (개설자) CancelRecruitmentButton (useCancelRecruitment)
app/recruitments/me.tsx (컨테이너: useMyApplications)
  └─ ApplicationCard
       └─ CancelApplicationSheet (A-R6, useCancelApplication + useCancellationPreview)
app/facility/[id]/index.tsx (확장 컨테이너: useFacilityDetail + usePrograms + useOperatingHours)
  ├─ OperatingHoursCard (프레젠테이션, 읽기)
  └─ ProgramList → ProgramCard → [예약] → /booking/new?programId=
communities/[id].tsx (S5 확장 — 동아리 상세)
  └─ CommunitySegmentTabs [소개|멤버|게시판|활동]
       ├─ CommunityBoardSection (useCommunityPosts(communityId) — 멤버십 게이트)
       │    └─ PostCard → app/community/[id] 이동 / BoardLockedView
       └─ CommunityBookingSection (useCommunityBookings — 멤버십 게이트)
            └─ CommunityBookingCard / (방장) LinkBookingButton
app/community/new.tsx (기존 확장, 컨테이너: useCreatePost) — communityId 유무로 종목 선택 UI 분기
app/community/[id].tsx (기존 확장, 컨테이너: usePost + useComments) — 전역·모임 게시글 공용 상세
  ├─ PostBody (프레젠테이션, 하드코딩 색 → 토큰 리트로핏)
  └─ CommentList + CommentInput (useAddComment)
app/(tabs)/community.tsx (기존 확장) — 전역 게시글 목록 + SportCategoryChips 필터

재사용 컴포넌트: components/uiCard·EmptyState·ErrorView·LoadingView·ThemedText, components/PaymentButton.tsx. lib/community-format.tsgetSportCategoryDisplay·getVisibilityLabel.

상태관리 설계

관심사저장 위치근거
모집/신청/게시글/댓글/program/소모임예약 서버 데이터TanStack Query 캐시 (lib/use*.ts)레포 정착 표준. 서버데이터 스토어 복사 금지
신청 취소 수수료 미리보기(환불액)서버 계산 우선 — 목록 응답의 신청 항목에 마감시각 포함 → 클라 계산 or 프리뷰 훅. 아래 “확인 필요” 참조수수료율 SSOT는 BE CancellationPolicy
개설 폼·댓글 입력·슬롯 선택지역 useState화면 로컬. 전역 승격 불필요
테마 모드기존 theme/themeStore.ts(Zustand, MMKV)이미 전역 — 신규 스토어 없음
세션/유저기존 인증 스토어재사용

신규 Zustand 스토어 0개. “일단 전역”은 미채택 — 모집/신청 상태는 전부 Query 캐시로 충분(화면 간 공유는 queryKey 무효화로 동기화).

낙관적 업데이트: 댓글 작성(A-P4)에만 적용 — 입력 즉시 리스트에 낙관적 반영, 실패 시 롤백 + 입력 복원. 모집 신청/취소는 결제·환불이 얽혀 있어 낙관 미적용(서버 확정 후 invalidate).

API 연동 표 (BE 계약 대조)

BE 계약 필드는 앱 타입(api/recruitment.ts·api/community-types.ts·api/types.ts)에 미러링한다.

모집 (recruitment)

화면메서드·경로훅 (신규)에러 처리
A-R1GET /recruitments?communityId=useRecruitments(communityId?)403(모임 소속·비멤버)→잠금
A-R2GET /recruitments/{id}useRecruitment(id)403→잠금, 404→없음
A-R3POST /recruitmentsuseCreateRecruitment400 검증→인라인, 성공→상세 이동
A-R4GET /recruitments/{id}/applicationsuseApplications(id)403(비개설자), 0건 정상
A-R2→결제POST /recruitments/{id}/applications{applicationId, paymentId?, checkoutUrl?}useApplyRecruitment(id)409 정원초과→“마감됨”, fee=0→즉시 확정(결제 스킵)
결제기존 app/payment/new.tsx + OrderType.RECRUITMENT재사용checkoutUrl 소비(아래 결정)
A-R5GET /recruitments(내 신청 필터) 또는 신청 내역 APIuseMyApplications”확인 필요” 참조
A-R6POST /applications/{id}/canceluseCancelApplication422 마감후→불가, PG오류→재시도 안내
A-R2(개설자)POST /recruitments/{id}/canceluseCancelRecruitment403(비개설자)

시설상품 (program) · 시설 상세

화면메서드·경로에러 처리
A-F1GET /facilities/{facilityId}/programsusePrograms(facilityId)0건→empty
A-F1운영시간·휴무 = 시설 상세 응답에 포함(BE 임베드) → useFacilityDetail 확장확장미등록→섹션 숨김
A-F2GET /facilities/{facilityId}/slots(programId 필터) → POST /bookings/payment?orderType=BOOKINGuseSlots(확장)·useCreateBooking 재사용슬롯 0건→empty, 409 정원/CLOSED→“예약 불가”

모임 게시글 (community post)

화면메서드·경로에러 처리
A-P2GET /communities/{communityId}/posts?sportCategory=&page=&size=useCommunityPosts(communityId, criteria)403 PRIVATE 비멤버→잠금, 0건 정상
A-P3POST /posts(communityId·type; 모임글은 sportCategory 상속·미전송)useCreatePost(확장)403 비멤버/NOTICE 비host
A-P4GET /posts/{id}(X-User-Id) + GET /posts/{postId}/comments(X-User-Id)usePost(확장)·useComments(신규)403→잠금
A-P4POST /posts/{postId}/commentsuseAddComment(신규)403 비멤버
A-P5GET /posts?sportCategory=&communityId=usePosts(확장, criteria)종목 0건 정상

소모임 예약 (community↔booking)

화면메서드·경로에러 처리
A-B1GET /communities/{communityId}/bookingsuseCommunityBookings(communityId)403 비멤버→잠금, 0건 정상
A-B2시설 slot 선택(GET /facilities/{id}/slots) → POST /communities/{communityId}/bookings{slotId}useSlots 재사용·useLinkCommunityBooking403(비방장)

결제 흐름 재사용 결정 (모집 신청)

BE 계약: POST /recruitments/{id}/applicationspaymentId/checkoutUrl 반환(신청+결제 개시). 기존 app/payment/new.tsx(MO-08)는 preparePayment(POST /payments/prepare)를 화면 내부에서 호출한다 — 모집은 서버가 이미 prepare한 상태라 흐름이 다르다.

  • 결정: OrderType'RECRUITMENT' 추가 + 결제 화면에 “pre-issued” 진입 모드 추가 — checkoutUrl·paymentId가 라우트 파라미터로 오면 preparePayment를 건너뛰고 바로 Linking.openURL(checkoutUrl)getPayment(paymentId) 폴링. (GET /payments/{id} 폴링·성공/실패 판정 로직은 그대로 재사용.)
  • fee=0 신청은 checkoutUrl 없음 → 결제 화면 진입 없이 즉시 CONFIRMED 처리 후 내 신청 목록으로.
  • 롤백: 이 확장은 additive(기존 BOOKING/GOODS/TICKETING 경로 무변경). 문제 시 RECRUITMENT 분기만 제거.

라우팅 · 내비게이션 흐름

flowchart LR
    Tabs["(tabs) community/search"] --> RList["recruitments/index (A-R1)"]
    RList --> RNew["recruitments/new (A-R3)"]
    RList --> RDetail["recruitments/[id] (A-R2)"]
    RDetail --> Pay["payment/new (RECRUITMENT)"]
    RDetail --> RApps["recruitments/[id]/applications (A-R4)"]
    Me["(tabs) me"] --> MyApps["recruitments/me (A-R5)"]
    MyApps --> CancelSheet["CancelApplicationSheet (A-R6)"]
    FacDetail["facility/[id] (A-F1)"] --> BookNew["booking/new?programId (A-F2)"]
    BookNew --> Pay2["payment/new (BOOKING)"]
    CommDetail["communities/[id] (S5)"] --> Board["게시판 섹션 (A-P1/P2)"]
    Board --> PostNew["community/new?communityId (A-P3)"]
    Board --> PostDetail["community/[id] (A-P4)"]
    GlobalList["(tabs)/community (A-P5)"] --> PostDetail
    CommDetail --> BookSection["소모임 예약 (A-B1)"]
    BookSection --> LinkNew["communities/[id]/bookings/new (A-B2)"]

테마 토큰 (시맨틱 토큰 → 라이트/다크 매핑)

SSOT는 기존 mobile/theme/tokens.ts. 신규 토큰 0개 — 아래는 이 설계가 사용하는 토큰의 재확인 매핑이다(색 하드코딩 금지, useTheme() 경유).

시맨틱 토큰라이트다크이 설계에서의 용도
background#FFFFFF#17171C화면 배경
surface#F9FAFB#202027카드(모집·게시글·program)
surfaceElevated#FFFFFF#26262E바텀시트(A-R6)·입력창
textPrimary#191F28#F2F4F6제목·참가비·환불액
textSecondary#4E5968#B0B8C1정원·마감·수수료 보조
textTertiary#8B95A1#6B7684시각·placeholder·CLOSED 슬롯
border#E5E8EB#2E2E36구분선·칩 테두리
accent#3182F6#4E93FB단일 CTA·선택 칩·활성 탭(화면당 1곳)
accentText#FFFFFF#FFFFFFCTA 텍스트
success#12B886#2AC29B무료·전액환불·OPEN 배지
warning#FF9500#FF9F0A수수료 공제 고지(A-R6)
danger#F04452#F76A78모집 취소·마감후 불가·되돌릴 수 없음
disabled#C7C7CC#48484A마감된 CTA·CLOSED 슬롯

두 모드 모두 구현·확인해야 화면 완료. RN은 useTheme() 토큰 + createStyles 팩토리로 소비(useColorScheme 기본 + themeStore 오버라이드).

Testing Plan (implementer TDD 입력)

Testing Library(@testing-library/react-native) + Jest. 사용자 관점 동작 검증(보이는 텍스트·role·인터랙션). 훅은 renderHook. 각 티켓 최소 3케이스(렌더·인터랙션·실패 상태).

대상핵심 케이스
모집 목록목록 렌더 / 0건 empty / fetch 실패 error / 개설 CTA 이동
모집 상세요약 렌더 / 정원마감 시 CTA disabled / 신청 탭→결제 이동 / 개설자 뷰에 취소·신청자 버튼 노출
개설 폼유효 입력 제출 성공 / 정원<1·참가비<0 인라인 검증 / 마감<활동일 검증 / 제출 실패 토스트
취소 시트(A-R6)5% 구간 환불액 4,750 표시 / 무료 구간 “수수료 없음” / 마감후 CTA disabled + 안내 / PG오류 재시도 안내
결제 재사용RECRUITMENT + pre-issued checkoutUrl 진입 시 prepare 스킵 / fee=0 즉시확정 결제 스킵
시설 상세program 목록 렌더 / 0건 empty / 운영시간 미등록 섹션 숨김 / 예약 버튼→booking/new?programId 이동
게시판 섹션ACTIVE 멤버 글쓰기 CTA 노출 / PRIVATE 비멤버 잠금 UI / NOTICE 배지 상단 표시 / 0건 empty
게시글 작성모임글은 종목 선택 UI 숨김 / 전역글은 종목 선택 노출 / 403 작성권한 없음 안내
게시글 상세+댓글본문·댓글 렌더 / 댓글 낙관 반영 후 실패 롤백 / 403 잠금 / 댓글 0건 empty
종목 필터칩 선택 시 필터 적용 / 해당 종목 0건 empty(정상) / 전체 선택 시 미지정 포함
소모임 예약멤버 목록 열람 / 방장 연결 버튼 노출·비방장 숨김 / CLOSED 슬롯 제외 / 0건 empty
각 훅(renderHook)성공 데이터 반환 / 에러 상태 / queryKey 무효화로 목록 갱신
다크 모드각 화면 라이트/다크 두 모드 스냅 아닌 토큰 적용 확인(하드코딩 색 0건)

Release Scenario — 기능 플래그 · 점진 공개

기존 lib/feature-flags.ts 확장(값 정의만 소유하는 단일 게이트 패턴 계승). BE Release Scenario 플래그(recruitment.enabled·facility.program.enabled·facility.autoslot.enabled·community.booking.enabled)와 정합하는 FE 진입 플래그 추가:

플래그envKey기본값게이트 대상
recruitment.enabledEXPO_PUBLIC_RECRUITMENT_ENABLEDfalseA-R1~R6 진입점·탭
facility.program.enabledEXPO_PUBLIC_FACILITY_PROGRAM_ENABLEDfalseA-F1 program 섹션·A-F2
community.post.enabledEXPO_PUBLIC_COMMUNITY_POST_ENABLEDfalseA-P1~P5 게시판·종목 필터
community.booking.enabledEXPO_PUBLIC_COMMUNITY_BOOKING_ENABLEDfalseA-B1·A-B2
  • 플래그 OFF 시 진입점(탭·버튼·섹션)을 렌더하지 않는다 → 점진 공개. BE 플래그 순서(community.booking→program→recruitment)와 축 단위로 맞춰 ON.
  • 신규 라우트·컴포넌트는 additive. 롤백 = 플래그 OFF(코드 롤백 없이 즉시 비활성).

Open Questions

항목처리
내 신청 목록(A-R5) 전용 엔드포인트BE TDD REST 계약에 “내 신청 목록” 전용 경로가 없다(GET /recruitments/{id}/applications는 개설자용). GET /applications?applicantUserId=(X-User-Id) 형태의 조회가 필요 — BE에 요청 계약으로 역제안(§확인 필요). 없으면 A-R5는 신청 시 로컬 캐싱으로 임시 대응
신청 취소 수수료 미리보기 계산 위치환불액 표시는 마감시각·참가비·현재시각으로 클라 계산 가능하나 수수료율 SSOT는 BE CancellationPolicy. 정확도 위해 GET /applications/{id}/cancellation-preview(환불액·수수료율 반환) 역제안 검토. 미제공 시 클라가 동일 단계율(7일↑0/3~7일 5/3일↓10)로 계산하고 실제 공제는 서버 확정값 사용
SlotResponse price기존 booking 화면이 price 미보유로 10,000원 고정(MO-04 주석). program 예약은 program.price가 있으므로 program 예약 결제 amount는 program.price 사용. slot 자체 price 추가는 booking 개선 과제
program 회차 slot 조회 필터GET /facilities/{id}/slots가 programId 필터를 지원하는지 BE 계약 미명시 — 지원 필요(§확인 필요). 미지원 시 전체 슬롯에서 클라 필터

Document History

날짜변경 내용
2026-07-07최초 작성 — 앱(소비자) FE 설계. 모집 6화면·시설상품/시설상세 2·모임게시판 5·소모임예약 2. 결제(RECRUITMENT pre-issued)·booking slot·게시글 훅·모임 상세(S5)·4상태 컴포넌트·theme tokens 재사용. 신규 Zustand·신규 토큰 0개