refactor: UI 컴포넌트 추상화 및 입금/출금 등록 버튼 추가
- 입금관리, 출금관리 리스트에 등록 버튼 추가 - skeleton, confirm-dialog, empty-state, status-badge UI 컴포넌트 추가 - document-system 컴포넌트 추상화 (ApprovalLine, DocumentHeader 등) - 여러 페이지 컴포넌트 리팩토링 및 코드 정리 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
546
claudedocs/[PLAN-2026-01-22] ui-component-abstraction.md
Normal file
546
claudedocs/[PLAN-2026-01-22] ui-component-abstraction.md
Normal file
@@ -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);
|
||||
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>삭제 확인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
```
|
||||
|
||||
**개선안:**
|
||||
```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<void>;
|
||||
}
|
||||
|
||||
// 사용 예시
|
||||
<ConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
title="삭제 확인"
|
||||
description="정말 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다."
|
||||
confirmText="삭제"
|
||||
variant="destructive"
|
||||
loading={isLoading}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
```
|
||||
|
||||
**효과:**
|
||||
- 코드량: ~30줄 → ~10줄 (70% 감소)
|
||||
- 일관된 UX 보장
|
||||
- 로딩 상태 자동 처리
|
||||
|
||||
---
|
||||
|
||||
#### 1-2. StatusBadge 컴포넌트 + createStatusConfig 유틸
|
||||
|
||||
**현재 (반복 코드):**
|
||||
```tsx
|
||||
// 80개 파일에서 각각 정의
|
||||
// estimates/types.ts
|
||||
export const STATUS_STYLES: Record<string, string> = {
|
||||
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<string, string> = {
|
||||
pending: '대기',
|
||||
inProgress: '진행중',
|
||||
completed: '완료',
|
||||
};
|
||||
|
||||
// site-management/types.ts (거의 동일)
|
||||
export const SITE_STATUS_STYLES: Record<string, string> = { ... };
|
||||
export const SITE_STATUS_LABELS: Record<string, string> = { ... };
|
||||
```
|
||||
|
||||
**개선안:**
|
||||
```tsx
|
||||
// src/lib/utils/status-config.ts
|
||||
export type StatusVariant = 'default' | 'success' | 'warning' | 'error' | 'info';
|
||||
|
||||
export interface StatusConfig<T extends string> {
|
||||
value: T;
|
||||
label: string;
|
||||
variant: StatusVariant;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function createStatusConfig<T extends string>(
|
||||
configs: StatusConfig<T>[]
|
||||
): {
|
||||
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<T extends string> {
|
||||
status: T;
|
||||
config: ReturnType<typeof createStatusConfig<T>>;
|
||||
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' },
|
||||
]);
|
||||
|
||||
// 컴포넌트에서
|
||||
<StatusBadge status={data.status} config={estimateStatusConfig} />
|
||||
```
|
||||
|
||||
**효과:**
|
||||
- 타입 안전성 강화
|
||||
- 일관된 색상 체계
|
||||
- options 자동 생성 (Select용)
|
||||
|
||||
---
|
||||
|
||||
#### 1-3. EmptyState 컴포넌트
|
||||
|
||||
**현재 (반복 코드):**
|
||||
```tsx
|
||||
// 70개 파일에서 다양한 형태로 반복
|
||||
{data.length === 0 && (
|
||||
<div className="text-center py-10 text-muted-foreground">
|
||||
데이터가 없습니다
|
||||
</div>
|
||||
)}
|
||||
|
||||
// 또는
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="text-center py-8">
|
||||
등록된 항목이 없습니다
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
```
|
||||
|
||||
**개선안:**
|
||||
```tsx
|
||||
// src/components/ui/empty-state.tsx
|
||||
interface EmptyStateProps {
|
||||
icon?: ReactNode;
|
||||
title?: string;
|
||||
description?: string;
|
||||
action?: ReactNode;
|
||||
variant?: 'default' | 'table' | 'card' | 'minimal';
|
||||
}
|
||||
|
||||
// 사용 예시
|
||||
<EmptyState
|
||||
icon={<FileX className="w-12 h-12" />}
|
||||
title="데이터가 없습니다"
|
||||
description="새로운 항목을 등록하거나 검색 조건을 변경해보세요."
|
||||
action={<Button onClick={onCreate}>등록하기</Button>}
|
||||
/>
|
||||
|
||||
// 테이블 내 사용
|
||||
<EmptyState variant="table" colSpan={10} title="검색 결과가 없습니다" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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 (
|
||||
<ListPageSkeleton
|
||||
hasFilters
|
||||
filterCount={4}
|
||||
hasDateRange
|
||||
rowCount={10}
|
||||
columnCount={8}
|
||||
hasActions
|
||||
hasPagination
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**2. CardGridSkeleton (카드 그리드용)**
|
||||
```tsx
|
||||
interface CardGridSkeletonProps {
|
||||
cardCount?: number;
|
||||
cols?: 1 | 2 | 3 | 4;
|
||||
cardHeight?: 'sm' | 'md' | 'lg';
|
||||
hasImage?: boolean;
|
||||
hasFooter?: boolean;
|
||||
}
|
||||
|
||||
// 대시보드 카드, 칸반 보드 등에 사용
|
||||
<CardGridSkeleton cardCount={6} cols={3} cardHeight="md" />
|
||||
```
|
||||
|
||||
**3. TableSkeleton (테이블용)**
|
||||
```tsx
|
||||
interface TableSkeletonProps {
|
||||
rowCount?: number;
|
||||
columnCount?: number;
|
||||
hasCheckbox?: boolean;
|
||||
hasActions?: boolean;
|
||||
columnWidths?: string[]; // ['w-12', 'w-32', 'flex-1', ...]
|
||||
}
|
||||
|
||||
<TableSkeleton rowCount={10} columnCount={8} hasCheckbox hasActions />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 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('');
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Input type="date" value={startDate} onChange={...} />
|
||||
<span>~</span>
|
||||
<Input type="date" value={endDate} onChange={...} />
|
||||
</div>
|
||||
```
|
||||
|
||||
**개선안:**
|
||||
```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;
|
||||
}
|
||||
|
||||
// 사용 예시
|
||||
<DateRangeFilter
|
||||
value={{ start: startDate, end: endDate }}
|
||||
onChange={({ start, end }) => {
|
||||
setStartDate(start);
|
||||
setEndDate(end);
|
||||
}}
|
||||
presets={['today', 'week', 'month']}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3-2. LoadingButton 컴포넌트
|
||||
|
||||
**현재 (반복 코드):**
|
||||
```tsx
|
||||
// 59개 파일에서 반복
|
||||
<Button disabled={isLoading}>
|
||||
{isLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
저장
|
||||
</Button>
|
||||
```
|
||||
|
||||
**개선안:**
|
||||
```tsx
|
||||
// src/components/ui/loading-button.tsx
|
||||
interface LoadingButtonProps extends ButtonProps {
|
||||
loading?: boolean;
|
||||
loadingText?: string;
|
||||
spinnerPosition?: 'left' | 'right';
|
||||
}
|
||||
|
||||
// 사용 예시
|
||||
<LoadingButton loading={isLoading} loadingText="저장 중...">
|
||||
저장
|
||||
</LoadingButton>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**다음 단계**: 위 결정 사항에 대한 의견 확정 후 구현 시작
|
||||
@@ -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<string | null>(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() {
|
||||
<Pencil className="h-4 w-4 mr-1" />
|
||||
수정
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm">
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
삭제
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>게시글 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
이 게시글을 삭제하시겠습니까? 삭제된 게시글은 복구할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeletePost}>삭제</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<Button variant="destructive" size="sm" onClick={() => setShowDeleteDialog(true)}>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -406,6 +383,15 @@ export default function DynamicBoardDetailPage() {
|
||||
목록으로
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 게시글 삭제 확인 다이얼로그 */}
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleDeletePost}
|
||||
title="게시글 삭제"
|
||||
description="이 게시글을 삭제하시겠습니까? 삭제된 게시글은 복구할 수 없습니다."
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
)}
|
||||
/>
|
||||
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>사원 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{employee.name}" 사원을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 사원 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? '삭제 중...' : '삭제'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onConfirm={confirmDelete}
|
||||
title="사원 삭제"
|
||||
description={
|
||||
<>
|
||||
"{employee.name}" 사원을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 사원 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
loading={isDeleting}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-white p-8 w-full text-sm shadow-sm print:shadow-none">
|
||||
{/* 헤더 */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-2xl font-bold">KD</div>
|
||||
<div className="text-xs text-gray-600">경동기업<br/>KYUNGDONG COMPANY</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-xl font-bold">절곡품</div>
|
||||
<div className="text-xl font-bold tracking-[0.2rem]">중간검사 성적서</div>
|
||||
</div>
|
||||
<table className="text-xs border-collapse">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-8 text-center" rowSpan={3}>
|
||||
<div className="flex flex-col items-center">
|
||||
<span>결</span><span>재</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">작성</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">검토</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">승인</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center font-medium">{data.approvers.writer}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.reviewer}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.approver}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">판매/전진</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">생산</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">품질</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="절곡품"
|
||||
subtitle="중간검사 성적서"
|
||||
layout="quality"
|
||||
logo={{ text: 'KD', subtext: '경동기업\nKYUNGDONG COMPANY' }}
|
||||
customApproval={
|
||||
<QualityApprovalTable
|
||||
type="4col"
|
||||
approvers={data.approvers}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 기본 정보 테이블 */}
|
||||
<table className="w-full border-collapse mb-4 text-xs">
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-white p-8 w-full text-sm shadow-sm print:shadow-none">
|
||||
{/* 헤더 */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-2xl font-bold">KD</div>
|
||||
<div className="text-xs text-gray-600">경동기업</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold tracking-[0.3rem]">수 입 검 사 성 적 서</div>
|
||||
<div className="text-right">
|
||||
<table className="text-xs border-collapse">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-12">담당</td>
|
||||
<td className="border border-gray-400 px-2 py-1 w-16">부서장</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100">결재</td>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8">{data.approvers.writer}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="text-xs text-right mt-1">접고일자: {data.reportDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="수 입 검 사 성 적 서"
|
||||
logo={{ text: 'KD', subtext: '경동기업' }}
|
||||
customApproval={
|
||||
<QualityApprovalTable
|
||||
type="2col"
|
||||
approvers={{ writer: data.approvers.writer }}
|
||||
reportDate={data.reportDate}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 기본 정보 테이블 */}
|
||||
<table className="w-full border-collapse mb-4 text-xs">
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-white p-8 w-full text-sm shadow-sm print:shadow-none">
|
||||
{/* 헤더 */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-2xl font-bold">KD</div>
|
||||
<div className="text-xs text-gray-600">경동기업<br/>KYUNGDONG COMPANY</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-xl font-bold">조인트바</div>
|
||||
<div className="text-xl font-bold tracking-[0.2rem]">중간검사 성적서</div>
|
||||
</div>
|
||||
<table className="text-xs border-collapse">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-8 text-center" rowSpan={3}>
|
||||
<div className="flex flex-col items-center">
|
||||
<span>결</span><span>재</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">작성</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">검토</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">승인</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center font-medium">{data.approvers.writer}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.reviewer}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.approver}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">판매/전진</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">생산</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">품질</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="조인트바"
|
||||
subtitle="중간검사 성적서"
|
||||
layout="quality"
|
||||
logo={{ text: 'KD', subtext: '경동기업\nKYUNGDONG COMPANY' }}
|
||||
customApproval={
|
||||
<QualityApprovalTable
|
||||
type="4col"
|
||||
approvers={data.approvers}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 기본 정보 테이블 */}
|
||||
<table className="w-full border-collapse mb-4 text-xs">
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-white p-8 w-full text-sm shadow-sm print:shadow-none">
|
||||
{/* 헤더 */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-2xl font-bold">KD</div>
|
||||
<div className="text-xs text-gray-600">경동기업<br/>KYUNGDONG COMPANY</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold tracking-[0.3rem]">제 품 검 사 성 적 서</div>
|
||||
<table className="text-xs border-collapse">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-8 text-center" rowSpan={3}>
|
||||
<div className="flex flex-col items-center">
|
||||
<span>결</span><span>재</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">작성</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">검토</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">승인</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.writer}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.reviewer}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.approver}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">판매/전진</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">생산</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">품질</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="제 품 검 사 성 적 서"
|
||||
logo={{ text: 'KD', subtext: '경동기업\nKYUNGDONG COMPANY' }}
|
||||
customApproval={
|
||||
<QualityApprovalTable
|
||||
type="4col"
|
||||
approvers={data.approvers}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 기본 정보 테이블 */}
|
||||
<table className="w-full border-collapse mb-4 text-xs">
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-white p-8 w-full text-sm shadow-sm print:shadow-none">
|
||||
{/* 헤더 */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-2xl font-bold">KD</div>
|
||||
<div className="text-xs text-gray-600">경동기업<br/>KYUNGDONG COMPANY</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-xl font-bold">스크린</div>
|
||||
<div className="text-xl font-bold tracking-[0.2rem]">중간검사 성적서</div>
|
||||
</div>
|
||||
<table className="text-xs border-collapse">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-8 text-center" rowSpan={3}>
|
||||
<div className="flex flex-col items-center">
|
||||
<span>결</span><span>재</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">작성</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">검토</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">승인</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center font-medium">{data.approvers.writer}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.reviewer}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.approver}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">판매/전진</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">생산</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">품질</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="스크린"
|
||||
subtitle="중간검사 성적서"
|
||||
layout="quality"
|
||||
logo={{ text: 'KD', subtext: '경동기업\nKYUNGDONG COMPANY' }}
|
||||
customApproval={
|
||||
<QualityApprovalTable
|
||||
type="4col"
|
||||
approvers={data.approvers}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 기본 정보 테이블 */}
|
||||
<table className="w-full border-collapse mb-4 text-xs">
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-white p-8 w-full text-sm shadow-sm print:shadow-none">
|
||||
{/* 헤더 */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-2xl font-bold">KD</div>
|
||||
<div className="text-xs text-gray-600">경동기업<br/>KYUNGDONG COMPANY</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-xl font-bold">슬랫</div>
|
||||
<div className="text-xl font-bold tracking-[0.2rem]">중간검사 성적서</div>
|
||||
</div>
|
||||
<table className="text-xs border-collapse">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-8 text-center" rowSpan={3}>
|
||||
<div className="flex flex-col items-center">
|
||||
<span>결</span><span>재</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">작성</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">검토</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">승인</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center font-medium">{data.approvers.writer}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.reviewer}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.approver}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">판매/전진</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">생산</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">품질</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="슬랫"
|
||||
subtitle="중간검사 성적서"
|
||||
layout="quality"
|
||||
logo={{ text: 'KD', subtext: '경동기업\nKYUNGDONG COMPANY' }}
|
||||
customApproval={
|
||||
<QualityApprovalTable
|
||||
type="4col"
|
||||
approvers={data.approvers}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 기본 정보 테이블 */}
|
||||
<table className="w-full border-collapse mb-4 text-xs">
|
||||
|
||||
@@ -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: () => (
|
||||
<>
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>거래처 삭제 확인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{deleteTargetId
|
||||
? `거래처: ${clients.find((c) => c.id === deleteTargetId)?.name || deleteTargetId}`
|
||||
: ""}
|
||||
<br />
|
||||
이 거래처를 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmDelete}>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={isDeleteDialogOpen}
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="거래처 삭제 확인"
|
||||
description={
|
||||
<>
|
||||
{deleteTargetId
|
||||
? `거래처: ${clients.find((c) => c.id === deleteTargetId)?.name || deleteTargetId}`
|
||||
: ""}
|
||||
<br />
|
||||
이 거래처를 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 일괄 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog
|
||||
<DeleteConfirmDialog
|
||||
open={isBulkDeleteDialogOpen}
|
||||
onOpenChange={setIsBulkDeleteDialogOpen}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>일괄 삭제 확인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
선택한 {selectedItems.size}개의 거래처를 삭제하시겠습니까?
|
||||
<br />
|
||||
onConfirm={handleConfirmBulkDelete}
|
||||
title="일괄 삭제 확인"
|
||||
description={
|
||||
<>
|
||||
선택한 <strong>{selectedItems.size}개</strong>의 거래처를 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
삭제된 데이터는 복구할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmBulkDelete}>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -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: () => (
|
||||
<>
|
||||
{/* 수주 취소 확인 다이얼로그 */}
|
||||
<AlertDialog open={isCancelDialogOpen} onOpenChange={setIsCancelDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>수주 취소 확인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{cancelTargetId
|
||||
? `수주번호: ${orders.find((o) => o.id === cancelTargetId)?.lotNumber || cancelTargetId}`
|
||||
: ""}
|
||||
<br />
|
||||
이 수주를 취소하시겠습니까? 취소된 수주는 복구할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>닫기</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmCancel} className="bg-orange-600 hover:bg-orange-700">
|
||||
취소 확정
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmDialog
|
||||
open={isCancelDialogOpen}
|
||||
onOpenChange={setIsCancelDialogOpen}
|
||||
onConfirm={handleConfirmCancel}
|
||||
variant="warning"
|
||||
title="수주 취소 확인"
|
||||
description={
|
||||
<>
|
||||
{cancelTargetId
|
||||
? `수주번호: ${orders.find((o) => o.id === cancelTargetId)?.lotNumber || cancelTargetId}`
|
||||
: ""}
|
||||
<br />
|
||||
이 수주를 취소하시겠습니까? 취소된 수주는 복구할 수 없습니다.
|
||||
</>
|
||||
}
|
||||
confirmText="취소 확정"
|
||||
/>
|
||||
|
||||
{/* 수주 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogContent className="max-w-md">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2">
|
||||
<span className="text-yellow-600">⚠️</span>
|
||||
삭제 확인
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="space-y-4">
|
||||
<p className="text-foreground">
|
||||
선택한 <strong>{deleteTargetIds.length}개</strong>의 수주를 삭제하시겠습니까?
|
||||
</p>
|
||||
<div className="bg-gray-100 rounded-lg p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-yellow-600 mt-0.5">⚠️</span>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">주의</span>
|
||||
<br />
|
||||
삭제된 수주는 복구할 수 없습니다. 관련된 작업지시, 출하정보도 함께 삭제될 수 있습니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
disabled={isDeleting}
|
||||
className="bg-gray-900 hover:bg-gray-800 text-white gap-2"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
{isDeleting ? "삭제 중..." : "삭제"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={isDeleteDialogOpen}
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="삭제 확인"
|
||||
description={
|
||||
<>
|
||||
선택한 <strong>{deleteTargetIds.length}개</strong>의 수주를 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
삭제된 수주는 복구할 수 없습니다. 관련된 작업지시, 출하정보도 함께 삭제될 수 있습니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
loading={isDeleting}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
|
||||
{/* 팝업1: 작업지시 생성 확인 다이얼로그 */}
|
||||
<AlertDialog
|
||||
<ConfirmDialog
|
||||
open={isCreateWorkOrderDialogOpen}
|
||||
onOpenChange={setIsCreateWorkOrderDialogOpen}
|
||||
>
|
||||
<AlertDialogContent className="max-w-md">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2">
|
||||
<Play className="h-5 w-5" />
|
||||
작업지시서 자동 생성
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="space-y-4 pt-2">
|
||||
<p className="font-medium text-foreground">
|
||||
다음 공정에 대한 작업지시서가 생성됩니다:
|
||||
</p>
|
||||
{/* 공정 목록 (실제로는 API에서 받아온 데이터) */}
|
||||
{productionOrder?.pendingWorkOrderCount && productionOrder.pendingWorkOrderCount > 0 && (
|
||||
<ul className="space-y-1 text-sm text-muted-foreground pl-4">
|
||||
{SAMPLE_PROCESSES.slice(0, productionOrder.pendingWorkOrderCount).map((process) => (
|
||||
<li key={process.id} className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-gray-400 rounded-full" />
|
||||
{process.name} ({process.quantity}개)
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<p className="text-muted-foreground">
|
||||
생성된 작업지시서는 생산팀에서 확인하고 작업을 진행할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isCreating}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmCreateWorkOrder}
|
||||
disabled={isCreating}
|
||||
className="gap-2"
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
{isCreating ? "생성 중..." : "작업지시 생성"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
onConfirm={handleConfirmCreateWorkOrder}
|
||||
title="작업지시서 자동 생성"
|
||||
description={
|
||||
<div className="space-y-4 pt-2">
|
||||
<p className="font-medium text-foreground">
|
||||
다음 공정에 대한 작업지시서가 생성됩니다:
|
||||
</p>
|
||||
{productionOrder?.pendingWorkOrderCount && productionOrder.pendingWorkOrderCount > 0 && (
|
||||
<ul className="space-y-1 text-sm text-muted-foreground pl-4">
|
||||
{SAMPLE_PROCESSES.slice(0, productionOrder.pendingWorkOrderCount).map((process) => (
|
||||
<li key={process.id} className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-gray-400 rounded-full" />
|
||||
{process.name} ({process.quantity}개)
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<p className="text-muted-foreground">
|
||||
생성된 작업지시서는 생산팀에서 확인하고 작업을 진행할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
confirmText="작업지시 생성"
|
||||
loading={isCreating}
|
||||
/>
|
||||
|
||||
{/* 팝업2: 작업지시 생성 성공 다이얼로그 */}
|
||||
<AlertDialog open={isSuccessDialogOpen} onOpenChange={setIsSuccessDialogOpen}>
|
||||
|
||||
@@ -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: () => (
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent className="max-w-md">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2">
|
||||
<span className="text-yellow-600">⚠️</span>
|
||||
삭제 확인
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="space-y-4">
|
||||
<p className="text-foreground">
|
||||
선택한 <strong>{deleteCount}개</strong>의 항목을 삭제하시겠습니까?
|
||||
</p>
|
||||
<div className="bg-gray-100 rounded-lg p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-yellow-600 mt-0.5">⚠️</span>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">주의</span>
|
||||
<br />
|
||||
삭제된 항목은 복구할 수 없습니다. 관련된 데이터도 함께 삭제될 수 있습니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-gray-900 hover:bg-gray-800 text-white gap-2"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="삭제 확인"
|
||||
description={
|
||||
<>
|
||||
선택한 <strong>{deleteCount}개</strong>의 항목을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
삭제된 항목은 복구할 수 없습니다. 관련된 데이터도 함께 삭제될 수 있습니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>악성채권 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
'{formData.vendorName}'의 악성채권 기록을 삭제하시겠습니까?
|
||||
<br />
|
||||
확인 클릭 시 목록으로 이동합니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="악성채권 삭제"
|
||||
description={
|
||||
<>
|
||||
'{formData.vendorName}'의 악성채권 기록을 삭제하시겠습니까?
|
||||
<br />
|
||||
확인 클릭 시 목록으로 이동합니다.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 저장 확인 다이얼로그 */}
|
||||
<AlertDialog open={showSaveDialog} onOpenChange={setShowSaveDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>저장 확인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
입력한 내용을 저장하시겠습니까?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmSave}
|
||||
className="bg-blue-500 hover:bg-blue-600"
|
||||
>
|
||||
저장
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<SaveConfirmDialog
|
||||
open={showSaveDialog}
|
||||
onOpenChange={setShowSaveDialog}
|
||||
onConfirm={handleConfirmSave}
|
||||
description="입력한 내용을 저장하시겠습니까?"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
}}
|
||||
/>
|
||||
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>어음 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
이 어음을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="어음 삭제"
|
||||
description="이 어음을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다."
|
||||
loading={isLoading}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
</Card>
|
||||
|
||||
{/* ===== 삭제 확인 다이얼로그 ===== */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>입금 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
이 입금 내역을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? '삭제중...' : '삭제'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleDelete}
|
||||
title="입금 삭제"
|
||||
description="이 입금 내역을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다."
|
||||
loading={isLoading}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: () => (
|
||||
<Button className="ml-auto" onClick={() => router.push('/ko/accounting/deposits/new')}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
입금등록
|
||||
</Button>
|
||||
),
|
||||
|
||||
// Stats 카드
|
||||
computeStats: (): StatCard[] => [
|
||||
{ label: '총 입금', value: `${stats.totalDeposit.toLocaleString()}원`, icon: Banknote, iconColor: 'text-blue-500' },
|
||||
|
||||
@@ -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({
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>지출 예상 내역 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
이 지출 예상 내역을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="지출 예상 내역 삭제"
|
||||
description="이 지출 예상 내역을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다."
|
||||
/>
|
||||
|
||||
{/* 예상 지급일 변경 다이얼로그 */}
|
||||
<Dialog open={showDateChangeDialog} onOpenChange={setShowDateChangeDialog}>
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>거래처 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
이 거래처를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? '삭제 중...' : '삭제'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="거래처 삭제"
|
||||
description="이 거래처를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다."
|
||||
loading={isLoading}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
</Card>
|
||||
|
||||
{/* ===== 삭제 확인 다이얼로그 ===== */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>출금 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
이 출금 내역을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? '삭제중...' : '삭제'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleDelete}
|
||||
title="출금 삭제"
|
||||
description="이 출금 내역을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다."
|
||||
loading={isLoading}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: () => (
|
||||
<Button className="ml-auto" onClick={() => router.push('/ko/accounting/withdrawals/new')}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
출금등록
|
||||
</Button>
|
||||
),
|
||||
|
||||
// 커스텀 필터 함수
|
||||
customFilterFn: (items) => {
|
||||
return items.filter((item) => {
|
||||
|
||||
@@ -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: () => (
|
||||
<>
|
||||
{/* 승인 확인 다이얼로그 */}
|
||||
<AlertDialog open={approveDialogOpen} onOpenChange={setApproveDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>결재 승인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말 {pendingSelectedItems.size}건을 승인하시겠습니까?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleApproveConfirm}>승인</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmDialog
|
||||
open={approveDialogOpen}
|
||||
onOpenChange={setApproveDialogOpen}
|
||||
onConfirm={handleApproveConfirm}
|
||||
title="결재 승인"
|
||||
description={`정말 ${pendingSelectedItems.size}건을 승인하시겠습니까?`}
|
||||
variant="success"
|
||||
confirmText="승인"
|
||||
/>
|
||||
|
||||
{/* 반려 확인 다이얼로그 */}
|
||||
<AlertDialog open={rejectDialogOpen} onOpenChange={setRejectDialogOpen}>
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-white p-8 min-h-full">
|
||||
{/* 문서 헤더 */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl font-bold text-center mb-2">지출 예상 내역서</h1>
|
||||
<p className="text-sm text-gray-600 text-center">
|
||||
문서번호: {data.documentNo} | 작성일자: {data.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<ApprovalLineBox drafter={data.drafter} approvers={data.approvers} />
|
||||
</div>
|
||||
</div>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="지출 예상 내역서"
|
||||
subtitle={`문서번호: ${data.documentNo} | 작성일자: ${data.createdAt}`}
|
||||
layout="centered"
|
||||
customApproval={<ApprovalLineBox drafter={data.drafter} approvers={data.approvers} />}
|
||||
/>
|
||||
|
||||
{/* 문서 내용 */}
|
||||
<div className="border border-gray-300">
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-white p-8 min-h-full">
|
||||
{/* 문서 헤더 */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl font-bold text-center mb-2">지출결의서</h1>
|
||||
<p className="text-sm text-gray-600 text-center">
|
||||
문서번호: {data.documentNo} | 작성일자: {data.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<ApprovalLineBox drafter={data.drafter} approvers={data.approvers} />
|
||||
</div>
|
||||
</div>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="지출결의서"
|
||||
subtitle={`문서번호: ${data.documentNo} | 작성일자: ${data.createdAt}`}
|
||||
layout="centered"
|
||||
customApproval={<ApprovalLineBox drafter={data.drafter} approvers={data.approvers} />}
|
||||
/>
|
||||
|
||||
{/* 문서 내용 */}
|
||||
<div className="border border-gray-300">
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-white p-8 min-h-full">
|
||||
{/* 문서 헤더 */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl font-bold text-center mb-2">품의서</h1>
|
||||
<p className="text-sm text-gray-600 text-center">
|
||||
문서번호: {data.documentNo} | 작성일자: {data.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<ApprovalLineBox drafter={data.drafter} approvers={data.approvers} />
|
||||
</div>
|
||||
</div>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="품의서"
|
||||
subtitle={`문서번호: ${data.documentNo} | 작성일자: ${data.createdAt}`}
|
||||
layout="centered"
|
||||
customApproval={<ApprovalLineBox drafter={data.drafter} approvers={data.approvers} />}
|
||||
/>
|
||||
|
||||
{/* 문서 내용 */}
|
||||
<div className="border border-gray-300">
|
||||
|
||||
@@ -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: () => (
|
||||
<>
|
||||
{/* 열람 처리 확인 다이얼로그 */}
|
||||
<AlertDialog open={markReadDialogOpen} onOpenChange={setMarkReadDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>열람 처리</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말 {selectedItems.size}건을 열람 처리하시겠습니까?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleMarkReadConfirm}>
|
||||
확인
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmDialog
|
||||
open={markReadDialogOpen}
|
||||
onOpenChange={setMarkReadDialogOpen}
|
||||
onConfirm={handleMarkReadConfirm}
|
||||
title="열람 처리"
|
||||
description={`정말 ${selectedItems.size}건을 열람 처리하시겠습니까?`}
|
||||
/>
|
||||
|
||||
{/* 미열람 처리 확인 다이얼로그 */}
|
||||
<AlertDialog open={markUnreadDialogOpen} onOpenChange={setMarkUnreadDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>미열람 처리</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말 {selectedItems.size}건을 미열람 처리하시겠습니까?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleMarkUnreadConfirm}>
|
||||
확인
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmDialog
|
||||
open={markUnreadDialogOpen}
|
||||
onOpenChange={setMarkUnreadDialogOpen}
|
||||
onConfirm={handleMarkUnreadConfirm}
|
||||
title="미열람 처리"
|
||||
description={`정말 ${selectedItems.size}건을 미열람 처리하시겠습니까?`}
|
||||
/>
|
||||
|
||||
{/* 문서 상세 모달 */}
|
||||
{selectedDocument && (
|
||||
|
||||
@@ -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 }:
|
||||
)}
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>게시글 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? '삭제 중...' : '삭제'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="게시글 삭제"
|
||||
description="정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
|
||||
loading={isDeleting}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: () => (
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>게시글 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>정말 삭제하시겠습니까?</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="게시글 삭제"
|
||||
description="정말 삭제하시겠습니까?"
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>게시판 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{boardData.boardName}" 게시판을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 게시판 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
삭제 중...
|
||||
</>
|
||||
) : (
|
||||
'삭제'
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onConfirm={confirmDelete}
|
||||
title="게시판 삭제"
|
||||
description={
|
||||
<>
|
||||
"{boardData.boardName}" 게시판을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 게시판 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
loading={isDeleting}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>댓글 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말 삭제하시겠습니까?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="댓글 삭제"
|
||||
description="정말 삭제하시겠습니까?"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>카테고리 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{categoryToDelete?.name}" 카테고리를 삭제하시겠습니까?
|
||||
{categoryToDelete?.isDefault && (
|
||||
<>
|
||||
<br />
|
||||
<span className="text-destructive font-medium">
|
||||
기본 카테고리는 삭제가 불가합니다.
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isSubmitting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
disabled={isSubmitting || categoryToDelete?.isDefault}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : null}
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onConfirm={confirmDelete}
|
||||
title="카테고리 삭제"
|
||||
description={
|
||||
<>
|
||||
"{categoryToDelete?.name}" 카테고리를 삭제하시겠습니까?
|
||||
{categoryToDelete?.isDefault && (
|
||||
<>
|
||||
<br />
|
||||
<span className="text-destructive font-medium">
|
||||
기본 카테고리는 삭제가 불가합니다.
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
loading={isSubmitting}
|
||||
disabled={categoryToDelete?.isDefault}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>견적 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
이 견적을 삭제하시겠습니까?
|
||||
<br />
|
||||
삭제된 견적은 복구할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="견적 삭제"
|
||||
description={
|
||||
<>
|
||||
이 견적을 삭제하시겠습니까?
|
||||
<br />
|
||||
삭제된 견적은 복구할 수 없습니다.
|
||||
</>
|
||||
}
|
||||
loading={isLoading}
|
||||
/>
|
||||
|
||||
{/* 저장 확인 다이얼로그 */}
|
||||
<AlertDialog open={showSaveDialog} onOpenChange={setShowSaveDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>수정 확인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
견적 정보를 수정하시겠습니까?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmSave}
|
||||
className="bg-blue-500 hover:bg-blue-600"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
확인
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<SaveConfirmDialog
|
||||
open={showSaveDialog}
|
||||
onOpenChange={setShowSaveDialog}
|
||||
onConfirm={handleConfirmSave}
|
||||
title="수정 확인"
|
||||
description="견적 정보를 수정하시겠습니까?"
|
||||
loading={isLoading}
|
||||
/>
|
||||
|
||||
{/* 입찰 등록 확인 다이얼로그 */}
|
||||
<AlertDialog open={showBiddingDialog} onOpenChange={setShowBiddingDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>입찰 등록</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
이 견적을 입찰로 등록하시겠습니까?
|
||||
<br />
|
||||
견적 정보가 입찰 관리로 전환됩니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmBidding}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
등록
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmDialog
|
||||
open={showBiddingDialog}
|
||||
onOpenChange={setShowBiddingDialog}
|
||||
onConfirm={handleConfirmBidding}
|
||||
title="입찰 등록"
|
||||
description={
|
||||
<>
|
||||
이 견적을 입찰로 등록하시겠습니까?
|
||||
<br />
|
||||
견적 정보가 입찰 관리로 전환됩니다.
|
||||
</>
|
||||
}
|
||||
variant="success"
|
||||
confirmText="등록"
|
||||
loading={isLoading}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<div className="p-8">
|
||||
{/* 상단: 제목 + 결재란 */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
{/* 좌측: 제목 및 문서정보 */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-2">인수인계보고서</h1>
|
||||
<div className="text-sm text-gray-600">
|
||||
문서번호: {report.reportNumber} | 작성일자: {formatDate(report.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우측: 결재란 */}
|
||||
<table className="border-collapse border border-gray-300 text-sm">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th rowSpan={3} className="border border-gray-300 px-2 py-1 bg-gray-50 text-center w-8 align-middle">
|
||||
<span className="writing-vertical">결<br />재</span>
|
||||
</th>
|
||||
<th className="border border-gray-300 px-3 py-1 bg-gray-50 text-center w-16">작성</th>
|
||||
<th className="border border-gray-300 px-3 py-1 bg-gray-50 text-center w-16">승인</th>
|
||||
<th className="border border-gray-300 px-3 py-1 bg-gray-50 text-center w-16">승인</th>
|
||||
<th className="border border-gray-300 px-3 py-1 bg-gray-50 text-center w-16">승인</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-300 px-3 py-2 text-center h-10">홍길동</td>
|
||||
<td className="border border-gray-300 px-3 py-2 text-center h-10">이름</td>
|
||||
<td className="border border-gray-300 px-3 py-2 text-center h-10">이름</td>
|
||||
<td className="border border-gray-300 px-3 py-2 text-center h-10">이름</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500">부서명</td>
|
||||
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500">부서명</td>
|
||||
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500">부서명</td>
|
||||
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500">부서명</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* 상단: 제목 + 결재란 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="인수인계보고서"
|
||||
documentCode={report.reportNumber}
|
||||
subtitle={`작성일자: ${formatDate(report.createdAt)}`}
|
||||
layout="construction"
|
||||
customApproval={
|
||||
<ConstructionApprovalTable
|
||||
approvers={{
|
||||
writer: { name: '홍길동', department: '부서명' },
|
||||
approver1: { name: '이름', department: '부서명' },
|
||||
approver2: { name: '이름', department: '부서명' },
|
||||
approver3: { name: '이름', department: '부서명' },
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 통합 테이블 - 기획서 구조 100% 반영 */}
|
||||
<table className="w-full border-collapse border border-gray-300 text-sm">
|
||||
|
||||
@@ -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({
|
||||
<UniversalListPage config={config} initialData={initialData} />
|
||||
|
||||
{/* 철회 확인 다이얼로그 */}
|
||||
<AlertDialog open={withdrawDialogOpen} onOpenChange={setWithdrawDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>이슈 철회</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
선택한 {itemsToWithdraw.size}건의 이슈를 철회하시겠습니까?
|
||||
<br />
|
||||
철회된 이슈는 복구할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleWithdraw}
|
||||
className="bg-orange-600 hover:bg-orange-700"
|
||||
>
|
||||
철회
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmDialog
|
||||
open={withdrawDialogOpen}
|
||||
onOpenChange={setWithdrawDialogOpen}
|
||||
onConfirm={handleWithdraw}
|
||||
variant="warning"
|
||||
title="이슈 철회"
|
||||
description={
|
||||
<>
|
||||
선택한 <strong>{itemsToWithdraw.size}건</strong>의 이슈를 철회하시겠습니까?
|
||||
<br />
|
||||
철회된 이슈는 복구할 수 없습니다.
|
||||
</>
|
||||
}
|
||||
confirmText="철회"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: () => (
|
||||
<>
|
||||
{/* 단일 삭제 다이얼로그 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>품목 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
선택한 품목을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeleteConfirm}>삭제</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
description="선택한 품목을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
|
||||
loading={isLoading}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
/>
|
||||
|
||||
{/* 일괄 삭제 다이얼로그 */}
|
||||
<AlertDialog open={bulkDeleteDialogOpen} onOpenChange={setBulkDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>품목 일괄 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
선택한 {selectedItems.size}개 품목을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleBulkDeleteConfirm}>삭제</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={bulkDeleteDialogOpen}
|
||||
onOpenChange={setBulkDeleteDialogOpen}
|
||||
description={`선택한 ${selectedItems.size}개 품목을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.`}
|
||||
loading={isLoading}
|
||||
onConfirm={handleBulkDeleteConfirm}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
</PageLayout>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>노임 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
이 노임을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete}>삭제</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
description="이 노임을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다."
|
||||
loading={isLoading}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
{/* 시공 완료 확인 다이얼로그 (특수 기능) */}
|
||||
<AlertDialog open={showCompleteDialog} onOpenChange={setShowCompleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>시공 완료</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
시공을 완료하시겠습니까? 완료 후에는 상태를 변경할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleComplete}>완료</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmDialog
|
||||
open={showCompleteDialog}
|
||||
onOpenChange={setShowCompleteDialog}
|
||||
title="시공 완료"
|
||||
description="시공을 완료하시겠습니까? 완료 후에는 상태를 변경할 수 없습니다."
|
||||
confirmText="완료"
|
||||
variant="warning"
|
||||
onConfirm={handleComplete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
{/* 저장 확인 다이얼로그 */}
|
||||
<AlertDialog open={showSaveDialog} onOpenChange={onSaveDialogChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>저장 확인</AlertDialogTitle>
|
||||
<AlertDialogDescription>변경사항을 저장하시겠습니까?</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onConfirmSave} disabled={isLoading}>
|
||||
저장
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<SaveConfirmDialog
|
||||
open={showSaveDialog}
|
||||
onOpenChange={onSaveDialogChange}
|
||||
loading={isLoading}
|
||||
onConfirm={onConfirmSave}
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={onDeleteDialogChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>발주 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
이 발주를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onConfirmDelete}
|
||||
disabled={isLoading}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={onDeleteDialogChange}
|
||||
description="이 발주를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
|
||||
loading={isLoading}
|
||||
onConfirm={onConfirmDelete}
|
||||
/>
|
||||
|
||||
{/* 카테고리 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog
|
||||
<DeleteConfirmDialog
|
||||
open={!!showCategoryDeleteDialog}
|
||||
onOpenChange={() => onCategoryDeleteDialogChange(null)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>발주 상세 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
이 발주 상세를 삭제하시겠습니까? 해당 카테고리의 모든 품목이 삭제됩니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onConfirmDeleteCategory}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
description="이 발주 상세를 삭제하시겠습니까? 해당 카테고리의 모든 품목이 삭제됩니다."
|
||||
onConfirm={onConfirmDeleteCategory}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<div className="p-8">
|
||||
{/* 상단: 제목 */}
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-bold mb-2">발주서</h1>
|
||||
<div className="text-sm text-gray-600">
|
||||
문서번호: {order.orderNumber} | 작성일자: {formatDate(order.orderDate)}
|
||||
</div>
|
||||
</div>
|
||||
{/* 상단: 제목 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="발주서"
|
||||
documentCode={order.orderNumber}
|
||||
subtitle={`작성일자: ${formatDate(order.orderDate)}`}
|
||||
layout="simple"
|
||||
/>
|
||||
|
||||
{/* 기본 정보 테이블 */}
|
||||
<table className="w-full border-collapse border border-gray-300 text-sm mb-8">
|
||||
|
||||
@@ -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
|
||||
</Card>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>단가 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
이 단가를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete}>삭제</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
description="이 단가를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
|
||||
loading={isLoading}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<table className={cn('border-collapse text-xs', className)}>
|
||||
@@ -84,15 +99,15 @@ export function ApprovalLine({
|
||||
</div>
|
||||
</td>
|
||||
<td className="w-16 p-2 text-center font-medium bg-gray-100 border border-gray-300">
|
||||
작성
|
||||
{labels.writer}
|
||||
</td>
|
||||
{is4Col && (
|
||||
<td className="w-16 p-2 text-center font-medium bg-gray-100 border border-gray-300">
|
||||
검토
|
||||
{labels.reviewer}
|
||||
</td>
|
||||
)}
|
||||
<td className="w-16 p-2 text-center font-medium bg-gray-100 border border-gray-300">
|
||||
승인
|
||||
{labels.approver}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 건설 문서용 결재란 컴포넌트
|
||||
*
|
||||
* @example
|
||||
* // 4열 결재란 (작성 + 승인 3개)
|
||||
* <ConstructionApprovalTable
|
||||
* approvers={{
|
||||
* writer: { name: '홍길동', department: '영업부' },
|
||||
* approver1: { name: '김부장', department: '기획부' },
|
||||
* approver2: { name: '이이사', department: '개발부' },
|
||||
* approver3: { name: '박대표', department: '경영부' },
|
||||
* }}
|
||||
* />
|
||||
*/
|
||||
|
||||
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 (
|
||||
<table className={cn('border-collapse border border-gray-300 text-sm', className)}>
|
||||
<tbody>
|
||||
{/* 헤더 행 */}
|
||||
<tr>
|
||||
<th
|
||||
rowSpan={3}
|
||||
className="border border-gray-300 px-2 py-1 bg-gray-50 text-center w-8 align-middle"
|
||||
>
|
||||
<span className="writing-vertical">결<br />재</span>
|
||||
</th>
|
||||
<th className="border border-gray-300 px-3 py-1 bg-gray-50 text-center w-16">
|
||||
{labels.writer}
|
||||
</th>
|
||||
<th className="border border-gray-300 px-3 py-1 bg-gray-50 text-center w-16">
|
||||
{labels.approver1}
|
||||
</th>
|
||||
<th className="border border-gray-300 px-3 py-1 bg-gray-50 text-center w-16">
|
||||
{labels.approver2}
|
||||
</th>
|
||||
<th className="border border-gray-300 px-3 py-1 bg-gray-50 text-center w-16">
|
||||
{labels.approver3}
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
{/* 이름 행 */}
|
||||
<tr>
|
||||
<td className="border border-gray-300 px-3 py-2 text-center h-10">
|
||||
{approvers.writer?.name || ''}
|
||||
</td>
|
||||
<td className="border border-gray-300 px-3 py-2 text-center h-10">
|
||||
{approvers.approver1?.name || ''}
|
||||
</td>
|
||||
<td className="border border-gray-300 px-3 py-2 text-center h-10">
|
||||
{approvers.approver2?.name || ''}
|
||||
</td>
|
||||
<td className="border border-gray-300 px-3 py-2 text-center h-10">
|
||||
{approvers.approver3?.name || ''}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* 부서 행 */}
|
||||
<tr>
|
||||
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500">
|
||||
{approvers.writer?.department || '부서명'}
|
||||
</td>
|
||||
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500">
|
||||
{approvers.approver1?.department || '부서명'}
|
||||
</td>
|
||||
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500">
|
||||
{approvers.approver2?.department || '부서명'}
|
||||
</td>
|
||||
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500">
|
||||
{approvers.approver3?.department || '부서명'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,15 @@
|
||||
*
|
||||
* // 외부 송부 (결재선 숨김)
|
||||
* <DocumentHeader title="견적서" mode="external" />
|
||||
*
|
||||
* // 품질검사 레이아웃 (2줄 제목)
|
||||
* <DocumentHeader
|
||||
* title="스크린"
|
||||
* subtitle="중간검사 성적서"
|
||||
* layout="quality"
|
||||
* logo={{ text: 'KD', subtext: '경동기업\nKYUNGDONG COMPANY' }}
|
||||
* customApproval={<QualityApprovalTable type="4col" approvers={approvers} />}
|
||||
* />
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
@@ -56,10 +65,12 @@ export interface DocumentHeaderProps {
|
||||
topInfo?: ReactNode;
|
||||
/** 결재란 설정 (null이면 숨김) */
|
||||
approval?: Omit<ApprovalLineProps, 'mode'> | 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 (
|
||||
<div className={cn('flex justify-between items-start mb-4', className)}>
|
||||
{/* 좌측: 로고 영역 */}
|
||||
{logo && (
|
||||
<div className="flex items-center gap-2">
|
||||
{logo.imageUrl ? (
|
||||
<img src={logo.imageUrl} alt={logo.text} className="h-8" />
|
||||
) : (
|
||||
<div className="text-2xl font-bold">{logo.text}</div>
|
||||
)}
|
||||
{logo.subtext && (
|
||||
<div className="text-xs text-gray-600 whitespace-pre-line">{logo.subtext}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 중앙: 2줄 제목 */}
|
||||
<div className="text-center">
|
||||
<div className="text-xl font-bold">{title}</div>
|
||||
{subtitle && (
|
||||
<div className="text-xl font-bold tracking-[0.2rem]">{subtitle}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 우측: 결재란 */}
|
||||
{showApproval && (
|
||||
customApproval || (approval && <ApprovalLine {...approval} mode={mode} />)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 건설 문서 레이아웃 (좌측 정렬 제목 + 결재란)
|
||||
if (layout === 'construction') {
|
||||
return (
|
||||
<div className={cn('flex justify-between items-start mb-6', className)}>
|
||||
{/* 좌측: 제목 및 문서정보 */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-2">{title}</h1>
|
||||
{(documentCode || subtitle) && (
|
||||
<div className="text-sm text-gray-600">
|
||||
{documentCode && <span>문서번호: {documentCode}</span>}
|
||||
{documentCode && subtitle && <span> | </span>}
|
||||
{subtitle && <span>{subtitle}</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 우측: 결재란 */}
|
||||
{showApproval && (
|
||||
customApproval || (approval && <ApprovalLine {...approval} mode={mode} />)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 견적/발주 문서 레이아웃 (중앙 제목 + 우측 로트번호/결재란)
|
||||
if (layout === 'quote') {
|
||||
return (
|
||||
<div className={cn('flex items-center justify-between mb-5 pb-4 border-b-2 border-black', className)}>
|
||||
{/* 중앙: 제목 */}
|
||||
<div className="flex-1 text-center">
|
||||
<h1 className="text-4xl font-bold tracking-[8px]">{title}</h1>
|
||||
</div>
|
||||
|
||||
{/* 우측: 로트번호 + 결재란 (customApproval로 전달) */}
|
||||
{showApproval && customApproval}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 중앙 정렬 레이아웃 (견적서 스타일)
|
||||
if (layout === 'centered') {
|
||||
return (
|
||||
@@ -107,8 +192,10 @@ export function DocumentHeader({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showApproval && approval && (
|
||||
<ApprovalLine {...approval} mode={mode} className="ml-4" />
|
||||
{showApproval && (
|
||||
<div className="ml-4">
|
||||
{customApproval || (approval && <ApprovalLine {...approval} mode={mode} />)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -150,8 +237,8 @@ export function DocumentHeader({
|
||||
{topInfo}
|
||||
</div>
|
||||
)}
|
||||
{showApproval && approval && (
|
||||
<ApprovalLine {...approval} mode={mode} />
|
||||
{showApproval && (
|
||||
customApproval || (approval && <ApprovalLine {...approval} mode={mode} />)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
121
src/components/document-system/components/LotApprovalTable.tsx
Normal file
121
src/components/document-system/components/LotApprovalTable.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 로트번호 + 결재란 통합 컴포넌트
|
||||
*
|
||||
* @example
|
||||
* <LotApprovalTable
|
||||
* lotNumber="KQ#-SC-250122-01"
|
||||
* approvers={{
|
||||
* writer: { name: '전진', department: '판매/전진' },
|
||||
* reviewer: { name: '', department: '회계' },
|
||||
* approver: { name: '', department: '생산' },
|
||||
* }}
|
||||
* />
|
||||
*/
|
||||
|
||||
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 (
|
||||
<div className={cn('border-2 border-black bg-white', className)}>
|
||||
{/* 로트번호 행 */}
|
||||
<div className="grid grid-cols-[100px_1fr] border-b-2 border-black">
|
||||
<div className="bg-gray-200 border-r-2 border-black px-2 py-2 text-center font-bold text-xs flex items-center justify-center">
|
||||
{lotLabel}
|
||||
</div>
|
||||
<div className="bg-white px-2 py-2 text-center font-bold text-sm flex items-center justify-center">
|
||||
{lotNumber}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 결재란 */}
|
||||
<div className="grid grid-cols-[60px_1fr]">
|
||||
{/* 결재 세로 셀 */}
|
||||
<div className="border-r border-black flex items-center justify-center bg-white row-span-3">
|
||||
<span className="text-xs font-semibold">결<br />재</span>
|
||||
</div>
|
||||
|
||||
{/* 결재 내용 */}
|
||||
<div>
|
||||
{/* 헤더 행 */}
|
||||
<div className="grid grid-cols-3 border-b border-black">
|
||||
<div className="border-r border-black px-3 py-2 text-center font-semibold text-xs bg-white">
|
||||
{labels.writer}
|
||||
</div>
|
||||
<div className="border-r border-black px-3 py-2 text-center font-semibold text-xs bg-white">
|
||||
{labels.reviewer}
|
||||
</div>
|
||||
<div className="px-3 py-2 text-center font-semibold text-xs bg-white">
|
||||
{labels.approver}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 서명 행 */}
|
||||
<div className="grid grid-cols-3 border-b border-black">
|
||||
<div className="border-r border-black px-3 py-2 text-center text-xs h-12 flex items-center justify-center bg-white">
|
||||
{approvers.writer?.name || ''}
|
||||
</div>
|
||||
<div className="border-r border-black px-3 py-2 text-center text-xs h-12 flex items-center justify-center bg-white">
|
||||
{approvers.reviewer?.name || ''}
|
||||
</div>
|
||||
<div className="px-3 py-2 text-center text-xs h-12 flex items-center justify-center bg-white">
|
||||
{approvers.approver?.name || ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 부서 행 */}
|
||||
<div className="grid grid-cols-3">
|
||||
<div className="border-r border-black px-2 py-1 text-center text-xs font-semibold bg-white">
|
||||
{approvers.writer?.department || ''}
|
||||
</div>
|
||||
<div className="border-r border-black px-2 py-1 text-center text-xs font-semibold bg-white">
|
||||
{approvers.reviewer?.department || ''}
|
||||
</div>
|
||||
<div className="px-2 py-1 text-center text-xs font-semibold bg-white">
|
||||
{approvers.approver?.department || ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 품질검사 문서용 결재란 컴포넌트
|
||||
*
|
||||
* @example
|
||||
* // 2열 타입 (수입검사 성적서)
|
||||
* <QualityApprovalTable
|
||||
* type="2col"
|
||||
* approvers={{ writer: '노원호' }}
|
||||
* reportDate="2025-07-15"
|
||||
* />
|
||||
*
|
||||
* // 4열 타입 (중간검사 성적서)
|
||||
* <QualityApprovalTable
|
||||
* type="4col"
|
||||
* approvers={{ writer: '전진', reviewer: '', approver: '' }}
|
||||
* departments={{ writer: '판매/전진', reviewer: '생산', approver: '품질' }}
|
||||
* />
|
||||
*/
|
||||
|
||||
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 (
|
||||
<div className={cn('text-right', className)}>
|
||||
<table className="text-xs border-collapse">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-12">담당</td>
|
||||
<td className="border border-gray-400 px-2 py-1 w-16">부서장</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100">결재</td>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8">{approvers.writer || ''}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{reportDate && (
|
||||
<div className="text-xs text-right mt-1">접고일자: {reportDate}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 4열 타입 (중간검사 성적서 - 결재/작성/검토/승인 + 부서)
|
||||
return (
|
||||
<table className={cn('text-xs border-collapse', className)}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-8 text-center" rowSpan={3}>
|
||||
<div className="flex flex-col items-center">
|
||||
<span>결</span><span>재</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">작성</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">검토</td>
|
||||
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center">승인</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center font-medium">
|
||||
{approvers.writer || ''}
|
||||
</td>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center">
|
||||
{approvers.reviewer || ''}
|
||||
</td>
|
||||
<td className="border border-gray-400 px-2 py-1 h-8 text-center">
|
||||
{approvers.approver || ''}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">
|
||||
{departments.writer || '판매/전진'}
|
||||
</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">
|
||||
{departments.reviewer || '생산'}
|
||||
</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">
|
||||
{departments.approver || '품질'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
106
src/components/document-system/components/SignatureSection.tsx
Normal file
106
src/components/document-system/components/SignatureSection.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 서명/도장 영역 컴포넌트
|
||||
*
|
||||
* @example
|
||||
* // 기본 사용 (도장 영역 포함)
|
||||
* <SignatureSection
|
||||
* date="2025년 01월 22일"
|
||||
* companyName="경동기업"
|
||||
* showStamp={true}
|
||||
* />
|
||||
*
|
||||
* // 커스텀 문구
|
||||
* <SignatureSection
|
||||
* label="상기와 같이 견적합니다."
|
||||
* date="2025년 01월 22일"
|
||||
* companyName="경동기업"
|
||||
* role="공급자"
|
||||
* />
|
||||
*/
|
||||
|
||||
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 (
|
||||
<div className={cn('mt-8', alignClass, className)}>
|
||||
<div className="inline-block text-left">
|
||||
{/* 안내 문구 */}
|
||||
{label && (
|
||||
<div className="mb-4 text-sm">
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 날짜 + 회사명 + 도장 */}
|
||||
<div className="flex items-center gap-5">
|
||||
<div>
|
||||
{date && (
|
||||
<div className="text-sm mb-1">{date}</div>
|
||||
)}
|
||||
{companyName && (
|
||||
<div className="text-base font-semibold">
|
||||
{role}: {companyName} (인)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 도장 영역 */}
|
||||
{showStamp && (
|
||||
<div className="border-2 border-black w-20 h-20 relative inline-block ml-5">
|
||||
{stampImageUrl ? (
|
||||
<img
|
||||
src={stampImageUrl}
|
||||
alt="도장"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-[10px] text-gray-400 text-center whitespace-pre-line leading-tight">
|
||||
{stampText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: () => (
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>카드 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{cardToDelete?.cardName}" 카드를 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 카드 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDeleteCard}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onConfirm={handleDeleteCard}
|
||||
title="카드 삭제"
|
||||
description={
|
||||
<>
|
||||
"{cardToDelete?.cardName}" 카드를 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 카드 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
}), [
|
||||
cards,
|
||||
|
||||
@@ -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() {
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>부서 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{isBulkDelete
|
||||
? `선택한 부서 ${selectedIds.size}개를 삭제하시겠습니까?`
|
||||
: `"${departmentToDelete?.name}" 부서를 삭제하시겠습니까?`
|
||||
}
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 부서의 인원은 회사(기본) 인원으로 변경됩니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
description={
|
||||
<>
|
||||
{isBulkDelete
|
||||
? `선택한 부서 ${selectedIds.size}개를 삭제하시겠습니까?`
|
||||
: `"${departmentToDelete?.name}" 부서를 삭제하시겠습니까?`
|
||||
}
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 부서의 인원은 회사(기본) 인원으로 변경됩니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
onConfirm={confirmDelete}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>사원 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{employeeToDelete?.name}" 사원을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 사원 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDeleteEmployee}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
삭제 중...
|
||||
</>
|
||||
) : (
|
||||
'삭제'
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onConfirm={handleDeleteEmployee}
|
||||
title="사원 삭제"
|
||||
description={
|
||||
<>
|
||||
"{employeeToDelete?.name}" 사원을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 사원 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
loading={isDeleting}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}), [
|
||||
|
||||
@@ -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() {
|
||||
/>
|
||||
|
||||
{/* 승인 확인 다이얼로그 */}
|
||||
<AlertDialog open={approveDialogOpen} onOpenChange={setApproveDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>휴가 승인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말 {selectedItems.size}건을 승인하시겠습니까?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleApproveConfirm}>
|
||||
승인
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmDialog
|
||||
open={approveDialogOpen}
|
||||
onOpenChange={setApproveDialogOpen}
|
||||
title="휴가 승인"
|
||||
description={`정말 ${selectedItems.size}건을 승인하시겠습니까?`}
|
||||
confirmText="승인"
|
||||
variant="success"
|
||||
onConfirm={handleApproveConfirm}
|
||||
/>
|
||||
|
||||
{/* 거절 확인 다이얼로그 */}
|
||||
<AlertDialog open={rejectDialogOpen} onOpenChange={setRejectDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>휴가 거절</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말 {selectedItems.size}건을 거절하시겠습니까?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleRejectConfirm}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
거절
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmDialog
|
||||
open={rejectDialogOpen}
|
||||
onOpenChange={setRejectDialogOpen}
|
||||
title="휴가 거절"
|
||||
description={`정말 ${selectedItems.size}건을 거절하시겠습니까?`}
|
||||
confirmText="거절"
|
||||
variant="destructive"
|
||||
onConfirm={handleRejectConfirm}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}), [
|
||||
|
||||
@@ -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 (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>품목코드 중복</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
입력하신 조건의 품목코드가 이미 존재합니다.
|
||||
<span className="block mt-2 font-medium text-foreground">
|
||||
기존 품목을 수정하시겠습니까?
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={onCancel}>
|
||||
취소
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onGoToEdit}>
|
||||
중복 품목 수정하러 가기
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmDialog
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
onConfirm={handleConfirm}
|
||||
title="품목코드 중복"
|
||||
description={
|
||||
<>
|
||||
입력하신 조건의 품목코드가 이미 존재합니다.
|
||||
<span className="block mt-2 font-medium text-foreground">
|
||||
기존 품목을 수정하시겠습니까?
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
confirmText="중복 품목 수정하러 가기"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
/>
|
||||
|
||||
{/* 개별 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>품목 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
품목 "{itemToDelete?.code}"을(를) 삭제하시겠습니까?
|
||||
<br />
|
||||
이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="품목 삭제"
|
||||
description={
|
||||
<>
|
||||
품목 "{itemToDelete?.code}"을(를) 삭제하시겠습니까?
|
||||
<br />
|
||||
이 작업은 되돌릴 수 없습니다.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
</Dialog>
|
||||
|
||||
{/* 탭 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={isDeleteTabDialogOpen} onOpenChange={setIsDeleteTabDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>탭 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{customTabs.find(t => t.id === deletingTabId)?.label}" 탭을 삭제하시겠습니까?
|
||||
<br />
|
||||
이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => {
|
||||
setIsDeleteTabDialogOpen(false);
|
||||
setDeletingTabId(null);
|
||||
}}>
|
||||
취소
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDeleteTab}
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={isDeleteTabDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsDeleteTabDialogOpen(open);
|
||||
if (!open) setDeletingTabId(null);
|
||||
}}
|
||||
onConfirm={confirmDeleteTab}
|
||||
title="탭 삭제"
|
||||
description={
|
||||
<>
|
||||
"{customTabs.find(t => t.id === deletingTabId)?.label}" 탭을 삭제하시겠습니까?
|
||||
<br />
|
||||
이 작업은 되돌릴 수 없습니다.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 탭 추가/수정 다이얼로그 */}
|
||||
<Dialog open={isAddTabDialogOpen} onOpenChange={(open) => {
|
||||
@@ -363,32 +353,22 @@ export function TabManagementDialogs({
|
||||
</Dialog>
|
||||
|
||||
{/* 속성 하위 탭 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={isDeleteAttributeTabDialogOpen} onOpenChange={setIsDeleteAttributeTabDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>속성 탭 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{attributeSubTabs.find(t => t.id === deletingAttributeTabId)?.label}" 탭을 삭제하시겠습니까?
|
||||
<br />
|
||||
이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => {
|
||||
setIsDeleteAttributeTabDialogOpen(false);
|
||||
setDeletingAttributeTabId(null);
|
||||
}}>
|
||||
취소
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDeleteAttributeTab}
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={isDeleteAttributeTabDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsDeleteAttributeTabDialogOpen(open);
|
||||
if (!open) setDeletingAttributeTabId(null);
|
||||
}}
|
||||
onConfirm={confirmDeleteAttributeTab}
|
||||
title="속성 탭 삭제"
|
||||
description={
|
||||
<>
|
||||
"{attributeSubTabs.find(t => t.id === deletingAttributeTabId)?.label}" 탭을 삭제하시겠습니까?
|
||||
<br />
|
||||
이 작업은 되돌릴 수 없습니다.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 속성 하위 탭 추가/수정 다이얼로그 */}
|
||||
<Dialog open={isAddAttributeTabDialogOpen} onOpenChange={(open) => {
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-white p-8 min-h-full">
|
||||
{/* 제목 */}
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-bold tracking-widest mb-2">계 약 서</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
수주번호: {orderNumber} | 계약일자: {orderDate}
|
||||
</p>
|
||||
</div>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="계 약 서"
|
||||
subtitle={`수주번호: ${orderNumber} | 계약일자: ${orderDate}`}
|
||||
layout="simple"
|
||||
approval={null}
|
||||
className="mb-6"
|
||||
/>
|
||||
|
||||
{/* 제품 정보 (개소별) */}
|
||||
<div className="border border-gray-300 mb-4">
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-white p-8 min-h-full">
|
||||
{/* 제목 */}
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-bold tracking-widest mb-2">거 래 명 세 서</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
수주번호: {orderNumber} | 발행일: {orderDate}
|
||||
</p>
|
||||
</div>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="거 래 명 세 서"
|
||||
subtitle={`수주번호: ${orderNumber} | 발행일: ${orderDate}`}
|
||||
layout="simple"
|
||||
approval={null}
|
||||
className="mb-6"
|
||||
/>
|
||||
|
||||
{/* 공급자/공급받는자 정보 */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
|
||||
@@ -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) {
|
||||
</Dialog>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>출하 정보 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
출하번호 {detail?.shipmentNo}을(를) 삭제하시겠습니까?
|
||||
<br />
|
||||
이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
삭제 중...
|
||||
</>
|
||||
) : (
|
||||
'삭제'
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleDelete}
|
||||
title="출하 정보 삭제"
|
||||
description={
|
||||
<>
|
||||
출하번호 {detail?.shipmentNo}을(를) 삭제하시겠습니까?
|
||||
<br />
|
||||
이 작업은 되돌릴 수 없습니다.
|
||||
</>
|
||||
}
|
||||
loading={isDeleting}
|
||||
/>
|
||||
|
||||
{/* 상태 변경 다이얼로그 */}
|
||||
<Dialog open={showStatusDialog} onOpenChange={setShowStatusDialog}>
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-white p-8 max-w-3xl mx-auto text-sm print:p-0 print:max-w-none">
|
||||
{/* 헤더 */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-2xl font-bold">KD</div>
|
||||
<div>
|
||||
<div className="text-xs">경동기업</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold tracking-[1rem]">납 품 확 인 서</div>
|
||||
<table className="text-xs border-collapse">
|
||||
<tbody>
|
||||
{/* 헤더: 결재 + 작성/검토/승인 */}
|
||||
<tr>
|
||||
<td className="border px-2 py-1 bg-muted" rowSpan={3}>
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<span>결</span>
|
||||
<span>재</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="border px-2 py-1 bg-muted text-center w-16">작성</td>
|
||||
<td className="border px-2 py-1 bg-muted text-center w-16">검토</td>
|
||||
<td className="border px-2 py-1 bg-muted text-center w-16">승인</td>
|
||||
</tr>
|
||||
{/* 내용: 서명란 */}
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center h-10"></td>
|
||||
<td className="border px-2 py-1 text-center h-10"></td>
|
||||
<td className="border px-2 py-1 text-center h-10"></td>
|
||||
</tr>
|
||||
{/* 부서 */}
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center bg-muted/50">판매/<br/>전산</td>
|
||||
<td className="border px-2 py-1 text-center bg-muted/50">출하</td>
|
||||
<td className="border px-2 py-1 text-center bg-muted/50">품질</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="납 품 확 인 서"
|
||||
logo={{ text: 'KD', subtext: '경동기업' }}
|
||||
layout="default"
|
||||
approval={{
|
||||
type: '4col',
|
||||
showDepartment: true,
|
||||
departmentLabels: { writer: '판매/전산', reviewer: '출하', approver: '품질' },
|
||||
}}
|
||||
className="mb-6"
|
||||
/>
|
||||
|
||||
{/* 출하 관리부서 */}
|
||||
<div className="text-xs text-muted-foreground mb-2">출하 관리부서</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-white p-8 max-w-4xl mx-auto text-sm print:p-0 print:max-w-none">
|
||||
{/* 헤더 */}
|
||||
<div className="flex justify-between items-start mb-6 border-b pb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-2xl font-bold">KD</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">경동기업</div>
|
||||
<div className="text-xs text-muted-foreground">KYUNGDONG COMPANY</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold tracking-widest">출 고 증</div>
|
||||
<table className="text-xs border-collapse">
|
||||
<tbody>
|
||||
{/* 헤더: 결재 + 작성/검토/승인 */}
|
||||
<tr>
|
||||
<td className="border px-2 py-1 bg-muted" rowSpan={3}>
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<span>결</span>
|
||||
<span>재</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="border px-2 py-1 bg-muted text-center w-20">작성</td>
|
||||
<td className="border px-2 py-1 bg-muted text-center w-16">검토</td>
|
||||
<td className="border px-2 py-1 bg-muted text-center w-16">승인</td>
|
||||
</tr>
|
||||
{/* 내용: 담당자 정보 */}
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center">
|
||||
<div>판매1팀 임</div>
|
||||
<div>판매</div>
|
||||
<div className="text-muted-foreground">12-20</div>
|
||||
</td>
|
||||
<td className="border px-2 py-1 text-center"></td>
|
||||
<td className="border px-2 py-1 text-center"></td>
|
||||
</tr>
|
||||
{/* 부서 */}
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center bg-muted/50">판매/전진</td>
|
||||
<td className="border px-2 py-1 text-center bg-muted/50">출하</td>
|
||||
<td className="border px-2 py-1 text-center bg-muted/50">생산관리</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="출 고 증"
|
||||
logo={{ text: 'KD', subtext: '경동기업' }}
|
||||
layout="default"
|
||||
approval={{
|
||||
type: '4col',
|
||||
writer: { name: '판매1팀 임', date: '12-20' },
|
||||
showDepartment: true,
|
||||
departmentLabels: { writer: '판매/전진', reviewer: '출하', approver: '생산관리' },
|
||||
}}
|
||||
className="mb-6 border-b pb-4"
|
||||
/>
|
||||
|
||||
{/* 출하 관리 */}
|
||||
<div className="text-xs text-muted-foreground mb-2">출하 관리</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="bg-white p-8 max-w-3xl mx-auto text-sm print:p-0 print:max-w-none">
|
||||
{/* 제목 */}
|
||||
<h1 className="text-2xl font-bold text-center tracking-[1rem] mb-8">
|
||||
거 래 명 세 서
|
||||
</h1>
|
||||
<p className="text-center text-xs text-muted-foreground mb-6">
|
||||
TRANSACTION STATEMENT
|
||||
</p>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="거 래 명 세 서"
|
||||
subtitle="TRANSACTION STATEMENT"
|
||||
layout="simple"
|
||||
approval={null}
|
||||
className="mb-6"
|
||||
/>
|
||||
|
||||
{/* 공급받는자 / 공급자 정보 */}
|
||||
<div className="grid grid-cols-2 gap-6 mb-6">
|
||||
|
||||
@@ -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
|
||||
<UniversalListPage config={config} initialData={allProcesses} />
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>공정 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
선택한 공정을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDeleteConfirm}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
삭제 중...
|
||||
</>
|
||||
) : (
|
||||
'삭제'
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
description="선택한 공정을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
|
||||
loading={isLoading}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-orange-600">
|
||||
자재 투입이 필요합니다!
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="space-y-3">
|
||||
<div className="bg-gray-50 p-3 rounded-lg space-y-1 text-sm">
|
||||
<p>
|
||||
<span className="text-muted-foreground">작업지시:</span>{' '}
|
||||
<span className="font-medium text-foreground">{order.orderNo}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-muted-foreground">공정:</span>{' '}
|
||||
<span className="font-medium text-foreground">
|
||||
{order.processName}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-orange-600 font-medium">
|
||||
자재 투입 없이 완료 처리하시겠습니까?
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
(LOT 추적이 불가능해집니다)
|
||||
</p>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleCancel}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirm}>
|
||||
확인
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmDialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
onConfirm={handleConfirm}
|
||||
variant="warning"
|
||||
title={<span className="text-orange-600">자재 투입이 필요합니다!</span>}
|
||||
description={
|
||||
<div className="space-y-3">
|
||||
<div className="bg-gray-50 p-3 rounded-lg space-y-1 text-sm">
|
||||
<p>
|
||||
<span className="text-muted-foreground">작업지시:</span>{' '}
|
||||
<span className="font-medium text-foreground">{order.orderNo}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-muted-foreground">공정:</span>{' '}
|
||||
<span className="font-medium text-foreground">
|
||||
{order.processName}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-orange-600 font-medium">
|
||||
자재 투입 없이 완료 처리하시겠습니까?
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
(LOT 추적이 불가능해집니다)
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
confirmText="확인"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
)}
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={!!deleteTarget} onOpenChange={() => setDeleteTarget(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>개소 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
선택한 개소를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
if (deleteTarget) {
|
||||
onDeleteLocation(deleteTarget);
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
}}
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={!!deleteTarget}
|
||||
onOpenChange={() => setDeleteTarget(null)}
|
||||
onConfirm={() => {
|
||||
if (deleteTarget) {
|
||||
onDeleteLocation(deleteTarget);
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
}}
|
||||
title="개소 삭제"
|
||||
description="선택한 개소를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
{/* 발주서 내용 */}
|
||||
<div id="purchase-order-content" className="purchase-order p-12 print:p-8">
|
||||
|
||||
{/* 헤더: 제목 + 결재란 */}
|
||||
<div className="po-header">
|
||||
{/* 제목 */}
|
||||
<div className="po-title">
|
||||
<h1>발 주 서</h1>
|
||||
</div>
|
||||
|
||||
{/* 로트번호 + 결재란 */}
|
||||
<div className="po-approval-section">
|
||||
{/* 로트번호 */}
|
||||
<div className="po-lot-number-row">
|
||||
<div className="po-lot-label">
|
||||
로트번호
|
||||
</div>
|
||||
<div className="po-lot-value">
|
||||
{purchaseOrderNumber}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 결재란 */}
|
||||
<div className="po-approval-box">
|
||||
<div className="po-approval-merged-vertical-cell">결<br/>재</div>
|
||||
{/* 결재란 헤더 */}
|
||||
<div className="po-approval-header">
|
||||
<div className="po-approval-header-cell">작성</div>
|
||||
<div className="po-approval-header-cell">검토</div>
|
||||
<div className="po-approval-header-cell">승인</div>
|
||||
</div>
|
||||
{/* 결재+서명란 */}
|
||||
<div className="po-approval-content-row">
|
||||
<div className="po-approval-signature-cell">전진</div>
|
||||
<div className="po-approval-signature-cell"></div>
|
||||
<div className="po-approval-signature-cell"></div>
|
||||
</div>
|
||||
{/* 이름란 */}
|
||||
<div className="po-approval-name-row">
|
||||
<div className="po-approval-name-cell">판매/전진</div>
|
||||
<div className="po-approval-name-cell">회계</div>
|
||||
<div className="po-approval-name-cell">생산</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 헤더: 제목 + 로트번호/결재란 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="발 주 서"
|
||||
layout="quote"
|
||||
customApproval={
|
||||
<LotApprovalTable
|
||||
lotNumber={purchaseOrderNumber}
|
||||
approvers={{
|
||||
writer: { name: '전진', department: '판매/전진' },
|
||||
reviewer: { name: '', department: '회계' },
|
||||
approver: { name: '', department: '생산' },
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 신청업체 */}
|
||||
<table className="po-section-table">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* 견적서 내용 */}
|
||||
<div id="quote-document-content" className="official-doc p-12 print:p-8">
|
||||
{/* 문서 헤더 */}
|
||||
<div className="doc-header">
|
||||
<div className="doc-title">견 적 서</div>
|
||||
<div className="doc-number">
|
||||
문서번호: {quote.id || 'Q-XXXXXX'} | 작성일자: {formatDate(quote.registrationDate || '')}
|
||||
</div>
|
||||
</div>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="견 적 서"
|
||||
documentCode={quote.id || 'Q-XXXXXX'}
|
||||
subtitle={`작성일자: ${formatDate(quote.registrationDate || '')}`}
|
||||
layout="simple"
|
||||
className="border-b-[3px] border-double border-black pb-5 mb-8"
|
||||
/>
|
||||
|
||||
{/* 수요자 정보 */}
|
||||
<div className="info-box">
|
||||
@@ -424,27 +382,14 @@ export function QuoteDocument({ quote, companyInfo }: QuoteDocumentProps) {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 서명란 */}
|
||||
<div className="signature-section">
|
||||
<div style={{ display: 'inline-block', textAlign: 'left' }}>
|
||||
<div style={{ marginBottom: '15px', fontSize: '14px' }}>
|
||||
상기와 같이 견적합니다.
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '13px', marginBottom: '5px' }}>{formatDate(quote.registrationDate || '')}</div>
|
||||
<div style={{ fontSize: '15px', fontWeight: '600' }}>
|
||||
공급자: {companyInfo?.companyName || '-'} (인)
|
||||
</div>
|
||||
</div>
|
||||
<div className="stamp-area">
|
||||
<div className="stamp-text">
|
||||
(인감<br/>날인)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 서명란 (공통 컴포넌트) */}
|
||||
<SignatureSection
|
||||
label="상기와 같이 견적합니다."
|
||||
date={formatDate(quote.registrationDate || '')}
|
||||
companyName={companyInfo?.companyName || '-'}
|
||||
role="공급자"
|
||||
showStamp={true}
|
||||
/>
|
||||
|
||||
{/* 하단 안내사항 */}
|
||||
<div className="footer-note">
|
||||
|
||||
@@ -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({
|
||||
</StandardDialog>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>견적 삭제 확인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{deleteTargetId
|
||||
? `견적번호: ${allQuotes.find((q) => q.id === deleteTargetId)?.quoteNumber || deleteTargetId}`
|
||||
: ''}
|
||||
<br />
|
||||
이 견적을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isPending}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmDelete} disabled={isPending}>
|
||||
{isPending ? '삭제 중...' : '삭제'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={isDeleteDialogOpen}
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
description={
|
||||
<>
|
||||
{deleteTargetId
|
||||
? `견적번호: ${allQuotes.find((q) => q.id === deleteTargetId)?.quoteNumber || deleteTargetId}`
|
||||
: ''}
|
||||
<br />
|
||||
이 견적을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.
|
||||
</>
|
||||
}
|
||||
loading={isPending}
|
||||
onConfirm={handleConfirmDelete}
|
||||
/>
|
||||
|
||||
{/* 일괄 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog
|
||||
<DeleteConfirmDialog
|
||||
open={isBulkDeleteDialogOpen}
|
||||
onOpenChange={setIsBulkDeleteDialogOpen}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>일괄 삭제 확인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
선택한 {bulkDeleteIds.length}개의 견적을 삭제하시겠습니까?
|
||||
<br />
|
||||
삭제된 데이터는 복구할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isPending}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmBulkDelete} disabled={isPending}>
|
||||
{isPending ? '삭제 중...' : '삭제'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
description={
|
||||
<>
|
||||
선택한 {bulkDeleteIds.length}개의 견적을 삭제하시겠습니까?
|
||||
<br />
|
||||
삭제된 데이터는 복구할 수 없습니다.
|
||||
</>
|
||||
}
|
||||
loading={isPending}
|
||||
onConfirm={handleConfirmBulkDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
</AlertDialog>
|
||||
|
||||
{/* 사용중지 확인 다이얼로그 */}
|
||||
<AlertDialog open={showSuspendDialog} onOpenChange={setShowSuspendDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>계정 사용중지</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
정말 사용중지하시겠습니까?
|
||||
<br />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
해당 테넌트의 사용이 중지됩니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isSuspending}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmSuspend}
|
||||
className="bg-orange-600 hover:bg-orange-700"
|
||||
disabled={isSuspending}
|
||||
>
|
||||
{isSuspending ? '처리 중...' : '확인'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmDialog
|
||||
open={showSuspendDialog}
|
||||
onOpenChange={setShowSuspendDialog}
|
||||
onConfirm={handleConfirmSuspend}
|
||||
title="계정 사용중지"
|
||||
description={
|
||||
<>
|
||||
정말 사용중지하시겠습니까?
|
||||
<br />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
해당 테넌트의 사용이 중지됩니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
variant="warning"
|
||||
loading={isSuspending}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
</div>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>계좌 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
계좌를 정말 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
삭제된 계좌의 과거 사용 내역은 보존됩니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
description={
|
||||
<>
|
||||
계좌를 정말 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
삭제된 계좌의 과거 사용 내역은 보존됩니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
onConfirm={handleConfirmDelete}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
<UniversalListPage config={config} />
|
||||
|
||||
{/* 단일 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>계좌 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
계좌를 정말 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
삭제된 계좌의 과거 사용 내역은 보존됩니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
삭제 중...
|
||||
</>
|
||||
) : '삭제'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="계좌 삭제"
|
||||
description={
|
||||
<>
|
||||
계좌를 정말 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
삭제된 계좌의 과거 사용 내역은 보존됩니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
loading={isDeleting}
|
||||
/>
|
||||
|
||||
{/* 다중 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={showBulkDeleteDialog} onOpenChange={setShowBulkDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>계좌 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
선택하신 {bulkDeleteIds.length}개의 계좌를 정말 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
삭제된 계좌의 과거 사용 내역은 보존됩니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmBulkDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
삭제 중...
|
||||
</>
|
||||
) : '삭제'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={showBulkDeleteDialog}
|
||||
onOpenChange={setShowBulkDeleteDialog}
|
||||
onConfirm={handleConfirmBulkDelete}
|
||||
title="계좌 삭제"
|
||||
description={
|
||||
<>
|
||||
선택하신 <strong>{bulkDeleteIds.length}개</strong>의 계좌를 정말 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
삭제된 계좌의 과거 사용 내역은 보존됩니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
loading={isDeleting}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>권한 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{permission.name}" 권한을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
이 권한을 사용 중인 사원이 있으면 해당 사원의 권한이 초기화됩니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onConfirm={confirmDelete}
|
||||
title="권한 삭제"
|
||||
description={
|
||||
<>
|
||||
"{permission.name}" 권한을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
이 권한을 사용 중인 사원이 있으면 해당 사원의 권한이 초기화됩니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>역할 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{role.name}" 역할을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
이 역할을 사용 중인 사원이 있으면 해당 사원의 역할이 초기화됩니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
삭제 중...
|
||||
</>
|
||||
) : (
|
||||
'삭제'
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onConfirm={confirmDelete}
|
||||
title="역할 삭제"
|
||||
description={
|
||||
<>
|
||||
"{role.name}" 역할을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
이 역할을 사용 중인 사원이 있으면 해당 사원의 역할이 초기화됩니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
loading={isDeleting}
|
||||
/>
|
||||
)}
|
||||
</PageLayout>
|
||||
);
|
||||
|
||||
@@ -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: () => (
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>역할 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{isBulkDelete
|
||||
? `선택한 ${selectedItems.size}개의 역할을 삭제하시겠습니까?`
|
||||
: `"${roleToDelete?.name}" 역할을 삭제하시겠습니까?`
|
||||
}
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
이 역할을 사용 중인 사원이 있으면 해당 사원의 역할이 초기화됩니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
삭제 중...
|
||||
</>
|
||||
) : (
|
||||
'삭제'
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onConfirm={confirmDelete}
|
||||
title="역할 삭제"
|
||||
description={
|
||||
<>
|
||||
{isBulkDelete
|
||||
? `선택한 ${selectedItems.size}개의 역할을 삭제하시겠습니까?`
|
||||
: `"${roleToDelete?.name}" 역할을 삭제하시겠습니까?`
|
||||
}
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
이 역할을 사용 중인 사원이 있으면 해당 사원의 역할이 초기화됩니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
loading={isDeleting}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
@@ -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() {
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>직급 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{rankToDelete?.name}" 직급을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
이 직급을 사용 중인 사원이 있으면 해당 사원의 직급이 초기화됩니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isSubmitting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
disabled={isSubmitting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : null}
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onConfirm={confirmDelete}
|
||||
title="직급 삭제"
|
||||
description={
|
||||
<>
|
||||
"{rankToDelete?.name}" 직급을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
이 직급을 사용 중인 사원이 있으면 해당 사원의 직급이 초기화됩니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
loading={isSubmitting}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -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) {
|
||||
</PageLayout>
|
||||
|
||||
{/* ===== 서비스 해지 확인 다이얼로그 ===== */}
|
||||
<AlertDialog open={showCancelDialog} onOpenChange={setShowCancelDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-red-500" />
|
||||
서비스 해지
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-left">
|
||||
모든 데이터가 삭제되며 복구할 수 없습니다.
|
||||
<br />
|
||||
<span className="font-medium text-red-600">
|
||||
정말 서비스를 해지하시겠습니까?
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isCancelling}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleCancelService}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
disabled={isCancelling}
|
||||
>
|
||||
{isCancelling ? '처리 중...' : '확인'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmDialog
|
||||
open={showCancelDialog}
|
||||
onOpenChange={setShowCancelDialog}
|
||||
onConfirm={handleCancelService}
|
||||
variant="destructive"
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-red-500" />
|
||||
서비스 해지
|
||||
</span>
|
||||
}
|
||||
description={
|
||||
<>
|
||||
모든 데이터가 삭제되며 복구할 수 없습니다.
|
||||
<br />
|
||||
<span className="font-medium text-red-600">
|
||||
정말 서비스를 해지하시겠습니까?
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
confirmText="확인"
|
||||
loading={isCancelling}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
</PageLayout>
|
||||
|
||||
{/* ===== 서비스 해지 확인 다이얼로그 ===== */}
|
||||
<AlertDialog open={showCancelDialog} onOpenChange={setShowCancelDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-red-500" />
|
||||
서비스 해지
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-left">
|
||||
모든 데이터가 삭제되며 복구할 수 없습니다.
|
||||
<br />
|
||||
<span className="font-medium text-red-600">
|
||||
정말 서비스를 해지하시겠습니까?
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isCancelling}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleCancelService}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
disabled={isCancelling}
|
||||
>
|
||||
{isCancelling ? '처리 중...' : '확인'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmDialog
|
||||
open={showCancelDialog}
|
||||
onOpenChange={setShowCancelDialog}
|
||||
onConfirm={handleCancelService}
|
||||
variant="destructive"
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-red-500" />
|
||||
서비스 해지
|
||||
</span>
|
||||
}
|
||||
description={
|
||||
<>
|
||||
모든 데이터가 삭제되며 복구할 수 없습니다.
|
||||
<br />
|
||||
<span className="font-medium text-red-600">
|
||||
정말 서비스를 해지하시겠습니까?
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
confirmText="확인"
|
||||
loading={isCancelling}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertTitle>직책 삭제</AlertTitle>
|
||||
<AlertDialogDescription>
|
||||
"{titleToDelete?.name}" 직책을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
이 직책을 사용 중인 사원이 있으면 해당 사원의 직책이 초기화됩니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isSubmitting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
disabled={isSubmitting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : null}
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onConfirm={confirmDelete}
|
||||
title="직책 삭제"
|
||||
description={
|
||||
<>
|
||||
"{titleToDelete?.name}" 직책을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
이 직책을 사용 중인 사원이 있으면 해당 사원의 직책이 초기화됩니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
loading={isSubmitting}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={cn('space-y-2 animate-pulse', colSpanClasses[colSpan], className)}>
|
||||
{/* 라벨 */}
|
||||
{showLabel && <Skeleton className="h-4 w-20" />}
|
||||
{/* 입력 필드 */}
|
||||
<Skeleton
|
||||
className={cn(
|
||||
'w-full rounded-md',
|
||||
type === 'textarea' ? 'h-24' : 'h-10'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 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 (
|
||||
<div className={cn('grid', colsClasses[cols], gapClasses[gap], className)}>
|
||||
{Array.from({ length: fieldCount }).map((_, i) => (
|
||||
<DetailFieldSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 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 (
|
||||
<Card className={cn('animate-pulse', className)}>
|
||||
{showTitle && (
|
||||
<CardHeader className="pb-4">
|
||||
<Skeleton className="h-5 w-32" />
|
||||
</CardHeader>
|
||||
)}
|
||||
<CardContent className={showTitle ? 'pt-0' : undefined}>
|
||||
<DetailGridSkeleton cols={cols} fieldCount={fieldCount} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 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 (
|
||||
<div className={cn('space-y-6 animate-pulse', className)}>
|
||||
{/* 페이지 헤더 */}
|
||||
{showHeader && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-4 w-64" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-10 w-20 rounded-md" />
|
||||
<Skeleton className="h-10 w-20 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 섹션들 */}
|
||||
{Array.from({ length: sections }).map((_, i) => (
|
||||
<DetailSectionSkeleton
|
||||
key={i}
|
||||
cols={cols}
|
||||
fieldCount={fieldsPerSection}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailSectionSkeleton;
|
||||
@@ -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<T extends Record<string, unknown>>({
|
||||
{afterContent}
|
||||
{/* 버튼 영역 - 하단 배치 시만 */}
|
||||
{!isTopButtons && renderActionButtons('mt-6')}
|
||||
<DeleteDialog
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleConfirmDelete}
|
||||
@@ -463,7 +454,7 @@ export function IntegratedDetailTemplate<T extends Record<string, unknown>>({
|
||||
</div>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<DeleteDialog
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleConfirmDelete}
|
||||
@@ -531,43 +522,6 @@ export function IntegratedDetailTemplate<T extends Record<string, unknown>>({
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 삭제 확인 다이얼로그 =====
|
||||
function DeleteDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: () => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{title || '삭제 확인'}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{description || '정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.'}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onConfirm}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 유효성 검사 헬퍼 =====
|
||||
function validateRule(
|
||||
rule: ValidationRule,
|
||||
|
||||
@@ -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<T = any>({
|
||||
|
||||
{/* 모바일/태블릿/소형 노트북 (~1279px) 카드 뷰 */}
|
||||
<div className="xl:hidden space-y-4 md:space-y-0 md:grid md:grid-cols-2 md:gap-4 lg:grid-cols-3">
|
||||
{mobileData.length === 0 ? (
|
||||
{isLoading ? (
|
||||
<div className="col-span-full">
|
||||
<MobileCardGridSkeleton count={6} showCheckbox={showCheckbox} />
|
||||
</div>
|
||||
) : mobileData.length === 0 ? (
|
||||
<div className="text-center py-6 text-muted-foreground border rounded-lg text-[14px]">
|
||||
검색 결과가 없습니다.
|
||||
</div>
|
||||
@@ -739,6 +735,14 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
|
||||
{/* 데스크톱 (1280px+) 테이블 뷰 */}
|
||||
<div className="hidden xl:block rounded-md border overflow-x-auto [&::-webkit-scrollbar]:h-3 [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:hover:bg-gray-400" style={{ scrollbarWidth: 'thin', scrollbarColor: '#d1d5db #f3f4f6' }}>
|
||||
{isLoading ? (
|
||||
<TableSkeleton
|
||||
rows={pagination.itemsPerPage || 10}
|
||||
columns={tableColumns.length}
|
||||
showCheckbox={showCheckbox}
|
||||
showActions={tableColumns.some(col => col.key === 'actions')}
|
||||
/>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -801,6 +805,7 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
</TableFooter>
|
||||
)}
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
))}
|
||||
@@ -863,44 +868,21 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 일괄 삭제 확인 다이얼로그 - 단일 삭제와 동일한 디자인 */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent className="max-w-md">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2">
|
||||
<span className="text-yellow-600">⚠️</span>
|
||||
삭제 확인
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="space-y-4">
|
||||
<p className="text-foreground">
|
||||
선택한 <strong>{selectedItems.size}개</strong>의 항목을 삭제하시겠습니까?
|
||||
</p>
|
||||
<div className="bg-gray-100 rounded-lg p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-yellow-600 mt-0.5">⚠️</span>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">주의</span>
|
||||
<br />
|
||||
삭제된 항목은 복구할 수 없습니다. 관련된 데이터도 함께 삭제될 수 있습니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-gray-900 hover:bg-gray-800 text-white gap-2"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
{/* 일괄 삭제 확인 다이얼로그 */}
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleConfirmDelete}
|
||||
description={
|
||||
<>
|
||||
선택한 <strong>{selectedItems.size}개</strong>의 항목을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
삭제된 항목은 복구할 수 없습니다. 관련된 데이터도 함께 삭제될 수 있습니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<T>({
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{deleteConfirmTitle}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{deleteConfirmDescription}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeleteConfirm}>삭제</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
title={deleteConfirmTitle}
|
||||
description={deleteConfirmDescription}
|
||||
/>
|
||||
|
||||
{/* 상세 모달 (detailMode === 'modal'일 때) */}
|
||||
{config.detailMode === 'modal' && config.DetailModalComponent && (
|
||||
|
||||
216
src/components/ui/confirm-dialog.tsx
Normal file
216
src/components/ui/confirm-dialog.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* ConfirmDialog - 확인/취소 다이얼로그 공통 컴포넌트
|
||||
*
|
||||
* 사용 예시:
|
||||
* ```tsx
|
||||
* <ConfirmDialog
|
||||
* open={showDeleteDialog}
|
||||
* onOpenChange={setShowDeleteDialog}
|
||||
* title="삭제 확인"
|
||||
* description="정말 삭제하시겠습니까?"
|
||||
* confirmText="삭제"
|
||||
* variant="destructive"
|
||||
* loading={isLoading}
|
||||
* onConfirm={handleDelete}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
|
||||
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<void>;
|
||||
/** 취소 버튼 클릭 핸들러 (선택사항) */
|
||||
onCancel?: () => void;
|
||||
/** 확인 버튼 비활성화 여부 */
|
||||
confirmDisabled?: boolean;
|
||||
/** 아이콘 (제목 옆에 표시) */
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
const variantStyles: Record<ConfirmDialogVariant, string> = {
|
||||
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 (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2">
|
||||
{icon}
|
||||
{title}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild={typeof description !== 'string'}>
|
||||
{typeof description === 'string' ? (
|
||||
description
|
||||
) : (
|
||||
<div>{description}</div>
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleCancel} disabled={isLoading}>
|
||||
{cancelText}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirm}
|
||||
disabled={isLoading || confirmDisabled}
|
||||
className={cn(variantStyles[variant])}
|
||||
>
|
||||
{isLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
{confirmText}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제 확인 다이얼로그 프리셋
|
||||
*/
|
||||
export interface DeleteConfirmDialogProps
|
||||
extends Omit<ConfirmDialogProps, 'title' | 'confirmText' | 'variant'> {
|
||||
/** 삭제 대상 이름 (선택사항) */
|
||||
itemName?: string;
|
||||
}
|
||||
|
||||
export function DeleteConfirmDialog({
|
||||
itemName,
|
||||
description,
|
||||
...props
|
||||
}: DeleteConfirmDialogProps) {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
title="삭제 확인"
|
||||
description={
|
||||
description ?? (
|
||||
<>
|
||||
{itemName ? `"${itemName}"을(를) ` : ''}정말 삭제하시겠습니까?
|
||||
<br />
|
||||
삭제된 데이터는 복구할 수 없습니다.
|
||||
</>
|
||||
)
|
||||
}
|
||||
confirmText="삭제"
|
||||
variant="destructive"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장 확인 다이얼로그 프리셋
|
||||
*/
|
||||
export interface SaveConfirmDialogProps
|
||||
extends Omit<ConfirmDialogProps, 'title' | 'confirmText' | 'variant'> {}
|
||||
|
||||
export function SaveConfirmDialog({
|
||||
description = '변경사항을 저장하시겠습니까?',
|
||||
...props
|
||||
}: SaveConfirmDialogProps) {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
title="저장 확인"
|
||||
description={description}
|
||||
confirmText="저장"
|
||||
variant="default"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 취소 확인 다이얼로그 프리셋
|
||||
*/
|
||||
export interface CancelConfirmDialogProps
|
||||
extends Omit<ConfirmDialogProps, 'title' | 'confirmText' | 'variant'> {}
|
||||
|
||||
export function CancelConfirmDialog({
|
||||
description = '작업을 취소하시겠습니까? 변경사항이 저장되지 않습니다.',
|
||||
...props
|
||||
}: CancelConfirmDialogProps) {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
title="취소 확인"
|
||||
description={description}
|
||||
confirmText="취소"
|
||||
variant="warning"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfirmDialog;
|
||||
226
src/components/ui/empty-state.tsx
Normal file
226
src/components/ui/empty-state.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* EmptyState - 빈 상태 표시용 공통 컴포넌트
|
||||
*
|
||||
* 사용 예시:
|
||||
* ```tsx
|
||||
* // 기본 사용
|
||||
* <EmptyState message="데이터가 없습니다." />
|
||||
*
|
||||
* // 아이콘과 설명 포함
|
||||
* <EmptyState
|
||||
* icon={<FileSearch className="h-12 w-12" />}
|
||||
* message="검색 결과가 없습니다."
|
||||
* description="다른 검색어로 다시 시도해 주세요."
|
||||
* />
|
||||
*
|
||||
* // 액션 버튼 포함
|
||||
* <EmptyState
|
||||
* icon={<Inbox className="h-12 w-12" />}
|
||||
* message="등록된 항목이 없습니다."
|
||||
* action={{
|
||||
* label: "새로 등록",
|
||||
* onClick: () => router.push('/new'),
|
||||
* }}
|
||||
* />
|
||||
*
|
||||
* // 테이블 내 사용 (compact)
|
||||
* <EmptyState
|
||||
* message="데이터가 없습니다."
|
||||
* variant="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: <Inbox className="h-12 w-12" />,
|
||||
message: '데이터가 없습니다.',
|
||||
description: '아직 등록된 데이터가 없습니다.',
|
||||
},
|
||||
noResults: {
|
||||
icon: <FileSearch className="h-12 w-12" />,
|
||||
message: '검색 결과가 없습니다.',
|
||||
description: '다른 검색어로 다시 시도해 주세요.',
|
||||
},
|
||||
noItems: {
|
||||
icon: <FolderOpen className="h-12 w-12" />,
|
||||
message: '등록된 항목이 없습니다.',
|
||||
description: '새 항목을 등록해 주세요.',
|
||||
},
|
||||
error: {
|
||||
icon: <AlertCircle className="h-12 w-12" />,
|
||||
message: '데이터를 불러올 수 없습니다.',
|
||||
description: '잠시 후 다시 시도해 주세요.',
|
||||
},
|
||||
};
|
||||
|
||||
// Variant 스타일
|
||||
const variantStyles: Record<EmptyStateVariant, { container: string; icon: string; text: string }> = {
|
||||
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 (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center gap-4',
|
||||
styles.container,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children ? (
|
||||
children
|
||||
) : (
|
||||
<>
|
||||
{finalIcon && (
|
||||
<div className="text-muted-foreground/50">{finalIcon}</div>
|
||||
)}
|
||||
|
||||
<div className="text-center space-y-1">
|
||||
<p className={cn('text-muted-foreground font-medium', styles.text)}>
|
||||
{finalMessage}
|
||||
</p>
|
||||
{finalDescription && (
|
||||
<p className="text-muted-foreground/70 text-sm">
|
||||
{finalDescription}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{action && (
|
||||
<Button
|
||||
variant={action.variant ?? 'outline'}
|
||||
onClick={action.onClick}
|
||||
className="mt-2"
|
||||
>
|
||||
{action.icon}
|
||||
{action.label}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블용 빈 상태 컴포넌트
|
||||
* TableCell 내에서 사용할 때 유용
|
||||
*/
|
||||
export interface TableEmptyStateProps {
|
||||
/** 컬럼 수 (colSpan 용) */
|
||||
colSpan: number;
|
||||
/** 메시지 */
|
||||
message?: string;
|
||||
/** variant */
|
||||
variant?: 'default' | 'compact';
|
||||
}
|
||||
|
||||
export function TableEmptyState({
|
||||
colSpan,
|
||||
message = '데이터가 없습니다.',
|
||||
variant = 'default',
|
||||
}: TableEmptyStateProps) {
|
||||
return (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={colSpan}
|
||||
className={cn(
|
||||
'text-center text-muted-foreground',
|
||||
variant === 'compact' ? 'py-6' : 'py-12'
|
||||
)}
|
||||
>
|
||||
{message}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmptyState;
|
||||
@@ -1,9 +1,30 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
'use client';
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
/**
|
||||
* 스켈레톤 컴포넌트 시스템
|
||||
*
|
||||
* 사용 가이드:
|
||||
* - 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<HTMLDivElement> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Skeleton({ className, ...props }: SkeletonProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn('animate-pulse rounded-md bg-muted', className)}
|
||||
@@ -12,4 +33,469 @@ function Skeleton({
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
// ============================================
|
||||
// 2. 테이블 행 스켈레톤
|
||||
// ============================================
|
||||
interface TableRowSkeletonProps {
|
||||
columns?: number;
|
||||
showCheckbox?: boolean;
|
||||
showActions?: boolean;
|
||||
}
|
||||
|
||||
function TableRowSkeleton({
|
||||
columns = 5,
|
||||
showCheckbox = true,
|
||||
showActions = true,
|
||||
}: TableRowSkeletonProps) {
|
||||
const totalCols = columns + (showCheckbox ? 1 : 0) + (showActions ? 1 : 0);
|
||||
|
||||
return (
|
||||
<tr className="border-b">
|
||||
{showCheckbox && (
|
||||
<td className="p-4 w-[50px]">
|
||||
<Skeleton className="h-4 w-4 rounded" />
|
||||
</td>
|
||||
)}
|
||||
{Array.from({ length: columns }).map((_, i) => (
|
||||
<td key={i} className="p-4">
|
||||
<Skeleton
|
||||
className={cn(
|
||||
'h-4',
|
||||
i === 0 ? 'w-12' : i === 1 ? 'w-32' : 'w-24'
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
{showActions && (
|
||||
<td className="p-4 w-[100px]">
|
||||
<Skeleton className="h-8 w-16 rounded" />
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 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 (
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full">
|
||||
{showHeader && (
|
||||
<thead className="bg-muted/50">
|
||||
<tr className="border-b">
|
||||
{showCheckbox && (
|
||||
<th className="p-4 w-[50px]">
|
||||
<Skeleton className="h-4 w-4 rounded" />
|
||||
</th>
|
||||
)}
|
||||
{Array.from({ length: columns }).map((_, i) => (
|
||||
<th key={i} className="p-4 text-left">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</th>
|
||||
))}
|
||||
{showActions && (
|
||||
<th className="p-4 w-[100px]">
|
||||
<Skeleton className="h-4 w-12" />
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
)}
|
||||
<tbody>
|
||||
{Array.from({ length: rows }).map((_, i) => (
|
||||
<TableRowSkeleton
|
||||
key={i}
|
||||
columns={columns}
|
||||
showCheckbox={showCheckbox}
|
||||
showActions={showActions}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 4. 모바일 카드 스켈레톤
|
||||
// ============================================
|
||||
interface MobileCardSkeletonProps {
|
||||
showCheckbox?: boolean;
|
||||
showBadge?: boolean;
|
||||
fields?: number;
|
||||
showActions?: boolean;
|
||||
}
|
||||
|
||||
function MobileCardSkeleton({
|
||||
showCheckbox = true,
|
||||
showBadge = true,
|
||||
fields = 4,
|
||||
showActions = true,
|
||||
}: MobileCardSkeletonProps) {
|
||||
return (
|
||||
<Card className="animate-pulse">
|
||||
<CardContent className="p-4">
|
||||
{/* 헤더 영역 */}
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
{showCheckbox && <Skeleton className="h-5 w-5 rounded flex-shrink-0" />}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Skeleton className="h-5 w-32" />
|
||||
{showBadge && <Skeleton className="h-5 w-16 rounded-full" />}
|
||||
</div>
|
||||
<Skeleton className="h-4 w-24 mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 정보 그리드 */}
|
||||
<div className="grid grid-cols-2 gap-2 mt-3">
|
||||
{Array.from({ length: fields }).map((_, i) => (
|
||||
<div key={i} className="space-y-1">
|
||||
<Skeleton className="h-3 w-12" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 액션 버튼 */}
|
||||
{showActions && (
|
||||
<div className="flex gap-2 mt-4 pt-3 border-t">
|
||||
<Skeleton className="h-8 w-16 rounded" />
|
||||
<Skeleton className="h-8 w-16 rounded" />
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 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 (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<MobileCardSkeleton
|
||||
key={i}
|
||||
showCheckbox={showCheckbox}
|
||||
showBadge={showBadge}
|
||||
fields={fields}
|
||||
showActions={showActions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 6. 폼 필드 스켈레톤
|
||||
// ============================================
|
||||
interface FormFieldSkeletonProps {
|
||||
showLabel?: boolean;
|
||||
type?: 'input' | 'textarea' | 'select';
|
||||
}
|
||||
|
||||
function FormFieldSkeleton({
|
||||
showLabel = true,
|
||||
type = 'input',
|
||||
}: FormFieldSkeletonProps) {
|
||||
return (
|
||||
<div className="space-y-2 animate-pulse">
|
||||
{showLabel && <Skeleton className="h-4 w-20" />}
|
||||
<Skeleton
|
||||
className={cn(
|
||||
'w-full rounded-md',
|
||||
type === 'textarea' ? 'h-24' : 'h-10'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 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 (
|
||||
<Card className="animate-pulse">
|
||||
{title && (
|
||||
<CardHeader className="pb-4">
|
||||
<Skeleton className="h-5 w-32" />
|
||||
</CardHeader>
|
||||
)}
|
||||
<CardContent>
|
||||
<div className={cn('grid gap-4', gridCols[columns])}>
|
||||
{Array.from({ length: fields }).map((_, i) => (
|
||||
<FormFieldSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 8. 상세 페이지 스켈레톤
|
||||
// ============================================
|
||||
interface DetailPageSkeletonProps {
|
||||
sections?: number;
|
||||
fieldsPerSection?: number;
|
||||
showHeader?: boolean;
|
||||
}
|
||||
|
||||
function DetailPageSkeleton({
|
||||
sections = 2,
|
||||
fieldsPerSection = 6,
|
||||
showHeader = true,
|
||||
}: DetailPageSkeletonProps) {
|
||||
return (
|
||||
<div className="space-y-6 animate-pulse">
|
||||
{/* 페이지 헤더 */}
|
||||
{showHeader && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-4 w-64" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-10 w-20 rounded-md" />
|
||||
<Skeleton className="h-10 w-20 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 섹션들 */}
|
||||
{Array.from({ length: sections }).map((_, i) => (
|
||||
<FormSectionSkeleton
|
||||
key={i}
|
||||
title={true}
|
||||
fields={fieldsPerSection}
|
||||
columns={2}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 9. 통계 카드 스켈레톤
|
||||
// ============================================
|
||||
interface StatCardSkeletonProps {
|
||||
showIcon?: boolean;
|
||||
showTrend?: boolean;
|
||||
}
|
||||
|
||||
function StatCardSkeleton({
|
||||
showIcon = true,
|
||||
showTrend = true,
|
||||
}: StatCardSkeletonProps) {
|
||||
return (
|
||||
<Card className="animate-pulse">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-8 w-24" />
|
||||
{showTrend && <Skeleton className="h-3 w-16" />}
|
||||
</div>
|
||||
{showIcon && <Skeleton className="h-12 w-12 rounded-lg" />}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 10. 통계 카드 그리드 스켈레톤
|
||||
// ============================================
|
||||
interface StatCardGridSkeletonProps {
|
||||
count?: number;
|
||||
showIcon?: boolean;
|
||||
showTrend?: boolean;
|
||||
}
|
||||
|
||||
function StatCardGridSkeleton({
|
||||
count = 4,
|
||||
showIcon = true,
|
||||
showTrend = true,
|
||||
}: StatCardGridSkeletonProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<StatCardSkeleton key={i} showIcon={showIcon} showTrend={showTrend} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 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 (
|
||||
<div className="space-y-6 animate-pulse">
|
||||
{/* 페이지 헤더 */}
|
||||
{showHeader && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-40" />
|
||||
<Skeleton className="h-4 w-56" />
|
||||
</div>
|
||||
<Skeleton className="h-10 w-24 rounded-md" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 통계 카드 */}
|
||||
{showStats && <StatCardGridSkeleton count={statsCount} />}
|
||||
|
||||
{/* 필터 영역 */}
|
||||
{showFilters && (
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Skeleton className="h-10 w-40 rounded-md" />
|
||||
<Skeleton className="h-10 w-32 rounded-md" />
|
||||
<Skeleton className="h-10 w-32 rounded-md" />
|
||||
<Skeleton className="h-10 w-24 rounded-md" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 데스크톱: 테이블 */}
|
||||
<div className="hidden xl:block">
|
||||
<TableSkeleton rows={tableRows} columns={tableColumns} />
|
||||
</div>
|
||||
|
||||
{/* 모바일/태블릿: 카드 그리드 */}
|
||||
<div className="xl:hidden">
|
||||
<MobileCardGridSkeleton count={mobileCards} />
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-10 w-10 rounded-md" />
|
||||
<Skeleton className="h-10 w-10 rounded-md" />
|
||||
<Skeleton className="h-10 w-10 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 12. 페이지 헤더 스켈레톤
|
||||
// ============================================
|
||||
interface PageHeaderSkeletonProps {
|
||||
showActions?: boolean;
|
||||
actionsCount?: number;
|
||||
}
|
||||
|
||||
function PageHeaderSkeleton({
|
||||
showActions = true,
|
||||
actionsCount = 2,
|
||||
}: PageHeaderSkeletonProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between mb-6 animate-pulse">
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="h-10 w-10 rounded-lg" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-7 w-40" />
|
||||
<Skeleton className="h-4 w-56" />
|
||||
</div>
|
||||
</div>
|
||||
{showActions && (
|
||||
<div className="flex gap-2">
|
||||
{Array.from({ length: actionsCount }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-10 w-20 rounded-md" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Export
|
||||
// ============================================
|
||||
export {
|
||||
Skeleton,
|
||||
TableRowSkeleton,
|
||||
TableSkeleton,
|
||||
MobileCardSkeleton,
|
||||
MobileCardGridSkeleton,
|
||||
FormFieldSkeleton,
|
||||
FormSectionSkeleton,
|
||||
DetailPageSkeleton,
|
||||
StatCardSkeleton,
|
||||
StatCardGridSkeleton,
|
||||
ListPageSkeleton,
|
||||
PageHeaderSkeleton,
|
||||
};
|
||||
|
||||
122
src/components/ui/status-badge.tsx
Normal file
122
src/components/ui/status-badge.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* StatusBadge - 상태 표시용 배지 컴포넌트
|
||||
*
|
||||
* 사용 예시:
|
||||
* ```tsx
|
||||
* // 기본 사용 (프리셋 variant)
|
||||
* <StatusBadge variant="success">완료</StatusBadge>
|
||||
* <StatusBadge variant="warning">대기</StatusBadge>
|
||||
*
|
||||
* // 커스텀 className
|
||||
* <StatusBadge className="bg-purple-100 text-purple-800">커스텀</StatusBadge>
|
||||
*
|
||||
* // createStatusConfig와 함께 사용
|
||||
* <StatusBadge className={STATUS_STYLES[status]}>
|
||||
* {STATUS_LABELS[status]}
|
||||
* </StatusBadge>
|
||||
*
|
||||
* // 또는 간단하게
|
||||
* <StatusBadge status={status} config={statusConfig} />
|
||||
* ```
|
||||
*/
|
||||
|
||||
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<string>;
|
||||
}
|
||||
|
||||
// 크기별 스타일
|
||||
const sizeStyles: Record<StatusBadgeSize, string> = {
|
||||
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 (
|
||||
<span className={cn(baseStyle, sizeStyles[size], finalStyle)}>
|
||||
{displayContent}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 간단한 상태 표시용 컴포넌트
|
||||
* createStatusConfig의 결과와 함께 사용
|
||||
*/
|
||||
export interface ConfiguredStatusBadgeProps<T extends string> {
|
||||
status: T;
|
||||
config: StatusConfig<T>;
|
||||
size?: StatusBadgeSize;
|
||||
mode?: StatusBadgeMode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ConfiguredStatusBadge<T extends string>({
|
||||
status,
|
||||
config,
|
||||
size = 'md',
|
||||
mode = 'badge',
|
||||
className,
|
||||
}: ConfiguredStatusBadgeProps<T>) {
|
||||
return (
|
||||
<StatusBadge
|
||||
size={size}
|
||||
mode={mode}
|
||||
className={cn(config.getStatusStyle(status), className)}
|
||||
>
|
||||
{config.getStatusLabel(status)}
|
||||
</StatusBadge>
|
||||
);
|
||||
}
|
||||
|
||||
export default StatusBadge;
|
||||
154
src/lib/utils/status-config.ts
Normal file
154
src/lib/utils/status-config.ts
Normal file
@@ -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<StatusStylePreset, string> = {
|
||||
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<StatusStylePreset, string> = {
|
||||
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<T extends string> {
|
||||
/** Select 컴포넌트용 옵션 배열 */
|
||||
STATUS_OPTIONS: ReadonlyArray<{ value: T | 'all'; label: string }>;
|
||||
/** 상태별 라벨 맵 */
|
||||
STATUS_LABELS: Record<T, string>;
|
||||
/** 상태별 스타일 맵 */
|
||||
STATUS_STYLES: Record<T, string>;
|
||||
/** 상태값으로 라벨 가져오기 */
|
||||
getStatusLabel: (status: T) => string;
|
||||
/** 상태값으로 스타일 가져오기 */
|
||||
getStatusStyle: (status: T) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 설정 생성 유틸리티
|
||||
*
|
||||
* @param config - 상태별 설정 객체
|
||||
* @param options - 추가 옵션
|
||||
* @returns STATUS_OPTIONS, STATUS_LABELS, STATUS_STYLES 및 헬퍼 함수
|
||||
*/
|
||||
export function createStatusConfig<T extends string>(
|
||||
config: Record<T, StatusItemConfig>,
|
||||
options: CreateStatusConfigOptions = {}
|
||||
): StatusConfig<T> {
|
||||
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<T, string>
|
||||
);
|
||||
|
||||
// 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<T, string>
|
||||
);
|
||||
|
||||
// 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;
|
||||
Reference in New Issue
Block a user