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:
@@ -14,7 +14,7 @@
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
|
||||
// ============================================
|
||||
// 타입 정의
|
||||
@@ -83,17 +83,10 @@ export interface UpdateDepartmentRequest {
|
||||
parentId?: number | null; // null이면 최상위로 이동
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
// API URL
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
|
||||
|
||||
/**
|
||||
@@ -130,36 +123,18 @@ function transformApiToFrontend(apiData: ApiDepartment, depth: number = 0): Depa
|
||||
*/
|
||||
export async function getDepartmentTree(params?: {
|
||||
withUsers?: boolean;
|
||||
}): Promise<{ success: boolean; data?: DepartmentRecord[]; error?: string }> {
|
||||
}): Promise<ActionResult<DepartmentRecord[]>> {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (params?.withUsers) {
|
||||
queryParams.append('with_users', '1');
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const url = `${API_URL}/v1/departments/tree${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '부서 트리 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiDepartment[]> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
const transformed = result.data.map((dept) => transformApiToFrontend(dept, 0));
|
||||
return {
|
||||
success: true,
|
||||
data: transformed,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 트리 조회에 실패했습니다.',
|
||||
};
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/tree${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: ApiDepartment[]) => data.map((dept) => transformApiToFrontend(dept, 0)),
|
||||
errorMessage: '부서 트리 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,26 +143,12 @@ export async function getDepartmentTree(params?: {
|
||||
*/
|
||||
export async function getDepartmentById(
|
||||
id: number
|
||||
): Promise<{ success: boolean; data?: DepartmentRecord; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/departments/${id}`, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '부서 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiDepartment> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 조회에 실패했습니다.',
|
||||
};
|
||||
): Promise<ActionResult<DepartmentRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/${id}`,
|
||||
transform: (data: ApiDepartment) => transformApiToFrontend(data),
|
||||
errorMessage: '부서 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,36 +157,21 @@ export async function getDepartmentById(
|
||||
*/
|
||||
export async function createDepartment(
|
||||
data: CreateDepartmentRequest
|
||||
): Promise<{ success: boolean; data?: DepartmentRecord; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/departments`, {
|
||||
): Promise<ActionResult<DepartmentRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments`,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
body: {
|
||||
parent_id: data.parentId,
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
is_active: data.isActive !== undefined ? (data.isActive ? 1 : 0) : undefined,
|
||||
sort_order: data.sortOrder,
|
||||
}),
|
||||
},
|
||||
transform: (data: ApiDepartment) => transformApiToFrontend(data),
|
||||
errorMessage: '부서 생성에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '부서 생성에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiDepartment> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 생성에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,36 +181,21 @@ export async function createDepartment(
|
||||
export async function updateDepartment(
|
||||
id: number,
|
||||
data: UpdateDepartmentRequest
|
||||
): Promise<{ success: boolean; data?: DepartmentRecord; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/departments/${id}`, {
|
||||
): Promise<ActionResult<DepartmentRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/${id}`,
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
parent_id: data.parentId === null ? 0 : data.parentId, // null이면 0(최상위)으로 전환
|
||||
body: {
|
||||
parent_id: data.parentId === null ? 0 : data.parentId,
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
is_active: data.isActive !== undefined ? (data.isActive ? 1 : 0) : undefined,
|
||||
sort_order: data.sortOrder,
|
||||
}),
|
||||
},
|
||||
transform: (data: ApiDepartment) => transformApiToFrontend(data),
|
||||
errorMessage: '부서 수정에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '부서 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiDepartment> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,23 +204,12 @@ export async function updateDepartment(
|
||||
*/
|
||||
export async function deleteDepartment(
|
||||
id: number
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/departments/${id}`, { method: 'DELETE' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '부서 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<{ id: number; deleted_at: string }> = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 삭제에 실패했습니다.',
|
||||
};
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '부서 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user