시설 전국 확장·대기질 연동 — FE 설계 (모바일 앱)

Background

근거 PRD: /Users/biuea/Desktop/dpdpdndn/프로젝트/스포츠앱/시설 전국 확장·대기질 연동/PRD.md 근거 BE TDD: /Users/biuea/Desktop/dpdpdndn/프로젝트/스포츠앱/시설 전국 확장·대기질 연동/TDD.md

이 문서는 B2C 모바일 앱(mobile/, React Native + Expo Router)의 FE 설계다. FE 범위는 다음이다.

  • FR-7/FR-12: 모바일 시설 상세(mobile/app/facility/[id]/index.tsx)에 시/도 표시 + 대기질(PM10/PM2.5 수치·등급 배지) 정보성 노출. 4상태 + 실패 폴백.
  • FR-13: 예약 생성(mobile/app/booking/new.tsx)에서 대상 시설 대기질 대표 등급이 “나쁨(BAD)” 이상이면 예약 확정 전 경고 + 사용자 확인 단계(확인 버튼). 확인 시 정상 진행(차단 없음).
  • FR-14: 대기질 조회 실패·타임아웃 시 예약 화면은 “대기질 정보를 불러올 수 없습니다” 폴백 + 경고 없이 정상 진행.

Overview

  • 무엇을: 모바일 시설 상세에 시/도·대기질 카드를 얹고, 예약 화면에 대기질 경고·확인 게이트를 넣는다.
  • : 야외 시설 예약자가 당일 대기질을 확인하고, 나쁠 때 인지한 뒤 예약하도록 한다(차단 없이 정보·확인만).
  • 어떻게: 기존 모바일 관례(TanStack Query v5 + lib/use*.ts 훅 + api/*.ts 함수 + axios be-client)를 그대로 따른다. 대기질은 BE 경유로만 호출한다(외부 API 직접 호출 금지 규칙 rules/fe-external-api-via-was.md). 모바일에는 테마 시스템이 전무하므로(모든 색 하드코딩), 신규 컴포넌트가 소비할 경량 테마 토큰 모듈 + useTheme()을 도입해 라이트/다크를 지원한다(no-single-mode 준수). 기존 화면은 이번 범위에서 리팩터하지 않는다(선기술 부채).

Terminology

용어정의
대표 등급PM10·PM2.5 중 더 나쁜 등급 (BE representativeGrade)
BAD 이상BAD 또는 VERY_BAD — 예약 경고 트리거
useTheme신규 도입 테마 훅. useColorScheme() 기반 light/dark 토큰 객체 반환
AQ 토큰대기질 등급 시맨틱 색 토큰 (테마 객체의 airGood·airBad 등)

Define Problem

AS-IS (실제 코드 근거)

  • 시설 상세: mobile/app/facility/[id]/index.tsxuseFacilityDetail(facilityId)(lib/useFacility.ts, useQuery(['facilities', id]))로 조회. gu 표시(:56-59 "구" + data.gu). loading/error/success 3상태(empty 미분기). 대기질 코드 전무. 하단 고정 “예약하기” 버튼(:79) → /booking/new?facilityId=.
  • 예약 생성: mobile/app/booking/new.tsxuseLocalSearchParams<{facilityId}>()(:68)로 facilityId만 수신. useSlots(facilityId) + useCreateBooking(). handleBook(:78-102)에서 슬롯 선택 검증 → createBooking(...) → 성공 시 /payment 이동. 4상태 처리됨.
  • 좌표: BE는 이미 노출, 모바일 TS 타입만 누락(핵심 이슈): BE backend/.../presentation/facility/dto/response/FacilityResponse.kt:11-12val lat: Double·val lng: Double이미 존재하고, 모바일 경로 FacilityApiController.kt:51 @GetMapping("/{id}")가 이 DTO를 반환한다 — 즉 서버는 좌표를 이미 내려준다. 결핍은 모바일 TS 타입뿐이다: mobile/api/types.ts FacilityResponse(:133-141)가 id/name/gu/type/address/parking/phone만 선언(lat/lng 없음, telphone 오기). SlotResponse(:104-111)에도 좌표 없음. → 대기질 GET /air-quality?lat&lng 호출에 필요한 위경도는 BE 변경 없이 모바일 TS 타입에 lat/lng를 추가(FE-11 소관) 하면 두 대상 화면 모두 확보된다. (좌표 포함 별도 타입 api/external-features.tsFacilitySummary/facilities/near 전용·필드 상이 — 이번엔 미사용.)
  • API 흐름: api/be-client.ts axios 싱글턴(BFF 없이 BE 직접, Authorization: Bearer 자동, 401 refresh). 엔드포인트 함수는 api/*.ts(예 getFacility(id)GET /facilities/{id}). 훅은 lib/use*.tsuseQuery/useMutation 얇게 래핑, queryKey 배열·enabled 가드·staleTime 상속.
  • 테마 전무: useTheme/useColorScheme 사용처 0건. 모든 색 StyleSheet.create 하드코딩(primary #007AFF, danger #FF3B30/#B00020, bg #F2F2F7 등). 시맨틱 토큰·라이트/다크 없음.
  • 상태관리: Zustand는 lib/auth.ts 세션 스토어 1개만. 테마 스토어 없음.
  • 테스트: Jest + @testing-library/react-native. components/**만 커버리지 수집(app/** 미수집). 접근성 셀렉터(accessibilityLabel/accessibilityRole) 광범위.
  • 타입: api/types.ts 단일 파일. 웹과 공유 안 함(모노레포 아님).

TO-BE

  • 시설 상세: gu 옆 시/도 표시 + AirQualityCard(PM 수치·대표 등급 배지, 4상태·UNKNOWN 폴백).
  • 예약 화면: 대상 시설 좌표로 대기질 조회 → BAD 이상이면 AirQualityWarning(경고 배너 + 확인 버튼) 게이트. 확인 후 예약 진행. 조회 실패 시 경고 없이 진행(FR-14).
  • 좌표 확보: BE 변경 불필요 — BE FacilityResponse는 이미 lat/lng를 노출한다. 모바일 TS 타입에 lat/lng를 추가(FE-11)하면 상세는 data.lat/lng, 예약은 useFacilityDetail(facilityId) 재사용으로 좌표를 획득한다.
  • 테마: 경량 theme/ 모듈(light/dark 토큰 + useTheme()) 신규. 신규 컴포넌트만 소비.

Architecture Benchmarking (토스 벤치마킹)

대상참고 패턴적용미참고
토스 — 확인 흐름되돌릴 수 없거나 주의가 필요한 행동 전 명확한 1스텝 확인(경고 문구 + 단일 CTA)예약 확정 전 대기질 BAD면 경고 배너 + “확인하고 예약” 버튼 1개. 차단 아님, 인지 후 진행모달 남발·다중 스텝 없음
토스 — 정보 카드부가 정보는 절제된 카드, 상태색 1곳상세 대기질 카드, 배지 색 1곳본체와 뒤섞지 않음
에어코리아 CAI4단계 색 위계배지 색 위계 AQ 토큰예보 그래프(범위 밖)
네이버 지도 결합 카드실패해도 본체 정상대기질 실패 시 카드만 폴백, 예약은 정상 진행강제 차단 없음

Possible Solutions

방안설명왜 채택 / 미채택
좌표: 모바일 TS 타입에 lat/lng 추가 (채택, BE 변경 불필요)BE FacilityResponse가 이미 lat/lng를 노출(FacilityResponse.kt:11-12) → 모바일 타입만 추가(FE-11)채택 — 상세는 data.lat/lng 바로 사용, 예약은 useFacilityDetail 재사용으로 획득. 두 화면 일관. FR-12/13 모두 좌표 필요. 서버 계약 변경 0
air-quality API가 facilityId 수용GET /air-quality?facilityId=미채택 — BE가 이미 좌표를 내려주므로 불필요. BE TDD 계약이 lat/lng 기반이라 그대로 사용
gu 기반 조회gu 문자열로 대기질미채택 — gu는 측정소 좌표 매핑 부정확, BE Gateway가 좌표 기반. 부정확
예약 경고: 클라이언트 UX 게이트 (채택)대기질 조회는 조회 전용 API, 경고/확인은 화면 로직채택 — FR-13 “경고까지만·차단 없음”에 부합. 예약 UseCase 무결합(BE TDD와 일치)
예약 API에 대기질 결합서버가 경고 플래그 반환미채택 — 예약이 외부 API 장애에 결합. BE TDD도 조회 전용 분리 채택
테마: 경량 신규 모듈 (채택)신규 컴포넌트만 useTheme 소비, 기존 화면 유지채택 — no-single-mode 충족하며 전면 리팩터 회피(범위 통제). 향후 테마 확산의 씨앗
전 화면 테마 마이그레이션모든 하드코딩 색 토큰화미채택 — 범위 초과·대규모. 이 과제 밖

Detail Design

화면 목록 · 변경 범위

화면/모듈파일변경
시설 상세mobile/app/facility/[id]/index.tsx시/도 표시 + AirQualityCard 통합
예약 생성mobile/app/booking/new.tsx대기질 조회 + AirQualityWarning 게이트
테마신규 mobile/theme/tokens.ts, mobile/theme/useTheme.tslight/dark 토큰 + 훅
대기질 데이터신규 mobile/api/air-quality.ts, mobile/lib/useAirQuality.ts, mobile/api/types.ts(AirQualityResponse + FacilityResponse lat/lng), 신규 mobile/lib/air-quality-format.tsapi·훅·타입·라벨 매핑
컴포넌트신규 mobile/components/AirQualityBadge.tsx, AirQualityCard.tsx, AirQualityWarning.tsx재사용 UI

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

시설 상세 (토스: 부가 정보 카드)

┌───────────────────────────┐
│ ← 뒤로                     │
│ 한강 축구장                │  data.name
│ 시/도  서울특별시          │  ← 신규
│ 구     광진구              │  기존
│ 유형   야외                │
│ 주소   서울 광진구 …       │
│ ┌─ 현재 대기질  [나쁨] ─┐ │  ← AirQualityCard (배지 색 1곳)
│ │ PM10   92 ㎍/㎥        │ │
│ │ PM2.5  41 ㎍/㎥        │ │
│ │ 광진구 측정소·14:00    │ │
│ └───────────────────────┘ │
│        [ 예약하기 ]        │  하단 고정
└───────────────────────────┘

예약 생성 — 대기질 경고 (토스: 확인 1스텝)

┌───────────────────────────┐
│ 예약하기                   │
│ ┌ 대기질 경고 (BAD 이상) ┐ │  ← AirQualityWarning (조건부)
│ │ ⚠ 현재 대기질이 나쁨    │ │
│ │   입니다 (PM10 92).     │ │
│ │   야외 활동에 유의하세요.│ │
│ └────────────────────────┘ │
│ 슬롯 선택                  │
│  ○ 09:00–10:00            │
│  ● 10:00–11:00            │
│ 결제수단  ● 카드 ○ …      │
│  [ 확인하고 예약 ]  ← BAD면 │  경고 확인 게이트
│  [ 예약 진행 ]     ← 그 외 │
└───────────────────────────┘
  • 대기질 UNKNOWN/실패 → 경고 배너 미표시, 버튼은 기본 “예약 진행”(FR-14).
  • 확인 게이트: BAD 이상일 때만 버튼 라벨이 “확인하고 예약”으로 바뀌고, 첫 탭에서 경고를 인지시키는 확인 상태로 전환(내부 confirmed 플래그) 후 예약 mutation 실행. 별도 모달 없이 인라인 확인(토스식 단순화). — 인터랙션 확정은 사용자 확인 대상(체크박스 vs 버튼 라벨 전환).

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

화면loadingemptyerrorsuccess
시설 상세”로딩 중” 스피너시설 미존재 → “시설을 찾을 수 없습니다”(신규 empty 분기 추가)“오류 발생” + 메시지시설 정보(시/도 포함) + 대기질 카드
대기질 카드(상세 내)“대기질 정보를 불러오는 중…“pm 모두 null → 폴백 문구조회 실패 → “대기질 정보를 불러올 수 없습니다”수치 + 배지 + 측정소·시각
예약 생성슬롯 로딩 스피너슬롯 0건 → “예약 가능한 시간이 없습니다”슬롯 조회 에러슬롯·결제수단 선택 + (조건부)경고
대기질 경고(예약 내)조회 중 배너 미표시(기본 버튼)UNKNOWN → 배너 미표시실패 → 배너 미표시·정상 진행(FR-14)BAD 이상 → 경고 배너 + 확인 게이트

테마 토큰 — 대기질 등급 (라이트/다크 매핑, 의무)

mobile/theme/tokens.ts에 light/dark 두 객체를 정의하고 useTheme()useColorScheme() 기준으로 반환. 신규 컴포넌트는 하드코딩 색 대신 이 토큰만 사용.

토큰 키용도lightdark
background화면 배경#F2F2F7#000000
surface카드 배경#FFFFFF#1C1C1E
textPrimary본문#1C1C1E#F2F2F7
textSecondary보조#8E8E93#98989F
border경계선#E5E5EA#38383A
accent강조/CTA#007AFF#0A84FF
danger위험/에러#FF3B30#FF453A
airGoodBg / airGoodFg좋음 배지#E6F0FF / #1565C0#10243D / #7FB3FF
airModerateBg / airModerateFg보통#E4F6EA / #1B7A3D#122A1B / #6FD08A
airBadBg / airBadFg나쁨#FFF0E0 / #C65A00#2E1B08 / #FFB166
airVeryBadBg / airVeryBadFg매우나쁨#FDE4E4 / #C62828 /#2E1212 / #FF8A8A
airUnknownBg / airUnknownFg정보없음#ECEEF0 / #6B7280#26282B / #9AA0A6

색 위계: 좋음=파랑, 보통=초록, 나쁨=주황, 매우나쁨=빨강(에어코리아 CAI). 기존 하드코딩 팔레트(primary #007AFF, danger #FF3B30)와 일관되게 accent/danger 토큰 채택. useColorScheme 기본, 향후 사용자 오버라이드 여지.

등급 → 라벨 매핑 (mobile/lib/air-quality-format.ts)

BE가 등급 계산 → FE는 라벨·토큰키만 매핑(순수 함수). 색 자체는 컴포넌트가 useTheme로 해석.

grade (BE)라벨토큰 키 접두
GOOD좋음airGood
MODERATE보통airModerate
BAD나쁨airBad
VERY_BAD매우나쁨airVeryBad
UNKNOWN정보없음airUnknown

isBadOrWorse(grade): grade === 'BAD' || grade === 'VERY_BAD' — 예약 경고 트리거 판정(순수 함수, 테스트 대상).

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

app/facility/[id]/index.tsx (컨테이너)
├─ useFacilityDetail(id)        ← 기존 훅 (lat/lng 포함 응답)
├─ useAirQuality(lat, lng)      ← 신규 훅
├─ 시설 정보 rows (시/도 추가)
└─ <AirQualityCard status pm10 pm25 grade stationName measuredAt/>
     └─ <AirQualityBadge grade/>   (useTheme 소비)

app/booking/new.tsx (컨테이너)
├─ useFacilityDetail(facilityId)  ← 좌표 획득용 재사용
├─ useAirQuality(lat, lng)
├─ useSlots / useCreateBooking     ← 기존
├─ <AirQualityWarning grade pm10 onConfirm/> (조건부, BAD 이상)
└─ 예약 버튼 (경고 확인 게이트)

재사용 컴포넌트: AirQualityBadge(카드·경고 공용), AirQualityCard(상세), AirQualityWarning(예약).

상태관리 설계

상태저장근거
시설/슬롯/대기질 서버 데이터TanStack Query (useFacilityDetail, useSlots, useAirQuality)레포 표준. 서버 상태는 Query 캐시가 SSOT (no-global-by-default)
예약 경고 확인 여부(confirmed)화면 useState화면 지역 상태. 전역 불필요
선택 슬롯·결제수단화면 useState(기존)기존 유지
테마 모드useColorScheme()(RN 시스템)전역 스토어 불필요. 시스템 값 소비
전역 클라이언트 상태신규 없음세션(auth) 외 전역 승격 근거 없음

API 연동 표

화면엔드포인트(BE 직접)api 함수 / 훅에러 처리
시설 상세GET /facilities/{id}getFacility / useFacilityDetail (기존, lat/lng 포함)기존 3상태 + empty 추가
대기질GET /air-quality?lat&lng신규 getAirQuality(lat,lng) / useAirQuality(lat,lng)실패/UNKNOWN → 폴백. enabled: lat!=null&&lng!=null
예약 좌표GET /facilities/{facilityId}useFacilityDetail(facilityId) 재사용실패 시 대기질 미조회 → 경고 없이 진행
예약 생성POST /bookingscreateBooking / useCreateBooking (기존)기존

대기질 호출은 BE 경유만(외부 직접 호출 금지 규칙). useAirQuality는 좌표가 확정되기 전(lat/lng null) 미조회.

라우팅 · 내비게이션 흐름

flowchart LR
    Search[검색/딥링크] --> Detail[시설 상세]
    Detail --> Aq1[대기질 카드]
    Detail --> Booking[예약 생성]
    Booking --> Coord[시설 재조회 좌표]
    Coord --> Aq2[대기질 조회]
    Aq2 --> Warn[BAD 경고·확인]
    Warn --> Create[예약 생성]
    Create --> Payment[결제]

Testing Plan (implementer TDD 입력)

Jest + @testing-library/react-native. 접근성 셀렉터 사용. 재사용 컴포넌트(components/**)는 커버리지 수집 대상 → 로직을 컴포넌트로 추출. 테스트명에 티켓ID 금지.

대상레벨핵심 케이스
air-quality-format유닛등급별 라벨·토큰키, isBadOrWorse(BAD/VERY_BAD true, GOOD/MODERATE/UNKNOWN false), 미지정 grade 방어
useThemelight/dark 토큰 반환, colorScheme 변경 반영
AirQualityBadge컴포넌트등급별 라벨·토큰 적용, UNKNOWN 배지 미표시
AirQualityCard컴포넌트success 수치·배지·측정소, loading 문구, UNKNOWN/실패 폴백, pm 일부 null
AirQualityWarning컴포넌트BAD 이상 경고 문구·확인 버튼 렌더, onConfirm 콜백, UNKNOWN·GOOD이면 미렌더
useAirQuality성공 반환, 실패 시 폴백 상태, 좌표 null이면 미조회
시설 상세시나리오시/도 표시, 대기질 카드 success/폴백, empty 분기
예약 생성시나리오BAD면 경고+확인 게이트 후 예약, UNKNOWN이면 경고 없이 예약, 실패 시 정상 진행(FR-14), 좌표 없으면 경고 없이 진행

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

  • FE는 BE 좌표 필드·/air-quality 배포 후 활성. 좌표 없으면(구 응답) useAirQuality 미조회 → 대기질·경고 미표시로 자연 폴백(앱 정상).
  • 롤백: 대기질 카드·경고를 조건부 렌더 제거 또는 엔드포인트 미호출로 즉시 비활성(BE TDD 롤백의 “FE 플래그”에 해당). 예약 흐름은 대기질과 무결합이라 무영향.

웹/앱 공유 로직 경계 (양 문서 동일)

항목공유 여부비고
타입(AirQuality 응답, grade enum)공유 안 함(각 플랫폼 정의)web/·mobile/는 별도 패키지·모노레포 미구성(mobile tsconfig 내부만 매핑). 웹은 lib/portal/, 모바일은 api/types.ts에 각자 정의
등급 → 라벨/색 매핑공유 안 함(각 정의)BE가 등급 계산 → FE는 소량 매핑만. 웹은 Tailwind 토큰 클래스, 모바일은 useTheme 토큰 객체로 상이
시/도 옵션(17건)공유 안 함(각 상수)값(코드·명칭)은 동일해야 하며 BE regions seed가 원천. 웹은 sido-options.ts, 모바일은 이번 범위에 시/도 입력 없어 미사용(표시만)
API 클라이언트공유 안 함웹=BFF Route+fetch, 모바일=axios be-client 직접. 구조 자체가 다름
컴포넌트공유 안 함(플랫폼 전용)웹 React DOM vs RN. 강제 공유 안 함
BE API 계약(엔드포인트·필드)공유(SSOT=BE TDD)양 플랫폼이 동일 계약 소비. 필드·타입은 BE TDD와 일치

API 계약 — 소비 / 역제안

소비(BE TDD·기존 코드 기준)

  • GET /air-quality?lat&lng{ pm10, pm25, pm10Grade, pm25Grade, representativeGrade, stationName, measuredAt } (실패 시 200 + UNKNOWN·null).
  • GET /facilities/{id} FacilityResponse — lat/lng는 이미 노출(FacilityResponse.kt:11-12). 지역 필드(sidoCode, sidoName, sigunguCode, sigunguName)는 BE-06이 공유 FacilityResponse에 추가.

역제안(BE에 요청)

  • 좌표: BE 변경 불필요 — FacilityResponse가 이미 lat/lng를 노출하므로 서버 계약 추가 요청 없음. 모바일 TS 타입에 lat/lng·tel(현재 phone 오기) 정합만 FE-11에서 해소.
  • 지역 필드(sidoName 등)는 BE-06이 공유 FacilityResponse에 추가하므로 별도 역제안 불필요 — 모바일 타입에 반영만(FE-11, FR-7 표시용).

Open Questions

  • 예약 경고 확인 인터랙션: “확인하고 예약” 버튼 라벨 전환(인라인) vs 체크박스(“대기질이 나쁨을 확인했습니다”) vs 모달 — 현행 설계는 인라인 버튼 전환. 사용자 확인 대상.
  • 좌표: BE FacilityResponse가 이미 lat/lng 노출 → BE 조율 불필요(모바일 타입 정합만 FE-11). 미해결 없음.

Document History

날짜변경 내용
2026-07-04최초 작성 — AS-IS 코드 근거, 경량 테마 모듈 도입, 예약 경고 인라인 확인 게이트, AQ 토큰 라이트/다크 매핑, 티켓 7건 분해
2026-07-04senior-pm 검증 정정 — 좌표 AS-IS 오류 수정: BE FacilityResponse(FacilityResponse.kt:11-12)가 이미 lat/lng 노출, 결핍은 모바일 TS 타입뿐. “역제안(BE 필수 lat/lng 추가)” 삭제→“BE 변경 불필요, FE-11 타입 추가만”. 티켓 무변경