피처 플래그 관리 화면 design-fe-web

Background

근거 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

용어정의
BFFBackend 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
variantEXPERIMENT의 배정 그룹(name + weight). 최대 4개, weight 합 100
감사 로그플래그 변경 이력(changeType / actorUserId / before / after / occurredAt)

Define Problem

AS-IS

  • 피처 플래그 관리 화면 그린필드webfeature-flag 관련 화면·타입·훅 0건.
  • 관리자 섹션은 이미 존재web/app/admin/layout.tsxAdminSidebar+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:6 server-only)가 자동 첨부.
  • 서버 상태 훅 패턴web/lib/portal/useProducts.ts(useState/useEffect/refetch), web/lib/admin/auditLogs.ts(fetch 함수)가 선례. TanStack Query 미사용(package.json 의존성 0건).
  • 입력 검증은 zodweb/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.tsdarkMode: ["class"] + 토큰을 Tailwind 색으로 매핑. components/ui/button.tsx는 토큰만 사용. 단, MCP 페이지들은 text-gray-500·bg-white 등 하드코딩 색이 섞임(기존 위반) — 신규 화면은 답습하지 않는다.
  • UI 프리미티브components/ui/에 button·input·badge·dialog·tabs·toast(Radix 기반). slider·switch 프리미티브·의존성 없음.
  • 테마 토글 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-flagsstatus/type 필터, 목록 테이블, 빈 상태 CTA, “플래그 추가”계좌 목록(라벨+절제된 배지, accent 최소)
S2플래그 생성/admin/feature-flags/newkey·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일 때). 재사용 컴포넌트로 분리한다.

텍스트 와이어프레임

S1 — 플래그 목록 /admin/feature-flags

┌───────────────────────────────────────────────────────────────┐
│ 피처 플래그                              [+ 플래그 추가] (primary)│  ← h1 + 우상단 단일 CTA
│ 런타임 토글·롤아웃을 관리합니다.                                  │  ← text-muted-foreground
├───────────────────────────────────────────────────────────────┤
│ 상태: [전체 ▾]  종류: [전체 ▾]                    (필터 select)   │
├───────────────────────────────────────────────────────────────┤
│ KEY                 종류        상태     전략        수정        │  ← 테이블 헤더
│ demo.feature.hello  RELEASE     ●ACTIVE  전역 ON     >          │  ← 행 클릭 → S3
│ promo.banner        OPERATIONAL ●ACTIVE  50% 롤아웃  >          │
│ old.experiment      EXPERIMENT  ○ARCHIVED A/B(50:50) >          │  ← ARCHIVED = muted
└───────────────────────────────────────────────────────────────┘
  • 토스 패턴: 계좌 목록 — 상태는 색 남용 없이 배지(ACTIVE=success 점, ARCHIVED=muted). accent는 “플래그 추가” CTA 한 곳만.
  • 행 클릭 → S3(수정). “변경 이력”은 S3 안 링크로 진입(목록 행에 부가 링크 X, 위계 단순화).

S2 — 플래그 생성 /admin/feature-flags/new

┌───────────────────────────────────────────────────────────────┐
│ ← 플래그 추가                                                    │
├───────────────────────────────────────────────────────────────┤
│ Key *        [ demo.feature.hello        ]  (소문자.점 형식)     │
│ 종류 *       [ RELEASE ▾ ]  RELEASE/OPERATIONAL/EXPERIMENT/ENTITLEMENT
│ 설명         [ 데모 인사 엔드포인트 킬스위치 ]                    │
│ ─ 평가 전략 ───────────────────────────────────────────────    │
│ 전략 유형 *  [ 전역 ON/OFF ▾ ]   (종류에 따라 선택지 분기)        │
│   └ (전략별 입력 섹션 — 아래 전략 폼)                            │
├───────────────────────────────────────────────────────────────┤
│                                   [취소]      [생성] (primary)   │  ← 하단 단일 주요 CTA
└───────────────────────────────────────────────────────────────┘
  • 토스 패턴: 송금 — 한 화면 한 과업, 하단 단일 CTA “생성”. 입력 실패는 필드 하단 인라인 에러.
  • 종류→전략 선택지 게이팅: EXPERIMENT → VARIANT_BUCKETING만, 그 외 → GLOBAL_TOGGLE/PERCENTAGE_ROLLOUT/ATTRIBUTE_MATCH. (BE가 최종 검증, FE는 미리 좁혀 라운드트립 절감.)

전략 입력 섹션 (S2/S3 공용 컴포넌트, S4 포함)

전략 유형 = 전역 ON/OFF (GLOBAL_TOGGLE)
   활성화  ( ●─ ON  |  ─○ OFF )        ← 라벨 스위치

전략 유형 = 퍼센티지 롤아웃 (PERCENTAGE_ROLLOUT)   ← S4
   노출 비율   [====●───────] 50 %      ← range 슬라이더 + 숫자 입력(0~100)
   userId 해시 기반 — 동일 사용자는 일관되게 노출됩니다. (안내 caption)

전략 유형 = 속성 매칭 (ATTRIBUTE_MATCH)
   속성 이름  [ plan          ]
   기대 값    [ PREMIUM       ]         ← attribute == value 일 때만 노출

전략 유형 = variant 버케팅 (VARIANT_BUCKETING, EXPERIMENT 전용)
   variant 1  [ A ] weight [ 50 ]  [삭제]
   variant 2  [ B ] weight [ 50 ]  [삭제]
   [+ variant 추가]  (최대 4)     weight 합계: 100 / 100  ✓
  • 토스 패턴: 한도 조정 슬라이더(값 동시 표기), 설정 토글(명확한 on/off 라벨).
  • 검증: percentage 0~100, variant ≤4·weight 합 100, weight 합≠100이면 저장 버튼 비활성 + caption 경고색(warning).

S3 — 플래그 수정 /admin/feature-flags/[key]

┌───────────────────────────────────────────────────────────────┐
│ ← demo.feature.hello   ●ACTIVE   RELEASE      [변경 이력 보기]   │  ← key/type 읽기전용
│ 설명         [ 데모 인사 엔드포인트 킬스위치 ]                    │
│ ─ 평가 전략 ── (위 전략 폼과 동일, ARCHIVED면 읽기전용/비활성) ─  │
├───────────────────────────────────────────────────────────────┤
│ [아카이브] (destructive-outline)          [변경 저장] (primary)  │
│ (ARCHIVED 상태면 → [재활성] 노출, 저장/전략수정 비활성)          │
└───────────────────────────────────────────────────────────────┘
  • 토스 패턴: 송금(단일 주요 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를 사람이 읽는 전략 요약으로.

화면별 상태 표 (loading / empty / error / success) — 의무

화면loadingemptyerrorsuccess
S1 목록”불러오는 중…” + aria-busy 스켈레톤 행플래그 0건 → 일러스트 텍스트 “등록된 플래그가 없습니다” + [+ 플래그 추가] CTA (User Scenario 8)role="alert" 오류 배너 + [다시 시도] 버튼(refetch)테이블 렌더, 필터 반영. 필터 결과 0건은 “조건에 맞는 플래그가 없습니다”(빈 필터 결과 ≠ 전체 빈)
S2 생성제출 중 CTA 비활성 + 스피너해당 없음(입력 폼)필드 하단 인라인 에러(zod) + 서버 400(key 중복·percentage>100·variant>4·weight≠100)은 폼 상단 role="alert" 배너201 → 토스트 “생성됨” + S1로 이동
S3 수정상세 조회 스켈레톤 / 제출 중 CTA 비활성해당 없음404 → “플래그를 찾을 수 없습니다” 빈 상태 + 목록 링크 / 409(ARCHIVED 수정·이미 상태) → role="alert" 배너 / 400 인라인200 → 토스트 “저장됨”·“아카이브됨”·“재활성됨”
S4 전략 섹션(부모 폼 로딩에 종속)해당 없음weight 합≠100·percentage 범위 초과 → caption 경고(warning) + 저장 비활성유효 시 저장 활성
S5 이력”불러오는 중…” + aria-busy이력 0건 → “변경 이력이 없습니다”404/오류 → role="alert" + [다시 시도]테이블 + 페이징

모든 데이터 화면이 4상태를 처리한다(no-single-state 아님). BE 계약의 실패 코드(400/404/409/503)를 화면 상태로 1:1 매핑한다.

테마 토큰 정의 표 (시맨틱 토큰 → 라이트/다크 매핑) — 의무

기존 web/app/globals.css 토큰을 사용하고, 상태/타입 배지용 2종을 추가한다. 값은 HSL(H S% L%), Tailwind는 hsl(var(--token))로 참조.

기존 토큰 (globals.css — 그대로 사용)

시맨틱 토큰용도(본 화면)라이트다크
background페이지 배경0 0% 100%222.2 84% 4.9%
foreground기본 텍스트222.2 84% 4.9%210 40% 98%
card / card-foreground섹션 카드 배경/텍스트0 0% 100% / 222.2 84% 4.9%222.2 84% 4.9% / 210 40% 98%
muted / muted-foreground보조 배경(ARCHIVED 배지)/보조 텍스트(caption)210 40% 96.1% / 215.4 16.3% 46.9%217.2 32.6% 17.5% / 215 20.2% 65.1%
primary / primary-foreground주요 CTA(생성/저장)222.2 47.4% 11.2% / 210 40% 98%210 40% 98% / 222.2 47.4% 11.2%
secondary / secondary-foreground보조 버튼(취소)210 40% 96.1% / 222.2 47.4% 11.2%217.2 32.6% 17.5% / 210 40% 98%
accent / accent-foregroundhover·강조(전략 배지)210 40% 96.1% / 222.2 47.4% 11.2%217.2 32.6% 17.5% / 210 40% 98%
destructive / destructive-foreground위험 액션(아카이브), 서버 에러 배너0 84.2% 60.2% / 210 40% 98%0 62.8% 30.6% / 210 40% 98%
border테두리·구분선214.3 31.8% 91.4%217.2 32.6% 17.5%
input입력 필드 테두리214.3 31.8% 91.4%217.2 32.6% 17.5%
ring포커스 링222.2 84% 4.9%212.7 26.8% 83.9%

추가 토큰 (FE-02에서 globals.css + tailwind.config.ts에 신설)

시맨틱 토큰용도라이트다크
success / success-foregroundACTIVE 상태 배지, GLOBAL_TOGGLE ON142 71% 45% / 0 0% 100%142 64% 42% / 0 0% 100%
warning / warning-foregroundweight 합 불일치 경고, 정리 후보38 92% 50% / 0 0% 100%38 84% 46% / 0 0% 100%
  • 배지는 배경 bg-success/15 + 텍스트 text-success(투명도로 라이트/다크 동시 대응)로 표현해 대비 유지.
  • 하드코딩 색 전면 금지#fff·text-gray-500·bg-green-100 등 사용 금지(no-hardcoded-color). MCP 페이지의 하드코딩은 답습하지 않는다.
  • 다크 모드: 모든 색이 토큰 참조라 .dark 클래스 하에서 자동 전환. 별도 다크 전용 클래스 작성 불필요. 두 모드 모두 완료 조건 — 테스트/개발에서 <html class="dark"> 토글로 검증(테마 토글 UI는 본 과제 범위 밖, 플랫폼 공통 관심사).

컴포넌트 트리 (컨테이너/프레젠테이션 분리)

app/admin/layout.tsx (기존)
  └─ AdminSidebar (기존, FE-11에서 "피처 플래그" nav 추가)
  └─ app/admin/feature-flags/
       ├─ page.tsx  [컨테이너, "use client"]                 ← S1
       │    ├─ FeatureFlagFilters   [프레젠테이션]            (status/type select)
       │    └─ FeatureFlagTable     [프레젠테이션]
       │         └─ StatusBadge / TypeBadge / StrategySummary [프레젠테이션, 재사용]
       ├─ new/page.tsx [컨테이너] ─ FeatureFlagForm            ← S2
       │    └─ StrategyForm [프레젠테이션 복합, 재사용]         ← S4 포함
       │         ├─ GlobalToggleField
       │         ├─ PercentageRolloutField (range+number)
       │         ├─ AttributeMatchField
       │         └─ VariantBucketingField (variant 리스트)
       ├─ [key]/page.tsx [컨테이너] ─ FeatureFlagForm + 상태전이 액션  ← S3
       └─ [key]/audit-logs/page.tsx [컨테이너] ─ AuditLogTable        ← S5
              └─ ChangeTypeBadge / StrategySummary [재사용]
  • 컨테이너(page): 훅으로 서버 상태 조회·mutation 호출·상태 분기(loading/empty/error). “use client”.
  • 프레젠테이션(배지·테이블·폼 필드): props만 받아 렌더, 비즈니스 로직 없음. 데이터 가공은 훅/유틸로(no-logic-in-component).
  • 재사용 식별: StatusBadge·TypeBadge·StrategySummary·ChangeTypeBadge는 S1·S3·S5 공유. StrategyForm(+하위 필드)은 S2·S3 공유.

상태관리 설계

상태종류소유근거
플래그 목록서버 상태useFeatureFlags(filters)서버 SSOT, 훅이 조회·refetch. 스토어 복사 금지
플래그 상세서버 상태useFeatureFlag(key)상세 화면 진입 시 조회
감사 로그서버 상태useFlagAuditLogs(key, page)페이징
생성/수정 mutation서버 쓰기createFeatureFlag·updateFeatureFlag·archiveFeatureFlag·activateFeatureFlag 함수훅이 아닌 명령 함수, 성공 시 부모가 refetch/라우팅
폼 입력값지역 상태useState(각 폼)화면 지역, 전역 불필요
필터(status/type)지역 상태useState(S1)URL query 동기화 선택 가능(선례 audit-logs는 지역 state)
토스트UI 상태components/ui/toast기존 프리미티브
  • 전역 스토어 미도입 — 화면 간 공유 클라이언트 전역 상태 없음. 서버 상태는 Query 캐시 대신 레포 관례(훅)가 SSOT. no-global-by-default 준수.
  • 낙관적 업데이트 미도입 — 관리 화면은 저빈도·정확성 우선. 저장 후 refetch로 서버 값 재확인(킬스위치 전파 지연이 있어 낙관적 표시는 오히려 오해 소지). 실패 롤백 복잡도 회피.

API 연동 표 (화면 → 엔드포인트 → 훅/함수 → 에러 처리) — BE 계약 1:1

브라우저는 동일 출처 BFF(/api/admin/feature-flags/**)만 호출한다. BFF가 BE /admin/feature-flags/**로 프록시(forwardBeResponse). 아래 “BE 계약”은 TDD “API 계약” 섹션과 필드·타입 일치.

화면BFF 경로BE 계약(TDD)훅/함수성공에러 처리
S1 목록GET /api/admin/feature-flags?status=&type=GET /admin/feature-flags?status=&type= → 200 FeatureFlagResponse[]useFeatureFlags({status?,type?})테이블 / 빈 목록 []5xx→오류 배너+재시도
S2 생성POST /api/admin/feature-flagsPOST /admin/feature-flags body {key,type,description,strategy} → 201 FeatureFlagResponsecreateFeatureFlag(input)토스트+S1 이동400(key중복·percentage>100·variant>4·weight≠100·EXPERIMENT아닌데variant)→배너/인라인
S3 상세GET /api/admin/feature-flags/{key}GET /admin/feature-flags/{key} → 200 FeatureFlagResponseuseFeatureFlag(key)폼 프리필404→빈 상태
S3 수정PUT /api/admin/feature-flags/{key}PUT /admin/feature-flags/{key} body {description,strategy} → 200updateFeatureFlag(key,input)토스트 저장됨400/404/409(ARCHIVED 수정)→배너
S3 아카이브POST /api/admin/feature-flags/{key}/archivePOST .../archive → 200 {key,status:"ARCHIVED"}archiveFeatureFlag(key)토스트+재활성 버튼 노출404/409(이미 ARCHIVED)→배너
S3 재활성POST /api/admin/feature-flags/{key}/activatePOST .../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 금지). strategyz.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개.

레벨대상케이스 예시
스키마(유닛)zod 스키마유효 strategy 4종 파싱 통과 / percentage 101이면 파싱 실패 / variant weight 합 90이면 실패 / EXPERIMENT 아닌데 VARIANT면 실패
useFeatureFlags성공 시 data 반환·isLoading false / 5xx 시 error 세팅 / refetch 재조회 (fetch mock)
컴포넌트StatusBadge/TypeBadge/StrategySummaryACTIVE→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-03senior-pm NEEDS_REVISION 반영 — 감사 로그를 total 포함 페이지 응답으로 소비하도록 개정(FeatureFlagAuditLogPage wire + canonical 뷰 정규화, 필드명 차이는 api.ts 1곳 격리). S5 “1 / N” 총페이지 표시 유지·배열 폴백 제거. 목록 훅도 total 흡수 가능하게 유연화. FE-01·FE-04·FE-10 티켓 동기 갱신