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

@@ -2,6 +2,7 @@
/**
* 수주 수정 컴포넌트 (Edit Mode)
* IntegratedDetailTemplate 마이그레이션 완료 (2026-01-20)
*
* - 기본 정보 (읽기전용)
* - 수주/배송 정보 (편집 가능)
@@ -9,7 +10,7 @@
* - 품목 내역 (생산 시작 후 수정 불가)
*/
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo, useCallback } from "react";
import { useRouter } from "next/navigation";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
@@ -31,11 +32,10 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { FileText, AlertTriangle } from "lucide-react";
import { AlertTriangle } from "lucide-react";
import { toast } from "sonner";
import { PageLayout } from "@/components/organisms/PageLayout";
import { PageHeader } from "@/components/organisms/PageHeader";
import { FormActions } from "@/components/organisms/FormActions";
import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate";
import { orderSalesConfig } from "./orderSalesConfig";
import { BadgeSm } from "@/components/atoms/BadgeSm";
import { formatAmount } from "@/utils/formatAmount";
import {
@@ -180,24 +180,21 @@ export function OrderSalesDetailEdit({ orderId }: OrderSalesDetailEditProps) {
router.push(`/sales/order-management-sales/${orderId}`);
};
const handleSave = async () => {
if (!form) return;
// IntegratedDetailTemplate용 onSubmit 핸들러
const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
if (!form) return { success: false, error: "폼 데이터가 없습니다." };
// 유효성 검사
if (!form.deliveryRequestDate) {
toast.error("납품요청일을 입력해주세요.");
return;
return { success: false, error: "납품요청일을 입력해주세요." };
}
if (!form.receiver.trim()) {
toast.error("수신(반장/업체)을 입력해주세요.");
return;
return { success: false, error: "수신(반장/업체)을 입력해주세요." };
}
if (!form.receiverContact.trim()) {
toast.error("수신처 연락처를 입력해주세요.");
return;
return { success: false, error: "수신처 연락처를 입력해주세요." };
}
setIsSaving(true);
try {
// API 연동
const result = await updateOrder(orderId, {
@@ -227,43 +224,47 @@ export function OrderSalesDetailEdit({ orderId }: OrderSalesDetailEditProps) {
toast.success("수주가 수정되었습니다.");
// V2 패턴: 저장 후 view 모드로 이동
router.push(`/sales/order-management-sales/${orderId}`);
return { success: true };
} else {
toast.error(result.error || "수주 수정에 실패했습니다.");
return { success: false, error: result.error || "수주 수정에 실패했습니다." };
}
} catch (error) {
console.error("Error updating order:", error);
toast.error("수주 수정 중 오류가 발생했습니다.");
} finally {
setIsSaving(false);
return { success: false, error: "수주 수정 중 오류가 발생했습니다." };
}
};
}, [form, orderId, router]);
if (loading || !form) {
// 동적 config (수정 모드용 타이틀)
const dynamicConfig = useMemo(() => {
return {
...orderSalesConfig,
title: "수주 수정",
actions: {
...orderSalesConfig.actions,
showEdit: false, // 수정 모드에서는 수정 버튼 숨김
showDelete: false,
},
};
}, []);
// 커스텀 헤더 액션 (상태 뱃지)
const customHeaderActions = useMemo(() => {
if (!form) return null;
return (
<PageLayout>
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
</div>
</PageLayout>
<div className="flex items-center gap-2">
<code className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
{form.lotNumber}
</code>
{getOrderStatusBadge(form.status)}
</div>
);
}
}, [form]);
return (
<PageLayout>
{/* 헤더 */}
<PageHeader
title="수주 수정"
icon={FileText}
actions={
<div className="flex items-center gap-2">
<code className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
{form.lotNumber}
</code>
{getOrderStatusBadge(form.status)}
</div>
}
/>
// 폼 콘텐츠 렌더링
const renderFormContent = useCallback(() => {
if (!form) return null;
return (
<div className="space-y-6">
{/* 기본 정보 (읽기전용) */}
<Card>
@@ -548,18 +549,20 @@ export function OrderSalesDetailEdit({ orderId }: OrderSalesDetailEditProps) {
</CardContent>
</Card>
</div>
);
}, [form]);
{/* 하단 버튼 */}
<div className="sticky bottom-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-t pt-4 pb-4 -mx-3 md:-mx-6 px-3 md:px-6 mt-6">
<FormActions
onSave={handleSave}
onCancel={handleCancel}
saveLabel="저장"
cancelLabel="취소"
saveLoading={isSaving}
saveDisabled={isSaving}
/>
</div>
</PageLayout>
return (
<IntegratedDetailTemplate
config={dynamicConfig}
mode="edit"
initialData={form || {}}
itemId={orderId}
isLoading={loading}
headerActions={customHeaderActions}
onSubmit={handleSubmit}
renderView={() => renderFormContent()}
renderForm={() => renderFormContent()}
/>
);
}

View File

@@ -2,6 +2,7 @@
/**
* 수주 상세 보기 컴포넌트 (View Mode)
* IntegratedDetailTemplate 마이그레이션 완료 (2026-01-20)
*
* - 문서 모달: 계약서, 거래명세서, 발주서
* - 기본 정보, 수주/배송 정보, 비고
@@ -9,7 +10,7 @@
* - 상태별 버튼 차이
*/
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo, useCallback } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -23,19 +24,16 @@ import {
} from "@/components/ui/table";
import {
FileText,
ArrowLeft,
Edit,
Factory,
XCircle,
FileSpreadsheet,
FileCheck,
ClipboardList,
Eye,
CheckCircle2,
} 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 { orderSalesConfig } from "./orderSalesConfig";
import { BadgeSm } from "@/components/atoms/BadgeSm";
import { formatAmount } from "@/utils/formatAmount";
import {
@@ -219,95 +217,66 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) {
};
// 문서 모달 열기
const openDocumentModal = (type: OrderDocumentType) => {
const openDocumentModal = useCallback((type: OrderDocumentType) => {
setDocumentType(type);
setDocumentModalOpen(true);
};
}, []);
// 동적 config (상태별 수정 버튼 표시)
const dynamicConfig = useMemo(() => {
const canEdit = order?.status !== "shipped" && order?.status !== "cancelled";
return {
...orderSalesConfig,
actions: {
...orderSalesConfig.actions,
showEdit: canEdit,
},
};
}, [order?.status]);
// 커스텀 헤더 액션 (상태별 버튼들)
const customHeaderActions = useMemo(() => {
if (!order) return null;
const showConfirmButton = order.status === "order_registered";
const showProductionCreateButton =
order.status !== "shipped" &&
order.status !== "cancelled" &&
order.status !== "production_ordered";
const showCancelButton =
order.status !== "shipped" &&
order.status !== "cancelled" &&
order.status !== "production_ordered";
if (loading) {
return (
<PageLayout>
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
</div>
</PageLayout>
<>
{showConfirmButton && (
<Button onClick={handleConfirmOrder} className="bg-green-600 hover:bg-green-700">
<CheckCircle2 className="h-4 w-4 mr-2" />
</Button>
)}
{showProductionCreateButton && (
<Button onClick={handleProductionOrder}>
<Factory className="h-4 w-4 mr-2" />
</Button>
)}
{showCancelButton && (
<Button variant="outline" onClick={handleCancel} className="border-orange-200 text-orange-600 hover:border-orange-300">
<XCircle className="h-4 w-4 mr-2" />
</Button>
)}
</>
);
}
}, [order, handleConfirmOrder, handleProductionOrder, handleCancel]);
// 폼 콘텐츠 렌더링
const renderFormContent = useCallback(() => {
if (!order) return null;
if (!order) {
return (
<ServerErrorPage
title="수주 정보를 불러올 수 없습니다"
message="수주 정보를 찾을 수 없습니다."
showBackButton={true}
showHomeButton={true}
/>
);
}
// 상태별 버튼 표시 여부
const showEditButton = order.status !== "shipped" && order.status !== "cancelled";
// 수주 확정 버튼: 수주등록 상태에서만 표시
const showConfirmButton = order.status === "order_registered";
// 생산지시 생성 버튼: 출하완료, 취소, 생산지시완료 제외하고 표시
// (수주등록, 수주확정, 생산중, 재작업중, 작업완료에서 표시)
const showProductionCreateButton =
order.status !== "shipped" &&
order.status !== "cancelled" &&
order.status !== "production_ordered";
// 생산지시 보기 버튼: 생산지시완료 상태에서 숨김 (기획서 오류로 제거)
const showProductionViewButton = false;
const showCancelButton =
order.status !== "shipped" &&
order.status !== "cancelled" &&
order.status !== "production_ordered";
return (
<PageLayout>
{/* 헤더 */}
<PageHeader
title="수주 상세"
icon={FileText}
actions={
<div className="flex items-center gap-2 flex-wrap">
<Button variant="outline" onClick={handleBack}>
<ArrowLeft className="h-4 w-4 mr-2" />
</Button>
{showEditButton && (
<Button variant="outline" onClick={handleEdit}>
<Edit className="h-4 w-4 mr-2" />
</Button>
)}
{showConfirmButton && (
<Button onClick={handleConfirmOrder} className="bg-green-600 hover:bg-green-700">
<CheckCircle2 className="h-4 w-4 mr-2" />
</Button>
)}
{showProductionCreateButton && (
<Button onClick={handleProductionOrder}>
<Factory className="h-4 w-4 mr-2" />
</Button>
)}
{showProductionViewButton && (
<Button onClick={handleViewProductionOrder}>
<Eye className="h-4 w-4 mr-2" />
</Button>
)}
{showCancelButton && (
<Button variant="outline" onClick={handleCancel} className="border-orange-200 text-orange-600 hover:border-orange-300">
<XCircle className="h-4 w-4 mr-2" />
</Button>
)}
</div>
}
/>
<div className="space-y-6">
{/* 수주 정보 헤더 */}
<Card>
@@ -473,9 +442,37 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) {
</CardContent>
</Card>
</div>
);
}, [order, openDocumentModal]);
// 에러 상태
if (!loading && !order) {
return (
<ServerErrorPage
title="수주 정보를 불러올 수 없습니다"
message="수주 정보를 찾을 수 없습니다."
showBackButton={true}
showHomeButton={true}
/>
);
}
return (
<>
<IntegratedDetailTemplate
config={dynamicConfig}
mode="view"
initialData={order || {}}
itemId={orderId}
isLoading={loading}
headerActions={customHeaderActions}
renderView={() => renderFormContent()}
renderForm={() => renderFormContent()}
/>
{/* 문서 모달 */}
<OrderDocumentModal
{order && (
<OrderDocumentModal
open={documentModalOpen}
onOpenChange={setDocumentModalOpen}
documentType={documentType}
@@ -497,8 +494,10 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) {
remarks: order.remarks,
}}
/>
)}
{/* 취소 확인 다이얼로그 */}
{order && (
<Dialog open={isCancelDialogOpen} onOpenChange={setIsCancelDialogOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
@@ -591,8 +590,10 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) {
</DialogFooter>
</DialogContent>
</Dialog>
)}
{/* 수주 확정 다이얼로그 */}
{order && (
<Dialog open={isConfirmDialogOpen} onOpenChange={setIsConfirmDialogOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
@@ -658,6 +659,7 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) {
</DialogFooter>
</DialogContent>
</Dialog>
</PageLayout>
)}
</>
);
}

View File

@@ -0,0 +1,32 @@
import { FileText } from 'lucide-react';
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
/**
* 수주관리 상세 페이지 Config
*
* 참고: 이 config는 타이틀/버튼 영역만 정의
* 폼 내용은 기존 OrderSalesDetailView/Edit의 renderView/renderForm에서 처리
* (문서 모달, 상태별 버튼, 취소/확정 다이얼로그 등 특수 기능 유지)
*
* 특이사항:
* - view/edit 모드 지원
* - 상태별 동적 버튼 (수주확정, 생산지시 생성, 취소 등)
* - 문서 모달: 계약서, 거래명세서, 발주서
*/
export const orderSalesConfig: DetailConfig = {
title: '수주 상세',
description: '수주 정보를 조회하고 관리합니다',
icon: FileText,
basePath: '/sales/order-management-sales',
fields: [], // renderView/renderForm 사용으로 필드 정의 불필요
gridColumns: 2,
actions: {
showBack: true,
showDelete: false, // 삭제 대신 취소 기능 사용
showEdit: true,
backLabel: '목록',
editLabel: '수정',
submitLabel: '저장',
cancelLabel: '취소',
},
};