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:
유병철
2026-01-20 19:31:07 +09:00
parent 6b0ffc810b
commit 62ef2b1ff9
24 changed files with 861 additions and 534 deletions

View File

@@ -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) 등록 마이그레이션 완료 |

View File

@@ -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: '복제된 문서를 수정 후 상신합니다',
};

View File

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

View 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: '저장',
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: '출고 정보를 수정합니다',
};

View File

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

View 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: '저장',
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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