근거 PRD: ./PRD.md (검수 PASS) — FR-10 (P1).
근거 BE TDD: ./TDD.md — “Detail Design → API 계약 (private-senior-fe 인계용)” 섹션의 관리 API 7개 + 데모 API 계약을 그대로 소비한다.
FR-10만 담당한다. 운영자가 재배포 없이 런타임 플래그를 관리하도록 웹 관리 화면(목록/생성/수정/아카이브·재활성, 퍼센티지 롤아웃 조정, 변경 이력 조회)을 제공한다. 클라이언트사이드 평가는 PRD Non-Goal이라 FE는 관리 UI만 만든다 — FE는 플래그를 정의·변경할 뿐 평가하지 않는다.
Overview
항목
내용
무엇
관리자용 피처 플래그 CRUD·아카이브·재활성·퍼센티지 조정·변경 이력 조회 웹 화면 5종
어디에
기존 web(Next.js 14 App Router) app/admin/ 섹션 확장 — app/admin/feature-flags/** (통합 결정, 아래 참조)
어떻게
레포 관례(BFF)를 따른다: "use client" 페이지 → 커스텀 훅(lib/admin/feature-flags/) → 동일 출처 BFF Route Handler(app/api/admin/feature-flags/**) → lib/server/be-client.ts(server-only) → BE /admin/feature-flags. 컴포넌트는 BE를 직접 호출하지 않는다
상태관리
서버 상태는 훅이 소유(SSOT), 폼 입력은 지역 useState. 전역 스토어 미도입(근거 아래)
테마
기존 시맨틱 토큰(globals.css) 사용 + 상태/타입 배지용 토큰 2종(success/warning) 추가. 라이트/다크 자동 대응
Terminology
용어
정의
BFF
Backend For Frontend. app/api/** Next.js Route Handler가 브라우저 요청을 받아 be-client로 BE를 호출·프록시
플래그 key
평가 식별자(예: demo.feature.hello). 생성 후 불변
전략(strategy)
평가 방식. strategyType으로 판별되는 4종 discriminated union — GLOBAL_TOGGLE / PERCENTAGE_ROLLOUT / ATTRIBUTE_MATCH / VARIANT_BUCKETING
variant
EXPERIMENT의 배정 그룹(name + weight). 최대 4개, weight 합 100
감사 로그
플래그 변경 이력(changeType / actorUserId / before / after / occurredAt)
Define Problem
AS-IS
피처 플래그 관리 화면 그린필드 — web에 feature-flag 관련 화면·타입·훅 0건.
관리자 섹션은 이미 존재 — web/app/admin/layout.tsx가 AdminSidebar+AdminTopbar+AuthGuard로 구성되고, web/app/admin/mcp/*(tokens·audit-logs·usage-analytics·anomalies)가 그 안에서 동작한다. AuthGuard는 prod에서 미인증 시 /login?redirect=/admin 리다이렉트(web/app/admin/_components/AuthGuard.tsx:22).
BFF 패턴 정착 — 클라이언트는 동일 출처 /api/admin/...만 호출(web/app/admin/mcp/tokens/page.tsx:20), Route Handler가 forwardBeResponse(web/app/api/portal/_lib/bff-helpers.ts:44)로 BE에 프록시. BE 직접 호출 0건. JWT 쿠키는 be-client(web/lib/server/be-client.ts:6server-only)가 자동 첨부.
서버 상태 훅 패턴 — web/lib/portal/useProducts.ts(useState/useEffect/refetch), web/lib/admin/auditLogs.ts(fetch 함수)가 선례. TanStack Query 미사용(package.json 의존성 0건).
입력 검증은 zod — web/lib/admin/mcp/schemas.ts가 zod 스키마 + DTO 타입을 BFF·클라이언트 공용으로 둠. BFF가 IssueMcpTokenInputSchema.parse 후 forward(web/app/api/admin/mcp/tokens/route.ts:19).
테마 토큰·다크 모드 인프라 완비 — web/app/globals.css에 :root/.dark 시맨틱 토큰 전체, web/tailwind.config.ts가 darkMode: ["class"] + 토큰을 Tailwind 색으로 매핑. components/ui/button.tsx는 토큰만 사용. 단, MCP 페이지들은 text-gray-500·bg-white 등 하드코딩 색이 섞임(기존 위반) — 신규 화면은 답습하지 않는다.
테마 토글 UI 없음 — .dark 클래스는 정의돼 있으나 이를 켜는 UI/ThemeProvider 없음(next-themes 미설치). 다크는 .dark 클래스가 붙는 환경에서 토큰으로 자동 적용된다.
TO-BE
app/admin/feature-flags/에 화면 5종 신설, 기존 AdminLayout 하위에 자동 편입(Sidebar/Topbar/AuthGuard 재사용).
lib/admin/feature-flags/에 zod 스키마·DTO 타입(BE 계약 1:1)·BFF fetch 함수·서버 상태 훅.
app/api/admin/feature-flags/**에 BFF Route Handler(BE 7개 엔드포인트 프록시).
재사용 컴포넌트: 상태/타입 배지, 전략 요약 표시, 전략 입력 폼(4종 분기 + 퍼센티지 슬라이더 + variant 에디터).
시맨틱 토큰에 success/warning 2종 추가(상태·타입 배지 색). 모든 신규 UI는 토큰만 사용 — 라이트/다크 자동 대응.
Architecture Benchmarking (의무)
토스(Toss) 벤치마킹 — 디자인 기준. 각 화면 설계에 참고 패턴을 1줄 명시한다.
사례
참고 패턴
본 설계 적용
미참고
토스 “송금” 플로우
한 화면 한 과업, 하단 고정 단일 CTA, 단계별 진행
생성/수정 화면을 한 과업(폼 저장)에 집중, 화면당 주요 CTA 1개(저장/생성)
다단계 위저드는 과함 — 플래그 생성은 단일 폼
토스 “내 계좌·자산 목록”
카드/리스트에 상태를 색이 아닌 라벨+절제된 배지로, accent 최소
플래그 목록을 테이블+상태 배지(success/muted)로, 색 남용 없이
화려한 그래프·색 코딩
토스 “약관/설정 토글”
토글은 즉시 반영 + 명확한 on/off 라벨
GLOBAL_TOGGLE 입력을 라벨 있는 스위치로
—
토스 “한도 조정 슬라이더”
슬라이더 + 현재값 숫자 동시 표기, 경계값 시각화
퍼센티지 롤아웃을 range 슬라이더 + 숫자 입력 병기(0~100)
—
디자인 입력(Figma)이 없으므로 위 토스 패턴을 텍스트 와이어프레임으로 직접 제안한다. 색은 전부 시맨틱 토큰으로만 표현한다.
Possible Solutions
방안 비교 — 화면 배치 (PRD Open Question 해소: 통합 vs 분리)
방안
설명
왜 채택 / 미채택
A. 기존 /admin 통합 (app/admin/feature-flags/**)
기존 AdminLayout(Sidebar/Topbar/AuthGuard) 하위에 화면 추가, 사이드바에 “피처 플래그” 그룹 추가
채택 — PRD Open Q 선례(“전용 UI 최소, 기존 포털 확장 우선”). 인증 가드·레이아웃·BFF·토큰 인프라를 전부 재사용해 신규 표면 최소. MCP 관리와 동일 성격(운영자 전용 관리)
B. 별도 신규 관리 앱/라우트
/feature-flags 최상위 별도 섹션·레이아웃
미채택 — 인증·레이아웃·네비를 중복 구축. 개인 프로젝트 규모에 과함. 운영자 동선이 /admin으로 이미 수렴
결정: A (통합). 근거: ① /admin + AuthGuard가 이미 hasRole ADMIN 보호 경로(BE SecurityConfig/admin/**)와 정합 ② app/admin/mcp/* 동일 패턴 재사용으로 학습·유지 비용 최소 ③ PRD Open Q가 명시한 선례.
방안 비교 — 서버 상태 관리
방안
설명
왜 채택 / 미채택
C. 레포 관례 커스텀 훅(useState/useEffect + BFF fetch)
useProducts 패턴대로 useFeatureFlags/useFeatureFlag/useFlagAuditLogs 훅이 서버 상태 소유, refetch 노출
채택 — 레포에 이미 정착(TanStack Query 미도입). private-fe-convention “레포 관례 우선” 적용. 화면 5개 규모에 충분
D. TanStack Query 신규 도입
캐시·무효화·낙관적 업데이트
미채택 — 신규 의존성. 레포 전체가 BFF+훅 관례라 이 과제만 다른 스택을 들이면 일관성 붕괴. 규모 대비 과함
E. 전역 스토어(Zustand 등)
플래그 목록을 전역 보관
미채택 — 서버 데이터를 스토어에 복사 보관 금지(no-global-by-default). 화면 간 공유 전역 상태 없음. 목록·상세는 각 화면이 훅으로 조회
방안 비교 — 슬라이더/토글 프리미티브
방안
설명
왜 채택 / 미채택
F. 네이티브 <input type=range> + 숫자 입력, 라벨 스위치(button/checkbox)
의존성 없이 접근성 있는 슬라이더·토글
채택 — @radix-ui/react-slider 미설치. 네이티브 range는 키보드·스크린리더 기본 지원. 신규 의존성 회피(단순함)
G. @radix-ui/react-slider·react-switch 신규 도입
스타일 유연
미채택(1차) — 신규 의존성. 네이티브로 요구 충족. 디자인 요구가 커지면 후속
Detail Design
화면 목록
#
화면
경로
주요 기능
참고 토스 패턴
S1
플래그 목록
/admin/feature-flags
status/type 필터, 목록 테이블, 빈 상태 CTA, “플래그 추가”
계좌 목록(라벨+절제된 배지, accent 최소)
S2
플래그 생성
/admin/feature-flags/new
key·type·description·strategy 입력 폼, 단일 CTA “생성”
송금(한 과업, 하단 단일 CTA)
S3
플래그 수정
/admin/feature-flags/[key]
description·strategy 수정, 아카이브/재활성 액션
송금 + 설정 토글
S4
퍼센티지 롤아웃 조정
S2/S3 폼 내 전략 섹션(PERCENTAGE_ROLLOUT)
슬라이더+숫자 0~100
한도 조정 슬라이더
S5
변경 이력(감사 로그)
/admin/feature-flags/[key]/audit-logs
플래그별 변경 이력 페이징 테이블, before→after
거래 내역 리스트
S4는 독립 화면이 아니라 S2·S3 폼에 포함되는 전략 입력 섹션이다(전략 4종 중 PERCENTAGE_ROLLOUT일 때). 재사용 컴포넌트로 분리한다.
토스 패턴: 송금(단일 주요 CTA “변경 저장”) + 위험 액션(아카이브)은 시각적으로 분리·절제(destructive outline).
상태 전이 UI: ACTIVE → “아카이브”·“변경 저장” / ARCHIVED → “재활성”만, 전략·설명 입력 비활성(BE가 409 반환하는 규칙을 UI에서 선반영).
S5 — 변경 이력 /admin/feature-flags/[key]/audit-logs
┌───────────────────────────────────────────────────────────────┐
│ ← demo.feature.hello 변경 이력 │
├───────────────────────────────────────────────────────────────┤
│ 시각 변경 변경자 이전 → 이후 │
│ 2026-07-03 14:20 ARCHIVED #12 전역 ON → (아카이브) │
│ 2026-07-03 10:05 UPDATED #12 전역 OFF → 전역 ON │
│ 2026-07-03 09:50 CREATED #12 (없음) → 전역 OFF │
│ [이전] 1 / 3 [다음] │
└───────────────────────────────────────────────────────────────┘
토스 패턴: 거래 내역 — 시간 역순 리스트, 변경 유형 배지, before/after를 사람이 읽는 전략 요약으로.
GET /admin/feature-flags/{key} → 200 FeatureFlagResponse
useFeatureFlag(key)
폼 프리필
404→빈 상태
S3 수정
PUT /api/admin/feature-flags/{key}
PUT /admin/feature-flags/{key} body {description,strategy} → 200
updateFeatureFlag(key,input)
토스트 저장됨
400/404/409(ARCHIVED 수정)→배너
S3 아카이브
POST /api/admin/feature-flags/{key}/archive
POST .../archive → 200 {key,status:"ARCHIVED"}
archiveFeatureFlag(key)
토스트+재활성 버튼 노출
404/409(이미 ARCHIVED)→배너
S3 재활성
POST /api/admin/feature-flags/{key}/activate
POST .../activate → 200 {key,status:"ACTIVE"}
activateFeatureFlag(key)
토스트+수정 활성화
404/409(이미 ACTIVE)→배너
S5 이력
GET /api/admin/feature-flags/{key}/audit-logs?page=&size=
GET .../audit-logs?page=&size= → 200 total 포함 페이지 응답(FeatureFlagAuditLogPage, 아래)
useFlagAuditLogs(key,page,size)
테이블 + 총페이지 페이징(“1 / N”)
404/오류→배너
BE 계약 타입 (TDD 그대로 소비 — lib/admin/feature-flags/schemas.ts)
strategy (discriminated union, strategyType 판별):
{ strategyType: "GLOBAL_TOGGLE", enabled: boolean }
{ strategyType: "PERCENTAGE_ROLLOUT", percentage: number } // 0..100
{ strategyType: "ATTRIBUTE_MATCH", attribute: string, value: string }
{ strategyType: "VARIANT_BUCKETING", variants: {name:string,weight:number}[] } // ≤4, 합100
FeatureFlagResponse:
{ id:number, key:string, type:"RELEASE"|"OPERATIONAL"|"EXPERIMENT"|"ENTITLEMENT",
status:"ACTIVE"|"ARCHIVED", description:string, strategy:Strategy,
createdAt:string(ISO), updatedAt:string(ISO) }
FeatureFlagAuditLogResponse (배열 원소):
{ changeType:"CREATED"|"UPDATED"|"ARCHIVED"|"ACTIVATED", actorUserId:number,
before:FeatureFlagSnapshot|null, after:FeatureFlagSnapshot, occurredAt:string(ISO) }
// 감사 로그는 total 포함 페이지 응답으로 소비한다 (senior-be 계약 개정 반영).
// BE 최종 형태(Spring Page vs {items,total})는 확정 대기 — 아래 canonical로 정규화해 필드명 변경을 api.ts 1곳에 격리한다.
FeatureFlagAuditLogPage (wire, BE 형태 택1 — 파싱 스키마만 이 형태에 맞춘다):
// (a) Spring Page(레포 audit-logs 선례): { content: FeatureFlagAuditLogResponse[], totalElements, totalPages, number, size }
// (b) 경량 래핑: { items: FeatureFlagAuditLogResponse[], total: number }
FeatureFlagAuditLogPageView (canonical, 화면이 소비 — api.ts가 wire→canonical 정규화):
{ logs: FeatureFlagAuditLogResponse[], total: number, page: number, size: number,
totalPages: number /* total·size로 계산 또는 wire의 totalPages */ }
FeatureFlagSnapshot: { key, type, status, description, strategy }
zod로 응답 검증 후 좁히기 — BFF·훅에서 FeatureFlagResponseSchema.parse로 검증(no-loose-assertion, 검증 없는 as 금지). strategy는 z.discriminatedUnion("strategyType", [...]).
감사 로그 total 포함(확정) — 원 TDD 배열 직반환에서 개정됨. S5의 “1 / N” 총페이지 표시를 위해 total이 필요하다. FeatureFlagAuditLogPageSchema가 wire 응답을 파싱하고, api.ts가 canonical FeatureFlagAuditLogPageView(logs/total/totalPages)로 정규화한다. BE 최종 형태(Spring Page vs {items,total})의 필드명 차이는 이 정규화 함수 1곳만 수정하면 되도록 격리 — 화면·훅·컴포넌트는 canonical 뷰만 의존. 배열 전용 폴백은 제거.
목록(S1) 응답 유연화 — 목록도 BE가 total 포함으로 통일할 수 있어(senior-be 결정 중), useFeatureFlags 훅은 wire가 배열이든 {content|items,total}이든 흡수해 canonical { flags, total? }로 정규화한다. 화면은 목록 배열만 쓰되, 향후 목록 페이징 도입 시 total을 추가 소비할 수 있게 훅 반환 형태를 열어 둔다.
라우팅·내비게이션 흐름 (Mermaid flowchart LR)
flowchart LR
Sidebar["AdminSidebar 피처 플래그"] --> List["/admin/feature-flags S1"]
List -->|플래그 추가| New["/new S2"]
List -->|행 클릭| Detail["/[key] S3"]
New -->|생성 성공| List
Detail -->|변경 이력 보기| Audit["/[key]/audit-logs S5"]
Detail -->|저장/아카이브/재활성| Detail
Audit -->|뒤로| Detail
New -->|취소| List
Testing Plan (implementer TDD 입력 — 사용자 관점 동작)
Vitest + @testing-library/react(레포 러너). 테스트명에 티켓ID 금지, 사용자 관점 동작 검증(role·텍스트·인터랙션). 각 단위 해피/실패/엣지 최소 3개.
성공 시 data 반환·isLoading false / 5xx 시 error 세팅 / refetch 재조회 (fetch mock)
컴포넌트
StatusBadge/TypeBadge/StrategySummary
ACTIVE→success 배지 텍스트 / ARCHIVED→muted / 전략별 요약 문자열(“50% 롤아웃”)
컴포넌트
StrategyForm
전략 유형 변경 시 입력 필드 전환 / percentage 슬라이더 변경 시 숫자 동기화 / variant 합≠100이면 저장 비활성
화면
S1 목록
로딩 표시 / 빈 목록 시 CTA 노출 / 오류 시 alert+재시도 / 데이터 렌더·필터
화면
S2 생성
유효 입력 제출 시 create 호출·성공 토스트 / key 중복 400 시 배너 / 필수 누락 인라인 에러
화면
S3 수정
프리필 렌더 / 아카이브 클릭 시 archive 호출 / ARCHIVED 상태면 저장 비활성·재활성 노출 / 404 빈 상태
화면
S5 이력
이력 렌더·before→after 표시 / 빈 이력 문구 / total 기반 “1 / N” 총페이지 계산·페이징 이동
a11y(e2e 선택)
Playwright + axe
기존 e2e/a11y-landing.spec.ts 패턴 참고(신규 화면 axe 스캔)
Release Scenario (기능 플래그·점진 공개 관점)
순수 가산(신규 라우트·파일). 기존 화면 무변경. FE-11(사이드바 nav 추가)만 공용 파일 수정 → 마지막 단독 통합.
BE 미배포 상태에서 FE 먼저 머지돼도 BFF는 forwardBeResponse가 503(BACKEND_URL 미설정)/BE 404를 반환 → 화면은 오류 배너로 안전 degrade. BE 계약 배포 후 정상 동작.
점진 공개: 사이드바 nav 추가(FE-11)를 마지막에 열기 전까지 화면은 URL 직접 접근으로만 노출 → 내부 검증 후 nav 공개.
롤백: 화면 문제 시 FE-11 nav 항목 제거(코드 롤백)로 진입점 차단. 라우트 파일은 잔존해도 무해.
Open Questions
감사/목록 응답 형태(필드명만 미확정) — 감사 로그는 total 포함으로 확정(senior-be 개정). BE 최종 wire 형태(Spring Page {content,totalElements,...} vs 경량 {items,total})만 확정 대기 — 확정 시 api.ts의 wire→canonical 정규화 함수 1곳만 수정(화면·훅·컴포넌트 무영향). 목록(S1)도 total 포함 통일 시 훅이 흡수하도록 열어 둠.
종류→전략 게이팅 규칙 — TDD는 “EXPERIMENT만 VariantBucketing”만 명시. RELEASE에 PercentageRollout 허용 여부 등 세부는 BE 검증이 SSOT. FE는 EXPERIMENT↔VARIANT만 강제하고 나머지 3종은 전체 허용(BE 400을 배너로 표면화).
테마 토글 UI — 본 과제 범위 밖(플랫폼 공통). 다크는 .dark 클래스 하 토큰 자동 적용으로만 보장.
actorUserId 표시 — 감사 로그의 변경자는 userId(number)만 계약에 있음. 이름 조인은 BE 미제공 → #{id} 표기.
Document History
날짜
변경 내용
2026-07-03
최초 작성 — 레포 AS-IS 실측(Next.js14 App Router·BFF·zod·토큰/다크 인프라·TanStack Query 미사용), /admin 통합 결정, 화면 5종 와이어프레임+상태표, 토큰 매핑(+success/warning), BE 계약 1:1 API표, 티켓 11건 분해
2026-07-03
senior-pm NEEDS_REVISION 반영 — 감사 로그를 total 포함 페이지 응답으로 소비하도록 개정(FeatureFlagAuditLogPage wire + canonical 뷰 정규화, 필드명 차이는 api.ts 1곳 격리). S5 “1 / N” 총페이지 표시 유지·배열 폴백 제거. 목록 훅도 total 흡수 가능하게 유연화. FE-01·FE-04·FE-10 티켓 동기 갱신