+
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() {
@@ -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) {
@@ -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 && (