Files
sam-react-prod/src/components/production/WorkerScreen/actions.ts
유병철 9464a368ba refactor: 모달 Content 컴포넌트 분리 및 파일 입력 UI 공통화
- 모달 컴포넌트에서 Content 분리하여 재사용성 향상
  - EstimateDocumentContent, DirectConstructionContent 등
  - WorkLogContent, QuotePreviewContent, ReceivingReceiptContent
- 파일 입력 공통 UI 컴포넌트 추가
  - file-dropzone, file-input, file-list, image-upload
- 폼 컴포넌트 코드 정리 및 중복 제거 (-4,056줄)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 15:07:17 +09:00

524 lines
14 KiB
TypeScript

/**
* 작업자 화면 서버 액션
* API 연동 완료 (2025-12-26)
*
* WorkOrders API를 호출하고 WorkerScreen에 맞는 형식으로 변환
*/
'use server';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { WorkOrder, WorkOrderStatus } from '../ProductionDashboard/types';
// ===== API 타입 =====
interface WorkOrderApiItem {
id: number;
work_order_no: string;
project_name: string | null;
process_type: 'screen' | 'slat' | 'bending';
status: 'unassigned' | 'pending' | 'waiting' | 'in_progress' | 'completed' | 'shipped';
scheduled_date: string | null;
memo: string | null;
created_at: string;
sales_order?: {
id: number;
order_no: string;
client?: { id: number; name: string };
};
assignee?: { id: number; name: string };
items?: { id: number; item_name: string; quantity: number }[];
}
// ===== 상태 변환 =====
function mapApiStatus(status: WorkOrderApiItem['status']): WorkOrderStatus {
switch (status) {
case 'in_progress':
return 'inProgress';
case 'completed':
case 'shipped':
return 'completed';
default:
return 'waiting';
}
}
// ===== API → WorkOrder 변환 =====
function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
const totalQuantity = (api.items || []).reduce((sum, item) => sum + item.quantity, 0);
const productName = api.items?.[0]?.item_name || '-';
// 납기일 계산 (지연 여부)
const dueDate = api.scheduled_date || '';
const today = new Date();
today.setHours(0, 0, 0, 0);
const due = dueDate ? new Date(dueDate) : null;
const isDelayed = due ? due < today : false;
const delayDays = due && isDelayed
? Math.ceil((today.getTime() - due.getTime()) / (1000 * 60 * 60 * 24))
: undefined;
return {
id: String(api.id),
orderNo: api.work_order_no,
productName,
process: api.process_type,
client: api.sales_order?.client?.name || '-',
projectName: api.project_name || '-',
assignees: api.assignee ? [api.assignee.name] : [],
quantity: totalQuantity,
dueDate,
priority: 5, // 기본 우선순위
status: mapApiStatus(api.status),
isUrgent: false, // 긴급 여부는 별도 필드 필요
isDelayed,
delayDays,
instruction: api.memo || undefined,
createdAt: api.created_at,
};
}
// ===== 내 작업 목록 조회 =====
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&assigned_to_me=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: '서버 오류가 발생했습니다.',
};
}
}
// ===== 작업 완료 처리 =====
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: '서버 오류가 발생했습니다.',
};
}
}
// ===== 자재 목록 조회 (BOM 기준) =====
export interface MaterialForInput {
id: number;
materialCode: string;
materialName: string;
unit: string;
currentStock: number;
fifoRank: number;
}
export async function getMaterialsForWorkOrder(
workOrderId: string
): Promise<{
success: boolean;
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: {
id: number;
material_code: string;
material_name: string;
unit: string;
current_stock: number;
fifo_rank: number;
}) => ({
id: item.id,
materialCode: item.material_code,
materialName: item.material_name,
unit: item.unit,
currentStock: item.current_stock,
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: '서버 오류가 발생했습니다.',
};
}
}
// ===== 자재 투입 등록 =====
export async function registerMaterialInput(
workOrderId: string,
materialIds: 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({ material_ids: materialIds }),
}
);
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: '서버 오류가 발생했습니다.',
};
}
}
// ===== 이슈 보고 =====
export async function reportIssue(
workOrderId: string,
data: {
title: string;
description?: string;
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: '서버 오류가 발생했습니다.',
};
}
}
// ===== 공정 단계 조회 =====
export interface ProcessStepItem {
id: string;
itemNo: string;
location: string;
isPriority: boolean;
spec: string;
material: string;
lot: string;
}
export interface ProcessStep {
id: string;
stepNo: number;
name: string;
isInspection?: boolean;
completed: number;
total: number;
items: ProcessStepItem[];
}
export async function getProcessSteps(
workOrderId: string
): Promise<{
success: boolean;
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: '서버 오류가 발생했습니다.',
};
}
}
// ===== 검사 요청 =====
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: '서버 오류가 발생했습니다.',
};
}
}