- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일) - sanitize 유틸 추가 (XSS 방지) - middleware CSP 헤더 추가 및 Open Redirect 방지 - 프록시 라우트 로깅 개발환경 한정으로 변경 - 프로덕션 불필요 console.log 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
137 lines
4.5 KiB
TypeScript
137 lines
4.5 KiB
TypeScript
'use server';
|
|
|
|
|
|
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
|
import type { ComprehensiveAnalysisData } from './types';
|
|
|
|
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
|
|
|
// ===== API 응답 타입 =====
|
|
interface TodayIssueItemApi {
|
|
id: string;
|
|
category: string;
|
|
description: string;
|
|
requires_approval: boolean;
|
|
time: string;
|
|
}
|
|
|
|
interface TodayIssueSectionApi {
|
|
filter_options: string[];
|
|
items: TodayIssueItemApi[];
|
|
}
|
|
|
|
interface CheckPointApi {
|
|
id: string;
|
|
type: 'success' | 'warning' | 'error' | 'info';
|
|
message: string;
|
|
highlight?: string;
|
|
}
|
|
|
|
interface AmountCardApi {
|
|
id: string;
|
|
label: string;
|
|
amount: number;
|
|
sub_amount?: number;
|
|
sub_label?: string;
|
|
previous_amount?: number;
|
|
previous_label?: string;
|
|
}
|
|
|
|
interface ExpenseSectionApi {
|
|
cards: AmountCardApi[];
|
|
check_points: CheckPointApi[];
|
|
}
|
|
|
|
interface ReceivableSectionApi extends ExpenseSectionApi {
|
|
has_detail_button: boolean;
|
|
detail_button_label: string;
|
|
detail_button_path: string;
|
|
}
|
|
|
|
interface ComprehensiveAnalysisDataApi {
|
|
today_issue: TodayIssueSectionApi;
|
|
monthly_expense: ExpenseSectionApi;
|
|
card_management: ExpenseSectionApi;
|
|
entertainment: ExpenseSectionApi;
|
|
welfare: ExpenseSectionApi;
|
|
receivable: ReceivableSectionApi;
|
|
debt_collection: ExpenseSectionApi;
|
|
}
|
|
|
|
// ===== API → Frontend 변환 =====
|
|
function transformCheckPoint(item: CheckPointApi): ComprehensiveAnalysisData['monthlyExpense']['checkPoints'][0] {
|
|
return { id: item.id, type: item.type, message: item.message, highlight: item.highlight };
|
|
}
|
|
|
|
function transformAmountCard(item: AmountCardApi): ComprehensiveAnalysisData['monthlyExpense']['cards'][0] {
|
|
return {
|
|
id: item.id, label: item.label, amount: item.amount,
|
|
subAmount: item.sub_amount, subLabel: item.sub_label,
|
|
previousAmount: item.previous_amount, previousLabel: item.previous_label,
|
|
};
|
|
}
|
|
|
|
function transformTodayIssueItem(item: TodayIssueItemApi): ComprehensiveAnalysisData['todayIssue']['items'][0] {
|
|
return { id: item.id, category: item.category, description: item.description, requiresApproval: item.requires_approval, time: item.time };
|
|
}
|
|
|
|
function transformExpenseSection(section: ExpenseSectionApi): ComprehensiveAnalysisData['monthlyExpense'] {
|
|
return { cards: section.cards.map(transformAmountCard), checkPoints: section.check_points.map(transformCheckPoint) };
|
|
}
|
|
|
|
function transformReceivableSection(section: ReceivableSectionApi): ComprehensiveAnalysisData['receivable'] {
|
|
return {
|
|
cards: section.cards.map(transformAmountCard),
|
|
checkPoints: section.check_points.map(transformCheckPoint),
|
|
hasDetailButton: section.has_detail_button,
|
|
detailButtonLabel: section.detail_button_label,
|
|
detailButtonPath: section.detail_button_path,
|
|
};
|
|
}
|
|
|
|
function transformAnalysisData(data: ComprehensiveAnalysisDataApi): ComprehensiveAnalysisData {
|
|
return {
|
|
todayIssue: { filterOptions: data.today_issue.filter_options, items: data.today_issue.items.map(transformTodayIssueItem) },
|
|
monthlyExpense: transformExpenseSection(data.monthly_expense),
|
|
cardManagement: transformExpenseSection(data.card_management),
|
|
entertainment: transformExpenseSection(data.entertainment),
|
|
welfare: transformExpenseSection(data.welfare),
|
|
receivable: transformReceivableSection(data.receivable),
|
|
debtCollection: transformExpenseSection(data.debt_collection),
|
|
};
|
|
}
|
|
|
|
// ===== 종합 분석 데이터 조회 =====
|
|
export async function getComprehensiveAnalysis(params?: {
|
|
date?: string;
|
|
}): Promise<ActionResult<ComprehensiveAnalysisData>> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.date) searchParams.set('date', params.date);
|
|
const queryString = searchParams.toString();
|
|
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/comprehensive-analysis${queryString ? `?${queryString}` : ''}`,
|
|
transform: (data: ComprehensiveAnalysisDataApi) => transformAnalysisData(data),
|
|
errorMessage: '종합 분석 조회에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 이슈 승인 =====
|
|
export async function approveIssue(issueId: string): Promise<ActionResult> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/approvals/${issueId}/approve`,
|
|
method: 'POST',
|
|
errorMessage: '승인에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 이슈 반려 =====
|
|
export async function rejectIssue(issueId: string, reason?: string): Promise<ActionResult> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/approvals/${issueId}/reject`,
|
|
method: 'POST',
|
|
body: { comment: reason },
|
|
errorMessage: '반려에 실패했습니다.',
|
|
});
|
|
}
|