diff --git a/src/app/[locale]/(protected)/quality/qms/page.tsx b/src/app/[locale]/(protected)/quality/qms/page.tsx index 7cafb88f..1658882f 100644 --- a/src/app/[locale]/(protected)/quality/qms/page.tsx +++ b/src/app/[locale]/(protected)/quality/qms/page.tsx @@ -229,7 +229,7 @@ export default function QualityInspectionPage() { }, []); return ( -
+
{/* 헤더 (설정 버튼 포함) */}
setSettingsOpen(true)} />} @@ -283,9 +283,9 @@ export default function QualityInspectionPage() { {activeDay === 1 ? ( // ===== 기준/매뉴얼 심사 심사 ===== -
+
{/* 좌측: 점검표 항목 */} -
@@ -327,8 +327,8 @@ export default function QualityInspectionPage() {
) : ( // ===== 로트 추적 심사 심사 ===== -
-
+
+
-
+
-
+
void; - onSelectSupplier: (supplier: { name: string; code?: string }) => void; + onSelectSupplier: (supplier: { id: number | string; name: string; code?: string }) => void; } // ============================================================================= @@ -115,7 +115,7 @@ export function SupplierSearchModal({ }, []); const handleSelect = useCallback((supplier: SupplierItem) => { - onSelectSupplier({ name: supplier.name, code: supplier.clientCode }); + onSelectSupplier({ id: supplier.id, name: supplier.name, code: supplier.clientCode }); }, [onSelectSupplier]); return ( diff --git a/src/components/material/StockStatus/StockStatusDetail.tsx b/src/components/material/StockStatus/StockStatusDetail.tsx index 5781d638..c0b71a76 100644 --- a/src/components/material/StockStatus/StockStatusDetail.tsx +++ b/src/components/material/StockStatus/StockStatusDetail.tsx @@ -9,6 +9,7 @@ */ import { useState, useCallback, useEffect } from 'react'; +import { invalidateDashboard } from '@/lib/dashboard-invalidation'; import { useRouter, useSearchParams } from 'next/navigation'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; @@ -133,6 +134,7 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) { const result = await updateStock(id, formData); if (result.success) { + invalidateDashboard('stock'); toast.success('재고 정보가 저장되었습니다.'); // 상세 데이터 업데이트 setDetail((prev) => diff --git a/src/components/orders/OrderRegistration.tsx b/src/components/orders/OrderRegistration.tsx index ecca6539..f1425c01 100644 --- a/src/components/orders/OrderRegistration.tsx +++ b/src/components/orders/OrderRegistration.tsx @@ -12,6 +12,7 @@ */ import { useState, useEffect, useCallback, useMemo } from "react"; +import { invalidateDashboard } from '@/lib/dashboard-invalidation'; import { useDaumPostcode } from "@/hooks/useDaumPostcode"; import { useClientList } from "@/hooks/useClientList"; import { Input } from "@/components/ui/input"; @@ -197,7 +198,7 @@ export function OrderRegistration({ // 컴포넌트 마운트 시 거래처 목록 불러오기 useEffect(() => { - fetchClients({ onlyActive: true, size: 100 }); + fetchClients({ onlyActive: true, size: 1000 }); }, [fetchClients]); // Daum 우편번호 서비스 @@ -504,6 +505,7 @@ export function OrderRegistration({ setIsSaving(true); try { await onSave(form); + invalidateDashboard('order'); return { success: true }; } catch (e) { const errorMsg = e instanceof Error ? e.message : '저장 중 오류가 발생했습니다.'; diff --git a/src/components/orders/OrderSalesDetailView.tsx b/src/components/orders/OrderSalesDetailView.tsx index 78ad5946..2eda838d 100644 --- a/src/components/orders/OrderSalesDetailView.tsx +++ b/src/components/orders/OrderSalesDetailView.tsx @@ -65,6 +65,7 @@ import { type OrderStatus, } from "@/components/orders"; import { ServerErrorPage } from "@/components/common/ServerErrorPage"; +import { invalidateDashboard } from "@/lib/dashboard-invalidation"; // 상태 뱃지 헬퍼 @@ -293,6 +294,7 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) { try { const result = await updateOrderStatus(order.id, "cancelled"); if (result.success) { + invalidateDashboard('sales'); setOrder({ ...order, status: "cancelled" }); toast.success("수주가 취소되었습니다."); setIsCancelDialogOpen(false); @@ -321,6 +323,7 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) { try { const result = await updateOrderStatus(order.id, "order_confirmed"); if (result.success && result.data) { + invalidateDashboard('sales'); setOrder(result.data); toast.success("수주가 확정되었습니다."); setIsConfirmDialogOpen(false); diff --git a/src/components/outbound/ShipmentManagement/ShipmentEdit.tsx b/src/components/outbound/ShipmentManagement/ShipmentEdit.tsx index dadba78b..a6ce622a 100644 --- a/src/components/outbound/ShipmentManagement/ShipmentEdit.tsx +++ b/src/components/outbound/ShipmentManagement/ShipmentEdit.tsx @@ -6,6 +6,7 @@ */ import { useState, useCallback, useEffect } from 'react'; +import { invalidateDashboard } from '@/lib/dashboard-invalidation'; import { useRouter } from 'next/navigation'; import { Plus, Trash2, ChevronDown, Search } from 'lucide-react'; import { Input } from '@/components/ui/input'; @@ -305,6 +306,7 @@ export function ShipmentEdit({ id }: ShipmentEditProps) { if (!result.success) { return { success: false, error: result.error || '출고 수정에 실패했습니다.' }; } + invalidateDashboard('shipment'); return { success: true }; } catch (err) { if (isNextRedirectError(err)) throw err; diff --git a/src/components/production/WorkOrders/WorkOrderCreate.tsx b/src/components/production/WorkOrders/WorkOrderCreate.tsx index 512ac0ab..13904ba4 100644 --- a/src/components/production/WorkOrders/WorkOrderCreate.tsx +++ b/src/components/production/WorkOrders/WorkOrderCreate.tsx @@ -7,6 +7,7 @@ */ import { useState, useEffect, useCallback } from 'react'; +import { invalidateDashboard } from '@/lib/dashboard-invalidation'; import { useRouter } from 'next/navigation'; import { FileText, X, Edit, Loader2, Plus, Search, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -291,6 +292,7 @@ export function WorkOrderCreate() { if (!result.success) { return { success: false, error: result.error || '작업지시 등록에 실패했습니다.' }; } + invalidateDashboard('production'); return { success: true }; } catch (error) { if (isNextRedirectError(error)) throw error; diff --git a/src/components/production/WorkOrders/WorkOrderDetail.tsx b/src/components/production/WorkOrders/WorkOrderDetail.tsx index d27d2457..f8e386b3 100644 --- a/src/components/production/WorkOrders/WorkOrderDetail.tsx +++ b/src/components/production/WorkOrders/WorkOrderDetail.tsx @@ -7,6 +7,7 @@ */ import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { invalidateDashboard } from '@/lib/dashboard-invalidation'; import { useRouter } from 'next/navigation'; import { FileText, Play, CheckCircle2, Loader2, Undo2, ClipboardCheck } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -272,6 +273,7 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) { try { const result = await updateWorkOrderStatus(orderId, newStatus); if (result.success && result.data) { + invalidateDashboard('production'); setOrder(result.data); const statusLabels = { waiting: '작업대기', diff --git a/src/components/production/WorkOrders/WorkOrderEdit.tsx b/src/components/production/WorkOrders/WorkOrderEdit.tsx index 1eaabe58..cf9d3b80 100644 --- a/src/components/production/WorkOrders/WorkOrderEdit.tsx +++ b/src/components/production/WorkOrders/WorkOrderEdit.tsx @@ -8,6 +8,7 @@ */ import { useState, useEffect, useCallback } from 'react'; +import { invalidateDashboard } from '@/lib/dashboard-invalidation'; import { useRouter } from 'next/navigation'; import { SquarePen, Trash2 } from 'lucide-react'; import { Input } from '@/components/ui/input'; @@ -239,6 +240,7 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) { }); if (result.success) { + invalidateDashboard('production'); toast.success('작업지시가 수정되었습니다.'); router.push(`/production/work-orders/${orderId}?mode=view`); return { success: true }; diff --git a/src/components/quality/InspectionManagement/InspectionCreate.tsx b/src/components/quality/InspectionManagement/InspectionCreate.tsx index 83e34b88..a13cd301 100644 --- a/src/components/quality/InspectionManagement/InspectionCreate.tsx +++ b/src/components/quality/InspectionManagement/InspectionCreate.tsx @@ -48,6 +48,9 @@ const OrderSelectModal = dynamic( const ProductInspectionInputModal = dynamic( () => import('./ProductInspectionInputModal').then(mod => ({ default: mod.ProductInspectionInputModal })), ); +const SupplierSearchModal = dynamic( + () => import('@/components/material/ReceivingManagement/SupplierSearchModal').then(mod => ({ default: mod.SupplierSearchModal })), +); import type { InspectionFormData, OrderSettingItem, OrderSelectItem, OrderGroup, ProductInspectionData } from './types'; import { emptyConstructionSite, @@ -77,6 +80,7 @@ export function InspectionCreate() { const [isSubmitting, setIsSubmitting] = useState(false); const [orderModalOpen, setOrderModalOpen] = useState(false); + const [clientModalOpen, setClientModalOpen] = useState(false); // 제품검사 입력 모달 const [inspectionInputOpen, setInspectionInputOpen] = useState(false); @@ -123,10 +127,15 @@ export function InspectionCreate() { changeReason: '', }] ); - setFormData((prev) => ({ - ...prev, - orderItems: [...prev.orderItems, ...newOrderItems], - })); + setFormData((prev) => { + const updated = { ...prev, orderItems: [...prev.orderItems, ...newOrderItems] }; + // 수주처 미선택 상태에서 수주 선택 시 → 수주처 자동 채움 + if (!prev.clientId && items.length > 0 && items[0].clientId) { + updated.clientId = items[0].clientId ?? undefined; + updated.client = items[0].clientName || ''; + } + return updated; + }); }, []); // ===== 수주 항목 삭제 ===== @@ -238,9 +247,9 @@ export function InspectionCreate() { toast.error('현장명은 필수 입력 항목입니다.'); return { success: false, error: '현장명을 입력해주세요.' }; } - if (!formData.client.trim()) { - toast.error('수주처는 필수 입력 항목입니다.'); - return { success: false, error: '수주처를 입력해주세요.' }; + if (!formData.clientId) { + toast.error('수주처는 필수 선택 항목입니다.'); + return { success: false, error: '수주처를 선택해주세요.' }; } setIsSubmitting(true); @@ -400,11 +409,29 @@ export function InspectionCreate() {
- updateField('client', e.target.value)} - placeholder="수주처 입력" - /> +
+ setClientModalOpen(true)} + /> + {formData.clientId && ( + + )} +
@@ -691,16 +718,28 @@ export function InspectionCreate() { [formData.orderItems] ); - // 이미 선택된 수주가 있으면 같은 거래처+모델만 필터 + // 수주 선택 필터: 기본정보 수주처 또는 이미 선택된 수주 기준 const orderFilter = useMemo(() => { - if (formData.orderItems.length === 0) return { clientId: undefined, itemId: undefined, label: undefined }; - const first = formData.orderItems[0]; - return { - clientId: first.clientId ?? undefined, - itemId: first.itemId ?? undefined, - label: [first.clientName, first.itemName].filter(Boolean).join(' / ') || undefined, - }; - }, [formData.orderItems]); + // 기본정보에서 수주처가 선택된 경우 → 해당 거래처 필터 + if (formData.clientId) { + const firstItem = formData.orderItems[0]; + return { + clientId: formData.clientId, + itemId: firstItem?.itemId ?? undefined, + label: [formData.client, firstItem?.itemName].filter(Boolean).join(' / ') || undefined, + }; + } + // 수주가 선택된 경우 → 첫 수주 기준 필터 + if (formData.orderItems.length > 0) { + const first = formData.orderItems[0]; + return { + clientId: first.clientId ?? undefined, + itemId: first.itemId ?? undefined, + label: [first.clientName, first.itemName].filter(Boolean).join(' / ') || undefined, + }; + } + return { clientId: undefined, itemId: undefined, label: undefined }; + }, [formData.clientId, formData.client, formData.orderItems]); return ( <> @@ -725,6 +764,17 @@ export function InspectionCreate() { filterLabel={orderFilter.label} /> + {/* 거래처(수주처) 검색 모달 */} + { + updateField('clientId', Number(supplier.id)); + updateField('client', supplier.name); + setClientModalOpen(false); + }} + /> + {/* 제품검사 입력 모달 */} import('./OrderSelectModal').then(mod => ({ default: mod.OrderSelectModal })), ); +const SupplierSearchModal = dynamic( + () => import('@/components/material/ReceivingManagement/SupplierSearchModal').then(mod => ({ default: mod.SupplierSearchModal })), +); import type { ProductInspection, InspectionFormData, @@ -137,6 +140,7 @@ export function InspectionDetail({ id }: InspectionDetailProps) { // 수주 선택 모달 const [orderModalOpen, setOrderModalOpen] = useState(false); + const [clientModalOpen, setClientModalOpen] = useState(false); // 문서 모달 const [requestDocOpen, setRequestDocOpen] = useState(false); @@ -213,6 +217,7 @@ export function InspectionDetail({ id }: InspectionDetailProps) { qualityDocNumber: result.data.qualityDocNumber, siteName: result.data.siteName, client: result.data.client, + clientId: result.data.clientId, manager: result.data.manager, managerContact: result.data.managerContact, constructionSite: { ...result.data.constructionSite }, @@ -374,10 +379,15 @@ export function InspectionDetail({ id }: InspectionDetailProps) { changeReason: '', }] ); - setFormData((prev) => ({ - ...prev, - orderItems: [...prev.orderItems, ...newOrderItems], - })); + setFormData((prev) => { + const updated = { ...prev, orderItems: [...prev.orderItems, ...newOrderItems] }; + // 수주처 미선택 상태에서 수주 선택 시 → 수주처 자동 채움 + if (!prev.clientId && items.length > 0 && items[0].clientId) { + updated.clientId = items[0].clientId ?? undefined; + updated.client = items[0].clientName || ''; + } + return updated; + }); }, []); const handleRemoveOrderItem = useCallback((itemId: string) => { @@ -393,17 +403,29 @@ export function InspectionDetail({ id }: InspectionDetailProps) { [formData.orderItems] ); - // 이미 선택된 수주가 있으면 같은 거래처+모델만 필터 + // 수주 선택 필터: 기본정보 수주처 또는 이미 선택된 수주 기준 const orderFilter = useMemo(() => { const items = isEditMode ? formData.orderItems : (inspection?.orderItems || []); - if (items.length === 0) return { clientId: undefined, itemId: undefined, label: undefined }; - const first = items[0]; - return { - clientId: first.clientId ?? undefined, - itemId: first.itemId ?? undefined, - label: [first.clientName, first.itemName].filter(Boolean).join(' / ') || undefined, - }; - }, [isEditMode, formData.orderItems, inspection?.orderItems]); + // 기본정보에서 수주처가 선택된 경우 → 해당 거래처 필터 + if (formData.clientId) { + const firstItem = items[0]; + return { + clientId: formData.clientId, + itemId: firstItem?.itemId ?? undefined, + label: [formData.client, firstItem?.itemName].filter(Boolean).join(' / ') || undefined, + }; + } + // 수주가 선택된 경우 → 첫 수주 기준 필터 + if (items.length > 0) { + const first = items[0]; + return { + clientId: first.clientId ?? undefined, + itemId: first.itemId ?? undefined, + label: [first.clientName, first.itemName].filter(Boolean).join(' / ') || undefined, + }; + } + return { clientId: undefined, itemId: undefined, label: undefined }; + }, [isEditMode, formData.clientId, formData.client, formData.orderItems, inspection?.orderItems]); // ===== 수주 설정 요약 ===== const orderSummary = useMemo(() => { @@ -976,10 +998,28 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
- updateField('client', e.target.value)} - /> +
+ setClientModalOpen(true)} + /> + {formData.clientId && ( + + )} +
@@ -1334,6 +1374,17 @@ export function InspectionDetail({ id }: InspectionDetailProps) { filterLabel={orderFilter.label} /> + {/* 거래처(수주처) 검색 모달 */} + { + updateField('clientId', Number(supplier.id)); + updateField('client', supplier.name); + setClientModalOpen(false); + }} + /> + {/* 제품검사요청서 모달 */} ({ - id: Number(item.id), - post_width: item.constructionWidth || null, - post_height: item.constructionHeight || null, - change_reason: item.changeReason || null, - inspection_data: item.inspectionData || null, - })); + // 새로 추가된 항목(id가 "orderId-nodeId" 형태)은 order_ids 동기화로 생성되므로 제외 + apiData.locations = data.orderItems + .filter((item) => !String(item.id).includes('-') && !isNaN(Number(item.id))) + .map((item) => ({ + id: Number(item.id), + post_width: item.constructionWidth || null, + post_height: item.constructionHeight || null, + change_reason: item.changeReason || null, + inspection_data: item.inspectionData || null, + })); } const result = await executeServerAction({ diff --git a/src/components/quality/InspectionManagement/types.ts b/src/components/quality/InspectionManagement/types.ts index 3c69238c..200437e8 100644 --- a/src/components/quality/InspectionManagement/types.ts +++ b/src/components/quality/InspectionManagement/types.ts @@ -152,6 +152,7 @@ export interface ProductInspection { qualityDocNumber: string; // 품질관리서 번호 siteName: string; // 현장명 client: string; // 수주처 + clientId?: number; // 수주처 ID locationCount: number; // 개소 requiredInfo: string; // 필수정보 (완료 / N건 누락) inspectionPeriod: string; // 검사기간 (2026-01-01 또는 2026-01-01~2026-01-02) diff --git a/src/components/templates/IntegratedDetailTemplate/components/DetailActions.tsx b/src/components/templates/IntegratedDetailTemplate/components/DetailActions.tsx index b6617ba9..c92d4962 100644 --- a/src/components/templates/IntegratedDetailTemplate/components/DetailActions.tsx +++ b/src/components/templates/IntegratedDetailTemplate/components/DetailActions.tsx @@ -5,7 +5,7 @@ * - 왼쪽: 목록으로/취소 (뒤로가기 성격) * - 오른쪽: [추가액션] 삭제 | 수정/저장/등록 (액션 성격) * - * View 모드: 목록으로 | [추가액션] 삭제 | 수정 + * View 모드: 목록으로 | [추가액션] 수정 * Edit 모드: 취소 | [추가액션] 삭제 | 저장 * Create 모드: 취소 | [추가액션] 등록 */ @@ -156,8 +156,8 @@ export function DetailActions({ ); })} - {/* 삭제 버튼: view, edit 모드에서 표시 (create는 삭제할 대상 없음) */} - {!isCreateMode && canDelete && showDelete && onDelete && ( + {/* 삭제 버튼: edit 모드에서만 표시 (view는 읽기 전용, create는 삭제 대상 없음) */} + {isEditMode && canDelete && showDelete && onDelete && (