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

219 lines
5.9 KiB
TypeScript

/**
* usePopAction - POP 액션 실행 React 훅
*
* executePopAction (순수 함수)를 래핑하여 React UI 관심사를 처리:
* - 로딩 상태 (isLoading)
* - 확인 다이얼로그 (pendingConfirm)
* - 토스트 알림
* - 후속 액션 체이닝 (followUpActions)
*
* 사용처:
* - PopButtonComponent (메인 버튼)
*
* pop-string-list 등 리스트 컴포넌트는 executePopAction을 직접 호출하여
* 훅 인스턴스 폭발 문제를 회피함.
*/
import { useState, useCallback, useRef } from "react";
import type {
ButtonMainAction,
FollowUpAction,
ConfirmConfig,
} from "@/lib/registry/pop-components/pop-button";
import { usePopEvent } from "./usePopEvent";
import { executePopAction } from "./executePopAction";
import type { ActionResult } from "./executePopAction";
import { toast } from "sonner";
// ========================================
// 타입 정의
// ========================================
/** 확인 대기 중인 액션 상태 */
export interface PendingConfirmState {
action: ButtonMainAction;
rowData?: Record<string, unknown>;
fieldMapping?: Record<string, string>;
confirm: ConfirmConfig;
followUpActions?: FollowUpAction[];
}
/** execute 호출 시 옵션 */
interface ExecuteActionOptions {
/** 대상 행 데이터 */
rowData?: Record<string, unknown>;
/** 필드 매핑 */
fieldMapping?: Record<string, string>;
/** 확인 다이얼로그 설정 */
confirm?: ConfirmConfig;
/** 후속 액션 */
followUpActions?: FollowUpAction[];
}
// ========================================
// 상수
// ========================================
/** 액션 성공 시 토스트 메시지 */
const ACTION_SUCCESS_MESSAGES: Record<string, string> = {
save: "저장되었습니다.",
delete: "삭제되었습니다.",
api: "요청이 완료되었습니다.",
modal: "",
event: "",
};
// ========================================
// 메인 훅
// ========================================
/**
* POP 액션 실행 훅
*
* @param screenId - 화면 ID (이벤트 버스 연결용)
* @returns execute, isLoading, pendingConfirm, confirmExecute, cancelConfirm
*/
export function usePopAction(screenId: string) {
const [isLoading, setIsLoading] = useState(false);
const [pendingConfirm, setPendingConfirm] = useState<PendingConfirmState | null>(null);
const { publish } = usePopEvent(screenId);
// publish 안정성 보장 (콜백 내에서 최신 참조 사용)
const publishRef = useRef(publish);
publishRef.current = publish;
/**
* 실제 실행 (확인 다이얼로그 이후 or 확인 불필요 시)
*/
const runAction = useCallback(
async (
action: ButtonMainAction,
rowData?: Record<string, unknown>,
fieldMapping?: Record<string, string>,
followUpActions?: FollowUpAction[]
): Promise<ActionResult> => {
setIsLoading(true);
try {
const result = await executePopAction(action, rowData, {
fieldMapping,
screenId,
publish: publishRef.current,
});
// 결과에 따른 토스트
if (result.success) {
const msg = ACTION_SUCCESS_MESSAGES[action.type];
if (msg) toast.success(msg);
} else {
toast.error(result.error || "작업에 실패했습니다.");
}
// 성공 시 후속 액션 실행
if (result.success && followUpActions?.length) {
await executeFollowUpActions(followUpActions);
}
return result;
} finally {
setIsLoading(false);
}
},
[screenId]
);
/**
* 후속 액션 실행
*/
const executeFollowUpActions = useCallback(
async (actions: FollowUpAction[]) => {
for (const followUp of actions) {
switch (followUp.type) {
case "event":
if (followUp.eventName) {
publishRef.current(followUp.eventName, followUp.eventPayload);
}
break;
case "refresh":
// 새로고침 이벤트 발행 (구독하는 컴포넌트가 refetch)
publishRef.current("__pop_refresh__");
break;
case "navigate":
if (followUp.targetScreenId) {
publishRef.current("__pop_navigate__", {
screenId: followUp.targetScreenId,
params: followUp.params,
});
}
break;
case "close-modal":
publishRef.current("__pop_modal_close__");
break;
}
}
},
[]
);
/**
* 외부에서 호출하는 실행 함수
* confirm이 활성화되어 있으면 pendingConfirm에 저장하고 대기.
* 비활성화이면 즉시 실행.
*/
const execute = useCallback(
async (
action: ButtonMainAction,
options?: ExecuteActionOptions
): Promise<ActionResult> => {
const { rowData, fieldMapping, confirm, followUpActions } = options || {};
// 확인 다이얼로그 필요 시 대기
if (confirm?.enabled) {
setPendingConfirm({
action,
rowData,
fieldMapping,
confirm,
followUpActions,
});
return { success: true }; // 대기 상태이므로 일단 success
}
// 즉시 실행
return runAction(action, rowData, fieldMapping, followUpActions);
},
[runAction]
);
/**
* 확인 다이얼로그에서 "확인" 클릭 시
*/
const confirmExecute = useCallback(async () => {
if (!pendingConfirm) return;
const { action, rowData, fieldMapping, followUpActions } = pendingConfirm;
setPendingConfirm(null);
await runAction(action, rowData, fieldMapping, followUpActions);
}, [pendingConfirm, runAction]);
/**
* 확인 다이얼로그에서 "취소" 클릭 시
*/
const cancelConfirm = useCallback(() => {
setPendingConfirm(null);
}, []);
return {
execute,
isLoading,
pendingConfirm,
confirmExecute,
cancelConfirm,
} as const;
}