Files
sam-react-prod/src/lib/api/create-crud-service.ts

141 lines
4.1 KiB
TypeScript
Raw Normal View History

/**
* 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';