ERP-node/frontend/hooks/pop/executePopAction.ts

200 lines
6.4 KiB
TypeScript

/**
* executePopAction - POP 액션 실행 순수 함수
*
* pop-button, pop-string-list 등 여러 컴포넌트에서 재사용 가능한
* 액션 실행 코어 로직. React 훅에 의존하지 않음.
*
* 사용처:
* - usePopAction 훅 (pop-button용 래퍼)
* - pop-string-list 카드 버튼 (직접 호출)
* - 향후 pop-table 행 액션 등
*/
import type { ButtonMainAction } from "@/lib/registry/pop-components/pop-button";
import { apiClient } from "@/lib/api/client";
import { dataApi } from "@/lib/api/data";
// ========================================
// 타입 정의
// ========================================
/** 액션 실행 결과 */
export interface ActionResult {
success: boolean;
data?: unknown;
error?: string;
}
/** 이벤트 발행 함수 시그니처 (usePopEvent의 publish와 동일) */
type PublishFn = (eventName: string, payload?: unknown) => void;
/** executePopAction 옵션 */
interface ExecuteOptions {
/** 필드 매핑 (소스 컬럼명 → 타겟 컬럼명) */
fieldMapping?: Record<string, string>;
/** 화면 ID (이벤트 발행 시 사용) */
screenId?: string;
/** 이벤트 발행 함수 (순수 함수이므로 외부에서 주입) */
publish?: PublishFn;
}
// ========================================
// 내부 헬퍼
// ========================================
/**
* 필드 매핑 적용
* 소스 데이터의 컬럼명을 타겟 테이블 컬럼명으로 변환
*/
function applyFieldMapping(
rowData: Record<string, unknown>,
mapping?: Record<string, string>
): Record<string, unknown> {
if (!mapping || Object.keys(mapping).length === 0) {
return { ...rowData };
}
const result: Record<string, unknown> = {};
for (const [sourceKey, value] of Object.entries(rowData)) {
// 매핑이 있으면 타겟 키로 변환, 없으면 원본 키 유지
const targetKey = mapping[sourceKey] || sourceKey;
result[targetKey] = value;
}
return result;
}
/**
* rowData에서 PK 추출
* id > pk 순서로 시도, 없으면 rowData 전체를 복합키로 사용
*/
function extractPrimaryKey(
rowData: Record<string, unknown>
): string | number | Record<string, unknown> {
if (rowData.id != null) return rowData.id as string | number;
if (rowData.pk != null) return rowData.pk as string | number;
// 복합키: rowData 전체를 Record로 전달 (dataApi.deleteRecord가 object 지원)
return rowData as Record<string, unknown>;
}
// ========================================
// 메인 함수
// ========================================
/**
* POP 액션 실행 (순수 함수)
*
* @param action - 버튼 메인 액션 설정
* @param rowData - 대상 행 데이터 (리스트 컴포넌트에서 전달)
* @param options - 필드 매핑, screenId, publish 함수
* @returns 실행 결과
*/
export async function executePopAction(
action: ButtonMainAction,
rowData?: Record<string, unknown>,
options?: ExecuteOptions
): Promise<ActionResult> {
const { fieldMapping, publish } = options || {};
try {
switch (action.type) {
// ── 저장 ──
case "save": {
if (!action.targetTable) {
return { success: false, error: "저장 대상 테이블이 설정되지 않았습니다." };
}
const data = rowData
? applyFieldMapping(rowData, fieldMapping)
: {};
const result = await dataApi.createRecord(action.targetTable, data);
return { success: !!result?.success, data: result?.data, error: result?.message };
}
// ── 삭제 ──
case "delete": {
if (!action.targetTable) {
return { success: false, error: "삭제 대상 테이블이 설정되지 않았습니다." };
}
if (!rowData) {
return { success: false, error: "삭제할 데이터가 없습니다." };
}
const mappedData = applyFieldMapping(rowData, fieldMapping);
const pk = extractPrimaryKey(mappedData);
const result = await dataApi.deleteRecord(action.targetTable, pk);
return { success: !!result?.success, error: result?.message };
}
// ── API 호출 ──
case "api": {
if (!action.apiEndpoint) {
return { success: false, error: "API 엔드포인트가 설정되지 않았습니다." };
}
const body = rowData
? applyFieldMapping(rowData, fieldMapping)
: undefined;
const method = (action.apiMethod || "POST").toUpperCase();
let response;
switch (method) {
case "GET":
response = await apiClient.get(action.apiEndpoint, { params: body });
break;
case "POST":
response = await apiClient.post(action.apiEndpoint, body);
break;
case "PUT":
response = await apiClient.put(action.apiEndpoint, body);
break;
case "DELETE":
response = await apiClient.delete(action.apiEndpoint, { data: body });
break;
default:
response = await apiClient.post(action.apiEndpoint, body);
}
const resData = response?.data;
return {
success: resData?.success !== false,
data: resData?.data ?? resData,
};
}
// ── 모달 열기 ──
case "modal": {
if (!publish) {
return { success: false, error: "이벤트 발행 함수가 제공되지 않았습니다." };
}
publish("__pop_modal_open__", {
modalId: action.modalScreenId,
title: action.modalTitle,
mode: action.modalMode,
items: action.modalItems,
rowData,
});
return { success: true };
}
// ── 이벤트 발행 ──
case "event": {
if (!publish) {
return { success: false, error: "이벤트 발행 함수가 제공되지 않았습니다." };
}
if (!action.eventName) {
return { success: false, error: "이벤트 이름이 설정되지 않았습니다." };
}
publish(action.eventName, {
...(action.eventPayload || {}),
row: rowData,
});
return { success: true };
}
default:
return { success: false, error: `알 수 없는 액션 타입: ${action.type}` };
}
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : "알 수 없는 오류가 발생했습니다.";
return { success: false, error: message };
}
}