From ccf8bd3284fcd61ab988f33065ed4bc38aa10045 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Fri, 5 Dec 2025 11:03:15 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EB=B2=84=ED=8A=BC=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config-panels/ButtonConfigPanel.tsx | 133 +++++++++++++++ .../button-primary/ButtonPrimaryComponent.tsx | 157 +++++++++++++++++- frontend/lib/utils/buttonActions.ts | 144 ++++++++++++++++ 3 files changed, 429 insertions(+), 5 deletions(-) diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index ba88befd..36f420fd 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -1953,6 +1953,139 @@ export const ButtonConfigPanel: React.FC = ({ )} + {/* πŸ†• λ²„νŠΌ ν™œμ„±ν™” 쑰건 μ„€μ • */} +
+
λ²„νŠΌ ν™œμ„±ν™” 쑰건
+ + {/* μΆœλ°œμ§€/도착지 ν•„μˆ˜ 체크 */} +
+
+ +

μ„ νƒν•˜μ§€ μ•ŠμœΌλ©΄ λ²„νŠΌ λΉ„ν™œμ„±ν™”

+
+ onUpdateProperty("componentConfig.action.requireLocationFields", checked)} + /> +
+ + {config.action?.requireLocationFields && ( +
+
+
+ + onUpdateProperty("componentConfig.action.trackingDepartureField", e.target.value)} + className="h-8 text-xs" + /> +
+
+ + onUpdateProperty("componentConfig.action.trackingArrivalField", e.target.value)} + className="h-8 text-xs" + /> +
+
+
+ )} + + {/* μƒνƒœ 기반 ν™œμ„±ν™” 쑰건 */} +
+
+ +

νŠΉμ • μƒνƒœμΌ λ•Œλ§Œ λ²„νŠΌ ν™œμ„±ν™”

+
+ onUpdateProperty("componentConfig.action.enableOnStatusCheck", checked)} + /> +
+ + {config.action?.enableOnStatusCheck && ( +
+
+ + +

+ μƒνƒœλ₯Ό μ‘°νšŒν•  ν…Œμ΄λΈ” (κΈ°λ³Έ: vehicles) +

+
+
+ + onUpdateProperty("componentConfig.action.statusCheckKeyField", e.target.value)} + className="h-8 text-xs" + /> +

+ ν˜„μž¬ 둜그인 μ‚¬μš©μž ID둜 μ‘°νšŒν•  ν•„λ“œ (κΈ°λ³Έ: user_id) +

+
+
+ + onUpdateProperty("componentConfig.action.statusCheckField", e.target.value)} + className="h-8 text-xs" + /> +

+ μƒνƒœ 값이 μ €μž₯된 컬럼λͺ… (κΈ°λ³Έ: status) +

+
+
+ + +
+
+ + onUpdateProperty("componentConfig.action.statusConditionValues", e.target.value)} + className="h-8 text-xs" + /> +

+ μ—¬λŸ¬ μƒνƒœκ°’μ€ μ‰Όν‘œ(,)둜 ꡬ뢄 +

+
+
+ )} +
+

μ‚¬μš© μ˜ˆμ‹œ: diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index 5816940a..6f5c8739 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -26,6 +26,7 @@ import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; import { useScreenContextOptional } from "@/contexts/ScreenContext"; import { useSplitPanelContext, SplitPanelPosition } from "@/contexts/SplitPanelContext"; import { applyMappingRules } from "@/lib/utils/dataMapping"; +import { apiClient } from "@/lib/api/client"; export interface ButtonPrimaryComponentProps extends ComponentRendererProps { config?: ButtonPrimaryConfig; @@ -148,6 +149,149 @@ export const ButtonPrimaryComponent: React.FC = ({ return result; }, [flowConfig, currentStep, component.id, component.label]); + // πŸ†• μš΄ν–‰μ•Œλ¦Ό λ²„νŠΌ 쑰건뢀 λΉ„ν™œμ„±ν™” (μΆœλ°œμ§€/도착지 ν•„μˆ˜, μƒνƒœ 체크) + // μƒνƒœλŠ” API둜 쑰회 (formData에 μ—†λŠ” 경우) + const [vehicleStatus, setVehicleStatus] = useState(null); + const [statusLoading, setStatusLoading] = useState(false); + + // μƒνƒœ 쑰회 (operation_control + enableOnStatusCheck일 λ•Œ) + const actionConfig = component.componentConfig?.action; + const shouldFetchStatus = actionConfig?.type === "operation_control" && actionConfig?.enableOnStatusCheck && userId; + const statusTableName = actionConfig?.statusCheckTableName || "vehicles"; + const statusKeyField = actionConfig?.statusCheckKeyField || "user_id"; + const statusFieldName = actionConfig?.statusCheckField || "status"; + + useEffect(() => { + if (!shouldFetchStatus) return; + + let isMounted = true; + + const fetchStatus = async () => { + if (!isMounted) return; + + try { + const response = await apiClient.post(`/table-management/tables/${statusTableName}/data`, { + page: 1, + size: 1, + search: { [statusKeyField]: userId }, + autoFilter: true, + }); + + if (!isMounted) return; + + const rows = response.data?.data?.data || response.data?.data?.rows || response.data?.rows || []; + const firstRow = Array.isArray(rows) ? rows[0] : null; + + if (response.data?.success && firstRow) { + const newStatus = firstRow[statusFieldName]; + if (newStatus !== vehicleStatus) { + // console.log("πŸ”„ [ButtonPrimary] μƒνƒœ λ³€κ²½ 감지:", { 이전: vehicleStatus, ν˜„μž¬: newStatus, buttonLabel: component.label }); + } + setVehicleStatus(newStatus); + } else { + setVehicleStatus(null); + } + } catch (error: any) { + // console.error("❌ [ButtonPrimary] μƒνƒœ 쑰회 였λ₯˜:", error?.message); + if (isMounted) setVehicleStatus(null); + } finally { + if (isMounted) setStatusLoading(false); + } + }; + + // μ¦‰μ‹œ μ‹€ν–‰ + setStatusLoading(true); + fetchStatus(); + + // 2μ΄ˆλ§ˆλ‹€ κ°±μ‹  + const interval = setInterval(fetchStatus, 2000); + + return () => { + isMounted = false; + clearInterval(interval); + }; + }, [shouldFetchStatus, statusTableName, statusKeyField, statusFieldName, userId, component.label]); + + // λ²„νŠΌ λΉ„ν™œμ„±ν™” 쑰건 계산 + const isOperationButtonDisabled = useMemo(() => { + const actionConfig = component.componentConfig?.action; + + if (actionConfig?.type !== "operation_control") return false; + + // 1. μΆœλ°œμ§€/도착지 ν•„μˆ˜ 체크 + if (actionConfig?.requireLocationFields) { + const departureField = actionConfig.trackingDepartureField || "departure"; + const destinationField = actionConfig.trackingArrivalField || "destination"; + + const departure = formData?.[departureField]; + const destination = formData?.[destinationField]; + + // console.log("πŸ” [ButtonPrimary] μΆœλ°œμ§€/도착지 체크:", { + // departureField, destinationField, departure, destination, + // buttonLabel: component.label + // }); + + if (!departure || departure === "" || !destination || destination === "") { + // console.log("🚫 [ButtonPrimary] μΆœλ°œμ§€/도착지 미선택 β†’ λΉ„ν™œμ„±ν™”:", component.label); + return true; + } + } + + // 2. μƒνƒœ 기반 ν™œμ„±ν™” 쑰건 (API둜 μ‘°νšŒν•œ vehicleStatus μš°μ„  μ‚¬μš©) + if (actionConfig?.enableOnStatusCheck) { + const statusField = actionConfig.statusCheckField || "status"; + // API 쑰회 κ²°κ³Όλ₯Ό μš°μ„  μ‚¬μš© (μ‹€μ‹œκ°„ DB μƒνƒœ 반영) + const currentStatus = vehicleStatus || formData?.[statusField]; + + const conditionType = actionConfig.statusConditionType || "enableOn"; + const conditionValues = (actionConfig.statusConditionValues || "") + .split(",") + .map((v: string) => v.trim()) + .filter((v: string) => v); + + // console.log("πŸ” [ButtonPrimary] μƒνƒœ 쑰건 체크:", { + // statusField, + // formDataStatus: formData?.[statusField], + // apiStatus: vehicleStatus, + // currentStatus, + // conditionType, + // conditionValues, + // buttonLabel: component.label, + // }); + + // μƒνƒœ λ‘œλ”© 쀑이면 λΉ„ν™œμ„±ν™” + if (statusLoading) { + // console.log("⏳ [ButtonPrimary] μƒνƒœ λ‘œλ”© 쀑 β†’ λΉ„ν™œμ„±ν™”:", component.label); + return true; + } + + // μƒνƒœκ°’μ΄ μ—†μœΌλ©΄ β†’ λΉ„ν™œμ„±ν™” (쑰건 확인 λΆˆκ°€) + if (!currentStatus) { + // console.log("🚫 [ButtonPrimary] μƒνƒœκ°’ μ—†μŒ β†’ λΉ„ν™œμ„±ν™”:", component.label); + return true; + } + + if (conditionValues.length > 0) { + if (conditionType === "enableOn") { + // 이 μƒνƒœμΌ λ•Œλ§Œ ν™œμ„±ν™” + if (!conditionValues.includes(currentStatus)) { + // console.log(`🚫 [ButtonPrimary] μƒνƒœ ${currentStatus} βˆ‰ [${conditionValues}] β†’ λΉ„ν™œμ„±ν™”:`, component.label); + return true; + } + } else if (conditionType === "disableOn") { + // 이 μƒνƒœμΌ λ•Œ λΉ„ν™œμ„±ν™” + if (conditionValues.includes(currentStatus)) { + // console.log(`🚫 [ButtonPrimary] μƒνƒœ ${currentStatus} ∈ [${conditionValues}] β†’ λΉ„ν™œμ„±ν™”:`, component.label); + return true; + } + } + } + } + + // console.log("βœ… [ButtonPrimary] λ²„νŠΌ ν™œμ„±ν™”:", component.label); + return false; + }, [component.componentConfig?.action, formData, vehicleStatus, statusLoading, component.label]); + // 확인 λ‹€μ΄μ–Όλ‘œκ·Έ μƒνƒœ const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [pendingAction, setPendingAction] = useState<{ @@ -877,6 +1021,9 @@ export const ButtonPrimaryComponent: React.FC = ({ } } + // πŸ†• μ΅œμ’… λΉ„ν™œμ„±ν™” μƒνƒœ (μ„€μ • + 쑰건뢀 λΉ„ν™œμ„±ν™”) + const finalDisabled = componentConfig.disabled || isOperationButtonDisabled || statusLoading; + // 곡톡 λ²„νŠΌ μŠ€νƒ€μΌ const buttonElementStyle: React.CSSProperties = { width: "100%", @@ -884,12 +1031,12 @@ export const ButtonPrimaryComponent: React.FC = ({ minHeight: "40px", border: "none", borderRadius: "0.5rem", - background: componentConfig.disabled ? "#e5e7eb" : buttonColor, - color: componentConfig.disabled ? "#9ca3af" : "white", + background: finalDisabled ? "#e5e7eb" : buttonColor, + color: finalDisabled ? "#9ca3af" : "white", // πŸ”§ 크기 μ„€μ • 적용 (sm/md/lg) fontSize: componentConfig.size === "sm" ? "0.75rem" : componentConfig.size === "lg" ? "1rem" : "0.875rem", fontWeight: "600", - cursor: componentConfig.disabled ? "not-allowed" : "pointer", + cursor: finalDisabled ? "not-allowed" : "pointer", outline: "none", boxSizing: "border-box", display: "flex", @@ -900,7 +1047,7 @@ export const ButtonPrimaryComponent: React.FC = ({ componentConfig.size === "sm" ? "0 0.75rem" : componentConfig.size === "lg" ? "0 1.25rem" : "0 1rem", margin: "0", lineHeight: "1.25", - boxShadow: componentConfig.disabled ? "none" : "0 1px 2px 0 rgba(0, 0, 0, 0.05)", + boxShadow: finalDisabled ? "none" : "0 1px 2px 0 rgba(0, 0, 0, 0.05)", // λ””μžμΈ λͺ¨λ“œμ™€ μΈν„°λž™ν‹°λΈŒ λͺ¨λ“œ λͺ¨λ‘μ—μ„œ μ‚¬μš©μž μŠ€νƒ€μΌ 적용 (width/height μ œμ™Έ) ...(component.style ? Object.fromEntries( Object.entries(component.style).filter(([key]) => key !== 'width' && key !== 'height') @@ -925,7 +1072,7 @@ export const ButtonPrimaryComponent: React.FC = ({ // 일반 λͺ¨λ“œ: button으둜 λ Œλ”λ§