"use client"; import React, { useState, useRef, useEffect, useMemo } from "react"; import { ComponentRendererProps } from "@/types/component"; import { ButtonPrimaryConfig } from "./types"; import { ButtonActionExecutor, ButtonActionContext, ButtonActionType, DEFAULT_BUTTON_ACTIONS, } from "@/lib/utils/buttonActions"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { toast } from "sonner"; import { filterDOMProps } from "@/lib/utils/domPropsFilter"; import { useCurrentFlowStep } from "@/stores/flowStepStore"; import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; export interface ButtonPrimaryComponentProps extends ComponentRendererProps { config?: ButtonPrimaryConfig; // μΆ”κ°€ props screenId?: number; tableName?: string; userId?: string; // πŸ†• ν˜„μž¬ μ‚¬μš©μž ID userName?: string; // πŸ†• ν˜„μž¬ μ‚¬μš©μž 이름 companyCode?: string; // πŸ†• ν˜„μž¬ μ‚¬μš©μžμ˜ νšŒμ‚¬ μ½”λ“œ onRefresh?: () => void; onClose?: () => void; onFlowRefresh?: () => void; // 폼 데이터 κ΄€λ ¨ originalData?: Record; // λΆ€λΆ„ μ—…λ°μ΄νŠΈμš© 원본 데이터 // ν…Œμ΄λΈ” μ„ νƒλœ ν–‰ 정보 (닀쀑 선택 μ•‘μ…˜μš©) selectedRows?: any[]; selectedRowsData?: any[]; // ν”Œλ‘œμš° μ„ νƒλœ 데이터 정보 (ν”Œλ‘œμš° μœ„μ ― 선택 μ•‘μ…˜μš©) flowSelectedData?: any[]; flowSelectedStepId?: number | null; } /** * ButtonPrimary μ»΄ν¬λ„ŒνŠΈ * button-primary μ»΄ν¬λ„ŒνŠΈμž…λ‹ˆλ‹€ */ export const ButtonPrimaryComponent: React.FC = ({ component, isDesignMode = false, isSelected = false, isInteractive = false, onClick, onDragStart, onDragEnd, config, className, style, formData, originalData, onFormDataChange, screenId, tableName, userId, // πŸ†• μ‚¬μš©μž ID userName, // πŸ†• μ‚¬μš©μž 이름 companyCode, // πŸ†• νšŒμ‚¬ μ½”λ“œ onRefresh, onClose, onFlowRefresh, selectedRows, selectedRowsData, flowSelectedData, flowSelectedStepId, ...props }) => { const { isPreviewMode } = useScreenPreview(); // 프리뷰 λͺ¨λ“œ 확인 // πŸ†• ν”Œλ‘œμš° 단계별 ν‘œμ‹œ μ œμ–΄ const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig; const currentStep = useCurrentFlowStep(flowConfig?.targetFlowComponentId); // πŸ†• λ²„νŠΌ ν‘œμ‹œ μ—¬λΆ€ 계산 const shouldShowButton = useMemo(() => { // ν”Œλ‘œμš° μ œμ–΄ λΉ„ν™œμ„±ν™” μ‹œ 항상 ν‘œμ‹œ if (!flowConfig?.enabled) { return true; } // ν”Œλ‘œμš° 단계가 μ„ νƒλ˜μ§€ μ•Šμ€ 경우 처리 if (currentStep === null) { // πŸ”§ ν™”μ΄νŠΈλ¦¬μŠ€νŠΈ λͺ¨λ“œμΌ λ•ŒλŠ” 단계 미선택 μ‹œ μˆ¨κΉ€ if (flowConfig.mode === "whitelist") { return false; } // λΈ”λž™λ¦¬μŠ€νŠΈλ‚˜ all λͺ¨λ“œλŠ” ν‘œμ‹œ return true; } const { mode, visibleSteps = [], hiddenSteps = [] } = flowConfig; let result = true; if (mode === "whitelist") { result = visibleSteps.includes(currentStep); } else if (mode === "blacklist") { result = !hiddenSteps.includes(currentStep); } else if (mode === "all") { result = true; } return result; }, [flowConfig, currentStep, component.id, component.label]); // 확인 λ‹€μ΄μ–Όλ‘œκ·Έ μƒνƒœ const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [pendingAction, setPendingAction] = useState<{ type: ButtonActionType; config: any; context: ButtonActionContext; } | null>(null); // ν† μŠ€νŠΈ 정리λ₯Ό μœ„ν•œ ref const currentLoadingToastRef = useRef(); // μ»΄ν¬λ„ŒνŠΈ μ–Έλ§ˆμš΄νŠΈ μ‹œ ν† μŠ€νŠΈ 정리 useEffect(() => { return () => { if (currentLoadingToastRef.current !== undefined) { toast.dismiss(currentLoadingToastRef.current); currentLoadingToastRef.current = undefined; } }; }, []); // μ‚­μ œ μ•‘μ…˜ 감지 둜직 (μ‹€μ œ ν•„λ“œλͺ… μ‚¬μš©) const isDeleteAction = () => { const deleteKeywords = ["μ‚­μ œ", "delete", "remove", "제거", "del"]; return ( component.componentConfig?.action?.type === "delete" || component.config?.action?.type === "delete" || component.webTypeConfig?.actionType === "delete" || component.text?.toLowerCase().includes("μ‚­μ œ") || component.text?.toLowerCase().includes("delete") || component.label?.toLowerCase().includes("μ‚­μ œ") || component.label?.toLowerCase().includes("delete") || deleteKeywords.some( (keyword) => component.config?.buttonText?.toLowerCase().includes(keyword) || component.config?.text?.toLowerCase().includes(keyword), ) ); }; // μ‚­μ œ μ•‘μ…˜μΌ λ•Œ 라벨 색상 μžλ™ μ„€μ • useEffect(() => { if (isDeleteAction() && !component.style?.labelColor) { // μ‚­μ œ μ•‘μ…˜μ΄κ³  라벨 색상이 μ„€μ •λ˜μ§€ μ•Šμ€ 경우 λΉ¨κ°„μƒ‰μœΌλ‘œ μžλ™ μ„€μ • if (component.style) { component.style.labelColor = "#ef4444"; } else { component.style = { labelColor: "#ef4444" }; } } }, [component.componentConfig?.action?.type, component.config?.action?.type, component.webTypeConfig?.actionType]); // μ»΄ν¬λ„ŒνŠΈ μ„€μ • const componentConfig = { ...config, ...component.config, } as ButtonPrimaryConfig; // 🎨 동적 색상 μ„€μ • (μ†μ„±νŽΈμ§‘ λͺ¨λ‹¬μ˜ "색상" ν•„λ“œμ™€ 연동) const getLabelColor = () => { if (isDeleteAction()) { return component.style?.labelColor || "#ef4444"; // 빨간색 κΈ°λ³Έκ°’ (Tailwind red-500) } return component.style?.labelColor || "#212121"; // 검은색 κΈ°λ³Έκ°’ (shadcn/ui primary) }; const buttonColor = getLabelColor(); // κ·ΈλΌλ°μ΄μ…˜μš© μ–΄λ‘μš΄ 색상 계산 const getDarkColor = (baseColor: string) => { const hex = baseColor.replace("#", ""); const r = Math.max(0, parseInt(hex.substr(0, 2), 16) - 40); const g = Math.max(0, parseInt(hex.substr(2, 2), 16) - 40); const b = Math.max(0, parseInt(hex.substr(4, 2), 16) - 40); return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; }; const buttonDarkColor = getDarkColor(buttonColor); // μ•‘μ…˜ μ„€μ • 처리 - DBμ—μ„œ λ¬Έμžμ—΄λ‘œ μ €μž₯된 μ•‘μ…˜μ„ 객체둜 λ³€ν™˜ const processedConfig = { ...componentConfig }; if (componentConfig.action && typeof componentConfig.action === "string") { const actionType = componentConfig.action as ButtonActionType; processedConfig.action = { ...DEFAULT_BUTTON_ACTIONS[actionType], type: actionType, // πŸ”₯ μ œμ–΄κ΄€λ¦¬ μ„€μ • μΆ”κ°€ (webTypeConfigμ—μ„œ κ°€μ Έμ˜΄) enableDataflowControl: component.webTypeConfig?.enableDataflowControl, dataflowConfig: component.webTypeConfig?.dataflowConfig, }; } else if (componentConfig.action && typeof componentConfig.action === "object") { // πŸ”₯ 이미 객체인 κ²½μš°μ—λ„ μ œμ–΄κ΄€λ¦¬ μ„€μ • μΆ”κ°€ processedConfig.action = { ...componentConfig.action, enableDataflowControl: component.webTypeConfig?.enableDataflowControl, dataflowConfig: component.webTypeConfig?.dataflowConfig, }; } // μŠ€νƒ€μΌ 계산 // height: 100%둜 λΆ€λͺ¨(RealtimePreviewDynamic의 λ‚΄λΆ€ div)의 높이λ₯Ό 따라감 const componentStyle: React.CSSProperties = { width: "100%", height: "100%", ...component.style, ...style, // widthλŠ” 항상 100%둜 κ³ μ • (λΆ€λͺ¨ μ»¨ν…Œμ΄λ„ˆκ°€ gridColumns둜 크기 μ œμ–΄) width: "100%", }; // λ””μžμΈ λͺ¨λ“œ μŠ€νƒ€μΌ (border 속성 λΆ„λ¦¬ν•˜μ—¬ 좩돌 λ°©μ§€) if (isDesignMode) { componentStyle.borderWidth = "1px"; componentStyle.borderStyle = "dashed"; componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1"; } // 확인 λ‹€μ΄μ–Όλ‘œκ·Έκ°€ ν•„μš”ν•œ μ•‘μ…˜ νƒ€μž…λ“€ const confirmationRequiredActions: ButtonActionType[] = ["save", "delete"]; // μ‹€μ œ μ•‘μ…˜ μ‹€ν–‰ ν•¨μˆ˜ const executeAction = async (actionConfig: any, context: ButtonActionContext) => { try { // κΈ°μ‘΄ ν† μŠ€νŠΈκ°€ μžˆλ‹€λ©΄ λ¨Όμ € 제거 if (currentLoadingToastRef.current !== undefined) { toast.dismiss(currentLoadingToastRef.current); currentLoadingToastRef.current = undefined; } // μΆ”κ°€ μ•ˆμ „μž₯치: λͺ¨λ“  λ‘œλ”© ν† μŠ€νŠΈ 제거 toast.dismiss(); // UI μ „ν™˜ μ•‘μ…˜ 및 λͺ¨λ‹¬ μ•‘μ…˜μ€ λ‘œλ”© ν† μŠ€νŠΈ ν‘œμ‹œν•˜μ§€ μ•ŠμŒ const silentActions = ["edit", "modal", "navigate", "excel_upload", "barcode_scan"]; if (!silentActions.includes(actionConfig.type)) { currentLoadingToastRef.current = toast.loading( actionConfig.type === "save" ? "μ €μž₯ 쀑..." : actionConfig.type === "delete" ? "μ‚­μ œ 쀑..." : actionConfig.type === "submit" ? "제좜 쀑..." : "처리 쀑...", { duration: Infinity, // λͺ…μ‹œμ μœΌλ‘œ λ¬΄ν•œλŒ€λ‘œ μ„€μ • }, ); } const success = await ButtonActionExecutor.executeAction(actionConfig, context); // λ‘œλ”© ν† μŠ€νŠΈ 제거 (μžˆλŠ” κ²½μš°μ—λ§Œ) if (currentLoadingToastRef.current !== undefined) { toast.dismiss(currentLoadingToastRef.current); currentLoadingToastRef.current = undefined; } // μ‹€νŒ¨ν•œ 경우 였λ₯˜ 처리 if (!success) { // UI μ „ν™˜ μ•‘μ…˜ 및 λͺ¨λ‹¬ μ•‘μ…˜μ€ μ—λŸ¬λ„ 쑰용히 처리 (λͺ¨λ‹¬ λ‚΄λΆ€μ—μ„œ 자체 μ—λŸ¬ ν‘œμ‹œ) const silentErrorActions = ["edit", "modal", "navigate", "excel_upload", "barcode_scan"]; if (silentErrorActions.includes(actionConfig.type)) { return; } // κΈ°λ³Έ μ—λŸ¬ λ©”μ‹œμ§€ κ²°μ • const defaultErrorMessage = actionConfig.type === "save" ? "μ €μž₯ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€." : actionConfig.type === "delete" ? "μ‚­μ œ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€." : actionConfig.type === "submit" ? "제좜 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€." : "처리 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."; // μ»€μŠ€ν…€ λ©”μ‹œμ§€ μ‚¬μš© 쑰건: // 1. μ»€μŠ€ν…€ λ©”μ‹œμ§€κ°€ 있고 // 2. (μ•‘μ…˜ νƒ€μž…μ΄ saveμ΄κ±°λ‚˜ OR λ©”μ‹œμ§€μ— "μ €μž₯"이 ν¬ν•¨λ˜μ§€ μ•Šμ€ 경우) const useCustomMessage = actionConfig.errorMessage && (actionConfig.type === "save" || !actionConfig.errorMessage.includes("μ €μž₯")); const errorMessage = useCustomMessage ? actionConfig.errorMessage : defaultErrorMessage; toast.error(errorMessage); return; } // μ„±κ³΅ν•œ κ²½μš°μ—λ§Œ 성곡 ν† μŠ€νŠΈ ν‘œμ‹œ // edit, modal, navigate, excel_upload, barcode_scan μ•‘μ…˜μ€ 쑰용히 처리 // (UI μ „ν™˜λ§Œ ν•˜κ±°λ‚˜ λͺ¨λ‹¬ λ‚΄λΆ€μ—μ„œ 자체적으둜 λ©”μ‹œμ§€ ν‘œμ‹œ) const silentSuccessActions = ["edit", "modal", "navigate", "excel_upload", "barcode_scan"]; if (!silentSuccessActions.includes(actionConfig.type)) { // κΈ°λ³Έ 성곡 λ©”μ‹œμ§€ κ²°μ • const defaultSuccessMessage = actionConfig.type === "save" ? "μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€." : actionConfig.type === "delete" ? "μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€." : actionConfig.type === "submit" ? "μ œμΆœλ˜μ—ˆμŠ΅λ‹ˆλ‹€." : "μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€."; // μ»€μŠ€ν…€ λ©”μ‹œμ§€ μ‚¬μš© 쑰건: // 1. μ»€μŠ€ν…€ λ©”μ‹œμ§€κ°€ 있고 // 2. (μ•‘μ…˜ νƒ€μž…μ΄ saveμ΄κ±°λ‚˜ OR λ©”μ‹œμ§€μ— "μ €μž₯"이 ν¬ν•¨λ˜μ§€ μ•Šμ€ 경우) const useCustomMessage = actionConfig.successMessage && (actionConfig.type === "save" || !actionConfig.successMessage.includes("μ €μž₯")); const successMessage = useCustomMessage ? actionConfig.successMessage : defaultSuccessMessage; toast.success(successMessage); } // μ €μž₯/μˆ˜μ • 성곡 μ‹œ μžλ™ 처리 if (actionConfig.type === "save" || actionConfig.type === "edit") { if (typeof window !== "undefined") { // 1. ν…Œμ΄λΈ” μƒˆλ‘œκ³ μΉ¨ 이벀트 λ¨Όμ € λ°œμ†‘ (λͺ¨λ‹¬μ΄ λ‹«νžˆκΈ° 전에) window.dispatchEvent(new CustomEvent("refreshTable")); // 2. λͺ¨λ‹¬ λ‹«κΈ° (μ•½κ°„μ˜ λ”œλ ˆμ΄) setTimeout(() => { // EditModal 내뢀인지 확인 (isInModal prop μ‚¬μš©) const isInEditModal = (props as any).isInModal; if (isInEditModal) { window.dispatchEvent(new CustomEvent("closeEditModal")); } // ScreenModal은 항상 λ‹«κΈ° window.dispatchEvent(new CustomEvent("closeSaveModal")); }, 100); } } } catch (error) { // λ‘œλ”© ν† μŠ€νŠΈ 제거 if (currentLoadingToastRef.current !== undefined) { toast.dismiss(currentLoadingToastRef.current); currentLoadingToastRef.current = undefined; } console.error("❌ λ²„νŠΌ μ•‘μ…˜ μ‹€ν–‰ 였λ₯˜:", error); // 였λ₯˜ ν† μŠ€νŠΈλŠ” buttonActions.tsμ—μ„œ 이미 ν‘œμ‹œλ˜λ―€λ‘œ μ—¬κΈ°μ„œλŠ” 제거 // (쀑볡 ν† μŠ€νŠΈ λ°©μ§€) } }; // 이벀트 ν•Έλ“€λŸ¬ const handleClick = async (e: React.MouseEvent) => { e.stopPropagation(); // 프리뷰 λͺ¨λ“œμ—μ„œλŠ” λ²„νŠΌ λ™μž‘ 차단 if (isPreviewMode) { return; } // λ””μžμΈ λͺ¨λ“œμ—μ„œλŠ” κΈ°λ³Έ onClick만 μ‹€ν–‰ if (isDesignMode) { onClick?.(); return; } // μΈν„°λž™ν‹°λΈŒ λͺ¨λ“œμ—μ„œ μ•‘μ…˜ μ‹€ν–‰ if (isInteractive && processedConfig.action) { // μ‚­μ œ μ•‘μ…˜μΈλ° μ„ νƒλœ 데이터가 μ—†μœΌλ©΄ κ²½κ³  λ©”μ‹œμ§€ ν‘œμ‹œν•˜κ³  쀑단 const hasDataToDelete = (selectedRowsData && selectedRowsData.length > 0) || (flowSelectedData && flowSelectedData.length > 0); if (processedConfig.action.type === "delete" && !hasDataToDelete) { toast.warning("μ‚­μ œν•  ν•­λͺ©μ„ λ¨Όμ € μ„ νƒν•΄μ£Όμ„Έμš”."); return; } const context: ButtonActionContext = { formData: formData || {}, originalData: originalData || {}, // λΆ€λΆ„ μ—…λ°μ΄νŠΈμš© 원본 데이터 μΆ”κ°€ screenId, tableName, userId, // πŸ†• μ‚¬μš©μž ID userName, // πŸ†• μ‚¬μš©μž 이름 companyCode, // πŸ†• νšŒμ‚¬ μ½”λ“œ onFormDataChange, onRefresh, onClose, onFlowRefresh, // ν”Œλ‘œμš° μƒˆλ‘œκ³ μΉ¨ 콜백 μΆ”κ°€ // ν…Œμ΄λΈ” μ„ νƒλœ ν–‰ 정보 μΆ”κ°€ selectedRows, selectedRowsData, // ν”Œλ‘œμš° μ„ νƒλœ 데이터 정보 μΆ”κ°€ flowSelectedData, flowSelectedStepId, }; // 확인이 ν•„μš”ν•œ μ•‘μ…˜μΈμ§€ 확인 if (confirmationRequiredActions.includes(processedConfig.action.type)) { // 확인 λ‹€μ΄μ–Όλ‘œκ·Έ ν‘œμ‹œ setPendingAction({ type: processedConfig.action.type, config: processedConfig.action, context, }); setShowConfirmDialog(true); } else { // 확인이 ν•„μš”ν•˜μ§€ μ•Šμ€ μ•‘μ…˜μ€ λ°”λ‘œ μ‹€ν–‰ await executeAction(processedConfig.action, context); } } else { // μ•‘μ…˜μ΄ μ„€μ •λ˜μ§€ μ•Šμ€ 경우 κΈ°λ³Έ onClick μ‹€ν–‰ onClick?.(); } }; // 확인 λ‹€μ΄μ–Όλ‘œκ·Έμ—μ„œ 확인 λ²„νŠΌ 클릭 μ‹œ const handleConfirmAction = async () => { if (pendingAction) { await executeAction(pendingAction.config, pendingAction.context); } setShowConfirmDialog(false); setPendingAction(null); }; // 확인 λ‹€μ΄μ–Όλ‘œκ·Έμ—μ„œ μ·¨μ†Œ λ²„νŠΌ 클릭 μ‹œ const handleCancelAction = () => { setShowConfirmDialog(false); setPendingAction(null); }; // DOM에 μ „λ‹¬ν•˜λ©΄ μ•ˆ λ˜λŠ” React-specific props 필터링 const { selectedScreen, onZoneComponentDrop, onZoneClick, componentConfig: _componentConfig, component: _component, isSelected: _isSelected, onClick: _onClick, onDragStart: _onDragStart, onDragEnd: _onDragEnd, size: _size, position: _position, style: _style, screenId: _screenId, tableName: _tableName, onRefresh: _onRefresh, onClose: _onClose, selectedRows: _selectedRows, selectedRowsData: _selectedRowsData, onSelectedRowsChange: _onSelectedRowsChange, flowSelectedData: _flowSelectedData, // ν”Œλ‘œμš° 선택 데이터 필터링 flowSelectedStepId: _flowSelectedStepId, // ν”Œλ‘œμš° 선택 μŠ€ν… ID 필터링 onFlowRefresh: _onFlowRefresh, // ν”Œλ‘œμš° μƒˆλ‘œκ³ μΉ¨ 콜백 필터링 originalData: _originalData, // λΆ€λΆ„ μ—…λ°μ΄νŠΈμš© 원본 데이터 필터링 refreshKey: _refreshKey, // 필터링 μΆ”κ°€ isInModal: _isInModal, // 필터링 μΆ”κ°€ mode: _mode, // 필터링 μΆ”κ°€ ...domProps } = props; // λ‹€μ΄μ–Όλ‘œκ·Έ λ©”μ‹œμ§€ 생성 const getConfirmMessage = () => { if (!pendingAction) return ""; const customMessage = pendingAction.config.confirmMessage; if (customMessage) return customMessage; switch (pendingAction.type) { case "save": return "변경사항을 μ €μž₯ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?"; case "delete": return "μ •λ§λ‘œ μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ? 이 μž‘μ—…μ€ 되돌릴 수 μ—†μŠ΅λ‹ˆλ‹€."; case "submit": return "μ œμΆœν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?"; default: return "이 μž‘μ—…μ„ μ‹€ν–‰ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?"; } }; const getConfirmTitle = () => { if (!pendingAction) return ""; switch (pendingAction.type) { case "save": return "μ €μž₯ 확인"; case "delete": return "μ‚­μ œ 확인"; case "submit": return "제좜 확인"; default: return "μž‘μ—… 확인"; } }; // DOM μ•ˆμ „ν•œ props만 필터링 const safeDomProps = filterDOMProps(domProps); // πŸ†• ν”Œλ‘œμš° 단계별 ν‘œμ‹œ μ œμ–΄ if (!shouldShowButton) { // λ ˆμ΄μ•„μ›ƒ λ™μž‘μ— 따라 λ‹€λ₯΄κ²Œ 처리 if (flowConfig?.layoutBehavior === "preserve-position") { // μœ„μΉ˜ μœ μ§€ (빈 곡간, display: none) return
; } else { // μ™„μ „νžˆ λ Œλ”λ§ν•˜μ§€ μ•ŠμŒ (auto-compact, 빈 곡간 제거) return null; } } return ( <>
{/* 확인 λ‹€μ΄μ–Όλ‘œκ·Έ - EditModal보닀 μœ„μ— ν‘œμ‹œν•˜λ„λ‘ z-index μ΅œμƒμœ„λ‘œ μ„€μ • */} {getConfirmTitle()} {getConfirmMessage()} μ·¨μ†Œ {pendingAction?.type === "save" ? "μ €μž₯" : pendingAction?.type === "delete" ? "μ‚­μ œ" : pendingAction?.type === "submit" ? "제좜" : "확인"} ); }; /** * ButtonPrimary 래퍼 μ»΄ν¬λ„ŒνŠΈ * 좔가적인 λ‘œμ§μ΄λ‚˜ μƒνƒœ 관리가 ν•„μš”ν•œ 경우 μ‚¬μš© */ export const ButtonPrimaryWrapper: React.FC = (props) => { return ; };