시설 운영시간·휴무·시설상품·슬롯 제어 — FE 설계 (웹 / Next.js 운영 포털)

Background

근거 PRD: 스포츠앱/모임·커뮤니티/20260706-모집-시설상품-소모임예약연동-prd.md (그룹 B — 시설상품·운영시간·자동슬롯 FR-7~12). 근거 BE TDD (API 계약 소비): 20260707-모집-시설상품-소모임예약연동-tdd.md “REST API 계약” 표 (facility 6 엔드포인트).

이 문서는 웹 운영 포털(web/ — Next.js App Router, 시설 운영자·관리자용) 의 FE 설계다. 소비자용 화면(모집·program 예약·게시글·소모임 예약)은 앱이 담당하며 20260707-...-design-fe-app.md가 다룬다. 이 문서는 운영자 전용 기능만 다룬다:

  • 요일별 운영시간 등록 (FR-7)
  • 휴무일 추가/해제 (FR-8)
  • 시설상품(program) 등록·목록 (FR-11)
  • 자동 생성된 슬롯 수동 마감/오픈 (FR-10)

자동 슬롯 생성(FR-9)은 BE @Scheduled 배치라 FE 화면 없음.

Overview

기능담당 화면재사용 기반
운영시간 등록시설 상세 내 “운영시간” 탭/섹션app/portal/facilities/[id]/page.tsx 확장 + 신규 lib/portal/operatingHours.ts + /api/portal/facilities/[id]/operating-hours BFF
휴무일 관리시설 상세 내 “휴무일” 섹션동상 + lib/portal/holidays.ts
시설상품(program) 등록·목록시설 상세 내 “시설상품” 탭신규 lib/portal/programs.ts + /api/portal/facilities/[id]/programs BFF
슬롯 open/close기존 app/portal/slots/page.tsx 확장lib/portal/slots.ts에 close/open 추가

Terminology

용어정의
OperatingHours요일별 운영시간 (오픈·마감 시각, 브레이크타임, 슬롯단위, 정원)
Holiday시설 휴무일 (특정 날짜, 슬롯 미생성)
Program시설상품 (PT·클래스) — 이름·설명·가격·정원·소요시간
SlotStatus슬롯 예약 가능 상태 — OPEN(예약 허용) / CLOSED(신규 예약 차단, 기존 예약 유지)

Define Problem

AS-IS (실제 코드 근거)

사실근거
시설 CRUD·슬롯 CRUD 존재app/portal/facilities/{page,[id]/page,new/page}.tsx + lib/portal/facilities.ts(create/list/get/update/delete). app/portal/slots/page.tsx 슬롯 달력 + lib/portal/slots.ts(fetchSlots/createSlot/updateSlot/deleteSlot)
운영시간·휴무·program·슬롯 상태 제어 UI 전무grep -rniE "operating|holiday|program|휴무|운영시간" web/{app,lib} → 0건. lib/portal/slots.ts에 open/close 없음
BFF·훅·검증 패턴 정착컴포넌트 → fetch('/api/portal/*')(Next Route Handler BFF) → lib/server/be-client.ts(Bearer 쿠키·5s 타임아웃). 도메인 함수는 lib/portal/*.ts, 응답은 zod 스키마(lib/portal/schemas.ts)로 파싱. 훅은 lib/portal/use*.tsuseState/useEffect/fetch (TanStack Query 미도입 — 레포 정착 관례)
shadcn/ui + 시맨틱 토큰 정착components/ui(button/dialog/tabs/toast/input/badge…). 토큰은 app/globals.css CSS 변수(:root 라이트 / .dark 다크) + tailwind.config.ts darkMode:["class"]
다크 모드 토큰은 있으나 토글 미배선.dark 블록·토큰 완비. 그러나 next-themes 없음, document.documentElement.classListdark를 세팅하는 코드 0건, layout.tsx가 클래스 로직 없이 <html lang="ko"> 렌더
zod·strict 정착tsconfig strict+noUncheckedIndexedAccess. zod ^4 응답 검증

TO-BE

  • 시설 상세(facilities/[id])에 탭(정보 / 운영시간 / 휴무일 / 시설상품) 추가.
  • 슬롯 화면에 status 표시 + open/close 액션.
  • 신규 BFF Route Handler 4종(operating-hours PUT / holidays POST·DELETE / programs POST·GET / slots close·open PATCH).
  • 다크 모드 토글 배선 — 토큰은 있으나 스위치가 없어 “한 모드만 사용 가능”한 상태. 라이트/다크 의무 준수를 위해 .dark 클래스 토글(localStorage 영속 + 시스템 기본)을 최소 배선(선행 티켓).

Architecture Benchmarking (토스 벤치마킹)

운영자 화면도 토스 어드민 톤을 벤치마킹한다.

화면참고 토스 패턴
운영시간 등록토스 설정 폼 — 요일별 행, 시각 인풋 쌍(오픈~마감), 브레이크 추가는 인라인 ”+ 추가”, 절제된 회색 위계
휴무일토스 캘린더 선택 — 날짜 칩 리스트 + 추가/삭제, 단일 accent
시설상품토스 상품 등록 — 가격 위계 최상단, 정원·소요시간 보조 필드, 하단 단일 저장 CTA
슬롯 제어토스 리스트 토글 — 각 슬롯 행에 OPEN/CLOSED 상태 배지 + 단일 토글 액션, 확인 다이얼로그

Detail Design

플랫폼 경계 (앱과의 공유)

web/(운영 포털)와 mobile/(소비자 앱)은 별도 앱이다. 유일한 공유 계약은 BE API 계약 필드다 — 웹은 lib/portal/types.ts에 계약을 미러링하고(zod 스키마 검증), 앱은 자체 api/types.ts에 미러링한다. 코드·컴포넌트·훅 공유 없음. 운영시간·휴무·program·슬롯 상태의 표시(읽기)는 앱 시설 상세(A-F1)에도 나타나지만, 편집은 웹 전용이다 (기능별 담당 플랫폼 표는 app 설계 문서 참조).

화면 목록 (웹)

ID화면경로신규/확장
W-OH운영시간 등록app/portal/facilities/[id] 내 “운영시간” 탭확장
W-HD휴무일 관리app/portal/facilities/[id] 내 “휴무일” 섹션확장
W-PG시설상품 목록·등록app/portal/facilities/[id] 내 “시설상품” 탭확장
W-SL슬롯 open/closeapp/portal/slots/page.tsx 확장확장
W-TH다크 모드 토글 배선app/layout.tsx + components/ui/ThemeToggle.tsx신규(선행)

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

W-OH 운영시간 — 요일별 행

운영시간
┌ 월 ─────────────────────────────┐
│ [06:00] ~ [22:00]   슬롯 [60]분   │
│ 정원 [10]  브레이크 [12:00~13:00] [+]│
├ 화 ─────────────────────────────┤
│ ...                               │
└──────────────────────────────────┘
        [ 저장 ]  ← 단일 CTA(accent)

W-HD 휴무일

휴무일
[ 7/15(화) ✕ ] [ 7/22(화) ✕ ]   ← 칩 + 삭제
[ + 날짜 추가 ]  → 날짜 선택 다이얼로그

W-PG 시설상품

시설상품                    [ + 상품 등록 ]
┌────────────────────────────────┐
│ PT 1:1        50,000원          │  ← 가격 위계 최상단
│ 정원 1 · 60분        [수정]      │
├────────────────────────────────┤
│ 필라테스 그룹  30,000원          │
│ 정원 6 · 50분        [수정]      │
└────────────────────────────────┘
등록 다이얼로그: 이름·설명·가격·정원·소요시간 + [저장]

W-SL 슬롯 open/close (기존 달력/리스트에 상태 추가)

7/12(토)
┌────────────────────────────────┐
│ 14:00~15:00  정원 2/8  [OPEN]   [마감]│
│ 15:00~16:00  정원 0/8  [CLOSED] [오픈]│  ← CLOSED는 회색 배지
└────────────────────────────────┘
  마감 클릭 → 확인 다이얼로그("신규 예약만 차단, 기존 예약은 유지됩니다")

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

화면loadingemptyerrorsuccess
W-OH폼 스켈레톤/스피너미등록 요일→기본값 빈 행403 비소유자→“권한 없음”, 저장 실패→toast저장 후 성공 toast
W-HD리스트 스피너”휴무일 없음”(정상)추가/삭제 실패 toast칩 갱신
W-PG리스트 스피너”등록된 상품이 없어요”조회/등록 실패 toast, 400 검증 인라인목록 갱신
W-SL달력 스피너해당 날짜 슬롯 0건open/close 실패 toast, 409 상태충돌상태 배지 즉시 갱신
W-TH해당 없음해당 없음해당 없음클릭 시 즉시 전환·localStorage 영속

컴포넌트 트리

app/portal/facilities/[id]/page.tsx (확장)
  └─ FacilityTabs [정보|운영시간|휴무일|시설상품]  (components/ui Tabs)
       ├─ OperatingHoursForm (W-OH) — useOperatingHours + updateOperatingHours
       │    └─ WeekdayRow × 7 (BreakTimeInput[])
       ├─ HolidaySection (W-HD) — useHolidays + add/removeHoliday
       │    └─ HolidayChip[] + DatePickerDialog
       └─ ProgramSection (W-PG) — usePrograms + createProgram
            └─ ProgramCard[] + ProgramFormDialog
app/portal/slots/page.tsx (확장)
  └─ SlotRow (status 배지 + closeSlot/openSlot 액션 + ConfirmDialog)
app/layout.tsx (확장) + components/ui/ThemeToggle.tsx (W-TH)

프레젠테이션 컴포넌트는 데이터 가공 없이 props 렌더. 폼 검증은 zod 스키마(lib/portal/schemas.ts 확장)로, 로직은 _hooks/로 추출(컴포넌트 내 비즈니스 로직 금지).

상태관리 설계

레포 관례 유지 — TanStack Query·Zustand 미도입. 서버 상태는 lib/portal/use*.tsuseState/useEffect/fetch 훅, 폼 상태는 지역 useState(+ zod 검증). 서버 데이터를 스토어에 복사 보관하지 않는다(관례 주석과 정합). 다크 모드만 localStorage + document.documentElement.classList 토글(전역 UI 상태, 최소 배선).

API 연동 표 (BE 계약 대조)

컴포넌트 → lib/portal/*.ts(fetch to /api/portal/*) → Next Route Handler(BFF) → lib/server/be-client.ts → BE. 응답은 zod 파싱.

화면BE 메서드·경로BFF Route + lib 함수에러 처리
W-OHPUT /facilities/{facilityId}/operating-hours/api/portal/facilities/[id]/operating-hours + updateOperatingHours403 비소유자, 400 검증→인라인
W-HDPOST /facilities/{facilityId}/holidays · DELETE .../holidays/api/portal/facilities/[id]/holidays + addHoliday/removeHoliday403, 중복 날짜 멱등
W-PGPOST /facilities/{facilityId}/programs/api/portal/facilities/[id]/programs + createProgram403, 400(가격<0·정원<1·소요분≤0)
W-PGGET /facilities/{facilityId}/programs동경로 GET + listPrograms공개, 0건 정상
W-SLPATCH /facilities/{facilityId}/slots/{slotId}/close/api/portal/facilities/[id]/slots/[slotId]/close + closeSlot403, 409 상태충돌
W-SLPATCH /facilities/{facilityId}/slots/{slotId}/open.../open + openSlot403, 409

운영시간·휴무는 BE에서 시설 문서에 임베드(Mongo) → 시설 상세 응답(GET /facilities/{id} = getMyFacility)에 포함될 수 있음. FE는 별도 조회 대신 시설 상세 응답에서 읽고, 편집만 전용 엔드포인트 사용(§확인 필요).

라우팅 흐름

flowchart LR
    List["portal/facilities (목록)"] --> Detail["portal/facilities/[id]"]
    Detail --> OH["운영시간 탭 (W-OH)"]
    Detail --> HD["휴무일 섹션 (W-HD)"]
    Detail --> PG["시설상품 탭 (W-PG)"]
    Slots["portal/slots (W-SL)"] --> SlotAction["close/open PATCH"]
    Layout["app/layout (W-TH)"] --> Toggle["ThemeToggle 전역"]

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

SSOT는 web/app/globals.css(HSL CSS 변수). 신규 토큰 0개 — 기존 shadcn 토큰 재사용. 색 하드코딩 금지, Tailwind 시맨틱 클래스(bg-background·text-muted-foreground 등)만 사용.

시맨틱 토큰라이트 (HSL)다크 (HSL)이 설계 용도
background0 0% 100%222.2 84% 4.9%페이지 배경
card0 0% 100%222.2 84% 4.9%시설상품·슬롯 카드
foreground222.2 84% 4.9%210 40% 98%본문·라벨
muted-foreground215.4 16.3% 46.9%215 20.2% 65.1%보조 메타(정원·소요시간)
primary222.2 47.4% 11.2%210 40% 98%저장 CTA
accent210 40% 96.1%217.2 32.6% 17.5%선택 탭 강조
destructive0 84.2% 60.2%0 62.8% 30.6%마감·삭제
success142 71% 45%142 64% 42%OPEN 배지 저장 성공
warning38 92% 50%38 84% 46%CLOSED·주의 고지
border/input214.3 31.8% 91.4%217.2 32.6% 17.5%폼 필드·구분선
  • 다크 모드 의무: 토큰은 완비됐으나 토글 미배선 → W-TH로 .dark 클래스 토글을 배선해야 두 모드 모두 사용 가능. W-TH 완료 전에는 신규 화면이 사실상 라이트만 확인 가능(미완성) → W-TH를 선행 wave에 둔다.

Testing Plan (implementer TDD 입력)

Vitest + @testing-library/react + jsdom. BFF는 msw로 목. 각 티켓 최소 3케이스.

대상핵심 케이스
W-OH 운영시간 폼요일별 입력 저장 성공 / 오픈>마감 검증 실패 인라인 / 브레이크 추가·삭제 / 403 권한 안내
W-HD 휴무일날짜 추가 후 칩 렌더 / 삭제 / 중복 추가 멱등(에러 아님) / 0건 empty
W-PG 시설상품목록 렌더 / 등록 성공 후 목록 갱신 / 가격<0·정원<1 검증 / 0건 empty
W-SL 슬롯OPEN→마감 상태 배지 전환 / CLOSED→오픈 / 확인 다이얼로그 노출 / 409 충돌 toast
W-TH 토글클릭 시 .dark 클래스 토글 / localStorage 영속 / 시스템 기본 반영
다크 모드각 화면 라이트/다크 두 모드에서 시맨틱 클래스 적용(하드코딩 색 0건)

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

  • 신규 BFF Route·탭·섹션은 additive — 기존 시설/슬롯 화면 무변경. 롤백 = 탭/액션 렌더 조건 제거.
  • 웹은 BE/Redis 백엔드 동적 플래그(admin feature-flags)가 이미 존재 → 신규 운영자 화면 노출을 facility.program.enabled 등 BE 플래그로 게이팅 가능(운영자 UI 조건부 렌더). 앱 플래그와 축 단위로 동기 ON.
  • W-TH(다크 토글)는 독립 배선 — 먼저 머지해도 무해.

Open Questions

항목처리
운영시간·휴무 조회 경로BE 계약에 운영시간·휴무 GET 전용 경로가 명시 안 됨(PUT/POST/DELETE만). 시설 상세 응답(GET /facilities/{id})에 임베드 포함되는지 확인 필요 — 포함이면 별도 조회 불요, 미포함이면 GET /facilities/{id}/operating-hours 역제안
슬롯 status 노출기존 GET /facilities/{id}/slots 응답에 status(OPEN/CLOSED)가 추가되는지 BE 계약 확인 필요 — W-SL 배지 표시의 전제
program 수정·삭제BE 계약에 program POST/GET만 있음. 수정(PUT)·삭제(DELETE) 필요 시 역제안 — 이번 스코프는 등록·목록만(수정은 후속)
다크 토글 소유앱은 이미 토글 존재. 웹은 미배선 — 이번에 배선(W-TH). 관리자 전체 포털 공통이므로 별도 소유 티켓

Document History

날짜변경 내용
2026-07-07최초 작성 — 웹 운영 포털 FE 설계. 운영시간·휴무·시설상품·슬롯 open/close 4기능 + 다크 토글 배선(W-TH). 시설 상세 탭 확장·슬롯 화면 확장·BFF Route Handler 6종. 레포 관례(fetch 훅·zod·shadcn) 유지, 신규 상태 라이브러리·토큰 0개