feat: [WEB] 중간검사 프론트엔드 저장 연동 (Phase 2)

- WorkerScreen/actions.ts에 saveItemInspection, getWorkOrderInspectionData 서버 액션 추가
- handleInspectionComplete에서 POST /items/{itemId}/inspection API 호출 연동
- 작업지시 선택 시 GET /inspection-data로 기존 검사 데이터 자동 로드
- InspectionInputModal에 initialData prop 추가 (재클릭 시 저장된 값 표시)
- WorkItemData에 apiItemId, workOrderId 필드 추가 (실제 DB ID 보존)
- 기존 saveInspectionData deprecated 처리
This commit is contained in:
2026-02-09 10:33:02 +09:00
parent 61bf95b58e
commit 7bd1269aad
5 changed files with 173 additions and 12 deletions

View File

@@ -625,7 +625,8 @@ export async function updateWorkOrderItemStatus(
}
}
// ===== 중간검사 데이터 저장 =====
// ===== 중간검사 데이터 저장 (deprecated: WorkerScreen/actions.ts의 saveItemInspection 사용) =====
/** @deprecated WorkerScreen/actions.ts의 saveItemInspection을 사용하세요 */
export async function saveInspectionData(
workOrderId: string,
processType: string,

View File

@@ -61,6 +61,7 @@ interface InspectionInputModalProps {
processType: InspectionProcessType;
productName?: string;
specification?: string;
initialData?: InspectionData;
onComplete: (data: InspectionData) => void;
}
@@ -192,6 +193,7 @@ export function InspectionInputModal({
processType,
productName = '',
specification = '',
initialData,
onComplete,
}: InspectionInputModalProps) {
const [formData, setFormData] = useState<InspectionData>({
@@ -208,6 +210,21 @@ export function InspectionInputModal({
useEffect(() => {
if (open) {
// initialData가 있으면 기존 저장 데이터로 복원
if (initialData) {
setFormData({
...initialData,
productName: initialData.productName || productName,
specification: initialData.specification || specification,
});
if (initialData.gapPoints) {
setGapPoints(initialData.gapPoints);
} else {
setGapPoints(Array(5).fill(null).map(() => ({ left: null, right: null })));
}
return;
}
// 공정별 기본값 설정 - 모두 양호/OK/적합 상태로 초기화
const baseData: InspectionData = {
productName,
@@ -259,7 +276,7 @@ export function InspectionInputModal({
setGapPoints(Array(5).fill(null).map(() => ({ left: null, right: null })));
}
}, [open, productName, specification, processType]);
}, [open, productName, specification, processType, initialData]);
const handleComplete = () => {
const data: InspectionData = {

View File

@@ -827,4 +827,87 @@ export async function getWorkOrderDetail(
console.error('[WorkerScreenActions] getWorkOrderDetail error:', error);
return { success: false, data: [], error: '서버 오류' };
}
}
// ===== 개소별 중간검사 데이터 저장 =====
export async function saveItemInspection(
workOrderId: string,
itemId: number,
processType: string,
inspectionData: Record<string, unknown>
): Promise<{ success: boolean; data?: Record<string, unknown>; error?: string }> {
try {
console.log('[WorkerScreenActions] POST item inspection:', { workOrderId, itemId, processType });
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/inspection`,
{
method: 'POST',
body: JSON.stringify({
process_type: processType,
inspection_data: inspectionData,
}),
}
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkerScreenActions] POST item inspection response:', result);
if (!response.ok || !result.success) {
return { success: false, error: result.message || '검사 데이터 저장에 실패했습니다.' };
}
return { success: true, data: result.data };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] saveItemInspection error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 작업지시 전체 검사 데이터 조회 =====
export interface InspectionDataItem {
item_id: number;
item_name: string;
specification: string | null;
quantity: number;
sort_order: number;
options: Record<string, unknown> | null;
inspection_data: Record<string, unknown>;
}
export async function getWorkOrderInspectionData(
workOrderId: string
): Promise<{
success: boolean;
data?: { work_order_id: number; items: InspectionDataItem[]; total: number };
error?: string;
}> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection-data`;
console.log('[WorkerScreenActions] GET inspection data:', url);
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '검사 데이터 조회에 실패했습니다.' };
}
return { success: true, data: result.data };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] getWorkOrderInspectionData error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}

View File

@@ -33,7 +33,7 @@ import { Button } from '@/components/ui/button';
import { PageLayout } from '@/components/organisms/PageLayout';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import { getMyWorkOrders, completeWorkOrder } from './actions';
import { getMyWorkOrders, completeWorkOrder, saveItemInspection, getWorkOrderInspectionData } from './actions';
import { getProcessList } from '@/components/process-management/actions';
import type { InspectionSetting, Process } from '@/types/process';
import type { WorkOrder } from '../ProductionDashboard/types';
@@ -445,6 +445,37 @@ export default function WorkerScreen() {
}
}, [activeTab, processListCache]);
// ===== 작업지시 선택 시 기존 검사 데이터 로드 =====
useEffect(() => {
if (!selectedSidebarOrderId) return;
// 목업 ID면 건너뛰기
if (selectedSidebarOrderId.startsWith('order-')) return;
const loadInspectionData = async () => {
try {
const result = await getWorkOrderInspectionData(selectedSidebarOrderId);
if (result.success && result.data?.items) {
setInspectionDataMap((prev) => {
const next = new Map(prev);
for (const apiItem of result.data!.items) {
if (!apiItem.inspection_data) continue;
// workItems에서 apiItemId가 일치하는 항목 찾기
const match = workItems.find((w) => w.apiItemId === apiItem.item_id);
if (match) {
next.set(match.id, apiItem.inspection_data as unknown as InspectionData);
}
}
return next;
});
}
} catch {
// 검사 데이터 로드 실패는 무시 (새 작업일 수 있음)
}
};
loadInspectionData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedSidebarOrderId]);
// ===== 탭별 필터링된 작업 =====
const filteredWorkOrders = useMemo(() => {
const selectedProcess = processListCache.find((p) => p.id === activeTab);
@@ -530,6 +561,8 @@ export default function WorkerScreen() {
apiItems.push({
id: `${selectedOrder.id}-node-${nodeKey}`,
apiItemId: group.items[0]?.id as number | undefined,
workOrderId: selectedOrder.id,
itemNo: index + 1,
itemCode: selectedOrder.orderNo || '-',
itemName: `${group.nodeName} : ${itemSummary}`,
@@ -557,6 +590,7 @@ export default function WorkerScreen() {
});
apiItems.push({
id: selectedOrder.id,
workOrderId: selectedOrder.id,
itemNo: 1,
itemCode: selectedOrder.orderNo || '-',
itemName: selectedOrder.productName || '-',
@@ -858,17 +892,40 @@ export default function WorkerScreen() {
}
}, [getTargetOrder]);
// 중간검사 완료 핸들러
const handleInspectionComplete = useCallback((data: InspectionData) => {
if (selectedOrder) {
setInspectionDataMap((prev) => {
const next = new Map(prev);
next.set(selectedOrder.id, data);
return next;
});
// 중간검사 완료 핸들러 (API 저장 + 메모리 업데이트)
const handleInspectionComplete = useCallback(async (data: InspectionData) => {
if (!selectedOrder) return;
// 메모리에 즉시 반영
setInspectionDataMap((prev) => {
const next = new Map(prev);
next.set(selectedOrder.id, data);
return next;
});
// 실제 API item인 경우 서버에 저장
const targetItem = workItems.find((w) => w.id === selectedOrder.id);
if (targetItem?.apiItemId && targetItem?.workOrderId) {
try {
const result = await saveItemInspection(
targetItem.workOrderId,
targetItem.apiItemId,
getInspectionProcessType(),
data as unknown as Record<string, unknown>
);
if (result.success) {
toast.success('중간검사가 저장되었습니다.');
} else {
toast.error(result.error || '검사 데이터 저장에 실패했습니다.');
}
} catch {
toast.error('검사 데이터 저장 중 오류가 발생했습니다.');
}
} else {
// 목업 데이터는 메모리만 저장
toast.success('중간검사가 완료되었습니다.');
}
}, [selectedOrder]);
}, [selectedOrder, workItems, getInspectionProcessType]);
// ===== 재공품 감지 =====
const hasWipItems = useMemo(() => {
@@ -1224,6 +1281,7 @@ export default function WorkerScreen() {
processType={getInspectionProcessType()}
productName={selectedOrder?.productName || workItems[0]?.itemName || ''}
specification={workItems[0]?.slatJointBarInfo?.specification || workItems[0]?.wipInfo?.specification || ''}
initialData={selectedOrder ? inspectionDataMap.get(selectedOrder.id) : undefined}
onComplete={handleInspectionComplete}
/>
</PageLayout>

View File

@@ -31,6 +31,8 @@ export interface WorkInfo {
// ===== 작업 아이템 (카드 1개 단위) =====
export interface WorkItemData {
id: string;
apiItemId?: number; // 실제 work_order_items.id (API 호출용)
workOrderId?: string; // 소속 작업지시 ID (API 호출용)
itemNo: number; // 번호 (1, 2, 3...)
itemCode: string; // 품목코드 (KWWS03)
itemName: string; // 품목명 (와이어)