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:
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user