refactor(WEB): API 유틸 분리, 불필요 코드 정리 및 프로젝트 규칙 업데이트
- executePaginatedAction, buildApiUrl 유틸 모듈 분리 - QuoteCalculationReport, demoStore, export.ts 불필요 코드 삭제 - CLAUDE.md에 Zod 스키마 검증 및 Server Action 공통 유틸 규칙 추가 - package.json 의존성 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
90
src/lib/api/execute-paginated-action.ts
Normal file
90
src/lib/api/execute-paginated-action.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 페이지네이션 조회 전용 Server Action 래퍼
|
||||
*
|
||||
* executeServerAction + toPaginationMeta 조합을 통합하여
|
||||
* 50+ 파일에서 반복되는 15~25줄 패턴을 5~8줄로 줄입니다.
|
||||
*
|
||||
* 적용 범위: 신규 코드만 (기존 코드 마이그레이션 없음)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Before: ~20줄
|
||||
* const result = await executeServerAction({
|
||||
* url: `${API_URL}/api/v1/bills?${queryString}`,
|
||||
* transform: (data: BillPaginatedResponse) => ({
|
||||
* items: (data?.data || []).map(transformApiToFrontend),
|
||||
* pagination: { currentPage: data?.current_page || 1, ... },
|
||||
* }),
|
||||
* errorMessage: '어음 목록 조회에 실패했습니다.',
|
||||
* });
|
||||
* return { success: result.success, data: result.data?.items || [], ... };
|
||||
*
|
||||
* // After: ~5줄
|
||||
* return executePaginatedAction({
|
||||
* url: buildApiUrl('/api/v1/bills', params),
|
||||
* transform: transformApiToFrontend,
|
||||
* errorMessage: '어음 목록 조회에 실패했습니다.',
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { executeServerAction } from './execute-server-action';
|
||||
import { toPaginationMeta, type PaginatedApiResponse, type PaginationMeta } from './types';
|
||||
|
||||
// ===== 반환 타입 =====
|
||||
export interface PaginatedActionResult<T> {
|
||||
success: boolean;
|
||||
data: T[];
|
||||
pagination: PaginationMeta;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}
|
||||
|
||||
// ===== 옵션 타입 =====
|
||||
interface PaginatedActionOptions<TApi, TResult> {
|
||||
/** API URL (전체 경로) */
|
||||
url: string;
|
||||
/** 개별 아이템 변환 함수 (API 응답 아이템 → 프론트엔드 타입) */
|
||||
transform: (item: TApi) => TResult;
|
||||
/** 실패 시 기본 에러 메시지 */
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
const DEFAULT_PAGINATION: PaginationMeta = {
|
||||
currentPage: 1,
|
||||
lastPage: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* 페이지네이션 조회 Server Action 실행
|
||||
*
|
||||
* executeServerAction으로 API 호출 → data 배열에 transform 적용 → toPaginationMeta 변환
|
||||
*/
|
||||
export async function executePaginatedAction<TApi, TResult>(
|
||||
options: PaginatedActionOptions<TApi, TResult>
|
||||
): Promise<PaginatedActionResult<TResult>> {
|
||||
const { url, transform, errorMessage } = options;
|
||||
|
||||
const result = await executeServerAction<PaginatedApiResponse<TApi>>({
|
||||
url,
|
||||
errorMessage,
|
||||
});
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return {
|
||||
success: result.success,
|
||||
data: [],
|
||||
pagination: DEFAULT_PAGINATION,
|
||||
error: result.error,
|
||||
__authError: result.__authError,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: (result.data.data || []).map(transform),
|
||||
pagination: toPaginationMeta(result.data),
|
||||
};
|
||||
}
|
||||
@@ -14,6 +14,12 @@ export {
|
||||
type SelectOption,
|
||||
} from './types';
|
||||
|
||||
// 쿼리 파라미터 빌더 (신규 코드용)
|
||||
export { buildQueryParams, buildApiUrl } from './query-params';
|
||||
|
||||
// 페이지네이션 조회 래퍼 (신규 코드용)
|
||||
export { executePaginatedAction, type PaginatedActionResult } from './execute-paginated-action';
|
||||
|
||||
// 공용 룩업 헬퍼 (거래처/계좌 조회)
|
||||
export {
|
||||
fetchVendorOptions,
|
||||
|
||||
48
src/lib/api/query-params.ts
Normal file
48
src/lib/api/query-params.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 조건부 쿼리 파라미터 빌더
|
||||
*
|
||||
* 43개 actions.ts에서 반복되는 URLSearchParams 보일러플레이트를 제거합니다.
|
||||
* - undefined/null/'' 자동 필터링
|
||||
* - boolean/number 자동 String 변환
|
||||
*
|
||||
* 적용 범위: 신규 코드만 (기존 코드 마이그레이션 없음)
|
||||
*/
|
||||
|
||||
type ParamValue = string | number | boolean | undefined | null;
|
||||
|
||||
/**
|
||||
* 조건부 쿼리 파라미터를 URLSearchParams로 변환
|
||||
* undefined/null/'' 값은 자동으로 제외됩니다.
|
||||
*/
|
||||
export function buildQueryParams(
|
||||
params: Record<string, ParamValue>
|
||||
): URLSearchParams {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value === undefined || value === null || value === '') continue;
|
||||
searchParams.set(key, String(value));
|
||||
}
|
||||
return searchParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* API URL + 조건부 쿼리 파라미터를 결합한 전체 URL 생성
|
||||
*
|
||||
* @example
|
||||
* buildApiUrl('/api/v1/bills', {
|
||||
* search: params.search,
|
||||
* bill_type: params.billType !== 'all' ? params.billType : undefined,
|
||||
* page: params.page,
|
||||
* per_page: params.perPage,
|
||||
* })
|
||||
* // → "https://api.example.com/api/v1/bills?search=test&page=1&per_page=20"
|
||||
*/
|
||||
export function buildApiUrl(
|
||||
path: string,
|
||||
params?: Record<string, ParamValue>
|
||||
): string {
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
if (!params) return `${API_URL}${path}`;
|
||||
const qs = buildQueryParams(params).toString();
|
||||
return qs ? `${API_URL}${path}?${qs}` : `${API_URL}${path}`;
|
||||
}
|
||||
@@ -23,6 +23,8 @@ export interface PaginationMeta {
|
||||
}
|
||||
|
||||
// ===== 프론트엔드 페이지네이션 결과 =====
|
||||
// 신규 코드용: executePaginatedAction 외부에서 transform 결과를 직접 조합할 때 사용
|
||||
// (현재 직접 사용처 0건, 삭제 금지)
|
||||
export interface PaginatedResult<T> {
|
||||
items: T[];
|
||||
pagination: PaginationMeta;
|
||||
|
||||
Reference in New Issue
Block a user