refactor: IntegratedDetailTemplate 마이그레이션 (수주/견적)
- order-management-sales/[id]/page.tsx: PageLayout → IntegratedDetailTemplate (view) - order-management-sales/[id]/edit/page.tsx: PageLayout → IntegratedDetailTemplate (edit) - EstimateDetailForm.tsx: PageLayout → IntegratedDetailTemplate (view/edit) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
* - 품목 내역 (생산 시작 후 수정 불가)
|
* - 품목 내역 (생산 시작 후 수정 불가)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useRouter, useParams } from "next/navigation";
|
import { useRouter, useParams } from "next/navigation";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
@@ -31,11 +31,10 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { FileText, AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { PageLayout } from "@/components/organisms/PageLayout";
|
import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate";
|
||||||
import { PageHeader } from "@/components/organisms/PageHeader";
|
import { orderSalesConfig } from "@/components/orders/orderSalesConfig";
|
||||||
import { FormActions } from "@/components/organisms/FormActions";
|
|
||||||
import { BadgeSm } from "@/components/atoms/BadgeSm";
|
import { BadgeSm } from "@/components/atoms/BadgeSm";
|
||||||
import { formatAmount } from "@/utils/formatAmount";
|
import { formatAmount } from "@/utils/formatAmount";
|
||||||
import {
|
import {
|
||||||
@@ -119,7 +118,6 @@ export default function OrderEditPage() {
|
|||||||
|
|
||||||
const [form, setForm] = useState<EditFormData | null>(null);
|
const [form, setForm] = useState<EditFormData | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
|
|
||||||
// 데이터 로드 (API)
|
// 데이터 로드 (API)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -177,28 +175,25 @@ export default function OrderEditPage() {
|
|||||||
router.push(`/sales/order-management-sales/${orderId}`);
|
router.push(`/sales/order-management-sales/${orderId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
// onSubmit wrapper for IntegratedDetailTemplate
|
||||||
if (!form) return;
|
const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
||||||
|
if (!form) return { success: false, error: "폼 데이터가 없습니다." };
|
||||||
|
|
||||||
// 유효성 검사
|
// 유효성 검사
|
||||||
if (!form.deliveryRequestDate) {
|
if (!form.deliveryRequestDate) {
|
||||||
toast.error("납품요청일을 입력해주세요.");
|
toast.error("납품요청일을 입력해주세요.");
|
||||||
return;
|
return { success: false, error: "납품요청일을 입력해주세요." };
|
||||||
}
|
}
|
||||||
if (!form.receiver.trim()) {
|
if (!form.receiver.trim()) {
|
||||||
toast.error("수신(반장/업체)을 입력해주세요.");
|
toast.error("수신(반장/업체)을 입력해주세요.");
|
||||||
return;
|
return { success: false, error: "수신(반장/업체)을 입력해주세요." };
|
||||||
}
|
}
|
||||||
if (!form.receiverContact.trim()) {
|
if (!form.receiverContact.trim()) {
|
||||||
toast.error("수신처 연락처를 입력해주세요.");
|
toast.error("수신처 연락처를 입력해주세요.");
|
||||||
return;
|
return { success: false, error: "수신처 연락처를 입력해주세요." };
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSaving(true);
|
|
||||||
try {
|
try {
|
||||||
// API 연동
|
|
||||||
// 주의: clientId를 보내지 않으면 기존 값 유지됨
|
|
||||||
// clientName과 clientContact는 반드시 보내야 기존 값이 유지됨
|
|
||||||
const result = await updateOrder(orderId, {
|
const result = await updateOrder(orderId, {
|
||||||
clientName: form.client,
|
clientName: form.client,
|
||||||
clientContact: form.contact,
|
clientContact: form.contact,
|
||||||
@@ -226,44 +221,32 @@ export default function OrderEditPage() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success("수주가 수정되었습니다.");
|
toast.success("수주가 수정되었습니다.");
|
||||||
router.push(`/sales/order-management-sales/${orderId}`);
|
router.push(`/sales/order-management-sales/${orderId}`);
|
||||||
|
return { success: true };
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.error || "수주 수정에 실패했습니다.");
|
toast.error(result.error || "수주 수정에 실패했습니다.");
|
||||||
|
return { success: false, error: result.error };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating order:", error);
|
console.error("Error updating order:", error);
|
||||||
toast.error("수주 수정 중 오류가 발생했습니다.");
|
toast.error("수주 수정 중 오류가 발생했습니다.");
|
||||||
} finally {
|
return { success: false, error: "수주 수정 중 오류가 발생했습니다." };
|
||||||
setIsSaving(false);
|
|
||||||
}
|
}
|
||||||
};
|
}, [form, orderId, router]);
|
||||||
|
|
||||||
|
// 폼 컨텐츠 렌더링
|
||||||
|
const renderFormContent = useCallback(() => {
|
||||||
|
if (!form) return null;
|
||||||
|
|
||||||
if (loading || !form) {
|
|
||||||
return (
|
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* 상태 뱃지 */}
|
||||||
|
<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>
|
||||||
|
|
||||||
{/* 기본 정보 (읽기전용) */}
|
{/* 기본 정보 (읽기전용) */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -545,18 +528,26 @@ export default function OrderEditPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}, [form]);
|
||||||
|
|
||||||
{/* 하단 버튼 */}
|
// Edit mode config override
|
||||||
<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">
|
const editConfig = {
|
||||||
<FormActions
|
...orderSalesConfig,
|
||||||
onSave={handleSave}
|
title: '수주 수정',
|
||||||
onCancel={handleCancel}
|
};
|
||||||
saveLabel="저장"
|
|
||||||
cancelLabel="취소"
|
return (
|
||||||
saveLoading={isSaving}
|
<IntegratedDetailTemplate
|
||||||
saveDisabled={isSaving}
|
config={editConfig}
|
||||||
/>
|
mode="edit"
|
||||||
</div>
|
initialData={form}
|
||||||
</PageLayout>
|
itemId={orderId}
|
||||||
|
isLoading={loading || !form}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onBack={handleCancel}
|
||||||
|
renderForm={renderFormContent}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
* - 상태별 버튼 차이
|
* - 상태별 버튼 차이
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useRouter, useParams } from "next/navigation";
|
import { useRouter, useParams } from "next/navigation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
@@ -22,8 +22,6 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import {
|
import {
|
||||||
FileText,
|
|
||||||
ArrowLeft,
|
|
||||||
Edit,
|
Edit,
|
||||||
Factory,
|
Factory,
|
||||||
XCircle,
|
XCircle,
|
||||||
@@ -39,8 +37,8 @@ import {
|
|||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { PageLayout } from "@/components/organisms/PageLayout";
|
import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate";
|
||||||
import { PageHeader } from "@/components/organisms/PageHeader";
|
import { orderSalesConfig } from "@/components/orders/orderSalesConfig";
|
||||||
import { BadgeSm } from "@/components/atoms/BadgeSm";
|
import { BadgeSm } from "@/components/atoms/BadgeSm";
|
||||||
import { formatAmount } from "@/utils/formatAmount";
|
import { formatAmount } from "@/utils/formatAmount";
|
||||||
import {
|
import {
|
||||||
@@ -339,105 +337,11 @@ export default function OrderDetailPage() {
|
|||||||
return order.items.filter((item) => !matchedIds.has(item.id));
|
return order.items.filter((item) => !matchedIds.has(item.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
// 상세 컨텐츠 렌더링
|
||||||
|
const renderViewContent = useCallback(() => {
|
||||||
|
if (!order) return null;
|
||||||
|
|
||||||
return (
|
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!order) {
|
|
||||||
return (
|
|
||||||
<PageLayout>
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<p className="text-muted-foreground">수주 정보를 찾을 수 없습니다.</p>
|
|
||||||
<Button variant="outline" onClick={handleBack} className="mt-4">
|
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
||||||
목록으로
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</PageLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 상태별 버튼 표시 여부
|
|
||||||
const showEditButton = order.status !== "shipped" && order.status !== "cancelled";
|
|
||||||
// 수주 확정 버튼: 수주등록 상태에서만 표시
|
|
||||||
const showConfirmButton = order.status === "order_registered";
|
|
||||||
// 생산지시 생성 버튼: 수주확정 상태에서만 표시
|
|
||||||
const showProductionCreateButton = order.status === "order_confirmed";
|
|
||||||
// 생산지시 보기 버튼: 생산지시완료 상태에서 숨김 (기획서 오류로 제거)
|
|
||||||
const showProductionViewButton = false;
|
|
||||||
// 생산지시 되돌리기 버튼: 생산지시완료 상태에서만 표시
|
|
||||||
const showRevertButton = order.status === "production_ordered";
|
|
||||||
// 수주확정 되돌리기 버튼: 수주확정 상태에서만 표시
|
|
||||||
const showRevertConfirmButton = order.status === "order_confirmed";
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
{showRevertButton && (
|
|
||||||
<Button variant="outline" onClick={handleRevertProduction} className="border-amber-200 text-amber-600 hover:border-amber-300 hover:bg-amber-50">
|
|
||||||
<RotateCcw className="h-4 w-4 mr-2" />
|
|
||||||
생산지시 되돌리기
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{showRevertConfirmButton && (
|
|
||||||
<Button variant="outline" onClick={handleRevertConfirmation} className="border-slate-300 text-slate-600 hover:border-slate-400 hover:bg-slate-50">
|
|
||||||
<RotateCcw 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">
|
<div className="space-y-6">
|
||||||
{/* 수주 정보 헤더 */}
|
{/* 수주 정보 헤더 */}
|
||||||
<Card>
|
<Card>
|
||||||
@@ -790,6 +694,85 @@ export default function OrderDetailPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}, [order, expandedProducts, expandAllProducts, collapseAllProducts, toggleProduct, getItemsForProduct, getUnmatchedItems, openDocumentModal]);
|
||||||
|
|
||||||
|
// 커스텀 헤더 액션 (상태별 버튼)
|
||||||
|
const renderHeaderActions = useCallback(() => {
|
||||||
|
if (!order) return null;
|
||||||
|
|
||||||
|
// 상태별 버튼 표시 여부
|
||||||
|
const showEditButton = order.status !== "shipped" && order.status !== "cancelled";
|
||||||
|
const showConfirmButton = order.status === "order_registered";
|
||||||
|
const showProductionCreateButton = order.status === "order_confirmed";
|
||||||
|
const showProductionViewButton = false;
|
||||||
|
const showRevertButton = order.status === "production_ordered";
|
||||||
|
const showRevertConfirmButton = order.status === "order_confirmed";
|
||||||
|
const showCancelButton =
|
||||||
|
order.status !== "shipped" &&
|
||||||
|
order.status !== "cancelled" &&
|
||||||
|
order.status !== "production_ordered";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
{showRevertButton && (
|
||||||
|
<Button variant="outline" onClick={handleRevertProduction} className="border-amber-200 text-amber-600 hover:border-amber-300 hover:bg-amber-50">
|
||||||
|
<RotateCcw className="h-4 w-4 mr-2" />
|
||||||
|
생산지시 되돌리기
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showRevertConfirmButton && (
|
||||||
|
<Button variant="outline" onClick={handleRevertConfirmation} className="border-slate-300 text-slate-600 hover:border-slate-400 hover:bg-slate-50">
|
||||||
|
<RotateCcw 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>
|
||||||
|
);
|
||||||
|
}, [order, handleEdit, handleConfirmOrder, handleProductionOrder, handleViewProductionOrder, handleRevertProduction, handleRevertConfirmation, handleCancel]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IntegratedDetailTemplate
|
||||||
|
config={orderSalesConfig}
|
||||||
|
mode="view"
|
||||||
|
initialData={order}
|
||||||
|
itemId={orderId}
|
||||||
|
isLoading={loading}
|
||||||
|
onBack={handleBack}
|
||||||
|
renderView={renderViewContent}
|
||||||
|
headerActions={renderHeaderActions()}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 문서 모달 */}
|
{/* 문서 모달 */}
|
||||||
<OrderDocumentModal
|
<OrderDocumentModal
|
||||||
@@ -1114,6 +1097,6 @@ export default function OrderDetailPage() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</PageLayout>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { FileText, Loader2, List } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { getExpenseItemOptions, updateEstimate, type ExpenseItemOption } from './actions';
|
import { getExpenseItemOptions, updateEstimate, type ExpenseItemOption } from './actions';
|
||||||
import { createBiddingFromEstimate } from '../bidding/actions';
|
import { createBiddingFromEstimate } from '../bidding/actions';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
@@ -17,8 +17,8 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
import { estimateConfig } from './estimateConfig';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import type {
|
import type {
|
||||||
EstimateDetail,
|
EstimateDetail,
|
||||||
@@ -106,9 +106,6 @@ export default function EstimateDetailForm({
|
|||||||
controller: number;
|
controller: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
// 조정단가 적용 여부 (전체 적용 버튼 클릭 시에만 true)
|
|
||||||
const useAdjustedPrice = appliedPrices !== null;
|
|
||||||
|
|
||||||
// ===== 네비게이션 핸들러 =====
|
// ===== 네비게이션 핸들러 =====
|
||||||
const handleBack = useCallback(() => {
|
const handleBack = useCallback(() => {
|
||||||
router.push('/ko/construction/project/bidding/estimates');
|
router.push('/ko/construction/project/bidding/estimates');
|
||||||
@@ -118,10 +115,6 @@ export default function EstimateDetailForm({
|
|||||||
router.push(`/ko/construction/project/bidding/estimates/${estimateId}/edit`);
|
router.push(`/ko/construction/project/bidding/estimates/${estimateId}/edit`);
|
||||||
}, [router, estimateId]);
|
}, [router, estimateId]);
|
||||||
|
|
||||||
const handleCancel = useCallback(() => {
|
|
||||||
router.push(`/ko/construction/project/bidding/estimates/${estimateId}`);
|
|
||||||
}, [router, estimateId]);
|
|
||||||
|
|
||||||
// ===== 저장/삭제 핸들러 =====
|
// ===== 저장/삭제 핸들러 =====
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
setShowSaveDialog(true);
|
setShowSaveDialog(true);
|
||||||
@@ -622,17 +615,8 @@ export default function EstimateDetailForm({
|
|||||||
[isViewMode]
|
[isViewMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ===== 타이틀 및 설명 =====
|
|
||||||
const pageTitle = useMemo(() => {
|
|
||||||
return isEditMode ? '견적 수정' : '견적 상세';
|
|
||||||
}, [isEditMode]);
|
|
||||||
|
|
||||||
const pageDescription = useMemo(() => {
|
|
||||||
return isEditMode ? '견적 정보를 수정합니다' : '견적 정보를 등록하고 관리합니다';
|
|
||||||
}, [isEditMode]);
|
|
||||||
|
|
||||||
// ===== 헤더 버튼 =====
|
// ===== 헤더 버튼 =====
|
||||||
const headerActions = useMemo(() => {
|
const renderHeaderActions = useCallback(() => {
|
||||||
if (isViewMode) {
|
if (isViewMode) {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -645,18 +629,11 @@ export default function EstimateDetailForm({
|
|||||||
<Button variant="outline" onClick={handleRegisterBidding} className="text-green-600 border-green-200 hover:bg-green-50">
|
<Button variant="outline" onClick={handleRegisterBidding} className="text-green-600 border-green-200 hover:bg-green-50">
|
||||||
입찰 등록
|
입찰 등록
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleEdit} className="bg-blue-500 hover:bg-blue-600">
|
|
||||||
수정
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button variant="outline" onClick={handleBack}>
|
|
||||||
<List className="h-4 w-4 mr-2" />
|
|
||||||
목록
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-red-500 border-red-200 hover:bg-red-50"
|
className="text-red-500 border-red-200 hover:bg-red-50"
|
||||||
@@ -670,18 +647,11 @@ export default function EstimateDetailForm({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, [isViewMode, isLoading, handleBack, handleEdit, handleDelete, handleSave, handleRegisterBidding]);
|
}, [isViewMode, isLoading, handleDelete, handleSave, handleRegisterBidding]);
|
||||||
|
|
||||||
return (
|
|
||||||
<PageLayout>
|
|
||||||
<PageHeader
|
|
||||||
title={pageTitle}
|
|
||||||
description={pageDescription}
|
|
||||||
icon={FileText}
|
|
||||||
actions={headerActions}
|
|
||||||
onBack={handleBack}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
// ===== 컨텐츠 렌더링 =====
|
||||||
|
const renderContent = useCallback(() => {
|
||||||
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* 견적 정보 + 현장설명회 + 입찰 정보 */}
|
{/* 견적 정보 + 현장설명회 + 입찰 정보 */}
|
||||||
<EstimateInfoSection
|
<EstimateInfoSection
|
||||||
@@ -746,6 +716,69 @@ export default function EstimateDetailForm({
|
|||||||
onReset={handleDetailReset}
|
onReset={handleDetailReset}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
formData,
|
||||||
|
isViewMode,
|
||||||
|
isDragging,
|
||||||
|
documentInputRef,
|
||||||
|
expenseOptions,
|
||||||
|
appliedPrices,
|
||||||
|
handleBidInfoChange,
|
||||||
|
handleDocumentUpload,
|
||||||
|
handleDocumentRemove,
|
||||||
|
handleDragOver,
|
||||||
|
handleDragLeave,
|
||||||
|
handleDrop,
|
||||||
|
handleAddSummaryItem,
|
||||||
|
handleRemoveSummaryItem,
|
||||||
|
handleSummaryItemChange,
|
||||||
|
handleSummaryMemoChange,
|
||||||
|
handleAddExpenseItems,
|
||||||
|
handleRemoveSelectedExpenseItems,
|
||||||
|
handleExpenseItemChange,
|
||||||
|
handleExpenseSelectItem,
|
||||||
|
handleExpenseSelectAll,
|
||||||
|
handlePriceAdjustmentChange,
|
||||||
|
handlePriceAdjustmentSave,
|
||||||
|
handlePriceAdjustmentApplyAll,
|
||||||
|
handlePriceAdjustmentReset,
|
||||||
|
handleAddDetailItems,
|
||||||
|
handleRemoveDetailItem,
|
||||||
|
handleRemoveSelectedDetailItems,
|
||||||
|
handleDetailItemChange,
|
||||||
|
handleDetailSelectItem,
|
||||||
|
handleDetailSelectAll,
|
||||||
|
handleApplyAdjustedPriceToSelected,
|
||||||
|
handleDetailReset,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Edit 모드용 config (타이틀 변경)
|
||||||
|
const currentConfig = useMemo(() => {
|
||||||
|
if (isEditMode) {
|
||||||
|
return {
|
||||||
|
...estimateConfig,
|
||||||
|
title: '견적 수정',
|
||||||
|
description: '견적 정보를 수정합니다',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return estimateConfig;
|
||||||
|
}, [isEditMode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IntegratedDetailTemplate
|
||||||
|
config={currentConfig}
|
||||||
|
mode={mode}
|
||||||
|
initialData={formData}
|
||||||
|
itemId={estimateId}
|
||||||
|
isLoading={false}
|
||||||
|
onBack={handleBack}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
renderView={renderContent}
|
||||||
|
renderForm={renderContent}
|
||||||
|
headerActions={renderHeaderActions()}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 전자결재 모달 */}
|
{/* 전자결재 모달 */}
|
||||||
<ElectronicApprovalModal
|
<ElectronicApprovalModal
|
||||||
@@ -839,6 +872,6 @@ export default function EstimateDetailForm({
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</PageLayout>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user