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

@@ -8,12 +8,9 @@
* - POST /api/v1/clients - 등록
* - PUT /api/v1/clients/{id} - 수정
* - DELETE /api/v1/clients/{id} - 삭제
*
* 🚨 401 에러 시 __authError: true 반환 → 클라이언트에서 로그인 페이지로 리다이렉트
*/
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 { Client, ClientFormData, ClientApiResponse } from '@/hooks/useClientList';
import {
transformClientFromApi,
@@ -21,229 +18,51 @@ import {
transformClientToApiUpdate,
} from '@/hooks/useClientList';
// ===== 응답 타입 =====
interface ActionResponse<T = unknown> {
success: boolean;
data?: T;
error?: string;
__authError?: boolean;
}
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
const API_URL = process.env.NEXT_PUBLIC_API_URL;
// ===== 거래처 단건 조회 =====
export async function getClientById(id: string): Promise<ActionResponse<Client>> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`;
console.log('[ClientActions] GET client URL:', url);
const { response, error } = await serverFetch(url, { method: 'GET' });
// 🚨 401 인증 에러
if (error?.__authError) {
console.error('[ClientActions] Auth error:', error);
return { success: false, __authError: true };
}
if (!response) {
console.error('[ClientActions] No response, error:', error);
return {
success: false,
error: error?.message || '서버 응답이 없습니다.',
};
}
// 응답 텍스트 먼저 읽기
const responseText = await response.text();
console.log('[ClientActions] Response status:', response.status);
console.log('[ClientActions] Response text:', responseText);
if (!response.ok) {
console.error('[ClientActions] GET client error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
}
// JSON 파싱
let result: ApiResponse<ClientApiResponse>;
try {
result = JSON.parse(responseText);
} catch {
console.error('[ClientActions] JSON parse error');
return {
success: false,
error: 'JSON 파싱 오류',
};
}
if (!result.success || !result.data) {
console.error('[ClientActions] API returned error:', result);
return {
success: false,
error: result.message || '거래처를 찾을 수 없습니다.',
};
}
return {
success: true,
data: transformClientFromApi(result.data),
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ClientActions] getClientById error:', error);
return {
success: false,
error: error instanceof Error ? error.message : '거래처 조회 중 오류가 발생했습니다.',
};
}
export async function getClientById(id: string): Promise<ActionResult<Client>> {
return executeServerAction({
url: `${API_URL}/api/v1/clients/${id}`,
transform: (data: ClientApiResponse) => transformClientFromApi(data),
errorMessage: '거래처 조회 중 오류가 발생했습니다.',
});
}
// ===== 거래처 생성 =====
export async function createClient(
formData: Partial<ClientFormData>
): Promise<ActionResponse<Client>> {
try {
const apiData = transformClientToApiCreate(formData as ClientFormData);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients`;
console.log('[ClientActions] POST client request:', apiData);
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify(apiData),
});
// 🚨 401 인증 에러
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return {
success: false,
error: error?.message || '서버 오류가 발생했습니다.',
};
}
const result: ApiResponse<ClientApiResponse> = await response.json();
console.log('[ClientActions] POST client response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '거래처 생성에 실패했습니다.',
};
}
return {
success: true,
data: transformClientFromApi(result.data),
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ClientActions] createClient error:', error);
return {
success: false,
error: error instanceof Error ? error.message : '거래처 생성 중 오류가 발생했습니다.',
};
}
): Promise<ActionResult<Client>> {
return executeServerAction({
url: `${API_URL}/api/v1/clients`,
method: 'POST',
body: transformClientToApiCreate(formData as ClientFormData),
transform: (data: ClientApiResponse) => transformClientFromApi(data),
errorMessage: '거래처 생성에 실패했습니다.',
});
}
// ===== 거래처 수정 =====
export async function updateClient(
id: string,
formData: Partial<ClientFormData>
): Promise<ActionResponse<Client>> {
try {
const apiData = transformClientToApiUpdate(formData as ClientFormData);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`;
console.log('[ClientActions] PUT client request:', apiData);
const { response, error } = await serverFetch(url, {
method: 'PUT',
body: JSON.stringify(apiData),
});
// 🚨 401 인증 에러
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return {
success: false,
error: error?.message || '서버 오류가 발생했습니다.',
};
}
const result: ApiResponse<ClientApiResponse> = await response.json();
console.log('[ClientActions] PUT client response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '거래처 수정에 실패했습니다.',
};
}
return {
success: true,
data: transformClientFromApi(result.data),
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ClientActions] updateClient error:', error);
return {
success: false,
error: error instanceof Error ? error.message : '거래처 수정 중 오류가 발생했습니다.',
};
}
): Promise<ActionResult<Client>> {
return executeServerAction({
url: `${API_URL}/api/v1/clients/${id}`,
method: 'PUT',
body: transformClientToApiUpdate(formData as ClientFormData),
transform: (data: ClientApiResponse) => transformClientFromApi(data),
errorMessage: '거래처 수정에 실패했습니다.',
});
}
// ===== 거래처 삭제 =====
export async function deleteClient(id: string): Promise<ActionResponse> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`;
const { response, error } = await serverFetch(url, { method: 'DELETE' });
// 🚨 401 인증 에러
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return {
success: false,
error: error?.message || '서버 오류가 발생했습니다.',
};
}
const result = await response.json();
console.log('[ClientActions] DELETE client response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '거래처 삭제에 실패했습니다.',
};
}
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ClientActions] deleteClient error:', error);
return {
success: false,
error: error instanceof Error ? error.message : '거래처 삭제 중 오류가 발생했습니다.',
};
}
export async function deleteClient(id: string): Promise<ActionResult> {
return executeServerAction({
url: `${API_URL}/api/v1/clients/${id}`,
method: 'DELETE',
errorMessage: '거래처 삭제에 실패했습니다.',
});
}
// ===== 거래처 코드 생성 (8자리 영문+숫자) =====