From f7384cb450fdcb2e2820d9f5d3d436c3fcd3ce10 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 15 Dec 2025 09:25:14 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix(modal-repeater-table):=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=A1=B0=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=20ID=20=ED=83=80=EC=9E=85=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/lib/registry/components/split-panel-layout2/README.md | 1 + .../components/split-panel-layout2/SplitPanelLayout2Renderer.tsx | 1 + .../universal-form-modal/modals/FieldDetailSettingsModal.tsx | 1 + .../components/universal-form-modal/modals/SaveSettingsModal.tsx | 1 + .../universal-form-modal/modals/SectionLayoutModal.tsx | 1 + 5 files changed, 5 insertions(+) diff --git a/frontend/lib/registry/components/split-panel-layout2/README.md b/frontend/lib/registry/components/split-panel-layout2/README.md index f1d8544b..4e5debe8 100644 --- a/frontend/lib/registry/components/split-panel-layout2/README.md +++ b/frontend/lib/registry/components/split-panel-layout2/README.md @@ -100,3 +100,4 @@ - [컴포넌트 시스템 가이드](../../docs/컴포넌트_시스템_가이드.md) - [split-panel-layout (v1)](../split-panel-layout/README.md) + diff --git a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Renderer.tsx b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Renderer.tsx index f582646e..21e70b13 100644 --- a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Renderer.tsx +++ b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Renderer.tsx @@ -40,3 +40,4 @@ export class SplitPanelLayout2Renderer extends AutoRegisteringComponentRenderer // 자동 등록 실행 SplitPanelLayout2Renderer.registerSelf(); + diff --git a/frontend/lib/registry/components/universal-form-modal/modals/FieldDetailSettingsModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/FieldDetailSettingsModal.tsx index 719a99e3..751ac2c6 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/FieldDetailSettingsModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/FieldDetailSettingsModal.tsx @@ -840,3 +840,4 @@ export function FieldDetailSettingsModal({ ); } + diff --git a/frontend/lib/registry/components/universal-form-modal/modals/SaveSettingsModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/SaveSettingsModal.tsx index 9d269c62..27ee00ff 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/SaveSettingsModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/SaveSettingsModal.tsx @@ -794,3 +794,4 @@ export function SaveSettingsModal({ ); } + diff --git a/frontend/lib/registry/components/universal-form-modal/modals/SectionLayoutModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/SectionLayoutModal.tsx index fe981260..dfdecbc0 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/SectionLayoutModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/SectionLayoutModal.tsx @@ -514,3 +514,4 @@ export function SectionLayoutModal({ ); } + From 16885225a020d7a53de556a486b9af2260ac8e07 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 15 Dec 2025 14:46:32 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat(edit-modal):=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=ED=9B=84=20=EC=A0=9C=EC=96=B4=EB=A1=9C?= =?UTF-8?q?=EC=A7=81(=EB=85=B8=EB=93=9C=20=ED=94=8C=EB=A1=9C=EC=9A=B0)=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=8B=A4=ED=96=89=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EditModal에서 INSERT/UPDATE/그룹 저장 완료 후 제어로직 자동 실행 - loadSaveButtonConfig(): 모달 내부 저장 버튼의 제어로직 설정 조회 - findSaveButtonInComponents(): 재귀적으로 저장 버튼 탐색 (conditional-container 내부 포함) - buttonActions.ts: openEditModal 이벤트에 buttonConfig, buttonContext 전달 - executeAfterSaveControl()을 public으로 변경하여 외부 호출 가능 - 제어로직 실행 오류 시 저장 성공 유지, 경고 토스트만 표시 --- frontend/components/screen/EditModal.tsx | 234 +++++++++++++++++- .../button-primary/ButtonPrimaryComponent.tsx | 12 + frontend/lib/utils/buttonActions.ts | 5 +- 3 files changed, 248 insertions(+), 3 deletions(-) diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 294bca7f..5a123b3f 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -26,12 +26,56 @@ interface EditModalState { onSave?: () => void; groupByColumns?: string[]; // 🆕 그룹핑 컬럼 (예: ["order_no"]) tableName?: string; // 🆕 테이블명 (그룹 조회용) + buttonConfig?: any; // 🆕 버튼 설정 (제어로직 실행용) + buttonContext?: any; // 🆕 버튼 컨텍스트 (screenId, userId 등) + saveButtonConfig?: { + enableDataflowControl?: boolean; + dataflowConfig?: any; + dataflowTiming?: string; + }; // 🆕 모달 내부 저장 버튼의 제어로직 설정 } interface EditModalProps { className?: string; } +/** + * 모달 내부에서 저장 버튼 찾기 (재귀적으로 탐색) + * action.type이 "save"인 button-primary 컴포넌트를 찾음 + */ +const findSaveButtonInComponents = (components: any[]): any | null => { + if (!components || !Array.isArray(components)) return null; + + for (const comp of components) { + // button-primary이고 action.type이 save인 경우 + if ( + comp.componentType === "button-primary" && + comp.componentConfig?.action?.type === "save" + ) { + return comp; + } + + // conditional-container의 sections 내부 탐색 + if (comp.componentType === "conditional-container" && comp.componentConfig?.sections) { + for (const section of comp.componentConfig.sections) { + if (section.screenId) { + // 조건부 컨테이너의 내부 화면은 별도로 로드해야 함 + // 여기서는 null 반환하고, loadSaveButtonConfig에서 처리 + continue; + } + } + } + + // 자식 컴포넌트가 있으면 재귀 탐색 + if (comp.children && Array.isArray(comp.children)) { + const found = findSaveButtonInComponents(comp.children); + if (found) return found; + } + } + + return null; +}; + export const EditModal: React.FC = ({ className }) => { const { user } = useAuth(); const [modalState, setModalState] = useState({ @@ -44,6 +88,9 @@ export const EditModal: React.FC = ({ className }) => { onSave: undefined, groupByColumns: undefined, tableName: undefined, + buttonConfig: undefined, + buttonContext: undefined, + saveButtonConfig: undefined, }); const [screenData, setScreenData] = useState<{ @@ -115,10 +162,88 @@ export const EditModal: React.FC = ({ className }) => { }; }; + // 🆕 모달 내부 저장 버튼의 제어로직 설정 조회 + const loadSaveButtonConfig = async (targetScreenId: number): Promise<{ + enableDataflowControl?: boolean; + dataflowConfig?: any; + dataflowTiming?: string; + } | null> => { + try { + // 1. 대상 화면의 레이아웃 조회 + const layoutData = await screenApi.getLayout(targetScreenId); + + if (!layoutData?.components) { + console.log("[EditModal] 레이아웃 컴포넌트 없음:", targetScreenId); + return null; + } + + // 2. 저장 버튼 찾기 + let saveButton = findSaveButtonInComponents(layoutData.components); + + // 3. conditional-container가 있는 경우 내부 화면도 탐색 + if (!saveButton) { + for (const comp of layoutData.components) { + if (comp.componentType === "conditional-container" && comp.componentConfig?.sections) { + for (const section of comp.componentConfig.sections) { + if (section.screenId) { + try { + const innerLayoutData = await screenApi.getLayout(section.screenId); + saveButton = findSaveButtonInComponents(innerLayoutData?.components || []); + if (saveButton) { + console.log("[EditModal] 조건부 컨테이너 내부에서 저장 버튼 발견:", { + sectionScreenId: section.screenId, + sectionLabel: section.label, + }); + break; + } + } catch (innerError) { + console.warn("[EditModal] 내부 화면 레이아웃 조회 실패:", section.screenId); + } + } + } + if (saveButton) break; + } + } + } + + if (!saveButton) { + console.log("[EditModal] 저장 버튼을 찾을 수 없음:", targetScreenId); + return null; + } + + // 4. webTypeConfig에서 제어로직 설정 추출 + const webTypeConfig = saveButton.webTypeConfig; + if (webTypeConfig?.enableDataflowControl) { + const config = { + enableDataflowControl: webTypeConfig.enableDataflowControl, + dataflowConfig: webTypeConfig.dataflowConfig, + dataflowTiming: webTypeConfig.dataflowConfig?.flowConfig?.executionTiming || "after", + }; + console.log("[EditModal] 저장 버튼 제어로직 설정 발견:", config); + return config; + } + + console.log("[EditModal] 저장 버튼에 제어로직 설정 없음"); + return null; + } catch (error) { + console.warn("[EditModal] 저장 버튼 설정 조회 실패:", error); + return null; + } + }; + // 전역 모달 이벤트 리스너 useEffect(() => { - const handleOpenEditModal = (event: CustomEvent) => { - const { screenId, title, description, modalSize, editData, onSave, groupByColumns, tableName, isCreateMode } = event.detail; + const handleOpenEditModal = async (event: CustomEvent) => { + const { screenId, title, description, modalSize, editData, onSave, groupByColumns, tableName, isCreateMode, buttonConfig, buttonContext } = event.detail; + + // 🆕 모달 내부 저장 버튼의 제어로직 설정 조회 + let saveButtonConfig: EditModalState["saveButtonConfig"] = undefined; + if (screenId) { + const config = await loadSaveButtonConfig(screenId); + if (config) { + saveButtonConfig = config; + } + } setModalState({ isOpen: true, @@ -130,6 +255,9 @@ export const EditModal: React.FC = ({ className }) => { onSave, groupByColumns, // 🆕 그룹핑 컬럼 tableName, // 🆕 테이블명 + buttonConfig, // 🆕 버튼 설정 + buttonContext, // 🆕 버튼 컨텍스트 + saveButtonConfig, // 🆕 모달 내부 저장 버튼의 제어로직 설정 }); // 편집 데이터로 폼 데이터 초기화 @@ -581,6 +709,46 @@ export const EditModal: React.FC = ({ className }) => { } } + // 🆕 저장 후 제어로직 실행 (버튼의 After 타이밍 제어) + // 우선순위: 모달 내부 저장 버튼 설정(saveButtonConfig) > 수정 버튼에서 전달받은 설정(buttonConfig) + try { + const controlConfig = modalState.saveButtonConfig || modalState.buttonConfig; + + console.log("[EditModal] 그룹 저장 완료 후 제어로직 실행 시도", { + hasSaveButtonConfig: !!modalState.saveButtonConfig, + hasButtonConfig: !!modalState.buttonConfig, + controlConfig, + }); + + if (controlConfig?.enableDataflowControl && controlConfig?.dataflowTiming === "after") { + console.log("🎯 [EditModal] 저장 후 제어로직 발견:", controlConfig.dataflowConfig); + + // buttonActions의 executeAfterSaveControl 동적 import + const { ButtonActionExecutor } = await import("@/lib/utils/buttonActions"); + + // 제어로직 실행 + await ButtonActionExecutor.executeAfterSaveControl( + controlConfig, + { + formData: modalState.editData, + screenId: modalState.buttonContext?.screenId || modalState.screenId, + tableName: modalState.buttonContext?.tableName || screenData?.screenInfo?.tableName, + userId: user?.userId, + companyCode: user?.companyCode, + onRefresh: modalState.onSave, + } + ); + + console.log("✅ [EditModal] 제어로직 실행 완료"); + } else { + console.log("ℹ️ [EditModal] 저장 후 실행할 제어로직 없음"); + } + } catch (controlError) { + console.error("❌ [EditModal] 제어로직 실행 오류:", controlError); + // 제어로직 오류는 저장 성공을 방해하지 않음 (경고만 표시) + toast.warning("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다."); + } + handleClose(); } else { toast.info("변경된 내용이 없습니다."); @@ -615,6 +783,37 @@ export const EditModal: React.FC = ({ className }) => { } } + // 🆕 저장 후 제어로직 실행 (버튼의 After 타이밍 제어) + // 우선순위: 모달 내부 저장 버튼 설정(saveButtonConfig) > 수정 버튼에서 전달받은 설정(buttonConfig) + try { + const controlConfig = modalState.saveButtonConfig || modalState.buttonConfig; + + console.log("[EditModal] INSERT 완료 후 제어로직 실행 시도", { controlConfig }); + + if (controlConfig?.enableDataflowControl && controlConfig?.dataflowTiming === "after") { + console.log("🎯 [EditModal] 저장 후 제어로직 발견:", controlConfig.dataflowConfig); + + const { ButtonActionExecutor } = await import("@/lib/utils/buttonActions"); + + await ButtonActionExecutor.executeAfterSaveControl( + controlConfig, + { + formData, + screenId: modalState.buttonContext?.screenId || modalState.screenId, + tableName: modalState.buttonContext?.tableName || screenData?.screenInfo?.tableName, + userId: user?.userId, + companyCode: user?.companyCode, + onRefresh: modalState.onSave, + } + ); + + console.log("✅ [EditModal] 제어로직 실행 완료"); + } + } catch (controlError) { + console.error("❌ [EditModal] 제어로직 실행 오류:", controlError); + toast.warning("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다."); + } + handleClose(); } else { throw new Error(response.message || "생성에 실패했습니다."); @@ -657,6 +856,37 @@ export const EditModal: React.FC = ({ className }) => { } } + // 🆕 저장 후 제어로직 실행 (버튼의 After 타이밍 제어) + // 우선순위: 모달 내부 저장 버튼 설정(saveButtonConfig) > 수정 버튼에서 전달받은 설정(buttonConfig) + try { + const controlConfig = modalState.saveButtonConfig || modalState.buttonConfig; + + console.log("[EditModal] UPDATE 완료 후 제어로직 실행 시도", { controlConfig }); + + if (controlConfig?.enableDataflowControl && controlConfig?.dataflowTiming === "after") { + console.log("🎯 [EditModal] 저장 후 제어로직 발견:", controlConfig.dataflowConfig); + + const { ButtonActionExecutor } = await import("@/lib/utils/buttonActions"); + + await ButtonActionExecutor.executeAfterSaveControl( + controlConfig, + { + formData, + screenId: modalState.buttonContext?.screenId || modalState.screenId, + tableName: modalState.buttonContext?.tableName || screenData?.screenInfo?.tableName, + userId: user?.userId, + companyCode: user?.companyCode, + onRefresh: modalState.onSave, + } + ); + + console.log("✅ [EditModal] 제어로직 실행 완료"); + } + } catch (controlError) { + console.error("❌ [EditModal] 제어로직 실행 오류:", controlError); + toast.warning("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다."); + } + handleClose(); } else { throw new Error(response.message || "수정에 실패했습니다."); diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index 1942d268..4b88c565 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -376,6 +376,7 @@ export const ButtonPrimaryComponent: React.FC = ({ // 🔥 제어관리 설정 추가 (webTypeConfig에서 가져옴) enableDataflowControl: component.webTypeConfig?.enableDataflowControl, dataflowConfig: component.webTypeConfig?.dataflowConfig, + dataflowTiming: component.webTypeConfig?.dataflowTiming, }; } else if (componentConfig.action && typeof componentConfig.action === "object") { // 🔥 이미 객체인 경우에도 제어관리 설정 추가 @@ -383,8 +384,19 @@ export const ButtonPrimaryComponent: React.FC = ({ ...componentConfig.action, enableDataflowControl: component.webTypeConfig?.enableDataflowControl, dataflowConfig: component.webTypeConfig?.dataflowConfig, + dataflowTiming: component.webTypeConfig?.dataflowTiming, }; } + + // 🔍 디버깅: processedConfig.action 확인 + console.log("[ButtonPrimaryComponent] processedConfig.action 생성 완료", { + actionType: processedConfig.action?.type, + enableDataflowControl: processedConfig.action?.enableDataflowControl, + dataflowTiming: processedConfig.action?.dataflowTiming, + dataflowConfig: processedConfig.action?.dataflowConfig, + webTypeConfigRaw: component.webTypeConfig, + componentText: component.text, + }); // 스타일 계산 // height: 100%로 부모(RealtimePreviewDynamic의 내부 div)의 높이를 따라감 diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 1ced2836..ede92868 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -2086,6 +2086,8 @@ export class ButtonActionExecutor { editData: rowData, groupByColumns: groupByColumns.length > 0 ? groupByColumns : undefined, // 🆕 그룹핑 컬럼 전달 tableName: context.tableName, // 🆕 테이블명 전달 + buttonConfig: config, // 🆕 버튼 설정 전달 (제어로직 실행용) + buttonContext: context, // 🆕 버튼 컨텍스트 전달 (screenId, userId 등) onSave: () => { context.onRefresh?.(); }, @@ -2621,8 +2623,9 @@ export class ButtonActionExecutor { /** * 저장 후 제어 실행 (After Timing) + * EditModal 등 외부에서도 호출 가능하도록 public으로 변경 */ - private static async executeAfterSaveControl( + public static async executeAfterSaveControl( config: ButtonActionConfig, context: ButtonActionContext, ): Promise { From c1be1893f5432789d5af22738b0d853e7287208d Mon Sep 17 00:00:00 2001 From: leeheejin Date: Mon, 15 Dec 2025 16:45:26 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=EC=A7=80=EA=B8=88=EC=9D=80=20=EC=B6=9C?= =?UTF-8?q?=EB=B0=9C=EC=A7=80=EB=AA=A9=EC=A0=81=EC=A7=80=EA=B0=80=20?= =?UTF-8?q?=EB=8B=AC=EB=9D=BC=EB=8F=84=20=EA=B0=95=EC=A0=9C=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=9D=B4=20=EA=B0=80=EB=8A=A5=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/lib/utils/buttonActions.ts | 64 +++++++++++++++-------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index c039d55a..4d5915e9 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -4116,39 +4116,43 @@ export class ButtonActionExecutor { try { console.log("🛑 [handleTrackingStop] 위치 추적 종료:", { config, context }); - // 추적 중인지 확인 - if (!this.trackingIntervalId) { - toast.warning("진행 중인 위치 추적이 없습니다."); - return false; + // 추적 중인지 확인 (새로고침 후에도 DB 상태 기반 종료 가능하도록 수정) + const isTrackingActive = !!this.trackingIntervalId; + + if (!isTrackingActive) { + // 추적 중이 아니어도 DB 상태 변경은 진행 (새로고침 후 종료 지원) + console.log("⚠️ [handleTrackingStop] trackingIntervalId 없음 - DB 상태 기반 종료 진행"); + } else { + // 타이머 정리 (추적 중인 경우에만) + clearInterval(this.trackingIntervalId); + this.trackingIntervalId = null; } - // 타이머 정리 - clearInterval(this.trackingIntervalId); - this.trackingIntervalId = null; - const tripId = this.currentTripId; - // 마지막 위치 저장 (trip_status를 completed로) - const departure = - this.trackingContext?.formData?.[this.trackingConfig?.trackingDepartureField || "departure"] || null; - const arrival = this.trackingContext?.formData?.[this.trackingConfig?.trackingArrivalField || "arrival"] || null; - const departureName = this.trackingContext?.formData?.["departure_name"] || null; - const destinationName = this.trackingContext?.formData?.["destination_name"] || null; - const vehicleId = - this.trackingContext?.formData?.[this.trackingConfig?.trackingVehicleIdField || "vehicle_id"] || null; + // 마지막 위치 저장 (추적 중이었던 경우에만) + if (isTrackingActive) { + const departure = + this.trackingContext?.formData?.[this.trackingConfig?.trackingDepartureField || "departure"] || null; + const arrival = this.trackingContext?.formData?.[this.trackingConfig?.trackingArrivalField || "arrival"] || null; + const departureName = this.trackingContext?.formData?.["departure_name"] || null; + const destinationName = this.trackingContext?.formData?.["destination_name"] || null; + const vehicleId = + this.trackingContext?.formData?.[this.trackingConfig?.trackingVehicleIdField || "vehicle_id"] || null; - await this.saveLocationToHistory( - tripId, - departure, - arrival, - departureName, - destinationName, - vehicleId, - "completed", - ); + await this.saveLocationToHistory( + tripId, + departure, + arrival, + departureName, + destinationName, + vehicleId, + "completed", + ); + } - // 🆕 거리/시간 계산 및 저장 - if (tripId) { + // 🆕 거리/시간 계산 및 저장 (추적 중이었던 경우에만) + if (isTrackingActive && tripId) { try { const tripStats = await this.calculateTripStats(tripId); console.log("📊 운행 통계:", tripStats); @@ -4260,9 +4264,9 @@ export class ButtonActionExecutor { } } - // 상태 변경 (vehicles 테이블 등) - const effectiveConfig = config.trackingStatusOnStop ? config : this.trackingConfig; - const effectiveContext = context.userId ? context : this.trackingContext; + // 상태 변경 (vehicles 테이블 등) - 새로고침 후에도 동작하도록 config 우선 사용 + const effectiveConfig = config.trackingStatusOnStop ? config : this.trackingConfig || config; + const effectiveContext = context.userId ? context : this.trackingContext || context; if (effectiveConfig?.trackingStatusOnStop && effectiveConfig?.trackingStatusField && effectiveContext) { try { From 7f15861b6efdc4be791e9468a479b687dcdd3b0d Mon Sep 17 00:00:00 2001 From: leeheejin Date: Mon, 15 Dec 2025 16:54:03 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=EC=B6=9C=EB=B0=9C=EC=A7=80=EB=8F=84?= =?UTF-8?q?=EC=B0=A9=EC=A7=80=20=EB=94=94=EB=B9=84=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=81=8C=EC=96=B4=EC=98=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LocationSwapSelectorComponent.tsx | 99 ++++++++++++++++++- .../LocationSwapSelectorConfigPanel.tsx | 54 ++++++++++ frontend/lib/utils/buttonActions.ts | 43 +++++++- 3 files changed, 191 insertions(+), 5 deletions(-) diff --git a/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorComponent.tsx b/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorComponent.tsx index 3f1a723b..7a693ad5 100644 --- a/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorComponent.tsx +++ b/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorComponent.tsx @@ -53,6 +53,9 @@ export interface LocationSwapSelectorProps { formData?: Record; onFormDataChange?: (field: string, value: any) => void; + // 🆕 사용자 정보 (DB에서 초기값 로드용) + userId?: string; + // componentConfig (화면 디자이너에서 전달) componentConfig?: { dataSource?: DataSourceConfig; @@ -65,6 +68,10 @@ export interface LocationSwapSelectorProps { showSwapButton?: boolean; swapButtonPosition?: "center" | "right"; variant?: "card" | "inline" | "minimal"; + // 🆕 DB 초기값 로드 설정 + loadFromDb?: boolean; // DB에서 초기값 로드 여부 + dbTableName?: string; // 조회할 테이블명 (기본: vehicles) + dbKeyField?: string; // 키 필드 (기본: user_id) }; } @@ -80,6 +87,7 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps) formData = {}, onFormDataChange, componentConfig, + userId, } = props; // componentConfig에서 설정 가져오기 (우선순위: componentConfig > props) @@ -93,6 +101,11 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps) const destinationLabel = config.destinationLabel || props.destinationLabel || "도착지"; const showSwapButton = config.showSwapButton !== false && props.showSwapButton !== false; const variant = config.variant || props.variant || "card"; + + // 🆕 DB 초기값 로드 설정 + const loadFromDb = config.loadFromDb !== false; // 기본값 true + const dbTableName = config.dbTableName || "vehicles"; + const dbKeyField = config.dbKeyField || "user_id"; // 기본 옵션 (포항/광양) const DEFAULT_OPTIONS: LocationOption[] = [ @@ -104,6 +117,7 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps) const [options, setOptions] = useState(DEFAULT_OPTIONS); const [loading, setLoading] = useState(false); const [isSwapping, setIsSwapping] = useState(false); + const [dbLoaded, setDbLoaded] = useState(false); // DB 로드 완료 여부 // 로컬 선택 상태 (Select 컴포넌트용) const [localDeparture, setLocalDeparture] = useState(""); @@ -193,8 +207,89 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps) loadOptions(); }, [dataSource, isDesignMode]); - // formData에서 초기값 동기화 + // 🆕 DB에서 초기값 로드 (새로고침 시에도 출발지/목적지 유지) useEffect(() => { + const loadFromDatabase = async () => { + // 디자인 모드이거나, DB 로드 비활성화이거나, userId가 없으면 스킵 + if (isDesignMode || !loadFromDb || !userId) { + console.log("[LocationSwapSelector] DB 로드 스킵:", { isDesignMode, loadFromDb, userId }); + return; + } + + // 이미 로드했으면 스킵 + if (dbLoaded) { + return; + } + + try { + console.log("[LocationSwapSelector] DB에서 출발지/목적지 로드 시작:", { dbTableName, dbKeyField, userId }); + + const response = await apiClient.post( + `/table-management/tables/${dbTableName}/data`, + { + page: 1, + size: 1, + search: { [dbKeyField]: userId }, + autoFilter: true, + } + ); + + const vehicleData = response.data?.data?.data?.[0] || response.data?.data?.rows?.[0]; + + if (vehicleData) { + const dbDeparture = vehicleData[departureField] || vehicleData.departure; + const dbDestination = vehicleData[destinationField] || vehicleData.arrival || vehicleData.destination; + + console.log("[LocationSwapSelector] DB에서 로드된 값:", { dbDeparture, dbDestination }); + + // DB에 값이 있으면 로컬 상태 및 formData 업데이트 + if (dbDeparture && options.some(o => o.value === dbDeparture)) { + setLocalDeparture(dbDeparture); + onFormDataChange?.(departureField, dbDeparture); + + // 라벨도 업데이트 + if (departureLabelField) { + const opt = options.find(o => o.value === dbDeparture); + if (opt) { + onFormDataChange?.(departureLabelField, opt.label); + } + } + } + + if (dbDestination && options.some(o => o.value === dbDestination)) { + setLocalDestination(dbDestination); + onFormDataChange?.(destinationField, dbDestination); + + // 라벨도 업데이트 + if (destinationLabelField) { + const opt = options.find(o => o.value === dbDestination); + if (opt) { + onFormDataChange?.(destinationLabelField, opt.label); + } + } + } + } + + setDbLoaded(true); + } catch (error) { + console.error("[LocationSwapSelector] DB 로드 실패:", error); + setDbLoaded(true); // 실패해도 다시 시도하지 않음 + } + }; + + // 옵션이 로드된 후에 DB 로드 실행 + if (options.length > 0) { + loadFromDatabase(); + } + }, [userId, loadFromDb, dbTableName, dbKeyField, departureField, destinationField, options, isDesignMode, dbLoaded, onFormDataChange, departureLabelField, destinationLabelField]); + + // formData에서 초기값 동기화 (DB 로드 후에도 formData 변경 시 반영) + useEffect(() => { + // DB 로드가 완료되지 않았으면 스킵 (DB 값 우선) + if (loadFromDb && userId && !dbLoaded) { + return; + } + const depVal = formData[departureField]; const destVal = formData[destinationField]; @@ -204,7 +299,7 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps) if (destVal && options.some(o => o.value === destVal)) { setLocalDestination(destVal); } - }, [formData, departureField, destinationField, options]); + }, [formData, departureField, destinationField, options, loadFromDb, userId, dbLoaded]); // 출발지 변경 const handleDepartureChange = (selectedValue: string) => { diff --git a/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorConfigPanel.tsx b/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorConfigPanel.tsx index 4e21cddf..cd84806f 100644 --- a/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorConfigPanel.tsx +++ b/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorConfigPanel.tsx @@ -470,6 +470,58 @@ export function LocationSwapSelectorConfigPanel({ + {/* DB 초기값 로드 설정 */} +
+

DB 초기값 로드

+

+ 새로고침 시에도 DB에 저장된 출발지/목적지를 자동으로 불러옵니다 +

+ +
+ + handleChange("loadFromDb", checked)} + /> +
+ + {config?.loadFromDb !== false && ( + <> +
+ + +
+ +
+ + handleChange("dbKeyField", e.target.value)} + placeholder="user_id" + /> +

+ 현재 사용자 ID로 조회할 필드 (기본: user_id) +

+
+ + )} +
+ {/* 안내 */}

@@ -480,6 +532,8 @@ export function LocationSwapSelectorConfigPanel({ 2. 출발지/도착지 값이 저장될 필드를 지정합니다
3. 교환 버튼을 클릭하면 출발지와 도착지가 바뀝니다 +
+ 4. DB 초기값 로드를 활성화하면 새로고침 후에도 값이 유지됩니다

diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 4d5915e9..e1573998 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -4130,14 +4130,51 @@ export class ButtonActionExecutor { const tripId = this.currentTripId; + // 🆕 DB에서 출발지/목적지 조회 (운전자가 중간에 바꿔도 원래 값 사용) + let dbDeparture: string | null = null; + let dbArrival: string | null = null; + let dbVehicleId: string | null = null; + + const userId = context.userId || this.trackingUserId; + if (userId) { + try { + const { apiClient } = await import("@/lib/api/client"); + const statusTableName = config.trackingStatusTableName || this.trackingConfig?.trackingStatusTableName || context.tableName || "vehicles"; + const keyField = config.trackingStatusKeyField || this.trackingConfig?.trackingStatusKeyField || "user_id"; + + // DB에서 현재 차량 정보 조회 + const vehicleResponse = await apiClient.post( + `/table-management/tables/${statusTableName}/data`, + { + page: 1, + size: 1, + search: { [keyField]: userId }, + autoFilter: true, + }, + ); + + const vehicleData = vehicleResponse.data?.data?.data?.[0] || vehicleResponse.data?.data?.rows?.[0]; + if (vehicleData) { + dbDeparture = vehicleData.departure || null; + dbArrival = vehicleData.arrival || null; + dbVehicleId = vehicleData.id || vehicleData.vehicle_id || null; + console.log("📍 [handleTrackingStop] DB에서 출발지/목적지 조회:", { dbDeparture, dbArrival, dbVehicleId }); + } + } catch (dbError) { + console.warn("⚠️ [handleTrackingStop] DB 조회 실패, formData 사용:", dbError); + } + } + // 마지막 위치 저장 (추적 중이었던 경우에만) if (isTrackingActive) { - const departure = + // DB 값 우선, 없으면 formData 사용 + const departure = dbDeparture || this.trackingContext?.formData?.[this.trackingConfig?.trackingDepartureField || "departure"] || null; - const arrival = this.trackingContext?.formData?.[this.trackingConfig?.trackingArrivalField || "arrival"] || null; + const arrival = dbArrival || + this.trackingContext?.formData?.[this.trackingConfig?.trackingArrivalField || "arrival"] || null; const departureName = this.trackingContext?.formData?.["departure_name"] || null; const destinationName = this.trackingContext?.formData?.["destination_name"] || null; - const vehicleId = + const vehicleId = dbVehicleId || this.trackingContext?.formData?.[this.trackingConfig?.trackingVehicleIdField || "vehicle_id"] || null; await this.saveLocationToHistory(