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

@@ -18,11 +18,10 @@ const USE_MOCK_DATA = false;
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { PaginatedApiResponse } from '@/lib/api/types';
import { buildApiUrl } from '@/lib/api/query-params';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { executeServerAction } from '@/lib/api/execute-server-action';
const API_URL = process.env.NEXT_PUBLIC_API_URL;
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
import type {
ReceivingItem,
@@ -361,8 +360,6 @@ interface ReceivingApiData {
has_inspection_template?: boolean;
}
type ReceivingApiPaginatedResponse = PaginatedApiResponse<ReceivingApiData>;
interface ReceivingApiStatsResponse {
receiving_pending_count: number;
shipping_count: number;
@@ -513,14 +510,6 @@ function transformProcessDataToApi(
};
}
// ===== 페이지네이션 타입 =====
interface PaginationMeta {
currentPage: number;
lastPage: number;
perPage: number;
total: number;
}
// ===== 입고 목록 조회 =====
export async function getReceivings(params?: {
page?: number;
@@ -529,13 +518,7 @@ export async function getReceivings(params?: {
endDate?: string;
status?: string;
search?: string;
}): Promise<{
success: boolean;
data: ReceivingItem[];
pagination: PaginationMeta;
error?: string;
__authError?: boolean;
}> {
}) {
// ===== 목데이터 모드 =====
if (USE_MOCK_DATA) {
let filteredData = [...MOCK_RECEIVING_LIST];
@@ -565,7 +548,7 @@ export async function getReceivings(params?: {
const paginatedData = filteredData.slice(startIndex, startIndex + perPage);
return {
success: true,
success: true as const,
data: paginatedData,
pagination: {
currentPage: page,
@@ -576,29 +559,18 @@ export async function getReceivings(params?: {
};
}
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
if (params?.search) searchParams.set('search', params.search);
const queryString = searchParams.toString();
const emptyPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
const result = await executeServerAction<ReceivingApiPaginatedResponse>({
url: `${API_URL}/api/v1/receivings${queryString ? `?${queryString}` : ''}`,
return executePaginatedAction<ReceivingApiData, ReceivingItem>({
url: buildApiUrl('/api/v1/receivings', {
page: params?.page,
per_page: params?.perPage,
start_date: params?.startDate,
end_date: params?.endDate,
status: params?.status && params.status !== 'all' ? params.status : undefined,
search: params?.search,
}),
transform: transformApiToListItem,
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 pd = result.data;
return {
success: true,
data: (pd.data || []).map(transformApiToListItem),
pagination: { currentPage: pd.current_page, lastPage: pd.last_page, perPage: pd.per_page, total: pd.total },
};
}
// ===== 입고 통계 조회 =====
@@ -611,7 +583,7 @@ export async function getReceivingStats(): Promise<{
if (USE_MOCK_DATA) return { success: true, data: MOCK_RECEIVING_STATS };
const result = await executeServerAction({
url: `${API_URL}/api/v1/receivings/stats`,
url: buildApiUrl('/api/v1/receivings/stats'),
transform: (data: ReceivingApiStatsResponse) => transformApiToStats(data),
errorMessage: '입고 통계 조회에 실패했습니다.',
});
@@ -632,7 +604,7 @@ export async function getReceivingById(id: string): Promise<{
}
const result = await executeServerAction({
url: `${API_URL}/api/v1/receivings/${id}`,
url: buildApiUrl(`/api/v1/receivings/${id}`),
transform: (data: ReceivingApiData) => transformApiToDetail(data),
errorMessage: '입고 조회에 실패했습니다.',
});
@@ -646,7 +618,7 @@ export async function createReceiving(
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
const apiData = transformFrontendToApi(data);
const result = await executeServerAction({
url: `${API_URL}/api/v1/receivings`,
url: buildApiUrl('/api/v1/receivings'),
method: 'POST',
body: apiData,
transform: (d: ReceivingApiData) => transformApiToDetail(d),
@@ -663,7 +635,7 @@ export async function updateReceiving(
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
const apiData = transformFrontendToApi(data);
const result = await executeServerAction({
url: `${API_URL}/api/v1/receivings/${id}`,
url: buildApiUrl(`/api/v1/receivings/${id}`),
method: 'PUT',
body: apiData,
transform: (d: ReceivingApiData) => transformApiToDetail(d),
@@ -678,7 +650,7 @@ export async function deleteReceiving(
id: string
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/receivings/${id}`,
url: buildApiUrl(`/api/v1/receivings/${id}`),
method: 'DELETE',
errorMessage: '입고 삭제에 실패했습니다.',
});
@@ -693,7 +665,7 @@ export async function processReceiving(
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
const apiData = transformProcessDataToApi(data);
const result = await executeServerAction({
url: `${API_URL}/api/v1/receivings/${id}/process`,
url: buildApiUrl(`/api/v1/receivings/${id}/process`),
method: 'POST',
body: apiData,
transform: (d: ReceivingApiData) => transformApiToDetail(d),
@@ -739,13 +711,9 @@ export async function searchItems(query?: string): Promise<{
return { success: true, data: filtered };
}
const searchParams = new URLSearchParams();
if (query) searchParams.set('search', query);
searchParams.set('per_page', '50');
interface ItemApiData { data: Array<Record<string, string>> }
const result = await executeServerAction<ItemApiData, ItemOption[]>({
url: `${API_URL}/api/v1/items?${searchParams.toString()}`,
url: buildApiUrl('/api/v1/items', { search: query, per_page: 50 }),
transform: (d) => (d.data || []).map((item) => ({
value: item.item_code,
label: item.item_code,
@@ -786,13 +754,9 @@ export async function searchSuppliers(query?: string): Promise<{
return { success: true, data: filtered };
}
const searchParams = new URLSearchParams();
if (query) searchParams.set('search', query);
searchParams.set('per_page', '50');
interface SupplierApiData { data: Array<Record<string, string>> }
const result = await executeServerAction<SupplierApiData, SupplierOption[]>({
url: `${API_URL}/api/v1/suppliers?${searchParams.toString()}`,
url: buildApiUrl('/api/v1/suppliers', { search: query, per_page: 50 }),
transform: (d) => (d.data || []).map((s) => ({
value: s.name,
label: s.name,
@@ -1041,11 +1005,10 @@ export async function checkInspectionTemplate(itemId?: number): Promise<{
}
try {
const searchParams = new URLSearchParams();
searchParams.append('category', 'incoming_inspection');
searchParams.append('item_id', String(itemId));
const url = `${API_URL}/api/v1/documents/resolve?${searchParams.toString()}`;
const url = buildApiUrl('/api/v1/documents/resolve', {
category: 'incoming_inspection',
item_id: itemId,
});
const { response, error } = await serverFetch(url, { method: 'GET' });
@@ -1240,12 +1203,11 @@ export async function getInspectionTemplate(params: {
return { success: false, error: '품목 ID가 필요합니다.' };
}
const searchParams = new URLSearchParams();
searchParams.set('category', 'incoming_inspection');
searchParams.set('item_id', String(params.itemId));
const result = await executeServerAction<DocumentResolveResponse>({
url: `${API_URL}/api/v1/documents/resolve?${searchParams.toString()}`,
url: buildApiUrl('/api/v1/documents/resolve', {
category: 'incoming_inspection',
item_id: params.itemId,
}),
errorMessage: '검사 템플릿 조회에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
@@ -1860,7 +1822,7 @@ export async function uploadInspectionFiles(files: File[]): Promise<{
formData.append('file', file);
const response = await fetch(
`${API_URL}/api/v1/files/upload`,
buildApiUrl('/api/v1/files/upload'),
{
method: 'POST',
headers: {
@@ -1881,7 +1843,7 @@ export async function uploadInspectionFiles(files: File[]): Promise<{
uploadedFiles.push({
id: result.data.id,
name: result.data.display_name || file.name,
url: `${API_URL}/api/v1/files/${result.data.id}/download`,
url: buildApiUrl(`/api/v1/files/${result.data.id}/download`),
size: result.data.file_size,
});
}
@@ -1911,7 +1873,7 @@ export async function saveInspectionData(params: {
}> {
// Step 1: POST /v1/documents/upsert - 검사 데이터 저장
const docResult = await executeServerAction({
url: `${API_URL}/api/v1/documents/upsert`,
url: buildApiUrl('/api/v1/documents/upsert'),
method: 'POST',
body: {
template_id: params.templateId,
@@ -1931,7 +1893,7 @@ export async function saveInspectionData(params: {
const inspectionResultLabel = params.inspectionResult === 'pass' ? '합격' : params.inspectionResult === 'fail' ? '불합격' : null;
const recResult = await executeServerAction({
url: `${API_URL}/api/v1/receivings/${params.receivingId}`,
url: buildApiUrl(`/api/v1/receivings/${params.receivingId}`),
method: 'PUT',
body: {
status: 'receiving_pending',