750 lines
21 KiB
Plaintext
750 lines
21 KiB
Plaintext
---
|
|
description: 관리자 페이지 표준 스타일 가이드 - shadcn/ui 기반 일관된 디자인 시스템
|
|
globs: **/app/(main)/admin/**/*.tsx,**/components/admin/**/*.tsx
|
|
---
|
|
|
|
# 관리자 페이지 표준 스타일 가이드
|
|
|
|
이 가이드는 관리자 페이지의 일관된 UI/UX를 위한 표준 스타일 규칙입니다.
|
|
모든 관리자 페이지는 이 가이드를 따라야 합니다.
|
|
|
|
## 1. 페이지 레이아웃 구조
|
|
|
|
### 기본 페이지 템플릿
|
|
|
|
```tsx
|
|
export default function AdminPage() {
|
|
return (
|
|
<div className="flex min-h-screen flex-col bg-background">
|
|
<div className="space-y-6 p-6">
|
|
{/* 페이지 헤더 */}
|
|
<div className="space-y-2 border-b pb-4">
|
|
<h1 className="text-3xl font-bold tracking-tight">페이지 제목</h1>
|
|
<p className="text-sm text-muted-foreground">페이지 설명</p>
|
|
</div>
|
|
|
|
{/* 메인 컨텐츠 */}
|
|
<MainComponent />
|
|
</div>
|
|
|
|
{/* Scroll to Top 버튼 (모바일/태블릿 전용) */}
|
|
<ScrollToTop />
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**필수 적용 사항:**
|
|
|
|
- 최상위: `flex min-h-screen flex-col bg-background`
|
|
- 컨텐츠 영역: `space-y-6 p-6` (24px 좌우 여백, 24px 간격)
|
|
- 헤더 구분선: `border-b pb-4` (테두리 박스 사용 금지)
|
|
- Scroll to Top: 모든 관리자 페이지에 포함
|
|
|
|
## 2. Color System (색상 시스템)
|
|
|
|
### CSS Variables 사용 (하드코딩 금지)
|
|
|
|
```tsx
|
|
// ❌ 잘못된 예시
|
|
<div className="bg-gray-50 text-gray-900 border-gray-200">
|
|
|
|
// ✅ 올바른 예시
|
|
<div className="bg-background text-foreground border-border">
|
|
<div className="bg-card text-card-foreground">
|
|
<div className="bg-muted text-muted-foreground">
|
|
```
|
|
|
|
**표준 색상 토큰:**
|
|
|
|
- `bg-background` / `text-foreground`: 기본 배경/텍스트
|
|
- `bg-card` / `text-card-foreground`: 카드 배경/텍스트
|
|
- `bg-muted` / `text-muted-foreground`: 보조 배경/텍스트
|
|
- `bg-primary` / `text-primary`: 메인 액션
|
|
- `bg-destructive` / `text-destructive`: 삭제/에러
|
|
- `border-border`: 테두리
|
|
- `ring-ring`: 포커스 링
|
|
|
|
## 3. Typography (타이포그래피)
|
|
|
|
### 표준 텍스트 크기와 가중치
|
|
|
|
```tsx
|
|
// 페이지 제목
|
|
<h1 className="text-3xl font-bold tracking-tight">
|
|
|
|
// 섹션 제목
|
|
<h2 className="text-xl font-semibold">
|
|
<h3 className="text-lg font-semibold">
|
|
<h4 className="text-sm font-semibold">
|
|
|
|
// 본문 텍스트
|
|
<p className="text-sm">
|
|
|
|
// 보조 텍스트
|
|
<p className="text-sm text-muted-foreground">
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
// 라벨
|
|
<label className="text-sm font-medium">
|
|
```
|
|
|
|
## 4. Spacing System (간격)
|
|
|
|
### 일관된 간격 (4px 기준)
|
|
|
|
```tsx
|
|
// 페이지 레벨 간격
|
|
<div className="space-y-6"> // 24px
|
|
|
|
// 섹션 레벨 간격
|
|
<div className="space-y-4"> // 16px
|
|
|
|
// 필드 레벨 간격
|
|
<div className="space-y-2"> // 8px
|
|
|
|
// 패딩
|
|
<div className="p-6"> // 24px (카드)
|
|
<div className="p-4"> // 16px (내부 섹션)
|
|
|
|
// 갭
|
|
<div className="gap-4"> // 16px (flex/grid)
|
|
<div className="gap-2"> // 8px (버튼 그룹)
|
|
```
|
|
|
|
## 5. 검색 툴바 (Toolbar)
|
|
|
|
### 패턴 A: 통합 검색 영역 (권장)
|
|
|
|
```tsx
|
|
<div className="space-y-4">
|
|
{/* 검색 및 액션 영역 */}
|
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
|
{/* 검색 영역 */}
|
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
|
{/* 통합 검색 */}
|
|
<div className="w-full sm:w-[400px]">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
<Input placeholder="통합 검색..." className="h-10 pl-10 text-sm" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* 고급 검색 토글 */}
|
|
<Button variant="outline" className="h-10 gap-2 text-sm font-medium">
|
|
고급 검색
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 액션 버튼 영역 */}
|
|
<div className="flex items-center gap-4">
|
|
<div className="text-sm text-muted-foreground">
|
|
총{" "}
|
|
<span className="font-semibold text-foreground">
|
|
{count.toLocaleString()}
|
|
</span>{" "}
|
|
건
|
|
</div>
|
|
<Button className="h-10 gap-2 text-sm font-medium">
|
|
<Plus className="h-4 w-4" />
|
|
등록
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 고급 검색 옵션 */}
|
|
{showAdvanced && (
|
|
<div className="space-y-4">
|
|
<div className="space-y-1">
|
|
<h4 className="text-sm font-semibold">고급 검색 옵션</h4>
|
|
<p className="text-xs text-muted-foreground">설명</p>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
<Input placeholder="필드 검색" className="h-10 text-sm" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
```
|
|
|
|
### 패턴 B: 제목 + 검색 + 버튼 한 줄 (공간 효율적)
|
|
|
|
```tsx
|
|
{
|
|
/* 상단 헤더: 제목 + 검색 + 버튼 */
|
|
}
|
|
<div className="relative flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
|
{/* 왼쪽: 제목 */}
|
|
<h2 className="text-xl font-semibold">페이지 제목</h2>
|
|
|
|
{/* 오른쪽: 검색 + 버튼 */}
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
|
{/* 필터 선택 */}
|
|
<div className="w-full sm:w-[160px]">
|
|
<Select>
|
|
<SelectTrigger className="h-10">
|
|
<SelectValue placeholder="필터" />
|
|
</SelectTrigger>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 검색 입력 */}
|
|
<div className="w-full sm:w-[240px]">
|
|
<Input placeholder="검색..." className="h-10 text-sm" />
|
|
</div>
|
|
|
|
{/* 초기화 버튼 */}
|
|
<Button variant="outline" className="h-10 text-sm font-medium">
|
|
초기화
|
|
</Button>
|
|
|
|
{/* 주요 액션 버튼 */}
|
|
<Button variant="outline" className="h-10 gap-2 text-sm font-medium">
|
|
<Plus className="h-4 w-4" />
|
|
등록
|
|
</Button>
|
|
|
|
{/* 조건부 버튼 (선택 시) */}
|
|
{selectedCount > 0 && (
|
|
<Button variant="destructive" className="h-10 gap-2 text-sm font-medium">
|
|
삭제 ({selectedCount})
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>;
|
|
```
|
|
|
|
**필수 적용 사항:**
|
|
|
|
- ❌ 검색 영역에 박스/테두리 사용 금지
|
|
- ✅ 검색창 권장 너비: `w-full sm:w-[240px]` ~ `sm:w-[400px]`
|
|
- ✅ 필터/Select 권장 너비: `w-full sm:w-[160px]` ~ `sm:w-[200px]`
|
|
- ✅ 고급 검색 필드: placeholder만 사용 (라벨 제거)
|
|
- ✅ 검색 아이콘: `Search` (lucide-react)
|
|
- ✅ Input/Select 높이: `h-10` (40px)
|
|
- ✅ 상단 헤더에 `relative` 추가 (드롭다운 표시용)
|
|
|
|
## 6. Button (버튼)
|
|
|
|
### 표준 버튼 variants와 크기
|
|
|
|
```tsx
|
|
// Primary 액션
|
|
<Button variant="default" size="default" className="h-10 gap-2 text-sm font-medium">
|
|
<Plus className="h-4 w-4" />
|
|
등록
|
|
</Button>
|
|
|
|
// Secondary 액션
|
|
<Button variant="outline" size="default" className="h-10 gap-2 text-sm font-medium">
|
|
취소
|
|
</Button>
|
|
|
|
// Ghost 버튼 (아이콘 전용)
|
|
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
<Icon className="h-4 w-4" />
|
|
</Button>
|
|
|
|
// Destructive
|
|
<Button variant="destructive" size="default" className="h-10 gap-2 text-sm font-medium">
|
|
삭제
|
|
</Button>
|
|
```
|
|
|
|
**표준 크기:**
|
|
|
|
- `h-10`: 기본 버튼 (40px)
|
|
- `h-9`: 작은 버튼 (36px)
|
|
- `h-8`: 아이콘 버튼 (32px)
|
|
|
|
**아이콘 크기:**
|
|
|
|
- `h-4 w-4`: 버튼 내 아이콘 (16px)
|
|
|
|
## 7. Input (입력 필드)
|
|
|
|
### 표준 Input 스타일
|
|
|
|
```tsx
|
|
// 기본
|
|
<Input placeholder="입력..." className="h-10 text-sm" />
|
|
|
|
// 검색 (아이콘 포함)
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
<Input placeholder="검색..." className="h-10 pl-10 text-sm" />
|
|
</div>
|
|
|
|
// 로딩/액티브
|
|
<Input className="h-10 text-sm border-primary ring-2 ring-primary/20" />
|
|
|
|
// 비활성화
|
|
<Input disabled className="h-10 text-sm cursor-not-allowed bg-muted text-muted-foreground" />
|
|
```
|
|
|
|
**필수 적용 사항:**
|
|
|
|
- 높이: `h-10` (40px)
|
|
- 텍스트: `text-sm`
|
|
- 포커스: 자동 적용 (`ring-2 ring-ring`)
|
|
|
|
## 8. Table & Card (테이블과 카드)
|
|
|
|
### 반응형 테이블/카드 구조
|
|
|
|
```tsx
|
|
// 실제 데이터 렌더링
|
|
return (
|
|
<>
|
|
{/* 데스크톱 테이블 뷰 (lg 이상) */}
|
|
<div className="hidden rounded-lg border bg-card shadow-sm lg:block">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="border-b bg-muted/50 hover:bg-muted/50">
|
|
<TableHead className="h-12 text-sm font-semibold">컬럼</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
<TableRow className="border-b transition-colors hover:bg-muted/50">
|
|
<TableCell className="h-16 text-sm">데이터</TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{/* 모바일/태블릿 카드 뷰 (lg 미만) */}
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:hidden">
|
|
{items.map((item) => (
|
|
<div
|
|
key={item.id}
|
|
className="rounded-lg border bg-card p-4 shadow-sm transition-colors hover:bg-muted/50"
|
|
>
|
|
{/* 헤더 */}
|
|
<div className="mb-4 flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<h3 className="text-base font-semibold">{item.name}</h3>
|
|
<p className="mt-1 text-sm text-muted-foreground">{item.id}</p>
|
|
</div>
|
|
<Switch checked={item.active} />
|
|
</div>
|
|
|
|
{/* 정보 */}
|
|
<div className="space-y-2 border-t pt-4">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">필드</span>
|
|
<span className="font-medium">{item.value}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 액션 */}
|
|
<div className="mt-4 flex gap-2 border-t pt-4">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-9 flex-1 gap-2 text-sm"
|
|
>
|
|
<Icon className="h-4 w-4" />
|
|
액션
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
);
|
|
```
|
|
|
|
**테이블 표준:**
|
|
|
|
- 헤더: `h-12` (48px), `bg-muted/50`, `font-semibold`
|
|
- 데이터 행: `h-16` (64px), `hover:bg-muted/50`
|
|
- 텍스트: `text-sm`
|
|
|
|
**카드 표준:**
|
|
|
|
- 컨테이너: `rounded-lg border bg-card p-4 shadow-sm`
|
|
- 헤더 제목: `text-base font-semibold`
|
|
- 부제목: `text-sm text-muted-foreground`
|
|
- 정보 라벨: `text-sm text-muted-foreground`
|
|
- 정보 값: `text-sm font-medium`
|
|
- 버튼: `h-9 flex-1 gap-2 text-sm`
|
|
|
|
## 9. Loading States (로딩 상태)
|
|
|
|
### Skeleton UI 패턴
|
|
|
|
```tsx
|
|
// 테이블 스켈레톤 (데스크톱)
|
|
<div className="hidden rounded-lg border bg-card shadow-sm lg:block">
|
|
<Table>
|
|
<TableHeader>...</TableHeader>
|
|
<TableBody>
|
|
{Array.from({ length: 10 }).map((_, index) => (
|
|
<TableRow key={index} className="border-b">
|
|
<TableCell className="h-16">
|
|
<div className="h-4 animate-pulse rounded bg-muted"></div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
// 카드 스켈레톤 (모바일/태블릿)
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:hidden">
|
|
{Array.from({ length: 6 }).map((_, index) => (
|
|
<div key={index} className="rounded-lg border bg-card p-4 shadow-sm">
|
|
<div className="mb-4 flex items-start justify-between">
|
|
<div className="flex-1 space-y-2">
|
|
<div className="h-5 w-32 animate-pulse rounded bg-muted"></div>
|
|
<div className="h-4 w-24 animate-pulse rounded bg-muted"></div>
|
|
</div>
|
|
<div className="h-6 w-11 animate-pulse rounded-full bg-muted"></div>
|
|
</div>
|
|
<div className="space-y-2 border-t pt-4">
|
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
<div key={i} className="flex justify-between">
|
|
<div className="h-4 w-16 animate-pulse rounded bg-muted"></div>
|
|
<div className="h-4 w-32 animate-pulse rounded bg-muted"></div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
```
|
|
|
|
## 10. Empty States (빈 상태)
|
|
|
|
### 표준 Empty State
|
|
|
|
```tsx
|
|
<div className="flex h-64 flex-col items-center justify-center rounded-lg border bg-card shadow-sm">
|
|
<div className="flex flex-col items-center gap-2 text-center">
|
|
<p className="text-sm text-muted-foreground">데이터가 없습니다.</p>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
## 11. Error States (에러 상태)
|
|
|
|
### 표준 에러 메시지
|
|
|
|
```tsx
|
|
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-4">
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-sm font-semibold text-destructive">
|
|
오류가 발생했습니다
|
|
</p>
|
|
<button
|
|
onClick={clearError}
|
|
className="text-destructive transition-colors hover:text-destructive/80"
|
|
aria-label="에러 메시지 닫기"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
<p className="mt-1.5 text-sm text-destructive/80">{errorMessage}</p>
|
|
</div>
|
|
```
|
|
|
|
## 12. Responsive Design (반응형)
|
|
|
|
### Breakpoints
|
|
|
|
- `sm`: 640px (모바일 가로/태블릿)
|
|
- `md`: 768px (태블릿)
|
|
- `lg`: 1024px (노트북)
|
|
- `xl`: 1280px (데스크톱)
|
|
|
|
### 모바일 우선 패턴
|
|
|
|
```tsx
|
|
// 레이아웃
|
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
|
|
|
// 그리드
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
|
|
// 검색창
|
|
<div className="w-full sm:w-[400px]">
|
|
|
|
// 테이블/카드 전환
|
|
<div className="hidden lg:block"> {/* 데스크톱 테이블 */}
|
|
<div className="lg:hidden"> {/* 모바일 카드 */}
|
|
|
|
// 간격
|
|
<div className="p-4 sm:p-6">
|
|
<div className="gap-2 sm:gap-4">
|
|
```
|
|
|
|
## 13. 좌우 레이아웃 (Side-by-Side Layout)
|
|
|
|
### 사이드바 + 메인 영역 구조
|
|
|
|
```tsx
|
|
<div className="flex h-full gap-6">
|
|
{/* 좌측 사이드바 (20-30%) */}
|
|
<div className="w-[20%] border-r pr-6">
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold">사이드바 제목</h3>
|
|
|
|
{/* 사이드바 컨텐츠 */}
|
|
<div className="space-y-3">
|
|
<div className="cursor-pointer rounded-lg border bg-card p-4 shadow-sm transition-all hover:shadow-md">
|
|
<h4 className="text-sm font-semibold">항목</h4>
|
|
<p className="mt-1 text-xs text-muted-foreground">설명</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 우측 메인 영역 (70-80%) */}
|
|
<div className="w-[80%] pl-0">
|
|
<div className="flex h-full flex-col space-y-4">
|
|
<h2 className="text-xl font-semibold">메인 제목</h2>
|
|
|
|
{/* 메인 컨텐츠 */}
|
|
<div className="flex-1 overflow-hidden">{/* 컨텐츠 */}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
**필수 적용 사항:**
|
|
|
|
- ✅ 좌우 구분: `border-r` 사용 (세로 구분선)
|
|
- ✅ 간격: `gap-6` (24px)
|
|
- ✅ 사이드바 패딩: `pr-6` (오른쪽 24px)
|
|
- ✅ 메인 영역 패딩: `pl-0` (gap으로 간격 확보)
|
|
- ✅ 비율: 20:80 또는 30:70
|
|
- ❌ 과도한 구분선 사용 금지 (세로 구분선 1개만)
|
|
- ❌ 사이드바와 메인 영역 각각에 추가 border 금지
|
|
|
|
## 14. Custom Dropdown (커스텀 드롭다운)
|
|
|
|
### 커스텀 Select/Dropdown 구조
|
|
|
|
```tsx
|
|
{
|
|
/* 드롭다운 컨테이너 */
|
|
}
|
|
<div className="w-full sm:w-[160px]">
|
|
<div className="company-dropdown relative">
|
|
{/* 트리거 버튼 */}
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
<span className={!value ? "text-muted-foreground" : ""}>
|
|
{value || "선택하세요"}
|
|
</span>
|
|
<svg
|
|
className={`h-4 w-4 transition-transform ${isOpen ? "rotate-180" : ""}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M19 9l-7 7-7-7"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
|
|
{/* 드롭다운 메뉴 */}
|
|
{isOpen && (
|
|
<div className="absolute top-full left-0 z-[100] mt-1 w-full min-w-[200px] rounded-md border bg-popover text-popover-foreground shadow-lg">
|
|
{/* 검색 (선택사항) */}
|
|
<div className="border-b p-2">
|
|
<Input
|
|
placeholder="검색..."
|
|
value={searchText}
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
className="h-8 text-sm"
|
|
onClick={(e) => e.stopPropagation()}
|
|
/>
|
|
</div>
|
|
|
|
{/* 옵션 목록 */}
|
|
<div className="max-h-48 overflow-y-auto">
|
|
{options.map((option) => (
|
|
<div
|
|
key={option.value}
|
|
className="flex cursor-pointer items-center px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground"
|
|
onClick={() => {
|
|
setValue(option.value);
|
|
setIsOpen(false);
|
|
}}
|
|
>
|
|
{option.label}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>;
|
|
```
|
|
|
|
**필수 적용 사항:**
|
|
|
|
- ✅ z-index: `z-[100]` (다른 요소 위에 표시)
|
|
- ✅ 그림자: `shadow-lg` (명확한 레이어 구분)
|
|
- ✅ 최소 너비: `min-w-[200px]` (내용이 잘리지 않도록)
|
|
- ✅ 최대 높이: `max-h-48` (스크롤 가능)
|
|
- ✅ 애니메이션: 화살표 아이콘 회전 (`rotate-180`)
|
|
- ✅ 부모 요소: `relative` 클래스 필요
|
|
- ⚠️ 부모에 `overflow-hidden` 사용 시 드롭다운 잘림 주의
|
|
|
|
**드롭다운이 잘릴 때 해결방법:**
|
|
|
|
```tsx
|
|
// 부모 요소의 overflow 제거
|
|
<div className="w-[80%] pl-0"> // overflow-hidden 제거
|
|
|
|
// 또는 상단 헤더에 relative 추가
|
|
<div className="relative flex ..."> // 드롭다운 포지셔닝 기준점
|
|
```
|
|
|
|
## 15. Scroll to Top Button
|
|
|
|
### 모바일/태블릿 전용 버튼
|
|
|
|
```tsx
|
|
import { ScrollToTop } from "@/components/common/ScrollToTop";
|
|
|
|
// 페이지에 추가
|
|
<ScrollToTop />;
|
|
```
|
|
|
|
**특징:**
|
|
|
|
- 데스크톱에서 숨김 (`lg:hidden`)
|
|
- 스크롤 200px 이상 시 나타남
|
|
- 부드러운 페이드 인/아웃 애니메이션
|
|
- 오른쪽 하단 고정 위치
|
|
- 원형 디자인 (`rounded-full`)
|
|
|
|
## 14. Accessibility (접근성)
|
|
|
|
### 필수 적용 사항
|
|
|
|
```tsx
|
|
// Label과 Input 연결
|
|
<label htmlFor="field-id" className="text-sm font-medium">
|
|
라벨
|
|
</label>
|
|
<Input id="field-id" />
|
|
|
|
// 버튼에 aria-label
|
|
<Button aria-label="설명">
|
|
<Icon />
|
|
</Button>
|
|
|
|
// Switch에 aria-label
|
|
<Switch
|
|
checked={isActive}
|
|
onCheckedChange={handleChange}
|
|
aria-label="상태 토글"
|
|
/>
|
|
|
|
// 포커스 표시 (자동 적용)
|
|
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
|
|
```
|
|
|
|
## 15. Class 순서 (일관성)
|
|
|
|
### 표준 클래스 작성 순서
|
|
|
|
1. Layout: `flex`, `grid`, `block`
|
|
2. Position: `fixed`, `absolute`, `relative`
|
|
3. Sizing: `w-full`, `h-10`
|
|
4. Spacing: `p-4`, `m-2`, `gap-4`
|
|
5. Typography: `text-sm`, `font-medium`
|
|
6. Colors: `bg-primary`, `text-white`
|
|
7. Border: `border`, `rounded-md`
|
|
8. Effects: `shadow-sm`, `opacity-50`
|
|
9. States: `hover:`, `focus:`, `disabled:`
|
|
10. Responsive: `sm:`, `md:`, `lg:`
|
|
|
|
## 16. 금지 사항
|
|
|
|
### ❌ 절대 사용하지 말 것
|
|
|
|
1. 하드코딩된 색상 (`bg-gray-50`, `text-blue-500` 등)
|
|
2. 인라인 스타일로 색상 지정 (`style={{ color: '#3b82f6' }}`)
|
|
3. 포커스 스타일 제거 (`outline-none`만 단독 사용)
|
|
4. 중첩된 박스 (Card 안에 Card, Border 안에 Border)
|
|
5. 검색 영역에 불필요한 박스/테두리
|
|
6. 검색 필드에 라벨 (placeholder만 사용)
|
|
7. 반응형 무시 (데스크톱 전용 스타일)
|
|
8. **이모지 사용** (사용자가 명시적으로 요청하지 않는 한 절대 사용 금지)
|
|
9. 과도한 구분선 사용 (최소한으로 유지)
|
|
10. 드롭다운 부모에 `overflow-hidden` (잘림 발생)
|
|
|
|
## 17. 체크리스트
|
|
|
|
새로운 관리자 페이지 작성 시 다음을 확인하세요:
|
|
|
|
### 페이지 레벨
|
|
|
|
- [ ] `bg-background` 사용 (하드코딩 금지)
|
|
- [ ] `space-y-6 p-6` 구조
|
|
- [ ] 페이지 헤더에 `border-b pb-4`
|
|
- [ ] `ScrollToTop` 컴포넌트 포함
|
|
|
|
### 검색 툴바
|
|
|
|
- [ ] 박스/테두리 없음
|
|
- [ ] 검색창 최대 너비 `sm:w-[400px]`
|
|
- [ ] 고급 검색 필드에 라벨 없음 (placeholder만)
|
|
- [ ] 반응형 레이아웃 적용
|
|
|
|
### 테이블/카드
|
|
|
|
- [ ] 데스크톱: 테이블 (`hidden lg:block`)
|
|
- [ ] 모바일: 카드 (`lg:hidden`)
|
|
- [ ] 표준 높이와 간격 적용
|
|
- [ ] 로딩/Empty 상태 구현
|
|
|
|
### 버튼
|
|
|
|
- [ ] 표준 variants 사용
|
|
- [ ] 표준 높이: `h-10`, `h-9`, `h-8`
|
|
- [ ] 아이콘 크기: `h-4 w-4`
|
|
- [ ] `gap-2`로 아이콘과 텍스트 간격
|
|
|
|
### 반응형
|
|
|
|
- [ ] 모바일 우선 디자인
|
|
- [ ] Breakpoints 적용 (`sm:`, `lg:`)
|
|
- [ ] 테이블/카드 전환
|
|
- [ ] Scroll to Top 버튼
|
|
|
|
### 접근성
|
|
|
|
- [ ] Label `htmlFor` / Input `id` 연결
|
|
- [ ] 버튼 `aria-label`
|
|
- [ ] Switch `aria-label`
|
|
- [ ] 포커스 표시 유지
|
|
|
|
## 참고 파일
|
|
|
|
완성된 예시:
|
|
|
|
### 기본 패턴
|
|
|
|
- [사용자 관리 페이지](<mdc:frontend/app/(main)/admin/userMng/page.tsx>) - 기본 페이지 구조
|
|
- [검색 툴바](mdc:frontend/components/admin/UserToolbar.tsx) - 패턴 A (통합 검색)
|
|
- [테이블/카드](mdc:frontend/components/admin/UserTable.tsx) - 반응형 테이블/카드
|
|
- [Scroll to Top](mdc:frontend/components/common/ScrollToTop.tsx) - 스크롤 버튼
|
|
|
|
### 고급 패턴
|
|
|
|
- [메뉴 관리 페이지](<mdc:frontend/app/(main)/admin/menu/page.tsx>) - 좌우 레이아웃 + 패턴 B (제목+검색+버튼)
|
|
- [메뉴 관리 컴포넌트](mdc:frontend/components/admin/MenuManagement.tsx) - 커스텀 드롭다운 + 좌우 레이아웃
|