refactor: 리스트 컴포넌트 UI 및 레이아웃 일관성 개선
- 여러 관리 페이지(영업, 회계, 인사, 결재, 게시판, 설정)의 리스트 UI 통일 - IntegratedListTemplateV2 기반 레이아웃 정리 - PricingHistoryDialog 개선 - 공통 컴포넌트 추출 계획 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
# 공통 컴포넌트 추출 계획서
|
||||
|
||||
> MVP 완료 후 리팩토링 계획 (2025-12-23)
|
||||
|
||||
## 개요
|
||||
|
||||
| 항목 | 수치 |
|
||||
|-----|------|
|
||||
| 예상 코드 절감 | ~1,900줄 |
|
||||
| 영향 파일 | 50+ 개 |
|
||||
| 유지보수 비용 감소 | 30-40% |
|
||||
| 예상 작업 기간 | 3-4일 |
|
||||
|
||||
---
|
||||
|
||||
## 현재 공통화 현황
|
||||
|
||||
### ✅ 잘 되어있는 것
|
||||
- `SearchFilter` - 검색 입력 + 필터
|
||||
- `TabFilter` - 탭 형태 필터
|
||||
- `DateRangeSelector` - 날짜 범위 선택
|
||||
- `TableActions` - 테이블 행 액션 버튼
|
||||
- `FormActions` - 폼 저장/취소 버튼
|
||||
- `FormField` - 개별 폼 필드
|
||||
- `StatCards` - 통계 카드
|
||||
- `StandardDialog` - 기본 다이얼로그 (but 사용률 저조)
|
||||
|
||||
### ❌ 공통화 필요한 것
|
||||
- 삭제 확인 다이얼로그 (40+ 파일 중복)
|
||||
- 금액 포맷 유틸 (30+ 파일 중복)
|
||||
- 상태 배지 + 색상 상수 (10+ 파일 중복)
|
||||
- 상세정보 카드 (15+ 파일 각자 구현)
|
||||
- 폼 레이아웃 템플릿 (20+ 파일 중복)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 핵심 컴포넌트 (1일)
|
||||
|
||||
### 1.1 DeleteDialog 컴포넌트
|
||||
|
||||
**위치**: `src/components/molecules/DeleteDialog.tsx`
|
||||
|
||||
**Props 설계**:
|
||||
```typescript
|
||||
interface DeleteDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
itemName?: string; // "거래처", "품목" 등
|
||||
itemLabel?: string; // 삭제 대상 이름 (예: "삼성전자")
|
||||
title?: string; // 커스텀 타이틀
|
||||
description?: string; // 커스텀 설명
|
||||
onConfirm: () => void;
|
||||
isLoading?: boolean;
|
||||
confirmText?: string; // 기본값: "삭제"
|
||||
cancelText?: string; // 기본값: "취소"
|
||||
}
|
||||
```
|
||||
|
||||
**사용 예시**:
|
||||
```typescript
|
||||
<DeleteDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
itemName="거래처"
|
||||
itemLabel={selectedVendor?.name}
|
||||
onConfirm={handleDelete}
|
||||
isLoading={isDeleting}
|
||||
/>
|
||||
```
|
||||
|
||||
**체크리스트**:
|
||||
- [ ] DeleteDialog 컴포넌트 생성
|
||||
- [ ] 기본 스타일 (빨간색 삭제 버튼)
|
||||
- [ ] isLoading 상태 처리
|
||||
- [ ] 접근성 (포커스 트랩, ESC 닫기)
|
||||
- [ ] molecules/index.ts export 추가
|
||||
|
||||
**적용 대상 파일** (40+ 파일):
|
||||
- [ ] `accounting/VendorManagement/index.tsx`
|
||||
- [ ] `accounting/BillManagement/index.tsx`
|
||||
- [ ] `accounting/SalesManagement/index.tsx`
|
||||
- [ ] `accounting/PurchaseManagement/index.tsx`
|
||||
- [ ] `accounting/DepositManagement/index.tsx`
|
||||
- [ ] `accounting/WithdrawalManagement/index.tsx`
|
||||
- [ ] `hr/EmployeeManagement/index.tsx`
|
||||
- [ ] `hr/DepartmentManagement/index.tsx`
|
||||
- [ ] `hr/VacationManagement/index.tsx`
|
||||
- [ ] `settings/RankManagement/index.tsx`
|
||||
- [ ] `settings/TitleManagement/index.tsx`
|
||||
- [ ] `settings/PermissionManagement/index.tsx`
|
||||
- [ ] `settings/AccountManagement/index.tsx`
|
||||
- [ ] `board/BoardManagement/index.tsx`
|
||||
- [ ] `items/ItemListClient.tsx`
|
||||
- [ ] (나머지 25+ 파일은 grep으로 검색)
|
||||
|
||||
---
|
||||
|
||||
### 1.2 포맷 유틸 함수
|
||||
|
||||
**위치**: `src/lib/formatters.ts`
|
||||
|
||||
**함수 설계**:
|
||||
```typescript
|
||||
// 금액 포맷
|
||||
export function formatCurrency(amount: number): string;
|
||||
export function formatCurrencyWithSign(amount: number): string; // +/- 표시
|
||||
|
||||
// 금액 셀 (조건부 표시)
|
||||
export function formatCurrencyOrDash(amount: number, dash?: string): string;
|
||||
|
||||
// 날짜 포맷
|
||||
export function formatDate(date: string | Date, format?: string): string;
|
||||
export function formatDateTime(date: string | Date): string;
|
||||
|
||||
// 숫자 포맷
|
||||
export function formatNumber(num: number): string;
|
||||
export function formatPercent(num: number, decimals?: number): string;
|
||||
|
||||
// 전화번호 포맷
|
||||
export function formatPhone(phone: string): string;
|
||||
|
||||
// 사업자번호 포맷
|
||||
export function formatBizNo(bizNo: string): string;
|
||||
```
|
||||
|
||||
**사용 예시**:
|
||||
```typescript
|
||||
formatCurrency(10000) // "10,000원"
|
||||
formatCurrencyOrDash(0) // "-"
|
||||
formatCurrencyWithSign(-5000) // "-5,000원"
|
||||
formatDate('2024-01-15') // "2024-01-15"
|
||||
formatPhone('01012345678') // "010-1234-5678"
|
||||
formatBizNo('1234567890') // "123-45-67890"
|
||||
```
|
||||
|
||||
**체크리스트**:
|
||||
- [ ] formatters.ts 파일 생성
|
||||
- [ ] formatCurrency 함수
|
||||
- [ ] formatCurrencyOrDash 함수
|
||||
- [ ] formatCurrencyWithSign 함수
|
||||
- [ ] formatDate 함수
|
||||
- [ ] formatDateTime 함수
|
||||
- [ ] formatNumber 함수
|
||||
- [ ] formatPercent 함수
|
||||
- [ ] formatPhone 함수
|
||||
- [ ] formatBizNo 함수
|
||||
- [ ] 단위 테스트 (선택)
|
||||
|
||||
**적용 대상 파일** (30+ 파일):
|
||||
- [ ] 모든 accounting/* 컴포넌트
|
||||
- [ ] 모든 테이블에서 금액 표시하는 곳
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 상태 표시 컴포넌트 (1일)
|
||||
|
||||
### 2.1 상태 색상 중앙화
|
||||
|
||||
**위치**: `src/lib/status-colors.ts`
|
||||
|
||||
**설계**:
|
||||
```typescript
|
||||
// 공통 상태 색상
|
||||
export const STATUS_COLORS = {
|
||||
// 일반 상태
|
||||
active: 'bg-green-100 text-green-800',
|
||||
inactive: 'bg-gray-100 text-gray-800',
|
||||
pending: 'bg-yellow-100 text-yellow-800',
|
||||
completed: 'bg-blue-100 text-blue-800',
|
||||
cancelled: 'bg-red-100 text-red-800',
|
||||
|
||||
// 결제/금융 상태
|
||||
paid: 'bg-green-100 text-green-800',
|
||||
unpaid: 'bg-red-100 text-red-800',
|
||||
partial: 'bg-orange-100 text-orange-800',
|
||||
overdue: 'bg-red-100 text-red-800',
|
||||
|
||||
// 기본값
|
||||
default: 'bg-gray-100 text-gray-800',
|
||||
} as const;
|
||||
|
||||
// 도메인별 상태 색상
|
||||
export const BILL_STATUS_COLORS = { ... };
|
||||
export const VENDOR_CATEGORY_COLORS = { ... };
|
||||
export const ORDER_STATUS_COLORS = { ... };
|
||||
export const INSPECTION_STATUS_COLORS = { ... };
|
||||
```
|
||||
|
||||
**체크리스트**:
|
||||
- [ ] status-colors.ts 파일 생성
|
||||
- [ ] 공통 STATUS_COLORS 정의
|
||||
- [ ] BILL_STATUS_COLORS 이동
|
||||
- [ ] VENDOR_CATEGORY_COLORS 이동
|
||||
- [ ] ORDER_STATUS_COLORS 정의
|
||||
- [ ] INSPECTION_STATUS_COLORS 정의
|
||||
- [ ] PRODUCTION_STATUS_COLORS 정의
|
||||
|
||||
---
|
||||
|
||||
### 2.2 StatusBadge 컴포넌트
|
||||
|
||||
**위치**: `src/components/ui/status-badge.tsx`
|
||||
|
||||
**Props 설계**:
|
||||
```typescript
|
||||
interface StatusBadgeProps {
|
||||
status: string;
|
||||
label?: string;
|
||||
colorMap?: Record<string, string>;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**사용 예시**:
|
||||
```typescript
|
||||
// 색상맵 지정
|
||||
<StatusBadge
|
||||
status="paymentComplete"
|
||||
label="결제완료"
|
||||
colorMap={BILL_STATUS_COLORS}
|
||||
/>
|
||||
|
||||
// 공통 색상 사용
|
||||
<StatusBadge status="active" label="활성" />
|
||||
```
|
||||
|
||||
**체크리스트**:
|
||||
- [ ] StatusBadge 컴포넌트 생성
|
||||
- [ ] 기본 색상 (STATUS_COLORS) 적용
|
||||
- [ ] colorMap prop으로 커스텀 색상 지원
|
||||
- [ ] size 변형 (sm, md, lg)
|
||||
- [ ] ui/index.ts export 추가
|
||||
|
||||
**적용 대상 파일** (10+ 파일):
|
||||
- [ ] `accounting/BillManagement/index.tsx`
|
||||
- [ ] `accounting/SalesManagement/index.tsx`
|
||||
- [ ] `accounting/VendorManagement/index.tsx`
|
||||
- [ ] `production/WorkOrders/WorkOrderList.tsx`
|
||||
- [ ] `quality/InspectionManagement/InspectionList.tsx`
|
||||
- [ ] (나머지 파일)
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 카드/레이아웃 컴포넌트 (1일)
|
||||
|
||||
### 3.1 DetailInfoCard 컴포넌트
|
||||
|
||||
**위치**: `src/components/molecules/DetailInfoCard.tsx`
|
||||
|
||||
**Props 설계**:
|
||||
```typescript
|
||||
interface InfoItem {
|
||||
label: string;
|
||||
value: React.ReactNode;
|
||||
className?: string;
|
||||
span?: number; // grid span
|
||||
}
|
||||
|
||||
interface DetailInfoCardProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
items: InfoItem[];
|
||||
columns?: 1 | 2 | 3 | 4;
|
||||
className?: string;
|
||||
headerAction?: React.ReactNode;
|
||||
}
|
||||
```
|
||||
|
||||
**사용 예시**:
|
||||
```typescript
|
||||
<DetailInfoCard
|
||||
title="거래처 정보"
|
||||
columns={2}
|
||||
headerAction={<Button size="sm">수정</Button>}
|
||||
items={[
|
||||
{ label: '상호명', value: data.name },
|
||||
{ label: '사업자번호', value: formatBizNo(data.bizNo) },
|
||||
{ label: '대표자', value: data.ceo },
|
||||
{ label: '연락처', value: formatPhone(data.phone) },
|
||||
{ label: '주소', value: data.address, span: 2 },
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
**체크리스트**:
|
||||
- [ ] DetailInfoCard 컴포넌트 생성
|
||||
- [ ] columns 1/2/3/4 지원
|
||||
- [ ] span으로 컬럼 병합 지원
|
||||
- [ ] headerAction 슬롯
|
||||
- [ ] 반응형 (모바일에서 1컬럼)
|
||||
- [ ] molecules/index.ts export 추가
|
||||
|
||||
**적용 대상 파일** (15+ 파일):
|
||||
- [ ] `accounting/VendorManagement/VendorDetail.tsx`
|
||||
- [ ] `accounting/BillManagement/BillDetail.tsx`
|
||||
- [ ] `accounting/SalesManagement/SalesDetail.tsx`
|
||||
- [ ] `accounting/PurchaseManagement/PurchaseDetail.tsx`
|
||||
- [ ] `hr/EmployeeManagement/EmployeeDetail.tsx`
|
||||
- [ ] (나머지 Detail 페이지들)
|
||||
|
||||
---
|
||||
|
||||
### 3.2 FormGridLayout 컴포넌트
|
||||
|
||||
**위치**: `src/components/molecules/FormGridLayout.tsx`
|
||||
|
||||
**Props 설계**:
|
||||
```typescript
|
||||
interface FormGridLayoutProps {
|
||||
children: React.ReactNode;
|
||||
columns?: 1 | 2 | 3 | 4;
|
||||
gap?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface FormSectionProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
columns?: 1 | 2 | 3 | 4;
|
||||
}
|
||||
```
|
||||
|
||||
**사용 예시**:
|
||||
```typescript
|
||||
<FormSection title="기본 정보" columns={2}>
|
||||
<FormField label="이름" required value={name} onChange={setName} />
|
||||
<FormField label="이메일" type="email" value={email} onChange={setEmail} />
|
||||
<FormField label="주소" className="col-span-2" value={address} onChange={setAddress} />
|
||||
</FormSection>
|
||||
```
|
||||
|
||||
**체크리스트**:
|
||||
- [ ] FormGridLayout 컴포넌트 생성
|
||||
- [ ] FormSection 컴포넌트 생성
|
||||
- [ ] columns 1/2/3/4 지원
|
||||
- [ ] gap 크기 (sm/md/lg)
|
||||
- [ ] col-span 클래스 지원
|
||||
- [ ] 반응형
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 마이그레이션 및 검증 (1일)
|
||||
|
||||
### 4.1 기존 코드 마이그레이션
|
||||
|
||||
**체크리스트**:
|
||||
- [ ] DeleteDialog 마이그레이션 (40+ 파일)
|
||||
- [ ] formatCurrency 마이그레이션 (30+ 파일)
|
||||
- [ ] StatusBadge 마이그레이션 (10+ 파일)
|
||||
- [ ] DetailInfoCard 마이그레이션 (15+ 파일)
|
||||
- [ ] 불필요한 import 제거
|
||||
- [ ] 미사용 코드 삭제
|
||||
|
||||
### 4.2 검증
|
||||
|
||||
**체크리스트**:
|
||||
- [ ] 빌드 에러 없음 확인 (`npm run build`)
|
||||
- [ ] 타입 에러 없음 확인 (`npm run type-check`)
|
||||
- [ ] 주요 페이지 동작 테스트
|
||||
- [ ] 거래처 삭제
|
||||
- [ ] 품목 삭제
|
||||
- [ ] 금액 표시 확인
|
||||
- [ ] 상태 배지 표시 확인
|
||||
- [ ] 반응형 테스트 (모바일)
|
||||
|
||||
### 4.3 문서화
|
||||
|
||||
**체크리스트**:
|
||||
- [ ] 컴포넌트 JSDoc 주석
|
||||
- [ ] 사용 예시 코드
|
||||
- [ ] claudedocs 업데이트
|
||||
|
||||
---
|
||||
|
||||
## 파일 구조 (최종)
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── ui/
|
||||
│ │ ├── status-badge.tsx # 🆕 Phase 2
|
||||
│ │ └── ...
|
||||
│ ├── molecules/
|
||||
│ │ ├── StandardDialog.tsx # 기존
|
||||
│ │ ├── DeleteDialog.tsx # 🆕 Phase 1
|
||||
│ │ ├── DetailInfoCard.tsx # 🆕 Phase 3
|
||||
│ │ ├── FormGridLayout.tsx # 🆕 Phase 3
|
||||
│ │ └── index.ts
|
||||
│ └── ...
|
||||
├── lib/
|
||||
│ ├── formatters.ts # 🆕 Phase 1
|
||||
│ ├── status-colors.ts # 🆕 Phase 2
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 예상 효과
|
||||
|
||||
| Phase | 컴포넌트 | 절감 라인 | 영향 파일 |
|
||||
|-------|---------|----------|----------|
|
||||
| 1 | DeleteDialog | ~800줄 | 40+ |
|
||||
| 1 | formatters | ~150줄 | 30+ |
|
||||
| 2 | status-colors | ~200줄 | 10+ |
|
||||
| 2 | StatusBadge | ~100줄 | 10+ |
|
||||
| 3 | DetailInfoCard | ~400줄 | 15+ |
|
||||
| 3 | FormGridLayout | ~250줄 | 20+ |
|
||||
| **합계** | | **~1,900줄** | **50+** |
|
||||
|
||||
---
|
||||
|
||||
## 우선순위 정리
|
||||
|
||||
### 🔴 필수 (Phase 1)
|
||||
1. **DeleteDialog** - 가장 많은 중복, 즉시 효과
|
||||
2. **formatters** - 유틸 함수, 간단히 적용
|
||||
|
||||
### 🟡 권장 (Phase 2)
|
||||
3. **status-colors** - 색상 상수 중앙화
|
||||
4. **StatusBadge** - 일관된 상태 표시
|
||||
|
||||
### 🟢 선택 (Phase 3)
|
||||
5. **DetailInfoCard** - 상세 페이지 통일
|
||||
6. **FormGridLayout** - 폼 레이아웃 통일
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 날짜 | 변경 내용 |
|
||||
|-----|----------|
|
||||
| 2025-12-23 | 최초 작성 - 공통 컴포넌트 추출 계획 |
|
||||
@@ -578,7 +578,7 @@ export default function CustomerAccountManagementPage() {
|
||||
description="거래처 정보 및 계정을 관리합니다"
|
||||
icon={Building2}
|
||||
headerActions={
|
||||
<Button onClick={handleAddNew}>
|
||||
<Button className="ml-auto" onClick={handleAddNew}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
거래처 등록
|
||||
</Button>
|
||||
|
||||
@@ -661,7 +661,7 @@ export default function OrderManagementSalesPage() {
|
||||
description="수주 관리 및 생산지시 연동"
|
||||
icon={FileText}
|
||||
headerActions={
|
||||
<Button onClick={() => router.push("/sales/order-management-sales/new")}>
|
||||
<Button className="ml-auto" onClick={() => router.push("/sales/order-management-sales/new")}>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
수주 등록
|
||||
</Button>
|
||||
|
||||
@@ -113,7 +113,7 @@ async function getApiHeaders(): Promise<HeadersInit> {
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -659,7 +659,7 @@ export default function QuoteManagementPage() {
|
||||
description="견적서 작성 및 관리"
|
||||
icon={FileText}
|
||||
headerActions={
|
||||
<Button onClick={() => router.push("/sales/quote-management/new")}>
|
||||
<Button className="ml-auto" onClick={() => router.push("/sales/quote-management/new")}>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
견적 등록
|
||||
</Button>
|
||||
|
||||
@@ -307,18 +307,18 @@ export function BillManagementClient({
|
||||
|
||||
// ===== 헤더 액션 =====
|
||||
const headerActions = (
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
extraActions={
|
||||
<Button onClick={() => router.push('/ko/accounting/bills/new')}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
어음 등록
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
<Button className="ml-auto" onClick={() => router.push('/ko/accounting/bills/new')}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
어음 등록
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
// ===== 거래처 목록 (필터용) =====
|
||||
|
||||
@@ -452,18 +452,18 @@ export function SalesManagement() {
|
||||
|
||||
// ===== 헤더 액션 =====
|
||||
const headerActions = (
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
extraActions={
|
||||
<Button onClick={handleNewSales}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
매출 등록
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
<Button className="ml-auto" onClick={handleNewSales}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
매출 등록
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
// ===== 계정과목명 저장 핸들러 =====
|
||||
|
||||
@@ -276,22 +276,23 @@ export function VendorLedger() {
|
||||
|
||||
// ===== 헤더 액션 =====
|
||||
const headerActions = (
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
extraActions={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleExcelDownload}
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
<Button
|
||||
className="ml-auto"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleExcelDownload}
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
// ===== 테이블 하단 합계 행 =====
|
||||
|
||||
@@ -463,26 +463,26 @@ export function ApprovalBox() {
|
||||
|
||||
// ===== 헤더 액션 (DateRangeSelector + 승인/반려 버튼) =====
|
||||
const headerActions = (
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
extraActions={
|
||||
selectedItems.size > 0 && (
|
||||
<>
|
||||
<Button variant="default" onClick={handleApproveClick}>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
승인
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleRejectClick}>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
반려
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
{selectedItems.size > 0 && (
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button variant="default" onClick={handleApproveClick}>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
승인
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleRejectClick}>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
반려
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
// ===== 테이블 헤더 액션 (필터 + 정렬 셀렉트) =====
|
||||
|
||||
@@ -443,32 +443,32 @@ export function DraftBox() {
|
||||
|
||||
// ===== 헤더 액션 (DateRangeSelector + 버튼들) =====
|
||||
const headerActions = (
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
extraActions={
|
||||
<>
|
||||
{selectedItems.size > 0 && (
|
||||
<>
|
||||
<Button variant="default" onClick={handleSubmit}>
|
||||
<Send className="h-4 w-4 mr-2" />
|
||||
상신
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleDelete}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
삭제
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button onClick={handleNewDocument}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
문서 작성
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
<div className="ml-auto flex gap-2">
|
||||
{selectedItems.size > 0 && (
|
||||
<>
|
||||
<Button variant="default" onClick={handleSubmit}>
|
||||
<Send className="h-4 w-4 mr-2" />
|
||||
상신
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleDelete}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
삭제
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button onClick={handleNewDocument}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
문서 작성
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
// ===== 테이블 헤더 액션 (필터 + 정렬 셀렉트) =====
|
||||
|
||||
@@ -372,26 +372,26 @@ export function ReferenceBox() {
|
||||
|
||||
// ===== 헤더 액션 (DateRangeSelector + 열람/미열람 버튼) =====
|
||||
const headerActions = (
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
extraActions={
|
||||
selectedItems.size > 0 && (
|
||||
<>
|
||||
<Button variant="default" onClick={handleMarkReadClick}>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
열람
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleMarkUnreadClick}>
|
||||
<EyeOff className="h-4 w-4 mr-2" />
|
||||
미열람
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
{selectedItems.size > 0 && (
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button variant="default" onClick={handleMarkReadClick}>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
열람
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleMarkUnreadClick}>
|
||||
<EyeOff className="h-4 w-4 mr-2" />
|
||||
미열람
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
// ===== 테이블 헤더 액션 (필터 + 정렬 셀렉트) =====
|
||||
|
||||
@@ -378,18 +378,18 @@ export function BoardList() {
|
||||
|
||||
// ===== 헤더 액션 =====
|
||||
const headerActions = (
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
extraActions={
|
||||
<Button onClick={handleNewPost}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
게시글 등록
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
<Button className="ml-auto" onClick={handleNewPost}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
게시글 등록
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
// ===== 테이블 헤더 액션 (필터 1개만: 게시판) =====
|
||||
|
||||
@@ -396,7 +396,7 @@ export function BoardManagement() {
|
||||
|
||||
// 헤더 액션
|
||||
const headerActions = (
|
||||
<Button onClick={handleAddBoard}>
|
||||
<Button className="ml-auto" onClick={handleAddBoard}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
게시판 등록
|
||||
</Button>
|
||||
|
||||
@@ -254,18 +254,18 @@ export function InquiryList() {
|
||||
description="1:1 문의를 등록하고 답변을 확인합니다."
|
||||
icon={MessageSquare}
|
||||
headerActions={
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<>
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
<Button onClick={handleCreate}>
|
||||
<Button className="ml-auto" onClick={handleCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
문의 등록
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
searchValue={searchValue}
|
||||
onSearchChange={setSearchValue}
|
||||
|
||||
@@ -426,24 +426,24 @@ export function AttendanceManagement() {
|
||||
|
||||
// 헤더 액션 (DateRangeSelector + 버튼들)
|
||||
const headerActions = (
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
extraActions={
|
||||
<>
|
||||
<Button variant="outline" onClick={handleExcelDownload}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
<Button onClick={handleAddAttendance}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
근태 등록
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button variant="outline" onClick={handleExcelDownload}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
<Button onClick={handleAddAttendance}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
근태 등록
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
// 테이블 헤더 액션 (필터 + 정렬 셀렉트) - 사원관리와 동일한 위치
|
||||
|
||||
@@ -390,7 +390,7 @@ export function CardManagement() {
|
||||
|
||||
// 헤더 액션
|
||||
const headerActions = (
|
||||
<Button onClick={handleAddCard}>
|
||||
<Button className="ml-auto" onClick={handleAddCard}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
카드 등록
|
||||
</Button>
|
||||
|
||||
@@ -580,31 +580,31 @@ export function EmployeeManagement() {
|
||||
|
||||
// 헤더 액션 (DateRangeSelector + 버튼들)
|
||||
const headerActions = (
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
extraActions={
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setUserInviteOpen(true)}
|
||||
>
|
||||
<Mail className="w-4 h-4 mr-2" />
|
||||
사용자 초대
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleCSVUpload}>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
CSV 일괄 등록
|
||||
</Button>
|
||||
<Button onClick={handleAddEmployee}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
사원 등록
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setUserInviteOpen(true)}
|
||||
>
|
||||
<Mail className="w-4 h-4 mr-2" />
|
||||
사용자 초대
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleCSVUpload}>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
CSV 일괄 등록
|
||||
</Button>
|
||||
<Button onClick={handleAddEmployee}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
사원 등록
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
// 테이블 헤더 액션 (필터/정렬 셀렉트박스)
|
||||
|
||||
@@ -501,51 +501,51 @@ export function VacationManagement() {
|
||||
|
||||
// ===== 헤더 액션 (DateRangeSelector + 버튼들) =====
|
||||
const headerActions = (
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
extraActions={
|
||||
<>
|
||||
{/* 탭별 액션 버튼 */}
|
||||
{mainTab === 'grant' && (
|
||||
<Button onClick={() => setGrantDialogOpen(true)}>
|
||||
<>
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
<div className="ml-auto flex gap-2">
|
||||
{/* 탭별 액션 버튼 */}
|
||||
{mainTab === 'grant' && (
|
||||
<Button onClick={() => setGrantDialogOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
부여등록
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{mainTab === 'request' && (
|
||||
<>
|
||||
{/* 버튼 순서: 승인 → 거절 → 휴가신청 (휴가신청 버튼 위치 고정) */}
|
||||
{selectedItems.size > 0 && (
|
||||
<>
|
||||
<Button variant="default" onClick={handleApproveClick}>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
승인
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleRejectClick}>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
거절
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button onClick={() => setRequestDialogOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
부여등록
|
||||
휴가신청
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{mainTab === 'request' && (
|
||||
<>
|
||||
{/* 버튼 순서: 승인 → 거절 → 휴가신청 (휴가신청 버튼 위치 고정) */}
|
||||
{selectedItems.size > 0 && (
|
||||
<>
|
||||
<Button variant="default" onClick={handleApproveClick}>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
승인
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleRejectClick}>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
거절
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button onClick={() => setRequestDialogOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
휴가신청
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 엑셀 다운로드 버튼 - 주석처리 */}
|
||||
{/* <Button variant="outline" onClick={() => console.log('엑셀 다운로드')}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
엑셀 다운로드
|
||||
</Button> */}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{/* 엑셀 다운로드 버튼 - 주석처리 */}
|
||||
{/* <Button variant="outline" onClick={() => console.log('엑셀 다운로드')}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
엑셀 다운로드
|
||||
</Button> */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
// ===== 테이블 헤더 액션 (필터 + 정렬 셀렉트) - 사원관리와 동일한 위치 =====
|
||||
|
||||
@@ -117,25 +117,25 @@ export function PricingHistoryDialog({
|
||||
<div>
|
||||
<span className="text-muted-foreground">매입단가:</span>
|
||||
<div>
|
||||
{revision.previousData.purchasePrice?.toLocaleString() || '-'}원
|
||||
{revision.previousData?.purchasePrice?.toLocaleString() || '-'}원
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">가공비:</span>
|
||||
<div>
|
||||
{revision.previousData.processingCost?.toLocaleString() || '-'}원
|
||||
{revision.previousData?.processingCost?.toLocaleString() || '-'}원
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">판매단가:</span>
|
||||
<div>
|
||||
{revision.previousData.salesPrice?.toLocaleString() || '-'}원
|
||||
{revision.previousData?.salesPrice?.toLocaleString() || '-'}원
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">마진율:</span>
|
||||
<div>
|
||||
{revision.previousData.marginRate?.toFixed(1) || '-'}%
|
||||
{revision.previousData?.marginRate?.toFixed(1) || '-'}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -403,7 +403,7 @@ export function PricingListClient({
|
||||
// TODO: API 연동 시 품목 마스터 동기화 로직 구현
|
||||
console.log('품목 마스터 동기화');
|
||||
}}
|
||||
className="gap-2"
|
||||
className="ml-auto gap-2"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
품목 마스터 동기화
|
||||
|
||||
@@ -294,7 +294,7 @@ export function AccountManagement() {
|
||||
|
||||
// ===== 헤더 액션 (카드관리와 동일한 패턴) =====
|
||||
const headerActions = (
|
||||
<Button onClick={handleCreate}>
|
||||
<Button className="ml-auto" onClick={handleCreate}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
계좌 등록
|
||||
</Button>
|
||||
|
||||
@@ -411,7 +411,7 @@ export function PermissionManagement() {
|
||||
|
||||
// ===== 헤더 액션 =====
|
||||
const headerActions = (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="flex items-center gap-2 flex-wrap ml-auto">
|
||||
{selectedItems.size > 0 && (
|
||||
<Button variant="destructive" onClick={handleBulkDelete}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
|
||||
@@ -286,7 +286,7 @@ export function PopupList() {
|
||||
description="팝업 목록을 관리합니다."
|
||||
icon={Megaphone}
|
||||
headerActions={
|
||||
<Button onClick={handleCreate}>
|
||||
<Button className="ml-auto" onClick={handleCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
팝업 등록
|
||||
</Button>
|
||||
|
||||
@@ -228,7 +228,7 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
|
||||
{/* 헤더 액션 (달력, 버튼 등) - 타이틀 아래 배치 */}
|
||||
{headerActions && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="flex items-center gap-2 flex-wrap w-full">
|
||||
{headerActions}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user