Files
sam-react-prod/src/components/quality/PerformanceReportManagement/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

270 lines
8.5 KiB
TypeScript

'use server';
/**
* 실적신고관리 Server Actions
*
* API Endpoints:
* - GET /api/v1/performance-reports - 분기별 실적신고 목록
* - GET /api/v1/performance-reports/stats - 통계
* - GET /api/v1/performance-reports/missed - 누락체크 목록
* - PATCH /api/v1/performance-reports/confirm - 선택 확정
* - PATCH /api/v1/performance-reports/unconfirm - 확정 해제
* - POST /api/v1/performance-reports/distribute - 배포
* - PATCH /api/v1/performance-reports/memo - 메모 일괄 적용
*/
import { executeServerAction } from '@/lib/api/execute-server-action';
import type {
PerformanceReport,
PerformanceReportStats,
MissedReport,
Quarter,
} from './types';
import {
mockPerformanceReports,
mockPerformanceReportStats,
mockMissedReports,
} from './mockData';
// 개발환경 Mock 데이터 fallback 플래그
const USE_MOCK_FALLBACK = true;
// ===== 페이지네이션 =====
interface PaginationMeta {
currentPage: number;
lastPage: number;
perPage: number;
total: number;
}
const API_BASE = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/performance-reports`;
// ===== 분기별 실적신고 목록 조회 =====
export async function getPerformanceReports(params?: {
page?: number;
size?: number;
q?: string;
year?: number;
quarter?: Quarter | '전체';
}): Promise<{
success: boolean;
data: PerformanceReport[];
pagination: PaginationMeta;
error?: string;
__authError?: boolean;
}> {
const defaultPagination = { 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('per_page', String(params.size));
if (params?.q) searchParams.set('q', params.q);
if (params?.year) searchParams.set('year', String(params.year));
if (params?.quarter && params.quarter !== '전체') {
searchParams.set('quarter', params.quarter);
}
const queryString = searchParams.toString();
interface ApiListData { items?: PerformanceReport[]; current_page?: number; last_page?: number; per_page?: number; total?: number }
const result = await executeServerAction<ApiListData>({
url: `${API_BASE}${queryString ? `?${queryString}` : ''}`,
errorMessage: '실적신고 목록 조회에 실패했습니다.',
});
if (!result.success) {
if (USE_MOCK_FALLBACK) {
let filtered = [...mockPerformanceReports];
if (params?.year) filtered = filtered.filter(i => i.year === params.year);
if (params?.quarter && params.quarter !== '전체') filtered = filtered.filter(i => i.quarter === params.quarter);
if (params?.q) {
const q = params.q.toLowerCase();
filtered = filtered.filter(i =>
i.siteName.toLowerCase().includes(q) || i.client.toLowerCase().includes(q) || i.qualityDocNumber.toLowerCase().includes(q)
);
}
const page = params?.page || 1;
const size = params?.size || 20;
const start = (page - 1) * size;
return {
success: true,
data: filtered.slice(start, start + size),
pagination: { currentPage: page, lastPage: Math.ceil(filtered.length / size), perPage: size, total: filtered.length },
};
}
return { success: false, data: [], pagination: defaultPagination, error: result.error, __authError: result.__authError };
}
const d = result.data;
return {
success: true,
data: d?.items || [],
pagination: {
currentPage: d?.current_page || 1,
lastPage: d?.last_page || 1,
perPage: d?.per_page || 20,
total: d?.total || 0,
},
};
}
// ===== 통계 조회 =====
export async function getPerformanceReportStats(params?: {
year?: number;
quarter?: Quarter | '전체';
}): Promise<{
success: boolean;
data?: PerformanceReportStats;
error?: string;
__authError?: boolean;
}> {
const searchParams = new URLSearchParams();
if (params?.year) searchParams.set('year', String(params.year));
if (params?.quarter && params.quarter !== '전체') {
searchParams.set('quarter', params.quarter);
}
const queryString = searchParams.toString();
const result = await executeServerAction<PerformanceReportStats>({
url: `${API_BASE}/stats${queryString ? `?${queryString}` : ''}`,
errorMessage: '실적신고 통계 조회에 실패했습니다.',
});
if (!result.success) {
if (USE_MOCK_FALLBACK) return { success: true, data: mockPerformanceReportStats };
return { success: false, error: result.error, __authError: result.__authError };
}
return { success: true, data: result.data };
}
// ===== 누락체크 목록 조회 =====
export async function getMissedReports(params?: {
page?: number;
size?: number;
q?: string;
}): Promise<{
success: boolean;
data: MissedReport[];
pagination: PaginationMeta;
error?: string;
__authError?: boolean;
}> {
const defaultPagination = { 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('per_page', String(params.size));
if (params?.q) searchParams.set('q', params.q);
const queryString = searchParams.toString();
interface ApiMissedData { items?: MissedReport[]; current_page?: number; last_page?: number; per_page?: number; total?: number }
const result = await executeServerAction<ApiMissedData>({
url: `${API_BASE}/missed${queryString ? `?${queryString}` : ''}`,
errorMessage: '누락체크 목록 조회에 실패했습니다.',
});
if (!result.success) {
if (USE_MOCK_FALLBACK) {
let filtered = [...mockMissedReports];
if (params?.q) {
const q = params.q.toLowerCase();
filtered = filtered.filter(i =>
i.siteName.toLowerCase().includes(q) || i.client.toLowerCase().includes(q) || i.qualityDocNumber.toLowerCase().includes(q)
);
}
const page = params?.page || 1;
const size = params?.size || 20;
const start = (page - 1) * size;
return {
success: true,
data: filtered.slice(start, start + size),
pagination: { currentPage: page, lastPage: Math.ceil(filtered.length / size), perPage: size, total: filtered.length },
};
}
return { success: false, data: [], pagination: defaultPagination, error: result.error, __authError: result.__authError };
}
const d = result.data;
return {
success: true,
data: d?.items || [],
pagination: {
currentPage: d?.current_page || 1,
lastPage: d?.last_page || 1,
perPage: d?.per_page || 20,
total: d?.total || 0,
},
};
}
// ===== 선택 확정 =====
export async function confirmReports(ids: string[]): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
const result = await executeServerAction({
url: `${API_BASE}/confirm`,
method: 'PATCH',
body: { ids },
errorMessage: '확정 처리에 실패했습니다.',
});
if (!result.success && USE_MOCK_FALLBACK) return { success: true };
return { success: result.success, error: result.error, __authError: result.__authError };
}
// ===== 확정 해제 =====
export async function unconfirmReports(ids: string[]): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
const result = await executeServerAction({
url: `${API_BASE}/unconfirm`,
method: 'PATCH',
body: { ids },
errorMessage: '확정 해제에 실패했습니다.',
});
if (!result.success && USE_MOCK_FALLBACK) return { success: true };
return { success: result.success, error: result.error, __authError: result.__authError };
}
// ===== 배포 =====
export async function distributeReports(ids: string[]): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
const result = await executeServerAction({
url: `${API_BASE}/distribute`,
method: 'POST',
body: { ids },
errorMessage: '배포에 실패했습니다.',
});
if (!result.success && USE_MOCK_FALLBACK) return { success: true };
return { success: result.success, error: result.error, __authError: result.__authError };
}
// ===== 메모 일괄 적용 =====
export async function updateMemo(ids: string[], memo: string): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
const result = await executeServerAction({
url: `${API_BASE}/memo`,
method: 'PATCH',
body: { ids, memo },
errorMessage: '메모 저장에 실패했습니다.',
});
if (!result.success && USE_MOCK_FALLBACK) return { success: true };
return { success: result.success, error: result.error, __authError: result.__authError };
}