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:
유병철
2026-02-09 20:42:05 +09:00
parent 3ea6a57a10
commit 4d79b178e3
22 changed files with 588 additions and 619 deletions

View File

@@ -1,11 +1,8 @@
'use server';
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
import { createCrudService, type ActionResult } from '@/lib/api/create-crud-service';
import type { Rank } from './types';
const API_URL = process.env.NEXT_PUBLIC_API_URL;
// ===== API 응답 타입 =====
interface PositionApiData {
id: number;
@@ -18,60 +15,45 @@ interface PositionApiData {
updated_at?: string;
}
// ===== 데이터 변환: API → Frontend =====
function transformApiToFrontend(apiData: PositionApiData): Rank {
return {
id: apiData.id,
name: apiData.name,
order: apiData.sort_order,
isActive: apiData.is_active,
createdAt: apiData.created_at,
updatedAt: apiData.updated_at,
};
}
// ===== CRUD 서비스 생성 =====
const rankService = createCrudService<PositionApiData, Rank>({
basePath: '/api/v1/positions',
transform: (api) => ({
id: api.id,
name: api.name,
order: api.sort_order,
isActive: api.is_active,
createdAt: api.created_at,
updatedAt: api.updated_at,
}),
entityName: '직급',
defaultQueryParams: { type: 'rank' },
defaultCreateBody: { type: 'rank' },
});
// ===== Server Action 래퍼 =====
// Next.js Server Action은 'use server' 파일에서 직접 선언된 async function만 인식
// 팩토리 반환 함수를 직접 export하면 Server Action으로 인식 안 될 수 있음
// ===== 직급 목록 조회 =====
export async function getRanks(params?: {
is_active?: boolean;
q?: string;
}): Promise<ActionResult<Rank[]>> {
const searchParams = new URLSearchParams();
searchParams.set('type', 'rank');
if (params?.is_active !== undefined) {
searchParams.set('is_active', params.is_active.toString());
}
if (params?.q) {
searchParams.set('q', params.q);
}
return executeServerAction({
url: `${API_URL}/api/v1/positions?${searchParams.toString()}`,
transform: (data: PositionApiData[]) => data.map(transformApiToFrontend),
errorMessage: '직급 목록 조회에 실패했습니다.',
});
return rankService.getList(params);
}
// ===== 직급 생성 =====
export async function createRank(data: {
name: string;
sort_order?: number;
is_active?: boolean;
}): Promise<ActionResult<Rank>> {
return executeServerAction({
url: `${API_URL}/api/v1/positions`,
method: 'POST',
body: {
type: 'rank',
name: data.name,
sort_order: data.sort_order,
is_active: data.is_active ?? true,
},
transform: transformApiToFrontend,
errorMessage: '직급 생성에 실패했습니다.',
return rankService.create({
name: data.name,
sort_order: data.sort_order,
is_active: data.is_active ?? true,
});
}
// ===== 직급 수정 =====
export async function updateRank(
id: number,
data: {
@@ -80,32 +62,15 @@ export async function updateRank(
is_active?: boolean;
}
): Promise<ActionResult<Rank>> {
return executeServerAction({
url: `${API_URL}/api/v1/positions/${id}`,
method: 'PUT',
body: data,
transform: transformApiToFrontend,
errorMessage: '직급 수정에 실패했습니다.',
});
return rankService.update(id, data);
}
// ===== 직급 삭제 =====
export async function deleteRank(id: number): Promise<ActionResult> {
return executeServerAction({
url: `${API_URL}/api/v1/positions/${id}`,
method: 'DELETE',
errorMessage: '직급 삭제에 실패했습니다.',
});
return rankService.remove(id);
}
// ===== 직급 순서 변경 =====
export async function reorderRanks(
items: { id: number; sort_order: number }[]
): Promise<ActionResult> {
return executeServerAction({
url: `${API_URL}/api/v1/positions/reorder`,
method: 'PUT',
body: { items },
errorMessage: '순서 변경에 실패했습니다.',
});
}
return rankService.reorder(items);
}