21 KiB
21 KiB
ERP 시스템 UI/UX 디자인 가이드
📋 문서 목적
이 문서는 ERP 시스템의 새로운 페이지나 컴포넌트를 개발할 때 참고할 수 있는 디자인 시스템 기준안입니다. 일관된 사용자 경험을 위해 모든 개발자는 이 가이드를 따라 개발해주세요.
🎨 디자인 시스템 개요
디자인 철학
- 일관성: 모든 페이지에서 동일한 패턴 사용
- 명확성: 직관적이고 이해하기 쉬운 UI
- 접근성: 모든 사용자가 쉽게 사용할 수 있도록
- 반응성: 다양한 화면 크기에 대응
기술 스택
- CSS Framework: Tailwind CSS
- UI Library: shadcn/ui
- Icons: Lucide React
📐 페이지 기본 구조
1. 표준 페이지 레이아웃
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. 구조 설명
최상위 래퍼
<div className="min-h-screen bg-gray-50">
min-h-screen: 최소 높이를 화면 전체로bg-gray-50: 연한 회색 배경 (전체 페이지 기본 배경)
컨테이너
<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
헤더 카드
<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)
<Button className="bg-orange-500 hover:bg-orange-600">
<Plus className="w-4 h-4 mr-2" />
버튼 텍스트
</Button>
보조 버튼 (Secondary)
<Button variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-2" />
새로고침
</Button>
위험 버튼 (Danger)
<Button
variant="ghost"
className="text-red-500 hover:text-red-600"
>
<Trash2 className="w-4 h-4" />
삭제
</Button>
2. 카드 (Card)
기본 카드
<Card className="shadow-sm">
<CardHeader>
<CardTitle>카드 제목</CardTitle>
</CardHeader>
<CardContent className="p-6">
{/* 내용 */}
</CardContent>
</Card>
강조 카드
<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. 테이블
기본 테이블 구조
<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)
입력 필드
<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>
셀렉트
<Select>
<SelectTrigger className="w-48">
<SelectValue placeholder="선택하세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">옵션 1</SelectItem>
<SelectItem value="2">옵션 2</SelectItem>
</SelectContent>
</Select>
5. 빈 상태 (Empty State)
<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. 로딩 상태
<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)
orange-50 #fff7ed /* 매우 연한 배경 */
orange-100 #ffedd5 /* 연한 배경 */
orange-500 #f97316 /* 주요 버튼, 강조 */
orange-600 #ea580c /* 버튼 호버 */
회색 (Gray)
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 /* 주요 제목 */
상태 색상
/* 성공 */
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
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
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)
/* 페이지 제목 */
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)
/* 일반 텍스트 */
text-sm text-gray-600
/* 14px, #4b5563 */
/* 보조 설명 */
text-sm text-gray-500
/* 14px, #6b7280 */
/* 라벨 */
text-sm font-medium text-gray-700
/* 14px, Medium */
🎭 인터랙션 패턴
호버 효과
/* 버튼 호버 */
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
포커스 효과
/* 입력 필드 포커스 */
focus:outline-none
focus:ring-2
focus:ring-orange-500
focus:border-orange-500
전환 효과
/* 일반 전환 */
transition-all duration-200
/* 그림자 전환 */
transition-shadow
/* 색상 전환 */
transition-colors duration-200
🔲 그리드 시스템
반응형 그리드
{/* 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>
브레이크포인트
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: 관리 페이지 (데이터 있음)
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: 빈 상태 페이지
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 공식 문서
shadcn/ui 컴포넌트
Lucide 아이콘
📧 메일 관리 시스템 UI 개선사항
최근 업데이트 (2025-01-02)
1. 메일 발송 페이지 헤더 개선
변경 전:
<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>
변경 후 (표준 헤더 카드 적용):
<Card>
<CardHeader>
<CardTitle className="text-2xl">메일 발송</CardTitle>
<p className="text-sm text-muted-foreground mt-1">
템플릿을 선택하거나 직접 작성하여 메일을 발송하세요
</p>
</CardHeader>
</Card>
개선 사항:
- ✅ 불필요한 아이콘 제거 (종이비행기)
- ✅ 표준 Card 컴포넌트 사용으로 통일감 향상
- ✅ 다른 페이지와 동일한 헤더 스타일 적용
2. 메일 내용 입력 개선
변경 전:
<Textarea placeholder="메일 내용을 html로 작성하세요" />
변경 후:
<Textarea
placeholder="메일 내용을 입력하세요
줄바꿈은 자동으로 처리됩니다."
/>
<p className="text-xs text-gray-500 mt-1">
💡 일반 텍스트로 작성하면 자동으로 메일 형식으로 변환됩니다
</p>
개선 사항:
- ✅ HTML 지식 없이도 사용 가능
- ✅ 일반 텍스트 입력 후 자동 HTML 변환
- ✅ 사용자 친화적인 안내 메시지
3. CC/BCC 기능 추가
구현 내용:
{/* To 태그 입력 */}
<EmailTagInput
tags={to}
onTagsChange={setTo}
placeholder="받는 사람 이메일"
/>
{/* CC 태그 입력 */}
<EmailTagInput
tags={cc}
onTagsChange={setCc}
placeholder="참조 (선택사항)"
/>
{/* BCC 태그 입력 */}
<EmailTagInput
tags={bcc}
onTagsChange={setBcc}
placeholder="숨은참조 (선택사항)"
/>
특징:
- ✅ 이메일 주소를 태그 형태로 시각화
- ✅ 쉼표로 구분하여 입력 가능
- ✅ 개별 삭제 가능
4. 파일 첨부 기능 (Phase 1 완료)
백엔드 구현:
// 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 가이드
이메일 태그 입력
// 이메일 주소를 시각적으로 표시
<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>
파일 첨부 영역 (예정)
<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>
발송 성공 토스트
<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를 만들 수 있습니다! 🎨✨