FeaturesSettings (설정)설정 7서브 (/settings)

운영사 설정 — /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.tsxnext/link 기반 라우트 탭. 활성 표시는 링크 표준 aria-current="page" + 좌측 primary 도트(audit 탭 시각 미러). isActive 는 정확 일치 + “href + 슬래시” 접두 매칭으로 sub-route 도 활성 처리. 탭 순서는 PRD 번호순(1→7).
  • (protected)/settings/page.tsxredirect("/settings/profile")(번호순 첫 탭, SPEC #128 D2).

7서브 (PRD Page 18 · 번호순 탭)

#slug상태
18-1내 프로필/settings/profile내실 완료(SPEC #130) — 본인 정보(이름·이메일·역할 read) + 비밀번호 변경. 상세
18-2알림 설정/settings/notificationsTTS 연결 확인(SPEC #134) — 안내방송(TTS) smoke-test 버튼. 이메일·시스템 알림 토글은 추후. 상세
18-3장르·무드 옵션/settings/music-options내실 완료(SPEC #132) — 장르(GENRE)·무드(MOOD) 태그 옵션 CRUD(추가·수정·삭제=soft). 상세
18-4시스템 정책/settings/system-policiesComingSoon — 18-7 /settings/policies 충돌 회피 분리 slug(SPEC #128 D4)
18-5신탁 재생 로그/settings/trust-playback-logsComingSoon — 재생 로그 인프라 의존(별도 SPEC)
18-6LLM 자동멘트/settings/llm-mentsComingSoon — 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 + ChangePasswordSection idiom 을 정확 미러로 atom-grounded 합성. 시안 도착 시 정합 교체 ([[feedback_design_only_from_handoff]]).

  • 본인 정보(read-only)useMe()(/api/auth/me BFF → 단일 소스 MeResponse) 응답을 DescCard 3행으로 노출: 이름(name, @nullable → null 이면 이메일(email역할 (roleOPERATOR “운영자” 등 한글 라벨). 운영자 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 OpenAPI ChangePasswordRequest 8~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): 503 TTS_TOKEN_NOT_CONFIGURED → “TTS 토큰이 설정되지 않았습니다. Render 환경변수 TYPECAST_API_TOKEN 을 확인해주세요.” · 502 TTS_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, mutator apiFetch 가 BFF catch-all /api/backend/... 경유·토큰 서버 전용)로 옵션 목록을 fetch. backend 가 sort_order ASC → value ASC 로 정렬해 내려주므로 FE 재정렬 없음. 비활성(active=false) 옵션도 노출하되 흐림(opacity) + “비활성” 배지로 구분.
  • 입력/출력
    • 추가 — 섹션 하단 인라인 폼(value 필수 · sortOrder 선택). useCreateMusicTagOptionPOST 201. 미입력 시 sortOrder omit(backend 기본 0). 성공 시 해당 타입 query invalidate(refetch).
    • 수정 — 행 [수정] → 인라인 편집 행(value input · sortOrder number input · active Switch). useUpdateMusicTagOptionPATCH 200. 변경된 필드만 전송(미변경 필드는 undefined → 직렬화 제외 = 부분 PATCH). 성공 시 invalidate.
    • 삭제 — 행 [삭제] → 확인 단계(soft delete = active:false, 멱등) → useDeleteMusicTagOptionDELETE 204. 기존 음원 참조 보존(안전). 성공 시 invalidate.
  • 검증/에러 분기value 는 backend OpenAPI(@minLength 1·@maxLength 50)와 동일한 zod 로 사전 검증([[feedback]] frontend.md §8): 빈값·51자 이상은 제출 차단 + inline 에러. sortOrder 는 0 이상 정수만 허용(음수·비숫자 inline 에러). 서버 에러는 mutator ApiError(status·body)에서 extractCode 로 backend ErrorResponse code 를 뽑아 매핑: 409 MUSIC_TAG_OPTION_DUPLICATE(중복 value) · 404 MUSIC_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·active aria-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).