@potenlab/ui 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +361 -0
  2. package/dist/cli.js +756 -0
  3. package/package.json +8 -5
  4. package/template/admin/README.md +36 -0
  5. package/template/admin/_gitignore +41 -0
  6. package/template/admin/components.json +23 -0
  7. package/template/admin/docs/changes.json +295 -0
  8. package/template/admin/docs/dev-plan.md +822 -0
  9. package/template/admin/docs/frontend-plan.md +874 -0
  10. package/template/admin/docs/prd.md +408 -0
  11. package/template/admin/docs/progress.json +777 -0
  12. package/template/admin/docs/test-plan.md +790 -0
  13. package/template/admin/docs/ui-ux-plan.md +1664 -0
  14. package/template/admin/eslint.config.mjs +18 -0
  15. package/template/admin/next.config.ts +7 -0
  16. package/template/admin/package.json +43 -0
  17. package/template/admin/postcss.config.mjs +7 -0
  18. package/template/admin/public/avatars/user1.svg +4 -0
  19. package/template/admin/public/avatars/user2.svg +4 -0
  20. package/template/admin/public/avatars/user3.svg +4 -0
  21. package/template/admin/public/avatars/user4.svg +4 -0
  22. package/template/admin/public/avatars/user5.svg +4 -0
  23. package/template/admin/public/file.svg +1 -0
  24. package/template/admin/public/globe.svg +1 -0
  25. package/template/admin/public/next.svg +1 -0
  26. package/template/admin/public/profile/img1.svg +7 -0
  27. package/template/admin/public/profile/img2.svg +7 -0
  28. package/template/admin/public/profile/img3.svg +7 -0
  29. package/template/admin/public/vercel.svg +1 -0
  30. package/template/admin/public/window.svg +1 -0
  31. package/template/admin/src/app/favicon.ico +0 -0
  32. package/template/admin/src/app/layout.tsx +38 -0
  33. package/template/admin/src/app/page.tsx +5 -0
  34. package/template/admin/src/app/users/[id]/page.tsx +10 -0
  35. package/template/admin/src/components/layouts/app-sidebar.tsx +152 -0
  36. package/template/admin/src/components/user-management/profile-images.tsx +69 -0
  37. package/template/admin/src/components/user-management/user-detail-form.tsx +143 -0
  38. package/template/admin/src/features/user-management/components/user-columns.tsx +101 -0
  39. package/template/admin/src/features/user-management/components/user-detail.tsx +79 -0
  40. package/template/admin/src/features/user-management/components/user-list.tsx +74 -0
  41. package/template/admin/src/features/user-management/types/index.ts +113 -0
  42. package/template/admin/src/features/user-management/utils/format.ts +2 -0
  43. package/template/admin/src/lib/mock-data.ts +131 -0
  44. package/template/admin/src/styles/globals.css +26 -0
  45. package/template/admin/tsconfig.json +34 -0
@@ -0,0 +1,874 @@
1
+ # Frontend Plan
2
+
3
+ Single source of truth for component specifications, TypeScript interfaces, data flow, and implementation order for the Potenlab Admin User Management UI.
4
+
5
+ - **Source PRD:** `template/docs/prd.md`
6
+ - **Source UI/UX:** `template/docs/ui-ux-plan.md`
7
+ - **Source Dev Plan:** `template/docs/dev-plan.md`
8
+ - **Tech Stack:** Next.js 16 (App Router), TypeScript, shadcn/ui, Tailwind CSS 4, Lucide React, Bun
9
+ - **Fonts:** Pretendard Variable, Inter
10
+ - **Platform:** Desktop-only (1920px target)
11
+ - **Backend:** NONE -- all data is static/mock, hardcoded in the frontend
12
+
13
+ ---
14
+
15
+ ## 1. Component Index Table
16
+
17
+ Every component in the project, organized by location and role.
18
+
19
+ ### 1.1 shadcn/ui Base Components (`src/components/ui/`)
20
+
21
+ | # | File Path | Type | Props Interface | shadcn Base | Description |
22
+ |---|-----------|------|-----------------|-------------|-------------|
23
+ | 1 | `src/components/ui/accordion.tsx` | ui | `AccordionProps` (Radix) | `Accordion` | Expandable/collapsible sections for sidebar nav |
24
+ | 2 | `src/components/ui/avatar.tsx` | ui | `AvatarProps` (Radix) | `Avatar` | 22px circular user avatar in table rows |
25
+ | 3 | `src/components/ui/badge.tsx` | ui | `BadgeProps` with `variant: "default" \| "green"` | `Badge` | Green pill badge for user count on dashboard header |
26
+ | 4 | `src/components/ui/button.tsx` | ui | `ButtonProps` with `variant: "primary" \| "secondary" \| "pagination" \| "ghost" \| "outline"` | `Button` | All interactive buttons with custom variant styles |
27
+ | 5 | `src/components/ui/card.tsx` | ui | `CardProps` (shadcn) | `Card` | White card container on both pages |
28
+ | 6 | `src/components/ui/input.tsx` | ui | `InputProps` (React.InputHTMLAttributes) | `Input` | Text input for search bar and detail form fields |
29
+ | 7 | `src/components/ui/label.tsx` | ui | `LabelProps` (Radix) | `Label` | Form field labels in detail page |
30
+ | 8 | `src/components/ui/select.tsx` | ui | `SelectProps` (Radix) | `Select` | Dropdown for Role, Gender, Exercise Style, etc. |
31
+ | 9 | `src/components/ui/separator.tsx` | ui | `SeparatorProps` (Radix) | `Separator` | Horizontal dividers between sections |
32
+ | 10 | `src/components/ui/sidebar.tsx` | ui | `SidebarProps` (shadcn) | `Sidebar` | Sidebar layout primitives |
33
+ | 11 | `src/components/ui/switch.tsx` | ui | `SwitchProps` (Radix) | `Switch` | Toggle switches for Other Settings |
34
+ | 12 | `src/components/ui/table.tsx` | ui | `TableProps` (shadcn) | `Table` | Data table with header, rows, cells |
35
+ | 13 | `src/components/ui/tabs.tsx` | ui | `TabsProps` (Radix) | `Tabs` | Tab navigation on dashboard |
36
+ | 14 | `src/components/ui/tooltip.tsx` | ui | `TooltipProps` (Radix) | `Tooltip` | Optional hover hints for icon-only buttons |
37
+
38
+ ### 1.2 Layout Components (`src/components/layouts/`)
39
+
40
+ | # | File Path | Type | Props Interface | shadcn Base | Description |
41
+ |---|-----------|------|-----------------|-------------|-------------|
42
+ | 15 | `src/components/layouts/app-sidebar.tsx` | layout | None (reads `usePathname()`) | `Sidebar` + `Accordion` | Fixed 300px sidebar with ADMIN header, accordion nav, logout footer |
43
+ | 16 | `src/components/layouts/content-layout.tsx` | layout | `ContentLayoutProps` | None | Content area wrapper with `ml-[324px]`, padding |
44
+
45
+ ### 1.3 Common Components (`src/components/common/`)
46
+
47
+ | # | File Path | Type | Props Interface | shadcn Base | Description |
48
+ |---|-----------|------|-----------------|-------------|-------------|
49
+ | 17 | `src/components/common/page-header.tsx` | common | `PageHeaderProps` | `Badge`, `Button`, `Separator` | Reusable title + badge + subtitle + action button + separator |
50
+
51
+ ### 1.4 Feature-Specific Styled/Presentational Components (`src/components/user-management/`)
52
+
53
+ | # | File Path | Type | Props Interface | shadcn Base | Description |
54
+ |---|-----------|------|-----------------|-------------|-------------|
55
+ | 18 | `src/components/user-management/user-table.tsx` | styled | `UserTableProps` | `Table`, `Avatar`, `Button(ghost)` | Data table with 10 columns, 5 rows, row click navigation, delete action |
56
+ | 19 | `src/components/user-management/pagination-controls.tsx` | styled | `PaginationControlsProps` | `Button(pagination)`, `Input`, `Select` | Pagination nav buttons + page jump + items per page |
57
+ | 20 | `src/components/user-management/search-bar.tsx` | styled | `SearchBarProps` | `Input` | Full-width search input with placeholder |
58
+ | 21 | `src/components/user-management/tab-navigation.tsx` | styled | `TabNavigationProps` | `Tabs`, `TabsList`, `TabsTrigger` | 3-tab bar (All, Tab, Tab) with active/inactive states |
59
+ | 22 | `src/components/user-management/user-detail-form.tsx` | styled | `UserDetailFormProps` | `Input`, `Select`, `Switch`, `Label`, `Separator` | Full detail form: Basic Info fields + Other Settings toggles |
60
+ | 23 | `src/components/user-management/profile-images.tsx` | styled | `ProfileImagesProps` | None | 3x profile image display (116x116px, 8px radius) |
61
+
62
+ ### 1.5 Route Pages / Business Logic Assembly (`src/app/`)
63
+
64
+ | # | File Path | Type | Props Interface | Composes | Description |
65
+ |---|-----------|------|-----------------|----------|-------------|
66
+ | 24 | `src/app/layout.tsx` | business | `RootLayoutProps` | `AppSidebar`, `ContentLayout` | Root layout: sidebar + content + fonts + skip link |
67
+ | 25 | `src/app/page.tsx` | business | Next.js page props | `Card`, `PageHeader`, `TabNavigation`, `SearchBar`, `PaginationControls`, `UserTable` | Dashboard: User Management List page assembly |
68
+ | 26 | `src/app/users/[id]/page.tsx` | business | `{ params: { id: string } }` | `Card`, `PageHeader`, `UserDetailForm` | User Detail page assembly, reads `params.id` |
69
+
70
+ ### 1.6 Utility / Data Files
71
+
72
+ | # | File Path | Type | Description |
73
+ |---|-----------|------|-------------|
74
+ | 27 | `src/lib/utils.ts` | utility | shadcn/ui `cn()` helper (clsx + tailwind-merge) |
75
+ | 28 | `src/lib/mock-data.ts` | data | Static mock user data (5 rows) + `totalUserCount` |
76
+ | 29 | `src/features/user-management/types/index.ts` | types | `User` interface, `UserTableColumn` type |
77
+ | 30 | `src/features/user-management/utils/format.ts` | utility | `formatNumber()` for comma-separated numbers |
78
+ | 31 | `src/types/index.ts` | types | Shared global TypeScript types |
79
+ | 32 | `src/styles/globals.css` | styles | Tailwind CSS 4 + shadcn/ui theme overrides + font imports |
80
+
81
+ ---
82
+
83
+ ## 2. TypeScript Interfaces
84
+
85
+ ### 2.1 User Type
86
+
87
+ ```typescript
88
+ // src/features/user-management/types/index.ts
89
+
90
+ export interface User {
91
+ id: string;
92
+ nickname: string;
93
+ grade: string;
94
+ avatar: string;
95
+ phone: string;
96
+ age: string;
97
+ gender: string;
98
+ region: string;
99
+ joinDate: string;
100
+ withdrawalDate: string;
101
+ role: string;
102
+ exerciseStyle: string;
103
+ gymRelocation: string;
104
+ bench: string;
105
+ deadlift: string;
106
+ squat: string;
107
+ intro: string;
108
+ profileImages: string[];
109
+ settings: UserSettings;
110
+ }
111
+
112
+ export interface UserSettings {
113
+ profilePublic: boolean;
114
+ matchChatNotification: boolean;
115
+ marketingNotification: boolean;
116
+ }
117
+ ```
118
+
119
+ ### 2.2 Table Column Definitions
120
+
121
+ ```typescript
122
+ // src/features/user-management/types/index.ts
123
+
124
+ export interface UserTableColumn {
125
+ key: keyof User | "delete";
126
+ label: string;
127
+ width: string; // CSS width: "flex-1", "80px", "57px"
128
+ sortable: boolean;
129
+ alignment: "left" | "center";
130
+ }
131
+
132
+ export const USER_TABLE_COLUMNS: UserTableColumn[] = [
133
+ { key: "nickname", label: "Nickname", width: "flex-1", sortable: false, alignment: "left" },
134
+ { key: "grade", label: "Grade", width: "flex-1", sortable: false, alignment: "left" },
135
+ { key: "avatar", label: "Avatar", width: "80px", sortable: false, alignment: "center" },
136
+ { key: "phone", label: "Phone Number", width: "flex-1", sortable: false, alignment: "left" },
137
+ { key: "age", label: "Age", width: "flex-1", sortable: false, alignment: "left" },
138
+ { key: "gender", label: "Gender", width: "flex-1", sortable: false, alignment: "left" },
139
+ { key: "region", label: "Region", width: "flex-1", sortable: false, alignment: "left" },
140
+ { key: "joinDate", label: "Join Date", width: "flex-1", sortable: true, alignment: "left" },
141
+ { key: "withdrawalDate", label: "Withdrawal Date", width: "flex-1", sortable: true, alignment: "left" },
142
+ { key: "delete", label: "Delete", width: "57px", sortable: false, alignment: "left" },
143
+ ];
144
+ ```
145
+
146
+ ### 2.3 Pagination State
147
+
148
+ ```typescript
149
+ // src/features/user-management/types/index.ts
150
+
151
+ export interface PaginationState {
152
+ currentPage: number;
153
+ totalPages: number;
154
+ itemsPerPage: number;
155
+ pageJumpInput: string;
156
+ }
157
+
158
+ export const DEFAULT_PAGINATION: PaginationState = {
159
+ currentPage: 1,
160
+ totalPages: 1,
161
+ itemsPerPage: 10,
162
+ pageJumpInput: "",
163
+ };
164
+ ```
165
+
166
+ ### 2.4 Navigation / Sidebar Types
167
+
168
+ ```typescript
169
+ // src/types/index.ts
170
+
171
+ import type { LucideIcon } from "lucide-react";
172
+
173
+ export interface SidebarNavItem {
174
+ label: string;
175
+ icon: LucideIcon;
176
+ href?: string;
177
+ isAccordion: boolean;
178
+ children?: SidebarSubItem[];
179
+ }
180
+
181
+ export interface SidebarSubItem {
182
+ label: string;
183
+ href: string;
184
+ }
185
+ ```
186
+
187
+ ### 2.5 Component Props Interfaces
188
+
189
+ ```typescript
190
+ // src/components/common/page-header.tsx
191
+ import type { LucideIcon } from "lucide-react";
192
+
193
+ export interface PageHeaderProps {
194
+ title: string;
195
+ subtitle: string;
196
+ badgeCount?: number;
197
+ actionLabel: string;
198
+ actionIcon?: LucideIcon;
199
+ onAction?: () => void;
200
+ }
201
+ ```
202
+
203
+ ```typescript
204
+ // src/components/layouts/content-layout.tsx
205
+ export interface ContentLayoutProps {
206
+ children: React.ReactNode;
207
+ }
208
+ ```
209
+
210
+ ```typescript
211
+ // src/components/user-management/user-table.tsx
212
+ import type { User } from "@/features/user-management/types";
213
+
214
+ export interface UserTableProps {
215
+ users: User[];
216
+ }
217
+ ```
218
+
219
+ ```typescript
220
+ // src/components/user-management/pagination-controls.tsx
221
+ export interface PaginationControlsProps {
222
+ currentPage: number;
223
+ totalPages: number;
224
+ itemsPerPage: number;
225
+ onPageChange: (page: number) => void;
226
+ onItemsPerPageChange: (count: number) => void;
227
+ }
228
+ ```
229
+
230
+ ```typescript
231
+ // src/components/user-management/search-bar.tsx
232
+ export interface SearchBarProps {
233
+ value: string;
234
+ onChange: (value: string) => void;
235
+ placeholder?: string;
236
+ }
237
+ ```
238
+
239
+ ```typescript
240
+ // src/components/user-management/tab-navigation.tsx
241
+ export interface TabNavigationProps {
242
+ defaultValue?: string;
243
+ onTabChange?: (value: string) => void;
244
+ }
245
+ ```
246
+
247
+ ```typescript
248
+ // src/components/user-management/user-detail-form.tsx
249
+ import type { User } from "@/features/user-management/types";
250
+
251
+ export interface UserDetailFormProps {
252
+ user: User;
253
+ }
254
+ ```
255
+
256
+ ```typescript
257
+ // src/components/user-management/profile-images.tsx
258
+ export interface ProfileImagesProps {
259
+ images: string[];
260
+ }
261
+ ```
262
+
263
+ ### 2.6 Form Field Types (Detail Page)
264
+
265
+ ```typescript
266
+ // src/features/user-management/types/index.ts
267
+
268
+ export type FormFieldType = "input" | "select";
269
+
270
+ export interface FormField {
271
+ key: keyof User;
272
+ label: string;
273
+ type: FormFieldType;
274
+ options?: string[]; // Only for type "select"
275
+ }
276
+
277
+ export const BASIC_INFO_ROW_1: FormField[] = [
278
+ { key: "role", label: "Role", type: "select", options: ["User", "Admin"] },
279
+ { key: "nickname", label: "Nickname", type: "input" },
280
+ { key: "phone", label: "Phone", type: "input" },
281
+ { key: "age", label: "Age", type: "input" },
282
+ ];
283
+
284
+ export const BASIC_INFO_ROW_2: FormField[] = [
285
+ { key: "gender", label: "Gender", type: "select", options: ["Male", "Female"] },
286
+ { key: "exerciseStyle", label: "Exercise Style", type: "select", options: ["Bodybuilding", "Crossfit", "Cardio"] },
287
+ { key: "gymRelocation", label: "Gym Relocation", type: "select", options: ["Available", "Not Available"] },
288
+ { key: "region", label: "Region", type: "select", options: ["Seoul Mapo-gu", "Gangnam-gu", "Songpa-gu"] },
289
+ ];
290
+
291
+ export const BASIC_INFO_ROW_3: FormField[] = [
292
+ { key: "bench", label: "Bench", type: "input" },
293
+ { key: "deadlift", label: "Deadlift", type: "input" },
294
+ { key: "squat", label: "Squat", type: "input" },
295
+ ];
296
+ ```
297
+
298
+ ---
299
+
300
+ ## 3. shadcn/ui Component Mapping
301
+
302
+ ### 3.1 Installation
303
+
304
+ ```bash
305
+ bunx --bun shadcn@latest init
306
+ # Select: New York style, CSS variables enabled
307
+
308
+ bunx --bun shadcn@latest add card button table input select badge avatar switch tabs separator accordion sidebar label tooltip
309
+ ```
310
+
311
+ ### 3.2 Component Customization Specifications
312
+
313
+ #### 3.2.1 Button (`src/components/ui/button.tsx`)
314
+
315
+ | Variant | Background | Text | Height | Radius | Font | Additional |
316
+ |---------|-----------|------|--------|--------|------|------------|
317
+ | `primary` | `bg-primary` | `text-white` | `h-10` (40px) | `rounded-lg` (8px) | Inter 18px SemiBold | hover: `bg-primary-hover`, active: `bg-primary-active scale-[0.98]`, disabled: `opacity-50 cursor-not-allowed pointer-events-none` |
318
+ | `secondary` | `bg-sidebar-selected` | `text-black` | `h-12` (48px) | `rounded-md` (6px) | Inter 18px SemiBold | -- |
319
+ | `pagination` | `bg-primary-light` | `text-primary` | `h-10 w-10` (40x40) | `rounded-md` (6px) | -- | hover: `bg-[#A0C4C3] text-primary-hover`, disabled: `opacity-50 cursor-not-allowed` |
320
+ | `ghost` | `bg-transparent` | `text-delete-text` (#3F7A79) | `h-auto` | `p-0` | Body 14px | hover: `underline text-primary-hover` |
321
+
322
+ **Focus ring (all variants):** `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2`
323
+
324
+ **CSS customization:**
325
+ ```tsx
326
+ const buttonVariants = cva(
327
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors duration-150 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
328
+ {
329
+ variants: {
330
+ variant: {
331
+ primary: "bg-primary hover:bg-primary-hover active:bg-primary-active active:scale-[0.98] text-white font-inter text-[18px] font-semibold",
332
+ secondary: "bg-sidebar-selected text-black font-inter text-[18px] font-semibold",
333
+ pagination: "bg-primary-light text-primary hover:bg-[#A0C4C3] hover:text-primary-hover",
334
+ ghost: "bg-transparent text-delete-text hover:underline hover:text-primary-hover p-0 h-auto text-[14px]",
335
+ outline: "border border-border bg-transparent text-black hover:bg-sidebar-selected",
336
+ },
337
+ size: {
338
+ default: "h-10 px-4 rounded-lg",
339
+ secondary: "h-12 px-4 rounded-md",
340
+ pagination: "h-10 w-10 rounded-md",
341
+ ghost: "h-auto p-0",
342
+ },
343
+ },
344
+ defaultVariants: {
345
+ variant: "primary",
346
+ size: "default",
347
+ },
348
+ }
349
+ );
350
+ ```
351
+
352
+ #### 3.2.2 Table (`src/components/ui/table.tsx`)
353
+
354
+ | Sub-component | CSS Customization |
355
+ |--------------|-------------------|
356
+ | `Table` (wrapper) | `border border-border bg-white` (no border-radius) |
357
+ | `TableHeader` | `bg-table-header` (#F9FAFC) |
358
+ | `TableHead` | `h-14 text-[14px] font-semibold text-table-cell px-4 align-middle` |
359
+ | `TableRow` | `h-[60px] border-b border-border hover:bg-table-header cursor-pointer transition-colors duration-150` |
360
+ | `TableCell` | `px-4 text-[14px] text-table-cell align-middle` |
361
+
362
+ #### 3.2.3 Input (`src/components/ui/input.tsx`)
363
+
364
+ | State | CSS |
365
+ |-------|-----|
366
+ | Base | `border border-border rounded-md font-inter text-[18px] text-black placeholder:text-placeholder bg-white px-4` |
367
+ | Hover | `hover:border-[#CBD5E0]` |
368
+ | Focus | `focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-[border-color,box-shadow] duration-150` |
369
+ | Disabled | `disabled:bg-table-header disabled:text-placeholder disabled:cursor-not-allowed` |
370
+
371
+ Height set at call site: Dashboard search `h-12` (48px), Detail form `h-[52px]` (52px).
372
+
373
+ #### 3.2.4 Select (`src/components/ui/select.tsx`)
374
+
375
+ | Sub-component | CSS Customization |
376
+ |--------------|-------------------|
377
+ | `SelectTrigger` | `h-[52px] border border-border rounded-md px-4 pr-12 font-inter text-[18px] text-[#2D3748] bg-white hover:border-[#CBD5E0] focus:border-primary focus:ring-2 focus:ring-primary/20` |
378
+ | `SelectContent` | `bg-white border border-border rounded-md shadow-md` |
379
+ | `SelectItem` | `hover:bg-sidebar-selected px-4 py-2 text-[18px]` |
380
+
381
+ Chevron rotates 180deg on open (300ms ease-in-out). Chevron color: default `#A0AEC0`, on focus/open `#509594`.
382
+
383
+ #### 3.2.5 Badge (`src/components/ui/badge.tsx`)
384
+
385
+ | Variant | CSS |
386
+ |---------|-----|
387
+ | `default` | (shadcn default) |
388
+ | `green` | `bg-badge-green-bg text-badge-green-text font-inter text-[18px] font-bold px-3 py-1 rounded-full` |
389
+
390
+ #### 3.2.6 Avatar (`src/components/ui/avatar.tsx`)
391
+
392
+ | Element | CSS |
393
+ |---------|-----|
394
+ | `Avatar` (table context) | `w-[22px] h-[22px] rounded-full` |
395
+ | `AvatarFallback` | `bg-sidebar-selected text-muted-fg text-[10px]` (first letter of nickname) |
396
+ | `AvatarImage` | Must include `alt` text |
397
+
398
+ #### 3.2.7 Switch (`src/components/ui/switch.tsx`)
399
+
400
+ | State | Track | Thumb |
401
+ |-------|-------|-------|
402
+ | OFF | `bg-toggle-off` (#CBD5E0) | White, positioned left |
403
+ | OFF hover | `bg-[#B0BEC5]` | White, positioned left |
404
+ | ON | `bg-toggle-on` (#3B82F6) | White, positioned right |
405
+ | ON hover | `bg-[#2563EB]` | White, positioned right |
406
+ | Disabled | 50% opacity | -- |
407
+
408
+ Track: `w-[44px] h-[24px] rounded-full`. Thumb: 20px with 2px inset. Transition: `200ms ease-in-out`.
409
+
410
+ #### 3.2.8 Tabs (`src/components/ui/tabs.tsx`)
411
+
412
+ | Element | CSS |
413
+ |---------|-----|
414
+ | `TabsList` | `gap-2 bg-transparent p-0` |
415
+ | `TabsTrigger` active | `data-[state=active]:bg-primary data-[state=active]:text-white font-pretendard font-semibold h-[52px] px-4 rounded-md` |
416
+ | `TabsTrigger` inactive | `bg-inactive-tab text-inactive-tab-text font-pretendard font-medium h-[52px] px-4 rounded-md hover:bg-border` |
417
+
418
+ Transition: `200ms ease-in-out` on background-color and color.
419
+
420
+ #### 3.2.9 Separator (`src/components/ui/separator.tsx`)
421
+
422
+ Default color: `bg-divider` (#EFF1F4). Height: 1px. Full width.
423
+
424
+ #### 3.2.10 Accordion (`src/components/ui/accordion.tsx`)
425
+
426
+ Chevron rotates 180deg on expand (300ms ease-in-out). Content: animated height expand/collapse (300ms ease-in-out). Trigger text: `font-inter text-[16px] text-muted-fg`.
427
+
428
+ #### 3.2.11 Label (`src/components/ui/label.tsx`)
429
+
430
+ Style: `font-pretendard text-[20px] font-semibold text-[#101010]`. Margin: `mb-2` (8px). Uses `htmlFor` to associate with input `id`.
431
+
432
+ #### 3.2.12 Card (`src/components/ui/card.tsx`)
433
+
434
+ Override: `bg-white border border-border rounded-lg p-8` (32px padding, 8px radius). Shadow: none.
435
+
436
+ #### 3.2.13 Sidebar (`src/components/ui/sidebar.tsx`)
437
+
438
+ Width: 300px fixed. Height: 100vh. Background: white. Border-right: `border-r border-border`.
439
+
440
+ #### 3.2.14 Tooltip (`src/components/ui/tooltip.tsx`)
441
+
442
+ Background: `#1A202C`. Text: `#FFFFFF`, 14px Regular. Padding: `8px 12px`. Radius: 6px. Delay: 300ms show, 100ms hide.
443
+
444
+ ---
445
+
446
+ ## 4. Business vs Styled Component Separation
447
+
448
+ ### 4.1 Principle
449
+
450
+ - **`src/app/` (Business / Page Assembly):** Route pages that import mock data, compose presentational components, manage page-level state, and handle navigation logic. These are the "smart" components.
451
+ - **`src/components/user-management/` (Styled / Presentational):** Receive data and callbacks via props. No direct data imports. No routing logic. Pure rendering and local UI state only (e.g., toggle state, input focus).
452
+
453
+ ### 4.2 Business Components (Page Assembly)
454
+
455
+ | Component | Location | Responsibilities |
456
+ |-----------|----------|-----------------|
457
+ | `Dashboard` | `src/app/page.tsx` | Imports `mockUsers` and `totalUserCount` from `lib/mock-data.ts`. Passes data to `UserTable`. Passes `totalUserCount` to `PageHeader`. Manages local state for search, tab, pagination. Composes all dashboard sub-components inside `Card`. |
458
+ | `UserDetailPage` | `src/app/users/[id]/page.tsx` | Reads `params.id` from route. Looks up user from `mockUsers` by id. Falls back to first user if not found. Passes user to `UserDetailForm`. Composes `PageHeader` + `UserDetailForm` inside `Card`. |
459
+ | `RootLayout` | `src/app/layout.tsx` | Loads fonts (Inter via `next/font/google`, Pretendard via CDN). Renders `AppSidebar` persistently. Wraps `children` in `ContentLayout`. Provides skip link. Sets `<html lang="ko">`. |
460
+
461
+ ### 4.3 Styled / Presentational Components
462
+
463
+ | Component | Location | Receives via Props | Local State |
464
+ |-----------|----------|--------------------|-------------|
465
+ | `UserTable` | `src/components/user-management/user-table.tsx` | `users: User[]` | None (navigation via `useRouter`) |
466
+ | `PaginationControls` | `src/components/user-management/pagination-controls.tsx` | `currentPage`, `totalPages`, `itemsPerPage`, callbacks | Page jump input value |
467
+ | `SearchBar` | `src/components/user-management/search-bar.tsx` | `value`, `onChange`, `placeholder` | None (controlled) |
468
+ | `TabNavigation` | `src/components/user-management/tab-navigation.tsx` | `defaultValue`, `onTabChange` | Active tab (via Radix Tabs) |
469
+ | `UserDetailForm` | `src/components/user-management/user-detail-form.tsx` | `user: User` | Form field values (local edits), toggle states |
470
+ | `ProfileImages` | `src/components/user-management/profile-images.tsx` | `images: string[]` | Image load error fallback |
471
+ | `PageHeader` | `src/components/common/page-header.tsx` | `title`, `subtitle`, `badgeCount?`, `actionLabel`, `actionIcon?`, `onAction?` | None |
472
+ | `AppSidebar` | `src/components/layouts/app-sidebar.tsx` | None | Reads `usePathname()` for active state |
473
+ | `ContentLayout` | `src/components/layouts/content-layout.tsx` | `children` | None |
474
+
475
+ ---
476
+
477
+ ## 5. State Management
478
+
479
+ ### 5.1 Overview
480
+
481
+ This is a frontend-only project with static mock data. No global state management library (e.g., Zustand, Redux) is needed. All state is local component state using React `useState`.
482
+
483
+ ### 5.2 State by Component
484
+
485
+ | State Variable | Type | Owner Component | Initialization | Description |
486
+ |---------------|------|-----------------|----------------|-------------|
487
+ | `activeTab` | `string` | `src/app/page.tsx` | `"all"` | Currently selected tab (All, Tab, Tab) |
488
+ | `searchValue` | `string` | `src/app/page.tsx` | `""` | Text in search input (UI only, no filtering) |
489
+ | `currentPage` | `number` | `src/app/page.tsx` | `1` | Current page number in pagination |
490
+ | `itemsPerPage` | `number` | `src/app/page.tsx` | `10` | Items per page selection |
491
+ | `pageJumpInput` | `string` | `pagination-controls.tsx` | `""` | Page jump text input value |
492
+ | `formValues` | `Partial<User>` | `user-detail-form.tsx` | Spread from `user` prop | Editable copy of user fields |
493
+ | `toggleStates` | `UserSettings` | `user-detail-form.tsx` | From `user.settings` | Toggle ON/OFF states for 3 switches |
494
+
495
+ ### 5.3 URL Parameters
496
+
497
+ | Parameter | Route | Usage |
498
+ |-----------|-------|-------|
499
+ | `id` | `/users/[id]` | Dynamic route param to select user from mock data |
500
+
501
+ ### 5.4 No Global State Needed
502
+
503
+ - No Zustand, Redux, or Context API for state management
504
+ - No server state (no TanStack Query, SWR)
505
+ - All data comes from `src/lib/mock-data.ts` as static imports
506
+ - Page-level state lives in page components and is passed down via props
507
+
508
+ ---
509
+
510
+ ## 6. Data Flow
511
+
512
+ ### 6.1 Data Flow Diagram
513
+
514
+ ```
515
+ src/lib/mock-data.ts
516
+ |
517
+ |-- exports: mockUsers (User[]), totalUserCount (number)
518
+ |
519
+ +---> src/app/page.tsx (Dashboard)
520
+ | |
521
+ | |-- imports mockUsers, totalUserCount
522
+ | |-- passes totalUserCount to PageHeader (badgeCount)
523
+ | |-- passes mockUsers to UserTable (users)
524
+ | |-- manages local state: searchValue, activeTab, currentPage, itemsPerPage
525
+ | |
526
+ | +---> PageHeader (title, subtitle, badgeCount, actionLabel)
527
+ | +---> TabNavigation (defaultValue, onTabChange)
528
+ | +---> SearchBar (value, onChange)
529
+ | +---> PaginationControls (currentPage, totalPages, itemsPerPage, callbacks)
530
+ | +---> UserTable (users)
531
+ | |
532
+ | +-- each row: onClick -> router.push(`/users/${user.id}`)
533
+ | +-- delete button: onClick -> event.stopPropagation() (no-op)
534
+ |
535
+ +---> src/app/users/[id]/page.tsx (User Detail)
536
+ |
537
+ |-- imports mockUsers
538
+ |-- finds user by params.id (fallback to mockUsers[0])
539
+ |-- passes user to UserDetailForm
540
+ |
541
+ +---> PageHeader (title, subtitle, actionLabel)
542
+ +---> UserDetailForm (user)
543
+ |
544
+ +-- initializes local form state from user props
545
+ +-- renders ProfileImages (user.profileImages)
546
+ +-- renders form fields from BASIC_INFO_ROW_1, ROW_2, ROW_3
547
+ +-- renders toggles from user.settings
548
+ ```
549
+
550
+ ### 6.2 Data Import Rules
551
+
552
+ 1. Only page components (`src/app/**/*.tsx`) import from `src/lib/mock-data.ts`
553
+ 2. Presentational components (`src/components/**/*.tsx`) receive data exclusively via props
554
+ 3. Type imports can be used anywhere: `import type { User } from "@/features/user-management/types"`
555
+ 4. Utility imports are direct: `import { formatNumber } from "@/features/user-management/utils/format"`
556
+
557
+ ---
558
+
559
+ ## 7. Performance Checklist
560
+
561
+ ### 7.1 Import Rules
562
+
563
+ - **No barrel file imports:** Do NOT use `export * from "./component"` or import from directory index files that re-export everything. Each component file exports only its own symbols.
564
+ - **Direct imports only:** Always import from the specific file path.
565
+
566
+ ```typescript
567
+ // CORRECT:
568
+ import { Button } from "@/components/ui/button";
569
+ import { UserTable } from "@/components/user-management/user-table";
570
+ import type { User } from "@/features/user-management/types";
571
+
572
+ // INCORRECT:
573
+ import { Button, Table, Input } from "@/components/ui"; // barrel import
574
+ import { UserTable } from "@/components/user-management"; // barrel import
575
+ ```
576
+
577
+ ### 7.2 React.memo for Table Rows
578
+
579
+ - Wrap table row rendering in `React.memo` to prevent unnecessary re-renders when parent state changes (search input, tab change, pagination):
580
+
581
+ ```typescript
582
+ const UserTableRow = React.memo(function UserTableRow({ user }: { user: User }) {
583
+ // row rendering
584
+ });
585
+ ```
586
+
587
+ ### 7.3 Next.js App Router Server Components
588
+
589
+ | File | Component Type | Reason |
590
+ |------|---------------|--------|
591
+ | `src/app/layout.tsx` | Server Component | Static layout, no interactivity |
592
+ | `src/app/page.tsx` | Client Component (`"use client"`) | Uses `useState` for search, tabs, pagination |
593
+ | `src/app/users/[id]/page.tsx` | Client Component (`"use client"`) | Uses `useState` for form state, toggles |
594
+ | `src/components/layouts/app-sidebar.tsx` | Client Component (`"use client"`) | Uses `usePathname()` |
595
+ | `src/components/layouts/content-layout.tsx` | Server Component | Pure wrapper, no state or hooks |
596
+ | `src/components/common/page-header.tsx` | Server Component (or Client if `onAction` is used) | If `onAction` callback is passed, needs `"use client"` |
597
+ | All `src/components/user-management/*` | Client Component (`"use client"`) | Interactive: form inputs, toggles, click handlers |
598
+
599
+ ### 7.4 Image Optimization
600
+
601
+ - Use Next.js `<Image>` component for profile images and avatars where possible
602
+ - Alternatively, use `<img>` with explicit `width`/`height` attributes to prevent layout shift
603
+ - Avatar images: 22x22px rendered, source at 44x44px (@2x)
604
+ - Profile images: 116x116px rendered, source at 232x232px (@2x)
605
+ - All images use `loading="lazy"` (default for `<Image>`)
606
+
607
+ ### 7.5 Font Loading
608
+
609
+ - Inter: loaded via `next/font/google` with `display: "swap"` to prevent FOUT
610
+ - Pretendard Variable: loaded via CDN `<link>` in layout `<head>`
611
+ - Both fonts use CSS variable strategy for tree-shaking compatibility
612
+
613
+ ---
614
+
615
+ ## 8. Accessibility Checklist
616
+
617
+ ### 8.1 Per-Component ARIA Roles and Labels
618
+
619
+ | Component | ARIA Requirement | Implementation |
620
+ |-----------|-----------------|----------------|
621
+ | **Skip Link** | First focusable element | `<a href="#main-content" className="sr-only focus:not-sr-only ...">Skip to main content</a>` |
622
+ | **Main Content** | Landmark | `<main id="main-content">` wraps content area |
623
+ | **Sidebar** | Navigation landmark | `<nav aria-label="Main navigation">` wrapping sidebar nav |
624
+ | **Sidebar Accordion** | Expand/collapse | `aria-expanded` on triggers (Radix handles this) |
625
+ | **Page Title** | Heading hierarchy | `<h1>` for "User Management" |
626
+ | **Section Labels** | Heading hierarchy | `<h2>` for "Basic Info", "Other Settings" |
627
+ | **Badge** | Meaningful text | `aria-label="Total users: 100,000"` on Badge element |
628
+ | **Tabs** | Tab pattern | `role="tablist"`, `role="tab"`, `aria-selected` (Radix handles) |
629
+ | **Search Input** | Label | `aria-label="Search users"` or visible label |
630
+ | **Table** | Semantic HTML | Uses `<table>`, `<thead>`, `<th>`, `<tbody>`, `<tr>`, `<td>` (shadcn Table) |
631
+ | **Sort Columns** | Sortable indicator | `aria-label="Join Date, sortable column"` on `<th>` |
632
+ | **Table Rows** | Interactive rows | `role="link"` or `tabIndex={0}` + `onKeyDown` for Enter navigation |
633
+ | **Delete Button** | Dynamic label | `aria-label="Delete user {nickname}"` per row |
634
+ | **Pagination Buttons** | Icon-only labels | `aria-label="Go to first page"`, `"Previous page"`, `"Next page"`, `"Go to last page"` |
635
+ | **Page Indicator** | Live region | `aria-live="polite"` on "1 / 1" region |
636
+ | **Form Labels** | Association | `<Label htmlFor="field-id">` paired with `<Input id="field-id">` |
637
+ | **Toggle Switches** | Checked state | `aria-checked` managed by Radix Switch |
638
+ | **Toggle Labels** | Text label | `<label>` associated with each Switch |
639
+ | **Logout Button** | Descriptive text | Text content "Logout" sufficient; optional `aria-label="Log out of admin panel"` |
640
+
641
+ ### 8.2 Keyboard Navigation Requirements
642
+
643
+ | Component | Key | Action |
644
+ |-----------|-----|--------|
645
+ | Skip link | `Tab` (first element) | Focus skip link |
646
+ | Skip link | `Enter` | Jump to `#main-content` |
647
+ | Sidebar menu items | `Tab` / `Shift+Tab` | Navigate between items |
648
+ | Sidebar accordion | `Enter` / `Space` | Toggle expand/collapse |
649
+ | Sidebar accordion | `Arrow Up` / `Arrow Down` | Navigate between accordion items |
650
+ | Tabs | `Arrow Left` / `Arrow Right` | Switch between tabs |
651
+ | Tabs | `Enter` / `Space` | Activate tab |
652
+ | Table rows | `Tab` | Focus row (or first interactive element) |
653
+ | Table rows | `Enter` | Navigate to user detail |
654
+ | Delete action | `Enter` / `Space` | Trigger delete (stopPropagation) |
655
+ | Delete action | `Tab` | Focusable separately from row |
656
+ | Pagination buttons | `Tab` + `Enter` | Navigate pages |
657
+ | Input fields | `Tab` | Focus field |
658
+ | Select dropdown | `Enter` / `Space` | Open dropdown |
659
+ | Select dropdown | `Arrow Up` / `Arrow Down` | Navigate options |
660
+ | Select dropdown | `Enter` | Select option |
661
+ | Select dropdown | `Escape` | Close dropdown |
662
+ | Toggle switch | `Space` | Toggle ON/OFF |
663
+ | Buttons | `Enter` / `Space` | Activate button |
664
+
665
+ ### 8.3 Focus Management
666
+
667
+ - **Focus ring style (all interactive elements):** `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2` (2px #509594 ring with 2px offset)
668
+ - **No keyboard traps:** All accordion sections, select dropdowns, and modals can be exited with `Escape` or `Tab`
669
+ - **Logical tab order:** Sidebar -> Content (top to bottom, left to right)
670
+ - **Focus preservation:** After tab switch, focus remains on the tab bar. After delete click, focus stays on the delete button.
671
+
672
+ ### 8.4 Screen Reader Announcements
673
+
674
+ | Element | Expected Announcement |
675
+ |---------|----------------------|
676
+ | Page title | "User Management, heading level 1" |
677
+ | Badge | "Total users: 100,000" |
678
+ | Active tab | "All, tab, selected, 1 of 3" |
679
+ | Table | "User management table, 10 columns, 5 rows" |
680
+ | Sort button | "Join Date, sortable column" |
681
+ | Delete action | "Delete user NicknameHere, button" |
682
+ | Toggle | "Profile Public, switch, off" / "Match & Chat Notification, switch, on" |
683
+ | Pagination | "Page 1 of 1" (via `aria-live="polite"`) |
684
+
685
+ ### 8.5 Color Contrast Compliance
686
+
687
+ | Element | Foreground | Background | Ratio | WCAG | Status |
688
+ |---------|-----------|------------|-------|------|--------|
689
+ | Page title | `#000000` | `#FFFFFF` | 21:1 | AAA | Pass |
690
+ | Subtitle | `#9DA0A8` | `#FFFFFF` | 3.01:1 | AA Large (18px) | Pass |
691
+ | Table cell text | `#3B3F4A` | `#FFFFFF` | 9.21:1 | AAA | Pass |
692
+ | Button text | `#FFFFFF` | `#509594` | 3.15:1 | AA Large (18px SemiBold) | Pass |
693
+ | Badge text | `#22543D` | `#C6F6D5` | 7.05:1 | AAA | Pass |
694
+ | Inactive tab text | `#1A202C` | `#EEF2F6` | 13.83:1 | AAA | Pass |
695
+ | Sidebar text | `#5A5E6A` | `#FFFFFF` | 5.32:1 | AA | Pass |
696
+ | Delete action | `#3F7A79` | `#FFFFFF` | 4.87:1 | AA | Pass (uses darker shade) |
697
+ | Section label | `#A0AEC0` | `#FFFFFF` | 2.64:1 | -- | Decorative; pair with `<h2>` for SR |
698
+ | Placeholder | `#A0AEC0` | `#FFFFFF` | 2.64:1 | -- | Placeholder only; label provides name |
699
+
700
+ ---
701
+
702
+ ## 9. File Creation Order
703
+
704
+ Exact sequence matching dev-plan.md phases. Each file lists its dependencies.
705
+
706
+ ### Phase 0: Foundation
707
+
708
+ | Order | File | Depends On | Dev Plan Task |
709
+ |-------|------|------------|---------------|
710
+ | 1 | `components.json` + `src/lib/utils.ts` | -- | P0-T1: Initialize shadcn/ui |
711
+ | 2 | `src/styles/globals.css` | P0-T1 | P0-T2: Design tokens |
712
+ | 3 | `src/app/layout.tsx` (font loading only) | P0-T2 | P0-T3: Load fonts |
713
+ | 4 | `src/features/user-management/types/index.ts` | -- | P0-T4: TypeScript types |
714
+ | 5 | `src/types/index.ts` | -- | P0-T4: Shared types |
715
+ | 6 | `src/lib/mock-data.ts` | P0-T4 | P0-T5: Mock data |
716
+ | 7 | `src/features/user-management/utils/format.ts` | P0-T4 | P0-T6: Utility functions |
717
+ | 8 | `public/avatars/*`, `public/profile/*` | -- | P0-T7: Placeholder images |
718
+
719
+ ### Phase 1: Shared UI Components
720
+
721
+ | Order | File | Depends On | Dev Plan Task |
722
+ |-------|------|------------|---------------|
723
+ | 9 | `src/components/ui/button.tsx` | P0-T1 | P1-T1 + P1-T2: Install + customize Button |
724
+ | 10 | `src/components/ui/card.tsx` | P0-T1 | P1-T1: Install Card |
725
+ | 11 | `src/components/ui/table.tsx` | P0-T1, P0-T2 | P1-T1 + P1-T3: Install + customize Table |
726
+ | 12 | `src/components/ui/input.tsx` | P0-T1, P0-T2 | P1-T1 + P1-T4: Install + customize Input |
727
+ | 13 | `src/components/ui/select.tsx` | P0-T1, P0-T2 | P1-T1 + P1-T5: Install + customize Select |
728
+ | 14 | `src/components/ui/badge.tsx` | P0-T1, P0-T2 | P1-T1 + P1-T6: Install + customize Badge |
729
+ | 15 | `src/components/ui/avatar.tsx` | P0-T1, P0-T2 | P1-T1 + P1-T7: Install + customize Avatar |
730
+ | 16 | `src/components/ui/switch.tsx` | P0-T1, P0-T2 | P1-T1 + P1-T8: Install + customize Switch |
731
+ | 17 | `src/components/ui/tabs.tsx` | P0-T1, P0-T2 | P1-T1 + P1-T9: Install + customize Tabs |
732
+ | 18 | `src/components/ui/separator.tsx` | P0-T1, P0-T2 | P1-T1 + P1-T10: Install + customize Separator |
733
+ | 19 | `src/components/ui/accordion.tsx` | P0-T1, P0-T2 | P1-T1 + P1-T11: Install + customize Accordion |
734
+ | 20 | `src/components/ui/label.tsx` | P0-T1, P0-T2 | P1-T1 + P1-T12: Install + customize Label |
735
+ | 21 | `src/components/ui/sidebar.tsx` | P0-T1 | P1-T1: Install Sidebar |
736
+ | 22 | `src/components/ui/tooltip.tsx` | P0-T1 | P1-T1: Install Tooltip |
737
+ | 23 | `src/components/common/page-header.tsx` | P1-T2, P1-T6, P1-T10 | P1-T13: PageHeader |
738
+
739
+ ### Phase 2: Layout & Navigation
740
+
741
+ | Order | File | Depends On | Dev Plan Task |
742
+ |-------|------|------------|---------------|
743
+ | 24 | `src/components/layouts/app-sidebar.tsx` | P1-T1, P1-T11, P0-T2, P0-T3 | P2-T1: AppSidebar |
744
+ | 25 | `src/components/layouts/content-layout.tsx` | P2-T1 | P2-T2: ContentLayout |
745
+ | 26 | `src/app/layout.tsx` (full implementation) | P2-T1, P2-T2, P0-T3 | P2-T3: Root layout |
746
+
747
+ ### Phase 3: Dashboard Page
748
+
749
+ | Order | File | Depends On | Dev Plan Task |
750
+ |-------|------|------------|---------------|
751
+ | 27 | `src/components/user-management/tab-navigation.tsx` | P1-T9 | P3-T1: TabNavigation |
752
+ | 28 | `src/components/user-management/search-bar.tsx` | P1-T4 | P3-T2: SearchBar |
753
+ | 29 | `src/components/user-management/pagination-controls.tsx` | P1-T2, P1-T4, P1-T5 | P3-T3: PaginationControls |
754
+ | 30 | `src/components/user-management/user-table.tsx` | P1-T3, P1-T7, P1-T2, P0-T5 | P3-T4: UserTable |
755
+ | 31 | `src/app/page.tsx` | P3-T1..T4, P1-T13, P0-T5, P0-T6 | P3-T5: Dashboard assembly |
756
+
757
+ ### Phase 4: User Detail Page
758
+
759
+ | Order | File | Depends On | Dev Plan Task |
760
+ |-------|------|------------|---------------|
761
+ | 32 | `src/components/user-management/profile-images.tsx` | P0-T5, P0-T7 | P4-T1: ProfileImages |
762
+ | 33 | `src/components/user-management/user-detail-form.tsx` | P1-T4, P1-T5, P1-T8, P1-T12, P1-T10, P4-T1 | P4-T2: UserDetailForm |
763
+ | 34 | `src/app/users/[id]/page.tsx` | P4-T2, P1-T13, P0-T5 | P4-T3: User Detail assembly |
764
+
765
+ ### Phase 5: Polish
766
+
767
+ | Order | Task | Depends On | Dev Plan Task |
768
+ |-------|------|------------|---------------|
769
+ | 35 | Keyboard navigation audit + fixes | P3-T5, P4-T3 | P5-T1: Keyboard a11y |
770
+ | 36 | ARIA + screen reader audit + fixes | P5-T1 | P5-T2: ARIA/SR a11y |
771
+ | 37 | Micro-interactions + transitions | P3-T5, P4-T3 | P5-T3: Animations |
772
+ | 38 | Final visual QA | P5-T3 | P5-T4: Visual QA |
773
+
774
+ **Total: 34 files + 4 audit/polish passes = 38 ordered steps**
775
+
776
+ ---
777
+
778
+ ## Appendix A: Utility Functions
779
+
780
+ ```typescript
781
+ // src/features/user-management/utils/format.ts
782
+
783
+ /**
784
+ * Formats a number with comma separators.
785
+ * @example formatNumber(100000) -> "100,000"
786
+ * @example formatNumber(0) -> "0"
787
+ */
788
+ export function formatNumber(n: number): string {
789
+ return n.toLocaleString("en-US");
790
+ }
791
+ ```
792
+
793
+ ---
794
+
795
+ ## Appendix B: Design Token Quick Reference
796
+
797
+ ### Colors
798
+
799
+ | Token | Hex | Tailwind Class |
800
+ |-------|-----|---------------|
801
+ | Primary | `#509594` | `bg-primary` / `text-primary` |
802
+ | Primary Hover | `#3F7A79` | `bg-primary-hover` |
803
+ | Primary Active | `#357070` | `bg-primary-active` |
804
+ | Primary Light | `#B9D5D4` | `bg-primary-light` |
805
+ | Background | `#FCFCFC` | `bg-background` |
806
+ | Surface / Card | `#FFFFFF` | `bg-surface` / `bg-white` |
807
+ | Border | `#E2E8F0` | `border-border` |
808
+ | Divider | `#EFF1F4` | `bg-divider` |
809
+ | Table Header | `#F9FAFC` | `bg-table-header` |
810
+ | Sidebar Selected | `#EEF2F6` | `bg-sidebar-selected` |
811
+ | Placeholder | `#A0AEC0` | `text-placeholder` |
812
+ | Subtitle | `#9DA0A8` | `text-subtitle` |
813
+ | Muted FG | `#5A5E6A` | `text-muted-fg` |
814
+ | Table Cell | `#3B3F4A` | `text-table-cell` |
815
+ | Inactive Tab | `#EFF1F4` | `bg-inactive-tab` |
816
+ | Inactive Tab Text | `#1A202C` | `text-inactive-tab-text` |
817
+ | Badge Green BG | `#C6F6D5` | `bg-badge-green-bg` |
818
+ | Badge Green Text | `#22543D` | `text-badge-green-text` |
819
+ | Toggle ON | `#3B82F6` | `bg-toggle-on` |
820
+ | Toggle OFF | `#CBD5E0` | `bg-toggle-off` |
821
+ | Delete Text | `#3F7A79` | `text-delete-text` |
822
+
823
+ ### Typography
824
+
825
+ | Style | Font | Size | Weight | Tailwind |
826
+ |-------|------|------|--------|----------|
827
+ | Page Title | Pretendard | 32px | SemiBold (600) | `font-pretendard text-[32px] font-semibold` |
828
+ | Section Label | Pretendard | 20px | Bold (700) | `font-pretendard text-[20px] font-bold text-placeholder` |
829
+ | Field Label | Pretendard | 20px | SemiBold (600) | `font-pretendard text-[20px] font-semibold text-[#101010]` |
830
+ | Subtitle | Pretendard | 18px | Medium (500) | `font-pretendard text-[18px] font-medium text-subtitle` |
831
+ | Body | Pretendard | 14px | Regular (400) | `font-pretendard text-[14px]` |
832
+ | Sidebar Title | Pretendard | 24px | SemiBold (600) | `font-pretendard text-[24px] font-semibold tracking-[0.05em]` |
833
+ | Sidebar Menu | Inter | 16px | Regular (400) | `font-inter text-[16px] text-muted-fg` |
834
+ | Button Text | Inter | 18px | SemiBold (600) | `font-inter text-[18px] font-semibold` |
835
+ | Input Text | Inter | 18px | Regular (400) | `font-inter text-[18px]` |
836
+ | Badge | Inter | 18px | Bold (700) | `font-inter text-[18px] font-bold` |
837
+
838
+ ### Spacing
839
+
840
+ | Element | Value | Tailwind |
841
+ |---------|-------|----------|
842
+ | Card padding | 32px | `p-8` |
843
+ | Form field column gap | 24px | `gap-6` |
844
+ | Content area left offset | 324px | `ml-[324px]` |
845
+ | Content area top padding | 20px | `pt-5` |
846
+ | Tab height | 52px | `h-[52px]` |
847
+ | Search input height | 48px | `h-12` |
848
+ | Detail input height | 52px | `h-[52px]` |
849
+ | Primary button height | 40px | `h-10` |
850
+ | Table header row height | 56px | `h-14` |
851
+ | Table body row height | 60px | `h-[60px]` |
852
+ | Avatar size (table) | 22px | `w-[22px] h-[22px]` |
853
+ | Profile image size | 116px | `w-[116px] h-[116px]` |
854
+ | Toggle switch size | 44x24px | `w-[44px] h-[24px]` |
855
+ | Toggle settings gap | 100px | `gap-[100px]` |
856
+
857
+ ---
858
+
859
+ ## Appendix C: Lucide Icons Used
860
+
861
+ | Icon | Import | Usage |
862
+ |------|--------|-------|
863
+ | `Pencil` | `lucide-react` | "Write" and "Save Changes" button icon |
864
+ | `Home` | `lucide-react` | Sidebar Home nav item |
865
+ | `Users` | `lucide-react` | Sidebar User section icon |
866
+ | `Gamepad2` | `lucide-react` | Sidebar Match section icon |
867
+ | `Shield` | `lucide-react` | Sidebar Admin section icon |
868
+ | `LogOut` | `lucide-react` | Sidebar Logout button icon |
869
+ | `ChevronsLeft` | `lucide-react` | Pagination "go to first page" |
870
+ | `ChevronLeft` | `lucide-react` | Pagination "previous page" |
871
+ | `ChevronRight` | `lucide-react` | Pagination "next page" |
872
+ | `ChevronsRight` | `lucide-react` | Pagination "go to last page" |
873
+ | `ChevronDown` | `lucide-react` | Table sort indicator, Select chevron, Accordion trigger |
874
+ | `ChevronUp` | `lucide-react` | Select chevron (open state) |