- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일) - sanitize 유틸 추가 (XSS 방지) - middleware CSP 헤더 추가 및 Open Redirect 방지 - 프록시 라우트 로깅 개발환경 한정으로 변경 - 프로덕션 불필요 console.log 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
194 lines
7.7 KiB
TypeScript
194 lines
7.7 KiB
TypeScript
'use server';
|
|
|
|
|
|
/**
|
|
* 작업실적 관리 Server Actions
|
|
*
|
|
* API Endpoints:
|
|
* - GET /api/v1/work-results - 목록 조회
|
|
* - GET /api/v1/work-results/stats - 통계 조회
|
|
* - GET /api/v1/work-results/{id} - 상세 조회
|
|
* - POST /api/v1/work-results - 등록
|
|
* - PUT /api/v1/work-results/{id} - 수정
|
|
* - DELETE /api/v1/work-results/{id} - 삭제
|
|
* - PATCH /api/v1/work-results/{id}/inspection - 검사 상태 토글
|
|
* - PATCH /api/v1/work-results/{id}/packaging - 포장 상태 토글
|
|
*/
|
|
|
|
import { executeServerAction } from '@/lib/api/execute-server-action';
|
|
import type { ProcessType } from '../WorkOrders/types';
|
|
import type {
|
|
WorkResult,
|
|
WorkResultStats,
|
|
WorkResultApiPaginatedResponse,
|
|
WorkResultStatsApi,
|
|
} from './types';
|
|
import {
|
|
transformApiToFrontend,
|
|
transformFrontendToApi,
|
|
transformStatsApiToFrontend,
|
|
} from './types';
|
|
|
|
// ===== 페이지네이션 타입 =====
|
|
interface PaginationMeta {
|
|
currentPage: number;
|
|
lastPage: number;
|
|
perPage: number;
|
|
total: number;
|
|
}
|
|
|
|
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
|
|
|
// ===== 작업실적 목록 조회 =====
|
|
export async function getWorkResults(params?: {
|
|
page?: number; size?: number; q?: string;
|
|
processType?: ProcessType | 'all'; workOrderId?: number; workerId?: number;
|
|
workDateFrom?: string; workDateTo?: string;
|
|
isInspected?: boolean; isPackaged?: boolean;
|
|
}): Promise<{ success: boolean; data: WorkResult[]; pagination: PaginationMeta; error?: string }> {
|
|
const emptyPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.page) searchParams.set('page', String(params.page));
|
|
if (params?.size) searchParams.set('size', String(params.size));
|
|
if (params?.q) searchParams.set('q', params.q);
|
|
if (params?.processType && params.processType !== 'all') searchParams.set('process_type', params.processType);
|
|
if (params?.workOrderId) searchParams.set('work_order_id', String(params.workOrderId));
|
|
if (params?.workerId) searchParams.set('worker_id', String(params.workerId));
|
|
if (params?.workDateFrom) searchParams.set('work_date_from', params.workDateFrom);
|
|
if (params?.workDateTo) searchParams.set('work_date_to', params.workDateTo);
|
|
if (params?.isInspected !== undefined) searchParams.set('is_inspected', params.isInspected ? '1' : '0');
|
|
if (params?.isPackaged !== undefined) searchParams.set('is_packaged', params.isPackaged ? '1' : '0');
|
|
|
|
const queryString = searchParams.toString();
|
|
const result = await executeServerAction<WorkResultApiPaginatedResponse>({
|
|
url: `${API_URL}/api/v1/work-results${queryString ? `?${queryString}` : ''}`,
|
|
errorMessage: '작업실적 목록 조회에 실패했습니다.',
|
|
});
|
|
|
|
if (!result.success || !result.data) {
|
|
return { success: false, data: [], pagination: emptyPagination, error: result.error };
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: result.data.data.map(transformApiToFrontend),
|
|
pagination: {
|
|
currentPage: result.data.current_page,
|
|
lastPage: result.data.last_page,
|
|
perPage: result.data.per_page,
|
|
total: result.data.total,
|
|
},
|
|
};
|
|
}
|
|
|
|
// ===== 작업실적 통계 조회 =====
|
|
export async function getWorkResultStats(params?: {
|
|
workDateFrom?: string; workDateTo?: string; processType?: ProcessType | 'all';
|
|
}): Promise<{ success: boolean; data?: WorkResultStats; error?: string }> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.workDateFrom) searchParams.set('work_date_from', params.workDateFrom);
|
|
if (params?.workDateTo) searchParams.set('work_date_to', params.workDateTo);
|
|
if (params?.processType && params.processType !== 'all') searchParams.set('process_type', params.processType);
|
|
|
|
const queryString = searchParams.toString();
|
|
const result = await executeServerAction({
|
|
url: `${API_URL}/api/v1/work-results/stats${queryString ? `?${queryString}` : ''}`,
|
|
transform: (data: WorkResultStatsApi) => transformStatsApiToFrontend(data),
|
|
errorMessage: '통계 조회에 실패했습니다.',
|
|
});
|
|
return { success: result.success, data: result.data, error: result.error };
|
|
}
|
|
|
|
// ===== 작업실적 상세 조회 =====
|
|
export async function getWorkResultById(id: string): Promise<{
|
|
success: boolean; data?: WorkResult; error?: string;
|
|
}> {
|
|
const result = await executeServerAction({
|
|
url: `${API_URL}/api/v1/work-results/${id}`,
|
|
transform: transformApiToFrontend,
|
|
errorMessage: '작업실적 조회에 실패했습니다.',
|
|
});
|
|
return { success: result.success, data: result.data, error: result.error };
|
|
}
|
|
|
|
// ===== 작업실적 등록 =====
|
|
export async function createWorkResult(data: {
|
|
workOrderId: number; lotNo: string; workDate: string;
|
|
productName: string; productionQty: number; defectQty: number;
|
|
processType?: ProcessType; specification?: string; goodQty?: number;
|
|
workerId?: number; isInspected?: boolean; isPackaged?: boolean; memo?: string;
|
|
}): Promise<{ success: boolean; data?: WorkResult; error?: string }> {
|
|
const apiData: Record<string, unknown> = {
|
|
work_order_id: data.workOrderId, lot_no: data.lotNo, work_date: data.workDate,
|
|
product_name: data.productName, production_qty: data.productionQty, defect_qty: data.defectQty,
|
|
};
|
|
if (data.processType) apiData.process_type = data.processType;
|
|
if (data.specification) apiData.specification = data.specification;
|
|
if (data.goodQty !== undefined) apiData.good_qty = data.goodQty;
|
|
if (data.workerId) apiData.worker_id = data.workerId;
|
|
if (data.isInspected !== undefined) apiData.is_inspected = data.isInspected;
|
|
if (data.isPackaged !== undefined) apiData.is_packaged = data.isPackaged;
|
|
if (data.memo) apiData.memo = data.memo;
|
|
|
|
const result = await executeServerAction({
|
|
url: `${API_URL}/api/v1/work-results`,
|
|
method: 'POST',
|
|
body: apiData,
|
|
transform: transformApiToFrontend,
|
|
errorMessage: '작업실적 등록에 실패했습니다.',
|
|
});
|
|
return { success: result.success, data: result.data, error: result.error };
|
|
}
|
|
|
|
// ===== 작업실적 수정 =====
|
|
export async function updateWorkResult(
|
|
id: string,
|
|
data: Partial<WorkResult> & { workOrderId?: number; workerId?: number }
|
|
): Promise<{ success: boolean; data?: WorkResult; error?: string }> {
|
|
const apiData = transformFrontendToApi(data);
|
|
const result = await executeServerAction({
|
|
url: `${API_URL}/api/v1/work-results/${id}`,
|
|
method: 'PUT',
|
|
body: apiData,
|
|
transform: transformApiToFrontend,
|
|
errorMessage: '작업실적 수정에 실패했습니다.',
|
|
});
|
|
return { success: result.success, data: result.data, error: result.error };
|
|
}
|
|
|
|
// ===== 작업실적 삭제 =====
|
|
export async function deleteWorkResult(id: string): Promise<{ success: boolean; error?: string }> {
|
|
const result = await executeServerAction({
|
|
url: `${API_URL}/api/v1/work-results/${id}`,
|
|
method: 'DELETE',
|
|
errorMessage: '작업실적 삭제에 실패했습니다.',
|
|
});
|
|
return { success: result.success, error: result.error };
|
|
}
|
|
|
|
// ===== 검사 상태 토글 =====
|
|
export async function toggleInspection(id: string): Promise<{
|
|
success: boolean; data?: WorkResult; error?: string;
|
|
}> {
|
|
const result = await executeServerAction({
|
|
url: `${API_URL}/api/v1/work-results/${id}/inspection`,
|
|
method: 'PATCH',
|
|
transform: transformApiToFrontend,
|
|
errorMessage: '검사 상태 변경에 실패했습니다.',
|
|
});
|
|
return { success: result.success, data: result.data, error: result.error };
|
|
}
|
|
|
|
// ===== 포장 상태 토글 =====
|
|
export async function togglePackaging(id: string): Promise<{
|
|
success: boolean; data?: WorkResult; error?: string;
|
|
}> {
|
|
const result = await executeServerAction({
|
|
url: `${API_URL}/api/v1/work-results/${id}/packaging`,
|
|
method: 'PATCH',
|
|
transform: transformApiToFrontend,
|
|
errorMessage: '포장 상태 변경에 실패했습니다.',
|
|
});
|
|
return { success: result.success, data: result.data, error: result.error };
|
|
}
|