2026-02-23 13:54:49 +09:00
|
|
|
/**
|
|
|
|
|
* 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";
|
2026-03-03 16:04:11 +09:00
|
|
|
import { showErrorToast } from "@/lib/utils/toastUtils";
|
2026-02-23 13:54:49 +09:00
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// 타입 정의
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
/** 확인 대기 중인 액션 상태 */
|
|
|
|
|
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 {
|
2026-03-03 16:04:11 +09:00
|
|
|
showErrorToast("작업에 실패했습니다", result.error, { guidance: "잠시 후 다시 시도해 주세요." });
|
2026-02-23 13:54:49 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 성공 시 후속 액션 실행
|
|
|
|
|
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;
|
|
|
|
|
}
|