refactor(WEB): 전체 actions.ts에 공통 API 유틸 적용

- buildApiUrl / executePaginatedAction 패턴으로 전환 (40+ actions 파일)
- 직접 URLSearchParams 조립 → buildApiUrl 유틸 사용
- 수동 페이지네이션 메타 변환 → executePaginatedAction 자동 처리
- HandoverReportDocumentModal, OrderDocumentModal 개선
- 급여관리 SalaryManagement 코드 개선
- CLAUDE.md Server Action 공통 유틸 규칙 정리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-12 20:59:59 +09:00
parent 31be9d4a25
commit cbb38d48b9
51 changed files with 1050 additions and 1405 deletions

View File

@@ -57,7 +57,7 @@ import { formatAmount, formatAmountManwon } from '@/utils/formatAmount';
import type { Quote, QuoteFilterType } from './types';
import { PRODUCT_CATEGORY_LABELS } from './types';
import { getQuotes, deleteQuote, bulkDeleteQuotes } from './actions';
import type { PaginationMeta } from './actions';
import type { PaginationMeta } from '@/lib/api/types';
// ===== Props 타입 =====
interface QuoteManagementClientProps {

View File

@@ -23,68 +23,35 @@
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import { executeServerAction } from '@/lib/api/execute-server-action';
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
import { buildApiUrl } from '@/lib/api/query-params';
import type {
Quote,
QuoteApiData,
QuoteApiPaginatedResponse,
QuoteListParams,
QuoteStatus,
ProductCategory,
BomCalculationResult,
} from './types';
import { transformApiToFrontend, transformFrontendToApi } from './types';
// ===== 페이지네이션 타입 =====
export interface PaginationMeta {
currentPage: number;
lastPage: number;
perPage: number;
total: number;
}
const API_URL = process.env.NEXT_PUBLIC_API_URL;
// ===== 견적 목록 조회 =====
export async function getQuotes(params?: QuoteListParams): Promise<{
success: boolean;
data: Quote[];
pagination: PaginationMeta;
error?: string;
__authError?: boolean;
}> {
const emptyPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('size', String(params.perPage));
if (params?.search) searchParams.set('q', params.search);
if (params?.status) searchParams.set('status', params.status);
if (params?.productCategory) searchParams.set('product_category', params.productCategory);
if (params?.clientId) searchParams.set('client_id', params.clientId);
if (params?.dateFrom) searchParams.set('date_from', params.dateFrom);
if (params?.dateTo) searchParams.set('date_to', params.dateTo);
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
if (params?.sortOrder) searchParams.set('sort_order', params.sortOrder);
const queryString = searchParams.toString();
const result = await executeServerAction<QuoteApiPaginatedResponse>({
url: `${API_URL}/api/v1/quotes${queryString ? `?${queryString}` : ''}`,
export async function getQuotes(params?: QuoteListParams) {
return executePaginatedAction<QuoteApiData, Quote>({
url: buildApiUrl('/api/v1/quotes', {
page: params?.page,
size: params?.perPage,
q: params?.search,
status: params?.status,
product_category: params?.productCategory,
client_id: params?.clientId,
date_from: params?.dateFrom,
date_to: params?.dateTo,
sort_by: params?.sortBy,
sort_order: params?.sortOrder,
}),
transform: transformApiToFrontend,
errorMessage: '견적 목록 조회에 실패했습니다.',
});
if (result.__authError) return { success: false, data: [], pagination: emptyPagination, __authError: true };
if (!result.success || !result.data) return { success: false, data: [], pagination: emptyPagination, error: result.error };
const paginatedData = result.data;
return {
success: true,
data: (paginatedData.data || []).map(transformApiToFrontend),
pagination: {
currentPage: paginatedData.current_page,
lastPage: paginatedData.last_page,
perPage: paginatedData.per_page,
total: paginatedData.total,
},
};
}
// ===== 견적 상세 조회 =====
@@ -95,7 +62,7 @@ export async function getQuoteById(id: string): Promise<{
__authError?: boolean;
}> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/quotes/${id}`,
url: buildApiUrl(`/api/v1/quotes/${id}`),
transform: (data: QuoteApiData) => transformApiToFrontend(data),
errorMessage: '견적 조회에 실패했습니다.',
});
@@ -108,7 +75,7 @@ export async function createQuote(
data: Record<string, unknown>
): Promise<{ success: boolean; data?: Quote; error?: string; __authError?: boolean }> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/quotes`,
url: buildApiUrl('/api/v1/quotes'),
method: 'POST',
body: data,
transform: (d: QuoteApiData) => transformApiToFrontend(d),
@@ -124,7 +91,7 @@ export async function updateQuote(
data: Record<string, unknown>
): Promise<{ success: boolean; data?: Quote; error?: string; __authError?: boolean }> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/quotes/${id}`,
url: buildApiUrl(`/api/v1/quotes/${id}`),
method: 'PUT',
body: data,
transform: (d: QuoteApiData) => transformApiToFrontend(d),
@@ -137,7 +104,7 @@ export async function updateQuote(
// ===== 견적 삭제 =====
export async function deleteQuote(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/quotes/${id}`,
url: buildApiUrl(`/api/v1/quotes/${id}`),
method: 'DELETE',
errorMessage: '견적 삭제에 실패했습니다.',
});
@@ -148,7 +115,7 @@ export async function deleteQuote(id: string): Promise<{ success: boolean; error
// ===== 견적 일괄 삭제 =====
export async function bulkDeleteQuotes(ids: string[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/quotes/bulk`,
url: buildApiUrl('/api/v1/quotes/bulk'),
method: 'DELETE',
body: { ids: ids.map(id => parseInt(id, 10)) },
errorMessage: '견적 일괄 삭제에 실패했습니다.',
@@ -165,7 +132,7 @@ export async function finalizeQuote(id: string): Promise<{
__authError?: boolean;
}> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/quotes/${id}/finalize`,
url: buildApiUrl(`/api/v1/quotes/${id}/finalize`),
method: 'POST',
transform: (d: QuoteApiData) => transformApiToFrontend(d),
errorMessage: '견적 확정에 실패했습니다.',
@@ -182,7 +149,7 @@ export async function cancelFinalizeQuote(id: string): Promise<{
__authError?: boolean;
}> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/quotes/${id}/cancel-finalize`,
url: buildApiUrl(`/api/v1/quotes/${id}/cancel-finalize`),
method: 'POST',
transform: (d: QuoteApiData) => transformApiToFrontend(d),
errorMessage: '견적 확정 취소에 실패했습니다.',
@@ -204,7 +171,7 @@ export async function convertQuoteToOrder(id: string): Promise<{
order?: { id: number };
}
const result = await executeServerAction<ConvertResponse>({
url: `${API_URL}/api/v1/quotes/${id}/convert`,
url: buildApiUrl(`/api/v1/quotes/${id}/convert`),
method: 'POST',
errorMessage: '수주 전환에 실패했습니다.',
});
@@ -227,7 +194,7 @@ export async function getQuoteNumberPreview(): Promise<{
}> {
interface PreviewResponse { quote_number?: string }
const result = await executeServerAction<PreviewResponse | string>({
url: `${API_URL}/api/v1/quotes/number/preview`,
url: buildApiUrl('/api/v1/quotes/number/preview'),
errorMessage: '견적번호 미리보기에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
@@ -246,7 +213,7 @@ export async function generateQuotePdf(id: string): Promise<{
__authError?: boolean;
}> {
try {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/pdf`;
const url = buildApiUrl(`/api/v1/quotes/${id}/pdf`);
const { response, error } = await serverFetch(url, {
method: 'POST',
@@ -296,7 +263,7 @@ export async function sendQuoteEmail(
emailData: { email: string; subject?: string; message?: string }
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/quotes/${id}/send/email`,
url: buildApiUrl(`/api/v1/quotes/${id}/send/email`),
method: 'POST',
body: emailData,
errorMessage: '이메일 발송에 실패했습니다.',
@@ -311,7 +278,7 @@ export async function sendQuoteKakao(
kakaoData: { phone: string; templateId?: string }
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/quotes/${id}/send/kakao`,
url: buildApiUrl(`/api/v1/quotes/${id}/send/kakao`),
method: 'POST',
body: kakaoData,
errorMessage: '카카오 발송에 실패했습니다.',
@@ -338,15 +305,14 @@ export async function getFinishedGoods(category?: string): Promise<{
error?: string;
__authError?: boolean;
}> {
const searchParams = new URLSearchParams();
searchParams.set('item_type', 'FG');
searchParams.set('has_bom', '1');
if (category) searchParams.set('item_category', category);
searchParams.set('size', '5000');
interface FGApiResponse { data?: Record<string, unknown>[] }
const result = await executeServerAction<FGApiResponse | Record<string, unknown>[]>({
url: `${API_URL}/api/v1/items?${searchParams.toString()}`,
url: buildApiUrl('/api/v1/items', {
item_type: 'FG',
has_bom: '1',
item_category: category,
size: '5000',
}),
errorMessage: '완제품 목록 조회에 실패했습니다.',
});
@@ -414,7 +380,7 @@ export async function calculateBomBulk(items: BomCalculateItem[], debug: boolean
__authError?: boolean;
}> {
const result = await executeServerAction<BomBulkResponse>({
url: `${API_URL}/api/v1/quotes/calculate/bom/bulk`,
url: buildApiUrl('/api/v1/quotes/calculate/bom/bulk'),
method: 'POST',
body: { items, debug },
errorMessage: 'BOM 계산에 실패했습니다.',
@@ -436,7 +402,7 @@ export async function getItemPrices(itemCodes: string[]): Promise<{
__authError?: boolean;
}> {
const result = await executeServerAction<Record<string, ItemPriceResult>>({
url: `${API_URL}/api/v1/quotes/items/prices`,
url: buildApiUrl('/api/v1/quotes/items/prices'),
method: 'POST',
body: { item_codes: itemCodes },
errorMessage: '단가 조회에 실패했습니다.',
@@ -534,7 +500,7 @@ export async function getQuoteReferenceData(): Promise<{
}> {
interface RefApiData { site_names?: string[]; location_codes?: string[] }
const result = await executeServerAction<RefApiData>({
url: `${API_URL}/api/v1/quotes/reference-data`,
url: buildApiUrl('/api/v1/quotes/reference-data'),
errorMessage: '참조 데이터 조회에 실패했습니다.',
});
const empty: QuoteReferenceData = { siteNames: [], locationCodes: [] };
@@ -566,7 +532,7 @@ export async function getItemCategoryTree(): Promise<{
__authError?: boolean;
}> {
const result = await executeServerAction<ItemCategoryNode[]>({
url: `${API_URL}/api/v1/categories/tree?code_group=item_category&only_active=true`,
url: buildApiUrl('/api/v1/categories/tree', { code_group: 'item_category', only_active: true }),
errorMessage: '카테고리 조회에 실패했습니다.',
});
if (result.__authError) return { success: false, data: [], __authError: true };