Files
sam-react-prod/src/components/production/WorkOrders/actions.ts
byeongcheolryu d38b1242d7 feat: fetchWrapper 마이그레이션 및 토큰 리프레시 캐싱 구현
- 40+ actions.ts 파일을 fetchWrapper 패턴으로 마이그레이션
- 토큰 리프레시 캐싱 로직 추가 (refresh-token.ts)
- ApiErrorContext 추가로 전역 에러 처리 개선
- HR EmployeeForm 컴포넌트 개선
- 참조함(ReferenceBox) 기능 수정
- juil 테스트 URL 페이지 추가
- claudedocs 문서 업데이트

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 17:00:18 +09:00

705 lines
20 KiB
TypeScript

/**
* 작업지시 관리 서버 액션
*
* API Endpoints:
* - GET /api/v1/work-orders - 목록 조회
* - GET /api/v1/work-orders/stats - 통계 조회
* - GET /api/v1/work-orders/{id} - 상세 조회
* - POST /api/v1/work-orders - 등록
* - PUT /api/v1/work-orders/{id} - 수정
* - DELETE /api/v1/work-orders/{id} - 삭제
* - PATCH /api/v1/work-orders/{id}/status - 상태 변경
* - PATCH /api/v1/work-orders/{id}/assign - 담당자 배정
* - PATCH /api/v1/work-orders/{id}/bending/toggle - 벤딩 필드 토글
* - POST /api/v1/work-orders/{id}/issues - 이슈 등록
* - PATCH /api/v1/work-orders/{id}/issues/{issueId}/resolve - 이슈 해결
*/
'use server';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type {
WorkOrder,
WorkOrderStats,
WorkOrderStatus,
ProcessType,
WorkOrderApiPaginatedResponse,
WorkOrderStatsApi,
} from './types';
import {
transformApiToFrontend,
transformFrontendToApi,
transformStatsApiToFrontend,
} from './types';
// ===== 페이지네이션 타입 =====
interface PaginationMeta {
currentPage: number;
lastPage: number;
perPage: number;
total: number;
}
// ===== 작업지시 목록 조회 =====
export async function getWorkOrders(params?: {
page?: number;
perPage?: number;
status?: WorkOrderStatus | 'all';
processType?: ProcessType | 'all';
search?: string;
startDate?: string;
endDate?: string;
}): Promise<{
success: boolean;
data: WorkOrder[];
pagination: PaginationMeta;
error?: string;
}> {
const emptyResponse = {
success: false,
data: [] as WorkOrder[],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
};
try {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
if (params?.status && params.status !== 'all') {
searchParams.set('status', params.status);
}
if (params?.processType && params.processType !== 'all') {
searchParams.set('process_type', params.processType);
}
if (params?.search) searchParams.set('search', params.search);
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders${queryString ? `?${queryString}` : ''}`;
console.log('[WorkOrderActions] GET work-orders:', url);
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { ...emptyResponse, error: error?.message || 'API 요청 실패' };
}
if (!response.ok) {
console.warn('[WorkOrderActions] GET work-orders error:', response.status);
return { ...emptyResponse, error: `API 오류: ${response.status}` };
}
const result = await response.json();
if (!result.success) {
return {
...emptyResponse,
error: result.message || '작업지시 목록 조회에 실패했습니다.',
};
}
const paginatedData: WorkOrderApiPaginatedResponse = result.data || {
data: [],
current_page: 1,
last_page: 1,
per_page: 20,
total: 0,
};
const workOrders = (paginatedData.data || []).map(transformApiToFrontend);
return {
success: true,
data: workOrders,
pagination: {
currentPage: paginatedData.current_page,
lastPage: paginatedData.last_page,
perPage: paginatedData.per_page,
total: paginatedData.total,
},
};
} catch (error) {
console.error('[WorkOrderActions] getWorkOrders error:', error);
return { ...emptyResponse, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 작업지시 통계 조회 =====
export async function getWorkOrderStats(): Promise<{
success: boolean;
data?: WorkOrderStats;
error?: string;
}> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/stats`;
console.log('[WorkOrderActions] GET stats:', url);
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
if (!response.ok) {
console.warn('[WorkOrderActions] GET stats error:', response.status);
return { success: false, error: `API 오류: ${response.status}` };
}
const result = await response.json();
if (!result.success) {
return {
success: false,
error: result.message || '통계 조회에 실패했습니다.',
};
}
const statsApi: WorkOrderStatsApi = result.data;
return {
success: true,
data: transformStatsApiToFrontend(statsApi),
};
} catch (error) {
console.error('[WorkOrderActions] getWorkOrderStats error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 작업지시 상세 조회 =====
export async function getWorkOrderById(id: string): Promise<{
success: boolean;
data?: WorkOrder;
error?: string;
}> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`;
console.log('[WorkOrderActions] GET work-order:', url);
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
if (!response.ok) {
console.error('[WorkOrderActions] GET work-order error:', response.status);
return { success: false, error: `API 오류: ${response.status}` };
}
const result = await response.json();
if (!result.success || !result.data) {
return {
success: false,
error: result.message || '작업지시 조회에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[WorkOrderActions] getWorkOrderById error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 작업지시 등록 =====
export async function createWorkOrder(
data: Partial<WorkOrder> & {
salesOrderId?: number;
assigneeId?: number;
teamId?: number;
}
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
try {
const apiData = {
...transformFrontendToApi(data),
sales_order_id: data.salesOrderId,
assignee_id: data.assigneeId,
team_id: data.teamId,
};
console.log('[WorkOrderActions] POST work-order request:', apiData);
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders`,
{
method: 'POST',
body: JSON.stringify(apiData),
}
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] POST work-order response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '작업지시 등록에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[WorkOrderActions] createWorkOrder error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 작업지시 수정 =====
export async function updateWorkOrder(
id: string,
data: Partial<WorkOrder>
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
try {
const apiData = transformFrontendToApi(data);
console.log('[WorkOrderActions] PUT work-order request:', apiData);
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`,
{
method: 'PUT',
body: JSON.stringify(apiData),
}
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] PUT work-order response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '작업지시 수정에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[WorkOrderActions] updateWorkOrder error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 작업지시 삭제 =====
export async function deleteWorkOrder(id: string): Promise<{ success: boolean; error?: string }> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`,
{ method: 'DELETE' }
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] DELETE work-order response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '작업지시 삭제에 실패했습니다.',
};
}
return { success: true };
} catch (error) {
console.error('[WorkOrderActions] deleteWorkOrder error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 작업지시 상태 변경 =====
export async function updateWorkOrderStatus(
id: string,
status: WorkOrderStatus
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
try {
console.log('[WorkOrderActions] PATCH status request:', { status });
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/status`,
{
method: 'PATCH',
body: JSON.stringify({ status }),
}
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] PATCH status response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '상태 변경에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[WorkOrderActions] updateWorkOrderStatus error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 담당자 배정 =====
export async function assignWorkOrder(
id: string,
assigneeId: number,
teamId?: number
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
try {
const body: { assignee_id: number; team_id?: number } = { assignee_id: assigneeId };
if (teamId) body.team_id = teamId;
console.log('[WorkOrderActions] PATCH assign request:', body);
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/assign`,
{
method: 'PATCH',
body: JSON.stringify(body),
}
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] PATCH assign response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '담당자 배정에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[WorkOrderActions] assignWorkOrder error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 벤딩 필드 토글 =====
export async function toggleBendingField(
id: string,
field: string
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
try {
console.log('[WorkOrderActions] PATCH bending toggle request:', { field });
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/bending/toggle`,
{
method: 'PATCH',
body: JSON.stringify({ field }),
}
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] PATCH bending toggle response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '벤딩 필드 토글에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[WorkOrderActions] toggleBendingField error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 이슈 등록 =====
export async function addWorkOrderIssue(
id: string,
data: {
title: string;
description?: string;
priority?: 'low' | 'medium' | 'high';
}
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
try {
console.log('[WorkOrderActions] POST issue request:', data);
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/issues`,
{
method: 'POST',
body: JSON.stringify(data),
}
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] POST issue response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '이슈 등록에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[WorkOrderActions] addWorkOrderIssue error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 이슈 해결 =====
export async function resolveWorkOrderIssue(
workOrderId: string,
issueId: string
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
try {
console.log('[WorkOrderActions] PATCH issue resolve:', { workOrderId, issueId });
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/issues/${issueId}/resolve`,
{ method: 'PATCH' }
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] PATCH issue resolve response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '이슈 해결 처리에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[WorkOrderActions] resolveWorkOrderIssue error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 수주 목록 조회 (작업지시 생성용) =====
export interface SalesOrderForWorkOrder {
id: number;
orderNo: string;
client: string;
projectName: string;
dueDate: string;
status: string;
itemCount: number;
splitCount: number;
}
export async function getSalesOrdersForWorkOrder(params?: {
q?: string;
status?: string;
}): Promise<{
success: boolean;
data: SalesOrderForWorkOrder[];
error?: string;
}> {
try {
const searchParams = new URLSearchParams();
// 작업지시 생성 가능한 상태만 조회 (예: 회계확인 완료)
searchParams.set('for_work_order', '1');
if (params?.q) searchParams.set('q', params.q);
if (params?.status) searchParams.set('status', params.status);
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales-orders${queryString ? `?${queryString}` : ''}`;
console.log('[WorkOrderActions] GET sales-orders for work-order:', url);
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, data: [], error: error?.message || 'API 요청 실패' };
}
if (!response.ok) {
console.warn('[WorkOrderActions] GET sales-orders 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 응답 변환
const salesOrders: SalesOrderForWorkOrder[] = (result.data?.data || result.data || []).map(
(item: {
id: number;
order_no: string;
client?: { name: string };
project_name?: string;
due_date?: string;
status: string;
items_count?: number;
split_count?: number;
}) => ({
id: item.id,
orderNo: item.order_no,
client: item.client?.name || '-',
projectName: item.project_name || '-',
dueDate: item.due_date || '-',
status: item.status,
itemCount: item.items_count || 0,
splitCount: item.split_count || 0,
})
);
return {
success: true,
data: salesOrders,
};
} catch (error) {
console.error('[WorkOrderActions] getSalesOrdersForWorkOrder error:', error);
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
}
}
// ===== 부서 + 사용자 조회 (담당자 선택용) =====
export interface DepartmentUser {
id: number;
name: string;
email: string;
}
export interface DepartmentWithUsers {
id: number;
name: string;
code: string | null;
users: DepartmentUser[];
children: DepartmentWithUsers[];
}
export async function getDepartmentsWithUsers(): Promise<{
success: boolean;
data: DepartmentWithUsers[];
error?: string;
}> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments/tree?with_users=1`;
console.log('[WorkOrderActions] GET departments with users:', url);
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, data: [], error: error?.message || 'API 요청 실패' };
}
if (!response.ok) {
console.warn('[WorkOrderActions] GET departments 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 응답을 프론트엔드 형식으로 변환
const transformDepartment = (dept: {
id: number;
name: string;
code: string | null;
users?: { id: number; name: string; email: string }[];
children?: unknown[];
}): DepartmentWithUsers => ({
id: dept.id,
name: dept.name,
code: dept.code,
users: (dept.users || []).map((u) => ({
id: u.id,
name: u.name,
email: u.email,
})),
children: (dept.children || []).map((child) =>
transformDepartment(child as typeof dept)
),
});
const departments = (result.data || []).map(transformDepartment);
return {
success: true,
data: departments,
};
} catch (error) {
console.error('[WorkOrderActions] getDepartmentsWithUsers error:', error);
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
}
}