모집·시설상품·소모임예약·모임게시글 연동 — 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) 슬롯 목록 → 슬롯·결제수단 선택 → useCreateBooking → router.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. CreatePostRequest에 communityId·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). SlotResponse에 programId·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-F2 | program 회차 예약 | 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)
| 화면 | loading | empty | error | success |
|---|---|---|---|---|
| A-R1 모집 목록 | LoadingView 스켈레톤 | EmptyState “아직 모집이 없어요” + 개설 유도 | ErrorView + 재시도 | 카드 리스트 |
| A-R2 모집 상세 | LoadingView | 해당 없음(단건) | ErrorView; 404→“삭제되었거나 없는 모집” | 요약 카드 + CTA. 정원마감→CTA disabled |
| A-R3 개설 폼 | 제출 중 CTA 스피너 | 해당 없음 | 필드 인라인 검증 + 제출 실패 토스트 | 성공 시 상세로 이동 |
| A-R4 신청자 목록 | LoadingView | EmptyState “아직 신청자가 없어요”(정상) | 403→“개설자만 볼 수 있어요” | 신청자 리스트 |
| A-R5 내 신청 목록 | LoadingView | EmptyState “신청한 모집이 없어요” | ErrorView | 신청 카드(상태 배지 + 취소 CTA) |
| A-R6 취소 시트 | 취소 중 CTA 스피너 | — | 422 마감후→“취소할 수 없어요”; PG오류→“환불 처리 실패, 잠시 후 다시” | 환불액 표시 후 목록 갱신 |
| A-F1 시설 상세 | 섹션별 LoadingView | program 0건→“등록된 상품이 없어요”; 휴무/운영시간 미등록→섹션 숨김 | 섹션별 ErrorView(다른 섹션 영향 없음) | 목록 + 운영시간 |
| A-F2 program 예약 | 슬롯 LoadingView | 슬롯 0건(휴무일 포함)→“예약 가능한 회차가 없어요” | ErrorView | 슬롯 선택 → 결제 이동 |
| A-P1/P2 게시판 | LoadingView | EmptyState “첫 글을 남겨보세요” | 403 PRIVATE 비멤버→잠금 상태 UI(에러 아님) | 게시글 리스트 |
| A-P3 작성 | 제출 중 스피너 | — | 403→“작성 권한이 없어요”(NOTICE 비host 포함) | 성공→목록 갱신 후 뒤로 |
| A-P4 상세+댓글 | LoadingView | 댓글 0건→“첫 댓글을 남겨보세요” | 403→잠금; 404→“없는 게시글” | 본문 + 댓글 리스트 + 입력 |
| A-P5 종목 필터 | 목록 LoadingView | 해당 종목 0건→“이 종목 글이 아직 없어요”(정상) | ErrorView | 필터된 리스트 |
| A-B1 소모임 예약 목록 | LoadingView | EmptyState “연결된 예약이 없어요” | 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/ui의 Card·EmptyState·ErrorView·LoadingView·ThemedText, components/PaymentButton.tsx. lib/community-format.ts의 getSportCategoryDisplay·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-R1 | GET /recruitments?communityId= | useRecruitments(communityId?) | 403(모임 소속·비멤버)→잠금 |
| A-R2 | GET /recruitments/{id} | useRecruitment(id) | 403→잠금, 404→없음 |
| A-R3 | POST /recruitments | useCreateRecruitment | 400 검증→인라인, 성공→상세 이동 |
| A-R4 | GET /recruitments/{id}/applications | useApplications(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-R5 | GET /recruitments(내 신청 필터) 또는 신청 내역 API | useMyApplications | ”확인 필요” 참조 |
| A-R6 | POST /applications/{id}/cancel | useCancelApplication | 422 마감후→불가, PG오류→재시도 안내 |
| A-R2(개설자) | POST /recruitments/{id}/cancel | useCancelRecruitment | 403(비개설자) |
시설상품 (program) · 시설 상세
| 화면 | 메서드·경로 | 훅 | 에러 처리 |
|---|---|---|---|
| A-F1 | GET /facilities/{facilityId}/programs | usePrograms(facilityId) | 0건→empty |
| A-F1 | 운영시간·휴무 = 시설 상세 응답에 포함(BE 임베드) → useFacilityDetail 확장 | 확장 | 미등록→섹션 숨김 |
| A-F2 | GET /facilities/{facilityId}/slots(programId 필터) → POST /bookings → /payment?orderType=BOOKING | useSlots(확장)·useCreateBooking 재사용 | 슬롯 0건→empty, 409 정원/CLOSED→“예약 불가” |
모임 게시글 (community post)
| 화면 | 메서드·경로 | 훅 | 에러 처리 |
|---|---|---|---|
| A-P2 | GET /communities/{communityId}/posts?sportCategory=&page=&size= | useCommunityPosts(communityId, criteria) | 403 PRIVATE 비멤버→잠금, 0건 정상 |
| A-P3 | POST /posts(communityId·type; 모임글은 sportCategory 상속·미전송) | useCreatePost(확장) | 403 비멤버/NOTICE 비host |
| A-P4 | GET /posts/{id}(X-User-Id) + GET /posts/{postId}/comments(X-User-Id) | usePost(확장)·useComments(신규) | 403→잠금 |
| A-P4 | POST /posts/{postId}/comments | useAddComment(신규) | 403 비멤버 |
| A-P5 | GET /posts?sportCategory=&communityId= | usePosts(확장, criteria) | 종목 0건 정상 |
소모임 예약 (community↔booking)
| 화면 | 메서드·경로 | 훅 | 에러 처리 |
|---|---|---|---|
| A-B1 | GET /communities/{communityId}/bookings | useCommunityBookings(communityId) | 403 비멤버→잠금, 0건 정상 |
| A-B2 | 시설 slot 선택(GET /facilities/{id}/slots) → POST /communities/{communityId}/bookings{slotId} | useSlots 재사용·useLinkCommunityBooking | 403(비방장) |
결제 흐름 재사용 결정 (모집 신청)
BE 계약: POST /recruitments/{id}/applications → paymentId/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 | #FFFFFF | CTA 텍스트 |
| 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.enabled | EXPO_PUBLIC_RECRUITMENT_ENABLED | false | A-R1~R6 진입점·탭 |
facility.program.enabled | EXPO_PUBLIC_FACILITY_PROGRAM_ENABLED | false | A-F1 program 섹션·A-F2 |
community.post.enabled | EXPO_PUBLIC_COMMUNITY_POST_ENABLED | false | A-P1~P5 게시판·종목 필터 |
community.booking.enabled | EXPO_PUBLIC_COMMUNITY_BOOKING_ENABLED | false | A-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개 |