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:
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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; // 품목명 (와이어)
|
||||
|
||||
Reference in New Issue
Block a user