From ccf8bd3284fcd61ab988f33065ed4bc38aa10045 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Fri, 5 Dec 2025 11:03:15 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B2=84=ED=8A=BC=ED=99=9C=EC=84=B1=ED=99=94?= =?UTF-8?q?=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์œผ๋กœ ๋ Œ๋”๋ง