feat(WEB): Phase 6 IntegratedDetailTemplate 마이그레이션 완료

Phase 6 마이그레이션 (41개 컴포넌트 완료):
- 건설/시공: 협력업체, 시공관리, 기성관리, 발주관리, 계약관리 등
- 영업: 견적관리(V2), 고객관리(V2), 수주관리
- 회계: 청구관리, 매입관리, 매출관리, 거래처관리, 악성채권 등
- 생산: 작업지시, 검수관리
- 출고: 출하관리
- 자재: 입고관리, 재고현황
- 고객센터: 문의관리, 이벤트관리, 공지관리
- 인사: 직원관리
- 설정: 권한관리

주요 변경사항:
- 34개 xxxConfig.ts 파일 생성 (설정 기반 페이지 구성)
- PageLayout/PageHeader → IntegratedDetailTemplate 통합
- 일관된 타이틀/버튼 영역 (목록, 상세, 수정, 삭제)
- 1112줄 코드 감소 (중복 제거)

프로젝트 공통화 현황 분석 문서 추가:
- 상세 페이지 62%, 목록 페이지 82% 공통화 달성
- 추가 공통화 기회 및 로드맵 정리

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-20 15:51:02 +09:00
parent 6f457b28f3
commit 61e3a0ed60
71 changed files with 4743 additions and 4402 deletions

View File

@@ -3,12 +3,12 @@
/**
* 작업지시 상세 페이지
* API 연동 완료 (2025-12-26)
* IntegratedDetailTemplate 마이그레이션 완료 (2026-01-20)
*/
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { FileText, List, AlertTriangle, Play, CheckCircle2, Loader2, Undo2, Pencil } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { FileText, Play, CheckCircle2, Loader2, Undo2, Pencil } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
@@ -19,7 +19,8 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
import { PageLayout } from '@/components/organisms/PageLayout';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { workOrderConfig } from './workOrderConfig';
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
import { WorkLogModal } from '../WorkerScreen/WorkLogModal';
import { toast } from 'sonner';
@@ -301,109 +302,84 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
}
}, [order, orderId]);
// 로딩 상태
if (isLoading) {
return (
<PageLayout>
<h1 className="text-2xl font-bold mb-6"> </h1>
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
</div>
</PageLayout>
);
}
// 커스텀 헤더 액션 (상태 변경 버튼, 작업일지 버튼)
const customHeaderActions = useMemo(() => {
if (!order) return null;
if (!order) {
return (
<ServerErrorPage
title="작업지시를 불러올 수 없습니다"
message="작업지시를 찾을 수 없습니다."
showBackButton={true}
showHomeButton={true}
/>
);
}
return (
<PageLayout>
{/* 헤더 */}
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold"> </h1>
<div className="flex items-center gap-2">
{/* 상태 변경 버튼 */}
{order.status === 'waiting' && (
<Button
onClick={() => handleStatusChange('in_progress')}
disabled={isStatusUpdating}
className="bg-green-600 hover:bg-green-700"
>
{isStatusUpdating ? (
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
) : (
<Play className="w-4 h-4 mr-1.5" />
)}
</Button>
)}
{order.status === 'in_progress' && (
<>
<Button
variant="outline"
onClick={() => handleStatusChange('waiting')}
disabled={isStatusUpdating}
className="text-muted-foreground hover:text-foreground"
>
{isStatusUpdating ? (
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
) : (
<Undo2 className="w-4 h-4 mr-1.5" />
)}
</Button>
<Button
onClick={() => handleStatusChange('completed')}
disabled={isStatusUpdating}
className="bg-purple-600 hover:bg-purple-700"
>
{isStatusUpdating ? (
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
) : (
<CheckCircle2 className="w-4 h-4 mr-1.5" />
)}
</Button>
</>
)}
{order.status === 'completed' && (
<>
{/* 상태 변경 버튼 */}
{order.status === 'waiting' && (
<Button
onClick={() => handleStatusChange('in_progress')}
disabled={isStatusUpdating}
className="bg-green-600 hover:bg-green-700"
>
{isStatusUpdating ? (
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
) : (
<Play className="w-4 h-4 mr-1.5" />
)}
</Button>
)}
{order.status === 'in_progress' && (
<>
<Button
variant="outline"
onClick={() => handleStatusChange('in_progress')}
onClick={() => handleStatusChange('waiting')}
disabled={isStatusUpdating}
className="text-orange-600 hover:text-orange-700 border-orange-300 hover:bg-orange-50"
className="text-muted-foreground hover:text-foreground"
>
{isStatusUpdating ? (
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
) : (
<Undo2 className="w-4 h-4 mr-1.5" />
)}
</Button>
)}
<Button variant="outline" onClick={() => router.push(`/production/work-orders/${orderId}/edit`)}>
<Pencil className="w-4 h-4 mr-1.5" />
<Button
onClick={() => handleStatusChange('completed')}
disabled={isStatusUpdating}
className="bg-purple-600 hover:bg-purple-700"
>
{isStatusUpdating ? (
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
) : (
<CheckCircle2 className="w-4 h-4 mr-1.5" />
)}
</Button>
</>
)}
{order.status === 'completed' && (
<Button
variant="outline"
onClick={() => handleStatusChange('in_progress')}
disabled={isStatusUpdating}
className="text-orange-600 hover:text-orange-700 border-orange-300 hover:bg-orange-50"
>
{isStatusUpdating ? (
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
) : (
<Undo2 className="w-4 h-4 mr-1.5" />
)}
</Button>
<Button variant="outline" onClick={() => setIsWorkLogOpen(true)}>
<FileText className="w-4 h-4 mr-1.5" />
</Button>
<Button variant="outline" onClick={() => router.push('/production/work-orders')}>
<List className="w-4 h-4 mr-1.5" />
</Button>
</div>
</div>
)}
<Button variant="outline" onClick={() => setIsWorkLogOpen(true)}>
<FileText className="w-4 h-4 mr-1.5" />
</Button>
</>
);
}, [order, isStatusUpdating, handleStatusChange]);
// 폼 내용 렌더링
const renderFormContent = () => {
if (!order) return null;
return (
<div className="space-y-6">
{/* 기본 정보 */}
<div className="bg-amber-50 border border-amber-200 rounded-lg p-6">
@@ -579,13 +555,42 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
{/* 이슈 섹션 */}
<IssueSection order={order} />
</div>
);
};
// 데이터 없음
if (!isLoading && !order) {
return (
<ServerErrorPage
title="작업지시를 불러올 수 없습니다"
message="작업지시를 찾을 수 없습니다."
showBackButton={true}
showHomeButton={true}
/>
);
}
return (
<>
<IntegratedDetailTemplate
config={workOrderConfig}
mode="view"
initialData={{}}
itemId={orderId}
isLoading={isLoading}
headerActions={customHeaderActions}
renderView={() => renderFormContent()}
renderForm={() => renderFormContent()}
/>
{/* 작업일지 모달 */}
<WorkLogModal
open={isWorkLogOpen}
onOpenChange={setIsWorkLogOpen}
workOrderId={order.id}
/>
</PageLayout>
{order && (
<WorkLogModal
open={isWorkLogOpen}
onOpenChange={setIsWorkLogOpen}
workOrderId={order.id}
/>
)}
</>
);
}

View File

@@ -0,0 +1,30 @@
import { FileText } from 'lucide-react';
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
/**
* 작업지시 상세 페이지 Config
*
* 참고: 이 config는 타이틀/버튼 영역만 정의
* 폼 내용은 기존 WorkOrderDetail의 renderView에서 처리
* (공정 진행 단계, 작업 품목 테이블, 상태 변경 버튼, 작업일지 모달 등 특수 기능 유지)
*
* 특이사항:
* - view 모드만 지원 (수정은 별도 /edit 페이지로 이동)
* - 삭제 기능 없음
* - 상태 변경 버튼이 많음 (작업 시작, 완료, 되돌리기 등)
*/
export const workOrderConfig: DetailConfig = {
title: '작업지시 상세',
description: '작업지시 정보를 조회하고 관리합니다',
icon: FileText,
basePath: '/production/work-orders',
fields: [], // renderView 사용으로 필드 정의 불필요
gridColumns: 2,
actions: {
showBack: true,
showDelete: false, // 작업지시는 삭제 기능 없음
showEdit: true,
backLabel: '목록',
editLabel: '수정',
},
};