ERP-node/.cursor/rules/admin-page-style-guide.mdc

750 lines
21 KiB
Plaintext
Raw Permalink Normal View History

2025-10-22 14:52:13 +09:00
---
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) - 커스텀 드롭다운 + 좌우 레이아웃