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 { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { format } from 'date-fns';
|
||||
import { FileText, Trash2, Send, Save, ArrowLeft, Eye } from 'lucide-react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { Trash2, Send, Save, Eye, Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import {
|
||||
documentCreateConfig,
|
||||
documentEditConfig,
|
||||
documentCopyConfig,
|
||||
} from './documentCreateConfig';
|
||||
import {
|
||||
getExpenseEstimateItems,
|
||||
getEmployees,
|
||||
createApproval,
|
||||
createAndSubmitApproval,
|
||||
getApprovalById,
|
||||
@@ -18,7 +21,6 @@ import {
|
||||
deleteApproval,
|
||||
} from './actions';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { BasicInfoSection } from './BasicInfoSection';
|
||||
import { ApprovalLineSection } from './ApprovalLineSection';
|
||||
import { ReferenceSection } from './ReferenceSection';
|
||||
@@ -33,7 +35,6 @@ import type {
|
||||
ExpenseEstimateDocumentData,
|
||||
} from '@/components/approval/DocumentDetail/types';
|
||||
import type {
|
||||
DocumentType,
|
||||
BasicInfo,
|
||||
ApprovalPerson,
|
||||
ProposalData,
|
||||
@@ -416,7 +417,7 @@ export function DocumentCreate() {
|
||||
approvers,
|
||||
drafter,
|
||||
};
|
||||
default:
|
||||
default: {
|
||||
// 이미 업로드된 파일 URL (Next.js 프록시 사용) + 새로 추가된 파일 미리보기 URL
|
||||
const uploadedFileUrls = (proposalData.uploadedFiles || []).map(f =>
|
||||
`/api/proxy/files/${f.id}/download`
|
||||
@@ -436,6 +437,7 @@ export function DocumentCreate() {
|
||||
approvers,
|
||||
drafter,
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [basicInfo, approvalLine, proposalData, expenseReportData, expenseEstimateData]);
|
||||
|
||||
@@ -453,75 +455,63 @@ export function DocumentCreate() {
|
||||
}
|
||||
};
|
||||
|
||||
// 문서 로딩 중
|
||||
if (isLoadingDocument) {
|
||||
// 현재 모드에 맞는 config 선택
|
||||
const currentConfig = isEditMode
|
||||
? documentEditConfig
|
||||
: isCopyMode
|
||||
? documentCopyConfig
|
||||
: documentCreateConfig;
|
||||
|
||||
// 헤더 액션 버튼 렌더링
|
||||
const renderHeaderActions = useCallback(() => {
|
||||
return (
|
||||
<div className="container mx-auto py-6 px-4 max-w-4xl">
|
||||
<Card className="mb-6">
|
||||
<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}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={handlePreview}>
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
상세
|
||||
미리보기
|
||||
</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" />
|
||||
삭제
|
||||
</Button>
|
||||
<Button variant="default" className="min-w-[80px]" onClick={handleSubmit} disabled={isPending}>
|
||||
{isPending ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Send className="w-4 h-4 mr-1" />}
|
||||
<Button
|
||||
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 variant="secondary" className="min-w-[80px]" onClick={handleSaveDraft} disabled={isPending}>
|
||||
{isPending ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Save className="w-4 h-4 mr-1" />}
|
||||
<Button
|
||||
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 ? '저장' : '임시저장'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}, [handlePreview, handleDelete, handleSubmit, handleSaveDraft, isPending, isEditMode]);
|
||||
|
||||
{/* 폼 영역 */}
|
||||
// 폼 컨텐츠 렌더링
|
||||
const renderFormContent = useCallback(() => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 기본 정보 */}
|
||||
<BasicInfoSection data={basicInfo} onChange={setBasicInfo} />
|
||||
@@ -535,27 +525,19 @@ export function DocumentCreate() {
|
||||
{/* 문서 유형별 폼 */}
|
||||
{renderDocumentTypeForm()}
|
||||
</div>
|
||||
);
|
||||
}, [basicInfo, approvalLine, references, renderDocumentTypeForm]);
|
||||
|
||||
{/* 하단 고정 버튼 (모바일) */}
|
||||
<div className="fixed bottom-0 left-0 right-0 p-4 bg-white border-t md:hidden">
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="flex-1" onClick={handleDelete} disabled={isPending}>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
삭제
|
||||
</Button>
|
||||
<Button variant="secondary" className="flex-1" onClick={handleSaveDraft} disabled={isPending}>
|
||||
{isPending ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Save className="w-4 h-4 mr-1" />}
|
||||
{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" />
|
||||
return (
|
||||
<>
|
||||
<IntegratedDetailTemplate
|
||||
config={currentConfig}
|
||||
mode={isEditMode ? 'edit' : 'create'}
|
||||
isLoading={isLoadingDocument}
|
||||
onBack={handleBack}
|
||||
renderForm={renderFormContent}
|
||||
headerActions={renderHeaderActions()}
|
||||
/>
|
||||
|
||||
{/* 미리보기 모달 */}
|
||||
<DocumentDetailModal
|
||||
@@ -577,6 +559,6 @@ export function DocumentCreate() {
|
||||
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 { useRouter } from 'next/navigation';
|
||||
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 { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { boardCreateConfig, boardEditConfig } from './boardFormConfig';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -212,31 +213,9 @@ export function BoardForm({ mode, initialData }: BoardFormProps) {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* 헤더 */}
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
|
||||
// ===== 폼 콘텐츠 렌더링 =====
|
||||
const renderFormContent = useCallback(() => (
|
||||
<>
|
||||
{/* 폼 카드 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -488,7 +467,28 @@ export function BoardForm({ mode, initialData }: BoardFormProps) {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</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 문의 등록/수정 폼 컴포넌트
|
||||
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||
*
|
||||
* 디자인 스펙:
|
||||
* - 페이지 타이틀: 1:1 문의 등록 / 1:1 문의 수정
|
||||
@@ -11,9 +12,9 @@
|
||||
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { MessageSquare, Upload, X, File, ArrowLeft, Save } from 'lucide-react';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { Upload, X, File } from 'lucide-react';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { inquiryCreateConfig, inquiryEditConfig } from './inquiryConfig';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -134,29 +135,9 @@ export function InquiryForm({ mode, initialData }: InquiryFormProps) {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* 헤더 */}
|
||||
<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>
|
||||
// ===== 폼 콘텐츠 렌더링 =====
|
||||
const renderFormContent = useCallback(() => (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-1">
|
||||
문의 정보
|
||||
@@ -303,7 +284,22 @@ export function InquiryForm({ mode, initialData }: InquiryFormProps) {
|
||||
</div>
|
||||
</CardContent>
|
||||
</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 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,11 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
/**
|
||||
* 사원 등록/수정/상세 폼 컴포넌트
|
||||
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useDaumPostcode } from '@/hooks/useDaumPostcode';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { toast } from 'sonner';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { employeeCreateConfig, employeeEditConfig, employeeConfig } from './employeeConfig';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -17,7 +22,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} 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 { FieldSettingsDialog } from './FieldSettingsDialog';
|
||||
import type {
|
||||
@@ -357,16 +362,12 @@ export function EmployeeForm({
|
||||
router.push(`/${locale}/hr/employee-management`);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* 헤더 + 버튼 영역 */}
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<PageHeader
|
||||
title={title}
|
||||
description={description}
|
||||
icon={Users}
|
||||
/>
|
||||
{!isViewMode && (
|
||||
// ===== 폼 콘텐츠 렌더링 =====
|
||||
const renderFormContent = useCallback(() => (
|
||||
<>
|
||||
{/* 항목 설정 버튼 */}
|
||||
{!isViewMode && (
|
||||
<div className="flex justify-end mb-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -375,10 +376,10 @@ export function EmployeeForm({
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
항목 설정
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-6">
|
||||
{/* 사원 정보 - 프로필 사진 + 기본 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -932,31 +933,7 @@ export function EmployeeForm({
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 버튼 영역 */}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* 항목 설정 모달 */}
|
||||
<FieldSettingsDialog
|
||||
@@ -965,6 +942,33 @@ export function EmployeeForm({
|
||||
settings={fieldSettings}
|
||||
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 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
|
||||
*
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { format, differenceInDays } from 'date-fns';
|
||||
import { CalendarIcon, Loader2 } from 'lucide-react';
|
||||
import { differenceInDays, parseISO } from 'date-fns';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -11,13 +11,8 @@ import {
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -25,7 +20,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { VacationRequestFormData, LeaveType } from './types';
|
||||
import { LEAVE_TYPE_LABELS } from './types';
|
||||
import { getActiveEmployees, type EmployeeOption } from './actions';
|
||||
@@ -48,8 +42,6 @@ export function VacationRequestDialog({
|
||||
endDate: '',
|
||||
vacationDays: 1,
|
||||
});
|
||||
const [startDate, setStartDate] = useState<Date | undefined>();
|
||||
const [endDate, setEndDate] = useState<Date | undefined>();
|
||||
const [employees, setEmployees] = useState<EmployeeOption[]>([]);
|
||||
const [isLoadingEmployees, setIsLoadingEmployees] = useState(false);
|
||||
|
||||
@@ -76,33 +68,31 @@ export function VacationRequestDialog({
|
||||
endDate: '',
|
||||
vacationDays: 1,
|
||||
});
|
||||
setStartDate(undefined);
|
||||
setEndDate(undefined);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// 날짜 변경 시 휴가 일수 자동 계산
|
||||
useEffect(() => {
|
||||
if (startDate && endDate) {
|
||||
const days = differenceInDays(endDate, startDate) + 1;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
startDate: format(startDate, 'yyyy-MM-dd'),
|
||||
endDate: format(endDate, 'yyyy-MM-dd'),
|
||||
vacationDays: days > 0 ? days : 1,
|
||||
}));
|
||||
if (formData.startDate && formData.endDate) {
|
||||
const start = parseISO(formData.startDate);
|
||||
const end = parseISO(formData.endDate);
|
||||
const days = differenceInDays(end, start) + 1;
|
||||
if (days > 0 && days !== formData.vacationDays) {
|
||||
setFormData(prev => ({ ...prev, vacationDays: days }));
|
||||
}
|
||||
}
|
||||
}, [startDate, endDate]);
|
||||
}, [formData.startDate, formData.endDate, formData.vacationDays]);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!formData.employeeId) {
|
||||
alert('사원을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
if (!startDate || !endDate) {
|
||||
if (!formData.startDate || !formData.endDate) {
|
||||
alert('휴가 기간을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
if (endDate < startDate) {
|
||||
if (formData.endDate < formData.startDate) {
|
||||
alert('종료일은 시작일 이후여야 합니다.');
|
||||
return;
|
||||
}
|
||||
@@ -174,61 +164,29 @@ export function VacationRequestDialog({
|
||||
|
||||
{/* 시작일 */}
|
||||
<div className="grid gap-2">
|
||||
<Label>시작일</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'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>
|
||||
<Label htmlFor="startDate">시작일</Label>
|
||||
<Input
|
||||
id="startDate"
|
||||
type="date"
|
||||
value={formData.startDate}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, startDate: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 종료일 */}
|
||||
<div className="grid gap-2">
|
||||
<Label>종료일</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'w-full justify-start text-left font-normal',
|
||||
!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>
|
||||
<Label htmlFor="endDate">종료일</Label>
|
||||
<Input
|
||||
id="endDate"
|
||||
type="date"
|
||||
value={formData.endDate}
|
||||
min={formData.startDate || undefined}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, endDate: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 휴가 일수 (자동 계산) */}
|
||||
{startDate && endDate && (
|
||||
{formData.startDate && formData.endDate && (
|
||||
<div className="grid gap-2">
|
||||
<Label>휴가 일수</Label>
|
||||
<div className="p-3 bg-muted rounded-md text-center font-medium">
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
/**
|
||||
* 수입검사 등록 (IQC) 페이지
|
||||
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||
*
|
||||
* - 검사 대상 선택
|
||||
* - 검사 정보 입력 (검사일, 검사자*, LOT번호)
|
||||
* - 검사 항목 테이블 (겉모양, 두께, 폭, 길이)
|
||||
@@ -10,13 +12,14 @@
|
||||
|
||||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
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 { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -183,27 +186,10 @@ export function InspectionCreate({ id }: Props) {
|
||||
router.push('/ko/material/receiving-management');
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="space-y-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">
|
||||
// ===== 폼 콘텐츠 렌더링 =====
|
||||
const renderFormContent = useCallback(() => (
|
||||
<>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* 좌측: 검사 대상 선택 */}
|
||||
<div className="lg:col-span-1 space-y-2">
|
||||
<Label className="text-sm font-medium">검사 대상 선택</Label>
|
||||
@@ -362,7 +348,6 @@ export function InspectionCreate({ id }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 성공 다이얼로그 */}
|
||||
<SuccessDialog
|
||||
@@ -371,6 +356,23 @@ export function InspectionCreate({ id }: Props) {
|
||||
lotNo={lotNo}
|
||||
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)
|
||||
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
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 { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
@@ -22,7 +20,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { shipmentCreateConfig } from './shipmentConfig';
|
||||
import {
|
||||
createShipment,
|
||||
getLotOptions,
|
||||
@@ -177,63 +176,10 @@ export function ShipmentCreate() {
|
||||
}
|
||||
}, [formData, router]);
|
||||
|
||||
// 로딩 상태 표시
|
||||
if (isLoading) {
|
||||
return (
|
||||
<PageLayout>
|
||||
<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 에러 표시 */}
|
||||
// 폼 컨텐츠 렌더링
|
||||
const renderFormContent = useCallback(() => (
|
||||
<div className="space-y-6">
|
||||
{/* Validation 에러 표시 */}
|
||||
{validationErrors.length > 0 && (
|
||||
<Alert className="bg-red-50 border-red-200">
|
||||
<AlertDescription className="text-red-900">
|
||||
@@ -428,6 +374,35 @@ export function ShipmentCreate() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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)
|
||||
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
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 { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
@@ -23,8 +21,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { shipmentEditConfig } from './shipmentConfig';
|
||||
import {
|
||||
getShipmentById,
|
||||
getLogisticsOptions,
|
||||
@@ -215,63 +213,23 @@ export function ShipmentEdit({ id }: ShipmentEditProps) {
|
||||
}
|
||||
}, [id, formData, router]);
|
||||
|
||||
// 로딩 상태 표시
|
||||
if (isLoading) {
|
||||
return (
|
||||
<PageLayout>
|
||||
<ContentLoadingSpinner text="출고 정보를 불러오는 중..." />
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
// 동적 config (로트번호 + 상태 표시)
|
||||
const dynamicConfig = {
|
||||
...shipmentEditConfig,
|
||||
title: detail ? `출고 수정 (${detail.lotNo})` : '출고 수정',
|
||||
};
|
||||
|
||||
// 에러 상태 표시
|
||||
if (error || !detail) {
|
||||
return (
|
||||
<ServerErrorPage
|
||||
title="출하 정보를 불러올 수 없습니다"
|
||||
message={error || '출하 정보를 찾을 수 없습니다.'}
|
||||
showBackButton={true}
|
||||
showHomeButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// 폼 컨텐츠 렌더링
|
||||
const renderFormContent = useCallback(() => {
|
||||
if (!detail) return null;
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
return (
|
||||
<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>
|
||||
<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 className="flex items-center gap-2">
|
||||
<Badge className={`text-xs ${SHIPMENT_STATUS_STYLES[detail.status]}`}>
|
||||
{SHIPMENT_STATUS_LABELS[detail.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Validation 에러 표시 */}
|
||||
@@ -536,6 +494,38 @@ export function ShipmentEdit({ id }: ShipmentEditProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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 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';
|
||||
|
||||
/**
|
||||
* 공정 등록/수정 폼 컴포넌트
|
||||
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
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 { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -17,7 +24,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { RuleModal } from './RuleModal';
|
||||
import { toast } from 'sonner';
|
||||
import type { Process, ClassificationRule, ProcessType } from '@/types/process';
|
||||
@@ -190,27 +196,9 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) {
|
||||
router.back();
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* 헤더 */}
|
||||
<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>
|
||||
|
||||
// ===== 폼 콘텐츠 렌더링 =====
|
||||
const renderFormContent = useCallback(() => (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
{/* 기본 정보 */}
|
||||
<Card>
|
||||
@@ -460,6 +448,27 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) {
|
||||
onAdd={handleSaveRule}
|
||||
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)
|
||||
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
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 { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -21,13 +22,14 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { SalesOrderSelectModal } from './SalesOrderSelectModal';
|
||||
import { AssigneeSelectModal } from './AssigneeSelectModal';
|
||||
import { toast } from 'sonner';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
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 에러 타입
|
||||
interface ValidationErrors {
|
||||
@@ -208,31 +210,9 @@ export function WorkOrderCreate() {
|
||||
return selectedProcess?.processCode || '-';
|
||||
};
|
||||
|
||||
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>
|
||||
</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">
|
||||
// 폼 컨텐츠 렌더링
|
||||
const renderFormContent = useCallback(() => (
|
||||
<div className="space-y-6">
|
||||
{/* Validation 에러 표시 */}
|
||||
{Object.keys(validationErrors).length > 0 && (
|
||||
<Alert className="bg-red-50 border-red-200">
|
||||
@@ -497,6 +477,19 @@ export function WorkOrderCreate() {
|
||||
/>
|
||||
</section>
|
||||
</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
|
||||
@@ -515,6 +508,6 @@ export function WorkOrderCreate() {
|
||||
setAssigneeNames(names);
|
||||
}}
|
||||
/>
|
||||
</PageLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,12 +3,11 @@
|
||||
/**
|
||||
* 작업지시 수정 페이지
|
||||
* WorkOrderCreate 패턴 기반
|
||||
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
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 { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
@@ -20,13 +19,13 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { AssigneeSelectModal } from './AssigneeSelectModal';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { toast } from 'sonner';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { getWorkOrderById, updateWorkOrder, getProcessOptions, type ProcessOption } from './actions';
|
||||
import type { WorkOrder } from './types';
|
||||
import { workOrderEditConfig } from './workOrderConfig';
|
||||
|
||||
// Validation 에러 타입
|
||||
interface ValidationErrors {
|
||||
@@ -199,41 +198,15 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
return selectedProcess?.processCode || '-';
|
||||
};
|
||||
|
||||
// 로딩 상태
|
||||
if (isLoading) {
|
||||
return <ContentLoadingSpinner text="작업지시 정보를 불러오는 중..." />;
|
||||
}
|
||||
// 동적 config (작업지시 번호 포함)
|
||||
const dynamicConfig = {
|
||||
...workOrderEditConfig,
|
||||
title: `작업지시 수정 ${workOrder ? `(${workOrder.workOrderNo})` : ''}`,
|
||||
};
|
||||
|
||||
if (!workOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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">
|
||||
// 폼 컨텐츠 렌더링
|
||||
const renderFormContent = useCallback(() => (
|
||||
<div className="space-y-6">
|
||||
{/* Validation 에러 표시 */}
|
||||
{Object.keys(validationErrors).length > 0 && (
|
||||
<Alert className="bg-red-50 border-red-200">
|
||||
@@ -386,6 +359,20 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
/>
|
||||
</section>
|
||||
</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
|
||||
@@ -397,6 +384,6 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
setAssigneeNames(names);
|
||||
}}
|
||||
/>
|
||||
</PageLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { FileText } from 'lucide-react';
|
||||
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||
|
||||
@@ -28,3 +30,35 @@ export const workOrderConfig: DetailConfig = {
|
||||
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)
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ClipboardCheck, ImageIcon, Loader2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ImageIcon } from 'lucide-react';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { qualityInspectionCreateConfig } from './inspectionConfig';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { toast } from 'sonner';
|
||||
import { createInspection } from './actions';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
@@ -89,9 +90,9 @@ export function InspectionCreate() {
|
||||
}, []);
|
||||
|
||||
// 취소
|
||||
const handleCancel = () => {
|
||||
const handleCancel = useCallback(() => {
|
||||
router.push('/quality/inspections');
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
// validation 체크
|
||||
const validateForm = (): boolean => {
|
||||
@@ -156,33 +157,10 @@ export function InspectionCreate() {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="space-y-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">검사 등록</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 에러 표시 */}
|
||||
// ===== 폼 콘텐츠 렌더링 =====
|
||||
const renderFormContent = useCallback(() => (
|
||||
<div className="space-y-6">
|
||||
{/* Validation 에러 표시 */}
|
||||
{validationErrors.length > 0 && (
|
||||
<Alert className="bg-red-50 border-red-200">
|
||||
<AlertDescription className="text-red-900">
|
||||
@@ -352,6 +330,18 @@ export function InspectionCreate() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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 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
|
||||
*
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface DetailActionsProps {
|
||||
back?: boolean;
|
||||
delete?: boolean;
|
||||
edit?: boolean;
|
||||
save?: boolean;
|
||||
};
|
||||
/** 버튼 라벨 */
|
||||
labels?: {
|
||||
@@ -74,6 +75,7 @@ export function DetailActions({
|
||||
back: showBack = true,
|
||||
delete: showDelete = true,
|
||||
edit: showEdit = true,
|
||||
save: showSave = true,
|
||||
} = showButtons;
|
||||
|
||||
const {
|
||||
@@ -133,11 +135,16 @@ export function DetailActions({
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
|
||||
{/* 오른쪽: 저장/등록 */}
|
||||
<Button onClick={onSubmit} disabled={isSubmitting}>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{actualSubmitLabel}
|
||||
</Button>
|
||||
{/* 오른쪽: 추가액션 + 저장/등록 */}
|
||||
<div className="flex items-center gap-2">
|
||||
{extraActions}
|
||||
{showSave && onSubmit && (
|
||||
<Button onClick={onSubmit} disabled={isSubmitting}>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{actualSubmitLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -302,6 +302,7 @@ export function IntegratedDetailTemplate<T extends Record<string, unknown>>({
|
||||
back: actions.showBack !== false,
|
||||
delete: actions.showDelete !== false && !!onDelete,
|
||||
edit: actions.showEdit !== false,
|
||||
save: actions.showSave !== false,
|
||||
}}
|
||||
labels={{
|
||||
back: actions.backLabel,
|
||||
|
||||
Reference in New Issue
Block a user