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>
This commit is contained in:
유병철
2026-02-09 16:14:06 +09:00
parent d014227e9c
commit 55e0791e16
85 changed files with 7211 additions and 17638 deletions

View File

@@ -1,8 +1,7 @@
'use server';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://sam.kr:8080';
@@ -49,7 +48,7 @@ function transformFromApi(data: ApiWorkSetting): WorkSettingFormData {
return {
workType: data.work_type,
workDays: data.work_days || ['mon', 'tue', 'wed', 'thu', 'fri'],
workStartTime: data.start_time?.substring(0, 5) || '09:00', // HH:mm:ss → HH:mm
workStartTime: data.start_time?.substring(0, 5) || '09:00',
workEndTime: data.end_time?.substring(0, 5) || '18:00',
weeklyWorkHours: data.standard_hours,
weeklyOvertimeHours: data.overtime_hours,
@@ -65,10 +64,9 @@ function transformFromApi(data: ApiWorkSetting): WorkSettingFormData {
*/
function transformToApi(data: Partial<WorkSettingFormData>): Record<string, unknown> {
const apiData: Record<string, unknown> = {};
if (data.workType !== undefined) apiData.work_type = data.workType;
if (data.workDays !== undefined) apiData.work_days = data.workDays;
if (data.workStartTime !== undefined) apiData.start_time = `${data.workStartTime}:00`; // HH:mm → HH:mm:ss
if (data.workStartTime !== undefined) apiData.start_time = `${data.workStartTime}:00`;
if (data.workEndTime !== undefined) apiData.end_time = `${data.workEndTime}:00`;
if (data.weeklyWorkHours !== undefined) apiData.standard_hours = data.weeklyWorkHours;
if (data.weeklyOvertimeHours !== undefined) apiData.overtime_hours = data.weeklyOvertimeHours;
@@ -76,7 +74,6 @@ function transformToApi(data: Partial<WorkSettingFormData>): Record<string, unkn
if (data.breakMinutes !== undefined) apiData.break_minutes = data.breakMinutes;
if (data.breakStartTime !== undefined) apiData.break_start = `${data.breakStartTime}:00`;
if (data.breakEndTime !== undefined) apiData.break_end = `${data.breakEndTime}:00`;
return apiData;
}
@@ -85,48 +82,12 @@ function transformToApi(data: Partial<WorkSettingFormData>): Record<string, unkn
/**
* 근무 설정 조회
*/
export async function getWorkSetting(): Promise<{
success: boolean;
data?: WorkSettingFormData;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(`${API_BASE_URL}/api/v1/settings/work`, {
method: 'GET',
cache: 'no-store',
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response || !response.ok) {
const errorData = await response?.json().catch(() => ({}));
return {
success: false,
error: errorData?.message || `API 오류: ${response?.status}`,
};
}
const result = await response.json();
return {
success: true,
data: transformFromApi(result.data),
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('getWorkSetting error:', error);
return {
success: false,
error: '근무 설정을 불러오는데 실패했습니다.',
};
}
export async function getWorkSetting(): Promise<ActionResult<WorkSettingFormData>> {
return executeServerAction({
url: `${API_BASE_URL}/api/v1/settings/work`,
transform: (data: ApiWorkSetting) => transformFromApi(data),
errorMessage: '근무 설정을 불러오는데 실패했습니다.',
});
}
/**
@@ -134,46 +95,12 @@ export async function getWorkSetting(): Promise<{
*/
export async function updateWorkSetting(
data: Partial<WorkSettingFormData>
): Promise<{
success: boolean;
data?: WorkSettingFormData;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(`${API_BASE_URL}/api/v1/settings/work`, {
method: 'PUT',
body: JSON.stringify(transformToApi(data)),
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response || !response.ok) {
const errorData = await response?.json().catch(() => ({}));
return {
success: false,
error: errorData?.message || `API 오류: ${response?.status}`,
};
}
const result = await response.json();
return {
success: true,
data: transformFromApi(result.data),
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('updateWorkSetting error:', error);
return {
success: false,
error: '근무 설정 저장에 실패했습니다.',
};
}
): Promise<ActionResult<WorkSettingFormData>> {
return executeServerAction({
url: `${API_BASE_URL}/api/v1/settings/work`,
method: 'PUT',
body: transformToApi(data),
transform: (data: ApiWorkSetting) => transformFromApi(data),
errorMessage: '근무 설정 저장에 실패했습니다.',
});
}