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:
byeongcheolryu
2025-12-24 11:34:42 +09:00
parent d5f758f1eb
commit c1abf89d80
24 changed files with 657 additions and 221 deletions

View File

@@ -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 | 최초 작성 - 공통 컴포넌트 추출 계획 |

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 || '',
};
}

View File

@@ -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>

View File

@@ -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>
</>
);
// ===== 거래처 목록 (필터용) =====

View File

@@ -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>
</>
);
// ===== 계정과목명 저장 핸들러 =====

View File

@@ -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>
</>
);
// ===== 테이블 하단 합계 행 =====

View File

@@ -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>
)}
</>
);
// ===== 테이블 헤더 액션 (필터 + 정렬 셀렉트) =====

View File

@@ -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>
</>
);
// ===== 테이블 헤더 액션 (필터 + 정렬 셀렉트) =====

View File

@@ -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>
)}
</>
);
// ===== 테이블 헤더 액션 (필터 + 정렬 셀렉트) =====

View File

@@ -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개만: 게시판) =====

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>
</>
);
// 테이블 헤더 액션 (필터 + 정렬 셀렉트) - 사원관리와 동일한 위치

View File

@@ -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>

View File

@@ -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>
</>
);
// 테이블 헤더 액션 (필터/정렬 셀렉트박스)

View File

@@ -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>
</>
);
// ===== 테이블 헤더 액션 (필터 + 정렬 셀렉트) - 사원관리와 동일한 위치 =====

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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>
)}