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:
@@ -2,236 +2,52 @@
|
||||
|
||||
|
||||
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 { SubscriptionApiData, UsageApiData, SubscriptionInfo } from './types';
|
||||
import { transformApiToFrontend } from './utils';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== 현재 활성 구독 조회 =====
|
||||
export async function getCurrentSubscription(): Promise<{
|
||||
success: boolean;
|
||||
data: SubscriptionApiData | null;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/current`,
|
||||
{
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: '구독 정보를 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: result.message || '구독 정보를 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[SubscriptionActions] getCurrentSubscription error:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
export async function getCurrentSubscription(): Promise<ActionResult<SubscriptionApiData>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/subscriptions/current`,
|
||||
errorMessage: '구독 정보를 불러오는데 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 사용량 조회 =====
|
||||
export async function getUsage(): Promise<{
|
||||
success: boolean;
|
||||
data: UsageApiData | null;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/usage`,
|
||||
{
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: '사용량 정보를 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: result.message || '사용량 정보를 불러오는데 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[SubscriptionActions] getUsage error:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
export async function getUsage(): Promise<ActionResult<UsageApiData>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/subscriptions/usage`,
|
||||
errorMessage: '사용량 정보를 불러오는데 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 구독 취소 =====
|
||||
export async function cancelSubscription(
|
||||
id: number,
|
||||
reason?: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/${id}/cancel`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ reason }),
|
||||
}
|
||||
);
|
||||
|
||||
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('[SubscriptionActions] cancelSubscription error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/subscriptions/${id}/cancel`,
|
||||
method: 'POST',
|
||||
body: { reason },
|
||||
errorMessage: '구독 취소에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 데이터 내보내기 요청 =====
|
||||
export async function requestDataExport(
|
||||
exportType: string = 'all'
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data?: { id: number; status: string };
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/export`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ export_type: exportType }),
|
||||
}
|
||||
);
|
||||
|
||||
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: {
|
||||
id: result.data.id,
|
||||
status: result.data.status,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[SubscriptionActions] requestDataExport error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
): Promise<ActionResult<{ id: number; status: string }>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/subscriptions/export`,
|
||||
method: 'POST',
|
||||
body: { export_type: exportType },
|
||||
transform: (data: { id: number; status: string }) => ({ id: data.id, status: data.status }),
|
||||
errorMessage: '내보내기 요청에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 통합 데이터 조회 (현재 구독 + 사용량) =====
|
||||
@@ -255,21 +71,14 @@ export async function getSubscriptionData(): Promise<{
|
||||
}
|
||||
|
||||
const data = transformApiToFrontend(
|
||||
subscriptionResult.data,
|
||||
usageResult.data
|
||||
subscriptionResult.data ?? null,
|
||||
usageResult.data ?? null
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
};
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[SubscriptionActions] getSubscriptionData error:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, data: null, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user