2026-01-20 09:00:27 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 수주 수정 컴포넌트 (Edit Mode)
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
* IntegratedDetailTemplate 마이그레이션 완료 (2026-01-20)
|
2026-01-20 09:00:27 +09:00
|
|
|
|
*
|
|
|
|
|
|
* - 기본 정보 (읽기전용)
|
|
|
|
|
|
* - 수주/배송 정보 (편집 가능)
|
|
|
|
|
|
* - 비고 (편집 가능)
|
|
|
|
|
|
* - 품목 내역 (생산 시작 후 수정 불가)
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
import { useState, useEffect, useMemo, useCallback } from "react";
|
2026-01-20 09:00:27 +09:00
|
|
|
|
import { useRouter } from "next/navigation";
|
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
|
import { Textarea } from "@/components/ui/textarea";
|
2026-01-21 20:56:17 +09:00
|
|
|
|
import { PhoneInput } from "@/components/ui/phone-input";
|
2026-01-20 09:00:27 +09:00
|
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Select,
|
|
|
|
|
|
SelectContent,
|
|
|
|
|
|
SelectItem,
|
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
|
SelectValue,
|
|
|
|
|
|
} from "@/components/ui/select";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Table,
|
|
|
|
|
|
TableBody,
|
|
|
|
|
|
TableCell,
|
|
|
|
|
|
TableHead,
|
|
|
|
|
|
TableHeader,
|
|
|
|
|
|
TableRow,
|
|
|
|
|
|
} from "@/components/ui/table";
|
2026-01-28 21:15:25 +09:00
|
|
|
|
import { AlertTriangle, ChevronDown, ChevronRight, ChevronsUpDown, Package } from "lucide-react";
|
2026-01-20 09:00:27 +09:00
|
|
|
|
import { toast } from "sonner";
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate";
|
|
|
|
|
|
import { orderSalesConfig } from "./orderSalesConfig";
|
2026-01-20 09:00:27 +09:00
|
|
|
|
import { BadgeSm } from "@/components/atoms/BadgeSm";
|
|
|
|
|
|
import { formatAmount } from "@/utils/formatAmount";
|
|
|
|
|
|
import {
|
|
|
|
|
|
OrderItem,
|
|
|
|
|
|
getOrderById,
|
|
|
|
|
|
updateOrder,
|
|
|
|
|
|
type OrderStatus,
|
|
|
|
|
|
} from "@/components/orders";
|
2026-01-29 01:12:58 +09:00
|
|
|
|
import { getDeliveryMethodOptions, getCommonCodeOptions } from "@/lib/api/common-codes";
|
2026-01-20 09:00:27 +09:00
|
|
|
|
|
|
|
|
|
|
// 수정 폼 데이터
|
|
|
|
|
|
interface EditFormData {
|
|
|
|
|
|
// 읽기전용 정보
|
|
|
|
|
|
lotNumber: string;
|
2026-01-28 21:15:25 +09:00
|
|
|
|
orderDate: string; // 접수일
|
2026-01-20 09:00:27 +09:00
|
|
|
|
quoteNumber: string;
|
|
|
|
|
|
client: string;
|
|
|
|
|
|
siteName: string;
|
|
|
|
|
|
manager: string;
|
|
|
|
|
|
contact: string;
|
|
|
|
|
|
status: OrderStatus;
|
|
|
|
|
|
|
|
|
|
|
|
// 수정 가능 정보
|
|
|
|
|
|
expectedShipDate: string;
|
|
|
|
|
|
expectedShipDateUndecided: boolean;
|
|
|
|
|
|
deliveryRequestDate: string;
|
|
|
|
|
|
deliveryMethod: string;
|
|
|
|
|
|
shippingCost: string;
|
|
|
|
|
|
receiver: string;
|
|
|
|
|
|
receiverContact: string;
|
|
|
|
|
|
address: string;
|
|
|
|
|
|
addressDetail: string;
|
|
|
|
|
|
remarks: string;
|
|
|
|
|
|
|
|
|
|
|
|
// 품목 (수정 제한)
|
|
|
|
|
|
items: OrderItem[];
|
|
|
|
|
|
canEditItems: boolean;
|
|
|
|
|
|
subtotal: number;
|
|
|
|
|
|
discountRate: number;
|
|
|
|
|
|
totalAmount: number;
|
2026-01-28 21:15:25 +09:00
|
|
|
|
// 제품 정보 (아코디언용)
|
|
|
|
|
|
products: Array<{
|
|
|
|
|
|
productName: string;
|
|
|
|
|
|
productCategory?: string;
|
|
|
|
|
|
openWidth?: string;
|
|
|
|
|
|
openHeight?: string;
|
|
|
|
|
|
quantity: number;
|
|
|
|
|
|
floor?: string;
|
|
|
|
|
|
code?: string;
|
|
|
|
|
|
}>;
|
2026-01-20 09:00:27 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 01:12:58 +09:00
|
|
|
|
// 옵션 타입 정의
|
|
|
|
|
|
interface SelectOption {
|
|
|
|
|
|
value: string;
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
}
|
2026-01-20 09:00:27 +09:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 상태 뱃지 헬퍼
|
|
|
|
|
|
function getOrderStatusBadge(status: OrderStatus) {
|
|
|
|
|
|
const statusConfig: Record<OrderStatus, { label: string; className: string }> = {
|
|
|
|
|
|
order_registered: { label: "수주등록", className: "bg-gray-100 text-gray-700 border-gray-200" },
|
|
|
|
|
|
order_confirmed: { label: "수주확정", className: "bg-gray-100 text-gray-700 border-gray-200" },
|
|
|
|
|
|
production_ordered: { label: "생산지시완료", className: "bg-blue-100 text-blue-700 border-blue-200" },
|
|
|
|
|
|
in_production: { label: "생산중", className: "bg-green-100 text-green-700 border-green-200" },
|
2026-01-26 15:07:10 +09:00
|
|
|
|
produced: { label: "생산완료", className: "bg-blue-100 text-blue-700 border-blue-200" },
|
2026-01-20 09:00:27 +09:00
|
|
|
|
rework: { label: "재작업중", className: "bg-orange-100 text-orange-700 border-orange-200" },
|
|
|
|
|
|
work_completed: { label: "작업완료", className: "bg-blue-600 text-white border-blue-600" },
|
2026-01-26 15:07:10 +09:00
|
|
|
|
shipping: { label: "출하중", className: "bg-purple-100 text-purple-700 border-purple-200" },
|
2026-01-20 09:00:27 +09:00
|
|
|
|
shipped: { label: "출하완료", className: "bg-gray-500 text-white border-gray-500" },
|
2026-01-26 15:07:10 +09:00
|
|
|
|
completed: { label: "완료", className: "bg-gray-500 text-white border-gray-500" },
|
2026-01-20 09:00:27 +09:00
|
|
|
|
cancelled: { label: "취소", className: "bg-red-100 text-red-700 border-red-200" },
|
|
|
|
|
|
};
|
2026-01-26 15:07:10 +09:00
|
|
|
|
const config = statusConfig[status] || { label: status, className: "bg-gray-100 text-gray-700 border-gray-200" };
|
2026-01-20 09:00:27 +09:00
|
|
|
|
return (
|
|
|
|
|
|
<BadgeSm className={config.className}>
|
|
|
|
|
|
{config.label}
|
|
|
|
|
|
</BadgeSm>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface OrderSalesDetailEditProps {
|
|
|
|
|
|
orderId: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function OrderSalesDetailEdit({ orderId }: OrderSalesDetailEditProps) {
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
|
|
const [form, setForm] = useState<EditFormData | null>(null);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
2026-01-28 21:15:25 +09:00
|
|
|
|
const [expandedProducts, setExpandedProducts] = useState<Set<string>>(new Set());
|
|
|
|
|
|
|
2026-01-29 01:12:58 +09:00
|
|
|
|
// 공통코드 옵션
|
|
|
|
|
|
const [deliveryMethods, setDeliveryMethods] = useState<SelectOption[]>([]);
|
|
|
|
|
|
const [shippingCosts, setShippingCosts] = useState<SelectOption[]>([]);
|
|
|
|
|
|
|
2026-01-28 21:15:25 +09:00
|
|
|
|
// 제품-부품 트리 토글
|
|
|
|
|
|
const toggleProduct = (key: string) => {
|
|
|
|
|
|
setExpandedProducts((prev) => {
|
|
|
|
|
|
const next = new Set(prev);
|
|
|
|
|
|
if (next.has(key)) {
|
|
|
|
|
|
next.delete(key);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
next.add(key);
|
|
|
|
|
|
}
|
|
|
|
|
|
return next;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 모든 제품 확장
|
|
|
|
|
|
const expandAllProducts = () => {
|
|
|
|
|
|
if (form?.products) {
|
|
|
|
|
|
const allKeys = form.products.map((p) => `${p.floor || ""}-${p.code || ""}`);
|
|
|
|
|
|
allKeys.push("other-parts"); // 기타부품도 포함
|
|
|
|
|
|
setExpandedProducts(new Set(allKeys));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 모든 제품 축소
|
|
|
|
|
|
const collapseAllProducts = () => {
|
|
|
|
|
|
setExpandedProducts(new Set());
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 제품별로 부품 그룹화 (floor_code, symbol_code 매칭)
|
|
|
|
|
|
const getItemsForProduct = (floor: string | undefined, code: string | undefined) => {
|
|
|
|
|
|
if (!form?.items) return [];
|
|
|
|
|
|
return form.items.filter((item) => {
|
|
|
|
|
|
const itemFloor = item.type || "";
|
|
|
|
|
|
const itemSymbol = item.symbol || "";
|
|
|
|
|
|
const productFloor = floor || "";
|
|
|
|
|
|
const productCode = code || "";
|
|
|
|
|
|
return itemFloor === productFloor && itemSymbol === productCode;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 매칭되지 않은 부품 (orphan items)
|
|
|
|
|
|
const getUnmatchedItems = () => {
|
|
|
|
|
|
if (!form?.items || !form?.products) return form?.items || [];
|
|
|
|
|
|
const matchedIds = new Set<string>();
|
|
|
|
|
|
form.products.forEach((product) => {
|
|
|
|
|
|
const items = getItemsForProduct(product.floor, product.code);
|
|
|
|
|
|
items.forEach((item) => matchedIds.add(item.id));
|
|
|
|
|
|
});
|
|
|
|
|
|
return form.items.filter((item) => !matchedIds.has(item.id));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 수량 포맷 함수
|
|
|
|
|
|
* - EA, SET, PCS 등 개수 단위: 정수로 표시
|
|
|
|
|
|
* - M, M2, KG, L 등 측정 단위: 소수점 이하 불필요한 0 제거
|
|
|
|
|
|
*/
|
|
|
|
|
|
const formatQuantity = (quantity: number, unit?: string): string => {
|
|
|
|
|
|
const countableUnits = ["EA", "SET", "PCS", "개", "세트", "BOX", "ROLL"];
|
|
|
|
|
|
const upperUnit = (unit || "").toUpperCase();
|
|
|
|
|
|
|
|
|
|
|
|
if (countableUnits.includes(upperUnit)) {
|
|
|
|
|
|
return Math.round(quantity).toLocaleString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const rounded = Math.round(quantity * 10000) / 10000;
|
|
|
|
|
|
return rounded.toLocaleString(undefined, {
|
|
|
|
|
|
minimumFractionDigits: 0,
|
|
|
|
|
|
maximumFractionDigits: 4
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2026-01-20 09:00:27 +09:00
|
|
|
|
|
|
|
|
|
|
// 데이터 로드 (API)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
async function loadOrder() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
const result = await getOrderById(orderId);
|
|
|
|
|
|
if (result.success && result.data) {
|
|
|
|
|
|
const order = result.data;
|
|
|
|
|
|
// 상태에 따라 품목 수정 가능 여부 결정
|
|
|
|
|
|
const canEditItems = !["in_production", "rework", "work_completed", "shipped"].includes(
|
|
|
|
|
|
order.status
|
|
|
|
|
|
);
|
|
|
|
|
|
// Order 데이터를 EditFormData로 변환
|
|
|
|
|
|
setForm({
|
|
|
|
|
|
lotNumber: order.lotNumber,
|
2026-01-28 21:15:25 +09:00
|
|
|
|
orderDate: order.orderDate || "",
|
2026-01-20 09:00:27 +09:00
|
|
|
|
quoteNumber: order.quoteNumber || "",
|
|
|
|
|
|
client: order.client,
|
|
|
|
|
|
siteName: order.siteName,
|
|
|
|
|
|
manager: order.manager || "",
|
|
|
|
|
|
contact: order.contact || "",
|
|
|
|
|
|
status: order.status,
|
|
|
|
|
|
expectedShipDate: order.expectedShipDate || "",
|
|
|
|
|
|
expectedShipDateUndecided: !order.expectedShipDate,
|
|
|
|
|
|
deliveryRequestDate: order.deliveryRequestDate || "",
|
|
|
|
|
|
deliveryMethod: order.deliveryMethod || "",
|
|
|
|
|
|
shippingCost: order.shippingCost || "",
|
|
|
|
|
|
receiver: order.receiver || "",
|
|
|
|
|
|
receiverContact: order.receiverContact || "",
|
|
|
|
|
|
address: order.address || "",
|
|
|
|
|
|
addressDetail: order.addressDetail || "",
|
|
|
|
|
|
remarks: order.remarks || "",
|
|
|
|
|
|
items: order.items || [],
|
|
|
|
|
|
canEditItems,
|
|
|
|
|
|
subtotal: order.subtotal || order.amount,
|
|
|
|
|
|
discountRate: order.discountRate || 0,
|
|
|
|
|
|
totalAmount: order.amount,
|
2026-01-28 21:15:25 +09:00
|
|
|
|
products: order.products || [],
|
2026-01-20 09:00:27 +09:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
toast.error(result.error || "수주 정보를 불러오는데 실패했습니다.");
|
|
|
|
|
|
router.push("/sales/order-management-sales");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Error loading order:", error);
|
|
|
|
|
|
toast.error("수주 정보를 불러오는 중 오류가 발생했습니다.");
|
|
|
|
|
|
router.push("/sales/order-management-sales");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
loadOrder();
|
|
|
|
|
|
}, [orderId, router]);
|
|
|
|
|
|
|
2026-01-29 01:12:58 +09:00
|
|
|
|
// 공통코드 옵션 로드
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
async function loadCommonCodes() {
|
|
|
|
|
|
const [deliveryResult, shippingResult] = await Promise.all([
|
|
|
|
|
|
getDeliveryMethodOptions(),
|
|
|
|
|
|
getCommonCodeOptions('shipping_cost'),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
if (deliveryResult.success && deliveryResult.data) {
|
|
|
|
|
|
setDeliveryMethods(deliveryResult.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (shippingResult.success && shippingResult.data) {
|
|
|
|
|
|
setShippingCosts(shippingResult.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
loadCommonCodes();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-01-20 09:00:27 +09:00
|
|
|
|
const handleCancel = () => {
|
2026-01-25 12:27:43 +09:00
|
|
|
|
// V2 패턴: ?mode=view로 이동
|
|
|
|
|
|
router.push(`/sales/order-management-sales/${orderId}?mode=view`);
|
2026-01-20 09:00:27 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
// IntegratedDetailTemplate용 onSubmit 핸들러
|
|
|
|
|
|
const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
|
|
|
|
|
if (!form) return { success: false, error: "폼 데이터가 없습니다." };
|
2026-01-20 09:00:27 +09:00
|
|
|
|
|
|
|
|
|
|
// 유효성 검사
|
|
|
|
|
|
if (!form.deliveryRequestDate) {
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
return { success: false, error: "납품요청일을 입력해주세요." };
|
2026-01-20 09:00:27 +09:00
|
|
|
|
}
|
|
|
|
|
|
if (!form.receiver.trim()) {
|
2026-01-28 21:15:25 +09:00
|
|
|
|
return { success: false, error: "수신자를 입력해주세요." };
|
2026-01-20 09:00:27 +09:00
|
|
|
|
}
|
|
|
|
|
|
if (!form.receiverContact.trim()) {
|
2026-01-28 21:15:25 +09:00
|
|
|
|
return { success: false, error: "수신처를 입력해주세요." };
|
2026-01-20 09:00:27 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// API 연동
|
|
|
|
|
|
const result = await updateOrder(orderId, {
|
|
|
|
|
|
clientId: undefined, // 기존 값 유지
|
|
|
|
|
|
siteName: form.siteName,
|
|
|
|
|
|
expectedShipDate: form.expectedShipDateUndecided ? undefined : form.expectedShipDate,
|
|
|
|
|
|
deliveryRequestDate: form.deliveryRequestDate,
|
|
|
|
|
|
deliveryMethod: form.deliveryMethod,
|
|
|
|
|
|
shippingCost: form.shippingCost,
|
|
|
|
|
|
receiver: form.receiver,
|
|
|
|
|
|
receiverContact: form.receiverContact,
|
|
|
|
|
|
address: form.address,
|
|
|
|
|
|
addressDetail: form.addressDetail,
|
|
|
|
|
|
remarks: form.remarks,
|
|
|
|
|
|
items: form.items.map((item) => ({
|
|
|
|
|
|
itemId: item.id ? parseInt(item.id, 10) : undefined,
|
|
|
|
|
|
itemCode: item.itemCode,
|
|
|
|
|
|
itemName: item.itemName,
|
|
|
|
|
|
specification: item.spec,
|
|
|
|
|
|
quantity: item.quantity,
|
|
|
|
|
|
unit: item.unit,
|
|
|
|
|
|
unitPrice: item.unitPrice,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
toast.success("수주가 수정되었습니다.");
|
|
|
|
|
|
// V2 패턴: 저장 후 view 모드로 이동
|
2026-01-25 12:27:43 +09:00
|
|
|
|
router.push(`/sales/order-management-sales/${orderId}?mode=view`);
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
return { success: true };
|
2026-01-20 09:00:27 +09:00
|
|
|
|
} else {
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
return { success: false, error: result.error || "수주 수정에 실패했습니다." };
|
2026-01-20 09:00:27 +09:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("Error updating order:", error);
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
return { success: false, error: "수주 수정 중 오류가 발생했습니다." };
|
2026-01-20 09:00:27 +09:00
|
|
|
|
}
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
}, [form, orderId, router]);
|
|
|
|
|
|
|
|
|
|
|
|
// 동적 config (수정 모드용 타이틀)
|
|
|
|
|
|
const dynamicConfig = useMemo(() => {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...orderSalesConfig,
|
2026-01-26 15:07:10 +09:00
|
|
|
|
title: "수주",
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
actions: {
|
|
|
|
|
|
...orderSalesConfig.actions,
|
|
|
|
|
|
showEdit: false, // 수정 모드에서는 수정 버튼 숨김
|
|
|
|
|
|
showDelete: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 커스텀 헤더 액션 (상태 뱃지)
|
|
|
|
|
|
const customHeaderActions = useMemo(() => {
|
|
|
|
|
|
if (!form) return null;
|
2026-01-20 09:00:27 +09:00
|
|
|
|
return (
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
<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>
|
2026-01-20 09:00:27 +09:00
|
|
|
|
);
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
}, [form]);
|
2026-01-20 09:00:27 +09:00
|
|
|
|
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
// 폼 콘텐츠 렌더링
|
|
|
|
|
|
const renderFormContent = useCallback(() => {
|
|
|
|
|
|
if (!form) return null;
|
2026-01-20 09:00:27 +09:00
|
|
|
|
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
return (
|
2026-01-20 09:00:27 +09:00
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
{/* 기본 정보 (읽기전용) */}
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
|
|
|
|
기본 정보
|
|
|
|
|
|
<span className="text-sm font-normal text-muted-foreground">(읽기전용)</span>
|
|
|
|
|
|
</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
2026-01-28 21:15:25 +09:00
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
2026-01-20 09:00:27 +09:00
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-muted-foreground text-sm">로트번호</Label>
|
|
|
|
|
|
<p className="font-medium">{form.lotNumber}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-1">
|
2026-01-28 21:15:25 +09:00
|
|
|
|
<Label className="text-muted-foreground text-sm">접수일</Label>
|
|
|
|
|
|
<p className="font-medium">{form.orderDate || "-"}</p>
|
2026-01-20 09:00:27 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-1">
|
2026-01-28 21:15:25 +09:00
|
|
|
|
<Label className="text-muted-foreground text-sm">수주처</Label>
|
2026-01-20 09:00:27 +09:00
|
|
|
|
<p className="font-medium">{form.client}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-muted-foreground text-sm">현장명</Label>
|
|
|
|
|
|
<p className="font-medium">{form.siteName}</p>
|
|
|
|
|
|
</div>
|
2026-01-28 21:15:25 +09:00
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-muted-foreground text-sm">담당자</Label>
|
|
|
|
|
|
<p className="font-medium">{form.manager || "-"}</p>
|
|
|
|
|
|
</div>
|
2026-01-20 09:00:27 +09:00
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-muted-foreground text-sm">연락처</Label>
|
2026-01-28 21:15:25 +09:00
|
|
|
|
<p className="font-medium">{form.contact || "-"}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-muted-foreground text-sm">상태</Label>
|
|
|
|
|
|
<div className="mt-1">{getOrderStatusBadge(form.status)}</div>
|
2026-01-20 09:00:27 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 수주/배송 정보 (편집 가능) */}
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-lg">수주/배송 정보</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
2026-01-28 21:15:25 +09:00
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
|
|
|
|
{/* 첫 번째 줄: 수주일, 납품요청일, 출고예정일, 배송방식 */}
|
2026-01-20 09:00:27 +09:00
|
|
|
|
<div className="space-y-2">
|
2026-01-28 21:15:25 +09:00
|
|
|
|
<Label className="text-muted-foreground">수주일</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={form.orderDate || ""}
|
|
|
|
|
|
disabled
|
|
|
|
|
|
className="bg-muted"
|
|
|
|
|
|
/>
|
2026-01-20 09:00:27 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label>
|
|
|
|
|
|
납품요청일 <span className="text-red-500">*</span>
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={form.deliveryRequestDate}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setForm({ ...form, deliveryRequestDate: e.target.value })
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-28 21:15:25 +09:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label>출고예정일</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={form.expectedShipDate}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setForm({ ...form, expectedShipDate: e.target.value })
|
|
|
|
|
|
}
|
|
|
|
|
|
disabled={form.expectedShipDateUndecided}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-20 09:00:27 +09:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label>배송방식</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
key={`deliveryMethod-${form.deliveryMethod}`}
|
|
|
|
|
|
value={form.deliveryMethod}
|
|
|
|
|
|
onValueChange={(value) =>
|
|
|
|
|
|
setForm({ ...form, deliveryMethod: value })
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger>
|
2026-01-28 21:15:25 +09:00
|
|
|
|
<SelectValue placeholder="선택" />
|
2026-01-20 09:00:27 +09:00
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
2026-01-29 01:12:58 +09:00
|
|
|
|
{deliveryMethods.map((method) => (
|
2026-01-20 09:00:27 +09:00
|
|
|
|
<SelectItem key={method.value} value={method.value}>
|
|
|
|
|
|
{method.label}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-28 21:15:25 +09:00
|
|
|
|
{/* 두 번째 줄: 운임비용, 수신자, 수신처 */}
|
2026-01-20 09:00:27 +09:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label>운임비용</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
key={`shippingCost-${form.shippingCost}`}
|
|
|
|
|
|
value={form.shippingCost}
|
|
|
|
|
|
onValueChange={(value) =>
|
|
|
|
|
|
setForm({ ...form, shippingCost: value })
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger>
|
2026-01-28 21:15:25 +09:00
|
|
|
|
<SelectValue placeholder="선택" />
|
2026-01-20 09:00:27 +09:00
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
2026-01-29 01:12:58 +09:00
|
|
|
|
{shippingCosts.map((cost) => (
|
2026-01-20 09:00:27 +09:00
|
|
|
|
<SelectItem key={cost.value} value={cost.value}>
|
|
|
|
|
|
{cost.label}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label>
|
2026-01-28 21:15:25 +09:00
|
|
|
|
수신자 <span className="text-red-500">*</span>
|
2026-01-20 09:00:27 +09:00
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={form.receiver}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setForm({ ...form, receiver: e.target.value })
|
|
|
|
|
|
}
|
2026-01-28 21:15:25 +09:00
|
|
|
|
placeholder="수신자명 입력"
|
2026-01-20 09:00:27 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label>
|
2026-01-28 21:15:25 +09:00
|
|
|
|
수신처 <span className="text-red-500">*</span>
|
2026-01-20 09:00:27 +09:00
|
|
|
|
</Label>
|
2026-01-28 21:15:25 +09:00
|
|
|
|
<Input
|
2026-01-20 09:00:27 +09:00
|
|
|
|
value={form.receiverContact}
|
2026-01-28 21:15:25 +09:00
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setForm({ ...form, receiverContact: e.target.value })
|
2026-01-20 09:00:27 +09:00
|
|
|
|
}
|
2026-01-28 21:15:25 +09:00
|
|
|
|
placeholder="수신처 입력"
|
2026-01-20 09:00:27 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-28 21:15:25 +09:00
|
|
|
|
{/* 주소 - 전체 너비 */}
|
|
|
|
|
|
<div className="space-y-2 md:col-span-4">
|
|
|
|
|
|
<Label>주소</Label>
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={form.address}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setForm({ ...form, address: e.target.value })
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder="주소"
|
|
|
|
|
|
className="flex-1"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-01-20 09:00:27 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 비고 */}
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-lg">비고</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<Textarea
|
|
|
|
|
|
value={form.remarks}
|
|
|
|
|
|
onChange={(e) => setForm({ ...form, remarks: e.target.value })}
|
|
|
|
|
|
placeholder="특이사항을 입력하세요"
|
|
|
|
|
|
rows={4}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
2026-01-28 21:15:25 +09:00
|
|
|
|
{/* 제품내용 (아코디언) */}
|
2026-01-20 09:00:27 +09:00
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
2026-01-28 21:15:25 +09:00
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
|
|
|
|
제품내용
|
|
|
|
|
|
{!form.canEditItems && (
|
|
|
|
|
|
<span className="flex items-center gap-1 text-sm font-normal text-orange-600">
|
|
|
|
|
|
<AlertTriangle className="h-4 w-4" />
|
|
|
|
|
|
생산 시작 후 수정 불가
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardTitle>
|
|
|
|
|
|
{form.products && form.products.length > 0 && (
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={expandAllProducts}
|
|
|
|
|
|
className="text-xs text-gray-500 hover:text-gray-700 flex items-center gap-1"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ChevronsUpDown className="h-3 w-3" />
|
|
|
|
|
|
모두 펼치기
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={collapseAllProducts}
|
|
|
|
|
|
className="text-xs text-gray-500 hover:text-gray-700"
|
|
|
|
|
|
>
|
|
|
|
|
|
모두 접기
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-01-20 09:00:27 +09:00
|
|
|
|
)}
|
2026-01-28 21:15:25 +09:00
|
|
|
|
</div>
|
2026-01-20 09:00:27 +09:00
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
2026-01-28 21:15:25 +09:00
|
|
|
|
{form.products && form.products.length > 0 ? (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
{form.products.map((product, productIndex) => {
|
|
|
|
|
|
const productKey = `${product.floor || ""}-${product.code || ""}`;
|
|
|
|
|
|
const isExpanded = expandedProducts.has(productKey);
|
|
|
|
|
|
const productItems = getItemsForProduct(product.floor, product.code);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={productIndex}
|
|
|
|
|
|
className="border border-gray-200 rounded-lg overflow-hidden"
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 제품 헤더 (클릭하면 확장/축소) */}
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => toggleProduct(productKey)}
|
|
|
|
|
|
className="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors text-left"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
{isExpanded ? (
|
|
|
|
|
|
<ChevronDown className="h-5 w-5 text-gray-500" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ChevronRight className="h-5 w-5 text-gray-500" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Package className="h-5 w-5 text-blue-600" />
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span className="font-medium">{product.productName}</span>
|
|
|
|
|
|
{product.openWidth && product.openHeight && (
|
|
|
|
|
|
<span className="ml-2 text-sm text-gray-500">
|
|
|
|
|
|
({product.openWidth} × {product.openHeight})
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span className="text-sm text-gray-500">
|
|
|
|
|
|
{productItems.length}개 부품
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 부품 목록 (확장 시 표시) */}
|
|
|
|
|
|
{isExpanded && (
|
|
|
|
|
|
<div className="border-t">
|
|
|
|
|
|
{productItems.length > 0 ? (
|
|
|
|
|
|
<Table>
|
|
|
|
|
|
<TableHeader>
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableHead className="w-[60px] text-center">순번</TableHead>
|
|
|
|
|
|
<TableHead>품목명</TableHead>
|
|
|
|
|
|
<TableHead>규격</TableHead>
|
|
|
|
|
|
<TableHead className="text-center">수량</TableHead>
|
|
|
|
|
|
<TableHead className="text-center">단위</TableHead>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableHeader>
|
|
|
|
|
|
<TableBody>
|
|
|
|
|
|
{productItems.map((item, index) => (
|
|
|
|
|
|
<TableRow key={item.id}>
|
|
|
|
|
|
<TableCell className="text-center">{index + 1}</TableCell>
|
|
|
|
|
|
<TableCell>{item.itemName}</TableCell>
|
|
|
|
|
|
<TableCell>{item.spec || "-"}</TableCell>
|
|
|
|
|
|
<TableCell className="text-center">{formatQuantity(item.quantity, item.unit)}</TableCell>
|
|
|
|
|
|
<TableCell className="text-center">{item.unit || "-"}</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</TableBody>
|
|
|
|
|
|
</Table>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="p-4 text-center text-gray-400 text-sm">
|
|
|
|
|
|
연결된 부품이 없습니다
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2026-01-20 09:00:27 +09:00
|
|
|
|
</div>
|
2026-01-28 21:15:25 +09:00
|
|
|
|
) : null}
|
2026-01-20 09:00:27 +09:00
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
2026-01-28 21:15:25 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 기타부품 (아코디언) */}
|
|
|
|
|
|
{(() => {
|
|
|
|
|
|
const unmatchedItems = getUnmatchedItems();
|
|
|
|
|
|
if (unmatchedItems.length === 0) return null;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-lg">기타부품</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => toggleProduct("other-parts")}
|
|
|
|
|
|
className="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors text-left"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
{expandedProducts.has("other-parts") ? (
|
|
|
|
|
|
<ChevronDown className="h-5 w-5 text-gray-500" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ChevronRight className="h-5 w-5 text-gray-500" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Package className="h-5 w-5 text-gray-400" />
|
|
|
|
|
|
<span className="font-medium text-gray-600">기타부품</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span className="text-sm text-gray-500">
|
|
|
|
|
|
{unmatchedItems.length}개
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{expandedProducts.has("other-parts") && (
|
|
|
|
|
|
<div className="border-t">
|
|
|
|
|
|
<Table>
|
|
|
|
|
|
<TableHeader>
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableHead className="w-[60px] text-center">순번</TableHead>
|
|
|
|
|
|
<TableHead>품목명</TableHead>
|
|
|
|
|
|
<TableHead>규격</TableHead>
|
|
|
|
|
|
<TableHead className="text-center">수량</TableHead>
|
|
|
|
|
|
<TableHead className="text-center">단위</TableHead>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableHeader>
|
|
|
|
|
|
<TableBody>
|
|
|
|
|
|
{unmatchedItems.map((item, index) => (
|
|
|
|
|
|
<TableRow key={item.id}>
|
|
|
|
|
|
<TableCell className="text-center">{index + 1}</TableCell>
|
|
|
|
|
|
<TableCell>{item.itemName}</TableCell>
|
|
|
|
|
|
<TableCell>{item.spec || "-"}</TableCell>
|
|
|
|
|
|
<TableCell className="text-center">{formatQuantity(item.quantity, item.unit)}</TableCell>
|
|
|
|
|
|
<TableCell className="text-center">{item.unit || "-"}</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</TableBody>
|
|
|
|
|
|
</Table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
})()}
|
|
|
|
|
|
|
2026-01-20 09:00:27 +09:00
|
|
|
|
</div>
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
);
|
|
|
|
|
|
}, [form]);
|
2026-01-20 09:00:27 +09:00
|
|
|
|
|
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>
2026-01-20 15:51:02 +09:00
|
|
|
|
return (
|
|
|
|
|
|
<IntegratedDetailTemplate
|
|
|
|
|
|
config={dynamicConfig}
|
|
|
|
|
|
mode="edit"
|
|
|
|
|
|
initialData={form || {}}
|
|
|
|
|
|
itemId={orderId}
|
|
|
|
|
|
isLoading={loading}
|
|
|
|
|
|
headerActions={customHeaderActions}
|
|
|
|
|
|
onSubmit={handleSubmit}
|
|
|
|
|
|
renderView={() => renderFormContent()}
|
|
|
|
|
|
renderForm={() => renderFormContent()}
|
|
|
|
|
|
/>
|
2026-01-20 09:00:27 +09:00
|
|
|
|
);
|
|
|
|
|
|
}
|