Files
sam-react-prod/src/components/production/WorkResults/actions.ts
유병철 55e0791e16 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>
2026-02-09 16:14:06 +09:00

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 };
}