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,10 +1,11 @@
'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';
import type { LeavePolicySettings } from './types';
const API_URL = process.env.NEXT_PUBLIC_API_URL;
// ===== API 응답 타입 =====
interface LeavePolicyApi {
id: number;
@@ -40,7 +41,6 @@ function transformLeavePolicy(data: LeavePolicyApi): LeavePolicySettings {
// ===== Frontend → API 변환 =====
function transformToApi(data: Partial<LeavePolicySettings>): Record<string, unknown> {
const result: Record<string, unknown> = {};
if (data.standardType !== undefined) result.standard_type = data.standardType;
if (data.fiscalStartMonth !== undefined) result.fiscal_start_month = data.fiscalStartMonth;
if (data.fiscalStartDay !== undefined) result.fiscal_start_day = data.fiscalStartDay;
@@ -50,119 +50,27 @@ function transformToApi(data: Partial<LeavePolicySettings>): Record<string, unkn
if (data.carryOverEnabled !== undefined) result.carry_over_enabled = data.carryOverEnabled;
if (data.carryOverMaxDays !== undefined) result.carry_over_max_days = data.carryOverMaxDays;
if (data.carryOverExpiryMonths !== undefined) result.carry_over_expiry_months = data.carryOverExpiryMonths;
return result;
}
// ===== 휴가 정책 조회 =====
export async function getLeavePolicy(): Promise<{
success: boolean;
data?: LeavePolicySettings;
error?: string;
__authError?: boolean;
}> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/leave-policy`;
const { response, error } = await serverFetch(url, {
method: 'GET',
cache: 'no-store',
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response || !response.ok) {
console.warn('[LeavePolicyActions] GET error:', response?.status);
return {
success: false,
error: `API 오류: ${response?.status}`,
};
}
const result = await response.json();
if (!result.success) {
return {
success: false,
error: result.message || '휴가 정책 조회에 실패했습니다.',
};
}
const transformedData = transformLeavePolicy(result.data);
return {
success: true,
data: transformedData,
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[LeavePolicyActions] getLeavePolicy error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
}
export async function getLeavePolicy(): Promise<ActionResult<LeavePolicySettings>> {
return executeServerAction({
url: `${API_URL}/api/v1/leave-policy`,
transform: (data: LeavePolicyApi) => transformLeavePolicy(data),
errorMessage: '휴가 정책 조회에 실패했습니다.',
});
}
// ===== 휴가 정책 업데이트 =====
export async function updateLeavePolicy(data: Partial<LeavePolicySettings>): Promise<{
success: boolean;
data?: LeavePolicySettings;
error?: string;
__authError?: boolean;
}> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/leave-policy`;
const apiData = transformToApi(data);
const { response, error } = await serverFetch(url, {
method: 'PUT',
body: JSON.stringify(apiData),
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response || !response.ok) {
console.warn('[LeavePolicyActions] PUT error:', response?.status);
return {
success: false,
error: `API 오류: ${response?.status}`,
};
}
const result = await response.json();
if (!result.success) {
return {
success: false,
error: result.message || '휴가 정책 저장에 실패했습니다.',
};
}
const transformedData = transformLeavePolicy(result.data);
return {
success: true,
data: transformedData,
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[LeavePolicyActions] updateLeavePolicy error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
}
export async function updateLeavePolicy(
data: Partial<LeavePolicySettings>
): Promise<ActionResult<LeavePolicySettings>> {
return executeServerAction({
url: `${API_URL}/api/v1/leave-policy`,
method: 'PUT',
body: transformToApi(data),
transform: (data: LeavePolicyApi) => transformLeavePolicy(data),
errorMessage: '휴가 정책 저장에 실패했습니다.',
});
}