ERP-node/UI_개선사항_문서.md

801 lines
21 KiB
Markdown

# ERP 시스템 UI/UX 디자인 가이드
## 📋 문서 목적
이 문서는 ERP 시스템의 새로운 페이지나 컴포넌트를 개발할 때 참고할 수 있는 **디자인 시스템 기준안**입니다.
일관된 사용자 경험을 위해 모든 개발자는 이 가이드를 따라 개발해주세요.
---
## 🎨 디자인 시스템 개요
### 디자인 철학
- **일관성**: 모든 페이지에서 동일한 패턴 사용
- **명확성**: 직관적이고 이해하기 쉬운 UI
- **접근성**: 모든 사용자가 쉽게 사용할 수 있도록
- **반응성**: 다양한 화면 크기에 대응
### 기술 스택
- **CSS Framework**: Tailwind CSS
- **UI Library**: shadcn/ui
- **Icons**: Lucide React
---
## 📐 페이지 기본 구조
### 1. 표준 페이지 레이아웃
```tsx
export default function YourPage() {
return (
<div className="min-h-screen bg-gray-50">
<div className="w-full max-w-none px-4 py-8 space-y-8">
{/* 페이지 제목 */}
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">페이지 제목</h1>
<p className="mt-2 text-gray-600">페이지 설명</p>
</div>
<div className="flex gap-2">
{/* 버튼들 */}
</div>
</div>
{/* 메인 컨텐츠 */}
<Card className="shadow-sm">
<CardContent className="p-6">
{/* 내용 */}
</CardContent>
</Card>
</div>
</div>
);
}
```
### 2. 구조 설명
#### 최상위 래퍼
```tsx
<div className="min-h-screen bg-gray-50">
```
- `min-h-screen`: 최소 높이를 화면 전체로
- `bg-gray-50`: 연한 회색 배경 (전체 페이지 기본 배경)
#### 컨테이너
```tsx
<div className="w-full max-w-none px-4 py-8 space-y-8">
```
- `w-full max-w-none`: 전체 너비 사용
- `px-4`: 좌우 패딩 1rem (16px)
- `py-8`: 상하 패딩 2rem (32px)
- `space-y-8`: 하위 요소 간 수직 간격 2rem
#### 헤더 카드
```tsx
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">제목</h1>
<p className="mt-2 text-gray-600">설명</p>
</div>
<div className="flex gap-2">
{/* 버튼들 */}
</div>
</div>
```
---
## 🎯 컴포넌트 디자인 기준
### 1. 버튼
#### 주요 버튼 (Primary)
```tsx
<Button className="bg-orange-500 hover:bg-orange-600">
<Plus className="w-4 h-4 mr-2" />
버튼 텍스트
</Button>
```
#### 보조 버튼 (Secondary)
```tsx
<Button variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-2" />
새로고침
</Button>
```
#### 위험 버튼 (Danger)
```tsx
<Button
variant="ghost"
className="text-red-500 hover:text-red-600"
>
<Trash2 className="w-4 h-4" />
삭제
</Button>
```
### 2. 카드 (Card)
#### 기본 카드
```tsx
<Card className="shadow-sm">
<CardHeader>
<CardTitle>카드 제목</CardTitle>
</CardHeader>
<CardContent className="p-6">
{/* 내용 */}
</CardContent>
</Card>
```
#### 강조 카드
```tsx
<Card className="bg-gradient-to-r from-orange-50 to-amber-50 border-orange-200 shadow-sm">
<CardHeader>
<CardTitle className="flex items-center">
<Icon className="w-5 h-5 mr-2 text-orange-500" />
제목
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-700">내용</p>
</CardContent>
</Card>
```
### 3. 테이블
#### 기본 테이블 구조
```tsx
<Card className="shadow-sm">
<CardContent className="p-6">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200/40 bg-gradient-to-r from-slate-50/90 to-gray-50/70">
<th className="h-12 px-6 py-4 text-left text-sm font-semibold text-gray-700">
컬럼명
</th>
</tr>
</thead>
<tbody>
<tr className="h-12 border-b border-gray-100/60 hover:bg-gradient-to-r hover:from-orange-50/80 hover:to-orange-100/60">
<td className="px-6 py-4 text-sm text-gray-600">
데이터
</td>
</tr>
</tbody>
</table>
</CardContent>
</Card>
```
### 4. 폼 (Form)
#### 입력 필드
```tsx
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
라벨
</label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500 transition-all duration-200"
/>
</div>
```
#### 셀렉트
```tsx
<Select>
<SelectTrigger className="w-48">
<SelectValue placeholder="선택하세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">옵션 1</SelectItem>
<SelectItem value="2">옵션 2</SelectItem>
</SelectContent>
</Select>
```
### 5. 빈 상태 (Empty State)
```tsx
<Card className="text-center py-16 bg-white shadow-sm">
<CardContent className="pt-6">
<Icon className="w-16 h-16 mx-auto mb-4 text-gray-300" />
<p className="text-gray-500 mb-4">데이터가 없습니다</p>
<Button className="bg-orange-500 hover:bg-orange-600">
<Plus className="w-4 h-4 mr-2" />
추가하기
</Button>
</CardContent>
</Card>
```
### 6. 로딩 상태
```tsx
<Card className="shadow-sm">
<CardContent className="flex justify-center items-center py-16">
<Loader2 className="w-8 h-8 animate-spin text-orange-500" />
</CardContent>
</Card>
```
---
## 🎨 색상 시스템
### 주 색상 (Primary)
```css
orange-50 #fff7ed /* 매우 연한 배경 */
orange-100 #ffedd5 /* 연한 배경 */
orange-500 #f97316 /* 주요 버튼, 강조 */
orange-600 #ea580c /* 버튼 호버 */
```
### 회색 (Gray)
```css
gray-50 #f9fafb /* 페이지 배경 */
gray-100 #f3f4f6 /* 카드 내부 구분 */
gray-200 #e5e7eb /* 테두리 */
gray-300 #d1d5db /* 입력 필드 테두리 */
gray-500 #6b7280 /* 보조 텍스트 */
gray-600 #4b5563 /* 일반 텍스트 */
gray-700 #374151 /* 라벨, 헤더 */
gray-800 #1f2937 /* 제목 */
gray-900 #111827 /* 주요 제목 */
```
### 상태 색상
```css
/* 성공 */
green-100 #dcfce7
green-500 #22c55e
green-700 #15803d
/* 경고 */
red-100 #fee2e2
red-500 #ef4444
red-600 #dc2626
/* 정보 */
blue-50 #eff6ff
blue-100 #dbeafe
blue-500 #3b82f6
```
---
## 📏 간격 시스템
### Spacing Scale
```css
space-y-2 0.5rem (8px) /* 폼 요소 간 간격 */
space-y-4 1rem (16px) /* 섹션 내부 간격 */
space-y-6 1.5rem (24px) /* 카드 내부 큰 간격 */
space-y-8 2rem (32px) /* 페이지 주요 섹션 간격 */
gap-2 0.5rem (8px) /* 버튼 그룹 간격 */
gap-4 1rem (16px) /* 카드 그리드 간격 */
gap-6 1.5rem (24px) /* 큰 카드 그리드 간격 */
```
### Padding
```css
p-2 0.5rem (8px) /* 작은 요소 */
p-4 1rem (16px) /* 일반 요소 */
p-6 1.5rem (24px) /* 카드, 헤더 */
p-8 2rem (32px) /* 큰 영역 */
px-3 좌우 0.75rem /* 입력 필드 */
px-4 좌우 1rem /* 버튼 */
px-6 좌우 1.5rem /* 테이블 셀 */
py-2 상하 0.5rem /* 버튼 */
py-4 상하 1rem /* 입력 필드 */
py-8 상하 2rem /* 페이지 컨테이너 */
```
---
## 📝 타이포그래피
### 제목 (Headings)
```css
/* 페이지 제목 */
text-3xl font-bold text-gray-900
/* 예: 30px, Bold, #111827 */
/* 섹션 제목 */
text-2xl font-bold text-gray-900
/* 예: 24px, Bold */
/* 카드 제목 */
text-lg font-semibold text-gray-800
/* 예: 18px, Semi-bold */
/* 작은 제목 */
text-base font-medium text-gray-700
/* 예: 16px, Medium */
```
### 본문 (Body Text)
```css
/* 일반 텍스트 */
text-sm text-gray-600
/* 14px, #4b5563 */
/* 보조 설명 */
text-sm text-gray-500
/* 14px, #6b7280 */
/* 라벨 */
text-sm font-medium text-gray-700
/* 14px, Medium */
```
---
## 🎭 인터랙션 패턴
### 호버 효과
```css
/* 버튼 호버 */
hover:bg-orange-600
hover:shadow-md
/* 카드 호버 */
hover:shadow-lg transition-shadow
/* 테이블 행 호버 */
hover:bg-gradient-to-r hover:from-orange-50/80 hover:to-orange-100/60
```
### 포커스 효과
```css
/* 입력 필드 포커스 */
focus:outline-none
focus:ring-2
focus:ring-orange-500
focus:border-orange-500
```
### 전환 효과
```css
/* 일반 전환 */
transition-all duration-200
/* 그림자 전환 */
transition-shadow
/* 색상 전환 */
transition-colors duration-200
```
---
## 🔲 그리드 시스템
### 반응형 그리드
```tsx
{/* 1열 → 2열 → 3열 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* 카드들 */}
</div>
{/* 1열 → 2열 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* 항목들 */}
</div>
```
### 브레이크포인트
```css
sm: 640px @media (min-width: 640px)
md: 768px @media (min-width: 768px)
lg: 1024px @media (min-width: 1024px)
xl: 1280px @media (min-width: 1280px)
2xl: 1536px @media (min-width: 1536px)
```
---
## 🎯 실전 예제
### 예제 1: 관리 페이지 (데이터 있음)
```tsx
export default function ManagementPage() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
return (
<div className="min-h-screen bg-gray-50">
<div className="w-full max-w-none px-4 py-8 space-y-8">
{/* 헤더 */}
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">데이터 관리</h1>
<p className="mt-2 text-gray-600">시스템 데이터를 관리합니다</p>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={loadData}>
<RefreshCw className="w-4 h-4 mr-2" />
새로고침
</Button>
<Button className="bg-orange-500 hover:bg-orange-600">
<Plus className="w-4 h-4 mr-2" />
새로 추가
</Button>
</div>
</div>
{/* 통계 카드 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card className="shadow-sm">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500"> 개수</p>
<p className="text-2xl font-bold text-gray-900">156</p>
</div>
<div className="bg-blue-100 p-3 rounded-lg">
<Database className="w-6 h-6 text-blue-500" />
</div>
</div>
</CardContent>
</Card>
{/* 나머지 통계 카드들... */}
</div>
{/* 데이터 테이블 */}
<Card className="shadow-sm">
<CardContent className="p-6">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200/40 bg-gradient-to-r from-slate-50/90 to-gray-50/70">
<th className="h-12 px-6 py-4 text-left text-sm font-semibold text-gray-700">
이름
</th>
<th className="h-12 px-6 py-4 text-left text-sm font-semibold text-gray-700">
상태
</th>
<th className="h-12 px-6 py-4 text-left text-sm font-semibold text-gray-700">
작업
</th>
</tr>
</thead>
<tbody>
{data.map((item) => (
<tr
key={item.id}
className="h-12 border-b border-gray-100/60 hover:bg-gradient-to-r hover:from-orange-50/80 hover:to-orange-100/60"
>
<td className="px-6 py-4 text-sm text-gray-600">
{item.name}
</td>
<td className="px-6 py-4">
<span className="px-2 py-1 text-xs rounded bg-green-100 text-green-700">
활성
</span>
</td>
<td className="px-6 py-4">
<div className="flex gap-2">
<Button size="sm" variant="outline">
수정
</Button>
<Button size="sm" variant="ghost" className="text-red-500">
삭제
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</CardContent>
</Card>
</div>
</div>
);
}
```
### 예제 2: 빈 상태 페이지
```tsx
export default function EmptyStatePage() {
return (
<div className="min-h-screen bg-gray-50">
<div className="w-full max-w-none px-4 py-8 space-y-8">
{/* 헤더 */}
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">데이터 관리</h1>
<p className="mt-2 text-gray-600">시스템 데이터를 관리합니다</p>
</div>
<Button className="bg-orange-500 hover:bg-orange-600">
<Plus className="w-4 h-4 mr-2" />
새로 추가
</Button>
</div>
{/* 빈 상태 */}
<Card className="text-center py-16 bg-white shadow-sm">
<CardContent className="pt-6">
<Database className="w-16 h-16 mx-auto mb-4 text-gray-300" />
<p className="text-gray-500 mb-4">아직 등록된 데이터가 없습니다</p>
<Button className="bg-orange-500 hover:bg-orange-600">
<Plus className="w-4 h-4 mr-2" />
데이터 추가하기
</Button>
</CardContent>
</Card>
{/* 안내 정보 */}
<Card className="bg-gradient-to-r from-orange-50 to-amber-50 border-orange-200 shadow-sm">
<CardHeader>
<CardTitle className="text-lg flex items-center">
<Info className="w-5 h-5 mr-2 text-orange-500" />
데이터 관리 안내
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-700 mb-4">
💡 데이터를 추가하여 시스템을 사용해보세요!
</p>
<ul className="space-y-2 text-sm text-gray-600">
<li className="flex items-start">
<span className="text-orange-500 mr-2"></span>
<span>기능 설명 1</span>
</li>
<li className="flex items-start">
<span className="text-orange-500 mr-2"></span>
<span>기능 설명 2</span>
</li>
</ul>
</CardContent>
</Card>
</div>
</div>
);
}
```
---
## ✅ 체크리스트
### 새 페이지 만들 때
- [ ] `min-h-screen bg-gray-50` 래퍼 사용
- [ ] 헤더 카드 (`bg-white rounded-lg shadow-sm border p-6`) 포함
- [ ] 제목은 `text-3xl font-bold text-gray-900`
- [ ] 설명은 `mt-2 text-gray-600`
- [ ] 주요 버튼은 `bg-orange-500 hover:bg-orange-600`
- [ ] 카드는 `shadow-sm` 클래스 포함
- [ ] 간격은 `space-y-8` 사용
### 새 컴포넌트 만들 때
- [ ] 일관된 패딩 사용 (`p-4`, `p-6`)
- [ ] 호버 효과 추가
- [ ] 전환 애니메이션 적용 (`transition-all duration-200`)
- [ ] 적절한 아이콘 사용 (Lucide React)
- [ ] 반응형 디자인 고려 (`md:`, `lg:`)
---
## 📚 참고 자료
### Tailwind CSS 공식 문서
- https://tailwindcss.com/docs
### shadcn/ui 컴포넌트
- https://ui.shadcn.com/
### Lucide 아이콘
- https://lucide.dev/icons/
---
## 📧 메일 관리 시스템 UI 개선사항
### 최근 업데이트 (2025-01-02)
#### 1. 메일 발송 페이지 헤더 개선
**변경 전:**
```tsx
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-3 bg-gradient-to-br from-blue-100 to-indigo-100 rounded-lg">
<Send className="w-6 h-6 text-blue-600" />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">메일 발송</h1>
<p className="text-sm text-gray-500">설명</p>
</div>
</div>
</div>
```
**변경 후 (표준 헤더 카드 적용):**
```tsx
<Card>
<CardHeader>
<CardTitle className="text-2xl">메일 발송</CardTitle>
<p className="text-sm text-muted-foreground mt-1">
템플릿을 선택하거나 직접 작성하여 메일을 발송하세요
</p>
</CardHeader>
</Card>
```
**개선 사항:**
- ✅ 불필요한 아이콘 제거 (종이비행기)
- ✅ 표준 Card 컴포넌트 사용으로 통일감 향상
- ✅ 다른 페이지와 동일한 헤더 스타일 적용
#### 2. 메일 내용 입력 개선
**변경 전:**
```tsx
<Textarea placeholder="메일 내용을 html로 작성하세요" />
```
**변경 후:**
```tsx
<Textarea
placeholder="메일 내용을 입력하세요
줄바꿈은 자동으로 처리됩니다."
/>
<p className="text-xs text-gray-500 mt-1">
💡 일반 텍스트로 작성하면 자동으로 메일 형식으로 변환됩니다
</p>
```
**개선 사항:**
- ✅ HTML 지식 없이도 사용 가능
- ✅ 일반 텍스트 입력 후 자동 HTML 변환
- ✅ 사용자 친화적인 안내 메시지
#### 3. CC/BCC 기능 추가
**구현 내용:**
```tsx
{/* To 태그 입력 */}
<EmailTagInput
tags={to}
onTagsChange={setTo}
placeholder="받는 사람 이메일"
/>
{/* CC 태그 입력 */}
<EmailTagInput
tags={cc}
onTagsChange={setCc}
placeholder="참조 (선택사항)"
/>
{/* BCC 태그 입력 */}
<EmailTagInput
tags={bcc}
onTagsChange={setBcc}
placeholder="숨은참조 (선택사항)"
/>
```
**특징:**
- ✅ 이메일 주소를 태그 형태로 시각화
- ✅ 쉼표로 구분하여 입력 가능
- ✅ 개별 삭제 가능
#### 4. 파일 첨부 기능 (Phase 1 완료)
**백엔드 구현:**
```typescript
// multer 설정
export const uploadMailAttachment = multer({
storage,
fileFilter,
limits: {
fileSize: 10 * 1024 * 1024, // 10MB 제한
files: 5, // 최대 5개 파일
},
});
// 발송 API
router.post(
'/simple',
uploadMailAttachment.array('attachments', 5),
(req, res) => mailSendSimpleController.sendMail(req, res)
);
```
**보안 기능:**
- ✅ 위험한 파일 확장자 차단 (.exe, .bat, .cmd, .sh 등)
- ✅ 파일 크기 제한 (10MB)
- ✅ 파일 개수 제한 (최대 5개)
- ✅ 안전한 파일명 생성
**프론트엔드 구현 예정 (Phase 1-3):**
- 드래그 앤 드롭 파일 업로드
- 첨부된 파일 목록 표시
- 파일 삭제 기능
- 미리보기에 첨부파일 정보 표시
#### 5. 향후 작업 계획
**Phase 2: 보낸메일함 백엔드**
- 발송 이력 자동 저장 (JSON 파일)
- 발송 상태 관리 (성공/실패)
- 발송 이력 조회 API
**Phase 3: 보낸메일함 프론트엔드**
- `/admin/mail/sent` 페이지
- 발송 목록 테이블
- 상세보기 모달
- 재전송 기능
**Phase 4: 대시보드 통합**
- 대시보드에 "보낸메일함" 링크
- 실제 발송 통계 연동
- 최근 활동 목록
### 메일 시스템 UI 가이드
#### 이메일 태그 입력
```tsx
// 이메일 주소를 시각적으로 표시
<div className="flex flex-wrap gap-2">
{tags.map((tag, index) => (
<div
key={index}
className="flex items-center gap-1 px-3 py-1.5 bg-blue-100 text-blue-700 rounded-md text-sm"
>
<Mail className="w-3 h-3" />
{tag}
<button onClick={() => removeTag(index)}>
<X className="w-3 h-3" />
</button>
</div>
))}
</div>
```
#### 파일 첨부 영역 (예정)
```tsx
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 hover:border-orange-400 transition-colors">
<input type="file" multiple className="hidden" />
<div className="text-center">
<Upload className="w-12 h-12 mx-auto text-gray-400" />
<p className="mt-2 text-sm text-gray-600">
파일을 드래그하거나 클릭하여 선택하세요
</p>
<p className="text-xs text-gray-500 mt-1">
최대 5, 10MB 이하
</p>
</div>
</div>
```
#### 발송 성공 토스트
```tsx
<div className="fixed top-4 right-4 bg-white rounded-lg shadow-lg border border-green-200 p-4">
<div className="flex items-center gap-3">
<CheckCircle className="w-5 h-5 text-green-500" />
<div>
<p className="font-medium text-gray-900">메일이 발송되었습니다</p>
<p className="text-sm text-gray-600">
{to.length}명에게 전송 완료
</p>
</div>
</div>
</div>
```
---
**이 가이드를 따라 개발하면 일관되고 아름다운 UI를 만들 수 있습니다!** 🎨✨