diff --git a/backend-node/src/routes/dynamicFormRoutes.ts b/backend-node/src/routes/dynamicFormRoutes.ts index 21140617..dae17283 100644 --- a/backend-node/src/routes/dynamicFormRoutes.ts +++ b/backend-node/src/routes/dynamicFormRoutes.ts @@ -22,9 +22,9 @@ router.use(authenticateToken); // 폼 데이터 CRUD router.post("/save", saveFormData); // 기존 버전 (레거시 지원) router.post("/save-enhanced", saveFormDataEnhanced); // 개선된 버전 +router.put("/update-field", updateFieldValue); // 특정 필드만 업데이트 (다른 테이블 지원) - /:id 보다 먼저 선언! router.put("/:id", updateFormData); router.patch("/:id/partial", updateFormDataPartial); // 부분 업데이트 -router.put("/update-field", updateFieldValue); // 특정 필드만 업데이트 (다른 테이블 지원) router.delete("/:id", deleteFormData); router.get("/:id", getFormData); diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 11648577..7d03f257 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -1662,12 +1662,47 @@ export class DynamicFormService { companyCode, }); - // 멀티테넌시: company_code 조건 추가 (최고관리자는 제외) - let whereClause = `"${keyField}" = $1`; - const params: any[] = [keyValue, updateValue, userId]; - let paramIndex = 4; + // 테이블 컬럼 정보 조회 (updated_by, updated_at 존재 여부 확인) + const columnQuery = ` + SELECT column_name + FROM information_schema.columns + WHERE table_name = $1 AND column_name IN ('updated_by', 'updated_at', 'company_code') + `; + const columnResult = await client.query(columnQuery, [tableName]); + const existingColumns = columnResult.rows.map((row: any) => row.column_name); + + const hasUpdatedBy = existingColumns.includes('updated_by'); + const hasUpdatedAt = existingColumns.includes('updated_at'); + const hasCompanyCode = existingColumns.includes('company_code'); - if (companyCode && companyCode !== "*") { + console.log("🔍 [updateFieldValue] 테이블 컬럼 확인:", { + hasUpdatedBy, + hasUpdatedAt, + hasCompanyCode, + }); + + // 동적 SET 절 구성 + let setClause = `"${updateField}" = $1`; + const params: any[] = [updateValue]; + let paramIndex = 2; + + if (hasUpdatedBy) { + setClause += `, updated_by = $${paramIndex}`; + params.push(userId); + paramIndex++; + } + + if (hasUpdatedAt) { + setClause += `, updated_at = NOW()`; + } + + // WHERE 절 구성 + let whereClause = `"${keyField}" = $${paramIndex}`; + params.push(keyValue); + paramIndex++; + + // 멀티테넌시: company_code 조건 추가 (최고관리자는 제외, 컬럼이 있는 경우만) + if (hasCompanyCode && companyCode && companyCode !== "*") { whereClause += ` AND company_code = $${paramIndex}`; params.push(companyCode); paramIndex++; @@ -1675,9 +1710,7 @@ export class DynamicFormService { const sqlQuery = ` UPDATE "${tableName}" - SET "${updateField}" = $2, - updated_by = $3, - updated_at = NOW() + SET ${setClause} WHERE ${whereClause} `; diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 1a4a9608..5c9360c8 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -1774,6 +1774,255 @@ export const ButtonConfigPanel: React.FC = ({ /> + {/* 첫 번째 추가 테이블 설정 (위치정보와 함께 상태 변경) */} +
+
+
+ +

위치정보와 함께 다른 테이블의 필드 값을 변경합니다

+
+ onUpdateProperty("componentConfig.action.geolocationUpdateField", checked)} + /> +
+ + {config.action?.geolocationUpdateField && ( +
+
+ + +
+
+
+ + onUpdateProperty("componentConfig.action.geolocationExtraField", e.target.value)} + className="h-8 text-xs" + /> +
+
+ + onUpdateProperty("componentConfig.action.geolocationExtraValue", e.target.value)} + className="h-8 text-xs" + /> +
+
+
+
+ + onUpdateProperty("componentConfig.action.geolocationExtraKeyField", e.target.value)} + className="h-8 text-xs" + /> +
+
+ + +
+
+
+ )} +
+ + {/* 두 번째 추가 테이블 설정 */} +
+
+
+ +

두 번째 테이블의 필드 값도 함께 변경합니다

+
+ onUpdateProperty("componentConfig.action.geolocationSecondTableEnabled", checked)} + /> +
+ + {config.action?.geolocationSecondTableEnabled && ( +
+
+
+ + +
+
+ + +
+
+
+
+ + onUpdateProperty("componentConfig.action.geolocationSecondField", e.target.value)} + className="h-8 text-xs" + /> +
+
+ + onUpdateProperty("componentConfig.action.geolocationSecondValue", e.target.value)} + className="h-8 text-xs" + /> +
+
+
+
+ + onUpdateProperty("componentConfig.action.geolocationSecondKeyField", e.target.value)} + className="h-8 text-xs" + /> +
+
+ + +
+
+ + {config.action?.geolocationSecondMode === "insert" && ( +
+
+ +

위도/경도를 이 테이블에도 저장

+
+ onUpdateProperty("componentConfig.action.geolocationSecondInsertFields", { + ...config.action?.geolocationSecondInsertFields, + includeLocation: checked + })} + /> +
+ )} + +

+ {config.action?.geolocationSecondMode === "insert" + ? "새 레코드를 생성합니다. 연결 필드로 현재 폼 데이터와 연결됩니다." + : "기존 레코드를 수정합니다. 키 필드로 레코드를 찾아 값을 변경합니다."} +

+
+ )} +
+

사용 방법: @@ -1784,6 +2033,11 @@ export const ButtonConfigPanel: React.FC = ({
3. 위도/경도가 지정된 필드에 자동으로 입력됩니다
+ 4. 추가 테이블 설정이 있으면 해당 테이블의 필드도 함께 변경됩니다 +
+
+ 예시: 위치정보 저장 + vehicles.status를 inactive로 변경 +

참고: HTTPS 환경에서만 위치정보가 작동합니다.

@@ -1852,6 +2106,62 @@ export const ButtonConfigPanel: React.FC = ({
+ {/* 🆕 키 필드 설정 (레코드 식별용) */} +
+
레코드 식별 설정
+
+
+ + onUpdateProperty("componentConfig.action.updateKeyField", e.target.value)} + className="h-8 text-xs" + /> +

레코드를 찾을 DB 컬럼명

+
+
+ + +

키 값을 가져올 소스

+
+
+
+
@@ -1899,15 +2209,78 @@ export const ButtonConfigPanel: React.FC = ({
+ {/* 위치정보 수집 옵션 */} +
+
+
+ +

상태 변경과 함께 현재 GPS 좌표를 수집합니다

+
+ onUpdateProperty("componentConfig.action.updateWithGeolocation", checked)} + /> +
+ + {config.action?.updateWithGeolocation && ( +
+
+
+ + onUpdateProperty("componentConfig.action.updateGeolocationLatField", e.target.value)} + className="h-8 text-xs" + /> +
+
+ + onUpdateProperty("componentConfig.action.updateGeolocationLngField", e.target.value)} + className="h-8 text-xs" + /> +
+
+
+
+ + onUpdateProperty("componentConfig.action.updateGeolocationAccuracyField", e.target.value)} + className="h-8 text-xs" + /> +
+
+ + onUpdateProperty("componentConfig.action.updateGeolocationTimestampField", e.target.value)} + className="h-8 text-xs" + /> +
+
+

+ 버튼 클릭 시 GPS 위치를 수집하여 위 필드에 저장합니다. +

+
+ )} +
+

사용 예시:
- - 운행알림 버튼: status 필드를 "active"로 변경 + - 운행알림 버튼: status를 "active"로 + 위치정보 수집
- - 승인 버튼: approval_status 필드를 "approved"로 변경 + - 출발 버튼: status를 "inactive"로 + 위치정보 수집
- - 완료 버튼: is_completed 필드를 "Y"로 변경 + - 완료 버튼: is_completed를 "Y"로 변경

diff --git a/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorConfigPanel.tsx b/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorConfigPanel.tsx index 518b6172..4e21cddf 100644 --- a/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorConfigPanel.tsx +++ b/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorConfigPanel.tsx @@ -90,7 +90,7 @@ export function LocationSwapSelectorConfigPanel({ } }, [config?.dataSource?.tableName, config?.dataSource?.type]); - // 코드 카테고리 로드 + // 코드 카테고리 로드 (API가 없을 수 있으므로 에러 무시) useEffect(() => { const loadCodeCategories = async () => { try { @@ -103,8 +103,11 @@ export function LocationSwapSelectorConfigPanel({ })) ); } - } catch (error) { - console.error("코드 카테고리 로드 실패:", error); + } catch (error: any) { + // 404는 API가 없는 것이므로 무시 + if (error?.response?.status !== 404) { + console.error("코드 카테고리 로드 실패:", error); + } } }; loadCodeCategories(); @@ -368,14 +371,14 @@ export function LocationSwapSelectorConfigPanel({ {tableColumns.length > 0 ? ( handleChange("destinationLabelField", value)} + value={config?.destinationLabelField || "__none__"} + onValueChange={(value) => handleChange("destinationLabelField", value === "__none__" ? "" : value)} > - 없음 + 없음 {tableColumns.map((col) => ( {col.columnLabel || col.columnName} diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 537a6e7d..fe544706 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -110,6 +110,16 @@ export interface ButtonActionConfig { geolocationExtraValue?: string | number | boolean; // 추가로 변경할 값 (예: "active") geolocationExtraKeyField?: string; // 다른 테이블의 키 필드 (예: "vehicle_id") geolocationExtraKeySourceField?: string; // 현재 폼에서 키 값을 가져올 필드 (예: "vehicle_id") + + // 🆕 두 번째 테이블 설정 (위치정보 + 상태변경을 각각 다른 테이블에) + geolocationSecondTableEnabled?: boolean; // 두 번째 테이블 사용 여부 + geolocationSecondTableName?: string; // 두 번째 테이블명 (예: "vehicles") + geolocationSecondMode?: "update" | "insert"; // 작업 모드 (기본: update) + geolocationSecondField?: string; // 두 번째 테이블에서 변경할 필드명 (예: "status") + geolocationSecondValue?: string | number | boolean; // 두 번째 테이블에서 변경할 값 (예: "inactive") + geolocationSecondKeyField?: string; // 두 번째 테이블의 키 필드 (예: "id") - UPDATE 모드에서만 사용 + geolocationSecondKeySourceField?: string; // 현재 폼에서 키 값을 가져올 필드 (예: "vehicle_id") - UPDATE 모드에서만 사용 + geolocationSecondInsertFields?: Record; // INSERT 모드에서 추가로 넣을 필드들 // 필드 값 교환 관련 (출발지 ↔ 목적지) swapFieldA?: string; // 교환할 첫 번째 필드명 (예: "departure") @@ -121,6 +131,13 @@ export interface ButtonActionConfig { updateTargetValue?: string | number | boolean; // 변경할 값 (예: "active") updateAutoSave?: boolean; // 변경 후 자동 저장 여부 (기본: true) updateMultipleFields?: Array<{ field: string; value: string | number | boolean }>; // 여러 필드 동시 변경 + + // 🆕 필드 값 변경 + 위치정보 수집 (update_field 액션에서 사용) + updateWithGeolocation?: boolean; // 위치정보도 함께 수집할지 여부 + updateGeolocationLatField?: string; // 위도 저장 필드 + updateGeolocationLngField?: string; // 경도 저장 필드 + updateGeolocationAccuracyField?: string; // 정확도 저장 필드 (선택) + updateGeolocationTimestampField?: string; // 타임스탬프 저장 필드 (선택) // 편집 관련 (수주관리 등 그룹별 다중 레코드 편집) editMode?: "modal" | "navigate" | "inline"; // 편집 모드 @@ -217,6 +234,44 @@ export interface ButtonActionContext { componentConfigs?: Record; // 컴포넌트 ID → 컴포넌트 설정 } +/** + * 🆕 특수 키워드를 실제 값으로 변환하는 헬퍼 함수 + * 지원하는 키워드: + * - __userId__ : 로그인한 사용자 ID + * - __userName__ : 로그인한 사용자 이름 + * - __companyCode__ : 로그인한 사용자의 회사 코드 + * - __screenId__ : 현재 화면 ID + * - __tableName__ : 현재 테이블명 + */ +export function resolveSpecialKeyword( + sourceField: string | undefined, + context: ButtonActionContext +): any { + if (!sourceField) return undefined; + + // 특수 키워드 처리 + switch (sourceField) { + case "__userId__": + console.log("🔑 특수 키워드 변환: __userId__ →", context.userId); + return context.userId; + case "__userName__": + console.log("🔑 특수 키워드 변환: __userName__ →", context.userName); + return context.userName; + case "__companyCode__": + console.log("🔑 특수 키워드 변환: __companyCode__ →", context.companyCode); + return context.companyCode; + case "__screenId__": + console.log("🔑 특수 키워드 변환: __screenId__ →", context.screenId); + return context.screenId; + case "__tableName__": + console.log("🔑 특수 키워드 변환: __tableName__ →", context.tableName); + return context.tableName; + default: + // 일반 폼 데이터에서 가져오기 + return context.formData?.[sourceField]; + } +} + /** * 버튼 액션 실행기 */ @@ -3236,6 +3291,14 @@ export class ButtonActionExecutor { private static async handleGeolocation(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { console.log("📍 위치정보 가져오기 액션 실행:", { config, context }); + console.log("📍 [디버그] 추가 필드 설정값:", { + geolocationUpdateField: config.geolocationUpdateField, + geolocationExtraField: config.geolocationExtraField, + geolocationExtraValue: config.geolocationExtraValue, + geolocationExtraTableName: config.geolocationExtraTableName, + geolocationExtraKeyField: config.geolocationExtraKeyField, + geolocationExtraKeySourceField: config.geolocationExtraKeySourceField, + }); // 브라우저 Geolocation API 지원 확인 if (!navigator.geolocation) { @@ -3296,26 +3359,35 @@ export class ButtonActionExecutor { // 🆕 추가 필드 변경 (위치정보 + 상태변경) let extraTableUpdated = false; + let secondTableUpdated = false; + if (config.geolocationUpdateField && config.geolocationExtraField && config.geolocationExtraValue !== undefined) { - const extraTableName = config.geolocationExtraTableName; + const extraTableName = config.geolocationExtraTableName || context.tableName; // 🆕 대상 테이블이 없으면 현재 테이블 사용 const currentTableName = config.geolocationTableName || context.tableName; + const keySourceField = config.geolocationExtraKeySourceField; - // 다른 테이블에 UPDATE하는 경우 - if (extraTableName && extraTableName !== currentTableName) { - console.log("📍 다른 테이블 필드 변경:", { + // 🆕 특수 키워드가 설정되어 있으면 바로 DB UPDATE (같은 테이블이어도) + const hasSpecialKeyword = keySourceField?.startsWith("__") && keySourceField?.endsWith("__"); + const isDifferentTable = extraTableName && extraTableName !== currentTableName; + + // 다른 테이블이거나 특수 키워드가 설정된 경우 → 바로 DB UPDATE + if (isDifferentTable || hasSpecialKeyword) { + console.log("📍 DB 직접 UPDATE:", { targetTable: extraTableName, field: config.geolocationExtraField, value: config.geolocationExtraValue, keyField: config.geolocationExtraKeyField, - keySourceField: config.geolocationExtraKeySourceField, + keySourceField: keySourceField, + hasSpecialKeyword, + isDifferentTable, }); - // 키 값 가져오기 - const keyValue = context.formData?.[config.geolocationExtraKeySourceField || ""]; + // 키 값 가져오기 (특수 키워드 지원) + const keyValue = resolveSpecialKeyword(keySourceField, context); if (keyValue && config.geolocationExtraKeyField) { try { - // 다른 테이블 UPDATE API 호출 + // DB UPDATE API 호출 const { apiClient } = await import("@/lib/api/client"); const response = await apiClient.put(`/dynamic-form/update-field`, { tableName: extraTableName, @@ -3327,30 +3399,131 @@ export class ButtonActionExecutor { if (response.data?.success) { extraTableUpdated = true; - console.log("✅ 다른 테이블 UPDATE 성공:", response.data); + console.log("✅ DB UPDATE 성공:", response.data); } else { - console.error("❌ 다른 테이블 UPDATE 실패:", response.data); + console.error("❌ DB UPDATE 실패:", response.data); toast.error(`${extraTableName} 테이블 업데이트에 실패했습니다.`); } } catch (apiError) { - console.error("❌ 다른 테이블 UPDATE API 오류:", apiError); + console.error("❌ DB UPDATE API 오류:", apiError); toast.error(`${extraTableName} 테이블 업데이트 중 오류가 발생했습니다.`); } } else { - console.warn("⚠️ 키 값이 없어서 다른 테이블 UPDATE를 건너뜁니다:", { - keySourceField: config.geolocationExtraKeySourceField, + console.warn("⚠️ 키 값이 없어서 DB UPDATE를 건너뜁니다:", { + keySourceField: keySourceField, keyValue, }); } } else { - // 같은 테이블 (현재 폼 데이터에 추가) + // 같은 테이블이고 특수 키워드가 없는 경우 (현재 폼 데이터에 추가) updates[config.geolocationExtraField] = config.geolocationExtraValue; - console.log("📍 같은 테이블 추가 필드 변경:", { + console.log("📍 같은 테이블 추가 필드 변경 (폼 데이터):", { field: config.geolocationExtraField, value: config.geolocationExtraValue, }); } } + + // 🆕 두 번째 테이블 INSERT 또는 UPDATE + if (config.geolocationSecondTableEnabled && + config.geolocationSecondTableName) { + + const secondMode = config.geolocationSecondMode || "update"; + + console.log("📍 두 번째 테이블 작업:", { + mode: secondMode, + targetTable: config.geolocationSecondTableName, + field: config.geolocationSecondField, + value: config.geolocationSecondValue, + keyField: config.geolocationSecondKeyField, + keySourceField: config.geolocationSecondKeySourceField, + }); + + try { + const { apiClient } = await import("@/lib/api/client"); + + if (secondMode === "insert") { + // INSERT 모드: 새 레코드 생성 + const insertData: Record = { + // 위치정보 포함 (선택적) + ...(config.geolocationSecondInsertFields || {}), + }; + + // 기본 필드 추가 + if (config.geolocationSecondField && config.geolocationSecondValue !== undefined) { + insertData[config.geolocationSecondField] = config.geolocationSecondValue; + } + + // 위치정보도 두 번째 테이블에 저장하려면 추가 + // (선택적으로 위도/경도도 저장) + if (config.geolocationSecondInsertFields?.includeLocation) { + insertData[latField] = latitude; + insertData[lngField] = longitude; + if (config.geolocationAccuracyField) { + insertData[config.geolocationAccuracyField] = accuracy; + } + if (config.geolocationTimestampField) { + insertData[config.geolocationTimestampField] = timestamp.toISOString(); + } + } + + // 현재 폼에서 키 값 가져와서 연결 (외래키) - 특수 키워드 지원 + if (config.geolocationSecondKeySourceField && config.geolocationSecondKeyField) { + const keyValue = resolveSpecialKeyword(config.geolocationSecondKeySourceField, context); + if (keyValue) { + insertData[config.geolocationSecondKeyField] = keyValue; + } + } + + console.log("📍 두 번째 테이블 INSERT 데이터:", insertData); + + const response = await apiClient.post(`/dynamic-form/save`, { + tableName: config.geolocationSecondTableName, + data: insertData, + }); + + if (response.data?.success) { + secondTableUpdated = true; + console.log("✅ 두 번째 테이블 INSERT 성공:", response.data); + } else { + console.error("❌ 두 번째 테이블 INSERT 실패:", response.data); + toast.error(`${config.geolocationSecondTableName} 테이블 저장에 실패했습니다.`); + } + } else { + // UPDATE 모드: 기존 레코드 수정 + if (config.geolocationSecondField && config.geolocationSecondValue !== undefined) { + // 특수 키워드 지원 + const secondKeyValue = resolveSpecialKeyword(config.geolocationSecondKeySourceField, context); + + if (secondKeyValue && config.geolocationSecondKeyField) { + const response = await apiClient.put(`/dynamic-form/update-field`, { + tableName: config.geolocationSecondTableName, + keyField: config.geolocationSecondKeyField, + keyValue: secondKeyValue, + updateField: config.geolocationSecondField, + updateValue: config.geolocationSecondValue, + }); + + if (response.data?.success) { + secondTableUpdated = true; + console.log("✅ 두 번째 테이블 UPDATE 성공:", response.data); + } else { + console.error("❌ 두 번째 테이블 UPDATE 실패:", response.data); + toast.error(`${config.geolocationSecondTableName} 테이블 업데이트에 실패했습니다.`); + } + } else { + console.warn("⚠️ 두 번째 테이블 키 값이 없어서 UPDATE를 건너뜁니다:", { + keySourceField: config.geolocationSecondKeySourceField, + keyValue: secondKeyValue, + }); + } + } + } + } catch (apiError) { + console.error("❌ 두 번째 테이블 API 오류:", apiError); + toast.error(`${config.geolocationSecondTableName} 테이블 작업 중 오류가 발생했습니다.`); + } + } // formData 업데이트 if (context.onFormDataChange) { @@ -3371,6 +3544,11 @@ export class ButtonActionExecutor { successMsg += `\n${config.geolocationExtraField}: ${config.geolocationExtraValue}`; } } + + // 두 번째 테이블 변경이 있으면 메시지에 포함 + if (secondTableUpdated && config.geolocationSecondTableName) { + successMsg += `\n[${config.geolocationSecondTableName}] ${config.geolocationSecondField}: ${config.geolocationSecondValue}`; + } // 성공 메시지 표시 toast.success(successMsg); @@ -3470,6 +3648,7 @@ export class ButtonActionExecutor { /** * 필드 값 변경 액션 처리 (예: status를 active로 변경) + * 🆕 위치정보 수집 기능 추가 */ private static async handleUpdateField(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { @@ -3483,7 +3662,7 @@ export class ButtonActionExecutor { const multipleFields = config.updateMultipleFields || []; // 단일 필드 변경이나 다중 필드 변경 중 하나는 있어야 함 - if (!targetField && multipleFields.length === 0) { + if (!targetField && multipleFields.length === 0 && !config.updateWithGeolocation) { toast.error("변경할 필드가 설정되지 않았습니다."); return false; } @@ -3510,6 +3689,69 @@ export class ButtonActionExecutor { updates[field] = value; }); + // 🆕 위치정보 수집 (updateWithGeolocation이 true인 경우) + if (config.updateWithGeolocation) { + const latField = config.updateGeolocationLatField; + const lngField = config.updateGeolocationLngField; + + if (!latField || !lngField) { + toast.error("위도/경도 저장 필드가 설정되지 않았습니다."); + return false; + } + + // 브라우저 Geolocation API 지원 확인 + if (!navigator.geolocation) { + toast.error("이 브라우저는 위치정보를 지원하지 않습니다."); + return false; + } + + // 로딩 토스트 표시 + const loadingToastId = toast.loading("위치 정보를 가져오는 중..."); + + try { + // 위치 정보 가져오기 + const position = await new Promise((resolve, reject) => { + navigator.geolocation.getCurrentPosition(resolve, reject, { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 0, + }); + }); + + toast.dismiss(loadingToastId); + + const { latitude, longitude, accuracy } = position.coords; + const timestamp = new Date(position.timestamp); + + console.log("📍 위치정보 획득:", { latitude, longitude, accuracy }); + + // 위치정보를 updates에 추가 + updates[latField] = latitude; + updates[lngField] = longitude; + + if (config.updateGeolocationAccuracyField && accuracy !== null) { + updates[config.updateGeolocationAccuracyField] = accuracy; + } + if (config.updateGeolocationTimestampField) { + updates[config.updateGeolocationTimestampField] = timestamp.toISOString(); + } + } catch (geoError: any) { + toast.dismiss(loadingToastId); + + // GeolocationPositionError 처리 + if (geoError.code === 1) { + toast.error("위치 정보 접근이 거부되었습니다."); + } else if (geoError.code === 2) { + toast.error("위치 정보를 사용할 수 없습니다."); + } else if (geoError.code === 3) { + toast.error("위치 정보 요청 시간이 초과되었습니다."); + } else { + toast.error("위치 정보를 가져오는 중 오류가 발생했습니다."); + } + return false; + } + } + console.log("🔄 변경할 필드들:", updates); // formData 업데이트 @@ -3523,6 +3765,67 @@ export class ButtonActionExecutor { const autoSave = config.updateAutoSave !== false; if (autoSave) { + // 🆕 키 필드 설정이 있는 경우 (특수 키워드 지원) - 직접 DB UPDATE + const keyField = config.updateKeyField; + const keySourceField = config.updateKeySourceField; + const targetTableName = config.updateTableName || tableName; + + if (keyField && keySourceField) { + // 특수 키워드 변환 (예: __userId__ → 실제 사용자 ID) + const keyValue = resolveSpecialKeyword(keySourceField, context); + + console.log("🔄 필드 값 변경 - 키 필드 사용:", { + targetTable: targetTableName, + keyField, + keySourceField, + keyValue, + updates, + }); + + if (!keyValue) { + console.warn("⚠️ 키 값이 없어서 업데이트를 건너뜁니다:", { keySourceField, keyValue }); + toast.error("레코드를 식별할 키 값이 없습니다."); + return false; + } + + try { + // 각 필드에 대해 개별 UPDATE 호출 + const { apiClient } = await import("@/lib/api/client"); + + for (const [field, value] of Object.entries(updates)) { + console.log(`🔄 DB UPDATE: ${targetTableName}.${field} = ${value} WHERE ${keyField} = ${keyValue}`); + + const response = await apiClient.put(`/dynamic-form/update-field`, { + tableName: targetTableName, + keyField: keyField, + keyValue: keyValue, + updateField: field, + updateValue: value, + }); + + if (!response.data?.success) { + console.error(`❌ ${field} 업데이트 실패:`, response.data); + toast.error(`${field} 업데이트에 실패했습니다.`); + return false; + } + } + + console.log("✅ 모든 필드 업데이트 성공"); + toast.success(config.successMessage || "상태가 변경되었습니다."); + + // 테이블 새로고침 이벤트 발생 + window.dispatchEvent(new CustomEvent("refreshTableData", { + detail: { tableName: targetTableName } + })); + + return true; + } catch (apiError) { + console.error("❌ 필드 값 변경 API 호출 실패:", apiError); + toast.error(config.errorMessage || "상태 변경 중 오류가 발생했습니다."); + return false; + } + } + // onSave 콜백이 있으면 사용 if (onSave) { console.log("🔄 필드 값 변경 후 자동 저장 (onSave 콜백)"); @@ -3537,7 +3840,7 @@ export class ButtonActionExecutor { } } - // API를 통한 직접 저장 + // API를 통한 직접 저장 (기존 방식: formData에 PK가 있는 경우) if (tableName && formData) { console.log("🔄 필드 값 변경 후 자동 저장 (API 직접 호출)"); try { @@ -3546,7 +3849,7 @@ export class ButtonActionExecutor { const pkValue = formData[pkField] || formData.id; if (!pkValue) { - toast.error("레코드 ID를 찾을 수 없습니다."); + toast.error("레코드 ID를 찾을 수 없습니다. 키 필드를 설정해주세요."); return false; }