Files
sam-react-prod/src/components/production/WorkerScreen/actions.ts

925 lines
33 KiB
TypeScript

/**
* 작업자 화면 서버 액션
* API 연동 (2025-12-26 초기, 2026-02-06 options + step-progress 확장)
*
* WorkOrders API를 호출하고 WorkerScreen에 맞는 형식으로 변환
*/
'use server';
import { executeServerAction } from '@/lib/api/execute-server-action';
import { buildApiUrl } from '@/lib/api/query-params';
import type { WorkOrder, WorkOrderStatus } from '../ProductionDashboard/types';
import type { WorkItemData, WorkStepData, ProcessTab } from './types';
// ===== API 타입 =====
interface WorkOrderApiItem {
id: number;
work_order_no: string;
project_name: string | null;
process_id: number | null;
process?: {
id: number;
process_name: string;
process_code: string;
department?: string | null;
options?: {
needs_inspection?: boolean;
needs_work_log?: boolean;
} | null;
};
/** @deprecated process_id + process relation 사용 */
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;
item?: { id: number; code: string; name: string } | null;
client?: { id: number; name: string };
client_contact?: string;
options?: { manager_name?: string; [key: string]: unknown };
root_nodes_count?: number;
};
team_id?: number | null;
team?: { id: number; name: string } | null;
assignee?: { id: number; name: string };
assignees?: { id: number; user_id: number; user?: { id: number; name: string } }[];
items?: {
id: number;
item_name: string;
item_id?: number | null;
item?: { id: number; code: string; name: string } | null;
quantity: number;
specification?: string | null;
options?: Record<string, unknown> | null;
source_order_item?: {
id: number;
order_node_id: number | null;
floor_code?: string | null;
symbol_code?: string | null;
node?: { id: number; name: string; code: string } | null;
} | null;
material_inputs?: {
id: number;
stock_lot_id: number;
item_id: number;
qty: number;
input_by: number | null;
input_at: string | null;
stock_lot?: { id: number; lot_no: string } | null;
item?: { id: number; code: string; name: string; unit: string } | null;
}[];
}[];
}
// ===== 상태 변환 =====
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 + Number(item.quantity), 0);
const productCode = (api.items?.[0]?.options?.product_code as string) || api.sales_order?.item?.code || '-';
const productName = (api.items?.[0]?.options?.product_name as string) || 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;
// process relation → processCode/processName 매핑
// 신규: process relation 사용, 폴백: deprecated process_type 필드
const rawProcessName = api.process?.process_name || '';
const rawProcessCode = api.process?.process_code || '';
// 한글 공정명 → 탭 키 매핑 (필터링에 사용)
const nameToTab: Record<string, { code: string; name: string }> = {
'스크린': { code: 'screen', name: '스크린' },
'슬랫': { code: 'slat', name: '슬랫' },
'절곡': { code: 'bending', name: '절곡' },
};
// 1차: process relation의 process_name으로 매핑
let processInfo = Object.entries(nameToTab).find(
([key]) => rawProcessName.includes(key)
)?.[1];
// 2차: deprecated process_type 폴백
if (!processInfo && api.process_type) {
const legacyMap: Record<string, { code: string; name: string }> = {
screen: { code: 'screen', name: '스크린' },
slat: { code: 'slat', name: '슬랫' },
bending: { code: 'bending', name: '절곡' },
};
processInfo = legacyMap[api.process_type];
}
// 3차: 그래도 없으면 원시값 사용
if (!processInfo) {
processInfo = { code: rawProcessCode || 'unknown', name: rawProcessName || '알수없음' };
}
// 아이템을 개소(order_node_id)별로 그룹핑 (floor_code/symbol_code는 표시용)
const nodeMap = new Map<string, { nodeId: number | null; nodeName: string; items: typeof api.items }>();
for (const item of (api.items || [])) {
const nodeId = item.source_order_item?.order_node_id ?? null;
const floorCode = item.source_order_item?.floor_code;
const symbolCode = item.source_order_item?.symbol_code;
const floorLabel = [floorCode, symbolCode].filter(v => v && v !== '-').join('/');
const nodeName = floorLabel || item.source_order_item?.node?.name || '미지정';
const key = nodeId != null ? String(nodeId) : (floorLabel || `unassigned-${item.id}`);
if (!nodeMap.has(key)) {
nodeMap.set(key, { nodeId, nodeName, items: [] });
}
nodeMap.get(key)!.items!.push(item);
}
const nodeGroups = Array.from(nodeMap.values()).map((g) => ({
nodeId: g.nodeId,
nodeName: g.nodeName,
items: (g.items || []).map((it) => ({
id: it.id,
itemCode: it.item?.code || null,
itemName: it.item_name,
quantity: Number(it.quantity),
specification: it.specification,
options: it.options,
materialInputs: (it.material_inputs || []).map((mi) => ({
id: mi.id,
stockLotId: mi.stock_lot_id,
lotNo: mi.stock_lot?.lot_no || null,
itemId: mi.item_id,
materialName: mi.item?.name || null,
qty: Number(mi.qty),
unit: mi.item?.unit || 'EA',
})),
})),
totalQuantity: (g.items || []).reduce((sum, it) => sum + Number(it.quantity), 0),
}));
return {
id: String(api.id),
orderNo: api.work_order_no,
productCode,
productName,
processCode: processInfo.code,
processName: processInfo.name,
client: api.sales_order?.client?.name || '-',
projectName: api.project_name || '-',
assignees: api.assignees?.length
? api.assignees.map((a) => a.user?.name || '').filter(Boolean)
: api.assignee ? [api.assignee.name] : [],
quantity: totalQuantity,
shutterCount: nodeGroups.length || api.sales_order?.root_nodes_count || 0,
dueDate,
priority: 5, // 기본 우선순위
status: mapApiStatus(api.status),
isUrgent: false, // 긴급 여부는 별도 필드 필요
isDelayed,
delayDays,
instruction: api.memo || undefined,
salesOrderNo: api.sales_order?.order_no || undefined,
salesManager: api.sales_order?.options?.manager_name as string || undefined,
managerPhone: api.sales_order?.client_contact || undefined,
teamId: api.team_id ?? null,
teamName: api.team?.name || undefined,
processDepartment: api.process?.department || undefined,
scheduledDate: api.scheduled_date || undefined,
createdAt: api.created_at,
processOptions: {
needsInspection: api.process?.options?.needs_inspection ?? false,
needsWorkLog: api.process?.options?.needs_work_log ?? false,
},
nodeGroups,
};
}
const API_URL = process.env.NEXT_PUBLIC_API_URL;
// ===== 내 작업 목록 조회 =====
export async function getMyWorkOrders(): Promise<{
success: boolean;
data: WorkOrder[];
error?: string;
}> {
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 };
}
// ===== 작업 완료 처리 =====
export async function completeWorkOrder(
id: string,
materials?: { materialId: number; quantity: number; lotNo?: string }[]
): Promise<{ success: boolean; lotNo?: string; error?: string }> {
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 };
// 백엔드에서 생성한 실제 LOT 번호 사용
const lotNo = (result.data as { lot_no?: string })?.lot_no;
return { success: true, lotNo };
}
// ===== 자재 목록 조회 (로트 기준) =====
export interface MaterialForInput {
stockLotId: number | null; // StockLot ID (null이면 재고 없음)
itemId: number;
lotNo: string | null; // 실제 입고 로트번호
materialCode: string;
materialName: string;
specification: string;
unit: string;
requiredQty: number; // 필요 수량
lotAvailableQty: number; // 로트별 가용 수량
fifoRank: number;
// dynamic_bom 추가 필드 (절곡 세부품목용)
workOrderItemId?: number; // 개소(작업지시품목) ID
lotPrefix?: string; // LOT prefix (RS, RM 등)
partType?: string; // 파트 타입 (finish, body 등)
category?: string; // 카테고리 (guideRail, bottomBar 등)
}
export async function getMaterialsForWorkOrder(
workOrderId: string
): Promise<{
success: boolean;
data: MaterialForInput[];
error?: string;
}> {
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;
// dynamic_bom 추가 필드
work_order_item_id?: number; lot_prefix?: string; part_type?: string; category?: string;
}
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,
workOrderItemId: item.work_order_item_id, lotPrefix: item.lot_prefix,
partType: item.part_type, category: item.category,
})),
};
}
// ===== 자재 투입 등록 (로트별 수량) =====
export async function registerMaterialInput(
workOrderId: string,
inputs: { stock_lot_id: number; qty: number; work_order_item_id?: number }[]
): Promise<{ success: boolean; error?: string }> {
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 };
}
// ===== 개소별 자재 목록 조회 =====
export interface MaterialForItemInput extends MaterialForInput {
alreadyInputted: number; // 이미 투입된 수량
remainingRequiredQty: number; // 남은 필요 수량
lotInputtedQty: number; // 해당 LOT의 기투입 수량
bomGroupKey?: string; // BOM 엔트리별 고유 그룹키 (category+partType 기반)
}
export async function getMaterialsForItem(
workOrderId: string,
itemId: number
): Promise<{
success: boolean;
data: MaterialForItemInput[];
error?: string;
}> {
interface MaterialItemApiItem {
stock_lot_id: number | null; item_id: number; lot_no: string | null;
material_code: string; material_name: string; specification: string;
unit: string; bom_qty: number; required_qty: number;
already_inputted: number; remaining_required_qty: number; lot_inputted_qty: number;
lot_available_qty: number; fifo_rank: number;
lot_qty: number; lot_reserved_qty: number;
receipt_date: string | null; supplier: string | null;
// dynamic_bom 추가 필드
work_order_item_id?: number; lot_prefix?: string; part_type?: string; category?: string;
bom_group_key?: string;
}
const result = await executeServerAction<MaterialItemApiItem[]>({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/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,
alreadyInputted: item.already_inputted,
remainingRequiredQty: item.remaining_required_qty,
lotInputtedQty: item.lot_inputted_qty ?? 0,
workOrderItemId: item.work_order_item_id, lotPrefix: item.lot_prefix,
partType: item.part_type, category: item.category,
bomGroupKey: item.bom_group_key,
})),
};
}
// ===== 개소별 자재 투입 등록 =====
export async function registerMaterialInputForItem(
workOrderId: string,
itemId: number,
inputs: { stock_lot_id: number; qty: number; bom_group_key?: string }[],
replace = false
): Promise<{ success: boolean; error?: string }> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/material-inputs`,
method: 'POST',
body: { inputs, replace },
errorMessage: '개소별 자재 투입 등록에 실패했습니다.',
});
return { success: result.success, error: result.error };
}
// ===== 개소별 자재 투입 이력 조회 =====
export interface MaterialInputHistoryItem {
id: number;
stockLotId: number;
lotNo: string | null;
itemId: number;
materialCode: string | null;
materialName: string | null;
qty: number;
unit: string;
inputBy: number | null;
inputByName: string | null;
inputAt: string | null;
}
export async function getMaterialInputsForItem(
workOrderId: string,
itemId: number
): Promise<{
success: boolean;
data: MaterialInputHistoryItem[];
error?: string;
}> {
interface HistoryApiItem {
id: number; stock_lot_id: number; lot_no: string | null;
item_id: number; material_code: string | null; material_name: string | null;
qty: number; unit: string;
input_by: number | null; input_by_name: string | null; input_at: string | null;
}
const result = await executeServerAction<HistoryApiItem[]>({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/material-inputs`,
errorMessage: '개소별 투입 이력 조회에 실패했습니다.',
});
if (!result.success || !result.data) return { success: false, data: [], error: result.error };
return {
success: true,
data: result.data.map((item) => ({
id: item.id, stockLotId: item.stock_lot_id, lotNo: item.lot_no,
itemId: item.item_id, materialCode: item.material_code,
materialName: item.material_name, qty: item.qty, unit: item.unit,
inputBy: item.input_by, inputByName: item.input_by_name, inputAt: item.input_at,
})),
};
}
// ===== 자재 투입 삭제 (재고 복원) =====
export async function deleteMaterialInput(
workOrderId: string,
inputId: number
): Promise<{ success: boolean; error?: string }> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/material-inputs/${inputId}`,
method: 'DELETE',
errorMessage: '자재 투입 삭제에 실패했습니다.',
});
return { success: result.success, error: result.error };
}
// ===== 자재 투입 수량 수정 =====
export async function updateMaterialInput(
workOrderId: string,
inputId: number,
qty: number
): Promise<{ success: boolean; data?: { id: number; qty: number; changed: boolean }; error?: string }> {
const result = await executeServerAction<{ id: number; qty: number; changed: boolean }>({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/material-inputs/${inputId}`,
method: 'PATCH',
body: { qty },
errorMessage: '자재 투입 수정에 실패했습니다.',
});
return { success: result.success, data: result.data, error: result.error };
}
// ===== 이슈 보고 =====
export async function reportIssue(
workOrderId: string,
data: {
title: string;
description?: string;
priority?: 'low' | 'medium' | 'high';
}
): Promise<{ success: boolean; error?: string }> {
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 };
}
// ===== 공정 단계 조회 =====
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;
}> {
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,
})),
})),
};
}
// ===== 검사 요청 =====
export async function requestInspection(
workOrderId: string,
stepId: string
): Promise<{ success: boolean; error?: string }> {
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 };
}
// ===== 공정 단계 진행 현황 조회 =====
export interface StepProgressItem {
id: number;
process_step_id: number;
work_order_item_id: number | null;
step_code: string;
step_name: string;
sort_order: number;
needs_inspection: boolean;
connection_type: string | null;
completion_type: string | null;
status: string;
is_completed: boolean;
completed_at: string | null;
completed_by: number | null;
}
export async function getStepProgress(
workOrderId: string
): Promise<{
success: boolean;
data: StepProgressItem[];
error?: string;
}> {
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 };
}
// ===== 공정 단계 완료 토글 =====
export async function toggleStepProgress(
workOrderId: string,
progressId: number
): Promise<{ success: boolean; data?: StepProgressItem; error?: string }> {
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 포함) =====
export async function getWorkOrderDetail(
workOrderId: string
): Promise<{
success: boolean;
data: WorkItemData[];
error?: string;
}> {
// 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 wo = result.data;
const processName = (wo.process?.process_name || '').toLowerCase();
const processType: ProcessTab =
processName.includes('스크린') ? 'screen' :
processName.includes('슬랫') ? 'slat' :
processName.includes('절곡') ? 'bending' : 'screen';
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 || [];
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;
needs_inspection?: boolean; connection_type?: string; completion_type?: 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('자재투입'),
isInspection: sp.process_step?.needs_inspection || false,
isCompleted: sp.status === 'completed',
stepProgressId: sp.id,
needsInspection: sp.process_step?.needs_inspection || false,
connectionType: sp.process_step?.connection_type || undefined,
connectionTarget: undefined, // step_progress API에 미포함, processListCache에서 보완
completionType: sp.process_step?.completion_type || undefined,
}));
} else {
steps = processSteps.map((ps: {
id: number; step_name: string; step_code: string;
needs_inspection?: boolean; connection_type?: string; completion_type?: string;
}, si: number) => ({
id: `${item.id}-step-${si}`,
name: ps.step_name,
isMaterialInput: ps.step_code.includes('MAT') || ps.step_name.includes('자재투입'),
isInspection: ps.needs_inspection || false,
isCompleted: false,
needsInspection: ps.needs_inspection || false,
connectionType: ps.connection_type || undefined,
completionType: ps.completion_type || undefined,
}));
}
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; glass_qty: number };
workItem.slatInfo = { length: si.length, slatCount: si.slat_count, jointBar: si.joint_bar, glassQty: si.glass_qty || 0 };
}
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 } | undefined;
if (wi) {
workItem.wipInfo = { specification: wi.specification, lengthQuantity: wi.length_quantity };
}
}
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 { success: true, data: items };
}
// ===== 개소별 중간검사 데이터 저장 =====
export async function saveItemInspection(
workOrderId: string,
itemId: number,
processType: string,
inspectionData: Record<string, unknown>
): Promise<{ success: boolean; data?: Record<string, unknown>; error?: string }> {
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 };
}
// ===== 작업지시 전체 검사 데이터 조회 =====
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;
}> {
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 };
}
// ===== 작업일지 저장 =====
export async function saveWorkLog(
workOrderId: string,
data: {
basic_data?: Record<string, string>;
table_data?: Array<Record<string, unknown>>;
remarks?: string;
title?: string;
rendered_html?: string;
}
): Promise<{
success: boolean;
data?: { document_id: number; document_no: string; status: string };
error?: string;
}> {
const result = await executeServerAction<{ document_id: number; document_no: string; status: string }>({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/work-log`,
method: 'POST',
body: data,
errorMessage: '작업일지 저장에 실패했습니다.',
});
return { success: result.success, data: result.data, error: result.error };
}
// ===== 작업일지 조회 =====
export async function getWorkLog(
workOrderId: string
): Promise<{
success: boolean;
data?: {
template: Record<string, unknown>;
document: Record<string, unknown> | null;
auto_values: Record<string, string>;
work_stats: Record<string, unknown>;
bending_images: Record<string, string>;
};
error?: string;
}> {
const result = await executeServerAction<{
template: Record<string, unknown>;
document: Record<string, unknown> | null;
auto_values: Record<string, string>;
work_stats: Record<string, unknown>;
bending_images: Record<string, string>;
}>({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/work-log`,
errorMessage: '작업일지 조회에 실패했습니다.',
});
return { success: result.success, data: result.data, error: result.error };
}
// ===== 검사 문서 템플릿 타입 (types.ts에서 import) =====
import type { InspectionTemplateData } from './types';
export async function getInspectionTemplate(
workOrderId: string
): Promise<{
success: boolean;
data?: InspectionTemplateData;
error?: string;
}> {
const result = await executeServerAction<InspectionTemplateData>({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/inspection-template`,
errorMessage: '검사 템플릿 조회에 실패했습니다.',
});
return { success: result.success, data: result.data, error: result.error };
}
// ===== 검사 문서 동기화 (원본: work_order_items → document_data) =====
export async function saveInspectionDocument(
workOrderId: string,
data: {
title?: string;
approvers?: { role_name: string; user_id?: number }[];
} = {}
): Promise<{
success: boolean;
data?: { document_id: number; document_no: string; status: string };
error?: string;
}> {
const result = await executeServerAction<{ document_id: number; document_no: string; status: string }>({
url: `${API_URL}/api/v1/work-orders/${workOrderId}/inspection-document`,
method: 'POST',
body: data,
errorMessage: '검사 문서 동기화에 실패했습니다.',
});
return { success: result.success, data: result.data, error: result.error };
}
// ===== 부서 목록 조회 (작업 정보용) =====
export interface DepartmentOption {
id: number;
name: string;
}
export async function getDepartments(): Promise<{
success: boolean;
data: DepartmentOption[];
error?: string;
}> {
interface DeptApiItem { id: number; name: string; parent_id?: number | null; [key: string]: unknown }
const result = await executeServerAction<{ data: DeptApiItem[] } | DeptApiItem[]>({
url: buildApiUrl('/api/v1/departments', { per_page: 100 }),
errorMessage: '부서 목록 조회에 실패했습니다.',
});
if (!result.success || !result.data) return { success: false, data: [], error: result.error };
const list = Array.isArray(result.data) ? result.data : (result.data.data || []);
return {
success: true,
data: list.map((d) => ({ id: d.id, name: d.name })),
};
}
// ===== 부서별 사용자 목록 조회 =====
export interface DepartmentUser {
id: number;
name: string;
}
export async function getDepartmentUsers(departmentId: number): Promise<{
success: boolean;
data: DepartmentUser[];
error?: string;
}> {
interface UserApiItem { id: number; name: string; [key: string]: unknown }
const result = await executeServerAction<{ data: UserApiItem[] } | UserApiItem[]>({
url: buildApiUrl(`/api/v1/departments/${departmentId}/users`),
errorMessage: '부서 사용자 목록 조회에 실패했습니다.',
});
if (!result.success || !result.data) return { success: false, data: [], error: result.error };
const list = Array.isArray(result.data) ? result.data : (result.data.data || []);
return {
success: true,
data: list.map((u) => ({ id: u.id, name: u.name })),
};
}
// ===== 자재 투입용 재고 검색 =====
export interface StockSearchResult {
itemId: number;
itemCode: string;
itemName: string;
stockQty: number;
availableQty: number;
lotCount: number;
lots: Array<{ lotNo: string; availableQty: number; fifoOrder: number }>;
}
export async function searchStockByCode(
search: string
): Promise<{ success: boolean; data: StockSearchResult[]; error?: string }> {
const result = await executeServerAction<{
data: Array<{
id: number;
item_id: number;
item_code: string;
item_name: string;
stock_qty: number;
available_qty: number;
lot_count: number;
lots?: Array<{ lot_no: string; available_qty: number; fifo_order: number }>;
}>;
}>({
url: buildApiUrl('/api/v1/stocks', { search, per_page: 10, with_lots: 'true' }),
errorMessage: '재고 검색에 실패했습니다.',
});
if (!result.success || !result.data) return { success: false, data: [], error: result.error };
const items = Array.isArray(result.data) ? result.data : (result.data.data || []);
return {
success: true,
data: items.map((s) => ({
itemId: s.item_id,
itemCode: s.item_code,
itemName: s.item_name,
stockQty: s.stock_qty || 0,
availableQty: s.available_qty || 0,
lotCount: s.lot_count || 0,
lots: (s.lots || []).map((l) => ({
lotNo: l.lot_no,
availableQty: l.available_qty,
fifoOrder: l.fifo_order,
})),
})),
};
}