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,382 +1,117 @@
/**
* 동적 게시판 Server Actions
* 일반 게시판 게시글 API 호출 (/api/v1/boards/{code}/posts)
*/
'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 {
PostApiData,
PostPaginationResponse,
ApiResponse,
PostFilters,
CommentApiData,
CommentsApiResponse,
} from '@/components/customer-center/shared/types';
/**
* 게시글 목록 조회
*/
const API_URL = process.env.NEXT_PUBLIC_API_URL;
// ===== 게시글 API =====
export async function getDynamicBoardPosts(
boardCode: string,
filters?: PostFilters
): Promise<{ success: boolean; data?: PostPaginationResponse; error?: string; __authError?: boolean }> {
try {
const params = new URLSearchParams();
if (filters?.search) params.append('search', filters.search);
if (filters?.is_notice !== undefined) params.append('is_notice', String(filters.is_notice));
if (filters?.status) params.append('status', filters.status);
if (filters?.per_page) params.append('per_page', String(filters.per_page));
if (filters?.page) params.append('page', String(filters.page));
const queryString = params.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts${queryString ? `?${queryString}` : ''}`;
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) {
return { success: false, error: '게시글 목록 조회에 실패했습니다.' };
}
let result: ApiResponse<PostPaginationResponse>;
try {
result = await response.json();
} catch {
console.error('[DynamicBoardActions] JSON parse error');
return { success: false, error: '서버 응답 형식 오류입니다.' };
}
if (!response.ok || !result.success) {
return { success: false, error: result.message || '게시글 목록 조회에 실패했습니다.' };
}
return { success: true, data: result.data };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[DynamicBoardActions] getDynamicBoardPosts error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
boardCode: string, filters?: PostFilters
): Promise<ActionResult<PostPaginationResponse>> {
const params = new URLSearchParams();
if (filters?.search) params.append('search', filters.search);
if (filters?.is_notice !== undefined) params.append('is_notice', String(filters.is_notice));
if (filters?.status) params.append('status', filters.status);
if (filters?.per_page) params.append('per_page', String(filters.per_page));
if (filters?.page) params.append('page', String(filters.page));
const queryString = params.toString();
return executeServerAction<PostPaginationResponse>({
url: `${API_URL}/api/v1/boards/${boardCode}/posts${queryString ? `?${queryString}` : ''}`,
errorMessage: '게시글 목록 조회에 실패했습니다.',
});
}
/**
* 게시글 상세 조회
*/
export async function getDynamicBoardPost(
boardCode: string,
postId: number | string
): Promise<{ success: boolean; data?: PostApiData; error?: string; __authError?: boolean }> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
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) {
return { success: false, error: '게시글 조회에 실패했습니다.' };
}
const result: ApiResponse<PostApiData> = await response.json();
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '게시글을 찾을 수 없습니다.' };
}
return { success: true, data: result.data };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[DynamicBoardActions] getDynamicBoardPost error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
boardCode: string, postId: number | string
): Promise<ActionResult<PostApiData>> {
return executeServerAction<PostApiData>({
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`,
errorMessage: '게시글을 찾을 수 없습니다.',
});
}
/**
* 게시글 등록
*/
export async function createDynamicBoardPost(
boardCode: string,
data: {
title: string;
content: string;
is_secret?: boolean;
is_notice?: boolean;
custom_fields?: Record<string, string>;
}
): Promise<{ success: boolean; data?: PostApiData; error?: string; __authError?: boolean }> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts`;
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify(data),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시글 등록에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '게시글 등록에 실패했습니다.' };
}
return { success: true, data: result.data };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[DynamicBoardActions] createDynamicBoardPost error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
data: { title: string; content: string; is_secret?: boolean; is_notice?: boolean; custom_fields?: Record<string, string> }
): Promise<ActionResult<PostApiData>> {
return executeServerAction<PostApiData>({
url: `${API_URL}/api/v1/boards/${boardCode}/posts`,
method: 'POST',
body: data,
errorMessage: '게시글 등록에 실패했습니다.',
});
}
/**
* 게시글 수정
*/
export async function updateDynamicBoardPost(
boardCode: string,
postId: number | string,
data: {
title?: string;
content?: string;
is_secret?: boolean;
is_notice?: boolean;
custom_fields?: Record<string, string>;
}
): Promise<{ success: boolean; data?: PostApiData; error?: string; __authError?: boolean }> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
const { response, error } = await serverFetch(url, {
method: 'PUT',
body: JSON.stringify(data),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시글 수정에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '게시글 수정에 실패했습니다.' };
}
return { success: true, data: result.data };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[DynamicBoardActions] updateDynamicBoardPost error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
boardCode: string, postId: number | string,
data: { title?: string; content?: string; is_secret?: boolean; is_notice?: boolean; custom_fields?: Record<string, string> }
): Promise<ActionResult<PostApiData>> {
return executeServerAction<PostApiData>({
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`,
method: 'PUT',
body: data,
errorMessage: '게시글 수정에 실패했습니다.',
});
}
/**
* 게시글 삭제
*/
export async function deleteDynamicBoardPost(
boardCode: string,
postId: number | string
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
const { response, error } = await serverFetch(url, {
method: 'DELETE',
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시글 삭제에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '게시글 삭제에 실패했습니다.' };
}
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[DynamicBoardActions] deleteDynamicBoardPost error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
boardCode: string, postId: number | string
): Promise<ActionResult> {
return executeServerAction({
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`,
method: 'DELETE',
errorMessage: '게시글 삭제에 실패했습니다.',
});
}
// ===== 댓글 API =====
/**
* 댓글 목록 조회
*/
export async function getDynamicBoardComments(
boardCode: string,
postId: number | string
): Promise<{ success: boolean; data?: CommentsApiResponse; error?: string; __authError?: boolean }> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments`;
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) {
return { success: false, error: '댓글 목록 조회에 실패했습니다.' };
}
const result: ApiResponse<CommentsApiResponse> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '댓글 목록 조회에 실패했습니다.' };
}
return { success: true, data: result.data };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[DynamicBoardActions] getDynamicBoardComments error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
boardCode: string, postId: number | string
): Promise<ActionResult<CommentsApiResponse>> {
return executeServerAction<CommentsApiResponse>({
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments`,
errorMessage: '댓글 목록 조회에 실패했습니다.',
});
}
/**
* 댓글 작성
*/
export async function createDynamicBoardComment(
boardCode: string,
postId: number | string,
content: string
): Promise<{ success: boolean; data?: CommentApiData; error?: string; __authError?: boolean }> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments`;
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify({ content }),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '댓글 등록에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '댓글 등록에 실패했습니다.' };
}
return { success: true, data: result.data };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[DynamicBoardActions] createDynamicBoardComment error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
boardCode: string, postId: number | string, content: string
): Promise<ActionResult<CommentApiData>> {
return executeServerAction<CommentApiData>({
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments`,
method: 'POST',
body: { content },
errorMessage: '댓글 등록에 실패했습니다.',
});
}
/**
* 댓글 수정
*/
export async function updateDynamicBoardComment(
boardCode: string,
postId: number | string,
commentId: number | string,
content: string
): Promise<{ success: boolean; data?: CommentApiData; error?: string; __authError?: boolean }> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments/${commentId}`;
const { response, error } = await serverFetch(url, {
method: 'PUT',
body: JSON.stringify({ content }),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '댓글 수정에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '댓글 수정에 실패했습니다.' };
}
return { success: true, data: result.data };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[DynamicBoardActions] updateDynamicBoardComment error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
boardCode: string, postId: number | string, commentId: number | string, content: string
): Promise<ActionResult<CommentApiData>> {
return executeServerAction<CommentApiData>({
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments/${commentId}`,
method: 'PUT',
body: { content },
errorMessage: '댓글 수정에 실패했습니다.',
});
}
/**
* 댓글 삭제
*/
export async function deleteDynamicBoardComment(
boardCode: string,
postId: number | string,
commentId: number | string
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments/${commentId}`;
const { response, error } = await serverFetch(url, {
method: 'DELETE',
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '댓글 삭제에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '댓글 삭제에 실패했습니다.' };
}
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[DynamicBoardActions] deleteDynamicBoardComment error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
boardCode: string, postId: number | string, commentId: number | string
): Promise<ActionResult> {
return executeServerAction({
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments/${commentId}`,
method: 'DELETE',
errorMessage: '댓글 삭제에 실패했습니다.',
});
}