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:
@@ -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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
32
src/components/orders/orderSalesConfig.ts
Normal file
32
src/components/orders/orderSalesConfig.ts
Normal 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: '취소',
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user