/** * 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; /** 화면 ID (이벤트 발행 시 사용) */ screenId?: string; /** 이벤트 발행 함수 (순수 함수이므로 외부에서 주입) */ publish?: PublishFn; } // ======================================== // 내부 헬퍼 // ======================================== /** * 필드 매핑 적용 * 소스 데이터의 컬럼명을 타겟 테이블 컬럼명으로 변환 */ function applyFieldMapping( rowData: Record, mapping?: Record ): Record { if (!mapping || Object.keys(mapping).length === 0) { return { ...rowData }; } const result: Record = {}; 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 | number | Record { 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; } // ======================================== // 메인 함수 // ======================================== /** * POP 액션 실행 (순수 함수) * * @param action - 버튼 메인 액션 설정 * @param rowData - 대상 행 데이터 (리스트 컴포넌트에서 전달) * @param options - 필드 매핑, screenId, publish 함수 * @returns 실행 결과 */ export async function executePopAction( action: ButtonMainAction, rowData?: Record, options?: ExecuteOptions ): Promise { 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 }; } }