From 269b901e64574ff3427f16ad6406cd5a1ddf44a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Thu, 22 Jan 2026 17:21:42 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20UI=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EC=83=81=ED=99=94=20=EB=B0=8F=20=EC=9E=85?= =?UTF-8?q?=EA=B8=88/=EC=B6=9C=EA=B8=88=20=EB=93=B1=EB=A1=9D=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 입금관리, 출금관리 리스트에 등록 버튼 추가 - skeleton, confirm-dialog, empty-state, status-badge UI 컴포넌트 추가 - document-system 컴포넌트 추상화 (ApprovalLine, DocumentHeader 등) - 여러 페이지 컴포넌트 리팩토링 및 코드 정리 Co-Authored-By: Claude Opus 4.5 --- ...AN-2026-01-22] ui-component-abstraction.md | 546 ++++++++++++++++++ .../boards/[boardCode]/[postId]/page.tsx | 48 +- .../hr/employee-management/[id]/page.tsx | 51 +- .../documents/BendingInspectionDocument.tsx | 56 +- .../documents/ImportInspectionDocument.tsx | 43 +- .../documents/JointbarInspectionDocument.tsx | 56 +- .../documents/ProductInspectionDocument.tsx | 51 +- .../documents/ScreenInspectionDocument.tsx | 56 +- .../documents/SlatInspectionDocument.tsx | 56 +- .../client-management-sales-admin/page.tsx | 76 +-- .../sales/order-management-sales/page.tsx | 106 ++-- .../production-orders/[id]/page.tsx | 71 +-- .../production-orders/page.tsx | 63 +- .../BadDebtCollection/BadDebtDetail.tsx | 70 +-- .../BillManagement/BillManagementClient.tsx | 38 +- .../DepositManagement/DepositDetail.tsx | 39 +- .../accounting/DepositManagement/index.tsx | 9 + .../ExpectedExpenseManagement/index.tsx | 27 +- .../VendorManagementClient.tsx | 39 +- .../WithdrawalManagement/WithdrawalDetail.tsx | 39 +- .../accounting/WithdrawalManagement/index.tsx | 9 + src/components/approval/ApprovalBox/index.tsx | 24 +- .../ExpenseEstimateDocument.tsx | 27 +- .../DocumentDetail/ExpenseReportDocument.tsx | 27 +- .../DocumentDetail/ProposalDocument.tsx | 27 +- .../approval/ReferenceBox/index.tsx | 57 +- src/components/board/BoardDetail/index.tsx | 39 +- src/components/board/BoardList/index.tsx | 35 +- .../BoardManagement/BoardDetailClientV2.tsx | 58 +- .../board/CommentSection/CommentItem.tsx | 37 +- .../category-management/index.tsx | 63 +- .../estimates/EstimateDetailForm.tsx | 116 ++-- .../modals/HandoverReportDocumentModal.tsx | 60 +- .../IssueManagementListClient.tsx | 47 +- .../item-management/ItemManagementClient.tsx | 53 +- .../labor-management/LaborDetailClient.tsx | 32 +- .../management/ConstructionDetailClient.tsx | 34 +- .../order-management/dialogs/OrderDialogs.tsx | 85 +-- .../modals/OrderDocumentModal.tsx | 16 +- .../PricingDetailClient.tsx | 32 +- .../components/ApprovalLine.tsx | 25 +- .../components/ConstructionApprovalTable.tsx | 115 ++++ .../components/DocumentHeader.tsx | 99 +++- .../components/LotApprovalTable.tsx | 121 ++++ .../components/QualityApprovalTable.tsx | 122 ++++ .../components/SignatureSection.tsx | 106 ++++ .../document-system/components/index.ts | 18 + src/components/document-system/index.ts | 12 + src/components/hr/CardManagement/index.tsx | 49 +- .../hr/DepartmentManagement/index.tsx | 51 +- .../hr/EmployeeManagement/index.tsx | 58 +- .../hr/VacationManagement/index.tsx | 64 +- .../components/DuplicateCodeDialog.tsx | 58 +- src/components/items/ItemListClient.tsx | 45 +- .../dialogs/TabManagementDialogs.tsx | 86 ++- .../orders/documents/ContractDocument.tsx | 21 +- .../orders/documents/TransactionDocument.tsx | 20 +- .../ShipmentManagement/ShipmentDetail.tsx | 54 +- .../documents/DeliveryConfirmation.tsx | 54 +- .../documents/ShippingSlip.tsx | 60 +- .../documents/TransactionStatement.tsx | 19 +- .../process-management/ProcessListClient.tsx | 45 +- .../WorkerScreen/CompletionConfirmDialog.tsx | 82 +-- src/components/quotes/LocationListPanel.tsx | 47 +- .../quotes/PurchaseOrderDocument.tsx | 198 +------ src/components/quotes/QuoteDocument.tsx | 99 +--- .../quotes/QuoteManagementClient.tsx | 76 +-- .../settings/AccountInfoManagement/index.tsx | 42 +- .../AccountManagement/AccountDetail.tsx | 48 +- .../settings/AccountManagement/index.tsx | 102 ++-- .../PermissionManagement/PermissionDetail.tsx | 49 +- .../PermissionDetailClient.tsx | 58 +- .../settings/PermissionManagement/index.tsx | 64 +- .../settings/RankManagement/index.tsx | 54 +- .../SubscriptionClient.tsx | 61 +- .../SubscriptionManagement.tsx | 61 +- .../settings/TitleManagement/index.tsx | 54 +- .../components/skeletons.tsx | 182 ++++++ .../IntegratedDetailTemplate/index.tsx | 52 +- .../templates/IntegratedListTemplateV2.tsx | 80 +-- .../templates/UniversalListPage/index.tsx | 30 +- src/components/ui/confirm-dialog.tsx | 216 +++++++ src/components/ui/empty-state.tsx | 226 ++++++++ src/components/ui/skeleton.tsx | 498 +++++++++++++++- src/components/ui/status-badge.tsx | 122 ++++ src/lib/utils/status-config.ts | 154 +++++ 86 files changed, 3761 insertions(+), 2614 deletions(-) create mode 100644 claudedocs/[PLAN-2026-01-22] ui-component-abstraction.md create mode 100644 src/components/document-system/components/ConstructionApprovalTable.tsx create mode 100644 src/components/document-system/components/LotApprovalTable.tsx create mode 100644 src/components/document-system/components/QualityApprovalTable.tsx create mode 100644 src/components/document-system/components/SignatureSection.tsx create mode 100644 src/components/templates/IntegratedDetailTemplate/components/skeletons.tsx create mode 100644 src/components/ui/confirm-dialog.tsx create mode 100644 src/components/ui/empty-state.tsx create mode 100644 src/components/ui/status-badge.tsx create mode 100644 src/lib/utils/status-config.ts diff --git a/claudedocs/[PLAN-2026-01-22] ui-component-abstraction.md b/claudedocs/[PLAN-2026-01-22] ui-component-abstraction.md new file mode 100644 index 00000000..d9a8007e --- /dev/null +++ b/claudedocs/[PLAN-2026-01-22] ui-component-abstraction.md @@ -0,0 +1,546 @@ +# UI 컴포넌트 공통화/추상화 계획 + +> **작성일**: 2026-01-22 +> **상태**: 🟢 진행 중 +> **범위**: 공통 UI 컴포넌트 추상화 및 스켈레톤 시스템 구축 + +--- + +## 결정 사항 (2026-01-22) + +| 항목 | 결정 | +|------|------| +| 스켈레톤 전환 범위 | **Option A: 전체 스켈레톤 전환** | +| 구현 우선순위 | **Phase 1 먼저** (ConfirmDialog → StatusBadge → EmptyState) | +| 확장 전략 | **옵션 기반 확장** - 새 패턴 발견 시 props 옵션으로 추가 | + +--- + +## 1. 현황 분석 요약 + +### 반복 패턴 현황 + +| 패턴 | 파일 수 | 발생 횟수 | 복잡도 | 우선순위 | +|------|---------|----------|--------|----------| +| 확인 다이얼로그 (삭제/저장) | 67개 | 170회 | 낮음 | 🔴 높음 | +| 상태 스타일 매핑 | 80개 | 다수 | 낮음 | 🔴 높음 | +| 날짜 범위 필터 | 55개 | 146회 | 중간 | 🟡 중간 | +| 빈 상태 UI | 70개 | 86회 | 낮음 | 🟡 중간 | +| 로딩 스피너/버튼 | 59개 | 120회 | 중간 | 🟡 중간 | +| 스켈레톤 UI | 4개 | 92회 | 높음 | 🔴 높음 | + +### 현재 스켈레톤 현황 + +**기존 구현:** +- `src/components/ui/skeleton.tsx` - 기본 스켈레톤 (단순 animate-pulse div) +- `IntegratedDetailTemplate/components/skeletons/` - 상세 페이지용 3종 + - `DetailFieldSkeleton.tsx` + - `DetailSectionSkeleton.tsx` + - `DetailGridSkeleton.tsx` +- `loading.tsx` - 4개 파일만 존재 (대부분 PageLoadingSpinner 사용) + +**문제점:** +1. 대부분 페이지에서 로딩 스피너 사용 (스켈레톤 미적용) +2. 리스트 페이지용 스켈레톤 없음 +3. 카드/대시보드용 스켈레톤 없음 +4. 페이지별 loading.tsx 부재 (4개만 존재) + +--- + +## 2. 공통화 대상 상세 + +### Phase 1: 핵심 공통 컴포넌트 (1주차) + +#### 1-1. ConfirmDialog 컴포넌트 + +**현재 (반복 코드):** +```tsx +// 67개 파일에서 거의 동일하게 반복 +const [showDeleteDialog, setShowDeleteDialog] = useState(false); + + + + + 삭제 확인 + + 정말 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다. + + + + 취소 + + {isLoading && } + 삭제 + + + + +``` + +**개선안:** +```tsx +// src/components/ui/confirm-dialog.tsx +interface ConfirmDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + description: string; + confirmText?: string; + cancelText?: string; + variant?: 'default' | 'destructive' | 'warning'; + loading?: boolean; + onConfirm: () => void | Promise; +} + +// 사용 예시 + +``` + +**효과:** +- 코드량: ~30줄 → ~10줄 (70% 감소) +- 일관된 UX 보장 +- 로딩 상태 자동 처리 + +--- + +#### 1-2. StatusBadge 컴포넌트 + createStatusConfig 유틸 + +**현재 (반복 코드):** +```tsx +// 80개 파일에서 각각 정의 +// estimates/types.ts +export const STATUS_STYLES: Record = { + pending: 'bg-yellow-100 text-yellow-800', + inProgress: 'bg-blue-100 text-blue-800', + completed: 'bg-green-100 text-green-800', +}; +export const STATUS_LABELS: Record = { + pending: '대기', + inProgress: '진행중', + completed: '완료', +}; + +// site-management/types.ts (거의 동일) +export const SITE_STATUS_STYLES: Record = { ... }; +export const SITE_STATUS_LABELS: Record = { ... }; +``` + +**개선안:** +```tsx +// src/lib/utils/status-config.ts +export type StatusVariant = 'default' | 'success' | 'warning' | 'error' | 'info'; + +export interface StatusConfig { + value: T; + label: string; + variant: StatusVariant; + description?: string; +} + +export function createStatusConfig( + configs: StatusConfig[] +): { + options: { value: T; label: string }[]; + getLabel: (status: T) => string; + getVariant: (status: T) => StatusVariant; + isValid: (status: string) => status is T; +} + +// src/components/ui/status-badge.tsx +interface StatusBadgeProps { + status: T; + config: ReturnType>; + size?: 'sm' | 'md' | 'lg'; +} + +// 사용 예시 +// estimates/types.ts +export const estimateStatusConfig = createStatusConfig([ + { value: 'pending', label: '대기', variant: 'warning' }, + { value: 'inProgress', label: '진행중', variant: 'info' }, + { value: 'completed', label: '완료', variant: 'success' }, +]); + +// 컴포넌트에서 + +``` + +**효과:** +- 타입 안전성 강화 +- 일관된 색상 체계 +- options 자동 생성 (Select용) + +--- + +#### 1-3. EmptyState 컴포넌트 + +**현재 (반복 코드):** +```tsx +// 70개 파일에서 다양한 형태로 반복 +{data.length === 0 && ( +
+ 데이터가 없습니다 +
+)} + +// 또는 + + + 등록된 항목이 없습니다 + + +``` + +**개선안:** +```tsx +// src/components/ui/empty-state.tsx +interface EmptyStateProps { + icon?: ReactNode; + title?: string; + description?: string; + action?: ReactNode; + variant?: 'default' | 'table' | 'card' | 'minimal'; +} + +// 사용 예시 +} + title="데이터가 없습니다" + description="새로운 항목을 등록하거나 검색 조건을 변경해보세요." + action={} +/> + +// 테이블 내 사용 + +``` + +--- + +### Phase 2: 스켈레톤 시스템 구축 (2주차) + +#### 2-1. 스켈레톤 컴포넌트 확장 + +**현재 문제:** +- 기본 Skeleton만 존재 (단순 div) +- 페이지 유형별 스켈레톤 부재 +- 대부분 PageLoadingSpinner 사용 (스켈레톤 미적용) + +**추가할 스켈레톤:** + +```tsx +// src/components/ui/skeletons/ +├── index.ts // 통합 export +├── ListPageSkeleton.tsx // 리스트 페이지용 +├── DetailPageSkeleton.tsx // 상세 페이지용 (기존 확장) +├── CardGridSkeleton.tsx // 카드 그리드용 +├── DashboardSkeleton.tsx // 대시보드용 +├── TableSkeleton.tsx // 테이블용 +├── FormSkeleton.tsx // 폼용 +└── ChartSkeleton.tsx // 차트용 +``` + +**1. ListPageSkeleton (리스트 페이지용)** +```tsx +interface ListPageSkeletonProps { + hasFilters?: boolean; + filterCount?: number; + hasDateRange?: boolean; + rowCount?: number; + columnCount?: number; + hasActions?: boolean; + hasPagination?: boolean; +} + +// 사용 예시 +export default function EstimateListLoading() { + return ( + + ); +} +``` + +**2. CardGridSkeleton (카드 그리드용)** +```tsx +interface CardGridSkeletonProps { + cardCount?: number; + cols?: 1 | 2 | 3 | 4; + cardHeight?: 'sm' | 'md' | 'lg'; + hasImage?: boolean; + hasFooter?: boolean; +} + +// 대시보드 카드, 칸반 보드 등에 사용 + +``` + +**3. TableSkeleton (테이블용)** +```tsx +interface TableSkeletonProps { + rowCount?: number; + columnCount?: number; + hasCheckbox?: boolean; + hasActions?: boolean; + columnWidths?: string[]; // ['w-12', 'w-32', 'flex-1', ...] +} + + +``` + +--- + +#### 2-2. loading.tsx 파일 생성 전략 + +**현재:** 4개 파일만 존재 +**목표:** 주요 페이지 경로에 맞춤형 loading.tsx 생성 + +**생성 대상 (우선순위):** + +| 경로 | 스켈레톤 타입 | 우선순위 | +|------|-------------|----------| +| `/construction/project/bidding/estimates` | ListPageSkeleton | 🔴 | +| `/construction/project/bidding` | ListPageSkeleton | 🔴 | +| `/construction/project/contract` | ListPageSkeleton | 🔴 | +| `/construction/order/*` | ListPageSkeleton | 🔴 | +| `/accounting/*` | ListPageSkeleton | 🟡 | +| `/hr/*` | ListPageSkeleton | 🟡 | +| `/settings/*` | ListPageSkeleton | 🟢 | +| `상세 페이지` | DetailPageSkeleton | 🟡 | +| `대시보드` | DashboardSkeleton | 🟡 | + +--- + +### Phase 3: 날짜 범위 필터 + 로딩 버튼 (3주차) + +#### 3-1. DateRangeFilter 컴포넌트 + +**현재 (반복 코드):** +```tsx +// 55개 파일에서 반복 +const [startDate, setStartDate] = useState(''); +const [endDate, setEndDate] = useState(''); + +
+ + ~ + +
+``` + +**개선안:** +```tsx +// src/components/ui/date-range-filter.tsx +interface DateRangeFilterProps { + value: { start: string; end: string }; + onChange: (range: { start: string; end: string }) => void; + presets?: ('today' | 'week' | 'month' | 'quarter' | 'year')[]; + disabled?: boolean; +} + +// 사용 예시 + { + setStartDate(start); + setEndDate(end); + }} + presets={['today', 'week', 'month']} +/> +``` + +--- + +#### 3-2. LoadingButton 컴포넌트 + +**현재 (반복 코드):** +```tsx +// 59개 파일에서 반복 + +``` + +**개선안:** +```tsx +// src/components/ui/loading-button.tsx +interface LoadingButtonProps extends ButtonProps { + loading?: boolean; + loadingText?: string; + spinnerPosition?: 'left' | 'right'; +} + +// 사용 예시 + + 저장 + +``` + +--- + +## 3. 로딩 스피너 vs 스켈레톤 전략 + +### 논의 사항 + +**Option A: 전체 스켈레톤 전환** +- 장점: 더 나은 UX, 레이아웃 시프트 방지 +- 단점: 구현 비용 높음, 페이지별 커스텀 필요 + +**Option B: 하이브리드 (권장)** +- 페이지 로딩: 스켈레톤 (loading.tsx) +- 버튼/액션 로딩: 스피너 유지 (LoadingButton) +- 데이터 갱신: 스피너 유지 + +**Option C: 현행 유지** +- 대부분 스피너 유지 +- 특정 페이지만 스켈레톤 + +### 권장안: Option B (하이브리드) + +| 상황 | 로딩 UI | 이유 | +|------|---------|------| +| 페이지 초기 로딩 | 스켈레톤 | 레이아웃 힌트 제공 | +| 페이지 전환 | 스켈레톤 | Next.js loading.tsx 활용 | +| 버튼 클릭 (저장/삭제) | 스피너 | 짧은 작업, 버튼 내 피드백 | +| 데이터 갱신 (필터 변경) | 스피너 or 스켈레톤 | 상황에 따라 | +| 무한 스크롤 | 스켈레톤 | 추가 컨텐츠 힌트 | + +--- + +## 4. 구현 로드맵 + +### Week 1: 핵심 컴포넌트 +- [x] ConfirmDialog 컴포넌트 생성 ✅ (2026-01-22) + - `src/components/ui/confirm-dialog.tsx` + - variants: default, destructive, warning, success + - presets: DeleteConfirmDialog, SaveConfirmDialog, CancelConfirmDialog + - 내부/외부 로딩 상태 자동 관리 +- [x] StatusBadge + createStatusConfig 유틸 생성 ✅ (2026-01-22) + - `src/lib/utils/status-config.ts` + - `src/components/ui/status-badge.tsx` + - 프리셋: default, success, warning, destructive, info, muted, orange, purple + - 모드: badge (배경+텍스트), text (텍스트만) + - OPTIONS, LABELS, STYLES 자동 생성 +- [x] EmptyState 컴포넌트 생성 ✅ (2026-01-22) + - `src/components/ui/empty-state.tsx` + - variants: default, compact, large + - presets: noData, noResults, noItems, error + - TableEmptyState 추가 (테이블용) +- [x] 기존 코드 마이그레이션 (10개 파일 시범) ✅ (2026-01-22) + - PricingDetailClient.tsx - 삭제 확인 + - ItemManagementClient.tsx - 단일/일괄 삭제 + - LaborDetailClient.tsx - 삭제 확인 + - ConstructionDetailClient.tsx - 완료 확인 (warning) + - QuoteManagementClient.tsx - 단일/일괄 삭제 + - OrderDialogs.tsx - 저장/삭제/카테고리삭제 + - DepartmentManagement/index.tsx - 삭제 확인 + - VacationManagement/index.tsx - 승인/거절 확인 + - AccountDetail.tsx - 삭제 확인 + - ProcessListClient.tsx - 삭제 확인 + +### Week 2: 스켈레톤 시스템 +- [ ] ListPageSkeleton 컴포넌트 생성 +- [ ] TableSkeleton 컴포넌트 생성 +- [ ] CardGridSkeleton 컴포넌트 생성 +- [ ] 주요 경로 loading.tsx 생성 (construction/*) + +### Week 3: 필터 + 버튼 + 마이그레이션 +- [ ] DateRangeFilter 컴포넌트 생성 +- [ ] LoadingButton 컴포넌트 생성 +- [ ] 전체 코드 마이그레이션 + +### Week 4: 마무리 + QA +- [ ] 남은 마이그레이션 +- [ ] 문서화 +- [ ] 성능 테스트 + +--- + +## 5. 예상 효과 + +### 코드량 감소 +| 컴포넌트 | Before | After | 감소율 | +|---------|--------|-------|--------| +| ConfirmDialog | ~30줄 | ~10줄 | 67% | +| StatusBadge | ~20줄 | ~5줄 | 75% | +| EmptyState | ~10줄 | ~3줄 | 70% | +| DateRangeFilter | ~15줄 | ~5줄 | 67% | + +### 일관성 향상 +- 동일한 UX 패턴 적용 +- 디자인 시스템 강화 +- 유지보수 용이성 증가 + +### 성능 개선 +- 스켈레톤으로 인지 성능 향상 +- 레이아웃 시프트 감소 +- 사용자 이탈률 감소 + +--- + +## 6. 결정 필요 사항 + +### Q1: 스켈레톤 전환 범위 +- [ ] Option A: 전체 스켈레톤 전환 +- [ ] Option B: 하이브리드 (권장) +- [ ] Option C: 현행 유지 + +### Q2: 구현 우선순위 +- [ ] Phase 1 먼저 (ConfirmDialog, StatusBadge, EmptyState) +- [ ] Phase 2 먼저 (스켈레톤 시스템) +- [ ] 동시 진행 + +### Q3: 마이그레이션 범위 +- [ ] 전체 파일 한번에 +- [ ] 점진적 (신규/수정 파일만) +- [ ] 도메인별 순차 (construction → accounting → hr) + +--- + +## 7. 파일 구조 (최종) + +``` +src/components/ui/ +├── confirm-dialog.tsx # Phase 1 +├── status-badge.tsx # Phase 1 +├── empty-state.tsx # Phase 1 +├── date-range-filter.tsx # Phase 3 +├── loading-button.tsx # Phase 3 +├── skeleton.tsx # 기존 +└── skeletons/ # Phase 2 + ├── index.ts + ├── ListPageSkeleton.tsx + ├── DetailPageSkeleton.tsx + ├── CardGridSkeleton.tsx + ├── DashboardSkeleton.tsx + ├── TableSkeleton.tsx + ├── FormSkeleton.tsx + └── ChartSkeleton.tsx + +src/lib/utils/ +└── status-config.ts # Phase 1 +``` + +--- + +**다음 단계**: 위 결정 사항에 대한 의견 확정 후 구현 시작 diff --git a/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx b/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx index 27f0ba3e..5ce0cb53 100644 --- a/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx +++ b/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx @@ -15,17 +15,7 @@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import { Textarea } from '@/components/ui/textarea'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { getDynamicBoardPost, getDynamicBoardComments, @@ -99,6 +89,9 @@ export default function DynamicBoardDetailPage() { const [editingCommentId, setEditingCommentId] = useState(null); const [editingContent, setEditingContent] = useState(''); + // 삭제 다이얼로그 + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + // 현재 사용자 ID 가져오기 useEffect(() => { const userStr = localStorage.getItem('user'); @@ -269,26 +262,10 @@ export default function DynamicBoardDetailPage() { 수정 - - - - - - - 게시글 삭제 - - 이 게시글을 삭제하시겠습니까? 삭제된 게시글은 복구할 수 없습니다. - - - - 취소 - 삭제 - - - + )} @@ -406,6 +383,15 @@ export default function DynamicBoardDetailPage() { 목록으로 + + {/* 게시글 삭제 확인 다이얼로그 */} + ); } \ No newline at end of file diff --git a/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx b/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx index aa321783..2764cad9 100644 --- a/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx +++ b/src/app/[locale]/(protected)/hr/employee-management/[id]/page.tsx @@ -5,16 +5,7 @@ import { useState, useEffect, useCallback } from 'react'; import { EmployeeForm } from '@/components/hr/EmployeeManagement/EmployeeForm'; import { getEmployeeById, deleteEmployee, updateEmployee } from '@/components/hr/EmployeeManagement/actions'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; import type { Employee, EmployeeFormData } from '@/components/hr/EmployeeManagement/types'; @@ -123,30 +114,22 @@ export default function EmployeeDetailPage() { )} /> - - - - 사원 삭제 - - "{employee.name}" 사원을 삭제하시겠습니까? -
- - 삭제된 사원 정보는 복구할 수 없습니다. - -
-
- - 취소 - - {isDeleting ? '삭제 중...' : '삭제'} - - -
-
+ + "{employee.name}" 사원을 삭제하시겠습니까? +
+ + 삭제된 사원 정보는 복구할 수 없습니다. + + + } + loading={isDeleting} + /> ); } diff --git a/src/app/[locale]/(protected)/quality/qms/components/documents/BendingInspectionDocument.tsx b/src/app/[locale]/(protected)/quality/qms/components/documents/BendingInspectionDocument.tsx index 874f8074..db5d7b17 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/documents/BendingInspectionDocument.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/documents/BendingInspectionDocument.tsx @@ -1,6 +1,14 @@ "use client"; +/** + * 절곡품 중간검사 성적서 문서 컴포넌트 + * + * 공통 컴포넌트 사용: + * - DocumentHeader: quality 레이아웃 + customApproval (QualityApprovalTable) + */ + import React from 'react'; +import { DocumentHeader, QualityApprovalTable } from '@/components/document-system'; // 절곡품 중간검사 성적서 데이터 타입 export interface BendingInspectionData { @@ -132,41 +140,19 @@ interface BendingInspectionDocumentProps { export const BendingInspectionDocument = ({ data = MOCK_BENDING_INSPECTION }: BendingInspectionDocumentProps) => { return (
- {/* 헤더 */} -
-
-
KD
-
경동기업
KYUNGDONG COMPANY
-
-
-
절곡품
-
중간검사 성적서
-
- - - - - - - - - - - - - - - - - - - -
-
- -
-
작성검토승인
{data.approvers.writer}{data.approvers.reviewer}{data.approvers.approver}
판매/전진생산품질
-
+ {/* 문서 헤더 (공통 컴포넌트) */} + + } + /> {/* 기본 정보 테이블 */} diff --git a/src/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument.tsx b/src/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument.tsx index a4752b5f..5cca39d2 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument.tsx @@ -1,6 +1,14 @@ "use client"; +/** + * 수입검사 성적서 문서 컴포넌트 + * + * 공통 컴포넌트 사용: + * - DocumentHeader: default 레이아웃 + customApproval (QualityApprovalTable) + */ + import React from 'react'; +import { DocumentHeader, QualityApprovalTable } from '@/components/document-system'; // 수입검사 성적서 데이터 타입 export interface ImportInspectionData { @@ -125,29 +133,18 @@ interface ImportInspectionDocumentProps { export const ImportInspectionDocument = ({ data = MOCK_IMPORT_INSPECTION }: ImportInspectionDocumentProps) => { return (
- {/* 헤더 */} -
-
-
KD
-
경동기업
-
-
수 입 검 사 성 적 서
-
-
- - - - - - - - - - -
담당부서장
결재{data.approvers.writer}
-
접고일자: {data.reportDate}
-
- + {/* 문서 헤더 (공통 컴포넌트) */} + + } + /> {/* 기본 정보 테이블 */} diff --git a/src/app/[locale]/(protected)/quality/qms/components/documents/JointbarInspectionDocument.tsx b/src/app/[locale]/(protected)/quality/qms/components/documents/JointbarInspectionDocument.tsx index c1fce2cc..87e117b1 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/documents/JointbarInspectionDocument.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/documents/JointbarInspectionDocument.tsx @@ -1,6 +1,14 @@ "use client"; +/** + * 조인트바 중간검사 성적서 문서 컴포넌트 + * + * 공통 컴포넌트 사용: + * - DocumentHeader: quality 레이아웃 + customApproval (QualityApprovalTable) + */ + import React from 'react'; +import { DocumentHeader, QualityApprovalTable } from '@/components/document-system'; // 조인트바 중간검사 성적서 데이터 타입 export interface JointbarInspectionData { @@ -100,41 +108,19 @@ interface JointbarInspectionDocumentProps { export const JointbarInspectionDocument = ({ data = MOCK_JOINTBAR_INSPECTION }: JointbarInspectionDocumentProps) => { return (
- {/* 헤더 */} -
-
-
KD
-
경동기업
KYUNGDONG COMPANY
-
-
-
조인트바
-
중간검사 성적서
-
-
- - - - - - - - - - - - - - - - - - -
-
- -
-
작성검토승인
{data.approvers.writer}{data.approvers.reviewer}{data.approvers.approver}
판매/전진생산품질
- + {/* 문서 헤더 (공통 컴포넌트) */} + + } + /> {/* 기본 정보 테이블 */} diff --git a/src/app/[locale]/(protected)/quality/qms/components/documents/ProductInspectionDocument.tsx b/src/app/[locale]/(protected)/quality/qms/components/documents/ProductInspectionDocument.tsx index ef83ccf2..84431d14 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/documents/ProductInspectionDocument.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/documents/ProductInspectionDocument.tsx @@ -1,6 +1,14 @@ "use client"; +/** + * 제품검사 성적서 문서 컴포넌트 + * + * 공통 컴포넌트 사용: + * - DocumentHeader: default 레이아웃 + customApproval (QualityApprovalTable) + */ + import React from 'react'; +import { DocumentHeader, QualityApprovalTable } from '@/components/document-system'; // 제품검사 성적서 데이터 타입 export interface ProductInspectionData { @@ -149,38 +157,17 @@ interface ProductInspectionDocumentProps { export const ProductInspectionDocument = ({ data = MOCK_PRODUCT_INSPECTION }: ProductInspectionDocumentProps) => { return (
- {/* 헤더 */} -
-
-
KD
-
경동기업
KYUNGDONG COMPANY
-
-
제 품 검 사 성 적 서
-
- - - - - - - - - - - - - - - - - - -
-
- -
-
작성검토승인
{data.approvers.writer}{data.approvers.reviewer}{data.approvers.approver}
판매/전진생산품질
- + {/* 문서 헤더 (공통 컴포넌트) */} + + } + /> {/* 기본 정보 테이블 */} diff --git a/src/app/[locale]/(protected)/quality/qms/components/documents/ScreenInspectionDocument.tsx b/src/app/[locale]/(protected)/quality/qms/components/documents/ScreenInspectionDocument.tsx index 842898b5..3e99eaa7 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/documents/ScreenInspectionDocument.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/documents/ScreenInspectionDocument.tsx @@ -1,6 +1,14 @@ "use client"; +/** + * 스크린 중간검사 성적서 문서 컴포넌트 + * + * 공통 컴포넌트 사용: + * - DocumentHeader: quality 레이아웃 + customApproval (QualityApprovalTable) + */ + import React from 'react'; +import { DocumentHeader, QualityApprovalTable } from '@/components/document-system'; // 스크린 중간검사 성적서 데이터 타입 export interface ScreenInspectionData { @@ -103,41 +111,19 @@ interface ScreenInspectionDocumentProps { export const ScreenInspectionDocument = ({ data = MOCK_SCREEN_INSPECTION }: ScreenInspectionDocumentProps) => { return (
- {/* 헤더 */} -
-
-
KD
-
경동기업
KYUNGDONG COMPANY
-
-
-
스크린
-
중간검사 성적서
-
-
- - - - - - - - - - - - - - - - - - -
-
- -
-
작성검토승인
{data.approvers.writer}{data.approvers.reviewer}{data.approvers.approver}
판매/전진생산품질
- + {/* 문서 헤더 (공통 컴포넌트) */} + + } + /> {/* 기본 정보 테이블 */} diff --git a/src/app/[locale]/(protected)/quality/qms/components/documents/SlatInspectionDocument.tsx b/src/app/[locale]/(protected)/quality/qms/components/documents/SlatInspectionDocument.tsx index 70dbcabf..b7798274 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/documents/SlatInspectionDocument.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/documents/SlatInspectionDocument.tsx @@ -1,6 +1,14 @@ "use client"; +/** + * 슬랫 중간검사 성적서 문서 컴포넌트 + * + * 공통 컴포넌트 사용: + * - DocumentHeader: quality 레이아웃 + customApproval (QualityApprovalTable) + */ + import React from 'react'; +import { DocumentHeader, QualityApprovalTable } from '@/components/document-system'; // 슬랫 중간검사 성적서 데이터 타입 export interface SlatInspectionData { @@ -99,41 +107,19 @@ interface SlatInspectionDocumentProps { export const SlatInspectionDocument = ({ data = MOCK_SLAT_INSPECTION }: SlatInspectionDocumentProps) => { return (
- {/* 헤더 */} -
-
-
KD
-
경동기업
KYUNGDONG COMPANY
-
-
-
슬랫
-
중간검사 성적서
-
-
- - - - - - - - - - - - - - - - - - -
-
- -
-
작성검토승인
{data.approvers.writer}{data.approvers.reviewer}{data.approvers.approver}
판매/전진생산품질
- + {/* 문서 헤더 (공통 컴포넌트) */} + + } + /> {/* 기본 정보 테이블 */} diff --git a/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx b/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx index 2a8950df..3ded7d50 100644 --- a/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx +++ b/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx @@ -46,16 +46,7 @@ import { } from "@/components/ui/table"; import { Checkbox } from "@/components/ui/checkbox"; import { ListMobileCard, InfoField } from "@/components/organisms/MobileCard"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; +import { DeleteConfirmDialog } from "@/components/ui/confirm-dialog"; import { sendNewClientNotification } from "@/lib/actions/fcm"; import { isNextRedirectError } from "@/lib/utils/redirect-error"; @@ -663,49 +654,38 @@ export default function CustomerAccountManagementPage() { renderDialogs: () => ( <> {/* 삭제 확인 다이얼로그 */} - - - - 거래처 삭제 확인 - - {deleteTargetId - ? `거래처: ${clients.find((c) => c.id === deleteTargetId)?.name || deleteTargetId}` - : ""} -
- 이 거래처를 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다. -
-
- - 취소 - - 삭제 - - -
-
+ + {deleteTargetId + ? `거래처: ${clients.find((c) => c.id === deleteTargetId)?.name || deleteTargetId}` + : ""} +
+ 이 거래처를 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다. + + } + /> {/* 일괄 삭제 확인 다이얼로그 */} - - - - 일괄 삭제 확인 - - 선택한 {selectedItems.size}개의 거래처를 삭제하시겠습니까? -
+ onConfirm={handleConfirmBulkDelete} + title="일괄 삭제 확인" + description={ + <> + 선택한 {selectedItems.size}개의 거래처를 삭제하시겠습니까? +
+ 삭제된 데이터는 복구할 수 없습니다. -
-
- - 취소 - - 삭제 - - -
-
+ + + } + /> ), }; diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx index c8cfb6df..93fb6876 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx @@ -41,16 +41,7 @@ import { import { Checkbox } from "@/components/ui/checkbox"; import { formatAmount, formatAmountManwon } from "@/utils/formatAmount"; import { ListMobileCard, InfoField } from "@/components/organisms/MobileCard"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; +import { ConfirmDialog, DeleteConfirmDialog } from "@/components/ui/confirm-dialog"; import { getOrders, getOrderStats, @@ -709,70 +700,41 @@ export default function OrderManagementSalesPage() { renderDialogs: () => ( <> {/* 수주 취소 확인 다이얼로그 */} - - - - 수주 취소 확인 - - {cancelTargetId - ? `수주번호: ${orders.find((o) => o.id === cancelTargetId)?.lotNumber || cancelTargetId}` - : ""} -
- 이 수주를 취소하시겠습니까? 취소된 수주는 복구할 수 없습니다. -
-
- - 닫기 - - 취소 확정 - - -
-
+ + {cancelTargetId + ? `수주번호: ${orders.find((o) => o.id === cancelTargetId)?.lotNumber || cancelTargetId}` + : ""} +
+ 이 수주를 취소하시겠습니까? 취소된 수주는 복구할 수 없습니다. + + } + confirmText="취소 확정" + /> {/* 수주 삭제 확인 다이얼로그 */} - - - - - ⚠️ - 삭제 확인 - - -
-

- 선택한 {deleteTargetIds.length}개의 수주를 삭제하시겠습니까? -

-
-
- ⚠️ -
- 주의 -
- 삭제된 수주는 복구할 수 없습니다. 관련된 작업지시, 출하정보도 함께 삭제될 수 있습니다. -
-
-
-
-
-
- - 취소 - - {isDeleting ? ( - - ) : ( - - )} - {isDeleting ? "삭제 중..." : "삭제"} - - -
-
+ + 선택한 {deleteTargetIds.length}개의 수주를 삭제하시겠습니까? +
+ + 삭제된 수주는 복구할 수 없습니다. 관련된 작업지시, 출하정보도 함께 삭제될 수 있습니다. + + + } + loading={isDeleting} + /> ), }; diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx index b6eb9a94..6f7fb13f 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/[id]/page.tsx @@ -37,13 +37,13 @@ import { BadgeSm } from "@/components/atoms/BadgeSm"; import { AlertDialog, AlertDialogAction, - AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; +import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { toast } from "sonner"; import { ServerErrorPage } from "@/components/common/ServerErrorPage"; @@ -676,51 +676,34 @@ export default function ProductionOrderDetailPage() { {/* 팝업1: 작업지시 생성 확인 다이얼로그 */} - - - - - - 작업지시서 자동 생성 - - -
-

- 다음 공정에 대한 작업지시서가 생성됩니다: -

- {/* 공정 목록 (실제로는 API에서 받아온 데이터) */} - {productionOrder?.pendingWorkOrderCount && productionOrder.pendingWorkOrderCount > 0 && ( -
    - {SAMPLE_PROCESSES.slice(0, productionOrder.pendingWorkOrderCount).map((process) => ( -
  • - - {process.name} ({process.quantity}개) -
  • - ))} -
- )} -

- 생성된 작업지시서는 생산팀에서 확인하고 작업을 진행할 수 있습니다. -

-
-
-
- - 취소 - - - {isCreating ? "생성 중..." : "작업지시 생성"} - - -
-
+ onConfirm={handleConfirmCreateWorkOrder} + title="작업지시서 자동 생성" + description={ +
+

+ 다음 공정에 대한 작업지시서가 생성됩니다: +

+ {productionOrder?.pendingWorkOrderCount && productionOrder.pendingWorkOrderCount > 0 && ( +
    + {SAMPLE_PROCESSES.slice(0, productionOrder.pendingWorkOrderCount).map((process) => ( +
  • + + {process.name} ({process.quantity}개) +
  • + ))} +
+ )} +

+ 생성된 작업지시서는 생산팀에서 확인하고 작업을 진행할 수 있습니다. +

+
+ } + confirmText="작업지시 생성" + loading={isCreating} + /> {/* 팝업2: 작업지시 생성 성공 다이얼로그 */} diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx index 0f4d4e29..1c91bc32 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/production-orders/page.tsx @@ -14,16 +14,7 @@ import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; +import { DeleteConfirmDialog } from "@/components/ui/confirm-dialog"; import { Table, TableBody, @@ -590,43 +581,21 @@ export default function ProductionOrdersListPage() { renderMobileCard, renderDialogs: () => ( - - - - - ⚠️ - 삭제 확인 - - -
-

- 선택한 {deleteCount}개의 항목을 삭제하시겠습니까? -

-
-
- ⚠️ -
- 주의 -
- 삭제된 항목은 복구할 수 없습니다. 관련된 데이터도 함께 삭제될 수 있습니다. -
-
-
-
-
-
- - 취소 - - - 삭제 - - -
-
+ + 선택한 {deleteCount}개의 항목을 삭제하시겠습니까? +
+ + 삭제된 항목은 복구할 수 없습니다. 관련된 데이터도 함께 삭제될 수 있습니다. + + + } + /> ), }; diff --git a/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx index 6ecdcbc2..f438b8ee 100644 --- a/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx +++ b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx @@ -27,16 +27,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog, SaveConfirmDialog } from '@/components/ui/confirm-dialog'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { badDebtConfig } from './badDebtConfig'; import { toast } from 'sonner'; @@ -1033,48 +1024,27 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp /> {/* 삭제 확인 다이얼로그 */} - - - - 악성채권 삭제 - - '{formData.vendorName}'의 악성채권 기록을 삭제하시겠습니까? -
- 확인 클릭 시 목록으로 이동합니다. -
-
- - 취소 - - 삭제 - - -
-
+ + '{formData.vendorName}'의 악성채권 기록을 삭제하시겠습니까? +
+ 확인 클릭 시 목록으로 이동합니다. + + } + /> {/* 저장 확인 다이얼로그 */} - - - - 저장 확인 - - 입력한 내용을 저장하시겠습니까? - - - - 취소 - - 저장 - - - - + ); } \ No newline at end of file diff --git a/src/components/accounting/BillManagement/BillManagementClient.tsx b/src/components/accounting/BillManagement/BillManagementClient.tsx index 6ec4fa03..36cff75c 100644 --- a/src/components/accounting/BillManagement/BillManagementClient.tsx +++ b/src/components/accounting/BillManagement/BillManagementClient.tsx @@ -22,16 +22,7 @@ import { import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { TableRow, TableCell } from '@/components/ui/table'; import { Select, @@ -563,25 +554,14 @@ export function BillManagementClient({ }} /> - - - - 어음 삭제 - - 이 어음을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다. - - - - 취소 - - 삭제 - - - - + ); } diff --git a/src/components/accounting/DepositManagement/DepositDetail.tsx b/src/components/accounting/DepositManagement/DepositDetail.tsx index 3863d704..b8e8a092 100644 --- a/src/components/accounting/DepositManagement/DepositDetail.tsx +++ b/src/components/accounting/DepositManagement/DepositDetail.tsx @@ -17,16 +17,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { toast } from 'sonner'; @@ -323,26 +314,14 @@ export function DepositDetail({ depositId, mode }: DepositDetailProps) { {/* ===== 삭제 확인 다이얼로그 ===== */} - - - - 입금 삭제 - - 이 입금 내역을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다. - - - - 취소 - - {isLoading ? '삭제중...' : '삭제'} - - - - + ); } diff --git a/src/components/accounting/DepositManagement/index.tsx b/src/components/accounting/DepositManagement/index.tsx index faef545f..6bb0c2b8 100644 --- a/src/components/accounting/DepositManagement/index.tsx +++ b/src/components/accounting/DepositManagement/index.tsx @@ -18,6 +18,7 @@ import { useRouter } from 'next/navigation'; import { Banknote, Pencil, + Plus, Save, Trash2, RefreshCw, @@ -324,6 +325,14 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan }, filterTitle: '입금 필터', + // 헤더 액션 (등록 버튼) + headerActions: () => ( + + ), + // Stats 카드 computeStats: (): StatCard[] => [ { label: '총 입금', value: `${stats.totalDeposit.toLocaleString()}원`, icon: Banknote, iconColor: 'text-blue-500' }, diff --git a/src/components/accounting/ExpectedExpenseManagement/index.tsx b/src/components/accounting/ExpectedExpenseManagement/index.tsx index 0b95511e..ebe85b91 100644 --- a/src/components/accounting/ExpectedExpenseManagement/index.tsx +++ b/src/components/accounting/ExpectedExpenseManagement/index.tsx @@ -36,6 +36,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { Dialog, DialogContent, @@ -1040,25 +1041,13 @@ export function ExpectedExpenseManagement({ /> {/* 삭제 확인 다이얼로그 */} - - - - 지출 예상 내역 삭제 - - 이 지출 예상 내역을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다. - - - - 취소 - - 삭제 - - - - + {/* 예상 지급일 변경 다이얼로그 */} diff --git a/src/components/accounting/VendorManagement/VendorManagementClient.tsx b/src/components/accounting/VendorManagement/VendorManagementClient.tsx index 4205cb8e..31842ac5 100644 --- a/src/components/accounting/VendorManagement/VendorManagementClient.tsx +++ b/src/components/accounting/VendorManagement/VendorManagementClient.tsx @@ -20,16 +20,7 @@ import { import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { TableRow, TableCell } from '@/components/ui/table'; import { Select, @@ -583,26 +574,14 @@ export function VendorManagementClient({ initialData, initialTotal }: VendorMana /> {/* 삭제 확인 다이얼로그 */} - - - - 거래처 삭제 - - 이 거래처를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다. - - - - 취소 - - {isLoading ? '삭제 중...' : '삭제'} - - - - + ); } diff --git a/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx b/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx index 2088dc57..c11db270 100644 --- a/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx +++ b/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx @@ -17,16 +17,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { toast } from 'sonner'; @@ -323,26 +314,14 @@ export function WithdrawalDetail({ withdrawalId, mode }: WithdrawalDetailProps) {/* ===== 삭제 확인 다이얼로그 ===== */} - - - - 출금 삭제 - - 이 출금 내역을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다. - - - - 취소 - - {isLoading ? '삭제중...' : '삭제'} - - - - + ); } diff --git a/src/components/accounting/WithdrawalManagement/index.tsx b/src/components/accounting/WithdrawalManagement/index.tsx index 03f18c78..10486617 100644 --- a/src/components/accounting/WithdrawalManagement/index.tsx +++ b/src/components/accounting/WithdrawalManagement/index.tsx @@ -17,6 +17,7 @@ import { useRouter } from 'next/navigation'; import { Banknote, Pencil, + Plus, Save, Trash2, RefreshCw, @@ -296,6 +297,14 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra }, filterTitle: '출금 필터', + // 헤더 액션 (등록 버튼) + headerActions: () => ( + + ), + // 커스텀 필터 함수 customFilterFn: (items) => { return items.filter((item) => { diff --git a/src/components/approval/ApprovalBox/index.tsx b/src/components/approval/ApprovalBox/index.tsx index f2f39d6e..aea6f28f 100644 --- a/src/components/approval/ApprovalBox/index.tsx +++ b/src/components/approval/ApprovalBox/index.tsx @@ -43,6 +43,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { UniversalListPage, type UniversalListConfig, @@ -690,20 +691,15 @@ export function ApprovalBox() { renderDialogs: () => ( <> {/* 승인 확인 다이얼로그 */} - - - - 결재 승인 - - 정말 {pendingSelectedItems.size}건을 승인하시겠습니까? - - - - 취소 - 승인 - - - + {/* 반려 확인 다이얼로그 */} diff --git a/src/components/approval/DocumentDetail/ExpenseEstimateDocument.tsx b/src/components/approval/DocumentDetail/ExpenseEstimateDocument.tsx index d7e34128..998505d2 100644 --- a/src/components/approval/DocumentDetail/ExpenseEstimateDocument.tsx +++ b/src/components/approval/DocumentDetail/ExpenseEstimateDocument.tsx @@ -1,8 +1,16 @@ 'use client'; +/** + * 지출 예상 내역서 문서 컴포넌트 + * + * 공통 컴포넌트 사용: + * - DocumentHeader: centered 레이아웃 + customApproval (ApprovalLineBox) + */ + import { Fragment } from 'react'; import { ApprovalLineBox } from './ApprovalLineBox'; import type { ExpenseEstimateDocumentData } from './types'; +import { DocumentHeader } from '@/components/document-system'; interface ExpenseEstimateDocumentProps { data: ExpenseEstimateDocumentData; @@ -34,18 +42,13 @@ export function ExpenseEstimateDocument({ data }: ExpenseEstimateDocumentProps) return (
- {/* 문서 헤더 */} -
-
-

지출 예상 내역서

-

- 문서번호: {data.documentNo} | 작성일자: {data.createdAt} -

-
-
- -
-
+ {/* 문서 헤더 (공통 컴포넌트) */} + } + /> {/* 문서 내용 */}
diff --git a/src/components/approval/DocumentDetail/ExpenseReportDocument.tsx b/src/components/approval/DocumentDetail/ExpenseReportDocument.tsx index 22c48db2..fd20d5d7 100644 --- a/src/components/approval/DocumentDetail/ExpenseReportDocument.tsx +++ b/src/components/approval/DocumentDetail/ExpenseReportDocument.tsx @@ -1,7 +1,15 @@ 'use client'; +/** + * 지출결의서 문서 컴포넌트 + * + * 공통 컴포넌트 사용: + * - DocumentHeader: centered 레이아웃 + customApproval (ApprovalLineBox) + */ + import { ApprovalLineBox } from './ApprovalLineBox'; import type { ExpenseReportDocumentData } from './types'; +import { DocumentHeader } from '@/components/document-system'; interface ExpenseReportDocumentProps { data: ExpenseReportDocumentData; @@ -14,18 +22,13 @@ export function ExpenseReportDocument({ data }: ExpenseReportDocumentProps) { return (
- {/* 문서 헤더 */} -
-
-

지출결의서

-

- 문서번호: {data.documentNo} | 작성일자: {data.createdAt} -

-
-
- -
-
+ {/* 문서 헤더 (공통 컴포넌트) */} + } + /> {/* 문서 내용 */}
diff --git a/src/components/approval/DocumentDetail/ProposalDocument.tsx b/src/components/approval/DocumentDetail/ProposalDocument.tsx index cb068a13..864c9fe5 100644 --- a/src/components/approval/DocumentDetail/ProposalDocument.tsx +++ b/src/components/approval/DocumentDetail/ProposalDocument.tsx @@ -1,7 +1,15 @@ 'use client'; +/** + * 품의서 문서 컴포넌트 + * + * 공통 컴포넌트 사용: + * - DocumentHeader: centered 레이아웃 + customApproval (ApprovalLineBox) + */ + import { ApprovalLineBox } from './ApprovalLineBox'; import type { ProposalDocumentData } from './types'; +import { DocumentHeader } from '@/components/document-system'; interface ProposalDocumentProps { data: ProposalDocumentData; @@ -14,18 +22,13 @@ export function ProposalDocument({ data }: ProposalDocumentProps) { return (
- {/* 문서 헤더 */} -
-
-

품의서

-

- 문서번호: {data.documentNo} | 작성일자: {data.createdAt} -

-
-
- -
-
+ {/* 문서 헤더 (공통 컴포넌트) */} + } + /> {/* 문서 내용 */}
diff --git a/src/components/approval/ReferenceBox/index.tsx b/src/components/approval/ReferenceBox/index.tsx index 61fcc2fc..bfaf7ae8 100644 --- a/src/components/approval/ReferenceBox/index.tsx +++ b/src/components/approval/ReferenceBox/index.tsx @@ -25,16 +25,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { UniversalListPage, type UniversalListConfig, @@ -595,40 +586,22 @@ export function ReferenceBox() { renderDialogs: () => ( <> {/* 열람 처리 확인 다이얼로그 */} - - - - 열람 처리 - - 정말 {selectedItems.size}건을 열람 처리하시겠습니까? - - - - 취소 - - 확인 - - - - + {/* 미열람 처리 확인 다이얼로그 */} - - - - 미열람 처리 - - 정말 {selectedItems.size}건을 미열람 처리하시겠습니까? - - - - 취소 - - 확인 - - - - + {/* 문서 상세 모달 */} {selectedDocument && ( diff --git a/src/components/board/BoardDetail/index.tsx b/src/components/board/BoardDetail/index.tsx index 7a320ad7..36586aed 100644 --- a/src/components/board/BoardDetail/index.tsx +++ b/src/components/board/BoardDetail/index.tsx @@ -28,16 +28,7 @@ import { CardContent, CardHeader, } from '@/components/ui/card'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { toast } from 'sonner'; import { CommentSection } from '../CommentSection'; import { deletePost } from '../actions'; @@ -226,26 +217,14 @@ export function BoardDetail({ post, comments: initialComments, currentUserId }: )} {/* 삭제 확인 다이얼로그 */} - - - - 게시글 삭제 - - 정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. - - - - 취소 - - {isDeleting ? '삭제 중...' : '삭제'} - - - - + ); } diff --git a/src/components/board/BoardList/index.tsx b/src/components/board/BoardList/index.tsx index c5c77e0b..1b286985 100644 --- a/src/components/board/BoardList/index.tsx +++ b/src/components/board/BoardList/index.tsx @@ -17,16 +17,7 @@ import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { TableRow, TableCell } from '@/components/ui/table'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { UniversalListPage, type UniversalListConfig, @@ -382,23 +373,13 @@ export function BoardList() { }, renderDialogs: () => ( - - - - 게시글 삭제 - 정말 삭제하시겠습니까? - - - 취소 - - 삭제 - - - - + ), }), [ diff --git a/src/components/board/BoardManagement/BoardDetailClientV2.tsx b/src/components/board/BoardManagement/BoardDetailClientV2.tsx index 58840cd9..4924f808 100644 --- a/src/components/board/BoardManagement/BoardDetailClientV2.tsx +++ b/src/components/board/BoardManagement/BoardDetailClientV2.tsx @@ -18,16 +18,7 @@ import type { Board, BoardFormData } from './types'; import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; import { ErrorCard } from '@/components/ui/error-card'; import { Button } from '@/components/ui/button'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { toast } from 'sonner'; type DetailMode = 'view' | 'edit' | 'create'; @@ -281,37 +272,22 @@ export function BoardDetailClientV2({ boardId, initialMode }: BoardDetailClientV onDelete={handleDelete} /> - - - - 게시판 삭제 - - "{boardData.boardName}" 게시판을 삭제하시겠습니까? -
- - 삭제된 게시판 정보는 복구할 수 없습니다. - -
-
- - 취소 - - {isDeleting ? ( - <> - - 삭제 중... - - ) : ( - '삭제' - )} - - -
-
+ + "{boardData.boardName}" 게시판을 삭제하시겠습니까? +
+ + 삭제된 게시판 정보는 복구할 수 없습니다. + + + } + loading={isDeleting} + /> ); } diff --git a/src/components/board/CommentSection/CommentItem.tsx b/src/components/board/CommentSection/CommentItem.tsx index eea7d059..2d1b6c64 100644 --- a/src/components/board/CommentSection/CommentItem.tsx +++ b/src/components/board/CommentSection/CommentItem.tsx @@ -15,16 +15,7 @@ import { format } from 'date-fns'; import { User, Pencil, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import type { Comment } from '../types'; interface CommentItemProps { @@ -156,25 +147,13 @@ export function CommentItem({
{/* 삭제 확인 다이얼로그 */} - - - - 댓글 삭제 - - 정말 삭제하시겠습니까? - - - - 취소 - - 삭제 - - - - +
); } diff --git a/src/components/business/construction/category-management/index.tsx b/src/components/business/construction/category-management/index.tsx index 888c7938..60fcff05 100644 --- a/src/components/business/construction/category-management/index.tsx +++ b/src/components/business/construction/category-management/index.tsx @@ -9,16 +9,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent } from '@/components/ui/card'; import { CategoryDialog } from './CategoryDialog'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { toast } from 'sonner'; import type { Category } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; @@ -340,37 +331,27 @@ export function CategoryManagement() { /> {/* 삭제 확인 다이얼로그 */} - - - - 카테고리 삭제 - - "{categoryToDelete?.name}" 카테고리를 삭제하시겠습니까? - {categoryToDelete?.isDefault && ( - <> -
- - 기본 카테고리는 삭제가 불가합니다. - - - )} -
-
- - 취소 - - {isSubmitting ? ( - - ) : null} - 삭제 - - -
-
+ + "{categoryToDelete?.name}" 카테고리를 삭제하시겠습니까? + {categoryToDelete?.isDefault && ( + <> +
+ + 기본 카테고리는 삭제가 불가합니다. + + + )} + + } + loading={isSubmitting} + disabled={categoryToDelete?.isDefault} + /> ); } diff --git a/src/components/business/construction/estimates/EstimateDetailForm.tsx b/src/components/business/construction/estimates/EstimateDetailForm.tsx index 086a72ad..2284a569 100644 --- a/src/components/business/construction/estimates/EstimateDetailForm.tsx +++ b/src/components/business/construction/estimates/EstimateDetailForm.tsx @@ -7,16 +7,7 @@ import { getExpenseItemOptions, updateEstimate, type ExpenseItemOption } from '. import { createBiddingFromEstimate } from '../bidding/actions'; import { useAuth } from '@/contexts/AuthContext'; import { Button } from '@/components/ui/button'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { ConfirmDialog, DeleteConfirmDialog, SaveConfirmDialog } from '@/components/ui/confirm-dialog'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { estimateConfig } from './estimateConfig'; import { toast } from 'sonner'; @@ -724,77 +715,48 @@ export default function EstimateDetailForm({ /> {/* 삭제 확인 다이얼로그 */} - - - - 견적 삭제 - - 이 견적을 삭제하시겠습니까? -
- 삭제된 견적은 복구할 수 없습니다. -
-
- - 취소 - - {isLoading && } - 삭제 - - -
-
+ + 이 견적을 삭제하시겠습니까? +
+ 삭제된 견적은 복구할 수 없습니다. + + } + loading={isLoading} + /> {/* 저장 확인 다이얼로그 */} - - - - 수정 확인 - - 견적 정보를 수정하시겠습니까? - - - - 취소 - - {isLoading && } - 확인 - - - - + {/* 입찰 등록 확인 다이얼로그 */} - - - - 입찰 등록 - - 이 견적을 입찰로 등록하시겠습니까? -
- 견적 정보가 입찰 관리로 전환됩니다. -
-
- - 취소 - - {isLoading && } - 등록 - - -
-
+ + 이 견적을 입찰로 등록하시겠습니까? +
+ 견적 정보가 입찰 관리로 전환됩니다. + + } + variant="success" + confirmText="등록" + loading={isLoading} + /> ); } diff --git a/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx b/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx index af210b6b..6c423fe5 100644 --- a/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx +++ b/src/components/business/construction/handover-report/modals/HandoverReportDocumentModal.tsx @@ -1,6 +1,10 @@ 'use client'; -import { DocumentViewer } from '@/components/document-system'; +import { + DocumentViewer, + DocumentHeader, + ConstructionApprovalTable, +} from '@/components/document-system'; import { toast } from 'sonner'; import { useRouter } from 'next/navigation'; import type { HandoverReportDetail } from '../types'; @@ -64,43 +68,23 @@ export function HandoverReportDocumentModal({ onDelete={handleDelete} >
- {/* 상단: 제목 + 결재란 */} -
- {/* 좌측: 제목 및 문서정보 */} -
-

인수인계보고서

-
- 문서번호: {report.reportNumber} | 작성일자: {formatDate(report.createdAt)} -
-
- - {/* 우측: 결재란 */} -
- - - - - - - - - - - - - - - - - - - - - -
-
-
작성승인승인승인
홍길동이름이름이름
부서명부서명부서명부서명
- + {/* 상단: 제목 + 결재란 (공통 컴포넌트) */} + + } + /> {/* 통합 테이블 - 기획서 구조 100% 반영 */} diff --git a/src/components/business/construction/issue-management/IssueManagementListClient.tsx b/src/components/business/construction/issue-management/IssueManagementListClient.tsx index 6973106a..5bd62c0d 100644 --- a/src/components/business/construction/issue-management/IssueManagementListClient.tsx +++ b/src/components/business/construction/issue-management/IssueManagementListClient.tsx @@ -18,16 +18,7 @@ import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; import { MobileCard } from '@/components/organisms/MobileCard'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { toast } from 'sonner'; import { UniversalListPage, @@ -511,27 +502,21 @@ export default function IssueManagementListClient({ {/* 철회 확인 다이얼로그 */} - - - - 이슈 철회 - - 선택한 {itemsToWithdraw.size}건의 이슈를 철회하시겠습니까? -
- 철회된 이슈는 복구할 수 없습니다. -
-
- - 취소 - - 철회 - - -
-
+ + 선택한 {itemsToWithdraw.size}건의 이슈를 철회하시겠습니까? +
+ 철회된 이슈는 복구할 수 없습니다. + + } + confirmText="철회" + /> ); } diff --git a/src/components/business/construction/item-management/ItemManagementClient.tsx b/src/components/business/construction/item-management/ItemManagementClient.tsx index 18762159..40a4708f 100644 --- a/src/components/business/construction/item-management/ItemManagementClient.tsx +++ b/src/components/business/construction/item-management/ItemManagementClient.tsx @@ -11,16 +11,7 @@ import { Badge } from '@/components/ui/badge'; import { UniversalListPage, type UniversalListConfig, type TableColumn, type FilterFieldConfig, type FilterValues } from '@/components/templates/UniversalListPage'; import { MobileCard } from '@/components/organisms/MobileCard'; import { toast } from 'sonner'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import type { Item, ItemStats, ItemType, Specification, OrderType, ItemStatus } from './types'; import { ITEM_TYPE_OPTIONS, @@ -578,36 +569,22 @@ export default function ItemManagementClient({ renderDialogs: () => ( <> {/* 단일 삭제 다이얼로그 */} - - - - 품목 삭제 - - 선택한 품목을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. - - - - 취소 - 삭제 - - - + {/* 일괄 삭제 다이얼로그 */} - - - - 품목 일괄 삭제 - - 선택한 {selectedItems.size}개 품목을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. - - - - 취소 - 삭제 - - - + ), }; diff --git a/src/components/business/construction/labor-management/LaborDetailClient.tsx b/src/components/business/construction/labor-management/LaborDetailClient.tsx index d92d8eb6..5c36eab8 100644 --- a/src/components/business/construction/labor-management/LaborDetailClient.tsx +++ b/src/components/business/construction/labor-management/LaborDetailClient.tsx @@ -13,16 +13,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; @@ -452,20 +443,13 @@ export default function LaborDetailClient({ {/* 삭제 확인 다이얼로그 */} - - - - 노임 삭제 - - 이 노임을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다. - - - - 취소 - 삭제 - - - + ); } \ No newline at end of file diff --git a/src/components/business/construction/management/ConstructionDetailClient.tsx b/src/components/business/construction/management/ConstructionDetailClient.tsx index a877c450..68310e44 100644 --- a/src/components/business/construction/management/ConstructionDetailClient.tsx +++ b/src/components/business/construction/management/ConstructionDetailClient.tsx @@ -39,16 +39,7 @@ import { CONSTRUCTION_MANAGEMENT_STATUS_LABELS, CONSTRUCTION_MANAGEMENT_STATUS_STYLES, } from './types'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; interface ConstructionDetailClientProps { id: string; @@ -726,20 +717,15 @@ export default function ConstructionDetailClient({ id, mode }: ConstructionDetai )} {/* 시공 완료 확인 다이얼로그 (특수 기능) */} - - - - 시공 완료 - - 시공을 완료하시겠습니까? 완료 후에는 상태를 변경할 수 없습니다. - - - - 취소 - 완료 - - - + ); } \ No newline at end of file diff --git a/src/components/business/construction/order-management/dialogs/OrderDialogs.tsx b/src/components/business/construction/order-management/dialogs/OrderDialogs.tsx index efeb5797..29084aaf 100644 --- a/src/components/business/construction/order-management/dialogs/OrderDialogs.tsx +++ b/src/components/business/construction/order-management/dialogs/OrderDialogs.tsx @@ -1,15 +1,9 @@ 'use client'; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; + SaveConfirmDialog, + DeleteConfirmDialog, +} from '@/components/ui/confirm-dialog'; interface OrderDialogsProps { // 저장 다이얼로그 @@ -43,66 +37,29 @@ export function OrderDialogs({ return ( <> {/* 저장 확인 다이얼로그 */} - - - - 저장 확인 - 변경사항을 저장하시겠습니까? - - - 취소 - - 저장 - - - - + {/* 삭제 확인 다이얼로그 */} - - - - 발주 삭제 - - 이 발주를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. - - - - 취소 - - 삭제 - - - - + {/* 카테고리 삭제 확인 다이얼로그 */} - onCategoryDeleteDialogChange(null)} - > - - - 발주 상세 삭제 - - 이 발주 상세를 삭제하시겠습니까? 해당 카테고리의 모든 품목이 삭제됩니다. - - - - 취소 - - 삭제 - - - - + description="이 발주 상세를 삭제하시겠습니까? 해당 카테고리의 모든 품목이 삭제됩니다." + onConfirm={onConfirmDeleteCategory} + /> ); -} \ No newline at end of file +} diff --git a/src/components/business/construction/order-management/modals/OrderDocumentModal.tsx b/src/components/business/construction/order-management/modals/OrderDocumentModal.tsx index 7290bbf7..000175b1 100644 --- a/src/components/business/construction/order-management/modals/OrderDocumentModal.tsx +++ b/src/components/business/construction/order-management/modals/OrderDocumentModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { DocumentViewer } from '@/components/document-system'; +import { DocumentViewer, DocumentHeader } from '@/components/document-system'; import { toast } from 'sonner'; import { useRouter } from 'next/navigation'; import type { OrderDetail, OrderDetailItem } from '../types'; @@ -73,13 +73,13 @@ export function OrderDocumentModal({ onDelete={handleDelete} >
- {/* 상단: 제목 */} -
-

발주서

-
- 문서번호: {order.orderNumber} | 작성일자: {formatDate(order.orderDate)} -
-
+ {/* 상단: 제목 (공통 컴포넌트) */} + {/* 기본 정보 테이블 */}
diff --git a/src/components/business/construction/pricing-management/PricingDetailClient.tsx b/src/components/business/construction/pricing-management/PricingDetailClient.tsx index 605b7b7e..22b031b4 100644 --- a/src/components/business/construction/pricing-management/PricingDetailClient.tsx +++ b/src/components/business/construction/pricing-management/PricingDetailClient.tsx @@ -19,16 +19,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { toast } from 'sonner'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import type { Pricing, PricingStatus } from './types'; import { PRICING_STATUS_LABELS } from './types'; import { @@ -427,20 +418,13 @@ export default function PricingDetailClient({ id, mode }: PricingDetailClientPro {/* 삭제 확인 다이얼로그 */} - - - - 단가 삭제 - - 이 단가를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. - - - - 취소 - 삭제 - - - + ); } diff --git a/src/components/document-system/components/ApprovalLine.tsx b/src/components/document-system/components/ApprovalLine.tsx index a66e8fcf..a3552582 100644 --- a/src/components/document-system/components/ApprovalLine.tsx +++ b/src/components/document-system/components/ApprovalLine.tsx @@ -23,8 +23,8 @@ export interface ApprovalPerson { } export interface ApprovalLineProps { - /** 결재란 유형: 3열(작성/승인) 또는 4열(작성/검토/승인) */ - type?: '3col' | '4col'; + /** 결재란 유형: 2열(담당/부서장), 3열(작성/승인), 4열(작성/검토/승인) */ + type?: '2col' | '3col' | '4col'; /** 외부 송부 시 숨김 */ visible?: boolean; /** 문서 모드: internal(내부), external(외부송부) */ @@ -43,6 +43,12 @@ export interface ApprovalLineProps { reviewer?: string; approver?: string; }; + /** 컬럼 헤더 라벨 커스텀 (기본값: 작성/검토/승인) */ + columnLabels?: { + writer?: string; + reviewer?: string; + approver?: string; + }; /** 추가 className */ className?: string; } @@ -60,6 +66,7 @@ export function ApprovalLine({ reviewer: '생산', approver: '품질', }, + columnLabels, className, }: ApprovalLineProps) { // 외부 송부 모드이거나 visible이 false면 렌더링 안함 @@ -68,6 +75,14 @@ export function ApprovalLine({ } const is4Col = type === '4col'; + const is2Col = type === '2col'; + + // 기본 컬럼 라벨 + const labels = { + writer: columnLabels?.writer ?? (is2Col ? '담당' : '작성'), + reviewer: columnLabels?.reviewer ?? '검토', + approver: columnLabels?.approver ?? (is2Col ? '부서장' : '승인'), + }; return (
@@ -84,15 +99,15 @@ export function ApprovalLine({ {is4Col && ( )} diff --git a/src/components/document-system/components/ConstructionApprovalTable.tsx b/src/components/document-system/components/ConstructionApprovalTable.tsx new file mode 100644 index 00000000..e88d2ca1 --- /dev/null +++ b/src/components/document-system/components/ConstructionApprovalTable.tsx @@ -0,0 +1,115 @@ +'use client'; + +/** + * 건설 문서용 결재란 컴포넌트 + * + * @example + * // 4열 결재란 (작성 + 승인 3개) + * + */ + +import { cn } from '@/lib/utils'; + +export interface ConstructionApprover { + name?: string; + department?: string; +} + +export interface ConstructionApprovalTableProps { + /** 결재자 정보 */ + approvers?: { + writer?: ConstructionApprover; + approver1?: ConstructionApprover; + approver2?: ConstructionApprover; + approver3?: ConstructionApprover; + }; + /** 컬럼 헤더 라벨 커스텀 */ + columnLabels?: { + writer?: string; + approver1?: string; + approver2?: string; + approver3?: string; + }; + /** 추가 className */ + className?: string; +} + +export function ConstructionApprovalTable({ + approvers = {}, + columnLabels = {}, + className, +}: ConstructionApprovalTableProps) { + const labels = { + writer: columnLabels.writer ?? '작성', + approver1: columnLabels.approver1 ?? '승인', + approver2: columnLabels.approver2 ?? '승인', + approver3: columnLabels.approver3 ?? '승인', + }; + + return ( +
- 작성 + {labels.writer} - 검토 + {labels.reviewer} - 승인 + {labels.approver}
+ + {/* 헤더 행 */} + + + + + + + + + {/* 이름 행 */} + + + + + + + + {/* 부서 행 */} + + + + + + + +
+
+
+ {labels.writer} + + {labels.approver1} + + {labels.approver2} + + {labels.approver3} +
+ {approvers.writer?.name || ''} + + {approvers.approver1?.name || ''} + + {approvers.approver2?.name || ''} + + {approvers.approver3?.name || ''} +
+ {approvers.writer?.department || '부서명'} + + {approvers.approver1?.department || '부서명'} + + {approvers.approver2?.department || '부서명'} + + {approvers.approver3?.department || '부서명'} +
+ ); +} diff --git a/src/components/document-system/components/DocumentHeader.tsx b/src/components/document-system/components/DocumentHeader.tsx index 85067419..ce717df5 100644 --- a/src/components/document-system/components/DocumentHeader.tsx +++ b/src/components/document-system/components/DocumentHeader.tsx @@ -28,6 +28,15 @@ * * // 외부 송부 (결재선 숨김) * + * + * // 품질검사 레이아웃 (2줄 제목) + * } + * /> */ import { ReactNode } from 'react'; @@ -56,10 +65,12 @@ export interface DocumentHeaderProps { topInfo?: ReactNode; /** 결재란 설정 (null이면 숨김) */ approval?: Omit | null; + /** 커스텀 결재란 컴포넌트 (approval 대신 사용) */ + customApproval?: ReactNode; /** 문서 모드: internal(내부), external(외부송부) */ mode?: 'internal' | 'external'; /** 레이아웃 유형 */ - layout?: 'default' | 'centered' | 'simple'; + layout?: 'default' | 'centered' | 'simple' | 'quality' | 'construction' | 'quote'; /** 추가 className */ className?: string; } @@ -71,12 +82,13 @@ export function DocumentHeader({ logo, topInfo, approval, + customApproval, mode = 'internal', layout = 'default', className, }: DocumentHeaderProps) { const isExternal = mode === 'external'; - const showApproval = approval !== null && !isExternal; + const showApproval = (approval !== null || customApproval) && !isExternal; // 간단한 레이아웃 (제목만) if (layout === 'simple') { @@ -93,6 +105,79 @@ export function DocumentHeader({ ); } + // 품질검사 레이아웃 (로고 + 2줄 제목 + 결재란, 테두리 없음) + if (layout === 'quality') { + return ( +
+ {/* 좌측: 로고 영역 */} + {logo && ( +
+ {logo.imageUrl ? ( + {logo.text} + ) : ( +
{logo.text}
+ )} + {logo.subtext && ( +
{logo.subtext}
+ )} +
+ )} + + {/* 중앙: 2줄 제목 */} +
+
{title}
+ {subtitle && ( +
{subtitle}
+ )} +
+ + {/* 우측: 결재란 */} + {showApproval && ( + customApproval || (approval && ) + )} +
+ ); + } + + // 건설 문서 레이아웃 (좌측 정렬 제목 + 결재란) + if (layout === 'construction') { + return ( +
+ {/* 좌측: 제목 및 문서정보 */} +
+

{title}

+ {(documentCode || subtitle) && ( +
+ {documentCode && 문서번호: {documentCode}} + {documentCode && subtitle && | } + {subtitle && {subtitle}} +
+ )} +
+ + {/* 우측: 결재란 */} + {showApproval && ( + customApproval || (approval && ) + )} +
+ ); + } + + // 견적/발주 문서 레이아웃 (중앙 제목 + 우측 로트번호/결재란) + if (layout === 'quote') { + return ( +
+ {/* 중앙: 제목 */} +
+

{title}

+
+ + {/* 우측: 로트번호 + 결재란 (customApproval로 전달) */} + {showApproval && customApproval} +
+ ); + } + // 중앙 정렬 레이아웃 (견적서 스타일) if (layout === 'centered') { return ( @@ -107,8 +192,10 @@ export function DocumentHeader({ )} - {showApproval && approval && ( - + {showApproval && ( +
+ {customApproval || (approval && )} +
)} ); @@ -150,8 +237,8 @@ export function DocumentHeader({ {topInfo} )} - {showApproval && approval && ( - + {showApproval && ( + customApproval || (approval && ) )} )} diff --git a/src/components/document-system/components/LotApprovalTable.tsx b/src/components/document-system/components/LotApprovalTable.tsx new file mode 100644 index 00000000..f92b7114 --- /dev/null +++ b/src/components/document-system/components/LotApprovalTable.tsx @@ -0,0 +1,121 @@ +'use client'; + +/** + * 로트번호 + 결재란 통합 컴포넌트 + * + * @example + * + */ + +import { cn } from '@/lib/utils'; + +export interface LotApprover { + name?: string; + department?: string; +} + +export interface LotApprovalTableProps { + /** 로트번호 */ + lotNumber: string; + /** 로트번호 라벨 (기본값: '로트번호') */ + lotLabel?: string; + /** 결재자 정보 */ + approvers?: { + writer?: LotApprover; + reviewer?: LotApprover; + approver?: LotApprover; + }; + /** 컬럼 헤더 라벨 커스텀 */ + columnLabels?: { + writer?: string; + reviewer?: string; + approver?: string; + }; + /** 추가 className */ + className?: string; +} + +export function LotApprovalTable({ + lotNumber, + lotLabel = '로트번호', + approvers = {}, + columnLabels = {}, + className, +}: LotApprovalTableProps) { + const labels = { + writer: columnLabels.writer ?? '작성', + reviewer: columnLabels.reviewer ?? '검토', + approver: columnLabels.approver ?? '승인', + }; + + return ( +
+ {/* 로트번호 행 */} +
+
+ {lotLabel} +
+
+ {lotNumber} +
+
+ + {/* 결재란 */} +
+ {/* 결재 세로 셀 */} +
+
+
+ + {/* 결재 내용 */} +
+ {/* 헤더 행 */} +
+
+ {labels.writer} +
+
+ {labels.reviewer} +
+
+ {labels.approver} +
+
+ + {/* 서명 행 */} +
+
+ {approvers.writer?.name || ''} +
+
+ {approvers.reviewer?.name || ''} +
+
+ {approvers.approver?.name || ''} +
+
+ + {/* 부서 행 */} +
+
+ {approvers.writer?.department || ''} +
+
+ {approvers.reviewer?.department || ''} +
+
+ {approvers.approver?.department || ''} +
+
+
+
+
+ ); +} diff --git a/src/components/document-system/components/QualityApprovalTable.tsx b/src/components/document-system/components/QualityApprovalTable.tsx new file mode 100644 index 00000000..881e8c46 --- /dev/null +++ b/src/components/document-system/components/QualityApprovalTable.tsx @@ -0,0 +1,122 @@ +'use client'; + +/** + * 품질검사 문서용 결재란 컴포넌트 + * + * @example + * // 2열 타입 (수입검사 성적서) + * + * + * // 4열 타입 (중간검사 성적서) + * + */ + +import { cn } from '@/lib/utils'; + +export interface QualityApprovers { + writer?: string; + reviewer?: string; + approver?: string; +} + +export interface QualityDepartments { + writer?: string; + reviewer?: string; + approver?: string; +} + +export interface QualityApprovalTableProps { + /** 결재란 타입: 2col(담당/부서장), 4col(결재/작성/검토/승인) */ + type?: '2col' | '4col'; + /** 결재자 정보 */ + approvers?: QualityApprovers; + /** 부서 정보 (4col 전용) */ + departments?: QualityDepartments; + /** 보고일자 (2col 전용) */ + reportDate?: string; + /** 추가 className */ + className?: string; +} + +export function QualityApprovalTable({ + type = '4col', + approvers = {}, + departments = { + writer: '판매/전진', + reviewer: '생산', + approver: '품질', + }, + reportDate, + className, +}: QualityApprovalTableProps) { + // 2열 타입 (수입검사 성적서 - 담당/부서장) + if (type === '2col') { + return ( +
+ + + + + + + + + + + +
담당부서장
결재{approvers.writer || ''}
+ {reportDate && ( +
접고일자: {reportDate}
+ )} +
+ ); + } + + // 4열 타입 (중간검사 성적서 - 결재/작성/검토/승인 + 부서) + return ( + + + + + + + + + + + + + + + + + + + +
+
+ +
+
작성검토승인
+ {approvers.writer || ''} + + {approvers.reviewer || ''} + + {approvers.approver || ''} +
+ {departments.writer || '판매/전진'} + + {departments.reviewer || '생산'} + + {departments.approver || '품질'} +
+ ); +} diff --git a/src/components/document-system/components/SignatureSection.tsx b/src/components/document-system/components/SignatureSection.tsx new file mode 100644 index 00000000..8512890e --- /dev/null +++ b/src/components/document-system/components/SignatureSection.tsx @@ -0,0 +1,106 @@ +'use client'; + +/** + * 서명/도장 영역 컴포넌트 + * + * @example + * // 기본 사용 (도장 영역 포함) + * + * + * // 커스텀 문구 + * + */ + +import { cn } from '@/lib/utils'; + +export interface SignatureSectionProps { + /** 상단 안내 문구 (기본값: '상기와 같이 견적합니다.') */ + label?: string; + /** 날짜 */ + date?: string; + /** 회사명 */ + companyName?: string; + /** 역할 라벨 (기본값: '공급자') */ + role?: string; + /** 도장 영역 표시 여부 */ + showStamp?: boolean; + /** 도장 내부 텍스트 */ + stampText?: string; + /** 도장 이미지 URL */ + stampImageUrl?: string; + /** 정렬 (기본값: 'right') */ + align?: 'left' | 'center' | 'right'; + /** 추가 className */ + className?: string; +} + +export function SignatureSection({ + label = '상기와 같이 견적합니다.', + date, + companyName, + role = '공급자', + showStamp = true, + stampText = '(인감\n날인)', + stampImageUrl, + align = 'right', + className, +}: SignatureSectionProps) { + const alignClass = { + left: 'text-left', + center: 'text-center', + right: 'text-right', + }[align]; + + return ( +
+
+ {/* 안내 문구 */} + {label && ( +
+ {label} +
+ )} + + {/* 날짜 + 회사명 + 도장 */} +
+
+ {date && ( +
{date}
+ )} + {companyName && ( +
+ {role}: {companyName} (인) +
+ )} +
+ + {/* 도장 영역 */} + {showStamp && ( +
+ {stampImageUrl ? ( + 도장 + ) : ( +
+ {stampText} +
+ )} +
+ )} +
+
+
+ ); +} diff --git a/src/components/document-system/components/index.ts b/src/components/document-system/components/index.ts index 963e33ea..7b42b731 100644 --- a/src/components/document-system/components/index.ts +++ b/src/components/document-system/components/index.ts @@ -3,9 +3,27 @@ export { ApprovalLine } from './ApprovalLine'; export { DocumentHeader } from './DocumentHeader'; export { SectionHeader } from './SectionHeader'; export { InfoTable } from './InfoTable'; +export { QualityApprovalTable } from './QualityApprovalTable'; +export { ConstructionApprovalTable } from './ConstructionApprovalTable'; +export { LotApprovalTable } from './LotApprovalTable'; +export { SignatureSection } from './SignatureSection'; // Types export type { ApprovalPerson, ApprovalLineProps } from './ApprovalLine'; export type { DocumentHeaderLogo, DocumentHeaderProps } from './DocumentHeader'; export type { SectionHeaderProps } from './SectionHeader'; export type { InfoTableCell, InfoTableProps } from './InfoTable'; +export type { + QualityApprovers, + QualityDepartments, + QualityApprovalTableProps, +} from './QualityApprovalTable'; +export type { + ConstructionApprover, + ConstructionApprovalTableProps, +} from './ConstructionApprovalTable'; +export type { + LotApprover, + LotApprovalTableProps, +} from './LotApprovalTable'; +export type { SignatureSectionProps } from './SignatureSection'; diff --git a/src/components/document-system/index.ts b/src/components/document-system/index.ts index 93ec067d..96923f04 100644 --- a/src/components/document-system/index.ts +++ b/src/components/document-system/index.ts @@ -7,6 +7,10 @@ export { DocumentHeader, SectionHeader, InfoTable, + QualityApprovalTable, + ConstructionApprovalTable, + LotApprovalTable, + SignatureSection, } from './components'; // Hooks @@ -25,6 +29,14 @@ export type { SectionHeaderProps, InfoTableCell, InfoTableProps, + QualityApprovers, + QualityDepartments, + QualityApprovalTableProps, + ConstructionApprover, + ConstructionApprovalTableProps, + LotApprover, + LotApprovalTableProps, + SignatureSectionProps, } from './components'; export type { diff --git a/src/components/hr/CardManagement/index.tsx b/src/components/hr/CardManagement/index.tsx index 8042d292..3bbe5d9b 100644 --- a/src/components/hr/CardManagement/index.tsx +++ b/src/components/hr/CardManagement/index.tsx @@ -7,16 +7,7 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { TableRow, TableCell } from '@/components/ui/table'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { UniversalListPage, type UniversalListConfig, @@ -379,29 +370,21 @@ export function CardManagement({ initialData }: CardManagementProps) { }, renderDialogs: () => ( - - - - 카드 삭제 - - "{cardToDelete?.cardName}" 카드를 삭제하시겠습니까? -
- - 삭제된 카드 정보는 복구할 수 없습니다. - -
-
- - 취소 - - 삭제 - - -
-
+ + "{cardToDelete?.cardName}" 카드를 삭제하시겠습니까? +
+ + 삭제된 카드 정보는 복구할 수 없습니다. + + + } + /> ), }), [ cards, diff --git a/src/components/hr/DepartmentManagement/index.tsx b/src/components/hr/DepartmentManagement/index.tsx index 64807414..55cf1fc6 100644 --- a/src/components/hr/DepartmentManagement/index.tsx +++ b/src/components/hr/DepartmentManagement/index.tsx @@ -8,16 +8,7 @@ import { DepartmentStats } from './DepartmentStats'; import { DepartmentToolbar } from './DepartmentToolbar'; import { DepartmentTree } from './DepartmentTree'; import { DepartmentDialog } from './DepartmentDialog'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import type { Department } from './types'; import { countAllDepartments, getAllDepartmentIds, findDepartmentById } from './types'; import { @@ -318,29 +309,23 @@ export function DepartmentManagement() { /> {/* 삭제 확인 다이얼로그 */} - - - - 부서 삭제 - - {isBulkDelete - ? `선택한 부서 ${selectedIds.size}개를 삭제하시겠습니까?` - : `"${departmentToDelete?.name}" 부서를 삭제하시겠습니까?` - } -
- - 삭제된 부서의 인원은 회사(기본) 인원으로 변경됩니다. - -
-
- - 취소 - - 삭제 - - -
-
+ + {isBulkDelete + ? `선택한 부서 ${selectedIds.size}개를 삭제하시겠습니까?` + : `"${departmentToDelete?.name}" 부서를 삭제하시겠습니까?` + } +
+ + 삭제된 부서의 인원은 회사(기본) 인원으로 변경됩니다. + + + } + onConfirm={confirmDelete} + /> ); } \ No newline at end of file diff --git a/src/components/hr/EmployeeManagement/index.tsx b/src/components/hr/EmployeeManagement/index.tsx index f1fedd53..4eb9ef1f 100644 --- a/src/components/hr/EmployeeManagement/index.tsx +++ b/src/components/hr/EmployeeManagement/index.tsx @@ -8,16 +8,7 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { TableRow, TableCell } from '@/components/ui/table'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { UniversalListPage, type UniversalListConfig, @@ -726,37 +717,22 @@ export function EmployeeManagement() { /> {/* 삭제 확인 다이얼로그 */} - - - - 사원 삭제 - - "{employeeToDelete?.name}" 사원을 삭제하시겠습니까? -
- - 삭제된 사원 정보는 복구할 수 없습니다. - -
-
- - 취소 - - {isDeleting ? ( - <> - - 삭제 중... - - ) : ( - '삭제' - )} - - -
-
+ + "{employeeToDelete?.name}" 사원을 삭제하시겠습니까? +
+ + 삭제된 사원 정보는 복구할 수 없습니다. + + + } + loading={isDeleting} + /> ), }), [ diff --git a/src/components/hr/VacationManagement/index.tsx b/src/components/hr/VacationManagement/index.tsx index f8597d1e..91cb6051 100644 --- a/src/components/hr/VacationManagement/index.tsx +++ b/src/components/hr/VacationManagement/index.tsx @@ -28,16 +28,7 @@ import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { TableRow, TableCell } from '@/components/ui/table'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { UniversalListPage, type UniversalListConfig, @@ -793,43 +784,26 @@ export function VacationManagement() { /> {/* 승인 확인 다이얼로그 */} - - - - 휴가 승인 - - 정말 {selectedItems.size}건을 승인하시겠습니까? - - - - 취소 - - 승인 - - - - + {/* 거절 확인 다이얼로그 */} - - - - 휴가 거절 - - 정말 {selectedItems.size}건을 거절하시겠습니까? - - - - 취소 - - 거절 - - - - + ), }), [ diff --git a/src/components/items/DynamicItemForm/components/DuplicateCodeDialog.tsx b/src/components/items/DynamicItemForm/components/DuplicateCodeDialog.tsx index c66df2d7..277d640a 100644 --- a/src/components/items/DynamicItemForm/components/DuplicateCodeDialog.tsx +++ b/src/components/items/DynamicItemForm/components/DuplicateCodeDialog.tsx @@ -1,15 +1,6 @@ 'use client'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; export interface DuplicateCodeDialogProps { open: boolean; @@ -27,27 +18,32 @@ export function DuplicateCodeDialog({ onCancel, onGoToEdit, }: DuplicateCodeDialogProps) { + const handleConfirm = () => { + onGoToEdit(); + }; + + const handleOpenChange = (isOpen: boolean) => { + onOpenChange(isOpen); + if (!isOpen) { + onCancel(); + } + }; + return ( - - - - 품목코드 중복 - - 입력하신 조건의 품목코드가 이미 존재합니다. - - 기존 품목을 수정하시겠습니까? - - - - - - 취소 - - - 중복 품목 수정하러 가기 - - - - + + 입력하신 조건의 품목코드가 이미 존재합니다. + + 기존 품목을 수정하시겠습니까? + + + } + confirmText="중복 품목 수정하러 가기" + /> ); } \ No newline at end of file diff --git a/src/components/items/ItemListClient.tsx b/src/components/items/ItemListClient.tsx index cad79c92..070ca0e5 100644 --- a/src/components/items/ItemListClient.tsx +++ b/src/components/items/ItemListClient.tsx @@ -16,16 +16,7 @@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { TableRow, TableCell } from '@/components/ui/table'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { Search, Plus, Edit, Trash2, Package } from 'lucide-react'; import { TableLoadingSpinner } from '@/components/ui/loading-spinner'; import { useItemList } from '@/hooks/useItemList'; @@ -503,27 +494,19 @@ export default function ItemListClient() { /> {/* 개별 삭제 확인 다이얼로그 */} - - - - 품목 삭제 - - 품목 "{itemToDelete?.code}"을(를) 삭제하시겠습니까? -
- 이 작업은 되돌릴 수 없습니다. -
-
- - 취소 - - 삭제 - - -
-
+ + 품목 "{itemToDelete?.code}"을(를) 삭제하시겠습니까? +
+ 이 작업은 되돌릴 수 없습니다. + + } + /> ); } \ No newline at end of file diff --git a/src/components/items/ItemMasterDataManagement/dialogs/TabManagementDialogs.tsx b/src/components/items/ItemMasterDataManagement/dialogs/TabManagementDialogs.tsx index 34b166a4..8cc237ab 100644 --- a/src/components/items/ItemMasterDataManagement/dialogs/TabManagementDialogs.tsx +++ b/src/components/items/ItemMasterDataManagement/dialogs/TabManagementDialogs.tsx @@ -1,7 +1,7 @@ 'use client'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; @@ -212,32 +212,22 @@ export function TabManagementDialogs({ {/* 탭 삭제 확인 다이얼로그 */} - - - - 탭 삭제 - - "{customTabs.find(t => t.id === deletingTabId)?.label}" 탭을 삭제하시겠습니까? -
- 이 작업은 되돌릴 수 없습니다. -
-
- - { - setIsDeleteTabDialogOpen(false); - setDeletingTabId(null); - }}> - 취소 - - - 삭제 - - -
-
+ { + setIsDeleteTabDialogOpen(open); + if (!open) setDeletingTabId(null); + }} + onConfirm={confirmDeleteTab} + title="탭 삭제" + description={ + <> + "{customTabs.find(t => t.id === deletingTabId)?.label}" 탭을 삭제하시겠습니까? +
+ 이 작업은 되돌릴 수 없습니다. + + } + /> {/* 탭 추가/수정 다이얼로그 */} { @@ -363,32 +353,22 @@ export function TabManagementDialogs({ {/* 속성 하위 탭 삭제 확인 다이얼로그 */} - - - - 속성 탭 삭제 - - "{attributeSubTabs.find(t => t.id === deletingAttributeTabId)?.label}" 탭을 삭제하시겠습니까? -
- 이 작업은 되돌릴 수 없습니다. -
-
- - { - setIsDeleteAttributeTabDialogOpen(false); - setDeletingAttributeTabId(null); - }}> - 취소 - - - 삭제 - - -
-
+ { + setIsDeleteAttributeTabDialogOpen(open); + if (!open) setDeletingAttributeTabId(null); + }} + onConfirm={confirmDeleteAttributeTab} + title="속성 탭 삭제" + description={ + <> + "{attributeSubTabs.find(t => t.id === deletingAttributeTabId)?.label}" 탭을 삭제하시겠습니까? +
+ 이 작업은 되돌릴 수 없습니다. + + } + /> {/* 속성 하위 탭 추가/수정 다이얼로그 */} { diff --git a/src/components/orders/documents/ContractDocument.tsx b/src/components/orders/documents/ContractDocument.tsx index 65f4fd8b..76814417 100644 --- a/src/components/orders/documents/ContractDocument.tsx +++ b/src/components/orders/documents/ContractDocument.tsx @@ -2,12 +2,14 @@ /** * 계약서 문서 컴포넌트 - * - 스크린샷 형식 + 지출결의서 디자인 스타일 - * - 제품 정보는 견적의 calculation_inputs에서 추출한 products로 표시 + * + * 공통 컴포넌트 사용: + * - DocumentHeader: simple 레이아웃 (결재란 없음) */ import { formatAmount } from "@/utils/formatAmount"; import { OrderItem } from "../actions"; +import { DocumentHeader } from "@/components/document-system"; // 제품 정보 타입 interface ProductInfo { @@ -66,13 +68,14 @@ export function ContractDocument({ return (
- {/* 제목 */} -
-

계 약 서

-

- 수주번호: {orderNumber} | 계약일자: {orderDate} -

-
+ {/* 문서 헤더 (공통 컴포넌트) */} + {/* 제품 정보 (개소별) */}
diff --git a/src/components/orders/documents/TransactionDocument.tsx b/src/components/orders/documents/TransactionDocument.tsx index 88115de6..05b7aa46 100644 --- a/src/components/orders/documents/TransactionDocument.tsx +++ b/src/components/orders/documents/TransactionDocument.tsx @@ -2,11 +2,14 @@ /** * 거래명세서 문서 컴포넌트 - * - 스크린샷 형식 + 지출결의서 디자인 스타일 + * + * 공통 컴포넌트 사용: + * - DocumentHeader: simple 레이아웃 (결재란 없음) */ import { formatAmount } from "@/utils/formatAmount"; import { OrderItem } from "../actions"; +import { DocumentHeader } from "@/components/document-system"; interface TransactionDocumentProps { orderNumber: string; @@ -54,13 +57,14 @@ export function TransactionDocument({ return (
- {/* 제목 */} -
-

거 래 명 세 서

-

- 수주번호: {orderNumber} | 발행일: {orderDate} -

-
+ {/* 문서 헤더 (공통 컴포넌트) */} + {/* 공급자/공급받는자 정보 */}
diff --git a/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx b/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx index 71ede914..6e233af0 100644 --- a/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx +++ b/src/components/outbound/ShipmentManagement/ShipmentDetail.tsx @@ -35,16 +35,7 @@ import { DialogContent, DialogTitle, } from '@/components/ui/dialog'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { DialogDescription, DialogFooter, @@ -557,35 +548,20 @@ export function ShipmentDetail({ id }: ShipmentDetailProps) {
{/* 삭제 확인 다이얼로그 */} - - - - 출하 정보 삭제 - - 출하번호 {detail?.shipmentNo}을(를) 삭제하시겠습니까? -
- 이 작업은 되돌릴 수 없습니다. -
-
- - 취소 - - {isDeleting ? ( - <> - - 삭제 중... - - ) : ( - '삭제' - )} - - -
-
+ + 출하번호 {detail?.shipmentNo}을(를) 삭제하시겠습니까? +
+ 이 작업은 되돌릴 수 없습니다. + + } + loading={isDeleting} + /> {/* 상태 변경 다이얼로그 */} diff --git a/src/components/outbound/ShipmentManagement/documents/DeliveryConfirmation.tsx b/src/components/outbound/ShipmentManagement/documents/DeliveryConfirmation.tsx index 50808d26..bbc83a62 100644 --- a/src/components/outbound/ShipmentManagement/documents/DeliveryConfirmation.tsx +++ b/src/components/outbound/ShipmentManagement/documents/DeliveryConfirmation.tsx @@ -2,9 +2,13 @@ /** * 납품확인서 미리보기/인쇄 문서 + * + * 공통 컴포넌트 사용: + * - DocumentHeader: default 레이아웃 + 4col 결재란 */ import type { ShipmentDetail } from '../types'; +import { DocumentHeader } from '@/components/document-system'; interface DeliveryConfirmationProps { data: ShipmentDetail; @@ -13,44 +17,18 @@ interface DeliveryConfirmationProps { export function DeliveryConfirmation({ data }: DeliveryConfirmationProps) { return (
- {/* 헤더 */} -
-
-
KD
-
-
경동기업
-
-
-
납 품 확 인 서
- - - {/* 헤더: 결재 + 작성/검토/승인 */} - - - - - - - {/* 내용: 서명란 */} - - - - - - {/* 부서 */} - - - - - - -
-
- - -
-
작성검토승인
판매/
전산
출하품질
-
+ {/* 문서 헤더 (공통 컴포넌트) */} + {/* 출하 관리부서 */}
출하 관리부서
diff --git a/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx b/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx index 31250c9d..1fb3a069 100644 --- a/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx +++ b/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx @@ -2,9 +2,13 @@ /** * 출고증 미리보기/인쇄 문서 + * + * 공통 컴포넌트 사용: + * - DocumentHeader: default 레이아웃 + 4col 결재란 */ import type { ShipmentDetail } from '../types'; +import { DocumentHeader } from '@/components/document-system'; interface ShippingSlipProps { data: ShipmentDetail; @@ -13,49 +17,19 @@ interface ShippingSlipProps { export function ShippingSlip({ data }: ShippingSlipProps) { return (
- {/* 헤더 */} -
-
-
KD
-
-
경동기업
-
KYUNGDONG COMPANY
-
-
-
출 고 증
- - - {/* 헤더: 결재 + 작성/검토/승인 */} - - - - - - - {/* 내용: 담당자 정보 */} - - - - - - {/* 부서 */} - - - - - - -
-
- - -
-
작성검토승인
-
판매1팀 임
-
판매
-
12-20
-
판매/전진출하생산관리
-
+ {/* 문서 헤더 (공통 컴포넌트) */} + {/* 출하 관리 */}
출하 관리
diff --git a/src/components/outbound/ShipmentManagement/documents/TransactionStatement.tsx b/src/components/outbound/ShipmentManagement/documents/TransactionStatement.tsx index 7fce1c9b..29e1d175 100644 --- a/src/components/outbound/ShipmentManagement/documents/TransactionStatement.tsx +++ b/src/components/outbound/ShipmentManagement/documents/TransactionStatement.tsx @@ -2,9 +2,13 @@ /** * 거래명세서 미리보기/인쇄 문서 + * + * 공통 컴포넌트 사용: + * - DocumentHeader: simple 레이아웃 (결재란 없음) */ import type { ShipmentDetail } from '../types'; +import { DocumentHeader } from '@/components/document-system'; interface TransactionStatementProps { data: ShipmentDetail; @@ -19,13 +23,14 @@ export function TransactionStatement({ data }: TransactionStatementProps) { return (
- {/* 제목 */} -

- 거 래 명 세 서 -

-

- TRANSACTION STATEMENT -

+ {/* 문서 헤더 (공통 컴포넌트) */} + {/* 공급받는자 / 공급자 정보 */}
diff --git a/src/components/process-management/ProcessListClient.tsx b/src/components/process-management/ProcessListClient.tsx index edf72b14..1e0ec34b 100644 --- a/src/components/process-management/ProcessListClient.tsx +++ b/src/components/process-management/ProcessListClient.tsx @@ -26,16 +26,7 @@ import { } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import { toast } from 'sonner'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import type { Process } from '@/types/process'; import { getProcessList, deleteProcess, deleteProcesses, toggleProcessActive, getProcessStats } from './actions'; @@ -465,33 +456,13 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr {/* 삭제 확인 다이얼로그 */} - - - - 공정 삭제 - - 선택한 공정을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. - - - - 취소 - - {isLoading ? ( - <> - - 삭제 중... - - ) : ( - '삭제' - )} - - - - + ); } diff --git a/src/components/production/WorkerScreen/CompletionConfirmDialog.tsx b/src/components/production/WorkerScreen/CompletionConfirmDialog.tsx index 608753aa..0483a589 100644 --- a/src/components/production/WorkerScreen/CompletionConfirmDialog.tsx +++ b/src/components/production/WorkerScreen/CompletionConfirmDialog.tsx @@ -6,16 +6,7 @@ * "자재 투입이 필요합니다" 안내 후 확인 클릭 시 MaterialInputModal로 이동 */ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import type { WorkOrder } from '../ProductionDashboard/types'; interface CompletionConfirmDialogProps { @@ -36,49 +27,38 @@ export function CompletionConfirmDialog({ onConfirm(); // 부모에서 MaterialInputModal 열기 }; - const handleCancel = () => { - onOpenChange(false); - }; - if (!order) return null; return ( - - - - - 자재 투입이 필요합니다! - - -
-
-

- 작업지시:{' '} - {order.orderNo} -

-

- 공정:{' '} - - {order.processName} - -

-
-

- 자재 투입 없이 완료 처리하시겠습니까? -

-

- (LOT 추적이 불가능해집니다) -

-
-
-
- - 취소 - - 확인 - - -
-
+ 자재 투입이 필요합니다!} + description={ +
+
+

+ 작업지시:{' '} + {order.orderNo} +

+

+ 공정:{' '} + + {order.processName} + +

+
+

+ 자재 투입 없이 완료 처리하시겠습니까? +

+

+ (LOT 추적이 불가능해집니다) +

+
+ } + confirmText="확인" + /> ); } \ No newline at end of file diff --git a/src/components/quotes/LocationListPanel.tsx b/src/components/quotes/LocationListPanel.tsx index 8f888249..c76b6f7d 100644 --- a/src/components/quotes/LocationListPanel.tsx +++ b/src/components/quotes/LocationListPanel.tsx @@ -31,16 +31,7 @@ import { TableHeader, TableRow, } from "../ui/table"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "../ui/alert-dialog"; +import { DeleteConfirmDialog } from "../ui/confirm-dialog"; import type { LocationItem } from "./QuoteRegistrationV2"; import type { FinishedGoods } from "./actions"; @@ -518,30 +509,18 @@ export function LocationListPanel({ )} {/* 삭제 확인 다이얼로그 */} - setDeleteTarget(null)}> - - - 개소 삭제 - - 선택한 개소를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. - - - - 취소 - { - if (deleteTarget) { - onDeleteLocation(deleteTarget); - setDeleteTarget(null); - } - }} - className="bg-red-500 hover:bg-red-600" - > - 삭제 - - - - + setDeleteTarget(null)} + onConfirm={() => { + if (deleteTarget) { + onDeleteLocation(deleteTarget); + setDeleteTarget(null); + } + }} + title="개소 삭제" + description="선택한 개소를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." + />
); } \ No newline at end of file diff --git a/src/components/quotes/PurchaseOrderDocument.tsx b/src/components/quotes/PurchaseOrderDocument.tsx index 19cbef22..ed64dbe4 100644 --- a/src/components/quotes/PurchaseOrderDocument.tsx +++ b/src/components/quotes/PurchaseOrderDocument.tsx @@ -1,14 +1,13 @@ /** * 발주서 (Purchase Order Document) * - * - 로트번호 및 결재란 - * - 신청업체 정보 - * - 신청내용 - * - 부자재 목록 + * 공통 컴포넌트 사용: + * - DocumentHeader: quote 레이아웃 + LotApprovalTable */ import { QuoteFormData } from "./QuoteRegistration"; import type { CompanyFormData } from "@/components/settings/CompanyInfoManagement/types"; +import { DocumentHeader, LotApprovalTable } from "@/components/document-system"; interface PurchaseOrderDocumentProps { quote: QuoteFormData; @@ -64,138 +63,7 @@ export function PurchaseOrderDocument({ quote, companyInfo }: PurchaseOrderDocum line-height: 1.4; } - .po-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 20px; - padding-bottom: 15px; - border-bottom: 2px solid #000; - } - - .po-title { - flex: 1; - text-align: center; - } - - .po-title h1 { - font-size: 36px; - font-weight: 700; - letter-spacing: 8px; - margin: 0; - } - - .po-approval-section { - border: 2px solid #000; - background: white; - } - - .po-lot-number-row { - display: grid; - grid-template-columns: 100px 1fr; - border-bottom: 2px solid #000; - } - - .po-lot-label { - background: #e8e8e8; - border-right: 2px solid #000; - padding: 8px; - text-align: center; - font-weight: 700; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; - } - - .po-lot-value { - background: white; - padding: 8px; - text-align: center; - font-weight: 700; - font-size: 14px; - color: #000; - display: flex; - align-items: center; - justify-content: center; - } - - .po-approval-box { - width: 100%; - border: none; - display: grid; - grid-template-columns: 60px 1fr; - grid-template-rows: auto auto auto; - } - - .po-approval-merged-vertical-cell { - border-right: 1px solid #000; - padding: 4px; - text-align: center; - font-weight: 600; - font-size: 11px; - background: white; - display: flex; - align-items: center; - justify-content: center; - grid-row: 1 / 4; - } - - .po-approval-header { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - border-bottom: 1px solid #000; - } - - .po-approval-header-cell { - border-right: 1px solid #000; - padding: 8px; - text-align: center; - font-weight: 600; - font-size: 12px; - background: white; - } - - .po-approval-header-cell:last-child { - border-right: none; - } - - .po-approval-content-row { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - border-bottom: 1px solid #000; - } - - .po-approval-name-row { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - } - - .po-approval-signature-cell { - border-right: 1px solid #000; - padding: 8px; - text-align: center; - font-size: 11px; - height: 50px; - background: white; - } - - .po-approval-signature-cell:last-child { - border-right: none; - } - - .po-approval-name-cell { - border-right: 1px solid #000; - padding: 6px; - text-align: center; - font-weight: 600; - font-size: 11px; - background: white; - } - - .po-approval-name-cell:last-child { - border-right: none; - } + /* 헤더 스타일은 공통 컴포넌트 사용 */ .po-section-table { width: 100%; @@ -255,49 +123,21 @@ export function PurchaseOrderDocument({ quote, companyInfo }: PurchaseOrderDocum {/* 발주서 내용 */}
- {/* 헤더: 제목 + 결재란 */} -
- {/* 제목 */} -
-

발 주 서

-
- - {/* 로트번호 + 결재란 */} -
- {/* 로트번호 */} -
-
- 로트번호 -
-
- {purchaseOrderNumber} -
-
- - {/* 결재란 */} -
-

- {/* 결재란 헤더 */} -
-
작성
-
검토
-
승인
-
- {/* 결재+서명란 */} -
-
전진
-
-
-
- {/* 이름란 */} -
-
판매/전진
-
회계
-
생산
-
-
-
-
+ {/* 헤더: 제목 + 로트번호/결재란 (공통 컴포넌트) */} + + } + /> {/* 신청업체 */} diff --git a/src/components/quotes/QuoteDocument.tsx b/src/components/quotes/QuoteDocument.tsx index 225bf840..e2d302cf 100644 --- a/src/components/quotes/QuoteDocument.tsx +++ b/src/components/quotes/QuoteDocument.tsx @@ -1,17 +1,14 @@ /** * 견적서 (Quote Document) * - * - 수요자/공급자 정보 - * - 총 견적금액 - * - 제품구성 정보 - * - 품목 내역 테이블 - * - 비용 산출 - * - 비고사항 - * - 서명란 + * 공통 컴포넌트 사용: + * - DocumentHeader: simple 레이아웃 + * - SignatureSection: 서명/도장 영역 */ import { QuoteFormData } from "./QuoteRegistration"; import type { CompanyFormData } from "@/components/settings/CompanyInfoManagement/types"; +import { DocumentHeader, SignatureSection } from "@/components/document-system"; interface QuoteDocumentProps { quote: QuoteFormData; @@ -81,24 +78,7 @@ export function QuoteDocument({ quote, companyInfo }: QuoteDocumentProps) { line-height: 1.5; } - .doc-header { - text-align: center; - border-bottom: 3px double #000; - padding-bottom: 20px; - margin-bottom: 30px; - } - - .doc-title { - font-size: 26px; - font-weight: 700; - letter-spacing: 2px; - margin-bottom: 12px; - } - - .doc-number { - font-size: 14px; - color: #333; - } + /* 헤더 스타일은 공통 컴포넌트 사용 */ .info-box { border: 2px solid #000; @@ -210,25 +190,7 @@ export function QuoteDocument({ quote, companyInfo }: QuoteDocumentProps) { font-size: 13px; } - .stamp-area { - border: 2px solid #000; - width: 80px; - height: 80px; - display: inline-block; - position: relative; - margin-left: 20px; - } - - .stamp-text { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 10px; - color: #999; - text-align: center; - line-height: 1.3; - } + /* 서명/도장 스타일은 공통 컴포넌트 사용 */ .footer-note { margin-top: 40px; @@ -238,22 +200,18 @@ export function QuoteDocument({ quote, companyInfo }: QuoteDocumentProps) { color: #666; line-height: 1.6; } - - .signature-section { - margin-top: 30px; - text-align: right; - } `} {/* 견적서 내용 */}
- {/* 문서 헤더 */} -
-
견 적 서
-
- 문서번호: {quote.id || 'Q-XXXXXX'} | 작성일자: {formatDate(quote.registrationDate || '')} -
-
+ {/* 문서 헤더 (공통 컴포넌트) */} + {/* 수요자 정보 */}
@@ -424,27 +382,14 @@ export function QuoteDocument({ quote, companyInfo }: QuoteDocumentProps) { )} - {/* 서명란 */} -
-
-
- 상기와 같이 견적합니다. -
-
-
-
{formatDate(quote.registrationDate || '')}
-
- 공급자: {companyInfo?.companyName || '-'} (인) -
-
-
-
- (인감
날인) -
-
-
-
-
+ {/* 서명란 (공통 컴포넌트) */} + {/* 하단 안내사항 */}
diff --git a/src/components/quotes/QuoteManagementClient.tsx b/src/components/quotes/QuoteManagementClient.tsx index cd486d80..11e79d9c 100644 --- a/src/components/quotes/QuoteManagementClient.tsx +++ b/src/components/quotes/QuoteManagementClient.tsx @@ -43,16 +43,7 @@ import { } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import { StandardDialog } from '@/components/molecules/StandardDialog'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { toast } from 'sonner'; import { formatAmount, formatAmountManwon } from '@/utils/formatAmount'; import type { Quote, QuoteFilterType } from './types'; @@ -696,49 +687,36 @@ export function QuoteManagementClient({ {/* 삭제 확인 다이얼로그 */} - - - - 견적 삭제 확인 - - {deleteTargetId - ? `견적번호: ${allQuotes.find((q) => q.id === deleteTargetId)?.quoteNumber || deleteTargetId}` - : ''} -
- 이 견적을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다. -
-
- - 취소 - - {isPending ? '삭제 중...' : '삭제'} - - -
-
+ + {deleteTargetId + ? `견적번호: ${allQuotes.find((q) => q.id === deleteTargetId)?.quoteNumber || deleteTargetId}` + : ''} +
+ 이 견적을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다. + + } + loading={isPending} + onConfirm={handleConfirmDelete} + /> {/* 일괄 삭제 확인 다이얼로그 */} - - - - 일괄 삭제 확인 - - 선택한 {bulkDeleteIds.length}개의 견적을 삭제하시겠습니까? -
- 삭제된 데이터는 복구할 수 없습니다. -
-
- - 취소 - - {isPending ? '삭제 중...' : '삭제'} - - -
-
+ description={ + <> + 선택한 {bulkDeleteIds.length}개의 견적을 삭제하시겠습니까? +
+ 삭제된 데이터는 복구할 수 없습니다. + + } + loading={isPending} + onConfirm={handleConfirmBulkDelete} + /> ); } diff --git a/src/components/settings/AccountInfoManagement/index.tsx b/src/components/settings/AccountInfoManagement/index.tsx index c6579f9a..c762ef66 100644 --- a/src/components/settings/AccountInfoManagement/index.tsx +++ b/src/components/settings/AccountInfoManagement/index.tsx @@ -21,6 +21,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import type { AccountInfo, TermsAgreement, MarketingConsent } from './types'; @@ -441,30 +442,23 @@ export function AccountInfoClient({ {/* 사용중지 확인 다이얼로그 */} - - - - 계정 사용중지 - - 정말 사용중지하시겠습니까? -
- - 해당 테넌트의 사용이 중지됩니다. - -
-
- - 취소 - - {isSuspending ? '처리 중...' : '확인'} - - -
-
+ + 정말 사용중지하시겠습니까? +
+ + 해당 테넌트의 사용이 중지됩니다. + + + } + variant="warning" + loading={isSuspending} + /> ); } diff --git a/src/components/settings/AccountManagement/AccountDetail.tsx b/src/components/settings/AccountManagement/AccountDetail.tsx index 7a8ef102..a5123aa1 100644 --- a/src/components/settings/AccountManagement/AccountDetail.tsx +++ b/src/components/settings/AccountManagement/AccountDetail.tsx @@ -16,16 +16,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { toast } from 'sonner'; @@ -218,29 +209,20 @@ export function AccountDetail({ account, mode: initialMode }: AccountDetailProps
{/* 삭제 확인 다이얼로그 */} - - - - 계좌 삭제 - - 계좌를 정말 삭제하시겠습니까? -
- - 삭제된 계좌의 과거 사용 내역은 보존됩니다. - -
-
- - 취소 - - 삭제 - - -
-
+ + 계좌를 정말 삭제하시겠습니까? +
+ + 삭제된 계좌의 과거 사용 내역은 보존됩니다. + + + } + onConfirm={handleConfirmDelete} + /> ); } diff --git a/src/components/settings/AccountManagement/index.tsx b/src/components/settings/AccountManagement/index.tsx index f7eebdeb..8b3b5276 100644 --- a/src/components/settings/AccountManagement/index.tsx +++ b/src/components/settings/AccountManagement/index.tsx @@ -15,22 +15,12 @@ import { Pencil, Trash2, Plus, - Loader2, } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { TableRow, TableCell } from '@/components/ui/table'; import { UniversalListPage, @@ -349,66 +339,40 @@ export function AccountManagement() { {/* 단일 삭제 확인 다이얼로그 */} - - - - 계좌 삭제 - - 계좌를 정말 삭제하시겠습니까? -
- - 삭제된 계좌의 과거 사용 내역은 보존됩니다. - -
-
- - 취소 - - {isDeleting ? ( - <> - - 삭제 중... - - ) : '삭제'} - - -
-
+ + 계좌를 정말 삭제하시겠습니까? +
+ + 삭제된 계좌의 과거 사용 내역은 보존됩니다. + + + } + loading={isDeleting} + /> {/* 다중 삭제 확인 다이얼로그 */} - - - - 계좌 삭제 - - 선택하신 {bulkDeleteIds.length}개의 계좌를 정말 삭제하시겠습니까? -
- - 삭제된 계좌의 과거 사용 내역은 보존됩니다. - -
-
- - 취소 - - {isDeleting ? ( - <> - - 삭제 중... - - ) : '삭제'} - - -
-
+ + 선택하신 {bulkDeleteIds.length}개의 계좌를 정말 삭제하시겠습니까? +
+ + 삭제된 계좌의 과거 사용 내역은 보존됩니다. + + + } + loading={isDeleting} + /> ); } \ No newline at end of file diff --git a/src/components/settings/PermissionManagement/PermissionDetail.tsx b/src/components/settings/PermissionManagement/PermissionDetail.tsx index 3b80014f..f4147614 100644 --- a/src/components/settings/PermissionManagement/PermissionDetail.tsx +++ b/src/components/settings/PermissionManagement/PermissionDetail.tsx @@ -21,16 +21,7 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { permissionConfig } from './permissionConfig'; import type { Permission, MenuPermission, PermissionType } from './types'; @@ -444,29 +435,21 @@ export function PermissionDetail({ permission, onBack, onSave, onDelete }: Permi /> {/* 삭제 확인 다이얼로그 */} - - - - 권한 삭제 - - "{permission.name}" 권한을 삭제하시겠습니까? -
- - 이 권한을 사용 중인 사원이 있으면 해당 사원의 권한이 초기화됩니다. - -
-
- - 취소 - - 삭제 - - -
-
+ + "{permission.name}" 권한을 삭제하시겠습니까? +
+ + 이 권한을 사용 중인 사원이 있으면 해당 사원의 권한이 초기화됩니다. + + + } + /> ); } diff --git a/src/components/settings/PermissionManagement/PermissionDetailClient.tsx b/src/components/settings/PermissionManagement/PermissionDetailClient.tsx index f1f9a2bf..3488bc71 100644 --- a/src/components/settings/PermissionManagement/PermissionDetailClient.tsx +++ b/src/components/settings/PermissionManagement/PermissionDetailClient.tsx @@ -29,16 +29,7 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { PageHeader } from '@/components/organisms/PageHeader'; import { PageLayout } from '@/components/organisms/PageLayout'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; @@ -669,37 +660,22 @@ export function PermissionDetailClient({ permissionId, isNew = false }: Permissi {/* 삭제 확인 다이얼로그 */} {!isNew && role && ( - - - - 역할 삭제 - - "{role.name}" 역할을 삭제하시겠습니까? -
- - 이 역할을 사용 중인 사원이 있으면 해당 사원의 역할이 초기화됩니다. - -
-
- - 취소 - - {isDeleting ? ( - <> - - 삭제 중... - - ) : ( - '삭제' - )} - - -
-
+ + "{role.name}" 역할을 삭제하시겠습니까? +
+ + 이 역할을 사용 중인 사원이 있으면 해당 사원의 역할이 초기화됩니다. + + + } + loading={isDeleting} + /> )} ); diff --git a/src/components/settings/PermissionManagement/index.tsx b/src/components/settings/PermissionManagement/index.tsx index f2adc1db..66e71513 100644 --- a/src/components/settings/PermissionManagement/index.tsx +++ b/src/components/settings/PermissionManagement/index.tsx @@ -27,16 +27,7 @@ import { type TabOption, } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { toast } from 'sonner'; import type { Role, RoleStats } from './types'; import { fetchRoles, fetchRoleStats, deleteRole } from './actions'; @@ -467,40 +458,25 @@ export function PermissionManagement() { renderMobileCard, renderDialogs: () => ( - - - - 역할 삭제 - - {isBulkDelete - ? `선택한 ${selectedItems.size}개의 역할을 삭제하시겠습니까?` - : `"${roleToDelete?.name}" 역할을 삭제하시겠습니까?` - } -
- - 이 역할을 사용 중인 사원이 있으면 해당 사원의 역할이 초기화됩니다. - -
-
- - 취소 - - {isDeleting ? ( - <> - - 삭제 중... - - ) : ( - '삭제' - )} - - -
-
+ + {isBulkDelete + ? `선택한 ${selectedItems.size}개의 역할을 삭제하시겠습니까?` + : `"${roleToDelete?.name}" 역할을 삭제하시겠습니까?` + } +
+ + 이 역할을 사용 중인 사원이 있으면 해당 사원의 역할이 초기화됩니다. + + + } + loading={isDeleting} + /> ), }; diff --git a/src/components/settings/RankManagement/index.tsx b/src/components/settings/RankManagement/index.tsx index 09ef6f86..2ee65e50 100644 --- a/src/components/settings/RankManagement/index.tsx +++ b/src/components/settings/RankManagement/index.tsx @@ -9,16 +9,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent } from '@/components/ui/card'; import { RankDialog } from './RankDialog'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { toast } from 'sonner'; import type { Rank } from './types'; import { @@ -339,33 +330,22 @@ export function RankManagement() { /> {/* 삭제 확인 다이얼로그 */} - - - - 직급 삭제 - - "{rankToDelete?.name}" 직급을 삭제하시겠습니까? -
- - 이 직급을 사용 중인 사원이 있으면 해당 사원의 직급이 초기화됩니다. - -
-
- - 취소 - - {isSubmitting ? ( - - ) : null} - 삭제 - - -
-
+ + "{rankToDelete?.name}" 직급을 삭제하시겠습니까? +
+ + 이 직급을 사용 중인 사원이 있으면 해당 사원의 직급이 초기화됩니다. + + + } + loading={isSubmitting} + /> ); } \ No newline at end of file diff --git a/src/components/settings/SubscriptionManagement/SubscriptionClient.tsx b/src/components/settings/SubscriptionManagement/SubscriptionClient.tsx index 542cea48..b3ef8533 100644 --- a/src/components/settings/SubscriptionManagement/SubscriptionClient.tsx +++ b/src/components/settings/SubscriptionManagement/SubscriptionClient.tsx @@ -6,16 +6,7 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; import { Badge } from '@/components/ui/badge'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { toast } from 'sonner'; @@ -218,33 +209,29 @@ export function SubscriptionClient({ initialData }: SubscriptionClientProps) { {/* ===== 서비스 해지 확인 다이얼로그 ===== */} - - - - - - 서비스 해지 - - - 모든 데이터가 삭제되며 복구할 수 없습니다. -
- - 정말 서비스를 해지하시겠습니까? - -
-
- - 취소 - - {isCancelling ? '처리 중...' : '확인'} - - -
-
+ + + 서비스 해지 + + } + description={ + <> + 모든 데이터가 삭제되며 복구할 수 없습니다. +
+ + 정말 서비스를 해지하시겠습니까? + + + } + confirmText="확인" + loading={isCancelling} + /> ); } diff --git a/src/components/settings/SubscriptionManagement/SubscriptionManagement.tsx b/src/components/settings/SubscriptionManagement/SubscriptionManagement.tsx index 12a19bcd..acc2cac4 100644 --- a/src/components/settings/SubscriptionManagement/SubscriptionManagement.tsx +++ b/src/components/settings/SubscriptionManagement/SubscriptionManagement.tsx @@ -6,16 +6,7 @@ import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import type { SubscriptionInfo } from './types'; @@ -231,33 +222,29 @@ export function SubscriptionManagement({ initialData }: SubscriptionManagementPr {/* ===== 서비스 해지 확인 다이얼로그 ===== */} - - - - - - 서비스 해지 - - - 모든 데이터가 삭제되며 복구할 수 없습니다. -
- - 정말 서비스를 해지하시겠습니까? - -
-
- - 취소 - - {isCancelling ? '처리 중...' : '확인'} - - -
-
+ + + 서비스 해지 + + } + description={ + <> + 모든 데이터가 삭제되며 복구할 수 없습니다. +
+ + 정말 서비스를 해지하시겠습니까? + + + } + confirmText="확인" + loading={isCancelling} + /> ); } \ No newline at end of file diff --git a/src/components/settings/TitleManagement/index.tsx b/src/components/settings/TitleManagement/index.tsx index 2edf5258..643873d3 100644 --- a/src/components/settings/TitleManagement/index.tsx +++ b/src/components/settings/TitleManagement/index.tsx @@ -9,16 +9,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent } from '@/components/ui/card'; import { TitleDialog } from './TitleDialog'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle as AlertTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { toast } from 'sonner'; import type { Title } from './types'; import { @@ -339,33 +330,22 @@ export function TitleManagement() { /> {/* 삭제 확인 다이얼로그 */} - - - - 직책 삭제 - - "{titleToDelete?.name}" 직책을 삭제하시겠습니까? -
- - 이 직책을 사용 중인 사원이 있으면 해당 사원의 직책이 초기화됩니다. - -
-
- - 취소 - - {isSubmitting ? ( - - ) : null} - 삭제 - - -
-
+ + "{titleToDelete?.name}" 직책을 삭제하시겠습니까? +
+ + 이 직책을 사용 중인 사원이 있으면 해당 사원의 직책이 초기화됩니다. + + + } + loading={isSubmitting} + /> ); } \ No newline at end of file diff --git a/src/components/templates/IntegratedDetailTemplate/components/skeletons.tsx b/src/components/templates/IntegratedDetailTemplate/components/skeletons.tsx new file mode 100644 index 00000000..f8513356 --- /dev/null +++ b/src/components/templates/IntegratedDetailTemplate/components/skeletons.tsx @@ -0,0 +1,182 @@ +/** + * IntegratedDetailTemplate Skeleton Components + * + * 상세 페이지 로딩 스켈레톤 컴포넌트 + */ + +'use client'; + +import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { cn } from '@/lib/utils'; + +// ============================================ +// 1. 필드 스켈레톤 +// ============================================ +export interface DetailFieldSkeletonProps { + /** 라벨 표시 여부 (default: true) */ + showLabel?: boolean; + /** 그리드 span (1~4) */ + colSpan?: 1 | 2 | 3 | 4; + /** 필드 타입에 따른 높이 조절 */ + type?: 'input' | 'textarea' | 'select'; + /** 추가 클래스 */ + className?: string; +} + +const colSpanClasses = { + 1: '', + 2: 'md:col-span-2', + 3: 'md:col-span-2 lg:col-span-3', + 4: 'md:col-span-2 lg:col-span-4', +}; + +export function DetailFieldSkeleton({ + showLabel = true, + colSpan = 1, + type = 'input', + className, +}: DetailFieldSkeletonProps) { + return ( +
+ {/* 라벨 */} + {showLabel && } + {/* 입력 필드 */} + +
+ ); +} + +// ============================================ +// 2. 그리드 스켈레톤 +// ============================================ +export interface DetailGridSkeletonProps { + /** 그리드 열 수 (default: 2) */ + cols?: 1 | 2 | 3 | 4; + /** 필드 개수 (default: 4) */ + fieldCount?: number; + /** 그리드 간격 (default: 'md') */ + gap?: 'sm' | 'md' | 'lg'; + /** 추가 클래스 */ + className?: string; +} + +const colsClasses = { + 1: 'grid-cols-1', + 2: 'grid-cols-1 md:grid-cols-2', + 3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3', + 4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4', +}; + +const gapClasses = { + sm: 'gap-4', + md: 'gap-6', + lg: 'gap-8', +}; + +export function DetailGridSkeleton({ + cols = 2, + fieldCount = 4, + gap = 'md', + className, +}: DetailGridSkeletonProps) { + return ( +
+ {Array.from({ length: fieldCount }).map((_, i) => ( + + ))} +
+ ); +} + +// ============================================ +// 3. 섹션 스켈레톤 +// ============================================ +export interface DetailSectionSkeletonProps { + /** 제목 표시 여부 (default: true) */ + showTitle?: boolean; + /** 그리드 열 수 (default: 2) */ + cols?: 1 | 2 | 3 | 4; + /** 필드 개수 (default: 6) */ + fieldCount?: number; + /** 추가 클래스 */ + className?: string; +} + +export function DetailSectionSkeleton({ + showTitle = true, + cols = 2, + fieldCount = 6, + className, +}: DetailSectionSkeletonProps) { + return ( + + {showTitle && ( + + + + )} + + + + + ); +} + +// ============================================ +// 4. 전체 페이지 스켈레톤 +// ============================================ +export interface DetailPageSkeletonProps { + /** 섹션 개수 (default: 1) */ + sections?: number; + /** 섹션당 필드 개수 (default: 6) */ + fieldsPerSection?: number; + /** 그리드 열 수 (default: 2) */ + cols?: 1 | 2 | 3 | 4; + /** 헤더 표시 여부 (default: true) */ + showHeader?: boolean; + /** 추가 클래스 */ + className?: string; +} + +export function DetailPageSkeleton({ + sections = 1, + fieldsPerSection = 6, + cols = 2, + showHeader = true, + className, +}: DetailPageSkeletonProps) { + return ( +
+ {/* 페이지 헤더 */} + {showHeader && ( +
+
+ + +
+
+ + +
+
+ )} + + {/* 섹션들 */} + {Array.from({ length: sections }).map((_, i) => ( + + ))} +
+ ); +} + +export default DetailSectionSkeleton; diff --git a/src/components/templates/IntegratedDetailTemplate/index.tsx b/src/components/templates/IntegratedDetailTemplate/index.tsx index 0821dae9..d2572a72 100644 --- a/src/components/templates/IntegratedDetailTemplate/index.tsx +++ b/src/components/templates/IntegratedDetailTemplate/index.tsx @@ -11,16 +11,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useRouter, useParams } from 'next/navigation'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { toast } from 'sonner'; @@ -375,7 +366,7 @@ export function IntegratedDetailTemplate>({ {afterContent} {/* 버튼 영역 - 하단 배치 시만 */} {!isTopButtons && renderActionButtons('mt-6')} - >({
{/* 삭제 확인 다이얼로그 */} - >({ } } -// ===== 삭제 확인 다이얼로그 ===== -function DeleteDialog({ - open, - onOpenChange, - onConfirm, - title, - description, -}: { - open: boolean; - onOpenChange: (open: boolean) => void; - onConfirm: () => void; - title?: string; - description?: string; -}) { - return ( - - - - {title || '삭제 확인'} - - {description || '정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.'} - - - - 취소 - - 삭제 - - - - - ); -} - // ===== 유효성 검사 헬퍼 ===== function validateRule( rule: ValidationRule, diff --git a/src/components/templates/IntegratedListTemplateV2.tsx b/src/components/templates/IntegratedListTemplateV2.tsx index e87c5f40..706cb19a 100644 --- a/src/components/templates/IntegratedListTemplateV2.tsx +++ b/src/components/templates/IntegratedListTemplateV2.tsx @@ -5,6 +5,7 @@ import { LucideIcon, Trash2, Plus, Loader2 } from "lucide-react"; import { DateRangeSelector } from "@/components/molecules/DateRangeSelector"; import { Card, CardContent } from "@/components/ui/card"; import { Tabs, TabsContent } from "@/components/ui/tabs"; +import { TableSkeleton, MobileCardGridSkeleton } from "@/components/ui/skeleton"; import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Checkbox } from "@/components/ui/checkbox"; import { Button } from "@/components/ui/button"; @@ -15,16 +16,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { PageLayout } from "@/components/organisms/PageLayout"; import { PageHeader } from "@/components/organisms/PageHeader"; import { StatCards } from "@/components/organisms/StatCards"; @@ -670,7 +662,11 @@ export function IntegratedListTemplateV2({ {/* 모바일/태블릿/소형 노트북 (~1279px) 카드 뷰 */}
- {mobileData.length === 0 ? ( + {isLoading ? ( +
+ +
+ ) : mobileData.length === 0 ? (
검색 결과가 없습니다.
@@ -739,6 +735,14 @@ export function IntegratedListTemplateV2({ {/* 데스크톱 (1280px+) 테이블 뷰 */}
+ {isLoading ? ( + col.key === 'actions')} + /> + ) : (
@@ -801,6 +805,7 @@ export function IntegratedListTemplateV2({ )}
+ )}
))} @@ -863,44 +868,21 @@ export function IntegratedListTemplateV2({ )}
- {/* 일괄 삭제 확인 다이얼로그 - 단일 삭제와 동일한 디자인 */} - - - - - ⚠️ - 삭제 확인 - - -
-

- 선택한 {selectedItems.size}개의 항목을 삭제하시겠습니까? -

-
-
- ⚠️ -
- 주의 -
- 삭제된 항목은 복구할 수 없습니다. 관련된 데이터도 함께 삭제될 수 있습니다. -
-
-
-
-
-
- - 취소 - - - 삭제 - - -
-
+ {/* 일괄 삭제 확인 다이얼로그 */} + + 선택한 {selectedItems.size}개의 항목을 삭제하시겠습니까? +
+ + 삭제된 항목은 복구할 수 없습니다. 관련된 데이터도 함께 삭제될 수 있습니다. + + + } + /> ); } diff --git a/src/components/templates/UniversalListPage/index.tsx b/src/components/templates/UniversalListPage/index.tsx index 295b0cc9..cb62dfba 100644 --- a/src/components/templates/UniversalListPage/index.tsx +++ b/src/components/templates/UniversalListPage/index.tsx @@ -14,16 +14,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useRouter, useParams } from 'next/navigation'; import { toast } from 'sonner'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { IntegratedListTemplateV2, type PaginationConfig, @@ -607,18 +598,13 @@ export function UniversalListPage({ /> {/* 삭제 확인 다이얼로그 */} - - - - {deleteConfirmTitle} - {deleteConfirmDescription} - - - 취소 - 삭제 - - - + {/* 상세 모달 (detailMode === 'modal'일 때) */} {config.detailMode === 'modal' && config.DetailModalComponent && ( diff --git a/src/components/ui/confirm-dialog.tsx b/src/components/ui/confirm-dialog.tsx new file mode 100644 index 00000000..1c2ef3e9 --- /dev/null +++ b/src/components/ui/confirm-dialog.tsx @@ -0,0 +1,216 @@ +'use client'; + +/** + * ConfirmDialog - 확인/취소 다이얼로그 공통 컴포넌트 + * + * 사용 예시: + * ```tsx + * + * ``` + */ + +import { ReactNode, useCallback, useState } from 'react'; +import { Loader2 } from 'lucide-react'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { cn } from '@/lib/utils'; + +export type ConfirmDialogVariant = 'default' | 'destructive' | 'warning' | 'success'; + +export interface ConfirmDialogProps { + /** 다이얼로그 열림 상태 */ + open: boolean; + /** 열림 상태 변경 핸들러 */ + onOpenChange: (open: boolean) => void; + /** 다이얼로그 제목 */ + title: string; + /** 다이얼로그 설명 (문자열 또는 ReactNode) */ + description: ReactNode; + /** 확인 버튼 텍스트 (기본값: '확인') */ + confirmText?: string; + /** 취소 버튼 텍스트 (기본값: '취소') */ + cancelText?: string; + /** 버튼 스타일 변형 */ + variant?: ConfirmDialogVariant; + /** 외부 로딩 상태 (외부에서 관리할 때) */ + loading?: boolean; + /** 확인 버튼 클릭 핸들러 (Promise 반환 시 내부 로딩 상태 자동 관리) */ + onConfirm: () => void | Promise; + /** 취소 버튼 클릭 핸들러 (선택사항) */ + onCancel?: () => void; + /** 확인 버튼 비활성화 여부 */ + confirmDisabled?: boolean; + /** 아이콘 (제목 옆에 표시) */ + icon?: ReactNode; +} + +const variantStyles: Record = { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + warning: 'bg-orange-600 text-white hover:bg-orange-700', + success: 'bg-green-600 text-white hover:bg-green-700', +}; + +export function ConfirmDialog({ + open, + onOpenChange, + title, + description, + confirmText = '확인', + cancelText = '취소', + variant = 'default', + loading: externalLoading, + onConfirm, + onCancel, + confirmDisabled, + icon, +}: ConfirmDialogProps) { + const [internalLoading, setInternalLoading] = useState(false); + const isLoading = externalLoading ?? internalLoading; + + const handleConfirm = useCallback(async () => { + const result = onConfirm(); + + // Promise인 경우 내부 로딩 상태 관리 + if (result instanceof Promise && externalLoading === undefined) { + setInternalLoading(true); + try { + await result; + } finally { + setInternalLoading(false); + } + } + }, [onConfirm, externalLoading]); + + const handleCancel = useCallback(() => { + onCancel?.(); + onOpenChange(false); + }, [onCancel, onOpenChange]); + + return ( + + + + + {icon} + {title} + + + {typeof description === 'string' ? ( + description + ) : ( +
{description}
+ )} +
+
+ + + {cancelText} + + + {isLoading && } + {confirmText} + + +
+
+ ); +} + +/** + * 삭제 확인 다이얼로그 프리셋 + */ +export interface DeleteConfirmDialogProps + extends Omit { + /** 삭제 대상 이름 (선택사항) */ + itemName?: string; +} + +export function DeleteConfirmDialog({ + itemName, + description, + ...props +}: DeleteConfirmDialogProps) { + return ( + + {itemName ? `"${itemName}"을(를) ` : ''}정말 삭제하시겠습니까? +
+ 삭제된 데이터는 복구할 수 없습니다. + + ) + } + confirmText="삭제" + variant="destructive" + {...props} + /> + ); +} + +/** + * 저장 확인 다이얼로그 프리셋 + */ +export interface SaveConfirmDialogProps + extends Omit {} + +export function SaveConfirmDialog({ + description = '변경사항을 저장하시겠습니까?', + ...props +}: SaveConfirmDialogProps) { + return ( + + ); +} + +/** + * 취소 확인 다이얼로그 프리셋 + */ +export interface CancelConfirmDialogProps + extends Omit {} + +export function CancelConfirmDialog({ + description = '작업을 취소하시겠습니까? 변경사항이 저장되지 않습니다.', + ...props +}: CancelConfirmDialogProps) { + return ( + + ); +} + +export default ConfirmDialog; diff --git a/src/components/ui/empty-state.tsx b/src/components/ui/empty-state.tsx new file mode 100644 index 00000000..a8412b1d --- /dev/null +++ b/src/components/ui/empty-state.tsx @@ -0,0 +1,226 @@ +'use client'; + +/** + * EmptyState - 빈 상태 표시용 공통 컴포넌트 + * + * 사용 예시: + * ```tsx + * // 기본 사용 + * + * + * // 아이콘과 설명 포함 + * } + * message="검색 결과가 없습니다." + * description="다른 검색어로 다시 시도해 주세요." + * /> + * + * // 액션 버튼 포함 + * } + * message="등록된 항목이 없습니다." + * action={{ + * label: "새로 등록", + * onClick: () => router.push('/new'), + * }} + * /> + * + * // 테이블 내 사용 (compact) + * + * ``` + */ + +import { ReactNode } from 'react'; +import { Button, type ButtonProps } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { Inbox, FileSearch, AlertCircle, FolderOpen } from 'lucide-react'; + +export type EmptyStateVariant = 'default' | 'compact' | 'large'; +export type EmptyStatePreset = 'noData' | 'noResults' | 'noItems' | 'error'; + +export interface EmptyStateAction { + /** 버튼 라벨 */ + label: string; + /** 클릭 핸들러 */ + onClick: () => void; + /** 버튼 variant */ + variant?: ButtonProps['variant']; + /** 버튼 아이콘 */ + icon?: ReactNode; +} + +export interface EmptyStateProps { + /** 메인 메시지 */ + message?: string; + /** 부가 설명 */ + description?: string; + /** 아이콘 (ReactNode 또는 프리셋) */ + icon?: ReactNode | EmptyStatePreset; + /** 액션 버튼 설정 */ + action?: EmptyStateAction; + /** 스타일 variant */ + variant?: EmptyStateVariant; + /** 프리셋 (icon과 message 자동 설정) */ + preset?: EmptyStatePreset; + /** 커스텀 className */ + className?: string; + /** children (완전 커스텀 콘텐츠) */ + children?: ReactNode; +} + +// 프리셋 설정 +const PRESETS: Record< + EmptyStatePreset, + { icon: ReactNode; message: string; description?: string } +> = { + noData: { + icon: , + message: '데이터가 없습니다.', + description: '아직 등록된 데이터가 없습니다.', + }, + noResults: { + icon: , + message: '검색 결과가 없습니다.', + description: '다른 검색어로 다시 시도해 주세요.', + }, + noItems: { + icon: , + message: '등록된 항목이 없습니다.', + description: '새 항목을 등록해 주세요.', + }, + error: { + icon: , + message: '데이터를 불러올 수 없습니다.', + description: '잠시 후 다시 시도해 주세요.', + }, +}; + +// Variant 스타일 +const variantStyles: Record = { + default: { + container: 'py-12', + icon: 'h-12 w-12', + text: 'text-base', + }, + compact: { + container: 'py-6', + icon: 'h-8 w-8', + text: 'text-sm', + }, + large: { + container: 'py-20 min-h-[400px]', + icon: 'h-16 w-16', + text: 'text-lg', + }, +}; + +export function EmptyState({ + message, + description, + icon, + action, + variant = 'default', + preset, + className, + children, +}: EmptyStateProps) { + // 프리셋 적용 + const presetConfig = preset ? PRESETS[preset] : null; + const finalMessage = message ?? presetConfig?.message ?? '데이터가 없습니다.'; + const finalDescription = description ?? presetConfig?.description; + + // 아이콘 결정 + let finalIcon: ReactNode = null; + if (icon) { + // icon이 프리셋 키인 경우 + if (typeof icon === 'string' && icon in PRESETS) { + finalIcon = PRESETS[icon as EmptyStatePreset].icon; + } else { + finalIcon = icon; + } + } else if (presetConfig) { + finalIcon = presetConfig.icon; + } + + const styles = variantStyles[variant]; + + return ( +
+ {children ? ( + children + ) : ( + <> + {finalIcon && ( +
{finalIcon}
+ )} + +
+

+ {finalMessage} +

+ {finalDescription && ( +

+ {finalDescription} +

+ )} +
+ + {action && ( + + )} + + )} +
+ ); +} + +/** + * 테이블용 빈 상태 컴포넌트 + * TableCell 내에서 사용할 때 유용 + */ +export interface TableEmptyStateProps { + /** 컬럼 수 (colSpan 용) */ + colSpan: number; + /** 메시지 */ + message?: string; + /** variant */ + variant?: 'default' | 'compact'; +} + +export function TableEmptyState({ + colSpan, + message = '데이터가 없습니다.', + variant = 'default', +}: TableEmptyStateProps) { + return ( + + + {message} + + + ); +} + +export default EmptyState; diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx index a626d9ba..8da53197 100644 --- a/src/components/ui/skeleton.tsx +++ b/src/components/ui/skeleton.tsx @@ -1,9 +1,30 @@ -import { cn } from '@/lib/utils'; +'use client'; -function Skeleton({ - className, - ...props -}: React.HTMLAttributes) { +/** + * 스켈레톤 컴포넌트 시스템 + * + * 사용 가이드: + * - Skeleton: 기본 스켈레톤 (커스텀 크기) + * - TableRowSkeleton: 테이블 행 스켈레톤 + * - TableSkeleton: 테이블 전체 스켈레톤 + * - MobileCardSkeleton: 모바일 카드 스켈레톤 + * - FormFieldSkeleton: 폼 필드 스켈레톤 + * - DetailPageSkeleton: 상세 페이지 스켈레톤 + * - StatCardSkeleton: 통계 카드 스켈레톤 + * - ListPageSkeleton: 리스트 페이지 스켈레톤 + */ + +import { cn } from '@/lib/utils'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; + +// ============================================ +// 1. 기본 스켈레톤 (기존) +// ============================================ +interface SkeletonProps extends React.HTMLAttributes { + className?: string; +} + +function Skeleton({ className, ...props }: SkeletonProps) { return (
+ {showCheckbox && ( + + + + )} + {Array.from({ length: columns }).map((_, i) => ( + + + + ))} + {showActions && ( + + + + )} + + ); +} + +// ============================================ +// 3. 테이블 전체 스켈레톤 +// ============================================ +interface TableSkeletonProps { + rows?: number; + columns?: number; + showCheckbox?: boolean; + showActions?: boolean; + showHeader?: boolean; +} + +function TableSkeleton({ + rows = 5, + columns = 5, + showCheckbox = true, + showActions = true, + showHeader = true, +}: TableSkeletonProps) { + return ( +
+ + {showHeader && ( + + + {showCheckbox && ( + + )} + {Array.from({ length: columns }).map((_, i) => ( + + ))} + {showActions && ( + + )} + + + )} + + {Array.from({ length: rows }).map((_, i) => ( + + ))} + +
+ + + + + +
+
+ ); +} + +// ============================================ +// 4. 모바일 카드 스켈레톤 +// ============================================ +interface MobileCardSkeletonProps { + showCheckbox?: boolean; + showBadge?: boolean; + fields?: number; + showActions?: boolean; +} + +function MobileCardSkeleton({ + showCheckbox = true, + showBadge = true, + fields = 4, + showActions = true, +}: MobileCardSkeletonProps) { + return ( + + + {/* 헤더 영역 */} +
+ {showCheckbox && } +
+
+ + {showBadge && } +
+ +
+
+ + {/* 정보 그리드 */} +
+ {Array.from({ length: fields }).map((_, i) => ( +
+ + +
+ ))} +
+ + {/* 액션 버튼 */} + {showActions && ( +
+ + +
+ )} +
+
+ ); +} + +// ============================================ +// 5. 모바일 카드 그리드 스켈레톤 +// ============================================ +interface MobileCardGridSkeletonProps { + count?: number; + showCheckbox?: boolean; + showBadge?: boolean; + fields?: number; + showActions?: boolean; +} + +function MobileCardGridSkeleton({ + count = 6, + showCheckbox = true, + showBadge = true, + fields = 4, + showActions = true, +}: MobileCardGridSkeletonProps) { + return ( +
+ {Array.from({ length: count }).map((_, i) => ( + + ))} +
+ ); +} + +// ============================================ +// 6. 폼 필드 스켈레톤 +// ============================================ +interface FormFieldSkeletonProps { + showLabel?: boolean; + type?: 'input' | 'textarea' | 'select'; +} + +function FormFieldSkeleton({ + showLabel = true, + type = 'input', +}: FormFieldSkeletonProps) { + return ( +
+ {showLabel && } + +
+ ); +} + +// ============================================ +// 7. 폼 섹션 스켈레톤 +// ============================================ +interface FormSectionSkeletonProps { + title?: boolean; + fields?: number; + columns?: 1 | 2 | 3; +} + +function FormSectionSkeleton({ + title = true, + fields = 4, + columns = 2, +}: FormSectionSkeletonProps) { + const gridCols = { + 1: 'grid-cols-1', + 2: 'grid-cols-1 md:grid-cols-2', + 3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3', + }; + + return ( + + {title && ( + + + + )} + +
+ {Array.from({ length: fields }).map((_, i) => ( + + ))} +
+
+
+ ); +} + +// ============================================ +// 8. 상세 페이지 스켈레톤 +// ============================================ +interface DetailPageSkeletonProps { + sections?: number; + fieldsPerSection?: number; + showHeader?: boolean; +} + +function DetailPageSkeleton({ + sections = 2, + fieldsPerSection = 6, + showHeader = true, +}: DetailPageSkeletonProps) { + return ( +
+ {/* 페이지 헤더 */} + {showHeader && ( +
+
+ + +
+
+ + +
+
+ )} + + {/* 섹션들 */} + {Array.from({ length: sections }).map((_, i) => ( + + ))} +
+ ); +} + +// ============================================ +// 9. 통계 카드 스켈레톤 +// ============================================ +interface StatCardSkeletonProps { + showIcon?: boolean; + showTrend?: boolean; +} + +function StatCardSkeleton({ + showIcon = true, + showTrend = true, +}: StatCardSkeletonProps) { + return ( + + +
+
+ + + {showTrend && } +
+ {showIcon && } +
+
+
+ ); +} + +// ============================================ +// 10. 통계 카드 그리드 스켈레톤 +// ============================================ +interface StatCardGridSkeletonProps { + count?: number; + showIcon?: boolean; + showTrend?: boolean; +} + +function StatCardGridSkeleton({ + count = 4, + showIcon = true, + showTrend = true, +}: StatCardGridSkeletonProps) { + return ( +
+ {Array.from({ length: count }).map((_, i) => ( + + ))} +
+ ); +} + +// ============================================ +// 11. 리스트 페이지 스켈레톤 (통합) +// ============================================ +interface ListPageSkeletonProps { + showHeader?: boolean; + showFilters?: boolean; + showStats?: boolean; + statsCount?: number; + tableRows?: number; + tableColumns?: number; + mobileCards?: number; +} + +function ListPageSkeleton({ + showHeader = true, + showFilters = true, + showStats = false, + statsCount = 4, + tableRows = 10, + tableColumns = 6, + mobileCards = 6, +}: ListPageSkeletonProps) { + return ( +
+ {/* 페이지 헤더 */} + {showHeader && ( +
+
+ + +
+ +
+ )} + + {/* 통계 카드 */} + {showStats && } + + {/* 필터 영역 */} + {showFilters && ( + + +
+ + + + +
+
+
+ )} + + {/* 데스크톱: 테이블 */} +
+ +
+ + {/* 모바일/태블릿: 카드 그리드 */} +
+ +
+ + {/* 페이지네이션 */} +
+ +
+ + + +
+
+
+ ); +} + +// ============================================ +// 12. 페이지 헤더 스켈레톤 +// ============================================ +interface PageHeaderSkeletonProps { + showActions?: boolean; + actionsCount?: number; +} + +function PageHeaderSkeleton({ + showActions = true, + actionsCount = 2, +}: PageHeaderSkeletonProps) { + return ( +
+
+ +
+ + +
+
+ {showActions && ( +
+ {Array.from({ length: actionsCount }).map((_, i) => ( + + ))} +
+ )} +
+ ); +} + +// ============================================ +// Export +// ============================================ +export { + Skeleton, + TableRowSkeleton, + TableSkeleton, + MobileCardSkeleton, + MobileCardGridSkeleton, + FormFieldSkeleton, + FormSectionSkeleton, + DetailPageSkeleton, + StatCardSkeleton, + StatCardGridSkeleton, + ListPageSkeleton, + PageHeaderSkeleton, +}; diff --git a/src/components/ui/status-badge.tsx b/src/components/ui/status-badge.tsx new file mode 100644 index 00000000..218e81ae --- /dev/null +++ b/src/components/ui/status-badge.tsx @@ -0,0 +1,122 @@ +'use client'; + +/** + * StatusBadge - 상태 표시용 배지 컴포넌트 + * + * 사용 예시: + * ```tsx + * // 기본 사용 (프리셋 variant) + * 완료 + * 대기 + * + * // 커스텀 className + * 커스텀 + * + * // createStatusConfig와 함께 사용 + * + * {STATUS_LABELS[status]} + * + * + * // 또는 간단하게 + * + * ``` + */ + +import { ReactNode } from 'react'; +import { cn } from '@/lib/utils'; +import { + StatusStylePreset, + BADGE_STYLE_PRESETS, + TEXT_STYLE_PRESETS, + StatusConfig, +} from '@/lib/utils/status-config'; + +export type StatusBadgeVariant = StatusStylePreset; +export type StatusBadgeMode = 'badge' | 'text'; +export type StatusBadgeSize = 'sm' | 'md' | 'lg'; + +export interface StatusBadgeProps { + /** 표시할 내용 */ + children?: ReactNode; + /** 프리셋 variant (className보다 우선순위 낮음) */ + variant?: StatusBadgeVariant; + /** 스타일 모드: 'badge' (배경+텍스트) 또는 'text' (텍스트만) */ + mode?: StatusBadgeMode; + /** 배지 크기 */ + size?: StatusBadgeSize; + /** 커스텀 className (variant보다 우선) */ + className?: string; + /** createStatusConfig에서 생성된 설정과 함께 사용 */ + status?: string; + /** StatusConfig 객체 (status와 함께 사용) */ + config?: StatusConfig; +} + +// 크기별 스타일 +const sizeStyles: Record = { + sm: 'text-xs px-1.5 py-0.5', + md: 'text-sm px-2 py-0.5', + lg: 'text-sm px-2.5 py-1', +}; + +export function StatusBadge({ + children, + variant = 'default', + mode = 'badge', + size = 'md', + className, + status, + config, +}: StatusBadgeProps) { + // config와 status가 제공된 경우 자동으로 라벨과 스타일 적용 + const displayContent = status && config ? config.getStatusLabel(status) : children; + const configStyle = status && config ? config.getStatusStyle(status) : undefined; + + // 스타일 우선순위: className > configStyle > variant preset + const presets = mode === 'badge' ? BADGE_STYLE_PRESETS : TEXT_STYLE_PRESETS; + const variantStyle = presets[variant]; + + // 최종 스타일 결정 + const finalStyle = className || configStyle || variantStyle; + + // Badge 모드일 때만 기본 rounded 스타일 추가 + const baseStyle = mode === 'badge' ? 'inline-flex items-center rounded-md font-medium' : 'inline-flex items-center'; + + return ( + + {displayContent} + + ); +} + +/** + * 간단한 상태 표시용 컴포넌트 + * createStatusConfig의 결과와 함께 사용 + */ +export interface ConfiguredStatusBadgeProps { + status: T; + config: StatusConfig; + size?: StatusBadgeSize; + mode?: StatusBadgeMode; + className?: string; +} + +export function ConfiguredStatusBadge({ + status, + config, + size = 'md', + mode = 'badge', + className, +}: ConfiguredStatusBadgeProps) { + return ( + + {config.getStatusLabel(status)} + + ); +} + +export default StatusBadge; diff --git a/src/lib/utils/status-config.ts b/src/lib/utils/status-config.ts new file mode 100644 index 00000000..b13c61f9 --- /dev/null +++ b/src/lib/utils/status-config.ts @@ -0,0 +1,154 @@ +/** + * Status Configuration Utility + * + * 상태 관련 설정(OPTIONS, LABELS, STYLES)을 단일 설정에서 생성하는 유틸리티 + * + * 사용 예시: + * ```ts + * const { STATUS_OPTIONS, STATUS_LABELS, STATUS_STYLES, getStatusLabel, getStatusStyle } = + * createStatusConfig({ + * pending: { label: '대기', style: 'warning' }, + * completed: { label: '완료', style: 'success' }, + * rejected: { label: '반려', style: 'destructive' }, + * }, { includeAll: true, allLabel: '전체' }); + * ``` + */ + +// 프리셋 스타일 정의 +export type StatusStylePreset = + | 'default' + | 'success' + | 'warning' + | 'destructive' + | 'info' + | 'muted' + | 'orange' + | 'purple'; + +// 프리셋 스타일 맵 (Badge 스타일 - 배경 + 텍스트) +export const BADGE_STYLE_PRESETS: Record = { + default: 'bg-gray-100 text-gray-800', + success: 'bg-green-100 text-green-800', + warning: 'bg-yellow-100 text-yellow-800', + destructive: 'bg-red-100 text-red-800', + info: 'bg-blue-100 text-blue-800', + muted: 'bg-gray-100 text-gray-500', + orange: 'bg-orange-100 text-orange-800', + purple: 'bg-purple-100 text-purple-800', +}; + +// 프리셋 스타일 맵 (Text 스타일 - 텍스트만) +export const TEXT_STYLE_PRESETS: Record = { + default: 'text-gray-600 font-medium', + success: 'text-green-600 font-medium', + warning: 'text-yellow-600 font-medium', + destructive: 'text-red-500 font-medium', + info: 'text-blue-500 font-medium', + muted: 'text-gray-400 font-medium', + orange: 'text-orange-500 font-medium', + purple: 'text-purple-500 font-medium', +}; + +// 단일 상태 설정 타입 +export interface StatusItemConfig { + /** 표시 라벨 */ + label: string; + /** 프리셋 스타일 또는 커스텀 클래스 */ + style: StatusStylePreset | string; +} + +// createStatusConfig 옵션 +export interface CreateStatusConfigOptions { + /** '전체' 옵션 포함 여부 (기본값: false) */ + includeAll?: boolean; + /** '전체' 옵션 라벨 (기본값: '전체') */ + allLabel?: string; + /** 스타일 모드: 'badge' (배경+텍스트) 또는 'text' (텍스트만) */ + styleMode?: 'badge' | 'text'; +} + +// 반환 타입 +export interface StatusConfig { + /** Select 컴포넌트용 옵션 배열 */ + STATUS_OPTIONS: ReadonlyArray<{ value: T | 'all'; label: string }>; + /** 상태별 라벨 맵 */ + STATUS_LABELS: Record; + /** 상태별 스타일 맵 */ + STATUS_STYLES: Record; + /** 상태값으로 라벨 가져오기 */ + getStatusLabel: (status: T) => string; + /** 상태값으로 스타일 가져오기 */ + getStatusStyle: (status: T) => string; +} + +/** + * 상태 설정 생성 유틸리티 + * + * @param config - 상태별 설정 객체 + * @param options - 추가 옵션 + * @returns STATUS_OPTIONS, STATUS_LABELS, STATUS_STYLES 및 헬퍼 함수 + */ +export function createStatusConfig( + config: Record, + options: CreateStatusConfigOptions = {} +): StatusConfig { + const { includeAll = false, allLabel = '전체', styleMode = 'badge' } = options; + + const stylePresets = styleMode === 'badge' ? BADGE_STYLE_PRESETS : TEXT_STYLE_PRESETS; + + // STATUS_LABELS 생성 + const STATUS_LABELS = Object.entries(config).reduce( + (acc, [key, value]) => { + acc[key as T] = (value as StatusItemConfig).label; + return acc; + }, + {} as Record + ); + + // STATUS_STYLES 생성 + const STATUS_STYLES = Object.entries(config).reduce( + (acc, [key, value]) => { + const { style } = value as StatusItemConfig; + // 프리셋 스타일이면 변환, 아니면 커스텀 클래스 그대로 사용 + acc[key as T] = + style in stylePresets ? stylePresets[style as StatusStylePreset] : style; + return acc; + }, + {} as Record + ); + + // STATUS_OPTIONS 생성 + const statusOptions = Object.entries(config).map(([key, value]) => ({ + value: key as T, + label: (value as StatusItemConfig).label, + })); + + const STATUS_OPTIONS = includeAll + ? [{ value: 'all' as const, label: allLabel }, ...statusOptions] + : statusOptions; + + // 헬퍼 함수 + const getStatusLabel = (status: T): string => STATUS_LABELS[status] || status; + const getStatusStyle = (status: T): string => STATUS_STYLES[status] || ''; + + return { + STATUS_OPTIONS: STATUS_OPTIONS as ReadonlyArray<{ value: T | 'all'; label: string }>, + STATUS_LABELS, + STATUS_STYLES, + getStatusLabel, + getStatusStyle, + }; +} + +/** + * 프리셋 스타일을 CSS 클래스로 변환하는 헬퍼 + */ +export function getPresetStyle( + preset: StatusStylePreset, + mode: 'badge' | 'text' = 'badge' +): string { + const presets = mode === 'badge' ? BADGE_STYLE_PRESETS : TEXT_STYLE_PRESETS; + return presets[preset] || presets.default; +} + +export default createStatusConfig;