refactor(WEB): Server Action 공통화 및 보안 강화

- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일)
- sanitize 유틸 추가 (XSS 방지)
- middleware CSP 헤더 추가 및 Open Redirect 방지
- 프록시 라우트 로깅 개발환경 한정으로 변경
- 프로덕션 불필요 console.log 제거

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-09 16:14:06 +09:00
parent d014227e9c
commit 55e0791e16
85 changed files with 7211 additions and 17638 deletions

View File

@@ -8,8 +8,7 @@
'use server';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import { executeServerAction } from '@/lib/api/execute-server-action';
import type { WorkOrder, WorkOrderStatus } from '../ProductionDashboard/types';
import type { WorkItemData, WorkStepData, ProcessTab } from './types';
@@ -161,68 +160,24 @@ function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
};
}
const API_URL = process.env.NEXT_PUBLIC_API_URL;
// ===== 내 작업 목록 조회 =====
export async function getMyWorkOrders(): Promise<{
success: boolean;
data: WorkOrder[];
error?: string;
}> {
try {
// 작업자 화면용 캐스케이드 필터: 개인 배정 → 부서 → 전체
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders?per_page=100&worker_screen=1`;
console.log('[WorkerScreenActions] GET my work orders:', url);
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
console.warn('[WorkerScreenActions] GET error:', error?.message);
return {
success: false,
data: [],
error: error?.message || '네트워크 오류가 발생했습니다.',
};
}
if (!response.ok) {
console.warn('[WorkerScreenActions] GET error:', response.status);
return {
success: false,
data: [],
error: `API 오류: ${response.status}`,
};
}
const result = await response.json();
if (!result.success) {
return {
success: false,
data: [],
error: result.message || '작업 목록 조회에 실패했습니다.',
};
}
const apiData = result.data?.data || [];
// 완료/출하 상태 제외하고 변환
const workOrders = apiData
.filter((item: WorkOrderApiItem) => !['completed', 'shipped'].includes(item.status))
.map(transformToWorkerScreenFormat);
return {
success: true,
data: workOrders,
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] getMyWorkOrders error:', error);
return {
success: false,
data: [],
error: '서버 오류가 발생했습니다.',
};
}
interface PaginatedWO { data: WorkOrderApiItem[] }
const result = await executeServerAction<PaginatedWO>({
url: `${API_URL}/api/v1/work-orders?per_page=100&worker_screen=1`,
errorMessage: '작업 목록 조회에 실패했습니다.',
});
if (!result.success || !result.data) return { success: false, data: [], error: result.error };
const workOrders = (result.data.data || [])
.filter((item) => !['completed', 'shipped'].includes(item.status))
.map(transformToWorkerScreenFormat);
return { success: true, data: workOrders };
}
// ===== 작업 완료 처리 =====
@@ -230,51 +185,15 @@ export async function completeWorkOrder(
id: string,
materials?: { materialId: number; quantity: number; lotNo?: string }[]
): Promise<{ success: boolean; lotNo?: string; error?: string }> {
try {
// 상태를 completed로 변경
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/status`,
{
method: 'PATCH',
body: JSON.stringify({
status: 'completed',
materials,
}),
}
);
if (error || !response) {
return {
success: false,
error: error?.message || '네트워크 오류가 발생했습니다.',
};
}
const result = await response.json();
console.log('[WorkerScreenActions] Complete response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '작업 완료 처리에 실패했습니다.',
};
}
// LOT 번호 생성 (임시)
const lotNo = `KD-SA-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01`;
return {
success: true,
lotNo,
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] completeWorkOrder error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
}
const result = await executeServerAction({
url: `${API_URL}/api/v1/work-orders/${id}/status`,
method: 'PATCH',
body: { status: 'completed', materials },
errorMessage: '작업 완료 처리에 실패했습니다.',
});
if (!result.success) return { success: false, error: result.error };
const lotNo = `KD-SA-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01`;
return { success: true, lotNo };
}
// ===== 자재 목록 조회 (로트 기준) =====
@@ -298,80 +217,25 @@ export async function getMaterialsForWorkOrder(
data: MaterialForInput[];
error?: string;
}> {
try {
// 작업지시 BOM 기준 자재 목록 조회
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/materials`;
console.log('[WorkerScreenActions] GET materials for work order:', url);
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
console.warn('[WorkerScreenActions] GET materials error:', error?.message);
return {
success: false,
data: [],
error: error?.message || '네트워크 오류가 발생했습니다.',
};
}
if (!response.ok) {
console.warn('[WorkerScreenActions] GET materials error:', response.status);
return {
success: false,
data: [],
error: `API 오류: ${response.status}`,
};
}
const result = await response.json();
if (!result.success) {
return {
success: false,
data: [],
error: result.message || '자재 목록 조회에 실패했습니다.',
};
}
// API 응답을 MaterialForInput 형식으로 변환 (로트 단위)
const materials: MaterialForInput[] = (result.data || []).map((item: {
stock_lot_id: number | null;
item_id: number;
lot_no: string | null;
material_code: string;
material_name: string;
specification: string;
unit: string;
required_qty: number;
lot_available_qty: number;
fifo_rank: number;
}) => ({
stockLotId: item.stock_lot_id,
itemId: item.item_id,
lotNo: item.lot_no,
materialCode: item.material_code,
materialName: item.material_name,
specification: item.specification ?? '',
unit: item.unit,
requiredQty: item.required_qty,
lotAvailableQty: item.lot_available_qty,
fifoRank: item.fifo_rank,
}));
return {
success: true,
data: materials,
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] getMaterialsForWorkOrder error:', error);
return {
success: false,
data: [],
error: '서버 오류가 발생했습니다.',
};
interface MaterialApiItem {
stock_lot_id: number | null; item_id: number; lot_no: string | null;
material_code: string; material_name: string; specification: string;
unit: string; required_qty: number; lot_available_qty: number; fifo_rank: number;
}
const result = await executeServerAction<MaterialApiItem[]>({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/materials`,
errorMessage: '자재 목록 조회에 실패했습니다.',
});
if (!result.success || !result.data) return { success: false, data: [], error: result.error };
return {
success: true,
data: result.data.map((item) => ({
stockLotId: item.stock_lot_id, itemId: item.item_id, lotNo: item.lot_no,
materialCode: item.material_code, materialName: item.material_name,
specification: item.specification ?? '', unit: item.unit,
requiredQty: item.required_qty, lotAvailableQty: item.lot_available_qty, fifoRank: item.fifo_rank,
})),
};
}
// ===== 자재 투입 등록 (로트별 수량) =====
@@ -379,41 +243,13 @@ export async function registerMaterialInput(
workOrderId: string,
inputs: { stock_lot_id: number; qty: number }[]
): Promise<{ success: boolean; error?: string }> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/material-inputs`,
{
method: 'POST',
body: JSON.stringify({ inputs }),
}
);
if (error || !response) {
return {
success: false,
error: error?.message || '네트워크 오류가 발생했습니다.',
};
}
const result = await response.json();
console.log('[WorkerScreenActions] Register material input response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '자재 투입 등록에 실패했습니다.',
};
}
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] registerMaterialInput error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
}
const result = await executeServerAction({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/material-inputs`,
method: 'POST',
body: { inputs },
errorMessage: '자재 투입 등록에 실패했습니다.',
});
return { success: result.success, error: result.error };
}
// ===== 이슈 보고 =====
@@ -425,41 +261,13 @@ export async function reportIssue(
priority?: 'low' | 'medium' | 'high';
}
): Promise<{ success: boolean; error?: string }> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/issues`,
{
method: 'POST',
body: JSON.stringify(data),
}
);
if (error || !response) {
return {
success: false,
error: error?.message || '네트워크 오류가 발생했습니다.',
};
}
const result = await response.json();
console.log('[WorkerScreenActions] Report issue response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '이슈 보고에 실패했습니다.',
};
}
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] reportIssue error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
}
const result = await executeServerAction({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/issues`,
method: 'POST',
body: data,
errorMessage: '이슈 보고에 실패했습니다.',
});
return { success: result.success, error: result.error };
}
// ===== 공정 단계 조회 =====
@@ -490,89 +298,27 @@ export async function getProcessSteps(
data: ProcessStep[];
error?: string;
}> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/process-steps`;
console.log('[WorkerScreenActions] GET process steps:', url);
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
console.warn('[WorkerScreenActions] GET process steps error:', error?.message);
return {
success: false,
data: [],
error: error?.message || '네트워크 오류가 발생했습니다.',
};
}
if (!response.ok) {
console.warn('[WorkerScreenActions] GET process steps error:', response.status);
return {
success: false,
data: [],
error: `API 오류: ${response.status}`,
};
}
const result = await response.json();
if (!result.success) {
return {
success: false,
data: [],
error: result.message || '공정 단계 조회에 실패했습니다.',
};
}
// API 응답을 ProcessStep 형식으로 변환
const steps: ProcessStep[] = (result.data || []).map((step: {
id: number;
step_no: number;
name: string;
is_inspection?: boolean;
completed: number;
total: number;
items?: {
id: number;
item_no: string;
location: string;
is_priority: boolean;
spec: string;
material: string;
lot: string;
}[];
}) => ({
id: String(step.id),
stepNo: step.step_no,
name: step.name,
isInspection: step.is_inspection,
completed: step.completed,
total: step.total,
items: (step.items || []).map((item) => ({
id: String(item.id),
itemNo: item.item_no,
location: item.location,
isPriority: item.is_priority,
spec: item.spec,
material: item.material,
lot: item.lot,
})),
}));
return {
success: true,
data: steps,
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] getProcessSteps error:', error);
return {
success: false,
data: [],
error: '서버 오류가 발생했습니다.',
};
interface StepApiItem {
id: number; step_no: number; name: string; is_inspection?: boolean;
completed: number; total: number;
items?: { id: number; item_no: string; location: string; is_priority: boolean; spec: string; material: string; lot: string }[];
}
const result = await executeServerAction<StepApiItem[]>({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/process-steps`,
errorMessage: '공정 단계 조회에 실패했습니다.',
});
if (!result.success || !result.data) return { success: false, data: [], error: result.error };
return {
success: true,
data: result.data.map((step) => ({
id: String(step.id), stepNo: step.step_no, name: step.name,
isInspection: step.is_inspection, completed: step.completed, total: step.total,
items: (step.items || []).map((item) => ({
id: String(item.id), itemNo: item.item_no, location: item.location,
isPriority: item.is_priority, spec: item.spec, material: item.material, lot: item.lot,
})),
})),
};
}
// ===== 검사 요청 =====
@@ -580,41 +326,13 @@ export async function requestInspection(
workOrderId: string,
stepId: string
): Promise<{ success: boolean; error?: string }> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/process-steps/${stepId}/inspection-request`,
{
method: 'POST',
body: JSON.stringify({}),
}
);
if (error || !response) {
return {
success: false,
error: error?.message || '네트워크 오류가 발생했습니다.',
};
}
const result = await response.json();
console.log('[WorkerScreenActions] Inspection request response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '검사 요청에 실패했습니다.',
};
}
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] requestInspection error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
}
const result = await executeServerAction({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/process-steps/${stepId}/inspection-request`,
method: 'POST',
body: {},
errorMessage: '검사 요청에 실패했습니다.',
});
return { success: result.success, error: result.error };
}
// ===== 공정 단계 진행 현황 조회 =====
@@ -640,27 +358,12 @@ export async function getStepProgress(
data: StepProgressItem[];
error?: string;
}> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/step-progress`;
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, data: [], error: error?.message || '네트워크 오류' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, data: [], error: result.message || '단계 진행 조회 실패' };
}
return { success: true, data: result.data || [] };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] getStepProgress error:', error);
return { success: false, data: [], error: '서버 오류' };
}
const result = await executeServerAction<StepProgressItem[]>({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/step-progress`,
errorMessage: '단계 진행 조회에 실패했습니다.',
});
if (!result.success || !result.data) return { success: false, data: [], error: result.error };
return { success: true, data: result.data };
}
// ===== 공정 단계 완료 토글 =====
@@ -668,27 +371,12 @@ export async function toggleStepProgress(
workOrderId: string,
progressId: number
): Promise<{ success: boolean; data?: StepProgressItem; error?: string }> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/step-progress/${progressId}/toggle`;
const { response, error } = await serverFetch(url, { method: 'PATCH' });
if (error || !response) {
return { success: false, error: error?.message || '네트워크 오류' };
}
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] toggleStepProgress error:', error);
return { success: false, error: '서버 오류' };
}
const result = await executeServerAction<StepProgressItem>({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/step-progress/${progressId}/toggle`,
method: 'PATCH',
errorMessage: '단계 토글에 실패했습니다.',
});
return { success: result.success, data: result.data, error: result.error };
}
// ===== 작업지시 상세 조회 (items + options 포함) =====
@@ -699,134 +387,91 @@ export async function getWorkOrderDetail(
data: WorkItemData[];
error?: string;
}> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await executeServerAction<any>({
url: `${API_URL}/api/v1/work-orders/${workOrderId}`,
errorMessage: '작업지시 상세 조회에 실패했습니다.',
});
if (!result.success || !result.data) return { success: false, data: [], error: result.error };
const { response, error } = await serverFetch(url, { method: 'GET' });
const wo = result.data;
const processName = (wo.process?.process_name || '').toLowerCase();
const processType: ProcessTab =
processName.includes('스크린') ? 'screen' :
processName.includes('슬랫') ? 'slat' :
processName.includes('절곡') ? 'bending' : 'screen';
if (error || !response) {
return { success: false, data: [], error: error?.message || '네트워크 오류' };
}
const items: WorkItemData[] = (wo.items || []).map((item: {
id: number; item_name: string; specification: string | null;
quantity: string; unit: string | null; status: string;
sort_order: number; options: Record<string, unknown> | null;
}, index: number) => {
const opts = item.options || {};
const stepProgressList = wo.step_progress || [];
const processSteps = wo.process?.steps || [];
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, data: [], error: result.message || '상세 조회 실패' };
}
const wo = result.data;
const processName = (wo.process?.process_name || '').toLowerCase();
const processType: ProcessTab =
processName.includes('스크린') ? 'screen' :
processName.includes('슬랫') ? 'slat' :
processName.includes('절곡') ? 'bending' : 'screen';
// items → WorkItemData 변환 (options 파싱)
const items: WorkItemData[] = (wo.items || []).map((item: {
id: number;
item_name: string;
specification: string | null;
quantity: string;
unit: string | null;
status: string;
sort_order: number;
options: Record<string, unknown> | null;
}, index: number) => {
const opts = item.options || {};
// steps: stepProgress에서 가져오거나 process.steps에서 생성
const stepProgressList = wo.step_progress || [];
const processSteps = wo.process?.steps || [];
let steps: WorkStepData[];
if (stepProgressList.length > 0) {
steps = stepProgressList
.filter((sp: { work_order_item_id: number | null }) => !sp.work_order_item_id || sp.work_order_item_id === item.id)
.map((sp: { id: number; process_step: { step_name: string; step_code: string } | null; status: string }) => ({
id: String(sp.id),
name: sp.process_step?.step_name || '',
isMaterialInput: (sp.process_step?.step_code || '').includes('MAT') || (sp.process_step?.step_name || '').includes('자재투입'),
isCompleted: sp.status === 'completed',
}));
} else {
steps = processSteps.map((ps: { id: number; step_name: string; step_code: string }, si: number) => ({
id: `${item.id}-step-${si}`,
name: ps.step_name,
isMaterialInput: ps.step_code.includes('MAT') || ps.step_name.includes('자재투입'),
isCompleted: false,
let steps: WorkStepData[];
if (stepProgressList.length > 0) {
steps = stepProgressList
.filter((sp: { work_order_item_id: number | null }) => !sp.work_order_item_id || sp.work_order_item_id === item.id)
.map((sp: { id: number; process_step: { step_name: string; step_code: string } | null; status: string }) => ({
id: String(sp.id),
name: sp.process_step?.step_name || '',
isMaterialInput: (sp.process_step?.step_code || '').includes('MAT') || (sp.process_step?.step_name || '').includes('자재투입'),
isCompleted: sp.status === 'completed',
}));
}
} else {
steps = processSteps.map((ps: { id: number; step_name: string; step_code: string }, si: number) => ({
id: `${item.id}-step-${si}`,
name: ps.step_name,
isMaterialInput: ps.step_code.includes('MAT') || ps.step_name.includes('자재투입'),
isCompleted: false,
}));
}
const workItem: WorkItemData = {
id: String(item.id),
itemNo: index + 1,
itemCode: wo.work_order_no || '-',
itemName: item.item_name || '-',
floor: (opts.floor as string) || '-',
code: (opts.code as string) || '-',
width: (opts.width as number) || 0,
height: (opts.height as number) || 0,
quantity: Number(item.quantity) || 0,
processType,
steps,
materialInputs: [],
const workItem: WorkItemData = {
id: String(item.id), itemNo: index + 1,
itemCode: wo.work_order_no || '-', itemName: item.item_name || '-',
floor: (opts.floor as string) || '-', code: (opts.code as string) || '-',
width: (opts.width as number) || 0, height: (opts.height as number) || 0,
quantity: Number(item.quantity) || 0, processType, steps, materialInputs: [],
};
if (opts.cutting_info) {
const ci = opts.cutting_info as { width: number; sheets: number };
workItem.cuttingInfo = { width: ci.width, sheets: ci.sheets };
}
if (opts.slat_info) {
const si = opts.slat_info as { length: number; slat_count: number; joint_bar: number };
workItem.slatInfo = { length: si.length, slatCount: si.slat_count, jointBar: si.joint_bar };
}
if (opts.bending_info) {
const bi = opts.bending_info as {
common: { kind: string; type: string; length_quantities: { length: number; quantity: number }[] };
detail_parts: { part_name: string; material: string; barcy_info: string }[];
};
workItem.bendingInfo = {
common: { kind: bi.common.kind, type: bi.common.type, lengthQuantities: bi.common.length_quantities || [] },
detailParts: (bi.detail_parts || []).map(dp => ({ partName: dp.part_name, material: dp.material, barcyInfo: dp.barcy_info })),
};
}
if (opts.is_wip) {
workItem.isWip = true;
const wi = opts.wip_info as { specification: string; length_quantity: string; drawing_url?: string } | undefined;
if (wi) {
workItem.wipInfo = { specification: wi.specification, lengthQuantity: wi.length_quantity, drawingUrl: wi.drawing_url };
}
}
if (opts.is_joint_bar) {
workItem.isJointBar = true;
const jb = opts.slat_joint_bar_info as { specification: string; length: number; quantity: number } | undefined;
if (jb) workItem.slatJointBarInfo = jb;
}
// 공정별 상세 정보 파싱 (options에서)
if (opts.cutting_info) {
const ci = opts.cutting_info as { width: number; sheets: number };
workItem.cuttingInfo = { width: ci.width, sheets: ci.sheets };
}
if (opts.slat_info) {
const si = opts.slat_info as { length: number; slat_count: number; joint_bar: number };
workItem.slatInfo = { length: si.length, slatCount: si.slat_count, jointBar: si.joint_bar };
}
if (opts.bending_info) {
const bi = opts.bending_info as {
common: { kind: string; type: string; length_quantities: { length: number; quantity: number }[] };
detail_parts: { part_name: string; material: string; barcy_info: string }[];
};
workItem.bendingInfo = {
common: {
kind: bi.common.kind,
type: bi.common.type,
lengthQuantities: bi.common.length_quantities || [],
},
detailParts: (bi.detail_parts || []).map(dp => ({
partName: dp.part_name,
material: dp.material,
barcyInfo: dp.barcy_info,
})),
};
}
if (opts.is_wip) {
workItem.isWip = true;
const wi = opts.wip_info as { specification: string; length_quantity: string; drawing_url?: string } | undefined;
if (wi) {
workItem.wipInfo = {
specification: wi.specification,
lengthQuantity: wi.length_quantity,
drawingUrl: wi.drawing_url,
};
}
}
if (opts.is_joint_bar) {
workItem.isJointBar = true;
const jb = opts.slat_joint_bar_info as { specification: string; length: number; quantity: number } | undefined;
if (jb) {
workItem.slatJointBarInfo = jb;
}
}
return workItem;
});
return workItem;
});
return { success: true, data: items };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[WorkerScreenActions] getWorkOrderDetail error:', error);
return { success: false, data: [], error: '서버 오류' };
}
return { success: true, data: items };
}
// ===== 개소별 중간검사 데이터 저장 =====
@@ -836,37 +481,13 @@ export async function saveItemInspection(
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: '서버 오류가 발생했습니다.' };
}
const result = await executeServerAction<Record<string, unknown>>({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/inspection`,
method: 'POST',
body: { process_type: processType, inspection_data: inspectionData },
errorMessage: '검사 데이터 저장에 실패했습니다.',
});
return { success: result.success, data: result.data, error: result.error };
}
// ===== 작업지시 전체 검사 데이터 조회 =====
@@ -887,27 +508,9 @@ export async function getWorkOrderInspectionData(
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: '서버 오류가 발생했습니다.' };
}
const result = await executeServerAction<{ work_order_id: number; items: InspectionDataItem[]; total: number }>({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/inspection-data`,
errorMessage: '검사 데이터 조회에 실패했습니다.',
});
return { success: result.success, data: result.data, error: result.error };
}