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:
@@ -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자리 영문+숫자) =====
|
||||
|
||||
Reference in New Issue
Block a user