개발

오늘수영: 디자인 시스템 활용 & 문제 해결 기록

작성일: 2026년 06월 01일·...

1. 활용한 디자인 시스템

1.1 토큰 체계

예전 팀프로젝트의 컬러 시스템을 차용하고 키컬러를 서비스 성격에 맞게 변경했습니다. Figma Foundations 을 통해 미니멀한 디자인 시스템을 구축하고 코드 레벨에서는 두 파일로 분산하여 관리합니다.

위치역할
app/globals.css @themeTailwind v4 유틸리티 생성용 (bg-primary, text-now-open-ink 등)
constants/tokens.tsJS 참조용 미러 + statusToToken() 매핑

핵심 토큰:

  • Primary(워터블루): default #0E7C86 · strong #0A5F67 · blue #2E9BD6 · 10 #E5F4F5 · 5 #F2FAFA
  • 상태(앱 핵심 규약): now-open #2DB16B · upcoming #F0A231 · closed #9797A0 · error #C80000
  • 뱃지용 파생: -soft(10% 틴트 배경) + -ink(AA 대비 텍스트)
  • 타이포: h1 28 / h2 24 / h3 20 / body 16 / sm 14 / xs 12 / micro 10, Pretendard→Noto Sans KR 폴백, tracking −4%
  • 라운드: 버튼 8 / 인풋 12 / 시트 14
  • 설계 원칙: 상태 컬러 = 비즈니스 로직과 1:1. getPoolNowStatus()의 반환(open/soon/closed)이 디자인 상태 토큰(now-open/upcoming/closed)에 그대로 대응되어, 로직과 디자인이 같은 언어를 쓴다.

    1.2 컴포넌트 시스템

    Figma Components 페이지의 주요 컴포넌트를 코드로 1:1 구현했습니다.

    Figma 컴포넌트코드비고
    Button (solid/outline/medium/disabled)components/ui/Button.tsxbuttonClass() 헬퍼로 <a>/<Link>에도 동일 스타일 적용
    StatusBadge (now-open/upcoming/closed)components/ui/StatusBadge.tsxNowStatus → 틴트 pill
    FilterChip (selected/default)components/ui/Chip.tsx필터·제보사유 공용
    FreshnessTagcomponents/ui/FreshnessTag.tsx"✓ YYYY.MM.DD 확인"
    PoolCardcomponents/home/PoolCard.tsx
    TabBar (홈/강습알림/더보기)components/layout/TabBar.tsx
    BottomSheetcomponents/report/ReportSheet.tsx제보 시트
    Togglecomponents/ui/Toggle.tsx강습 알림·설정 공용
  • 아이콘: lucide-react 라이브러리 활용
  • 1.3 동시 작업 프로토콜 (Figma ↔ 코드)

    토큰·로직·라우트는 디자인과 독립적으로 선행 구현하고, 화면 디자인이 완료되면 노드별 컨텍스트를 요청해 마크업과 스타일만 바인딩했습니다. 덕분에 디자인 진행 속도에 코드가 묶이지 않고 재작업율을 최소화했습니다.


    2. 해결한 문제

    2.1 데이터 수급

  • 문제: 자유수영/강습 시간표·요금은 공공데이터포털·경기데이터드림 어디에도 없습니다(위치·제원·일반 운영시간까지만).
  • 해결: 하남도시공사 hanamsport.or.kr의 정형 HTML을 크롤링해 4개 시설(미사/풍산/덕풍/감일)의 자유수영 37세션 + 강습 18개를 추출, 공통 스키마로 정규화(data/pools.json).
  • 정규화 포인트: 요금은 4곳 동일 → full/half 공통 요금테이블 1개로, 세션은 tier만 참조. 일요일 운영 주차 차이는 weeksOfMonth로, 시설별 변칙(감일=평일 없음, 풍산=금요일 분리)은 세션 단위로 흡수.
  • 2.2 데이터 검증

  • 문제: 초기 라벨 "하남국민체육센터(미사)"의 권역 정확도 의심.
  • 해결: 네이버 플레이스로 4곳 행정동·정식명·좌표 교차검증. 결과 권역 라벨은 정확(망월동=미사강변)했고, 정식명은 "하남종합운동장 국민체육센터"로 정정. 검증된 좌표를 JSON에 직접 저장 → 런타임 지오코딩 의존 제거.
  • 2.3 스코프 정의

  • 문제: 사설 수영장도 넣어야 하나?
  • 해결: 네이버 플레이스 조사 결과 하남 사설 10곳이 전부 어린이 강습/1:1 전용, 자유수영 미제공('하남 자유수영' 검색 0건). → 자유수영은 공공 4곳이 사실상 전부임을 데이터로 확인하고, 사설은 data/private-pools.json에 분리 기록(향후 별개 기능 검토용)
  • 2.4 지금 상태

  • 문제: 자유수영 여부를 실시간으로 제공
  • 해결: dayjs(utc+timezone)로 Asia/Seoul 고정. dayjs를 사용하여 홈/상세에서 클라이언트 1분 라이브 갱신으로 사용자 시계 기준 실시간성 확보. "지금 상태 우선" 정렬로 핵심 가치를 상단에 노출.
  • 2.5 URL Fallback

  • 문제: not-found.tsx의 서버 redirect()가 Next에서 동작하지 않고 404를 그대로 반환(실측 확인).
  • 해결: 루트 catch-all 라우트(app/[...slug]/page.tsx)에서 서버 redirect('/')(307) + 상세 페이지의 잘못된 id도 redirect('/') + not-found.tsx는 클라이언트 안전망
  • 2.6 재사용성·클린코드 리팩토링

  • 문제: 버튼/칩/포맷 로직이 화면마다 인라인 중복.
  • 해결: 정본 컴포넌트 기준으로 Button(+buttonClass), Chip, Toggle 추출 및 lib/format.ts(formatWon, tierLabellib/cn.ts로 공통화. FilterChips·ReportSheet·Detail CTA·FeeCard·PoolCard·DaySchedule를 공용 조각으로 재구성.
  • 2.7 데이터 정직성 원칙

    분기·시즌 변동이 잦은 도메인이라 신선도를 숨기지 않고 노출("✓ YYYY.MM.DD 확인", "데이터 기준 안내"). 검증되지 않은 값(예: 강습 등록일 D-day)은 날조하지 않고 "시설 확인" 등으로 표기 — 위치/정보 앱의 신뢰가 곧 제품 가치이기 때문.


    3. 결과물 구조 (요약)

    oneul-swim/
    ├── app/            page(홈)·pool/[id](상세)·lessons·more·manifest·[...slug]·not-found
    ├── components/
    │   ├── ui/         Button·Chip·Toggle·StatusBadge·FreshnessTag·icons
    │   ├── home/       HomeHeader·HomeClient·FilterChips·PoolCard
    │   ├── pool/       DaySchedule·FeeCard
    │   ├── report/     ReportSheet(BottomSheet)
    │   └── layout/     TabBar
    ├── lib/            pools(전처리)·types·time(dayjs)·format·cn
    ├── constants/      tokens (Foundations 미러)
    └── data/           pools.json · private-pools.json

    4. 남은 작업

  • 화면: More 더보기, Home 상태(로딩/지금0곳/위치권한거부), Map 지도뷰(Kakao SDK — JS 키 필요)
  • 기능: useGeolocation 거리 정렬, 제보 백엔드(Tally/Supabase), 강습 등록일 데이터(예약시스템 연동), 위례 복합체육시설(공공 5번째) 개관 후 추가