프로젝트 초기 설정 및 구조 추가
- Vite + React 프로젝트 구조 설정 - 불필요한 PDF 파일 삭제 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
615
src/TABLE_DESIGN_SYSTEM.md
Normal file
615
src/TABLE_DESIGN_SYSTEM.md
Normal file
@@ -0,0 +1,615 @@
|
||||
# SAM MES 테이블 디자인 시스템
|
||||
|
||||
> **작성일**: 2025년 10월 24일
|
||||
> **버전**: 1.0.0
|
||||
|
||||
---
|
||||
|
||||
## 📋 목차
|
||||
|
||||
1. [개요](#개요)
|
||||
2. [테이블 시스템 구조](#테이블-시스템-구조)
|
||||
3. [셀 타입](#셀-타입)
|
||||
4. [컴포넌트 가이드](#컴포넌트-가이드)
|
||||
5. [사용 예시](#사용-예시)
|
||||
6. [Best Practices](#best-practices)
|
||||
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
SAM MES의 모든 테이블은 통일된 디자인 시스템을 사용합니다. 컬럼과 데이터만 정의하면 자동으로 적절한 형태로 렌더링됩니다.
|
||||
|
||||
### 핵심 원칙
|
||||
|
||||
1. **일관성**: 모든 테이블이 동일한 디자인 패턴
|
||||
2. **타입 안정성**: 셀 타입별 자동 렌더링
|
||||
3. **재사용성**: 컬럼 정의만으로 테이블 완성
|
||||
4. **반응형**: 데스크톱/모바일 자동 대응
|
||||
|
||||
---
|
||||
|
||||
## 테이블 시스템 구조
|
||||
|
||||
```
|
||||
DataTable (Organism)
|
||||
├── Column Definition (셀 타입 정의)
|
||||
├── Cell Renderer (타입별 자동 렌더링)
|
||||
│ ├── StatusBadge (Molecule)
|
||||
│ ├── IconWithBadge (Molecule)
|
||||
│ ├── TableActions (Molecule)
|
||||
│ └── Badge (Atom)
|
||||
└── Pagination (Optional)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 셀 타입
|
||||
|
||||
### 지원하는 셀 타입
|
||||
|
||||
| 타입 | 설명 | 정렬 | 예시 |
|
||||
|------|------|------|------|
|
||||
| **text** | 일반 텍스트 | 좌측 | "홍길동" |
|
||||
| **number** | 숫자 (자동 천 단위 구분) | 우측 | 1,234 |
|
||||
| **currency** | 통화 (₩ 표시) | 우측 | ₩1,234,567 |
|
||||
| **date** | 날짜 | 좌측 | 2025-10-24 |
|
||||
| **datetime** | 날짜 + 시간 | 좌측 | 2025-10-24 14:30 |
|
||||
| **status** | 상태 배지 | 중앙 | 🟢 활성 |
|
||||
| **badge** | 일반 배지 | 중앙 | v1.0 |
|
||||
| **icon** | 아이콘 | 중앙 | 📦 |
|
||||
| **iconBadge** | 아이콘 + 배지 | 좌측 | 📦 완제품 |
|
||||
| **actions** | 액션 버튼들 | 우측 | 👁️ ✏️ 🗑️ |
|
||||
| **custom** | 커스텀 렌더링 | 설정 가능 | (사용자 정의) |
|
||||
|
||||
---
|
||||
|
||||
## 컴포넌트 가이드
|
||||
|
||||
### 1. DataTable (Organism)
|
||||
|
||||
통일된 테이블 컴포넌트
|
||||
|
||||
**위치**: `/components/organisms/DataTable.tsx`
|
||||
|
||||
**Props**:
|
||||
```typescript
|
||||
interface DataTableProps<T> {
|
||||
columns: Column<T>[]; // 컬럼 정의
|
||||
data: T[]; // 데이터
|
||||
keyField: keyof T; // 고유 키 필드
|
||||
onRowClick?: (row: T) => void; // 행 클릭 핸들러
|
||||
loading?: boolean; // 로딩 상태
|
||||
emptyMessage?: string; // 빈 메시지
|
||||
pagination?: { // 페이지네이션
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
};
|
||||
striped?: boolean; // 줄무늬 (기본: false)
|
||||
hoverable?: boolean; // 호버 효과 (기본: true)
|
||||
compact?: boolean; // 컴팩트 모드 (기본: false)
|
||||
}
|
||||
```
|
||||
|
||||
**Column 정의**:
|
||||
```typescript
|
||||
interface Column<T> {
|
||||
key: keyof T | string; // 데이터 키
|
||||
label: string; // 컬럼 헤더
|
||||
type?: CellType; // 셀 타입
|
||||
width?: string; // 너비
|
||||
align?: "left" | "center" | "right"; // 정렬
|
||||
|
||||
// 타입별 설정
|
||||
statusConfig?: { showDot?: boolean };
|
||||
badgeConfig?: { variant?: string; className?: string };
|
||||
iconConfig?: { iconColor?: string; iconBgColor?: string; size?: "sm" | "md" | "lg" };
|
||||
currencyConfig?: { locale?: string; currency?: string };
|
||||
|
||||
// 커스텀
|
||||
render?: (value: any, row: T) => ReactNode;
|
||||
format?: (value: any) => string | number;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. StatusBadge (Molecule)
|
||||
|
||||
상태 표시 배지
|
||||
|
||||
**위치**: `/components/molecules/StatusBadge.tsx`
|
||||
|
||||
**지원 상태**:
|
||||
```typescript
|
||||
// 프로세스 상태
|
||||
"대기" | "진행중" | "완료" | "취소" | "반려"
|
||||
|
||||
// 문서 상태
|
||||
"작성중" | "검토중" | "승인" | "보류"
|
||||
|
||||
// 품질 상태
|
||||
"합격" | "부적합" | "조건부합격" | "검사중"
|
||||
|
||||
// 설비 상태
|
||||
"정상" | "고장" | "점검중"
|
||||
|
||||
// 활성화 상태
|
||||
"활성" | "비활성" | "사용" | "미사용" | "폐기"
|
||||
|
||||
// 생산 상태
|
||||
"생산중" | "배송중" | "생산대기"
|
||||
|
||||
// 재고 상태
|
||||
"입고" | "출고" | "재고부족"
|
||||
```
|
||||
|
||||
**사용법**:
|
||||
```typescript
|
||||
<StatusBadge status="활성" />
|
||||
<StatusBadge status="진행중" showDot />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. IconWithBadge (Molecule)
|
||||
|
||||
아이콘 + 라벨 + 배지 조합
|
||||
|
||||
**위치**: `/components/molecules/IconWithBadge.tsx`
|
||||
|
||||
**사용법**:
|
||||
```typescript
|
||||
<IconWithBadge
|
||||
icon={Package}
|
||||
label="완제품"
|
||||
iconColor="text-blue-600"
|
||||
iconBgColor="bg-blue-100"
|
||||
badge={{ label: "신규", variant: "default" }}
|
||||
size="md"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. TableActions (Molecule)
|
||||
|
||||
테이블 액션 버튼 그룹
|
||||
|
||||
**위치**: `/components/molecules/TableActions.tsx`
|
||||
|
||||
**지원 액션 타입**:
|
||||
- `view` - 상세보기 (👁️)
|
||||
- `edit` - 수정 (✏️)
|
||||
- `delete` - 삭제 (🗑️)
|
||||
- `copy` - 복사 (📋)
|
||||
- `download` - 다운로드 (⬇️)
|
||||
- `custom` - 커스텀
|
||||
|
||||
**레이아웃**:
|
||||
- `buttons` - 버튼으로 표시
|
||||
- `dropdown` - 드롭다운 메뉴
|
||||
- `auto` - 자동 (3개 이하: 버튼, 4개 이상: 드롭다운)
|
||||
|
||||
**사용법**:
|
||||
```typescript
|
||||
<TableActions
|
||||
actions={[
|
||||
{ type: "view", onClick: handleView },
|
||||
{ type: "edit", onClick: handleEdit },
|
||||
{ type: "delete", onClick: handleDelete, variant: "destructive" }
|
||||
]}
|
||||
layout="auto"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 사용 예시
|
||||
|
||||
### 예시 1: 기본 테이블
|
||||
|
||||
```typescript
|
||||
import { DataTable, Column } from "./organisms/DataTable";
|
||||
|
||||
interface Product {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
price: number;
|
||||
status: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
function ProductList() {
|
||||
const columns: Column<Product>[] = [
|
||||
{ key: "code", label: "제품코드", type: "text" },
|
||||
{ key: "name", label: "제품명", type: "text" },
|
||||
{ key: "price", label: "가격", type: "currency" },
|
||||
{ key: "status", label: "상태", type: "status" },
|
||||
{ key: "updatedAt", label: "수정일", type: "date" }
|
||||
];
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={products}
|
||||
keyField="id"
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 예시 2: 액션 버튼이 있는 테이블
|
||||
|
||||
```typescript
|
||||
const columns: Column<Product>[] = [
|
||||
{ key: "code", label: "제품코드", type: "text" },
|
||||
{ key: "name", label: "제품명", type: "text" },
|
||||
{ key: "price", label: "가격", type: "currency" },
|
||||
{ key: "status", label: "상태", type: "status" },
|
||||
{
|
||||
key: "id",
|
||||
label: "관리",
|
||||
type: "actions",
|
||||
render: (_, row) => [
|
||||
{ type: "view", onClick: () => handleView(row) },
|
||||
{ type: "edit", onClick: () => handleEdit(row) },
|
||||
{ type: "delete", onClick: () => handleDelete(row), variant: "destructive" }
|
||||
]
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### 예시 3: 아이콘 + 배지
|
||||
|
||||
```typescript
|
||||
const columns: Column<Item>[] = [
|
||||
{
|
||||
key: "type",
|
||||
label: "품목유형",
|
||||
type: "iconBadge",
|
||||
render: (value) => ({
|
||||
icon: getItemIcon(value),
|
||||
label: getItemLabel(value),
|
||||
badge: { label: "신규", variant: "default" }
|
||||
})
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### 예시 4: 커스텀 렌더링
|
||||
|
||||
```typescript
|
||||
const columns: Column<Order>[] = [
|
||||
{
|
||||
key: "customer",
|
||||
label: "고객사",
|
||||
type: "custom",
|
||||
render: (value, row) => (
|
||||
<div>
|
||||
<p className="font-medium">{value}</p>
|
||||
<p className="text-xs text-muted-foreground">{row.customerCode}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### 예시 5: 페이지네이션
|
||||
|
||||
```typescript
|
||||
const { currentPage, totalPages, paginatedData, goToPage } = usePagination(data, 10);
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={paginatedData}
|
||||
keyField="id"
|
||||
pagination={{
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange: goToPage
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### 예시 6: 다양한 옵션
|
||||
|
||||
```typescript
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
keyField="id"
|
||||
striped // 줄무늬
|
||||
compact // 컴팩트 모드
|
||||
hoverable={false} // 호버 효과 비활성화
|
||||
loading={isLoading} // 로딩 상태
|
||||
emptyMessage="검색 결과가 없습니다."
|
||||
onRowClick={handleRowClick} // 행 클릭
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. 올바른 타입 선택
|
||||
|
||||
```typescript
|
||||
// ✅ 좋은 예
|
||||
{ key: "price", label: "가격", type: "currency" }
|
||||
{ key: "quantity", label: "수량", type: "number" }
|
||||
{ key: "status", label: "상태", type: "status" }
|
||||
{ key: "createdAt", label: "생성일", type: "date" }
|
||||
|
||||
// ❌ 나쁜 예
|
||||
{ key: "price", label: "가격", type: "text" } // 통화 형식 미적용
|
||||
{ key: "status", label: "상태", type: "text" } // 배지 미적용
|
||||
```
|
||||
|
||||
### 2. 액션 버튼 정리
|
||||
|
||||
```typescript
|
||||
// ✅ 좋은 예 - 타입 시스템 활용
|
||||
{
|
||||
key: "id",
|
||||
label: "관리",
|
||||
type: "actions",
|
||||
render: (_, row) => [
|
||||
{ type: "view", onClick: () => handleView(row) },
|
||||
{ type: "edit", onClick: () => handleEdit(row) },
|
||||
{ type: "delete", onClick: () => handleDelete(row), variant: "destructive" }
|
||||
]
|
||||
}
|
||||
|
||||
// ❌ 나쁜 예 - 직접 구현
|
||||
{
|
||||
key: "id",
|
||||
label: "관리",
|
||||
render: (_, row) => (
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => handleView(row)}>보기</Button>
|
||||
<Button onClick={() => handleEdit(row)}>수정</Button>
|
||||
<Button onClick={() => handleDelete(row)}>삭제</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 상태 통일
|
||||
|
||||
```typescript
|
||||
// ✅ 좋은 예 - StatusBadge 활용
|
||||
{ key: "status", label: "상태", type: "status" }
|
||||
|
||||
// ❌ 나쁜 예 - 직접 구현
|
||||
{
|
||||
key: "status",
|
||||
render: (value) => (
|
||||
<Badge className={value === "활성" ? "bg-green-100" : "bg-gray-100"}>
|
||||
{value}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 정렬 기본값
|
||||
|
||||
타입별 자동 정렬을 활용하세요:
|
||||
- **text**: 좌측 정렬
|
||||
- **number**, **currency**, **actions**: 우측 정렬
|
||||
- **status**, **badge**, **icon**: 중앙 정렬
|
||||
|
||||
```typescript
|
||||
// ✅ 자동 정렬 활용
|
||||
{ key: "price", label: "가격", type: "currency" } // 자동 우측 정렬
|
||||
|
||||
// ⚠️ 필요시에만 명시
|
||||
{ key: "price", label: "가격", type: "currency", align: "left" } // 수동 좌측 정렬
|
||||
```
|
||||
|
||||
### 5. 반응형 대응
|
||||
|
||||
데스크톱과 모바일을 함께 고려하세요:
|
||||
|
||||
```typescript
|
||||
// 데스크톱: DataTable
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
keyField="id"
|
||||
/>
|
||||
|
||||
// 모바일: MobileCard
|
||||
<div className="md:hidden space-y-3">
|
||||
{data.map((item) => (
|
||||
<MobileCard
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
subtitle={item.code}
|
||||
fields={[...]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 가이드
|
||||
|
||||
### 기존 테이블 → 새 테이블 시스템
|
||||
|
||||
**Before**:
|
||||
```typescript
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>제품코드</TableHead>
|
||||
<TableHead>제품명</TableHead>
|
||||
<TableHead>가격</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
<TableCell>{row.code}</TableCell>
|
||||
<TableCell>{row.name}</TableCell>
|
||||
<TableCell>₩{row.price.toLocaleString()}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
```
|
||||
|
||||
**After**:
|
||||
```typescript
|
||||
const columns: Column<Product>[] = [
|
||||
{ key: "code", label: "제품코드", type: "text" },
|
||||
{ key: "name", label: "제품명", type: "text" },
|
||||
{ key: "price", label: "가격", type: "currency" }
|
||||
];
|
||||
|
||||
<DataTable columns={columns} data={data} keyField="id" />
|
||||
```
|
||||
|
||||
**효과**:
|
||||
- 코드 라인 수: 20+ 라인 → 5 라인 (75% 감소)
|
||||
- 일관성: 자동으로 통일된 디자인 적용
|
||||
- 유지보수: 컬럼 정의만 수정
|
||||
|
||||
---
|
||||
|
||||
## 컴포넌트 계층 구조
|
||||
|
||||
```
|
||||
Atoms (UI 기본 컴포넌트)
|
||||
├── Badge
|
||||
├── Button
|
||||
└── Table Components
|
||||
|
||||
Molecules (작은 조합)
|
||||
├── StatusBadge ⭐ 상태 배지
|
||||
├── IconWithBadge ⭐ 아이콘 + 배지
|
||||
└── TableActions ⭐ 액션 버튼 그룹
|
||||
|
||||
Organisms (복잡한 조합)
|
||||
└── DataTable ⭐ 통합 테이블
|
||||
|
||||
Templates (페이지 레이아웃)
|
||||
└── ListPageTemplate ⭐ 목록 페이지 (DataTable 포함)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 적용 대상 페이지
|
||||
|
||||
### 즉시 적용 가능 (40개 이상)
|
||||
|
||||
**관리 페이지**:
|
||||
- CustomerManagement
|
||||
- SupplierManagement
|
||||
- OrderManagement
|
||||
- PurchaseOrderManagement
|
||||
- ItemManagement
|
||||
- EquipmentManagement
|
||||
- VehicleManagement
|
||||
- EmployeeManagement
|
||||
- DepartmentManagement
|
||||
|
||||
**재고 관리**:
|
||||
- InventoryManagement
|
||||
- ReceivingManagement
|
||||
- ShippingManagement
|
||||
- StockStatus
|
||||
|
||||
**생산 관리**:
|
||||
- ProductionManagement
|
||||
- WorkOrderManagement
|
||||
- ProcessManagement
|
||||
|
||||
**품질 관리**:
|
||||
- IncomingInspectionManagement
|
||||
- ProcessInspectionManagement
|
||||
- FinalInspectionManagement
|
||||
- NonconformingManagement
|
||||
|
||||
**회계/영업**:
|
||||
- SalesAccountingManagement
|
||||
- PurchaseAccountingManagement
|
||||
- QuoteManagement
|
||||
- PricingManagement
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q1. 커스텀 렌더링과 타입 시스템 중 어떤 것을 사용해야 하나요?
|
||||
|
||||
**A**: 가능하면 타입 시스템을 우선 사용하세요. 커스텀 렌더링은 정말 특수한 경우에만 사용합니다.
|
||||
|
||||
```typescript
|
||||
// ✅ 타입 시스템 활용 (권장)
|
||||
{ key: "status", label: "상태", type: "status" }
|
||||
|
||||
// ⚠️ 커스텀 렌더링 (특수한 경우만)
|
||||
{
|
||||
key: "complex",
|
||||
render: (value, row) => (
|
||||
<div>
|
||||
<p>{value}</p>
|
||||
<p className="text-xs">{row.detail}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Q2. 액션 버튼이 4개 이상일 때 어떻게 표시되나요?
|
||||
|
||||
**A**: 자동으로 드롭다운 메뉴로 변환됩니다.
|
||||
|
||||
```typescript
|
||||
// 3개 이하: 버튼으로 표시
|
||||
// 4개 이상: 자동으로 드롭다운 메뉴로 변환
|
||||
layout="auto" // 기본값
|
||||
```
|
||||
|
||||
### Q3. 테이블 행을 클릭 가능하게 만들려면?
|
||||
|
||||
**A**: `onRowClick` prop을 사용하세요.
|
||||
|
||||
```typescript
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
keyField="id"
|
||||
onRowClick={(row) => navigate(`/detail/${row.id}`)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Q4. 로딩 상태는 어떻게 표시하나요?
|
||||
|
||||
**A**: `loading` prop을 사용하세요.
|
||||
|
||||
```typescript
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
keyField="id"
|
||||
loading={isLoading}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 버전 | 날짜 | 변경 내용 |
|
||||
|------|------|-----------|
|
||||
| 1.0.0 | 2025-10-24 | 초기 버전 생성 |
|
||||
|
||||
---
|
||||
|
||||
**문서 버전**: 1.0.0
|
||||
**최종 업데이트**: 2025년 10월 24일
|
||||
**작성자**: SAM MES 개발팀
|
||||
Reference in New Issue
Block a user