feat(WEB): IntegratedDetailTemplate 통합 템플릿 구현 및 Phase 1~8 마이그레이션
- Phase 1: 기안함(DocumentCreate) 마이그레이션 - Phase 2: 작업지시(WorkOrderCreate/Edit) 마이그레이션 - Phase 3: 출하(ShipmentCreate/Edit) 마이그레이션 - Phase 4: 사원(EmployeeForm) 마이그레이션 - Phase 5: 게시판(BoardForm) 마이그레이션 - Phase 6: 1:1문의(InquiryForm) 마이그레이션 - Phase 7: 공정(ProcessForm) 마이그레이션 - Phase 8: 수입검사/품질검사(InspectionCreate) 마이그레이션 - DetailActions에 showSave 옵션 추가 - 각 도메인별 config 파일 생성 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
|||||||
|
# IntegratedDetailTemplate 마이그레이션 체크리스트
|
||||||
|
|
||||||
|
> 작성일: 2025-01-20
|
||||||
|
> 목적: 별도 디자인 사용 중인 등록/수정 페이지를 IntegratedDetailTemplate으로 통합
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 마이그레이션 완료
|
||||||
|
|
||||||
|
### Phase 1 - 기안함 (2025-01-20)
|
||||||
|
- [x] DocumentCreate (기안함 등록/수정)
|
||||||
|
- 파일: `src/components/approval/DocumentCreate/index.tsx`
|
||||||
|
- config: `src/components/approval/DocumentCreate/documentCreateConfig.ts`
|
||||||
|
- 특이사항: 커스텀 headerActions (미리보기, 삭제, 상신, 임시저장)
|
||||||
|
|
||||||
|
### Phase 2 - 생산관리 (2025-01-20)
|
||||||
|
- [x] WorkOrderCreate (작업지시 등록)
|
||||||
|
- 파일: `src/components/production/WorkOrders/WorkOrderCreate.tsx`
|
||||||
|
- config: `src/components/production/WorkOrders/workOrderConfig.ts`
|
||||||
|
- [x] WorkOrderEdit (작업지시 수정)
|
||||||
|
- 파일: `src/components/production/WorkOrders/WorkOrderEdit.tsx`
|
||||||
|
- config: 동일 파일 (workOrderEditConfig)
|
||||||
|
|
||||||
|
### Phase 3 - 출고관리 (2025-01-20)
|
||||||
|
- [x] ShipmentCreate (출하 등록)
|
||||||
|
- 파일: `src/components/outbound/ShipmentManagement/ShipmentCreate.tsx`
|
||||||
|
- config: `src/components/outbound/ShipmentManagement/shipmentConfig.ts`
|
||||||
|
- [x] ShipmentEdit (출하 수정)
|
||||||
|
- 파일: `src/components/outbound/ShipmentManagement/ShipmentEdit.tsx`
|
||||||
|
- config: 동일 파일 (shipmentEditConfig)
|
||||||
|
|
||||||
|
### Phase 4 - HR (2025-01-20)
|
||||||
|
- [x] EmployeeForm (사원 등록/수정/상세)
|
||||||
|
- 파일: `src/components/hr/EmployeeManagement/EmployeeForm.tsx`
|
||||||
|
- config: `src/components/hr/EmployeeManagement/employeeConfig.ts`
|
||||||
|
- 특이사항: "항목 설정" 버튼, 복잡한 섹션 구조, view 모드 지원
|
||||||
|
|
||||||
|
### Phase 5 - 게시판 (2025-01-20)
|
||||||
|
- [x] BoardForm (게시판 글쓰기/수정)
|
||||||
|
- 파일: `src/components/board/BoardForm/index.tsx`
|
||||||
|
- config: `src/components/board/BoardForm/boardFormConfig.ts`
|
||||||
|
- 특이사항: 동적 게시판 코드 기반
|
||||||
|
|
||||||
|
### Phase 6 - 고객센터 (2025-01-20)
|
||||||
|
- [x] InquiryForm (문의 등록/수정)
|
||||||
|
- 파일: `src/components/customer-center/InquiryManagement/InquiryForm.tsx`
|
||||||
|
- config: `src/components/customer-center/InquiryManagement/inquiryConfig.ts`
|
||||||
|
|
||||||
|
### Phase 7 - 기준정보 (2025-01-20)
|
||||||
|
- [x] ProcessForm (공정 등록/수정)
|
||||||
|
- 파일: `src/components/process-management/ProcessForm.tsx`
|
||||||
|
- config: `src/components/process-management/processConfig.ts`
|
||||||
|
|
||||||
|
### Phase 8 - 자재/품질 (2025-01-20)
|
||||||
|
- [x] InspectionCreate - 자재 (수입검사 등록)
|
||||||
|
- 파일: `src/components/material/ReceivingManagement/InspectionCreate.tsx`
|
||||||
|
- config: `src/components/material/ReceivingManagement/inspectionConfig.ts`
|
||||||
|
- [x] InspectionCreate - 품질 (품질검사 등록)
|
||||||
|
- 파일: `src/components/quality/InspectionManagement/InspectionCreate.tsx`
|
||||||
|
- config: `src/components/quality/InspectionManagement/inspectionConfig.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 마이그레이션 제외 (특수 레이아웃)
|
||||||
|
|
||||||
|
| 페이지 | 사유 |
|
||||||
|
|--------|------|
|
||||||
|
| CEO 대시보드 | 대시보드 (특수 레이아웃) |
|
||||||
|
| 생산 대시보드 | 대시보드 (특수 레이아웃) |
|
||||||
|
| 작업자 화면 | 특수 UI |
|
||||||
|
| 설정 페이지들 | 트리 구조, 특수 레이아웃 |
|
||||||
|
| 부서 관리 | 트리 구조 |
|
||||||
|
| 일일보고서 | 특수 레이아웃 |
|
||||||
|
| 미수금현황 | 특수 레이아웃 |
|
||||||
|
| 종합분석 | 특수 레이아웃 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 마이그레이션 패턴
|
||||||
|
|
||||||
|
### 1. Config 파일 생성/수정
|
||||||
|
```typescript
|
||||||
|
export const xxxCreateConfig: DetailConfig = {
|
||||||
|
title: '페이지 제목',
|
||||||
|
description: '페이지 설명',
|
||||||
|
icon: IconComponent,
|
||||||
|
basePath: '/base/path',
|
||||||
|
fields: [],
|
||||||
|
actions: {
|
||||||
|
showBack: true,
|
||||||
|
showEdit: false,
|
||||||
|
showDelete: false,
|
||||||
|
showSave: true, // false로 설정하면 기본 저장 버튼 숨김
|
||||||
|
submitLabel: '등록',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 컴포넌트 수정
|
||||||
|
```typescript
|
||||||
|
// 변경 전
|
||||||
|
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||||
|
return <PageLayout>...</PageLayout>;
|
||||||
|
|
||||||
|
// 변경 후
|
||||||
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||||
|
const renderFormContent = useCallback(() => (...), [deps]);
|
||||||
|
return (
|
||||||
|
<IntegratedDetailTemplate
|
||||||
|
config={config}
|
||||||
|
mode="create"
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
onBack={handleCancel}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
renderForm={renderFormContent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 커스텀 버튼이 필요한 경우
|
||||||
|
- config에서 `showSave: false` 설정
|
||||||
|
- `headerActions` prop으로 커스텀 버튼 전달
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 검증 URL
|
||||||
|
|
||||||
|
마이그레이션 완료 후 확인할 URL:
|
||||||
|
- `/approval/draft/new` - 기안함 등록
|
||||||
|
- `/production/work-orders/create` - 작업지시 등록
|
||||||
|
- `/outbound/shipments/new` - 출하 등록
|
||||||
|
- `/hr/employee-management/new` - 사원 등록
|
||||||
|
- `/boards/notice/create` - 게시판 글쓰기
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 변경 이력
|
||||||
|
|
||||||
|
| 날짜 | 작업 내용 |
|
||||||
|
|------|----------|
|
||||||
|
| 2025-01-20 | 기안함 마이그레이션 완료 |
|
||||||
|
| 2025-01-20 | 작업지시 등록/수정 마이그레이션 완료 |
|
||||||
|
| 2025-01-20 | DetailActions에 showSave 옵션 추가 |
|
||||||
|
| 2025-01-20 | 출하 등록/수정 마이그레이션 완료 |
|
||||||
|
| 2025-01-20 | 사원 등록/수정/상세 마이그레이션 완료 |
|
||||||
|
| 2025-01-20 | 게시판 글쓰기/수정 마이그레이션 완료 |
|
||||||
|
| 2025-01-20 | 1:1 문의 등록/수정 마이그레이션 완료 |
|
||||||
|
| 2025-01-20 | 공정 등록/수정 마이그레이션 완료 |
|
||||||
|
| 2025-01-20 | 수입검사(IQC) 등록 마이그레이션 완료 |
|
||||||
|
| 2025-01-20 | 품질검사(PQC) 등록 마이그레이션 완료 |
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* 기안 문서 작성/수정 페이지 설정
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FileText } from 'lucide-react';
|
||||||
|
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||||
|
|
||||||
|
export const documentCreateConfig: DetailConfig = {
|
||||||
|
title: '문서 작성',
|
||||||
|
description: '새로운 결재 문서를 작성합니다',
|
||||||
|
icon: FileText,
|
||||||
|
basePath: '/approval/draft',
|
||||||
|
fields: [],
|
||||||
|
actions: {
|
||||||
|
showBack: true,
|
||||||
|
showEdit: false,
|
||||||
|
showDelete: false, // 커스텀 삭제 버튼 사용
|
||||||
|
showSave: false, // 상신/임시저장 버튼 사용
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const documentEditConfig: DetailConfig = {
|
||||||
|
...documentCreateConfig,
|
||||||
|
title: '문서 수정',
|
||||||
|
description: '기존 결재 문서를 수정합니다',
|
||||||
|
// actions는 documentCreateConfig에서 상속 (커스텀 버튼 사용)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const documentCopyConfig: DetailConfig = {
|
||||||
|
...documentCreateConfig,
|
||||||
|
title: '문서 복제',
|
||||||
|
description: '복제된 문서를 수정 후 상신합니다',
|
||||||
|
};
|
||||||
@@ -3,13 +3,16 @@
|
|||||||
import { useState, useCallback, useEffect, useTransition, useRef } from 'react';
|
import { useState, useCallback, useEffect, useTransition, useRef } from 'react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { FileText, Trash2, Send, Save, ArrowLeft, Eye } from 'lucide-react';
|
import { Trash2, Send, Save, Eye, Loader2 } from 'lucide-react';
|
||||||
import { Loader2 } from 'lucide-react';
|
|
||||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||||
|
import {
|
||||||
|
documentCreateConfig,
|
||||||
|
documentEditConfig,
|
||||||
|
documentCopyConfig,
|
||||||
|
} from './documentCreateConfig';
|
||||||
import {
|
import {
|
||||||
getExpenseEstimateItems,
|
getExpenseEstimateItems,
|
||||||
getEmployees,
|
|
||||||
createApproval,
|
createApproval,
|
||||||
createAndSubmitApproval,
|
createAndSubmitApproval,
|
||||||
getApprovalById,
|
getApprovalById,
|
||||||
@@ -18,7 +21,6 @@ import {
|
|||||||
deleteApproval,
|
deleteApproval,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
|
||||||
import { BasicInfoSection } from './BasicInfoSection';
|
import { BasicInfoSection } from './BasicInfoSection';
|
||||||
import { ApprovalLineSection } from './ApprovalLineSection';
|
import { ApprovalLineSection } from './ApprovalLineSection';
|
||||||
import { ReferenceSection } from './ReferenceSection';
|
import { ReferenceSection } from './ReferenceSection';
|
||||||
@@ -33,7 +35,6 @@ import type {
|
|||||||
ExpenseEstimateDocumentData,
|
ExpenseEstimateDocumentData,
|
||||||
} from '@/components/approval/DocumentDetail/types';
|
} from '@/components/approval/DocumentDetail/types';
|
||||||
import type {
|
import type {
|
||||||
DocumentType,
|
|
||||||
BasicInfo,
|
BasicInfo,
|
||||||
ApprovalPerson,
|
ApprovalPerson,
|
||||||
ProposalData,
|
ProposalData,
|
||||||
@@ -416,7 +417,7 @@ export function DocumentCreate() {
|
|||||||
approvers,
|
approvers,
|
||||||
drafter,
|
drafter,
|
||||||
};
|
};
|
||||||
default:
|
default: {
|
||||||
// 이미 업로드된 파일 URL (Next.js 프록시 사용) + 새로 추가된 파일 미리보기 URL
|
// 이미 업로드된 파일 URL (Next.js 프록시 사용) + 새로 추가된 파일 미리보기 URL
|
||||||
const uploadedFileUrls = (proposalData.uploadedFiles || []).map(f =>
|
const uploadedFileUrls = (proposalData.uploadedFiles || []).map(f =>
|
||||||
`/api/proxy/files/${f.id}/download`
|
`/api/proxy/files/${f.id}/download`
|
||||||
@@ -436,6 +437,7 @@ export function DocumentCreate() {
|
|||||||
approvers,
|
approvers,
|
||||||
drafter,
|
drafter,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [basicInfo, approvalLine, proposalData, expenseReportData, expenseEstimateData]);
|
}, [basicInfo, approvalLine, proposalData, expenseReportData, expenseEstimateData]);
|
||||||
|
|
||||||
@@ -453,75 +455,63 @@ export function DocumentCreate() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 문서 로딩 중
|
// 현재 모드에 맞는 config 선택
|
||||||
if (isLoadingDocument) {
|
const currentConfig = isEditMode
|
||||||
|
? documentEditConfig
|
||||||
|
: isCopyMode
|
||||||
|
? documentCopyConfig
|
||||||
|
: documentCreateConfig;
|
||||||
|
|
||||||
|
// 헤더 액션 버튼 렌더링
|
||||||
|
const renderHeaderActions = useCallback(() => {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-6 px-4 max-w-4xl">
|
<div className="flex items-center gap-2">
|
||||||
<Card className="mb-6">
|
<Button variant="outline" size="sm" onClick={handlePreview}>
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Button variant="ghost" size="icon" onClick={handleBack}>
|
|
||||||
<ArrowLeft className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FileText className="h-6 w-6 text-primary" />
|
|
||||||
<div>
|
|
||||||
<CardTitle>문서 불러오는 중...</CardTitle>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
<ContentLoadingSpinner text="문서를 불러오는 중..." />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-6 px-4 max-w-4xl">
|
|
||||||
{/* 헤더 */}
|
|
||||||
<Card className="mb-6">
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Button variant="ghost" size="icon" onClick={handleBack}>
|
|
||||||
<ArrowLeft className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FileText className="h-6 w-6 text-primary" />
|
|
||||||
<div>
|
|
||||||
<CardTitle>{isEditMode ? '문서 수정' : isCopyMode ? '문서 복제' : '문서 작성'}</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{isEditMode ? '기존 문서를 수정합니다' : isCopyMode ? '복제된 문서를 수정 후 상신합니다' : '새로운 문서를 작성합니다'}
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 액션 버튼 (스텝) */}
|
|
||||||
<div className="flex items-center justify-center gap-2 mb-6">
|
|
||||||
<Button variant="outline" className="min-w-[80px]" onClick={handlePreview}>
|
|
||||||
<Eye className="w-4 h-4 mr-1" />
|
<Eye className="w-4 h-4 mr-1" />
|
||||||
상세
|
미리보기
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" className="min-w-[80px]" onClick={handleDelete} disabled={isPending}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
<Trash2 className="w-4 h-4 mr-1" />
|
<Trash2 className="w-4 h-4 mr-1" />
|
||||||
삭제
|
삭제
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="default" className="min-w-[80px]" onClick={handleSubmit} disabled={isPending}>
|
<Button
|
||||||
{isPending ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Send className="w-4 h-4 mr-1" />}
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Send className="w-4 h-4 mr-1" />
|
||||||
|
)}
|
||||||
상신
|
상신
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" className="min-w-[80px]" onClick={handleSaveDraft} disabled={isPending}>
|
<Button
|
||||||
{isPending ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Save className="w-4 h-4 mr-1" />}
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSaveDraft}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="w-4 h-4 mr-1" />
|
||||||
|
)}
|
||||||
{isEditMode ? '저장' : '임시저장'}
|
{isEditMode ? '저장' : '임시저장'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}, [handlePreview, handleDelete, handleSubmit, handleSaveDraft, isPending, isEditMode]);
|
||||||
|
|
||||||
{/* 폼 영역 */}
|
// 폼 컨텐츠 렌더링
|
||||||
|
const renderFormContent = useCallback(() => {
|
||||||
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 기본 정보 */}
|
{/* 기본 정보 */}
|
||||||
<BasicInfoSection data={basicInfo} onChange={setBasicInfo} />
|
<BasicInfoSection data={basicInfo} onChange={setBasicInfo} />
|
||||||
@@ -535,27 +525,19 @@ export function DocumentCreate() {
|
|||||||
{/* 문서 유형별 폼 */}
|
{/* 문서 유형별 폼 */}
|
||||||
{renderDocumentTypeForm()}
|
{renderDocumentTypeForm()}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}, [basicInfo, approvalLine, references, renderDocumentTypeForm]);
|
||||||
|
|
||||||
{/* 하단 고정 버튼 (모바일) */}
|
return (
|
||||||
<div className="fixed bottom-0 left-0 right-0 p-4 bg-white border-t md:hidden">
|
<>
|
||||||
<div className="flex gap-2">
|
<IntegratedDetailTemplate
|
||||||
<Button variant="outline" className="flex-1" onClick={handleDelete} disabled={isPending}>
|
config={currentConfig}
|
||||||
<Trash2 className="w-4 h-4 mr-1" />
|
mode={isEditMode ? 'edit' : 'create'}
|
||||||
삭제
|
isLoading={isLoadingDocument}
|
||||||
</Button>
|
onBack={handleBack}
|
||||||
<Button variant="secondary" className="flex-1" onClick={handleSaveDraft} disabled={isPending}>
|
renderForm={renderFormContent}
|
||||||
{isPending ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Save className="w-4 h-4 mr-1" />}
|
headerActions={renderHeaderActions()}
|
||||||
{isEditMode ? '저장' : '임시저장'}
|
/>
|
||||||
</Button>
|
|
||||||
<Button variant="default" className="flex-1" onClick={handleSubmit} disabled={isPending}>
|
|
||||||
{isPending ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Send className="w-4 h-4 mr-1" />}
|
|
||||||
상신
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 모바일 하단 여백 */}
|
|
||||||
<div className="h-20 md:hidden" />
|
|
||||||
|
|
||||||
{/* 미리보기 모달 */}
|
{/* 미리보기 모달 */}
|
||||||
<DocumentDetailModal
|
<DocumentDetailModal
|
||||||
@@ -577,6 +559,6 @@ export function DocumentCreate() {
|
|||||||
handleSubmit();
|
handleSubmit();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/components/board/BoardForm/boardFormConfig.ts
Normal file
36
src/components/board/BoardForm/boardFormConfig.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FileText } from 'lucide-react';
|
||||||
|
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 게시글 등록 페이지 Config
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
|
*/
|
||||||
|
export const boardCreateConfig: DetailConfig = {
|
||||||
|
title: '게시글 등록',
|
||||||
|
description: '새로운 게시글을 등록합니다',
|
||||||
|
icon: FileText,
|
||||||
|
basePath: '/boards',
|
||||||
|
fields: [],
|
||||||
|
actions: {
|
||||||
|
showBack: true,
|
||||||
|
showEdit: false,
|
||||||
|
showDelete: false,
|
||||||
|
showSave: true,
|
||||||
|
submitLabel: '등록',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 게시글 수정 페이지 Config
|
||||||
|
*/
|
||||||
|
export const boardEditConfig: DetailConfig = {
|
||||||
|
...boardCreateConfig,
|
||||||
|
title: '게시글 수정',
|
||||||
|
description: '게시글을 수정합니다',
|
||||||
|
actions: {
|
||||||
|
...boardCreateConfig.actions,
|
||||||
|
submitLabel: '저장',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 게시글 등록/수정 폼 컴포넌트
|
* 게시글 등록/수정 폼 컴포넌트
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
*
|
*
|
||||||
* 디자인 스펙 기준:
|
* 디자인 스펙 기준:
|
||||||
* - 페이지 타이틀: 게시글 상세
|
* - 페이지 타이틀: 게시글 상세
|
||||||
@@ -12,10 +13,10 @@
|
|||||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { FileText, Upload, X, File, ArrowLeft, Save, Loader2 } from 'lucide-react';
|
import { Upload, X, File, Loader2 } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
import { boardCreateConfig, boardEditConfig } from './boardFormConfig';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@@ -212,31 +213,9 @@ export function BoardForm({ mode, initialData }: BoardFormProps) {
|
|||||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// ===== 폼 콘텐츠 렌더링 =====
|
||||||
<PageLayout>
|
const renderFormContent = useCallback(() => (
|
||||||
{/* 헤더 */}
|
<>
|
||||||
<PageHeader
|
|
||||||
title={mode === 'create' ? '게시글 등록' : '게시글 수정'}
|
|
||||||
description="게시글을 등록하고 관리합니다."
|
|
||||||
icon={FileText}
|
|
||||||
actions={
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="outline" onClick={handleCancel}>
|
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
||||||
취소
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Save className="h-4 w-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{mode === 'create' ? '등록' : '수정'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 폼 카드 */}
|
{/* 폼 카드 */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -488,7 +467,28 @@ export function BoardForm({ mode, initialData }: BoardFormProps) {
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</PageLayout>
|
</>
|
||||||
|
), [
|
||||||
|
boardCode, isPinned, title, content, allowComments, errors, boards,
|
||||||
|
isBoardsLoading, mode, initialData, attachments, existingAttachments,
|
||||||
|
showPinnedAlert, formatFileSize, handlePinnedChange, handleFileSelect,
|
||||||
|
handleRemoveFile, handleRemoveExistingFile,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Config 선택 (create/edit)
|
||||||
|
const config = mode === 'create' ? boardCreateConfig : boardEditConfig;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntegratedDetailTemplate
|
||||||
|
config={config}
|
||||||
|
mode={mode}
|
||||||
|
isLoading={isBoardsLoading}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
onBack={handleCancel}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
renderForm={renderFormContent}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 1:1 문의 등록/수정 폼 컴포넌트
|
* 1:1 문의 등록/수정 폼 컴포넌트
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
*
|
*
|
||||||
* 디자인 스펙:
|
* 디자인 스펙:
|
||||||
* - 페이지 타이틀: 1:1 문의 등록 / 1:1 문의 수정
|
* - 페이지 타이틀: 1:1 문의 등록 / 1:1 문의 수정
|
||||||
@@ -11,9 +12,9 @@
|
|||||||
|
|
||||||
import { useState, useCallback, useRef } from 'react';
|
import { useState, useCallback, useRef } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { MessageSquare, Upload, X, File, ArrowLeft, Save } from 'lucide-react';
|
import { Upload, X, File } from 'lucide-react';
|
||||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
import { inquiryCreateConfig, inquiryEditConfig } from './inquiryConfig';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@@ -134,29 +135,9 @@ export function InquiryForm({ mode, initialData }: InquiryFormProps) {
|
|||||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// ===== 폼 콘텐츠 렌더링 =====
|
||||||
<PageLayout>
|
const renderFormContent = useCallback(() => (
|
||||||
{/* 헤더 */}
|
<Card>
|
||||||
<PageHeader
|
|
||||||
title={mode === 'create' ? '1:1 문의 등록' : '1:1 문의 수정'}
|
|
||||||
description="1:1 문의를 등록합니다."
|
|
||||||
icon={MessageSquare}
|
|
||||||
actions={
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="outline" onClick={handleCancel}>
|
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
||||||
취소
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit}>
|
|
||||||
<Save className="h-4 w-4 mr-2" />
|
|
||||||
{mode === 'create' ? '등록' : '수정'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 폼 카드 */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-1">
|
<CardTitle className="flex items-center gap-1">
|
||||||
문의 정보
|
문의 정보
|
||||||
@@ -303,7 +284,22 @@ export function InquiryForm({ mode, initialData }: InquiryFormProps) {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</PageLayout>
|
), [category, title, content, errors, attachments, existingAttachments, formatFileSize, handleFileSelect, handleRemoveFile, handleRemoveExistingFile]);
|
||||||
|
|
||||||
|
// Config 선택 (create/edit)
|
||||||
|
const config = mode === 'create' ? inquiryCreateConfig : inquiryEditConfig;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntegratedDetailTemplate
|
||||||
|
config={config}
|
||||||
|
mode={mode}
|
||||||
|
isLoading={false}
|
||||||
|
isSubmitting={false}
|
||||||
|
onBack={handleCancel}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
renderForm={renderFormContent}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,40 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import { MessageSquare } from 'lucide-react';
|
import { MessageSquare } from 'lucide-react';
|
||||||
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1:1 문의 등록 페이지 Config
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
|
*/
|
||||||
|
export const inquiryCreateConfig: DetailConfig = {
|
||||||
|
title: '1:1 문의 등록',
|
||||||
|
description: '1:1 문의를 등록합니다',
|
||||||
|
icon: MessageSquare,
|
||||||
|
basePath: '/customer-center/qna',
|
||||||
|
fields: [],
|
||||||
|
actions: {
|
||||||
|
showBack: true,
|
||||||
|
showEdit: false,
|
||||||
|
showDelete: false,
|
||||||
|
showSave: true,
|
||||||
|
submitLabel: '등록',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1:1 문의 수정 페이지 Config
|
||||||
|
*/
|
||||||
|
export const inquiryEditConfig: DetailConfig = {
|
||||||
|
...inquiryCreateConfig,
|
||||||
|
title: '1:1 문의 수정',
|
||||||
|
description: '1:1 문의를 수정합니다',
|
||||||
|
actions: {
|
||||||
|
...inquiryCreateConfig.actions,
|
||||||
|
submitLabel: '저장',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1:1 문의 상세 페이지 Config
|
* 1:1 문의 상세 페이지 Config
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
/**
|
||||||
|
* 사원 등록/수정/상세 폼 컴포넌트
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useDaumPostcode } from '@/hooks/useDaumPostcode';
|
import { useDaumPostcode } from '@/hooks/useDaumPostcode';
|
||||||
import { useRouter, useParams } from 'next/navigation';
|
import { useRouter, useParams } from 'next/navigation';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
import { employeeCreateConfig, employeeEditConfig, employeeConfig } from './employeeConfig';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -17,7 +22,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Users, Plus, Trash2, ArrowLeft, Save, Settings, Camera, Edit } from 'lucide-react';
|
import { Plus, Trash2, Settings, Camera } from 'lucide-react';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
import { FieldSettingsDialog } from './FieldSettingsDialog';
|
import { FieldSettingsDialog } from './FieldSettingsDialog';
|
||||||
import type {
|
import type {
|
||||||
@@ -357,16 +362,12 @@ export function EmployeeForm({
|
|||||||
router.push(`/${locale}/hr/employee-management`);
|
router.push(`/${locale}/hr/employee-management`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// ===== 폼 콘텐츠 렌더링 =====
|
||||||
<PageLayout>
|
const renderFormContent = useCallback(() => (
|
||||||
{/* 헤더 + 버튼 영역 */}
|
<>
|
||||||
<div className="flex items-start justify-between mb-6">
|
{/* 항목 설정 버튼 */}
|
||||||
<PageHeader
|
{!isViewMode && (
|
||||||
title={title}
|
<div className="flex justify-end mb-4">
|
||||||
description={description}
|
|
||||||
icon={Users}
|
|
||||||
/>
|
|
||||||
{!isViewMode && (
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -375,10 +376,10 @@ export function EmployeeForm({
|
|||||||
<Settings className="w-4 h-4 mr-2" />
|
<Settings className="w-4 h-4 mr-2" />
|
||||||
항목 설정
|
항목 설정
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 사원 정보 - 프로필 사진 + 기본 정보 */}
|
{/* 사원 정보 - 프로필 사진 + 기본 정보 */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -932,31 +933,7 @@ export function EmployeeForm({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 버튼 영역 */}
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
|
||||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
||||||
목록
|
|
||||||
</Button>
|
|
||||||
{isViewMode ? (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button type="button" onClick={onEdit}>
|
|
||||||
<Edit className="w-4 h-4 mr-2" />
|
|
||||||
수정
|
|
||||||
</Button>
|
|
||||||
<Button type="button" variant="destructive" onClick={onDelete}>
|
|
||||||
<Trash2 className="w-4 h-4 mr-2" />
|
|
||||||
삭제
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Button type="submit">
|
|
||||||
<Save className="w-4 h-4 mr-2" />
|
|
||||||
{mode === 'create' ? '등록' : '저장'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* 항목 설정 모달 */}
|
{/* 항목 설정 모달 */}
|
||||||
<FieldSettingsDialog
|
<FieldSettingsDialog
|
||||||
@@ -965,6 +942,33 @@ export function EmployeeForm({
|
|||||||
settings={fieldSettings}
|
settings={fieldSettings}
|
||||||
onSave={handleSaveFieldSettings}
|
onSave={handleSaveFieldSettings}
|
||||||
/>
|
/>
|
||||||
</PageLayout>
|
</>
|
||||||
|
), [
|
||||||
|
formData, errors, isViewMode, mode, fieldSettings, showFieldSettings,
|
||||||
|
ranks, titles, departments, handleChange, handleSaveFieldSettings,
|
||||||
|
handleAddDepartmentPosition, handleRemoveDepartmentPosition,
|
||||||
|
handleDepartmentSelect, handlePositionSelect, openPostcode,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Config 선택 (create/edit/view)
|
||||||
|
const getConfig = () => {
|
||||||
|
if (mode === 'view') return employeeConfig;
|
||||||
|
if (mode === 'edit') return employeeEditConfig;
|
||||||
|
return employeeCreateConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntegratedDetailTemplate
|
||||||
|
config={getConfig()}
|
||||||
|
mode={mode}
|
||||||
|
isLoading={false}
|
||||||
|
isSubmitting={false}
|
||||||
|
onBack={handleCancel}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onDelete={onDelete}
|
||||||
|
renderForm={renderFormContent}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,40 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import { Users } from 'lucide-react';
|
import { Users } from 'lucide-react';
|
||||||
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사원 등록 페이지 Config
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
|
*/
|
||||||
|
export const employeeCreateConfig: DetailConfig = {
|
||||||
|
title: '사원 등록',
|
||||||
|
description: '새로운 사원을 등록합니다',
|
||||||
|
icon: Users,
|
||||||
|
basePath: '/hr/employee-management',
|
||||||
|
fields: [],
|
||||||
|
actions: {
|
||||||
|
showBack: true,
|
||||||
|
showEdit: false,
|
||||||
|
showDelete: false,
|
||||||
|
showSave: true,
|
||||||
|
submitLabel: '등록',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사원 수정 페이지 Config
|
||||||
|
*/
|
||||||
|
export const employeeEditConfig: DetailConfig = {
|
||||||
|
...employeeCreateConfig,
|
||||||
|
title: '사원 수정',
|
||||||
|
description: '사원 정보를 수정합니다',
|
||||||
|
actions: {
|
||||||
|
...employeeCreateConfig.actions,
|
||||||
|
submitLabel: '저장',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사원 상세 페이지 Config
|
* 사원 상세 페이지 Config
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { format, differenceInDays } from 'date-fns';
|
import { differenceInDays, parseISO } from 'date-fns';
|
||||||
import { CalendarIcon, Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -11,13 +11,8 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Calendar } from '@/components/ui/calendar';
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from '@/components/ui/popover';
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -25,7 +20,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import type { VacationRequestFormData, LeaveType } from './types';
|
import type { VacationRequestFormData, LeaveType } from './types';
|
||||||
import { LEAVE_TYPE_LABELS } from './types';
|
import { LEAVE_TYPE_LABELS } from './types';
|
||||||
import { getActiveEmployees, type EmployeeOption } from './actions';
|
import { getActiveEmployees, type EmployeeOption } from './actions';
|
||||||
@@ -48,8 +42,6 @@ export function VacationRequestDialog({
|
|||||||
endDate: '',
|
endDate: '',
|
||||||
vacationDays: 1,
|
vacationDays: 1,
|
||||||
});
|
});
|
||||||
const [startDate, setStartDate] = useState<Date | undefined>();
|
|
||||||
const [endDate, setEndDate] = useState<Date | undefined>();
|
|
||||||
const [employees, setEmployees] = useState<EmployeeOption[]>([]);
|
const [employees, setEmployees] = useState<EmployeeOption[]>([]);
|
||||||
const [isLoadingEmployees, setIsLoadingEmployees] = useState(false);
|
const [isLoadingEmployees, setIsLoadingEmployees] = useState(false);
|
||||||
|
|
||||||
@@ -76,33 +68,31 @@ export function VacationRequestDialog({
|
|||||||
endDate: '',
|
endDate: '',
|
||||||
vacationDays: 1,
|
vacationDays: 1,
|
||||||
});
|
});
|
||||||
setStartDate(undefined);
|
|
||||||
setEndDate(undefined);
|
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
// 날짜 변경 시 휴가 일수 자동 계산
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (startDate && endDate) {
|
if (formData.startDate && formData.endDate) {
|
||||||
const days = differenceInDays(endDate, startDate) + 1;
|
const start = parseISO(formData.startDate);
|
||||||
setFormData(prev => ({
|
const end = parseISO(formData.endDate);
|
||||||
...prev,
|
const days = differenceInDays(end, start) + 1;
|
||||||
startDate: format(startDate, 'yyyy-MM-dd'),
|
if (days > 0 && days !== formData.vacationDays) {
|
||||||
endDate: format(endDate, 'yyyy-MM-dd'),
|
setFormData(prev => ({ ...prev, vacationDays: days }));
|
||||||
vacationDays: days > 0 ? days : 1,
|
}
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}, [startDate, endDate]);
|
}, [formData.startDate, formData.endDate, formData.vacationDays]);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (!formData.employeeId) {
|
if (!formData.employeeId) {
|
||||||
alert('사원을 선택해주세요.');
|
alert('사원을 선택해주세요.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!startDate || !endDate) {
|
if (!formData.startDate || !formData.endDate) {
|
||||||
alert('휴가 기간을 선택해주세요.');
|
alert('휴가 기간을 선택해주세요.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (endDate < startDate) {
|
if (formData.endDate < formData.startDate) {
|
||||||
alert('종료일은 시작일 이후여야 합니다.');
|
alert('종료일은 시작일 이후여야 합니다.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -174,61 +164,29 @@ export function VacationRequestDialog({
|
|||||||
|
|
||||||
{/* 시작일 */}
|
{/* 시작일 */}
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>시작일</Label>
|
<Label htmlFor="startDate">시작일</Label>
|
||||||
<Popover>
|
<Input
|
||||||
<PopoverTrigger asChild>
|
id="startDate"
|
||||||
<Button
|
type="date"
|
||||||
variant="outline"
|
value={formData.startDate}
|
||||||
className={cn(
|
onChange={(e) => setFormData(prev => ({ ...prev, startDate: e.target.value }))}
|
||||||
'w-full justify-start text-left font-normal',
|
/>
|
||||||
!startDate && 'text-muted-foreground'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
||||||
{startDate ? format(startDate, 'yyyy-MM-dd') : '시작일 선택'}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
|
||||||
<Calendar
|
|
||||||
mode="single"
|
|
||||||
selected={startDate}
|
|
||||||
onSelect={setStartDate}
|
|
||||||
initialFocus
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 종료일 */}
|
{/* 종료일 */}
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>종료일</Label>
|
<Label htmlFor="endDate">종료일</Label>
|
||||||
<Popover>
|
<Input
|
||||||
<PopoverTrigger asChild>
|
id="endDate"
|
||||||
<Button
|
type="date"
|
||||||
variant="outline"
|
value={formData.endDate}
|
||||||
className={cn(
|
min={formData.startDate || undefined}
|
||||||
'w-full justify-start text-left font-normal',
|
onChange={(e) => setFormData(prev => ({ ...prev, endDate: e.target.value }))}
|
||||||
!endDate && 'text-muted-foreground'
|
/>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
||||||
{endDate ? format(endDate, 'yyyy-MM-dd') : '종료일 선택'}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
|
||||||
<Calendar
|
|
||||||
mode="single"
|
|
||||||
selected={endDate}
|
|
||||||
onSelect={setEndDate}
|
|
||||||
disabled={(date) => startDate ? date < startDate : false}
|
|
||||||
initialFocus
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 휴가 일수 (자동 계산) */}
|
{/* 휴가 일수 (자동 계산) */}
|
||||||
{startDate && endDate && (
|
{formData.startDate && formData.endDate && (
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>휴가 일수</Label>
|
<Label>휴가 일수</Label>
|
||||||
<div className="p-3 bg-muted rounded-md text-center font-medium">
|
<div className="p-3 bg-muted rounded-md text-center font-medium">
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 수입검사 등록 (IQC) 페이지
|
* 수입검사 등록 (IQC) 페이지
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
|
*
|
||||||
* - 검사 대상 선택
|
* - 검사 대상 선택
|
||||||
* - 검사 정보 입력 (검사일, 검사자*, LOT번호)
|
* - 검사 정보 입력 (검사일, 검사자*, LOT번호)
|
||||||
* - 검사 항목 테이블 (겉모양, 두께, 폭, 길이)
|
* - 검사 항목 테이블 (겉모양, 두께, 폭, 길이)
|
||||||
@@ -10,13 +12,14 @@
|
|||||||
|
|
||||||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { ClipboardCheck, Calendar } from 'lucide-react';
|
import { Calendar } from 'lucide-react';
|
||||||
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||||
|
import { materialInspectionCreateConfig } from './inspectionConfig';
|
||||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -183,27 +186,10 @@ export function InspectionCreate({ id }: Props) {
|
|||||||
router.push('/ko/material/receiving-management');
|
router.push('/ko/material/receiving-management');
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
return (
|
// ===== 폼 콘텐츠 렌더링 =====
|
||||||
<PageLayout>
|
const renderFormContent = useCallback(() => (
|
||||||
<div className="space-y-6">
|
<>
|
||||||
{/* 헤더 */}
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<ClipboardCheck className="w-6 h-6" />
|
|
||||||
<h1 className="text-xl font-semibold">수입검사 등록 (IQC)</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="outline" onClick={handleCancel}>
|
|
||||||
취소
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit}>
|
|
||||||
<ClipboardCheck className="w-4 h-4 mr-1.5" />
|
|
||||||
검사 저장
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
||||||
{/* 좌측: 검사 대상 선택 */}
|
{/* 좌측: 검사 대상 선택 */}
|
||||||
<div className="lg:col-span-1 space-y-2">
|
<div className="lg:col-span-1 space-y-2">
|
||||||
<Label className="text-sm font-medium">검사 대상 선택</Label>
|
<Label className="text-sm font-medium">검사 대상 선택</Label>
|
||||||
@@ -362,7 +348,6 @@ export function InspectionCreate({ id }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 성공 다이얼로그 */}
|
{/* 성공 다이얼로그 */}
|
||||||
<SuccessDialog
|
<SuccessDialog
|
||||||
@@ -371,6 +356,23 @@ export function InspectionCreate({ id }: Props) {
|
|||||||
lotNo={lotNo}
|
lotNo={lotNo}
|
||||||
onClose={handleSuccessClose}
|
onClose={handleSuccessClose}
|
||||||
/>
|
/>
|
||||||
</PageLayout>
|
</>
|
||||||
|
), [
|
||||||
|
isLoadingTargets, inspectionTargets, selectedTargetId, inspectionDate,
|
||||||
|
inspector, lotNo, inspectionItems, opinion, validationErrors, showSuccess,
|
||||||
|
handleTargetSelect, handleJudgmentChange, handleRemarkChange, handleSuccessClose,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntegratedDetailTemplate
|
||||||
|
config={materialInspectionCreateConfig}
|
||||||
|
mode="create"
|
||||||
|
isLoading={isLoadingTargets}
|
||||||
|
isSubmitting={false}
|
||||||
|
onBack={handleCancel}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
renderForm={renderFormContent}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ClipboardCheck } from 'lucide-react';
|
||||||
|
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수입검사 등록 (IQC) 페이지 Config
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
|
*/
|
||||||
|
export const materialInspectionCreateConfig: DetailConfig = {
|
||||||
|
title: '수입검사 등록',
|
||||||
|
description: '수입검사를 등록합니다',
|
||||||
|
icon: ClipboardCheck,
|
||||||
|
basePath: '/material/receiving-management',
|
||||||
|
fields: [],
|
||||||
|
actions: {
|
||||||
|
showBack: true,
|
||||||
|
showEdit: false,
|
||||||
|
showDelete: false,
|
||||||
|
showSave: true,
|
||||||
|
submitLabel: '등록',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -3,13 +3,11 @@
|
|||||||
/**
|
/**
|
||||||
* 출하 등록 페이지
|
* 출하 등록 페이지
|
||||||
* API 연동 완료 (2025-12-26)
|
* API 연동 완료 (2025-12-26)
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { ArrowLeft, Truck, Loader2, AlertCircle } from 'lucide-react';
|
|
||||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
@@ -22,7 +20,8 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||||
|
import { shipmentCreateConfig } from './shipmentConfig';
|
||||||
import {
|
import {
|
||||||
createShipment,
|
createShipment,
|
||||||
getLotOptions,
|
getLotOptions,
|
||||||
@@ -177,63 +176,10 @@ export function ShipmentCreate() {
|
|||||||
}
|
}
|
||||||
}, [formData, router]);
|
}, [formData, router]);
|
||||||
|
|
||||||
// 로딩 상태 표시
|
// 폼 컨텐츠 렌더링
|
||||||
if (isLoading) {
|
const renderFormContent = useCallback(() => (
|
||||||
return (
|
<div className="space-y-6">
|
||||||
<PageLayout>
|
{/* Validation 에러 표시 */}
|
||||||
<ContentLoadingSpinner text="출하 등록 정보를 불러오는 중..." />
|
|
||||||
</PageLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 에러 상태 표시
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<PageLayout>
|
|
||||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
|
||||||
<AlertCircle className="w-12 h-12 text-red-500" />
|
|
||||||
<p className="text-lg text-muted-foreground">{error}</p>
|
|
||||||
<Button onClick={loadOptions}>다시 시도</Button>
|
|
||||||
</div>
|
|
||||||
</PageLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageLayout>
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* 헤더 */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleCancel}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-5 h-5" />
|
|
||||||
</Button>
|
|
||||||
<Truck className="w-6 h-6" />
|
|
||||||
<h1 className="text-xl font-semibold">출하 등록</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="outline" onClick={handleCancel} disabled={isSubmitting}>
|
|
||||||
취소
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
저장 중...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'저장'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Validation 에러 표시 */}
|
|
||||||
{validationErrors.length > 0 && (
|
{validationErrors.length > 0 && (
|
||||||
<Alert className="bg-red-50 border-red-200">
|
<Alert className="bg-red-50 border-red-200">
|
||||||
<AlertDescription className="text-red-900">
|
<AlertDescription className="text-red-900">
|
||||||
@@ -428,6 +374,35 @@ export function ShipmentCreate() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
), [formData, validationErrors, isSubmitting, lotOptions, logisticsOptions, vehicleTonnageOptions]);
|
||||||
|
|
||||||
|
// 로딩 또는 에러 상태 처리
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<IntegratedDetailTemplate
|
||||||
|
config={shipmentCreateConfig}
|
||||||
|
mode="create"
|
||||||
|
isLoading={false}
|
||||||
|
onBack={handleCancel}
|
||||||
|
renderForm={() => (
|
||||||
|
<Alert className="bg-red-50 border-red-200">
|
||||||
|
<AlertDescription className="text-red-900">{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntegratedDetailTemplate
|
||||||
|
config={shipmentCreateConfig}
|
||||||
|
mode="create"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
onBack={handleCancel}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
renderForm={renderFormContent}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,11 @@
|
|||||||
/**
|
/**
|
||||||
* 출하 수정 페이지
|
* 출하 수정 페이지
|
||||||
* API 연동 완료 (2025-12-26)
|
* API 연동 완료 (2025-12-26)
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { ArrowLeft, Truck, Loader2, AlertCircle } from 'lucide-react';
|
|
||||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
@@ -23,8 +21,8 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||||
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
|
import { shipmentEditConfig } from './shipmentConfig';
|
||||||
import {
|
import {
|
||||||
getShipmentById,
|
getShipmentById,
|
||||||
getLogisticsOptions,
|
getLogisticsOptions,
|
||||||
@@ -215,63 +213,23 @@ export function ShipmentEdit({ id }: ShipmentEditProps) {
|
|||||||
}
|
}
|
||||||
}, [id, formData, router]);
|
}, [id, formData, router]);
|
||||||
|
|
||||||
// 로딩 상태 표시
|
// 동적 config (로트번호 + 상태 표시)
|
||||||
if (isLoading) {
|
const dynamicConfig = {
|
||||||
return (
|
...shipmentEditConfig,
|
||||||
<PageLayout>
|
title: detail ? `출고 수정 (${detail.lotNo})` : '출고 수정',
|
||||||
<ContentLoadingSpinner text="출고 정보를 불러오는 중..." />
|
};
|
||||||
</PageLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 에러 상태 표시
|
// 폼 컨텐츠 렌더링
|
||||||
if (error || !detail) {
|
const renderFormContent = useCallback(() => {
|
||||||
return (
|
if (!detail) return null;
|
||||||
<ServerErrorPage
|
|
||||||
title="출하 정보를 불러올 수 없습니다"
|
|
||||||
message={error || '출하 정보를 찾을 수 없습니다.'}
|
|
||||||
showBackButton={true}
|
|
||||||
showHomeButton={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 헤더 */}
|
{/* 상태 배지 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-3">
|
<Badge className={`text-xs ${SHIPMENT_STATUS_STYLES[detail.status]}`}>
|
||||||
<Button
|
{SHIPMENT_STATUS_LABELS[detail.status]}
|
||||||
variant="ghost"
|
</Badge>
|
||||||
size="icon"
|
|
||||||
onClick={handleCancel}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-5 h-5" />
|
|
||||||
</Button>
|
|
||||||
<Truck className="w-6 h-6" />
|
|
||||||
<h1 className="text-xl font-semibold">출고 수정</h1>
|
|
||||||
<span className="text-sm text-muted-foreground">{detail.lotNo}</span>
|
|
||||||
<Badge className={`text-xs ${SHIPMENT_STATUS_STYLES[detail.status]}`}>
|
|
||||||
{SHIPMENT_STATUS_LABELS[detail.status]}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="outline" onClick={handleCancel} disabled={isSubmitting}>
|
|
||||||
취소
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
저장 중...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'저장'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Validation 에러 표시 */}
|
{/* Validation 에러 표시 */}
|
||||||
@@ -536,6 +494,38 @@ export function ShipmentEdit({ id }: ShipmentEditProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
);
|
||||||
|
}, [detail, formData, validationErrors, isSubmitting, logisticsOptions, vehicleTonnageOptions]);
|
||||||
|
|
||||||
|
// 에러 상태 표시
|
||||||
|
if (error && !isLoading) {
|
||||||
|
return (
|
||||||
|
<IntegratedDetailTemplate
|
||||||
|
config={dynamicConfig}
|
||||||
|
mode="edit"
|
||||||
|
isLoading={false}
|
||||||
|
onBack={handleCancel}
|
||||||
|
renderForm={() => (
|
||||||
|
<Alert className="bg-red-50 border-red-200">
|
||||||
|
<AlertDescription className="text-red-900">
|
||||||
|
{error || '출하 정보를 찾을 수 없습니다.'}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntegratedDetailTemplate
|
||||||
|
config={dynamicConfig}
|
||||||
|
mode="edit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
onBack={handleCancel}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
renderForm={renderFormContent}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import { Truck } from 'lucide-react';
|
import { Truck } from 'lucide-react';
|
||||||
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||||
|
|
||||||
@@ -33,3 +35,31 @@ export const shipmentConfig: DetailConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 출하 등록 페이지 Config
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
|
*/
|
||||||
|
export const shipmentCreateConfig: DetailConfig = {
|
||||||
|
title: '출하 등록',
|
||||||
|
description: '새로운 출하를 등록합니다',
|
||||||
|
icon: Truck,
|
||||||
|
basePath: '/outbound/shipments',
|
||||||
|
fields: [],
|
||||||
|
actions: {
|
||||||
|
showBack: true,
|
||||||
|
showEdit: false,
|
||||||
|
showDelete: false,
|
||||||
|
showSave: true,
|
||||||
|
submitLabel: '저장',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 출하 수정 페이지 Config
|
||||||
|
*/
|
||||||
|
export const shipmentEditConfig: DetailConfig = {
|
||||||
|
...shipmentCreateConfig,
|
||||||
|
title: '출고 수정',
|
||||||
|
description: '출고 정보를 수정합니다',
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 공정 등록/수정 폼 컴포넌트
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { X, Save, Plus, Wrench, Trash2, Loader2, Pencil } from 'lucide-react';
|
import { Plus, Wrench, Trash2, Pencil } from 'lucide-react';
|
||||||
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||||
|
import { processCreateConfig, processEditConfig } from './processConfig';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@@ -17,7 +24,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
|
||||||
import { RuleModal } from './RuleModal';
|
import { RuleModal } from './RuleModal';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import type { Process, ClassificationRule, ProcessType } from '@/types/process';
|
import type { Process, ClassificationRule, ProcessType } from '@/types/process';
|
||||||
@@ -190,27 +196,9 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) {
|
|||||||
router.back();
|
router.back();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// ===== 폼 콘텐츠 렌더링 =====
|
||||||
<PageLayout>
|
const renderFormContent = useCallback(() => (
|
||||||
{/* 헤더 */}
|
<>
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<h1 className="text-xl font-semibold">공정 {isEdit ? '수정' : '등록'}</h1>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="outline" onClick={handleCancel} disabled={isLoading}>
|
|
||||||
<X className="h-4 w-4 mr-2" />
|
|
||||||
취소
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit} disabled={isLoading}>
|
|
||||||
{isLoading ? (
|
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Save className="h-4 w-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{isEdit ? '수정' : '등록'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 기본 정보 */}
|
{/* 기본 정보 */}
|
||||||
<Card>
|
<Card>
|
||||||
@@ -460,6 +448,27 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) {
|
|||||||
onAdd={handleSaveRule}
|
onAdd={handleSaveRule}
|
||||||
editRule={editingRule}
|
editRule={editingRule}
|
||||||
/>
|
/>
|
||||||
</PageLayout>
|
</>
|
||||||
|
), [
|
||||||
|
processName, processType, department, workLogTemplate, classificationRules,
|
||||||
|
requiredWorkers, equipmentInfo, workSteps, note, isActive, ruleModalOpen,
|
||||||
|
editingRule, departmentOptions, isDepartmentsLoading, handleSaveRule,
|
||||||
|
handleEditRule, handleDeleteRule, handleModalClose,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Config 선택 (create/edit)
|
||||||
|
const config = isEdit ? processEditConfig : processCreateConfig;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntegratedDetailTemplate
|
||||||
|
config={config}
|
||||||
|
mode={isEdit ? 'edit' : 'create'}
|
||||||
|
isLoading={isDepartmentsLoading}
|
||||||
|
isSubmitting={isLoading}
|
||||||
|
onBack={handleCancel}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
renderForm={renderFormContent}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/components/process-management/processConfig.ts
Normal file
36
src/components/process-management/processConfig.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Wrench } from 'lucide-react';
|
||||||
|
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 공정 등록 페이지 Config
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
|
*/
|
||||||
|
export const processCreateConfig: DetailConfig = {
|
||||||
|
title: '공정 등록',
|
||||||
|
description: '새로운 공정을 등록합니다',
|
||||||
|
icon: Wrench,
|
||||||
|
basePath: '/master-data/process-management',
|
||||||
|
fields: [],
|
||||||
|
actions: {
|
||||||
|
showBack: true,
|
||||||
|
showEdit: false,
|
||||||
|
showDelete: false,
|
||||||
|
showSave: true,
|
||||||
|
submitLabel: '등록',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 공정 수정 페이지 Config
|
||||||
|
*/
|
||||||
|
export const processEditConfig: DetailConfig = {
|
||||||
|
...processCreateConfig,
|
||||||
|
title: '공정 수정',
|
||||||
|
description: '공정 정보를 수정합니다',
|
||||||
|
actions: {
|
||||||
|
...processCreateConfig.actions,
|
||||||
|
submitLabel: '저장',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -3,11 +3,12 @@
|
|||||||
/**
|
/**
|
||||||
* 작업지시 등록 페이지
|
* 작업지시 등록 페이지
|
||||||
* API 연동 완료 (2025-12-26)
|
* API 연동 완료 (2025-12-26)
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { ArrowLeft, FileText, X, Edit2, Loader2 } from 'lucide-react';
|
import { X, Edit2, FileText } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@@ -21,13 +22,14 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||||
import { SalesOrderSelectModal } from './SalesOrderSelectModal';
|
import { SalesOrderSelectModal } from './SalesOrderSelectModal';
|
||||||
import { AssigneeSelectModal } from './AssigneeSelectModal';
|
import { AssigneeSelectModal } from './AssigneeSelectModal';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||||
import { createWorkOrder, getProcessOptions, type ProcessOption } from './actions';
|
import { createWorkOrder, getProcessOptions, type ProcessOption } from './actions';
|
||||||
import { PROCESS_TYPE_LABELS, type ProcessType, type SalesOrder } from './types';
|
import { type SalesOrder } from './types';
|
||||||
|
import { workOrderCreateConfig } from './workOrderConfig';
|
||||||
|
|
||||||
// Validation 에러 타입
|
// Validation 에러 타입
|
||||||
interface ValidationErrors {
|
interface ValidationErrors {
|
||||||
@@ -208,31 +210,9 @@ export function WorkOrderCreate() {
|
|||||||
return selectedProcess?.processCode || '-';
|
return selectedProcess?.processCode || '-';
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// 폼 컨텐츠 렌더링
|
||||||
<PageLayout>
|
const renderFormContent = useCallback(() => (
|
||||||
{/* 헤더 */}
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Button variant="ghost" size="icon" onClick={handleCancel}>
|
|
||||||
<ArrowLeft className="w-5 h-5" />
|
|
||||||
</Button>
|
|
||||||
<h1 className="text-2xl font-bold flex items-center gap-2">
|
|
||||||
<FileText className="w-5 h-5" />
|
|
||||||
작업지시 등록
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="outline" onClick={handleCancel} disabled={isSubmitting}>
|
|
||||||
취소
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
|
||||||
{isSubmitting && <Loader2 className="w-4 h-4 mr-1.5 animate-spin" />}
|
|
||||||
등록
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Validation 에러 표시 */}
|
{/* Validation 에러 표시 */}
|
||||||
{Object.keys(validationErrors).length > 0 && (
|
{Object.keys(validationErrors).length > 0 && (
|
||||||
<Alert className="bg-red-50 border-red-200">
|
<Alert className="bg-red-50 border-red-200">
|
||||||
@@ -497,6 +477,19 @@ export function WorkOrderCreate() {
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
), [mode, formData, validationErrors, processOptions, isLoadingProcesses, assigneeNames, getSelectedProcessCode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IntegratedDetailTemplate
|
||||||
|
config={workOrderCreateConfig}
|
||||||
|
mode="create"
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
onBack={handleCancel}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
renderForm={renderFormContent}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 수주 선택 모달 */}
|
{/* 수주 선택 모달 */}
|
||||||
<SalesOrderSelectModal
|
<SalesOrderSelectModal
|
||||||
@@ -515,6 +508,6 @@ export function WorkOrderCreate() {
|
|||||||
setAssigneeNames(names);
|
setAssigneeNames(names);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</PageLayout>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,11 @@
|
|||||||
/**
|
/**
|
||||||
* 작업지시 수정 페이지
|
* 작업지시 수정 페이지
|
||||||
* WorkOrderCreate 패턴 기반
|
* WorkOrderCreate 패턴 기반
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { ArrowLeft, FileText, Loader2 } from 'lucide-react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
@@ -20,13 +19,13 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||||
import { AssigneeSelectModal } from './AssigneeSelectModal';
|
import { AssigneeSelectModal } from './AssigneeSelectModal';
|
||||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||||
import { getWorkOrderById, updateWorkOrder, getProcessOptions, type ProcessOption } from './actions';
|
import { getWorkOrderById, updateWorkOrder, getProcessOptions, type ProcessOption } from './actions';
|
||||||
import type { WorkOrder } from './types';
|
import type { WorkOrder } from './types';
|
||||||
|
import { workOrderEditConfig } from './workOrderConfig';
|
||||||
|
|
||||||
// Validation 에러 타입
|
// Validation 에러 타입
|
||||||
interface ValidationErrors {
|
interface ValidationErrors {
|
||||||
@@ -199,41 +198,15 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
|||||||
return selectedProcess?.processCode || '-';
|
return selectedProcess?.processCode || '-';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 로딩 상태
|
// 동적 config (작업지시 번호 포함)
|
||||||
if (isLoading) {
|
const dynamicConfig = {
|
||||||
return <ContentLoadingSpinner text="작업지시 정보를 불러오는 중..." />;
|
...workOrderEditConfig,
|
||||||
}
|
title: `작업지시 수정 ${workOrder ? `(${workOrder.workOrderNo})` : ''}`,
|
||||||
|
};
|
||||||
|
|
||||||
if (!workOrder) {
|
// 폼 컨텐츠 렌더링
|
||||||
return null;
|
const renderFormContent = useCallback(() => (
|
||||||
}
|
<div className="space-y-6">
|
||||||
|
|
||||||
return (
|
|
||||||
<PageLayout>
|
|
||||||
{/* 헤더 */}
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Button variant="ghost" size="icon" onClick={handleCancel}>
|
|
||||||
<ArrowLeft className="w-5 h-5" />
|
|
||||||
</Button>
|
|
||||||
<h1 className="text-2xl font-bold flex items-center gap-2">
|
|
||||||
<FileText className="w-5 h-5" />
|
|
||||||
작업지시 수정
|
|
||||||
</h1>
|
|
||||||
<span className="text-muted-foreground">({workOrder.workOrderNo})</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="outline" onClick={handleCancel} disabled={isSubmitting}>
|
|
||||||
취소
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
|
||||||
{isSubmitting && <Loader2 className="w-4 h-4 mr-1.5 animate-spin" />}
|
|
||||||
저장
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Validation 에러 표시 */}
|
{/* Validation 에러 표시 */}
|
||||||
{Object.keys(validationErrors).length > 0 && (
|
{Object.keys(validationErrors).length > 0 && (
|
||||||
<Alert className="bg-red-50 border-red-200">
|
<Alert className="bg-red-50 border-red-200">
|
||||||
@@ -386,6 +359,20 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
), [formData, validationErrors, processOptions, isLoadingProcesses, assigneeNames, getSelectedProcessCode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IntegratedDetailTemplate
|
||||||
|
config={dynamicConfig}
|
||||||
|
mode="edit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
onBack={handleCancel}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
renderForm={renderFormContent}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 담당자 선택 모달 */}
|
{/* 담당자 선택 모달 */}
|
||||||
<AssigneeSelectModal
|
<AssigneeSelectModal
|
||||||
@@ -397,6 +384,6 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
|||||||
setAssigneeNames(names);
|
setAssigneeNames(names);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</PageLayout>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import { FileText } from 'lucide-react';
|
import { FileText } from 'lucide-react';
|
||||||
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||||
|
|
||||||
@@ -28,3 +30,35 @@ export const workOrderConfig: DetailConfig = {
|
|||||||
editLabel: '수정',
|
editLabel: '수정',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 작업지시 등록 페이지 Config
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
|
*/
|
||||||
|
export const workOrderCreateConfig: DetailConfig = {
|
||||||
|
title: '작업지시 등록',
|
||||||
|
description: '새로운 작업지시를 등록합니다',
|
||||||
|
icon: FileText,
|
||||||
|
basePath: '/production/work-orders',
|
||||||
|
fields: [],
|
||||||
|
actions: {
|
||||||
|
showBack: true,
|
||||||
|
showEdit: false,
|
||||||
|
showDelete: false,
|
||||||
|
showSave: true,
|
||||||
|
submitLabel: '등록',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 작업지시 수정 페이지 Config
|
||||||
|
*/
|
||||||
|
export const workOrderEditConfig: DetailConfig = {
|
||||||
|
...workOrderCreateConfig,
|
||||||
|
title: '작업지시 수정',
|
||||||
|
description: '작업지시 정보를 수정합니다',
|
||||||
|
actions: {
|
||||||
|
...workOrderCreateConfig.actions,
|
||||||
|
submitLabel: '저장',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,19 +2,20 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 검사 등록 페이지
|
* 검사 등록 페이지
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
* API 연동 완료 (2025-12-26)
|
* API 연동 완료 (2025-12-26)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { ClipboardCheck, ImageIcon, Loader2 } from 'lucide-react';
|
import { ImageIcon } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||||
|
import { qualityInspectionCreateConfig } from './inspectionConfig';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { createInspection } from './actions';
|
import { createInspection } from './actions';
|
||||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||||
@@ -89,9 +90,9 @@ export function InspectionCreate() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 취소
|
// 취소
|
||||||
const handleCancel = () => {
|
const handleCancel = useCallback(() => {
|
||||||
router.push('/quality/inspections');
|
router.push('/quality/inspections');
|
||||||
};
|
}, [router]);
|
||||||
|
|
||||||
// validation 체크
|
// validation 체크
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
@@ -156,33 +157,10 @@ export function InspectionCreate() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// ===== 폼 콘텐츠 렌더링 =====
|
||||||
<PageLayout>
|
const renderFormContent = useCallback(() => (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 헤더 */}
|
{/* Validation 에러 표시 */}
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<ClipboardCheck className="w-6 h-6" />
|
|
||||||
<h1 className="text-xl font-semibold">검사 등록</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="outline" onClick={handleCancel} disabled={isSubmitting}>
|
|
||||||
취소
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
등록 중...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'검사완료'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Validation 에러 표시 */}
|
|
||||||
{validationErrors.length > 0 && (
|
{validationErrors.length > 0 && (
|
||||||
<Alert className="bg-red-50 border-red-200">
|
<Alert className="bg-red-50 border-red-200">
|
||||||
<AlertDescription className="text-red-900">
|
<AlertDescription className="text-red-900">
|
||||||
@@ -352,6 +330,18 @@ export function InspectionCreate() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
), [formData, inspectionItems, validationErrors, handleInputChange, handleQualityResultChange, handleMeasurementChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntegratedDetailTemplate
|
||||||
|
config={qualityInspectionCreateConfig}
|
||||||
|
mode="create"
|
||||||
|
isLoading={false}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
onBack={handleCancel}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
renderForm={renderFormContent}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,27 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import { ClipboardCheck } from 'lucide-react';
|
import { ClipboardCheck } from 'lucide-react';
|
||||||
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 품질검사 등록 페이지 Config
|
||||||
|
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||||
|
*/
|
||||||
|
export const qualityInspectionCreateConfig: DetailConfig = {
|
||||||
|
title: '품질검사 등록',
|
||||||
|
description: '품질검사를 등록합니다',
|
||||||
|
icon: ClipboardCheck,
|
||||||
|
basePath: '/quality/inspection-management',
|
||||||
|
fields: [],
|
||||||
|
actions: {
|
||||||
|
showBack: true,
|
||||||
|
showEdit: false,
|
||||||
|
showDelete: false,
|
||||||
|
showSave: true,
|
||||||
|
submitLabel: '등록',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 검수관리 상세 페이지 Config
|
* 검수관리 상세 페이지 Config
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface DetailActionsProps {
|
|||||||
back?: boolean;
|
back?: boolean;
|
||||||
delete?: boolean;
|
delete?: boolean;
|
||||||
edit?: boolean;
|
edit?: boolean;
|
||||||
|
save?: boolean;
|
||||||
};
|
};
|
||||||
/** 버튼 라벨 */
|
/** 버튼 라벨 */
|
||||||
labels?: {
|
labels?: {
|
||||||
@@ -74,6 +75,7 @@ export function DetailActions({
|
|||||||
back: showBack = true,
|
back: showBack = true,
|
||||||
delete: showDelete = true,
|
delete: showDelete = true,
|
||||||
edit: showEdit = true,
|
edit: showEdit = true,
|
||||||
|
save: showSave = true,
|
||||||
} = showButtons;
|
} = showButtons;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -133,11 +135,16 @@ export function DetailActions({
|
|||||||
{cancelLabel}
|
{cancelLabel}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* 오른쪽: 저장/등록 */}
|
{/* 오른쪽: 추가액션 + 저장/등록 */}
|
||||||
<Button onClick={onSubmit} disabled={isSubmitting}>
|
<div className="flex items-center gap-2">
|
||||||
<Save className="w-4 h-4 mr-2" />
|
{extraActions}
|
||||||
{actualSubmitLabel}
|
{showSave && onSubmit && (
|
||||||
</Button>
|
<Button onClick={onSubmit} disabled={isSubmitting}>
|
||||||
|
<Save className="w-4 h-4 mr-2" />
|
||||||
|
{actualSubmitLabel}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ export function IntegratedDetailTemplate<T extends Record<string, unknown>>({
|
|||||||
back: actions.showBack !== false,
|
back: actions.showBack !== false,
|
||||||
delete: actions.showDelete !== false && !!onDelete,
|
delete: actions.showDelete !== false && !!onDelete,
|
||||||
edit: actions.showEdit !== false,
|
edit: actions.showEdit !== false,
|
||||||
|
save: actions.showSave !== false,
|
||||||
}}
|
}}
|
||||||
labels={{
|
labels={{
|
||||||
back: actions.backLabel,
|
back: actions.backLabel,
|
||||||
|
|||||||
Reference in New Issue
Block a user