- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일) - sanitize 유틸 추가 (XSS 방지) - middleware CSP 헤더 추가 및 Open Redirect 방지 - 프록시 라우트 로깅 개발환경 한정으로 변경 - 프로덕션 불필요 console.log 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
270 lines
8.5 KiB
TypeScript
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 };
|
|
}
|