refactor(WEB): 공통 훅(useDeleteDialog, useStatsLoader) 및 CRUD 서비스 추출
- useDeleteDialog 훅 추출로 삭제 다이얼로그 로직 공통화 - useStatsLoader 훅 추출로 통계 로딩 패턴 공통화 - create-crud-service 유틸 추가 - 차량관리/견적/출고/검사 등 리스트 컴포넌트 간소화 - RankManagement actions 정리 - 프로덕션 로거 불필요 출력 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
140
src/lib/api/create-crud-service.ts
Normal file
140
src/lib/api/create-crud-service.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* CRUD Server Action 팩토리
|
||||
*
|
||||
* 정형적인 CRUD actions.ts 파일의 보일러플레이트를 제거합니다.
|
||||
* executeServerAction 위에 한 단계 더 추상화하여
|
||||
* getList / create / update / remove / reorder 함수를 자동 생성합니다.
|
||||
*
|
||||
* 주의: 이 파일은 'use server'가 아닙니다.
|
||||
* 각 도메인의 actions.ts ('use server')에서 import하여 사용합니다.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // RankManagement/actions.ts
|
||||
* 'use server';
|
||||
* const service = createCrudService<PositionApiData, Rank>({
|
||||
* basePath: '/api/v1/positions',
|
||||
* transform: (api) => ({ id: api.id, name: api.name, ... }),
|
||||
* entityName: '직급',
|
||||
* defaultQueryParams: { type: 'rank' },
|
||||
* defaultCreateBody: { type: 'rank' },
|
||||
* });
|
||||
* export async function getRanks(params?) { return service.getList(params); }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { executeServerAction, type ActionResult } from './execute-server-action';
|
||||
|
||||
// ===== 설정 타입 =====
|
||||
export interface CrudServiceConfig<TApi, TFrontend> {
|
||||
/** API 경로 (예: '/api/v1/positions') */
|
||||
basePath: string;
|
||||
/** API → Frontend 데이터 변환 함수 */
|
||||
transform: (apiData: TApi) => TFrontend;
|
||||
/** 엔티티 한글명 (에러 메시지용, 예: '직급') */
|
||||
entityName: string;
|
||||
/** 목록 조회 시 기본 쿼리 파라미터 (예: { type: 'rank' }) */
|
||||
defaultQueryParams?: Record<string, string>;
|
||||
/** 생성 시 body에 추가할 기본값 (예: { type: 'rank' }) */
|
||||
defaultCreateBody?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ===== 서비스 반환 타입 =====
|
||||
export interface CrudService<TFrontend> {
|
||||
getList(params?: {
|
||||
is_active?: boolean;
|
||||
q?: string;
|
||||
}): Promise<ActionResult<TFrontend[]>>;
|
||||
|
||||
create(body: Record<string, unknown>): Promise<ActionResult<TFrontend>>;
|
||||
|
||||
update(
|
||||
id: number,
|
||||
body: Record<string, unknown>
|
||||
): Promise<ActionResult<TFrontend>>;
|
||||
|
||||
remove(id: number): Promise<ActionResult>;
|
||||
|
||||
reorder(
|
||||
items: { id: number; sort_order: number }[]
|
||||
): Promise<ActionResult>;
|
||||
}
|
||||
|
||||
// ===== 팩토리 함수 =====
|
||||
export function createCrudService<TApi, TFrontend>(
|
||||
config: CrudServiceConfig<TApi, TFrontend>
|
||||
): CrudService<TFrontend> {
|
||||
const {
|
||||
basePath,
|
||||
transform,
|
||||
entityName,
|
||||
defaultQueryParams,
|
||||
defaultCreateBody,
|
||||
} = config;
|
||||
|
||||
// API URL은 호출 시점에 resolve (SSR 안전)
|
||||
const getBaseUrl = () => `${process.env.NEXT_PUBLIC_API_URL}${basePath}`;
|
||||
|
||||
return {
|
||||
async getList(params) {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (defaultQueryParams) {
|
||||
Object.entries(defaultQueryParams).forEach(([k, v]) =>
|
||||
searchParams.set(k, v)
|
||||
);
|
||||
}
|
||||
if (params?.is_active !== undefined) {
|
||||
searchParams.set('is_active', params.is_active.toString());
|
||||
}
|
||||
if (params?.q) {
|
||||
searchParams.set('q', params.q);
|
||||
}
|
||||
|
||||
return executeServerAction({
|
||||
url: `${getBaseUrl()}?${searchParams.toString()}`,
|
||||
transform: (data: TApi[]) => data.map(transform),
|
||||
errorMessage: `${entityName} 목록 조회에 실패했습니다.`,
|
||||
});
|
||||
},
|
||||
|
||||
async create(body) {
|
||||
return executeServerAction({
|
||||
url: getBaseUrl(),
|
||||
method: 'POST',
|
||||
body: { ...defaultCreateBody, ...body },
|
||||
transform,
|
||||
errorMessage: `${entityName} 생성에 실패했습니다.`,
|
||||
});
|
||||
},
|
||||
|
||||
async update(id, body) {
|
||||
return executeServerAction({
|
||||
url: `${getBaseUrl()}/${id}`,
|
||||
method: 'PUT',
|
||||
body,
|
||||
transform,
|
||||
errorMessage: `${entityName} 수정에 실패했습니다.`,
|
||||
});
|
||||
},
|
||||
|
||||
async remove(id) {
|
||||
return executeServerAction({
|
||||
url: `${getBaseUrl()}/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: `${entityName} 삭제에 실패했습니다.`,
|
||||
});
|
||||
},
|
||||
|
||||
async reorder(items) {
|
||||
return executeServerAction({
|
||||
url: `${getBaseUrl()}/reorder`,
|
||||
method: 'PUT',
|
||||
body: { items },
|
||||
errorMessage: '순서 변경에 실패했습니다.',
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ActionResult 재export (actions.ts에서 import 편의)
|
||||
export type { ActionResult } from './execute-server-action';
|
||||
@@ -19,8 +19,8 @@ interface ApiLogEntry {
|
||||
level: LogLevel;
|
||||
method: string;
|
||||
url: string;
|
||||
requestData?: any;
|
||||
responseData?: any;
|
||||
requestData?: unknown;
|
||||
responseData?: unknown;
|
||||
statusCode?: number;
|
||||
error?: Error;
|
||||
duration?: number;
|
||||
@@ -58,7 +58,7 @@ class ApiLogger {
|
||||
/**
|
||||
* API 요청 시작 로그
|
||||
*/
|
||||
logRequest(method: string, url: string, data?: any): number {
|
||||
logRequest(method: string, url: string, data?: unknown): number {
|
||||
if (!this.enabled) return Date.now();
|
||||
|
||||
const startTime = Date.now();
|
||||
@@ -88,7 +88,7 @@ class ApiLogger {
|
||||
method: string,
|
||||
url: string,
|
||||
statusCode: number,
|
||||
data: any,
|
||||
data: unknown,
|
||||
startTime: number
|
||||
) {
|
||||
if (!this.enabled) return;
|
||||
@@ -154,7 +154,7 @@ class ApiLogger {
|
||||
/**
|
||||
* 경고 로그
|
||||
*/
|
||||
logWarning(message: string, data?: any) {
|
||||
logWarning(message: string, data?: unknown) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
const entry: ApiLogEntry = {
|
||||
@@ -172,7 +172,7 @@ class ApiLogger {
|
||||
/**
|
||||
* 디버그 로그
|
||||
*/
|
||||
logDebug(message: string, data?: any) {
|
||||
logDebug(message: string, data?: unknown) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
const entry: ApiLogEntry = {
|
||||
@@ -348,7 +348,7 @@ export async function loggedFetch<T>(
|
||||
|
||||
// 개발 도구를 window에 노출 (브라우저 콘솔에서 사용 가능)
|
||||
if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') {
|
||||
(window as any).apiLogger = apiLogger;
|
||||
(window as unknown as Record<string, unknown>).apiLogger = apiLogger;
|
||||
console.log(
|
||||
'💡 API Logger is available in console as "apiLogger"\n' +
|
||||
' - apiLogger.getLogs() - View all logs\n' +
|
||||
|
||||
Reference in New Issue
Block a user