운영사 설정 — /settings (7서브 탭 셸)
SPEC #128 정합 (FE-only·BE 0) — PRD Page 18 7서브 라우팅 + 탭 셸 + placeholder 스캐폴딩. 18-7 약관(SPEC #023)·18-1 내 프로필(SPEC #130)·18-3 장르·무드 옵션(SPEC #132) 내실 완료, 나머지 4서브는 ComingSoon 골격(내실은 각 별도 SPEC).
Overview
운영사(OPERATOR) 백오피스 설정 영역. PRD Page 18 의 7개 서브를 상단 라우트 탭으로 묶은
공유 셸이다. 사이드바 하단 “설정” 항목(/settings)으로 진입하면 첫 탭 /settings/profile
로 redirect 된다.
시안 출처: 탭 셸은 시안 부재 — 검증된 라우트 탭 관용구(
audit/audit-tabs.tsx)를 미러한settings-tabs.tsx. 6 placeholder 는 기존ComingSoon(atom-grounded) 재사용. 새 색/아이콘 추측 없음([[feedback_design_only_from_handoff]]).
구조
(protected)/settings/layout.tsx— 공유 셸.(protected)/layout의 ops 셸(topbar/sidebar/ 인증 가드) 하위에서<SettingsTabs/>를 한 번만 두고 그 아래 각 서브 page 본문을 렌더한다. 각 서브 page(약관 화면·ComingSoon)는 자체<PageHeader>를 그대로 가지므로 layout 은 탭만 얹는다.(protected)/settings/settings-tabs.tsx—next/link기반 라우트 탭. 활성 표시는 링크 표준aria-current="page"+ 좌측 primary 도트(audit 탭 시각 미러).isActive는 정확 일치 + “href + 슬래시” 접두 매칭으로 sub-route 도 활성 처리. 탭 순서는 PRD 번호순(1→7).(protected)/settings/page.tsx—redirect("/settings/profile")(번호순 첫 탭, SPEC #128 D2).
7서브 (PRD Page 18 · 번호순 탭)
| # | 탭 | slug | 상태 |
|---|---|---|---|
| 18-1 | 내 프로필 | /settings/profile | 내실 완료(SPEC #130) — 본인 정보(이름·이메일·역할 read) + 비밀번호 변경. 상세 |
| 18-2 | 알림 설정 | /settings/notifications | TTS 연결 확인(SPEC #134) — 안내방송(TTS) smoke-test 버튼. 이메일·시스템 알림 토글은 추후. 상세 |
| 18-3 | 장르·무드 옵션 | /settings/music-options | 내실 완료(SPEC #132) — 장르(GENRE)·무드(MOOD) 태그 옵션 CRUD(추가·수정·삭제=soft). 상세 |
| 18-4 | 시스템 정책 | /settings/system-policies | ComingSoon — 18-7 /settings/policies 충돌 회피 분리 slug(SPEC #128 D4) |
| 18-5 | 신탁 재생 로그 | /settings/trust-playback-logs | ComingSoon — 재생 로그 인프라 의존(별도 SPEC) |
| 18-6 | LLM 자동멘트 | /settings/llm-ments | ComingSoon — LLM 가드레일, LLM 인프라 의존(별도 SPEC) |
| 18-7 | 약관 버전 관리 | /settings/policies | 기존 구현(SPEC #023·#033·#038) — 탭으로 편입(동작 불변). 상세 |
18-1 내 프로필 (/settings/profile, SPEC #130)
#128 스캐폴딩한 ComingSoon 을 내실화. 운영자 본인 계정 정보(read) + 비밀번호 변경 2 섹션.
FE-only · BE 0 — 신규 endpoint 없이 기존 자산만 재사용한다.
시안 출처: 전용 시안 부재 — 점장 #100
store-profile-client.tsx·본사 #099 의 DescCard +ChangePasswordSectionidiom 을 정확 미러로 atom-grounded 합성. 시안 도착 시 정합 교체 ([[feedback_design_only_from_handoff]]).
- 본인 정보(read-only) —
useMe()(/api/auth/meBFF → 단일 소스MeResponse) 응답을 DescCard 3행으로 노출: 이름(name,@nullable→ null 이면—)·이메일(email)·역할 (role→OPERATOR“운영자” 등 한글 라벨). 운영자 me read endpoint 신설 없이 기존 세션 me 소스 재사용(BE 0). me 로딩 중 스켈레톤, me 실패 시 danger 배너(비번 변경 섹션은 me 와 무관하게 계속 노출). 이름 편집은 v1 제외(read-only) — 운영자 이름 편집 정책 확인 전 과설계 회피(후속). - 비밀번호 변경 —
@/components/change-password-section(admin). 본사 #099·점장 #100 의 공용ChangePasswordSection(apps/space) 과 폼 로직(현재/새/새 확인 state·검증·BFF reseal·코드별 에러 매핑·helper·a11y)을 1:1 미러. apps/space 패키지는 admin 앱에서 직접 import 불가(별도 앱)라 동일 idiom 을 admin 에 미러 추출했다. 같은 BFF route(POST /api/auth/change-password)를 호출해 reseal 로 같은 세션을 유지한다 — 강제 변경/onboarding/change-password와 달리 성공 후 destination redirect 없이 설정 화면에 머무르며 성공 배너만 표시. 검증은 backend OpenAPIChangePasswordRequest8~100자 제약과 정합.NO_SESSION·AUTH_UNAUTHENTICATED만/login으로 이동.
구조: page.tsx(server, client 마운트만) → settings-profile-client.tsx("use client",
useMe + DescCard + ChangePasswordSection). 보호 라우트 가드는 상위 (protected)/layout me 가드.
테스트: settings-profile-client.test.tsx(me read 표시·null —·me 실패 배너+비번 섹션 유지·
비번 섹션 동반 렌더), change-password-section.test.tsx(testid·검증 4종·성공 reseal·에러 매핑·
NO_SESSION redirect·네트워크 실패).
18-2 알림 설정 (/settings/notifications, SPEC #134)
#128 스캐폴딩한 ComingSoon 을 일부 내실화 — 안내방송(TTS) 연결 확인(smoke-test). TTS 가
안내방송을 구동하므로 알림/안내 설정 맥락에 둔다. 이메일·시스템 알림 수신 토글은 알림 인프라
의존으로 추후(별도 SPEC).
- [TTS 연결 확인](
tts-smoke-test-btn) →useRunTtsSmokeTest(POST /api/v1/admin/tts/smoke-test, OPERATOR). 고정 텍스트(voice SHEAN·NORMAL)로 부작용 없는 진단 합성 1회(blob·DB 미저장). - 성공(
tts-smoke-test-success, Banner success):voice· 소요elapsedMsms · 오디오 KB · 길이(durationSeconds) 표시. - 실패(
tts-smoke-test-error, Banner danger): 503TTS_TOKEN_NOT_CONFIGURED→ “TTS 토큰이 설정되지 않았습니다. Render 환경변수 TYPECAST_API_TOKEN 을 확인해주세요.” · 502TTS_SYNTHESIS_FAILED→ “음성 합성에 실패했습니다. 잠시 후 다시 시도하거나 Typecast 연동 상태를 확인해주세요.” (code 우선 + status fallback). 운영자 수동 전용(quota 보호 — 자동 폴링 없음). - 응답 타입은 generated
TtsSmokeTestResponse단일 소스. 신규 env:TYPECAST_API_TOKEN(Render).
테스트: notifications-client.test.tsx(버튼 렌더·성공 voice/소요/크기 표시·503 환경변수 안내·502
재시도 안내·성공 배너 단독 노출).
18-3 장르·무드 옵션 (/settings/music-options, SPEC #132)
#128 스캐폴딩한 ComingSoon 을 내실화. 라이브러리·플레이리스트 분류에 쓰이는 장르(GENRE)·
무드(MOOD) 태그 옵션의 CRUD 화면. OPERATOR 전용(backend 도 인가).
시안 출처: 전용 시안 부재 —
/settings/policies(#023)의 2섹션 카드 idiom +/settings/profile(#130)의 atom-grounded(명시적 fetch+refetch·낙관적 갱신 없음·inline 에러) 스타일을 미러로 합성. 시안 도착 시 정합 교체([[feedback_design_only_from_handoff]]).
- 2 섹션 — 장르·무드를 각각 카드 섹션으로 분리. 각 섹션은 generated
useListMusicTagOptions({type})(react-query client-query, mutatorapiFetch가 BFF catch-all/api/backend/...경유·토큰 서버 전용)로 옵션 목록을 fetch. backend 가sort_order ASC → value ASC로 정렬해 내려주므로 FE 재정렬 없음. 비활성(active=false) 옵션도 노출하되 흐림(opacity) + “비활성” 배지로 구분. - 입력/출력
- 추가 — 섹션 하단 인라인 폼(
value필수 ·sortOrder선택).useCreateMusicTagOption→POST201. 미입력 시sortOrderomit(backend 기본 0). 성공 시 해당 타입 query invalidate(refetch). - 수정 — 행 [수정] → 인라인 편집 행(
valueinput ·sortOrdernumber input ·activeSwitch).useUpdateMusicTagOption→PATCH200. 변경된 필드만 전송(미변경 필드는undefined→ 직렬화 제외 = 부분 PATCH). 성공 시 invalidate. - 삭제 — 행 [삭제] → 확인 단계(soft delete =
active:false, 멱등) →useDeleteMusicTagOption→DELETE204. 기존 음원 참조 보존(안전). 성공 시 invalidate.
- 추가 — 섹션 하단 인라인 폼(
- 검증/에러 분기 —
value는 backend OpenAPI(@minLength 1·@maxLength 50)와 동일한 zod 로 사전 검증([[feedback]] frontend.md §8): 빈값·51자 이상은 제출 차단 + inline 에러.sortOrder는 0 이상 정수만 허용(음수·비숫자 inline 에러). 서버 에러는 mutatorApiError(status·body)에서extractCode로 backendErrorResponsecode 를 뽑아 매핑: 409MUSIC_TAG_OPTION_DUPLICATE(중복 value) · 404MUSIC_TAG_OPTION_NOT_FOUND(부재) · 권한/5xx/네트워크 공통 메시지. 모두 inline 노출. - 빈 상태 CTA — 옵션 0건이면 점선 박스 빈상태(첫 옵션 추가 유도) + 추가 폼은 빈/에러와 무관하게 항상 노출.
구조: page.tsx(server, client 마운트만) → music-options-client.tsx("use client",
2 섹션 × useListMusicTagOptions + create/update/delete mutation). 보호 라우트 가드는 상위
(protected)/layout me 가드. FE-only 화면(BE 계약은 #132 BE 슬라이스) · 신규 env var 0.
테스트: music-options-client.test.tsx(목록 렌더·GENRE/MOOD 분리·비활성 배지·빈 상태 CTA·추가
후 refetch·중복 409 inline 에러·빈값 검증 차단·수정 부분 PATCH·삭제 soft·목록 5xx 에러 배너).
범위 밖: 음원 업로드 폼이 이 옵션을 소비(드롭다운 연동)하는 것은 후속 — 이번은 옵션 관리 CRUD 까지.
사이드바 진입점
OpsSidebar 하단 “설정” 항목 href 가 /settings/policies → /settings 로 변경(SPEC #128 D5).
isActive 가 /settings/* 하위 모든 탭(약관·시스템 정책 포함)을 active 매칭하므로 어느 탭에
있든 “설정” 항목이 활성으로 표시된다.
테스트
settings-tabs.test.tsx— 7탭 번호순 렌더·각 href·activearia-current·약관 탭 편입·sub-route startsWith 매칭.sidebar.test.tsx— “설정” href/settings·/settings/profile·/settings/policies에서 active.- 기존 약관(
policies-client.test·policy-publish-dialog.test·policy-history-dialog.test)은 탭 셸 편입 후에도 불변(회귀 0).