Files
sam-react-prod/src/components/settings/PopupManagement/actions.ts
유병철 55e0791e16 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>
2026-02-09 16:14:06 +09:00

132 lines
3.7 KiB
TypeScript

/**
* 팝업관리 서버 액션
*
* API Endpoints:
* - GET /api/v1/popups - 목록 조회
* - GET /api/v1/popups/{id} - 상세 조회
* - POST /api/v1/popups - 등록
* - PUT /api/v1/popups/{id} - 수정
* - DELETE /api/v1/popups/{id} - 삭제
*/
'use server';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
import type { Popup, PopupFormData } from './types';
import { transformApiToFrontend, transformFrontendToApi, type PopupApiData } from './utils';
const API_URL = process.env.NEXT_PUBLIC_API_URL;
// ============================================
// API 응답 타입 정의
// ============================================
interface PaginatedResponse<T> {
current_page: number;
data: T[];
total: number;
per_page: number;
last_page: number;
}
// ============================================
// API 함수
// ============================================
/**
* 팝업 목록 조회
*/
export async function getPopups(params?: {
page?: number;
size?: number;
status?: string;
}): Promise<Popup[]> {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.size) searchParams.set('size', String(params.size));
if (params?.status && params.status !== 'all') {
searchParams.set('status', params.status);
}
const result = await executeServerAction({
url: `${API_URL}/api/v1/popups?${searchParams.toString()}`,
transform: (data: PaginatedResponse<PopupApiData>) => data.data.map(transformApiToFrontend),
errorMessage: '팝업 목록 조회에 실패했습니다.',
});
return result.data || [];
}
/**
* 팝업 상세 조회
*/
export async function getPopupById(id: string): Promise<Popup | null> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/popups/${id}`,
transform: (data: PopupApiData) => transformApiToFrontend(data),
errorMessage: '팝업 조회에 실패했습니다.',
});
return result.data || null;
}
/**
* 팝업 등록
*/
export async function createPopup(
data: PopupFormData
): Promise<ActionResult<Popup>> {
return executeServerAction({
url: `${API_URL}/api/v1/popups`,
method: 'POST',
body: transformFrontendToApi(data),
transform: (data: PopupApiData) => transformApiToFrontend(data),
errorMessage: '팝업 등록에 실패했습니다.',
});
}
/**
* 팝업 수정
*/
export async function updatePopup(
id: string,
data: PopupFormData
): Promise<ActionResult<Popup>> {
return executeServerAction({
url: `${API_URL}/api/v1/popups/${id}`,
method: 'PUT',
body: transformFrontendToApi(data),
transform: (data: PopupApiData) => transformApiToFrontend(data),
errorMessage: '팝업 수정에 실패했습니다.',
});
}
/**
* 팝업 삭제
*/
export async function deletePopup(id: string): Promise<ActionResult> {
return executeServerAction({
url: `${API_URL}/api/v1/popups/${id}`,
method: 'DELETE',
errorMessage: '팝업 삭제에 실패했습니다.',
});
}
/**
* 팝업 일괄 삭제
*/
export async function deletePopups(ids: string[]): Promise<{ success: boolean; error?: string }> {
try {
const results = await Promise.all(ids.map(id => deletePopup(id)));
const failed = results.filter(r => !r.success);
if (failed.length > 0) {
return { success: false, error: `${failed.length}개 팝업 삭제에 실패했습니다.` };
}
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[PopupActions] deletePopups error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}