"use client"; import React from "react"; import { toast } from "sonner"; import { screenApi } from "@/lib/api/screen"; import { DynamicFormApi } from "@/lib/api/dynamicForm"; import { ImprovedButtonActionExecutor } from "@/lib/utils/improvedButtonActionExecutor"; import { apiClient } from "@/lib/api/client"; import type { ExtendedControlContext } from "@/types/control-management"; /** * ๐Ÿ”ง formData ๋‚ด ๋ฐฐ์—ด ๊ฐ’์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ * PostgreSQL ๋ฐฐ์—ด ํ˜•์‹ ์ €์žฅ ๋ฐฉ์ง€ */ function normalizeFormDataArrays(formData: Record): Record { if (!formData || typeof formData !== "object") return formData; const normalized: Record = {}; for (const [key, value] of Object.entries(formData)) { if (Array.isArray(value)) { // ๋ฐฐ์—ด ๋‚ด ์ˆซ์ž๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ ํ›„ ์‰ผํ‘œ ๊ตฌ๋ถ„ const stringValue = value .map(v => typeof v === "number" ? String(v) : v) .filter(v => v !== null && v !== undefined && v !== "") .join(","); console.log(`๐Ÿ”ง [normalizeFormDataArrays] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด: ${key}`, { original: value, converted: stringValue }); normalized[key] = stringValue; } else { normalized[key] = value; } } return normalized; } /** * ๋ฒ„ํŠผ ์•ก์…˜ ํƒ€์ž… ์ •์˜ */ export type ButtonActionType = | "save" // ์ €์žฅ | "delete" // ์‚ญ์ œ | "edit" // ํŽธ์ง‘ | "copy" // ๋ณต์‚ฌ (ํ’ˆ๋ชฉ์ฝ”๋“œ ์ดˆ๊ธฐํ™”) | "navigate" // ํŽ˜์ด์ง€ ์ด๋™ | "openRelatedModal" // ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ ๋ฒ„ํŠผ์˜ ์„ ํƒ ๋ฐ์ดํ„ฐ๋กœ ๋ชจ๋‹ฌ ์—ด๊ธฐ | "openModalWithData" // ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋ฉด์„œ ๋ชจ๋‹ฌ ์—ด๊ธฐ (deprecated: modal๋กœ ํ†ตํ•ฉ) | "modal" // ๋ชจ๋‹ฌ ์—ด๊ธฐ (์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์˜ต์…˜ ํฌํ•จ) | "control" // ์ œ์–ด ํ๋ฆ„ | "view_table_history" // ํ…Œ์ด๋ธ” ์ด๋ ฅ ๋ณด๊ธฐ | "excel_download" // ์—‘์…€ ๋‹ค์šด๋กœ๋“œ | "excel_upload" // ์—‘์…€ ์—…๋กœ๋“œ | "barcode_scan" // ๋ฐ”์ฝ”๋“œ ์Šค์บ” | "code_merge" // ์ฝ”๋“œ ๋ณ‘ํ•ฉ // | "empty_vehicle" // ๊ณต์ฐจ๋“ฑ๋ก (์œ„์น˜ ์ˆ˜์ง‘ + ์ƒํƒœ ๋ณ€๊ฒฝ) - ์šดํ–‰์•Œ๋ฆผ์œผ๋กœ ํ†ตํ•ฉ | "operation_control" // ์šดํ–‰์•Œ๋ฆผ ๋ฐ ์ข…๋ฃŒ (์œ„์น˜ ์ˆ˜์ง‘ + ์ƒํƒœ ๋ณ€๊ฒฝ + ์—ฐ์† ์ถ”์ ) | "swap_fields" // ํ•„๋“œ ๊ฐ’ ๊ตํ™˜ (์ถœ๋ฐœ์ง€ โ†” ๋ชฉ์ ์ง€) | "transferData" // ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (์ปดํฌ๋„ŒํŠธ ๊ฐ„ or ํ™”๋ฉด ๊ฐ„) | "quickInsert" // ์ฆ‰์‹œ ์ €์žฅ (์„ ํƒํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ํŠน์ • ํ…Œ์ด๋ธ”์— ์ฆ‰์‹œ INSERT) | "event"; // ์ด๋ฒคํŠธ ๋ฒ„์Šค๋กœ ์ด๋ฒคํŠธ ๋ฐœ์†ก (์Šค์ผ€์ค„ ์ƒ์„ฑ ๋“ฑ) /** * ๋ฒ„ํŠผ ์•ก์…˜ ์„ค์ • */ export interface ButtonActionConfig { type: ButtonActionType; // ์ €์žฅ/์ œ์ถœ ๊ด€๋ จ saveEndpoint?: string; validateForm?: boolean; // ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ด€๋ จ targetUrl?: string; targetScreenId?: number; // ๋ชจ๋‹ฌ/ํŒ์—… ๊ด€๋ จ modalTitle?: string; modalTitleBlocks?: Array<{ // ๐Ÿ†• ๋ธ”๋ก ๊ธฐ๋ฐ˜ ์ œ๋ชฉ (์šฐ์„ ์ˆœ์œ„ ๋†’์Œ) id: string; type: "text" | "field"; value: string; // type=text: ํ…์ŠคํŠธ ๋‚ด์šฉ, type=field: ์ปฌ๋Ÿผ๋ช… tableName?: string; // type=field์ผ ๋•Œ ํ…Œ์ด๋ธ”๋ช… label?: string; // type=field์ผ ๋•Œ ํ‘œ์‹œ์šฉ ๋ผ๋ฒจ }>; modalDescription?: string; modalSize?: "sm" | "md" | "lg" | "xl"; popupWidth?: number; popupHeight?: number; // ๐Ÿ†• ๋ชจ๋‹ฌ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์˜ต์…˜ (modal ์•ก์…˜ ํ†ตํ•ฉ) passSelectedData?: boolean; // ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์—ฌ๋ถ€ (๊ธฐ๋ณธ: true) autoDetectDataSource?: boolean; // ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ž๋™ ๊ฐ์ง€ (TableList/SplitPanel ๋“ฑ) dataSourceId?: string; // modalDataStore์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ID fieldMappings?: Array<{ sourceField: string; targetField: string }>; // ํ•„๋“œ ๋งคํ•‘ // ํ™•์ธ ๋ฉ”์‹œ์ง€ confirmMessage?: string; successMessage?: string; errorMessage?: string; // ์ œ์–ด๊ด€๋ฆฌ ๊ด€๋ จ enableDataflowControl?: boolean; dataflowConfig?: any; // ButtonDataflowConfig ํƒ€์ž… (์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด any ์‚ฌ์šฉ) dataflowTiming?: "before" | "after" | "replace"; // ์ œ์–ด ์‹คํ–‰ ํƒ€์ด๋ฐ // ํ…Œ์ด๋ธ” ์ด๋ ฅ ๋ณด๊ธฐ ๊ด€๋ จ historyTableName?: string; // ์ด๋ ฅ์„ ์กฐํšŒํ•  ํ…Œ์ด๋ธ”๋ช… (์ž๋™ ๊ฐ์ง€ ๋˜๋Š” ์ˆ˜๋™ ์ง€์ •) historyRecordIdField?: string; // PK ํ•„๋“œ๋ช… (๊ธฐ๋ณธ: "id") historyRecordIdSource?: "selected_row" | "form_field" | "context"; // ๋ ˆ์ฝ”๋“œ ID ๊ฐ€์ ธ์˜ฌ ์†Œ์Šค historyRecordLabelField?: string; // ๋ ˆ์ฝ”๋“œ ๋ผ๋ฒจ๋กœ ํ‘œ์‹œํ•  ํ•„๋“œ (์„ ํƒ์‚ฌํ•ญ) historyDisplayColumn?: string; // ์ „์ฒด ์ด๋ ฅ์—์„œ ๋ ˆ์ฝ”๋“œ ๊ตฌ๋ถ„์šฉ ์ปฌ๋Ÿผ (์˜ˆ: device_code, name) // ์—‘์…€ ๋‹ค์šด๋กœ๋“œ ๊ด€๋ จ excelFileName?: string; // ๋‹ค์šด๋กœ๋“œํ•  ํŒŒ์ผ๋ช… (๊ธฐ๋ณธ: ํ…Œ์ด๋ธ”๋ช…_๋‚ ์งœ.xlsx) excelSheetName?: string; // ์‹œํŠธ๋ช… (๊ธฐ๋ณธ: "Sheet1") excelIncludeHeaders?: boolean; // ํ—ค๋” ํฌํ•จ ์—ฌ๋ถ€ (๊ธฐ๋ณธ: true) // ์—‘์…€ ์—…๋กœ๋“œ ๊ด€๋ จ excelUploadMode?: "insert" | "update" | "upsert"; // ์—…๋กœ๋“œ ๋ชจ๋“œ excelKeyColumn?: string; // ์—…๋ฐ์ดํŠธ/Upsert ์‹œ ํ‚ค ์ปฌ๋Ÿผ excelNumberingRuleId?: string; // ์ฑ„๋ฒˆ ๊ทœ์น™ ID (๋‹จ์ผ ํ…Œ์ด๋ธ”์šฉ) excelNumberingTargetColumn?: string; // ์ฑ„๋ฒˆ ์ ์šฉ ์ปฌ๋Ÿผ (๋‹จ์ผ ํ…Œ์ด๋ธ”์šฉ) excelAfterUploadFlows?: Array<{ flowId: string; order: number }>; // ์—…๋กœ๋“œ ํ›„ ์ œ์–ด ์‹คํ–‰ // ๋ฐ”์ฝ”๋“œ ์Šค์บ” ๊ด€๋ จ barcodeTargetField?: string; // ์Šค์บ” ๊ฒฐ๊ณผ๋ฅผ ์ž…๋ ฅํ•  ํ•„๋“œ๋ช… barcodeFormat?: "all" | "1d" | "2d"; // ๋ฐ”์ฝ”๋“œ ํฌ๋งท (๊ธฐ๋ณธ: "all") barcodeAutoSubmit?: boolean; // ์Šค์บ” ํ›„ ์ž๋™ ์ œ์ถœ ์—ฌ๋ถ€ // ์ฝ”๋“œ ๋ณ‘ํ•ฉ ๊ด€๋ จ mergeColumnName?: string; // ๋ณ‘ํ•ฉํ•  ์ปฌ๋Ÿผ๋ช… (์˜ˆ: "item_code") mergeShowPreview?: boolean; // ๋ณ‘ํ•ฉ ์ „ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ‘œ์‹œ ์—ฌ๋ถ€ (๊ธฐ๋ณธ: true) // ์œ„์น˜์ •๋ณด ๊ด€๋ จ geolocationTableName?: string; // ์œ„์น˜์ •๋ณด ์ €์žฅ ํ…Œ์ด๋ธ”๋ช… (๊ธฐ๋ณธ: ํ˜„์žฌ ํ™”๋ฉด ํ…Œ์ด๋ธ”) geolocationLatField?: string; // ์œ„๋„๋ฅผ ์ €์žฅํ•  ํ•„๋“œ๋ช… (์˜ˆ: "latitude") geolocationLngField?: string; // ๊ฒฝ๋„๋ฅผ ์ €์žฅํ•  ํ•„๋“œ๋ช… (์˜ˆ: "longitude") geolocationAccuracyField?: string; // ์ •ํ™•๋„๋ฅผ ์ €์žฅํ•  ํ•„๋“œ๋ช… (์„ ํƒ, ์˜ˆ: "accuracy") geolocationTimestampField?: string; // ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ์ €์žฅํ•  ํ•„๋“œ๋ช… (์„ ํƒ, ์˜ˆ: "location_time") geolocationHighAccuracy?: boolean; // ๊ณ ์ •๋ฐ€ ๋ชจ๋“œ ์‚ฌ์šฉ ์—ฌ๋ถ€ (๊ธฐ๋ณธ: true) geolocationTimeout?: number; // ํƒ€์ž„์•„์›ƒ (ms, ๊ธฐ๋ณธ: 10000) geolocationMaxAge?: number; // ์บ์‹œ๋œ ์œ„์น˜ ์ตœ๋Œ€ ์ˆ˜๋ช… (ms, ๊ธฐ๋ณธ: 0) geolocationAutoSave?: boolean; // ์œ„์น˜ ๊ฐ€์ ธ์˜จ ํ›„ ์ž๋™ ์ €์žฅ ์—ฌ๋ถ€ (๊ธฐ๋ณธ: false) geolocationKeyField?: string; // DB UPDATE ์‹œ WHERE ์กฐ๊ฑด์— ์‚ฌ์šฉํ•  ํ‚ค ํ•„๋“œ (์˜ˆ: "user_id") geolocationKeySourceField?: string; // ํ‚ค ๊ฐ’ ์†Œ์Šค (์˜ˆ: "__userId__" ๋˜๋Š” ํผ ํ•„๋“œ๋ช…) geolocationUpdateField?: boolean; // ์œ„์น˜์ •๋ณด์™€ ํ•จ๊ป˜ ์ถ”๊ฐ€ ํ•„๋“œ ๋ณ€๊ฒฝ ์—ฌ๋ถ€ geolocationExtraTableName?: string; // ์ถ”๊ฐ€ ํ•„๋“œ ๋ณ€๊ฒฝ ๋Œ€์ƒ ํ…Œ์ด๋ธ” (๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ๊ฐ€๋Šฅ) geolocationExtraField?: string; // ์ถ”๊ฐ€๋กœ ๋ณ€๊ฒฝํ•  ํ•„๋“œ๋ช… (์˜ˆ: "status") 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 ๋ชจ๋“œ์—์„œ ์ถ”๊ฐ€๋กœ ๋„ฃ์„ ํ•„๋“œ๋“ค // ๐Ÿ†• ์—ฐ์† ์œ„์น˜ ์ถ”์  ์„ค์ • (update_field ์•ก์…˜์˜ updateWithTracking ์˜ต์…˜์šฉ) trackingInterval?: number; // ์œ„์น˜ ์ €์žฅ ์ฃผ๊ธฐ (ms, ๊ธฐ๋ณธ: 10000 = 10์ดˆ) trackingTripIdField?: string; // ์šดํ–‰ ID๋ฅผ ์ €์žฅํ•  ํ•„๋“œ๋ช… (์˜ˆ: "trip_id") trackingAutoGenerateTripId?: boolean; // ์šดํ–‰ ID ์ž๋™ ์ƒ์„ฑ ์—ฌ๋ถ€ (๊ธฐ๋ณธ: true) trackingDepartureField?: string; // ์ถœ๋ฐœ์ง€ ํ•„๋“œ๋ช… (formData์—์„œ ๊ฐ€์ ธ์˜ด) trackingArrivalField?: string; // ๋„์ฐฉ์ง€ ํ•„๋“œ๋ช… (formData์—์„œ ๊ฐ€์ ธ์˜ด) trackingVehicleIdField?: string; // ์ฐจ๋Ÿ‰ ID ํ•„๋“œ๋ช… (formData์—์„œ ๊ฐ€์ ธ์˜ด) trackingStatusOnStart?: string; // ์ถ”์  ์‹œ์ž‘ ์‹œ ์ƒํƒœ๊ฐ’ (์˜ˆ: "active") trackingStatusOnStop?: string; // ์ถ”์  ์ข…๋ฃŒ ์‹œ ์ƒํƒœ๊ฐ’ (์˜ˆ: "completed") trackingStatusField?: string; // ์ƒํƒœ ํ•„๋“œ๋ช… (vehicles ํ…Œ์ด๋ธ” ๋“ฑ) trackingStatusTableName?: string; // ์ƒํƒœ ๋ณ€๊ฒฝ ๋Œ€์ƒ ํ…Œ์ด๋ธ”๋ช… trackingStatusKeyField?: string; // ์ƒํƒœ ๋ณ€๊ฒฝ ํ‚ค ํ•„๋“œ (์˜ˆ: "user_id") trackingStatusKeySourceField?: string; // ํ‚ค ๊ฐ’ ์†Œ์Šค (์˜ˆ: "__userId__") // ํ•„๋“œ ๊ฐ’ ๊ตํ™˜ ๊ด€๋ จ (์ถœ๋ฐœ์ง€ โ†” ๋ชฉ์ ์ง€) swapFieldA?: string; // ๊ตํ™˜ํ•  ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ๋ช… (์˜ˆ: "departure") swapFieldB?: string; // ๊ตํ™˜ํ•  ๋‘ ๋ฒˆ์งธ ํ•„๋“œ๋ช… (์˜ˆ: "destination") swapRelatedFields?: Array<{ fieldA: string; fieldB: string }>; // ํ•จ๊ป˜ ๊ตํ™˜ํ•  ๊ด€๋ จ ํ•„๋“œ๋“ค (์˜ˆ: ์œ„๋„/๊ฒฝ๋„) // ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ ๊ด€๋ จ (ํŠน์ • ํ•„๋“œ๋ฅผ ํŠน์ • ๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝ) updateTargetField?: string; // ๋ณ€๊ฒฝํ•  ํ•„๋“œ๋ช… (์˜ˆ: "status") updateTargetValue?: string | number | boolean; // ๋ณ€๊ฒฝํ•  ๊ฐ’ (์˜ˆ: "active") updateAutoSave?: boolean; // ๋ณ€๊ฒฝ ํ›„ ์ž๋™ ์ €์žฅ ์—ฌ๋ถ€ (๊ธฐ๋ณธ: true) updateMultipleFields?: Array<{ field: string; value: string | number | boolean }>; // ์—ฌ๋Ÿฌ ํ•„๋“œ ๋™์‹œ ๋ณ€๊ฒฝ updateTableName?: string; // ๋Œ€์ƒ ํ…Œ์ด๋ธ”๋ช… (๋‹ค๋ฅธ ํ…Œ์ด๋ธ” UPDATE ์‹œ) updateKeyField?: string; // ํ‚ค ํ•„๋“œ๋ช… (WHERE ์กฐ๊ฑด์— ์‚ฌ์šฉ) updateKeySourceField?: string; // ํ‚ค ๊ฐ’ ์†Œ์Šค (ํผ ํ•„๋“œ๋ช… ๋˜๋Š” __userId__ ๋“ฑ ํŠน์ˆ˜ ํ‚ค์›Œ๋“œ) // ๐Ÿ†• ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ + ์œ„์น˜์ •๋ณด ์ˆ˜์ง‘ (update_field ์•ก์…˜์—์„œ ์‚ฌ์šฉ) updateWithGeolocation?: boolean; // ์œ„์น˜์ •๋ณด๋„ ํ•จ๊ป˜ ์ˆ˜์ง‘ํ• ์ง€ ์—ฌ๋ถ€ updateGeolocationLatField?: string; // ์œ„๋„ ์ €์žฅ ํ•„๋“œ updateGeolocationLngField?: string; // ๊ฒฝ๋„ ์ €์žฅ ํ•„๋“œ // ๐Ÿ†• ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ + ์—ฐ์† ์œ„์น˜ ์ถ”์  (update_field ์•ก์…˜์—์„œ ์‚ฌ์šฉ) updateWithTracking?: boolean; // ์—ฐ์† ์œ„์น˜ ์ถ”์  ์‚ฌ์šฉ ์—ฌ๋ถ€ updateTrackingMode?: "start" | "stop"; // ์ถ”์  ๋ชจ๋“œ (์‹œ์ž‘/์ข…๋ฃŒ) updateTrackingInterval?: number; // ์œ„์น˜ ์ €์žฅ ์ฃผ๊ธฐ (ms, ๊ธฐ๋ณธ: 10000) updateGeolocationAccuracyField?: string; // ์ •ํ™•๋„ ์ €์žฅ ํ•„๋“œ (์„ ํƒ) updateGeolocationTimestampField?: string; // ํƒ€์ž„์Šคํƒฌํ”„ ์ €์žฅ ํ•„๋“œ (์„ ํƒ) // ๐Ÿ†• ๊ณต์ฐจ๋“ฑ๋ก ์—ฐ์† ์œ„์น˜ ์ถ”์  ์„ค์ • (empty_vehicle ์•ก์…˜์—์„œ ์‚ฌ์šฉ) emptyVehicleTracking?: boolean; // ๊ณต์ฐจ ์ƒํƒœ์—์„œ ์—ฐ์† ์œ„์น˜ ์ถ”์  ์—ฌ๋ถ€ (๊ธฐ๋ณธ: true) emptyVehicleTrackingInterval?: number; // ์œ„์น˜ ์ €์žฅ ์ฃผ๊ธฐ (ms, ๊ธฐ๋ณธ: 10000 = 10์ดˆ) // ํŽธ์ง‘ ๊ด€๋ จ (์ˆ˜์ฃผ๊ด€๋ฆฌ ๋“ฑ ๊ทธ๋ฃน๋ณ„ ๋‹ค์ค‘ ๋ ˆ์ฝ”๋“œ ํŽธ์ง‘) editMode?: "modal" | "navigate" | "inline"; // ํŽธ์ง‘ ๋ชจ๋“œ editModalTitle?: string; // ํŽธ์ง‘ ๋ชจ๋‹ฌ ์ œ๋ชฉ editModalDescription?: string; // ํŽธ์ง‘ ๋ชจ๋‹ฌ ์„ค๋ช… groupByColumns?: string[]; // ๊ฐ™์€ ๊ทธ๋ฃน์˜ ์—ฌ๋Ÿฌ ํ–‰์„ ํ•จ๊ป˜ ํŽธ์ง‘ (์˜ˆ: ["order_no"]) // ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๊ด€๋ จ (transferData ์•ก์…˜์šฉ) dataTransfer?: { // ์†Œ์Šค ์„ค์ • sourceComponentId: string; // ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ปดํฌ๋„ŒํŠธ ID (ํ…Œ์ด๋ธ” ๋“ฑ) sourceComponentType?: string; // ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… // ํƒ€๊ฒŸ ์„ค์ • targetType: "component" | "screen"; // ํƒ€๊ฒŸ ํƒ€์ž… (๊ฐ™์€ ํ™”๋ฉด์˜ ์ปดํฌ๋„ŒํŠธ or ๋‹ค๋ฅธ ํ™”๋ฉด) // ํƒ€๊ฒŸ์ด ์ปดํฌ๋„ŒํŠธ์ธ ๊ฒฝ์šฐ targetComponentId?: string; // ํƒ€๊ฒŸ ์ปดํฌ๋„ŒํŠธ ID // ํƒ€๊ฒŸ์ด ํ™”๋ฉด์ธ ๊ฒฝ์šฐ targetScreenId?: number; // ํƒ€๊ฒŸ ํ™”๋ฉด ID // ๋ฐ์ดํ„ฐ ๋งคํ•‘ ๊ทœ์น™ mappingRules: Array<{ sourceField: string; // ์†Œ์Šค ํ•„๋“œ๋ช… targetField: string; // ํƒ€๊ฒŸ ํ•„๋“œ๋ช… transform?: "sum" | "average" | "concat" | "first" | "last" | "count"; // ๋ณ€ํ™˜ ํ•จ์ˆ˜ defaultValue?: any; // ๊ธฐ๋ณธ๊ฐ’ }>; // ์ „๋‹ฌ ์˜ต์…˜ mode?: "append" | "replace" | "merge"; // ์ˆ˜์‹  ๋ชจ๋“œ (๊ธฐ๋ณธ: append) clearAfterTransfer?: boolean; // ์ „๋‹ฌ ํ›„ ์†Œ์Šค ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” confirmBeforeTransfer?: boolean; // ์ „๋‹ฌ ์ „ ํ™•์ธ ๋ฉ”์‹œ์ง€ confirmMessage?: string; // ํ™•์ธ ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ // ๊ฒ€์ฆ validation?: { requireSelection?: boolean; // ์„ ํƒ ํ•„์ˆ˜ (๊ธฐ๋ณธ: true) minSelection?: number; // ์ตœ์†Œ ์„ ํƒ ๊ฐœ์ˆ˜ maxSelection?: number; // ์ตœ๋Œ€ ์„ ํƒ ๊ฐœ์ˆ˜ }; }; // ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ ๋ฒ„ํŠผ ๋ชจ๋‹ฌ ์—ด๊ธฐ ๊ด€๋ จ relatedModalConfig?: { targetScreenId: number; // ์—ด๋ฆด ๋ชจ๋‹ฌ ํ™”๋ฉด ID componentId?: string; // ํŠน์ • RelatedDataButtons ์ปดํฌ๋„ŒํŠธ ์ง€์ • (์„ ํƒ์‚ฌํ•ญ) }; // ์ฆ‰์‹œ ์ €์žฅ (Quick Insert) ๊ด€๋ จ quickInsertConfig?: { targetTable: string; // ์ €์žฅํ•  ํ…Œ์ด๋ธ”๋ช… columnMappings: Array<{ targetColumn: string; // ๋Œ€์ƒ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ๋ช… sourceType: "component" | "leftPanel" | "fixed" | "currentUser"; // ๊ฐ’ ์†Œ์Šค ํƒ€์ž… sourceComponentId?: string; // ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ ๊ฒฝ์šฐ ์ปดํฌ๋„ŒํŠธ ID sourceColumnName?: string; // ์ปดํฌ๋„ŒํŠธ์˜ columnName (formData ์ ‘๊ทผ์šฉ) sourceColumn?: string; // ์ขŒ์ธก ํŒจ๋„ ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์˜ ํŠน์ • ์ปฌ๋Ÿผ fixedValue?: any; // ๊ณ ์ •๊ฐ’ userField?: "userId" | "userName" | "companyCode"; // currentUser ํƒ€์ž…์ผ ๋•Œ ์‚ฌ์šฉํ•  ํ•„๋“œ }>; duplicateCheck?: { enabled: boolean; // ์ค‘๋ณต ์ฒดํฌ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ columns?: string[]; // ์ค‘๋ณต ์ฒดํฌํ•  ์ปฌ๋Ÿผ๋“ค errorMessage?: string; // ์ค‘๋ณต ์‹œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ }; afterInsert?: { refreshData?: boolean; // ์ €์žฅ ํ›„ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ (ํ…Œ์ด๋ธ”๋ฆฌ์ŠคํŠธ, ์นด๋“œ ๋””์Šคํ”Œ๋ ˆ์ด) clearComponents?: boolean; // ์ €์žฅ ํ›„ ์ปดํฌ๋„ŒํŠธ ๊ฐ’ ์ดˆ๊ธฐํ™” showSuccessMessage?: boolean; // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ์—ฌ๋ถ€ (๊ธฐ๋ณธ: true) successMessage?: string; // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ }; }; // ์ด๋ฒคํŠธ ๋ฒ„์Šค ๋ฐœ์†ก ๊ด€๋ จ (event ์•ก์…˜์šฉ) eventConfig?: { eventName: string; // ๋ฐœ์†กํ•  ์ด๋ฒคํŠธ ์ด๋ฆ„ (V2_EVENTS ํ‚ค) eventPayload?: Record; // ์ด๋ฒคํŠธ ํŽ˜์ด๋กœ๋“œ (requestId๋Š” ์ž๋™ ์ƒ์„ฑ) }; } /** * ๋ฒ„ํŠผ ์•ก์…˜ ์‹คํ–‰ ์ปจํ…์ŠคํŠธ */ export interface ButtonActionContext { formData: Record; originalData?: Record; // ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ์šฉ ์›๋ณธ ๋ฐ์ดํ„ฐ screenId?: number; tableName?: string; userId?: string; // ๐Ÿ†• ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ID userName?: string; // ๐Ÿ†• ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ์ด๋ฆ„ companyCode?: string; // ๐Ÿ†• ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ํšŒ์‚ฌ ์ฝ”๋“œ onFormDataChange?: (fieldName: string, value: any) => void; onClose?: () => void; onRefresh?: () => void; onFlowRefresh?: () => void; // ํ”Œ๋กœ์šฐ ์ƒˆ๋กœ๊ณ ์นจ ์ฝœ๋ฐฑ onSave?: () => Promise; // ๐Ÿ†• EditModal์˜ handleSave ์ฝœ๋ฐฑ // ํ…Œ์ด๋ธ” ์„ ํƒ๋œ ํ–‰ ์ •๋ณด (๋‹ค์ค‘ ์„ ํƒ ์•ก์…˜์šฉ) selectedRows?: any[]; selectedRowsData?: any[]; // ํ”Œ๋กœ์šฐ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ •๋ณด (ํ”Œ๋กœ์šฐ ์œ„์ ฏ ์„ ํƒ ์•ก์…˜์šฉ) flowSelectedData?: any[]; flowSelectedStepId?: number | null; // ๐Ÿ†• ๊ฐ™์€ ํ™”๋ฉด์˜ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ (TableList ์ž๋™ ๊ฐ์ง€์šฉ) allComponents?: any[]; // ์ œ์–ด ์‹คํ–‰์„ ์œ„ํ•œ ์ถ”๊ฐ€ ์ •๋ณด buttonId?: string; // ํ…Œ์ด๋ธ” ์ •๋ ฌ ์ •๋ณด (์—‘์…€ ๋‹ค์šด๋กœ๋“œ์šฉ) sortBy?: string; // ์ •๋ ฌ ์ปฌ๋Ÿผ๋ช… sortOrder?: "asc" | "desc"; // ์ •๋ ฌ ๋ฐฉํ–ฅ columnOrder?: string[]; // ์ปฌ๋Ÿผ ์ˆœ์„œ (์‚ฌ์šฉ์ž๊ฐ€ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์œผ๋กœ ๋ณ€๊ฒฝํ•œ ์ˆœ์„œ) tableDisplayData?: any[]; // ํ™”๋ฉด์— ํ‘œ์‹œ๋œ ๋ฐ์ดํ„ฐ (์ •๋ ฌ ๋ฐ ์ปฌ๋Ÿผ ์ˆœ์„œ ์ ์šฉ๋จ) // ๐Ÿ†• ์—‘์…€ ๋‹ค์šด๋กœ๋“œ ๊ฐœ์„ ์„ ์œ„ํ•œ ์ถ”๊ฐ€ ํ•„๋“œ filterConditions?: Record; // ํ•„ํ„ฐ ์กฐ๊ฑด (์˜ˆ: { status: "active", dept: "dev" }) searchTerm?: string; // ๊ฒ€์ƒ‰์–ด searchColumn?: string; // ๊ฒ€์ƒ‰ ๋Œ€์ƒ ์ปฌ๋Ÿผ visibleColumns?: string[]; // ํ™”๋ฉด์— ํ‘œ์‹œ ์ค‘์ธ ์ปฌ๋Ÿผ ๋ชฉ๋ก (์ˆœ์„œ ํฌํ•จ) columnLabels?: Record; // ์ปฌ๋Ÿผ๋ช… โ†’ ๋ผ๋ฒจ๋ช… ๋งคํ•‘ (ํ•œ๊ธ€) currentPage?: number; // ํ˜„์žฌ ํŽ˜์ด์ง€ pageSize?: number; // ํŽ˜์ด์ง€ ํฌ๊ธฐ totalItems?: number; // ์ „์ฒด ํ•ญ๋ชฉ ์ˆ˜ // ๐Ÿ†• ์ปดํฌ๋„ŒํŠธ๋ณ„ ์„ค์ • (parentDataMapping ๋“ฑ) componentConfigs?: Record; // ์ปดํฌ๋„ŒํŠธ ID โ†’ ์ปดํฌ๋„ŒํŠธ ์„ค์ • // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ (์ขŒ์ธก ํ™”๋ฉด์—์„œ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ) splitPanelParentData?: Record; // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ (quickInsert ๋“ฑ์—์„œ ์ขŒ์ธก ํŒจ๋„ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ์šฉ) splitPanelContext?: { selectedLeftData?: Record; refreshRightPanel?: () => void; }; // ๐Ÿ†• ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ (์ €์žฅ ํ›„ ์ œ์–ด ์‹คํ–‰ ์‹œ ํ”Œ๋กœ์šฐ์— ์ „๋‹ฌ) savedData?: any; } /** * ๐Ÿ†• ํŠน์ˆ˜ ํ‚ค์›Œ๋“œ๋ฅผ ์‹ค์ œ ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ—ฌํผ ํ•จ์ˆ˜ * ์ง€์›ํ•˜๋Š” ํ‚ค์›Œ๋“œ: * - __userId__ : ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ID * - __userName__ : ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ์ด๋ฆ„ * - __companyCode__ : ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž์˜ ํšŒ์‚ฌ ์ฝ”๋“œ * - __screenId__ : ํ˜„์žฌ ํ™”๋ฉด ID * - __tableName__ : ํ˜„์žฌ ํ…Œ์ด๋ธ”๋ช… */ export function resolveSpecialKeyword(sourceField: string | undefined, context: ButtonActionContext): any { if (!sourceField) return undefined; // ํŠน์ˆ˜ ํ‚ค์›Œ๋“œ ์ฒ˜๋ฆฌ switch (sourceField) { case "__userId__": return context.userId; case "__userName__": return context.userName; case "__companyCode__": return context.companyCode; case "__screenId__": return context.screenId; case "__tableName__": return context.tableName; default: // ์ผ๋ฐ˜ ํผ ๋ฐ์ดํ„ฐ์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ return context.formData?.[sourceField]; } } /** * ๋ฒ„ํŠผ ์•ก์…˜ ์‹คํ–‰๊ธฐ */ export class ButtonActionExecutor { /** * ์•ก์…˜ ์‹คํ–‰ */ static async executeAction(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { // ํ™•์ธ ๋กœ์ง์€ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ๋Š” ์ œ๊ฑฐ switch (config.type) { case "save": return await this.handleSave(config, context); case "delete": return await this.handleDelete(config, context); case "copy": return await this.handleCopy(config, context); case "navigate": return this.handleNavigate(config, context); case "openModalWithData": // deprecated: modal๋กœ ํ†ตํ•ฉ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€) return await this.handleModal({ ...config, passSelectedData: true, autoDetectDataSource: true }, context); case "openRelatedModal": return await this.handleOpenRelatedModal(config, context); case "modal": return await this.handleModal(config, context); case "edit": return await this.handleEdit(config, context); case "control": return this.handleControl(config, context); case "view_table_history": return this.handleViewTableHistory(config, context); case "excel_download": return await this.handleExcelDownload(config, context); case "excel_upload": return await this.handleExcelUpload(config, context); case "barcode_scan": return await this.handleBarcodeScan(config, context); case "code_merge": return await this.handleCodeMerge(config, context); case "transferData": return await this.handleTransferData(config, context); // case "empty_vehicle": // return await this.handleEmptyVehicle(config, context); case "operation_control": return await this.handleOperationControl(config, context); case "swap_fields": return await this.handleSwapFields(config, context); case "quickInsert": return await this.handleQuickInsert(config, context); case "event": return await this.handleEvent(config, context); default: console.warn(`์ง€์›๋˜์ง€ ์•Š๋Š” ์•ก์…˜ ํƒ€์ž…: ${config.type}`); return false; } } catch (error) { console.error("๋ฒ„ํŠผ ์•ก์…˜ ์‹คํ–‰ ์˜ค๋ฅ˜:", error); toast.error(config.errorMessage || "์ž‘์—… ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } /** * ํ•„์ˆ˜ ํ•ญ๋ชฉ ๊ฒ€์ฆ */ private static validateRequiredFields(context: ButtonActionContext): { isValid: boolean; missingFields: string[] } { const missingFields: string[] = []; const { formData, allComponents } = context; if (!allComponents || allComponents.length === 0) { console.log("โš ๏ธ [validateRequiredFields] allComponents ์—†์Œ - ๊ฒ€์ฆ ์Šคํ‚ต"); return { isValid: true, missingFields: [] }; } allComponents.forEach((component: any) => { // ์ปดํฌ๋„ŒํŠธ์˜ required ์†์„ฑ ํ™•์ธ (์—ฌ๋Ÿฌ ์œ„์น˜์—์„œ ์ฒดํฌ) const isRequired = component.required === true || component.style?.required === true || component.componentConfig?.required === true; const columnName = component.columnName || component.style?.columnName; const label = component.label || component.style?.label || columnName; if (isRequired && columnName) { const value = formData[columnName]; // ๊ฐ’์ด ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธ (null, undefined, ๋นˆ ๋ฌธ์ž์—ด, ๊ณต๋ฐฑ๋งŒ ์žˆ๋Š” ๋ฌธ์ž์—ด) if (value === null || value === undefined || (typeof value === "string" && value.trim() === "")) { missingFields.push(label || columnName); } } }); return { isValid: missingFields.length === 0, missingFields, }; } /** * ์ €์žฅ ์•ก์…˜ ์ฒ˜๋ฆฌ (INSERT/UPDATE ์ž๋™ ํŒ๋‹จ - DB ๊ธฐ๋ฐ˜) */ private static saveCallCount = 0; // ๐Ÿ†• ํ˜ธ์ถœ ํšŸ์ˆ˜ ์ถ”์  private static saveLock: Map = new Map(); // ๐Ÿ†• ์ค‘๋ณต ํ˜ธ์ถœ ๋ฐฉ์ง€ ๋ฝ private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise { this.saveCallCount++; const callId = this.saveCallCount; // ๐Ÿ”ง ๋””๋ฒ„๊ทธ: context.formData ํ™•์ธ (handleSave ์ง„์ž… ์‹œ์ ) console.log("๐Ÿ” [handleSave] ์ง„์ž… ์‹œ context.formData:", { keys: Object.keys(context.formData || {}), hasCompanyImage: "company_image" in (context.formData || {}), hasCompanyLogo: "company_logo" in (context.formData || {}), companyImageValue: context.formData?.company_image, companyLogoValue: context.formData?.company_logo, }); const { formData, originalData, tableName, screenId, onSave } = context; // ๐Ÿ†• ์ค‘๋ณต ํ˜ธ์ถœ ๋ฐฉ์ง€: ๊ฐ™์€ screenId + tableName + formData ์กฐํ•ฉ์œผ๋กœ 2์ดˆ ๋‚ด ์žฌํ˜ธ์ถœ ์‹œ ๋ฌด์‹œ const formDataHash = JSON.stringify(Object.keys(formData).sort()); const lockKey = `${screenId}-${tableName}-${formDataHash}`; const lastCallTime = this.saveLock.get(lockKey) || 0; const now = Date.now(); const timeDiff = now - lastCallTime; if (timeDiff < 2000) { return true; // ์ค‘๋ณต ํ˜ธ์ถœ์€ ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ } this.saveLock.set(lockKey, now); // โœ… ํ•„์ˆ˜ ํ•ญ๋ชฉ ๊ฒ€์ฆ const requiredValidation = this.validateRequiredFields(context); if (!requiredValidation.isValid) { console.log("โŒ [handleSave] ํ•„์ˆ˜ ํ•ญ๋ชฉ ๋ˆ„๋ฝ:", requiredValidation.missingFields); toast.error(`ํ•„์ˆ˜ ํ•ญ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”: ${requiredValidation.missingFields.join(", ")}`); return false; } // โœ… ์ž…๋ ฅ ํ˜•์‹ ๊ฒ€์ฆ (์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ, URL ๋“ฑ) const formatValidationDetail = { errors: [] as Array<{ columnName: string; message: string }> }; window.dispatchEvent( new CustomEvent("validateFormInputs", { detail: formatValidationDetail, }), ); // ์•ฝ๊ฐ„์˜ ๋Œ€๊ธฐ (์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰๋˜์ง€๋งŒ ์•ˆ์ „์„ ์œ„ํ•ด) await new Promise((resolve) => setTimeout(resolve, 50)); if (formatValidationDetail.errors.length > 0) { const errorMessages = formatValidationDetail.errors.map((e) => e.message); console.log("โŒ [handleSave] ์ž…๋ ฅ ํ˜•์‹ ๊ฒ€์ฆ ์‹คํŒจ:", formatValidationDetail.errors); toast.error(`์ž…๋ ฅ ํ˜•์‹์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”: ${errorMessages.join(", ")}`); return false; } // ๐Ÿ†• EditModal ๋“ฑ์—์„œ ์ „๋‹ฌ๋œ onSave ์ฝœ๋ฐฑ์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ if (onSave) { try { await onSave(); return true; } catch (error) { console.error("โŒ [handleSave] onSave ์ฝœ๋ฐฑ ์‹คํ–‰ ์˜ค๋ฅ˜:", error); throw error; } } console.log("โš ๏ธ [handleSave] onSave ์ฝœ๋ฐฑ ์—†์Œ - ๊ธฐ๋ณธ ์ €์žฅ ๋กœ์ง ์‹คํ–‰"); // ๐Ÿ†• ์ €์žฅ ์ „ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (SelectedItemsDetailInput ๋“ฑ์—์„œ ์ตœ์‹  ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘) // context.formData๋ฅผ ์ด๋ฒคํŠธ detail์— ํฌํ•จํ•˜์—ฌ ์ง์ ‘ ์ˆ˜์ • ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ // skipDefaultSave ํ”Œ๋ž˜๊ทธ๋ฅผ ํ†ตํ•ด ๊ธฐ๋ณธ ์ €์žฅ ๋กœ์ง์„ ๊ฑด๋„ˆ๋›ธ ์ˆ˜ ์žˆ์Œ // ๐Ÿ”ง ๋””๋ฒ„๊ทธ: beforeFormSave ์ด๋ฒคํŠธ ์ „ formData ํ™•์ธ console.log("๐Ÿ” [handleSave] beforeFormSave ์ด๋ฒคํŠธ ์ „:", { keys: Object.keys(context.formData || {}), hasCompanyImage: "company_image" in (context.formData || {}), companyImageValue: context.formData?.company_image, }); const beforeSaveEventDetail = { formData: context.formData, skipDefaultSave: false, validationFailed: false, validationErrors: [] as string[], }; window.dispatchEvent( new CustomEvent("beforeFormSave", { detail: beforeSaveEventDetail, }), ); // ์•ฝ๊ฐ„์˜ ๋Œ€๊ธฐ ์‹œ๊ฐ„์„ ์ฃผ์–ด ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ formData๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ await new Promise((resolve) => setTimeout(resolve, 100)); // ๐Ÿ”ง ๋””๋ฒ„๊ทธ: beforeFormSave ์ด๋ฒคํŠธ ํ›„ formData ํ™•์ธ console.log("๐Ÿ” [handleSave] beforeFormSave ์ด๋ฒคํŠธ ํ›„:", { keys: Object.keys(context.formData || {}), hasCompanyImage: "company_image" in (context.formData || {}), companyImageValue: context.formData?.company_image, }); // ๊ฒ€์ฆ ์‹คํŒจ ์‹œ ์ €์žฅ ์ค‘๋‹จ if (beforeSaveEventDetail.validationFailed) { console.log("โŒ [handleSave] ๊ฒ€์ฆ ์‹คํŒจ๋กœ ์ €์žฅ ์ค‘๋‹จ:", beforeSaveEventDetail.validationErrors); return false; } // ๐Ÿ”ง skipDefaultSave ํ”Œ๋ž˜๊ทธ ํ™•์ธ - SelectedItemsDetailInput ๋“ฑ์—์„œ ์ž์ฒด UPSERT ์ฒ˜๋ฆฌ ์‹œ ๊ธฐ๋ณธ ์ €์žฅ ๊ฑด๋„ˆ๋›ฐ๊ธฐ if (beforeSaveEventDetail.skipDefaultSave) { return true; } // ๐Ÿ†• _tableSection_ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ (TableSectionRenderer ์‚ฌ์šฉ ์‹œ) // beforeFormSave ์ด๋ฒคํŠธ ํ›„์— ์ฒดํฌํ•ด์•ผ UniversalFormModal์—์„œ ๋ณ‘ํ•ฉ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Œ const hasTableSectionData = Object.keys(context.formData || {}).some( (k) => k.startsWith("_tableSection_") || k.startsWith("__tableSection_"), ); if (hasTableSectionData) { } // ๐Ÿ†• EditModal ๋“ฑ์—์„œ ์ „๋‹ฌ๋œ onSave ์ฝœ๋ฐฑ์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ // ๋‹จ, _tableSection_ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๊ฑด๋„ˆ๋›ฐ๊ธฐ (handleUniversalFormModalTableSectionSave๊ฐ€ ์ฒ˜๋ฆฌ) if (onSave && !hasTableSectionData) { try { await onSave(); return true; } catch (error) { console.error("โŒ [handleSave] onSave ์ฝœ๋ฐฑ ์‹คํ–‰ ์˜ค๋ฅ˜:", error); throw error; } } console.log("โš ๏ธ [handleSave] ๊ธฐ๋ณธ ์ €์žฅ ๋กœ์ง ์‹คํ–‰ (onSave ์ฝœ๋ฐฑ ์—†์Œ ๋˜๋Š” _tableSection_ ๋ฐ์ดํ„ฐ ์žˆ์Œ)"); // ๐Ÿ†• ๋ ‰ ๊ตฌ์กฐ ์ปดํฌ๋„ŒํŠธ ์ผ๊ด„ ์ €์žฅ ๊ฐ์ง€ let rackStructureLocations: any[] | undefined; let rackStructureFieldKey = "_rackStructureLocations"; let hasEmptyRackStructureField = false; // formData์—์„œ ๋ ‰ ๊ตฌ์กฐ ๋ฐ์ดํ„ฐ ๋˜๋Š” ๋นˆ ๋ฐฐ์—ด ์ฐพ๊ธฐ for (const [key, value] of Object.entries(context.formData || {})) { // ๋ฐฐ์—ด์ธ ๊ฒฝ์šฐ๋งŒ ์ฒดํฌ if (Array.isArray(value)) { if (value.length > 0 && value[0]) { const firstItem = value[0]; const isNewFormat = firstItem.location_code && firstItem.location_name && firstItem.row_num !== undefined && firstItem.level_num !== undefined; const isOldFormat = firstItem.locationCode && firstItem.locationName && firstItem.rowNum !== undefined && firstItem.levelNum !== undefined; if (isNewFormat || isOldFormat) { rackStructureLocations = value; rackStructureFieldKey = key; break; } } else if (value.length === 0 && key.startsWith("comp_")) { // comp_๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋นˆ ๋ฐฐ์—ด์€ ๋ ‰ ๊ตฌ์กฐ ์ปดํฌ๋„ŒํŠธ์ผ ๊ฐ€๋Šฅ์„ฑ ์žˆ์Œ // allComponents์—์„œ ํ™•์ธ (v1, v2 ๋ชจ๋‘ ์ง€์›) const rackStructureComponentInLayout = context.allComponents?.find( (comp: any) => comp.type === "component" && (comp.componentId === "rack-structure" || comp.componentId === "v2-rack-structure") && comp.columnName === key, ); if (rackStructureComponentInLayout) { hasEmptyRackStructureField = true; rackStructureFieldKey = key; } } } } // ๋ ‰ ๊ตฌ์กฐ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ์ง€๋งŒ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ if (hasEmptyRackStructureField && (!rackStructureLocations || rackStructureLocations.length === 0)) { alert("๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ๋จผ์ € ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”.\n\n๋ ‰ ๊ตฌ์กฐ ์กฐ๊ฑด์„ ์„ค์ •ํ•œ ํ›„ '๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ƒ์„ฑ' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์„ธ์š”."); return false; } // ๐Ÿ†• ๋ ‰ ๊ตฌ์กฐ ๋“ฑ๋ก ํ™”๋ฉด ๊ฐ์ง€ (warehouse_location ํ…Œ์ด๋ธ” + floor/zone ํ•„๋“œ ์žˆ์Œ + ๋ ‰ ๊ตฌ์กฐ ๋ฐ์ดํ„ฐ ์—†์Œ) // ์ด ๊ฒฝ์šฐ ์ผ๋ฐ˜ ์ €์žฅ์„ ์ฐจ๋‹จํ•˜๊ณ  ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ƒ์„ฑ์„ ์š”๊ตฌ const isRackStructureScreen = context.tableName === "warehouse_location" && context.formData?.floor && context.formData?.zone && !rackStructureLocations; if (isRackStructureScreen) { alert( "๋ ‰ ๊ตฌ์กฐ ๋“ฑ๋ก ํ™”๋ฉด์ž…๋‹ˆ๋‹ค.\n\n" + "๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ๋จผ์ € ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”.\n" + "- ์ค‘๋ณต๋œ ์œ„์น˜๊ฐ€ ์žˆ์œผ๋ฉด ๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.\n" + "- ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ์—ด/๋‹จ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”.", ); return false; } // ๋ ‰ ๊ตฌ์กฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์ผ๊ด„ ์ €์žฅ if (rackStructureLocations && rackStructureLocations.length > 0) { return await this.handleRackStructureBatchSave(config, context, rackStructureLocations, rackStructureFieldKey); } // ๐Ÿ†• SelectedItemsDetailInput ๋ฐฐ์น˜ ์ €์žฅ ์ฒ˜๋ฆฌ (fieldGroups ๊ตฌ์กฐ) // ๐Ÿ”ง formData ์ž์ฒด๊ฐ€ ๋ฐฐ์—ด์ธ ๊ฒฝ์šฐ (ScreenModal์˜ ๊ทธ๋ฃน ๋ ˆ์ฝ”๋“œ ์ˆ˜์ •) if (Array.isArray(context.formData)) { console.log( "โš ๏ธ [handleSave] formData๊ฐ€ ๋ฐฐ์—ด์ž…๋‹ˆ๋‹ค - SelectedItemsDetailInput์ด ์ด๋ฏธ ์ฒ˜๋ฆฌํ–ˆ์œผ๋ฏ€๋กœ ์ผ๋ฐ˜ ์ €์žฅ ๊ฑด๋„ˆ๋œ€", ); console.log("โš ๏ธ [handleSave] formData ๋ฐฐ์—ด:", context.formData); // โœ… SelectedItemsDetailInput์ด ์ด๋ฏธ UPSERT๋ฅผ ์‹คํ–‰ํ–ˆ์œผ๋ฏ€๋กœ ์ผ๋ฐ˜ ์ €์žฅ์„ ๊ฑด๋„ˆ๋œ€ return true; // ์„ฑ๊ณต์œผ๋กœ ๋ฐ˜ํ™˜ } const selectedItemsKeys = Object.keys(context.formData).filter((key) => { const value = context.formData[key]; return Array.isArray(value) && value.length > 0 && value[0]?.originalData && value[0]?.fieldGroups; }); if (selectedItemsKeys.length > 0) { return await this.handleBatchSave(config, context, selectedItemsKeys); } else { console.log("โš ๏ธ [handleSave] SelectedItemsDetailInput ๋ฐ์ดํ„ฐ ๊ฐ์ง€ ์‹คํŒจ - ์ผ๋ฐ˜ ์ €์žฅ ์ง„ํ–‰"); // ๐Ÿ”ง ๋””๋ฒ„๊ทธ: formData ์ƒ์„ธ ํ™•์ธ console.log("๐Ÿ” [handleSave] formData ํ‚ค ๋ชฉ๋ก:", Object.keys(context.formData || {})); console.log("๐Ÿ” [handleSave] formData.company_image:", context.formData?.company_image); console.log("๐Ÿ” [handleSave] formData.company_logo:", context.formData?.company_logo); console.log("โš ๏ธ [handleSave] formData ์ „์ฒด ๋‚ด์šฉ:", context.formData); } // ๐Ÿ†• RepeaterFieldGroup JSON ๋ฌธ์ž์—ด ํŒŒ์‹ฑ ๋ฐ ์ €์žฅ ์ฒ˜๋ฆฌ // formData์— JSON ๋ฐฐ์—ด ๋ฌธ์ž์—ด์ด ์ €์žฅ๋œ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ (๋ฐ˜๋ณต_ํ•„๋“œ_๊ทธ๋ฃน ๋“ฑ) const repeaterJsonKeys = Object.keys(context.formData).filter((key) => { const value = context.formData[key]; if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) { try { const parsed = JSON.parse(value); return Array.isArray(parsed) && parsed.length > 0 && parsed[0]._targetTable; } catch { return false; } } return false; }); if (repeaterJsonKeys.length > 0) { // ๐ŸŽฏ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒ˜๋ฆฌ (RepeaterFieldGroup ์ €์žฅ ์ „์— ์‹คํ–‰) // ๐Ÿ”ง ์ˆ˜์ • ๋ชจ๋“œ ์ฒดํฌ: formData.id๊ฐ€ ์กด์žฌํ•˜๋ฉด UPDATE ๋ชจ๋“œ์ด๋ฏ€๋กœ ์ฑ„๋ฒˆ ์ฝ”๋“œ ์žฌํ• ๋‹น ๊ธˆ์ง€ const isEditModeRepeater = context.formData.id !== undefined && context.formData.id !== null && context.formData.id !== ""; const fieldsWithNumberingRepeater: Record = {}; // formData์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์„ค์ •๋œ ํ•„๋“œ ์ฐพ๊ธฐ for (const [key, value] of Object.entries(context.formData)) { if (key.endsWith("_numberingRuleId") && value) { const fieldName = key.replace("_numberingRuleId", ""); fieldsWithNumberingRepeater[fieldName] = value as string; } } // ๐Ÿ”ง ์ˆ˜์ • ๋ชจ๋“œ์—์„œ๋Š” ์ฑ„๋ฒˆ ์ฝ”๋“œ ํ• ๋‹น ๊ฑด๋„ˆ๋›ฐ๊ธฐ (๊ธฐ์กด ๋ฒˆํ˜ธ ์œ ์ง€) // ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ์—์„œ๋งŒ allocateCode ํ˜ธ์ถœํ•˜์—ฌ ์ƒˆ ๋ฒˆํ˜ธ ํ• ๋‹น if (Object.keys(fieldsWithNumberingRepeater).length > 0 && !isEditModeRepeater) { const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); for (const [fieldName, ruleId] of Object.entries(fieldsWithNumberingRepeater)) { try { // ๐Ÿ†• ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘ํ•œ ๊ฐ’์„ ์ „๋‹ฌ (์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ถ”์ถœ์šฉ) const userInputCode = context.formData[fieldName] as string; const allocateResult = await allocateNumberingCode(ruleId, userInputCode, context.formData); if (allocateResult.success && allocateResult.data?.generatedCode) { const newCode = allocateResult.data.generatedCode; context.formData[fieldName] = newCode; } else { console.warn(`โš ๏ธ [handleSave-RepeaterFieldGroup] ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์‹คํŒจ:`, allocateResult.error); } } catch (allocateError) { console.error(`โŒ [handleSave-RepeaterFieldGroup] ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์˜ค๋ฅ˜:`, allocateError); } } } else if (isEditModeRepeater) { } // ๐Ÿ†• ์ƒ๋‹จ ํผ ๋ฐ์ดํ„ฐ(๋งˆ์Šคํ„ฐ ์ •๋ณด) ์ถ”์ถœ // RepeaterFieldGroup JSON๊ณผ ์ปดํฌ๋„ŒํŠธ ํ‚ค๋ฅผ ์ œ์™ธํ•œ ๋‚˜๋จธ์ง€๊ฐ€ ๋งˆ์Šคํ„ฐ ์ •๋ณด const masterFields: Record = {}; Object.keys(context.formData).forEach((fieldKey) => { // ์ œ์™ธ ์กฐ๊ฑด if (fieldKey.startsWith("comp_")) return; if (fieldKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) return; if (fieldKey.endsWith("_label") || fieldKey.endsWith("_value_label")) return; const value = context.formData[fieldKey]; // JSON ๋ฐฐ์—ด ๋ฌธ์ž์—ด ์ œ์™ธ (RepeaterFieldGroup ๋ฐ์ดํ„ฐ) if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) return; // ๊ฐ์ฒด ํƒ€์ž…์ธ ๊ฒฝ์šฐ (๋ฒ”์šฉ_ํผ_๋ชจ๋‹ฌ ๋“ฑ) ๋‚ด๋ถ€ ํ•„๋“œ๋ฅผ ํŽผ์ณ์„œ ์ถ”๊ฐ€ if (typeof value === "object" && value !== null && !Array.isArray(value)) { Object.entries(value).forEach(([innerKey, innerValue]) => { if (innerKey.endsWith("_label") || innerKey.endsWith("_value_label")) return; if (innerValue !== undefined && innerValue !== null && innerValue !== "") { masterFields[innerKey] = innerValue; } }); return; } // ์œ ํšจํ•œ ๊ฐ’๋งŒ ํฌํ•จ if (value !== undefined && value !== null && value !== "") { masterFields[fieldKey] = value; } }); for (const key of repeaterJsonKeys) { try { const parsedData = JSON.parse(context.formData[key]); const repeaterTargetTable = parsedData[0]?._targetTable; if (!repeaterTargetTable) { console.warn(`โš ๏ธ [handleSave] RepeaterFieldGroup targetTable ์—†์Œ (key: ${key})`); continue; } // ๐Ÿ†• ํ’ˆ๋ชฉ ๊ณ ์œ  ํ•„๋“œ ๋ชฉ๋ก (RepeaterFieldGroup ์„ค์ •์—์„œ ๊ฐ€์ ธ์˜ด) // ์ฒซ ๋ฒˆ์งธ ์•„์ดํ…œ์˜ _repeaterFields์—์„œ ์ถ”์ถœ const repeaterFields: string[] = parsedData[0]?._repeaterFields || []; const itemOnlyFields = new Set([...repeaterFields, "id"]); // id๋Š” ํ•ญ์ƒ ํฌํ•จ for (const item of parsedData) { // ๋ฉ”ํƒ€ ํ•„๋“œ ์ œ๊ฑฐ const { _targetTable, _isNewItem, _existingRecord, _originalItemIds, _deletedItemIds, _repeaterFields, _subDataSelection, _subDataMaxValue, ...itemData } = item; // ๐Ÿ”ง ํ’ˆ๋ชฉ ๊ณ ์œ  ํ•„๋“œ๋งŒ ์ถ”์ถœ (RepeaterFieldGroup ์„ค์ • ๊ธฐ๋ฐ˜) const itemOnlyData: Record = {}; Object.keys(itemData).forEach((field) => { if (itemOnlyFields.has(field)) { itemOnlyData[field] = itemData[field]; } }); // ๐Ÿ†• ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์„ ํƒ์—์„œ ๊ฐ’ ์ถ”์ถœ (subDataSource ์„ค์ • ๊ธฐ๋ฐ˜) // ํ•„๋“œ ์ •์˜์—์„œ subDataSource.enabled๊ฐ€ true์ด๊ณ  sourceColumn์ด ์„ค์ •๋œ ํ•„๋“œ๋งŒ ์ฒ˜๋ฆฌ if (_subDataSelection && typeof _subDataSelection === "object") { // _repeaterFieldsConfig์—์„œ subDataSource ์„ค์ • ํ™•์ธ const fieldsConfig = item._repeaterFieldsConfig as | Array<{ name: string; subDataSource?: { enabled: boolean; sourceColumn: string }; }> | undefined; if (fieldsConfig && Array.isArray(fieldsConfig)) { fieldsConfig.forEach((fieldConfig) => { if (fieldConfig.subDataSource?.enabled && fieldConfig.subDataSource?.sourceColumn) { const targetField = fieldConfig.name; // ํ•„๋“œ๋ช… = ์ €์žฅํ•  ์ปฌ๋Ÿผ๋ช… const sourceColumn = fieldConfig.subDataSource.sourceColumn; const sourceValue = _subDataSelection[sourceColumn]; if (sourceValue !== undefined && sourceValue !== null) { itemOnlyData[targetField] = sourceValue; } } }); } else { // ํ•˜์œ„ ํ˜ธํ™˜์„ฑ: fieldsConfig๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ์กด ๋ฐฉ์‹ ์‚ฌ์šฉ Object.keys(_subDataSelection).forEach((subDataKey) => { if ( itemOnlyData[subDataKey] === undefined || itemOnlyData[subDataKey] === null || itemOnlyData[subDataKey] === "" ) { const subDataValue = _subDataSelection[subDataKey]; if (subDataValue !== undefined && subDataValue !== null) { itemOnlyData[subDataKey] = subDataValue; } } }); } } // ๐Ÿ”ง ๋งˆ์Šคํ„ฐ ์ •๋ณด + ํ’ˆ๋ชฉ ๊ณ ์œ  ์ •๋ณด ๋ณ‘ํ•ฉ // masterFields: ์ƒ๋‹จ ํผ์—์„œ ์ˆ˜์ •ํ•œ ์ตœ์‹  ๋งˆ์Šคํ„ฐ ์ •๋ณด // itemOnlyData: ํ’ˆ๋ชฉ ๊ณ ์œ  ํ•„๋“œ๋งŒ (ํ’ˆ๋ฒˆ, ํ’ˆ๋ช…, ์ˆ˜๋Ÿ‰ ๋“ฑ) const dataWithMeta: Record = { ...masterFields, // ์ƒ๋‹จ ๋งˆ์Šคํ„ฐ ์ •๋ณด (์ตœ์‹ ) ...itemOnlyData, // ํ’ˆ๋ชฉ ๊ณ ์œ  ํ•„๋“œ๋งŒ created_by: context.userId, updated_by: context.userId, company_code: context.companyCode, }; // ๋ถˆํ•„์š”ํ•œ ํ•„๋“œ ์ œ๊ฑฐ Object.keys(dataWithMeta).forEach((field) => { if (field.endsWith("_label") || field.endsWith("_value_label") || field.endsWith("_numberingRuleId")) { delete dataWithMeta[field]; } }); // ์ƒˆ ๋ ˆ์ฝ”๋“œ vs ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ ํŒ๋‹จ const isNewRecord = _isNewItem || !item.id || item.id === "" || item.id === undefined; if (isNewRecord) { // INSERT - DynamicFormApi ์‚ฌ์šฉํ•˜์—ฌ ์ œ์–ด๊ด€๋ฆฌ ์‹คํ–‰ delete dataWithMeta.id; const insertResult = await DynamicFormApi.saveFormData({ screenId: context.screenId || 0, tableName: repeaterTargetTable, data: dataWithMeta as Record, }); } else if (item.id && _existingRecord === true) { // UPDATE - ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ const originalData = { id: item.id }; const updatedData = { ...dataWithMeta, id: item.id }; const updateResult = await apiClient.put(`/table-management/tables/${repeaterTargetTable}/edit`, { originalData, updatedData, }); } } } catch (err) { console.error(`โŒ [handleSave] RepeaterFieldGroup ์ €์žฅ ์‹คํŒจ (key: ${key}):`, err); } } // RepeaterFieldGroup ์ €์žฅ ์™„๋ฃŒ ํ›„ ์ƒˆ๋กœ๊ณ ์นจ context.onRefresh?.(); context.onFlowRefresh?.(); window.dispatchEvent(new CustomEvent("closeEditModal")); window.dispatchEvent(new CustomEvent("saveSuccessInModal")); return true; } // ๐Ÿ†• Universal Form Modal ํ…Œ์ด๋ธ” ์„น์…˜ ๋ณ‘ํ•ฉ ์ €์žฅ ์ฒ˜๋ฆฌ // ๋ฒ”์šฉ_ํผ_๋ชจ๋‹ฌ ๋‚ด๋ถ€์— _tableSection_ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ๊ณตํ†ต ํ•„๋“œ + ๊ฐœ๋ณ„ ํ’ˆ๋ชฉ ๋ณ‘ํ•ฉ ์ €์žฅ const universalFormModalResult = await this.handleUniversalFormModalTableSectionSave(config, context, formData); if (universalFormModalResult.handled) { return universalFormModalResult.success; } // ํผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ if (config.validateForm) { const validation = this.validateFormData(formData); if (!validation.isValid) { toast.error(`์ž…๋ ฅ๊ฐ’์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”: ${validation.errors.join(", ")}`); return false; } } try { // API ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์ง€์ •๋œ ๊ฒฝ์šฐ if (config.saveEndpoint) { const response = await fetch(config.saveEndpoint, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(formData), }); if (!response.ok) { throw new Error(`์ €์žฅ ์‹คํŒจ: ${response.statusText}`); } } // saveResult๋ฅผ ์ƒ์œ„ ์Šค์ฝ”ํ”„์—์„œ ์ •์˜ (repeaterSave ์ด๋ฒคํŠธ์—์„œ ์‚ฌ์šฉ) let saveResult: { success: boolean; data?: any; message?: string } | undefined; if (tableName && screenId) { // DB์—์„œ ์‹ค์ œ ๊ธฐ๋ณธํ‚ค ์กฐํšŒํ•˜์—ฌ INSERT/UPDATE ์ž๋™ ํŒ๋‹จ const primaryKeyResult = await DynamicFormApi.getTablePrimaryKeys(tableName); if (!primaryKeyResult.success) { throw new Error(primaryKeyResult.message || "๊ธฐ๋ณธํ‚ค ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } const primaryKeys = primaryKeyResult.data || []; const primaryKeyValue = this.extractPrimaryKeyValueFromDB(formData, primaryKeys); // ๐Ÿ”ง ์ˆ˜์ •: originalData๊ฐ€ ์žˆ๊ณ  ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด UPDATE ๋ชจ๋“œ๋กœ ์ฒ˜๋ฆฌ // originalData๋Š” ์ˆ˜์ • ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ editData๋กœ ์ „๋‹ฌ๋˜์–ด context.originalData๋กœ ์„ค์ •๋จ // ๋นˆ ๊ฐ์ฒด {}๋„ truthy์ด๋ฏ€๋กœ Object.keys๋กœ ์‹ค์ œ ๋ฐ์ดํ„ฐ ์œ ๋ฌด ํ™•์ธ const hasRealOriginalData = originalData && Object.keys(originalData).length > 0; // ๐Ÿ†• ํด๋ฐฑ ๋กœ์ง: originalData๊ฐ€ ์—†์–ด๋„ formData์— id๊ฐ€ ์žˆ์œผ๋ฉด UPDATE๋กœ ํŒ๋‹จ // ์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ ๋“ฑ์—์„œ originalData ์ „๋‹ฌ์ด ๋ˆ„๋ฝ๋˜๋Š” ๊ฒฝ์šฐ๋ฅผ ์ฒ˜๋ฆฌ const hasIdInFormData = formData.id !== undefined && formData.id !== null && formData.id !== ""; const isUpdate = (hasRealOriginalData || hasIdInFormData) && !!primaryKeyValue; if (isUpdate) { // UPDATE ์ฒ˜๋ฆฌ - ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ ์‚ฌ์šฉ (์›๋ณธ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ) // ๐Ÿ”ง UPDATE ์ „ formData ๋ฐฐ์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด ๋ณ€ํ™˜ (๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ) for (const key of Object.keys(formData)) { const value = formData[key]; if (Array.isArray(value)) { // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ์ธ์ง€ ํ™•์ธ (๊ฐ์ฒด ๋ฐฐ์—ด์ด๊ณ  _targetTable ๋˜๋Š” _isNewItem์ด ์žˆ์œผ๋ฉด ๋ฆฌํ”ผํ„ฐ) const isRepeaterData = value.length > 0 && typeof value[0] === "object" && value[0] !== null && ("_targetTable" in value[0] || "_isNewItem" in value[0] || "_existingRecord" in value[0]); if (!isRepeaterData) { // ๐Ÿ”ง ๋‹ค์ค‘ ์„ ํƒ ๋ฐฐ์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ const stringValue = value .map(v => typeof v === "number" ? String(v) : v) .filter(v => v !== null && v !== undefined && v !== "") .join(","); console.log(`๐Ÿ”ง [handleSave UPDATE] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜: ${key}`, { original: value, converted: stringValue }); formData[key] = stringValue; } } } if (hasRealOriginalData) { // ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ: ๋ณ€๊ฒฝ๋œ ํ•„๋“œ๋งŒ ์—…๋ฐ์ดํŠธ saveResult = await DynamicFormApi.updateFormDataPartial(primaryKeyValue, originalData, formData, tableName); } else { // ์ „์ฒด ์—…๋ฐ์ดํŠธ (originalData ์—†์ด id๋กœ UPDATE ํŒ๋‹จ๋œ ๊ฒฝ์šฐ) saveResult = await DynamicFormApi.updateFormData(primaryKeyValue, { tableName, data: formData, }); } } else { // INSERT ์ฒ˜๋ฆฌ // ๐Ÿ†• ์ž๋™์œผ๋กœ ์ž‘์„ฑ์ž ์ •๋ณด ์ถ”๊ฐ€ if (!context.userId) { throw new Error("์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”."); } const writerValue = context.userId; const companyCodeValue = context.companyCode || ""; // console.log("๐Ÿ‘ค [buttonActions] ์‚ฌ์šฉ์ž ์ •๋ณด:", { // userId: context.userId, // userName: context.userName, // companyCode: context.companyCode, // formDataWriter: formData.writer, // formDataCompanyCode: formData.company_code, // defaultWriterValue: writerValue, // companyCodeValue, // }); // ๐ŸŽฏ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒ˜๋ฆฌ (์ €์žฅ ์‹œ์ ์— ์‹ค์ œ ์ˆœ๋ฒˆ ์ฆ๊ฐ€) const fieldsWithNumbering: Record = {}; // formData์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์„ค์ •๋œ ํ•„๋“œ ์ฐพ๊ธฐ for (const [key, value] of Object.entries(formData)) { if (key.endsWith("_numberingRuleId") && value) { const fieldName = key.replace("_numberingRuleId", ""); fieldsWithNumbering[fieldName] = value as string; } } // ๐Ÿ”ฅ ์ €์žฅ ์‹œ์ ์— allocateCode ํ˜ธ์ถœํ•˜์—ฌ ์‹ค์ œ ์ˆœ๋ฒˆ ์ฆ๊ฐ€ if (Object.keys(fieldsWithNumbering).length > 0) { const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); let hasAllocationFailure = false; const failedFields: string[] = []; for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) { try { // ๐Ÿ†• ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘ํ•œ ๊ฐ’์„ ์ „๋‹ฌ (์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ถ”์ถœ์šฉ) const userInputCode = formData[fieldName] as string; const allocateResult = await allocateNumberingCode(ruleId, userInputCode, formData); if (allocateResult.success && allocateResult.data?.generatedCode) { const newCode = allocateResult.data.generatedCode; formData[fieldName] = newCode; } else { console.warn(`โš ๏ธ ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์‹คํŒจ:`, allocateResult.error); // ๐Ÿ†• ๊ธฐ์กด ๊ฐ’์ด ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด ์‹คํŒจ๋กœ ํ‘œ์‹œ if (!formData[fieldName] || formData[fieldName] === "") { hasAllocationFailure = true; failedFields.push(fieldName); } } } catch (allocateError) { console.error(`โŒ ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์˜ค๋ฅ˜:`, allocateError); // ๐Ÿ†• ๊ธฐ์กด ๊ฐ’์ด ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด ์‹คํŒจ๋กœ ํ‘œ์‹œ if (!formData[fieldName] || formData[fieldName] === "") { hasAllocationFailure = true; failedFields.push(fieldName); } } } // ๐Ÿ†• ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์‹คํŒจ ์‹œ ์ €์žฅ ์ค‘๋‹จ if (hasAllocationFailure) { const fieldNames = failedFields.join(", "); toast.error(`์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค (${fieldNames}). ํ™”๋ฉด ์„ค์ •์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.`); console.error(`โŒ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์‹คํŒจ๋กœ ์ €์žฅ ์ค‘๋‹จ. ์‹คํŒจ ํ•„๋“œ: ${fieldNames}`); console.error("๐Ÿ’ก ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•: ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ํ•ด๋‹น ํ•„๋“œ์˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ์„ค์ •์„ ํ™•์ธํ•˜์„ธ์š”."); return false; } } // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ (์ขŒ์ธก ํ™”๋ฉด์—์„œ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ) // ๐Ÿ”ง ์ค‘์š”: ์‹ ๊ทœ ๋“ฑ๋ก ์‹œ์—๋Š” ์—ฐ๊ฒฐ ํ•„๋“œ(equipment_code ๋“ฑ)๋งŒ ๋ณ‘ํ•ฉํ•ด์•ผ ํ•จ // ๋ชจ๋“  ํ•„๋“œ๋ฅผ ๋ณ‘ํ•ฉํ•˜๋ฉด ๋™์ผํ•œ ์ปฌ๋Ÿผ๋ช…์ด ์žˆ์„ ๋•Œ ๋ถ€๋ชจ ๊ฐ’์ด ๋“ค์–ด๊ฐ€๋Š” ๋ฌธ์ œ ๋ฐœ์ƒ // ์˜ˆ: ์„ค๋น„์˜ manufacturer๊ฐ€ ์†Œ๋ชจํ’ˆ์˜ manufacturer๋กœ ๋“ค์–ด๊ฐ const rawSplitPanelData = context.splitPanelParentData || {}; // INSERT ๋ชจ๋“œ์—์„œ๋Š” ์—ฐ๊ฒฐ์— ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ์ถ”์ถœ const cleanedSplitPanelData: Record = {}; // ํ•„์ˆ˜ ์—ฐ๊ฒฐ ํ•„๋“œ: company_code (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ) if (rawSplitPanelData.company_code) { cleanedSplitPanelData.company_code = rawSplitPanelData.company_code; } // ์—ฐ๊ฒฐ ํ•„๋“œ ํŒจํ„ด์œผ๋กœ ์ž๋™ ๊ฐ์ง€ (equipment_code, xxx_code, xxx_id ํŒจํ„ด) const linkFieldPatterns = ["_code", "_id"]; const excludeFields = [ "id", "company_code", "created_date", "updated_date", "created_at", "updated_at", "writer", "created_by", "updated_by", ]; for (const [key, value] of Object.entries(rawSplitPanelData)) { if (excludeFields.includes(key)) continue; if (value === undefined || value === null) continue; // ์—ฐ๊ฒฐ ํ•„๋“œ ํŒจํ„ด ํ™•์ธ const isLinkField = linkFieldPatterns.some((pattern) => key.endsWith(pattern)); if (isLinkField) { cleanedSplitPanelData[key] = value; } } if (Object.keys(rawSplitPanelData).length > 0) { } const dataWithUserInfo = { ...cleanedSplitPanelData, // ์ •๋ฆฌ๋œ ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๋จผ์ € ์ ์šฉ ...formData, // ํผ ๋ฐ์ดํ„ฐ๊ฐ€ ์šฐ์„  (๋ฎ์–ด์“ฐ๊ธฐ ๊ฐ€๋Šฅ) writer: formData.writer || writerValue, // โœ… ์ž…๋ ฅ๊ฐ’ ์šฐ์„ , ์—†์œผ๋ฉด userId created_by: writerValue, // created_by๋Š” ํ•ญ์ƒ ๋กœ๊ทธ์ธํ•œ ์‚ฌ๋žŒ updated_by: writerValue, // updated_by๋Š” ํ•ญ์ƒ ๋กœ๊ทธ์ธํ•œ ์‚ฌ๋žŒ company_code: formData.company_code || companyCodeValue, // โœ… ์ž…๋ ฅ๊ฐ’ ์šฐ์„ , ์—†์œผ๋ฉด user.companyCode }; // ๐Ÿ”ง formData์—์„œ๋„ id ์ œ๊ฑฐ (์‹ ๊ทœ INSERT์ด๋ฏ€๋กœ) if ("id" in dataWithUserInfo && !formData.id) { delete dataWithUserInfo.id; } // _numberingRuleId ํ•„๋“œ ์ œ๊ฑฐ (์‹ค์ œ ์ €์žฅํ•˜์ง€ ์•Š์Œ) for (const key of Object.keys(dataWithUserInfo)) { if (key.endsWith("_numberingRuleId")) { delete dataWithUserInfo[key]; } } // ๐Ÿ†• ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ - ๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ”์—๋Š” ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š์Œ // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ๋Š” ๋ณ„๋„์˜ RepeaterFieldGroup/V2Repeater ์ €์žฅ ๋กœ์ง์—์„œ ์ฒ˜๋ฆฌ๋จ // ๐Ÿ”ง ๋‹จ์ˆœ ๋ฐฐ์—ด(๋‹ค์ค‘ ์„ ํƒ)์€ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜, ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ๋ฐฐ์—ด์€ ์ œ๊ฑฐ for (const key of Object.keys(dataWithUserInfo)) { const value = dataWithUserInfo[key]; if (Array.isArray(value)) { // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ์ธ์ง€ ํ™•์ธ (๊ฐ์ฒด ๋ฐฐ์—ด์ด๊ณ  _targetTable ๋˜๋Š” _isNewItem์ด ์žˆ์œผ๋ฉด ๋ฆฌํ”ผํ„ฐ) const isRepeaterData = value.length > 0 && typeof value[0] === "object" && value[0] !== null && ("_targetTable" in value[0] || "_isNewItem" in value[0] || "_existingRecord" in value[0]); if (isRepeaterData) { // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ๋Š” ์ œ๊ฑฐ (๋ณ„๋„ ์ €์žฅ ๋กœ์ง์—์„œ ์ฒ˜๋ฆฌ) delete dataWithUserInfo[key]; } else { // ๐Ÿ”ง ๋‹ค์ค‘ ์„ ํƒ ๋ฐฐ์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ const stringValue = value .map(v => typeof v === "number" ? String(v) : v) .filter(v => v !== null && v !== undefined && v !== "") .join(","); console.log(`๐Ÿ”ง [handleSave] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜: ${key}`, { original: value, converted: stringValue }); dataWithUserInfo[key] = stringValue; } } } // ๐Ÿ†• ๋ฐ˜๋ณต ํ•„๋“œ ๊ทธ๋ฃน์—์„œ ์‚ญ์ œ๋œ ํ•ญ๋ชฉ ์ฒ˜๋ฆฌ // formData์˜ ๊ฐ ํ•„๋“œ์—์„œ _deletedItemIds๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ for (const [key, value] of Object.entries(dataWithUserInfo)) { let parsedValue = value; // JSON ๋ฌธ์ž์—ด์ธ ๊ฒฝ์šฐ ํŒŒ์‹ฑ ์‹œ๋„ if (typeof value === "string" && value.startsWith("[")) { try { parsedValue = JSON.parse(value); } catch (e) { // ํŒŒ์‹ฑ ์‹คํŒจํ•˜๋ฉด ์›๋ณธ ๊ฐ’ ์œ ์ง€ } } if (Array.isArray(parsedValue) && parsedValue.length > 0) { const firstItem = parsedValue[0]; const deletedItemIds = firstItem?._deletedItemIds; const targetTable = firstItem?._targetTable; if (deletedItemIds && deletedItemIds.length > 0 && targetTable) { // ์‚ญ์ œ API ํ˜ธ์ถœ - screenId ์ „๋‹ฌํ•˜์—ฌ ์ œ์–ด๊ด€๋ฆฌ ์‹คํ–‰ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•จ for (const itemId of deletedItemIds) { try { const deleteResult = await DynamicFormApi.deleteFormDataFromTable( itemId, targetTable, context.screenId, ); if (deleteResult.success) { } else { console.warn(`โš ๏ธ [handleSave] ํ•ญ๋ชฉ ์‚ญ์ œ ์‹คํŒจ: ${itemId}`, deleteResult.message); } } catch (deleteError) { console.error(`โŒ [handleSave] ํ•ญ๋ชฉ ์‚ญ์ œ ์˜ค๋ฅ˜: ${itemId}`, deleteError); } } } } } // ๐Ÿ†• RepeaterFieldGroup ๋ฐ์ดํ„ฐ ์ €์žฅ ์ฒ˜๋ฆฌ (_targetTable์ด ์žˆ๋Š” ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ) // formData์—์„œ _targetTable ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๊ฐ€ ํฌํ•จ๋œ ๋ฐฐ์—ด ํ•„๋“œ ์ฐพ๊ธฐ // ๐Ÿ†• ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ์ €์žฅ: ํ…Œ์ด๋ธ” ๊ฐ„ ์กฐ์ธ ๊ด€๊ณ„ ์บ์‹œ const joinRelationshipCache: Record = {}; for (const [fieldKey, fieldValue] of Object.entries(context.formData)) { // JSON ๋ฌธ์ž์—ด์ธ ๊ฒฝ์šฐ ํŒŒ์‹ฑ let parsedData = fieldValue; if (typeof fieldValue === "string" && fieldValue.startsWith("[")) { try { parsedData = JSON.parse(fieldValue); } catch { continue; } } // ๋ฐฐ์—ด์ด๊ณ  ์ฒซ ๋ฒˆ์งธ ํ•ญ๋ชฉ์— _targetTable์ด ์žˆ๋Š” ๊ฒฝ์šฐ๋งŒ ์ฒ˜๋ฆฌ if (!Array.isArray(parsedData) || parsedData.length === 0) continue; const firstItem = parsedData[0]; const repeaterTargetTable = firstItem?._targetTable; // _targetTable์ด ์—†๊ฑฐ๋‚˜, _repeatScreenModal_ ํ‚ค๋ฉด ์Šคํ‚ต (๋‹ค๋ฅธ ๋กœ์ง์—์„œ ์ฒ˜๋ฆฌ) if (!repeaterTargetTable || fieldKey.startsWith("_repeatScreenModal_")) continue; // ๐Ÿ†• V2Repeater๊ฐ€ ๋“ฑ๋ก๋œ ํ…Œ์ด๋ธ”์ด๋ฉด RepeaterFieldGroup ์ €์žฅ ์Šคํ‚ต // V2Repeater๊ฐ€ repeaterSave ์ด๋ฒคํŠธ๋กœ ์ €์žฅ ์ฒ˜๋ฆฌํ•จ // @ts-ignore - window์— ๋™์  ์†์„ฑ ์‚ฌ์šฉ const registeredV2RepeaterTables = Array.from(window.__v2RepeaterInstances || []); if (registeredV2RepeaterTables.includes(repeaterTargetTable)) { continue; } // ๐Ÿ†• ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ์กฐ์ธ ๊ด€๊ณ„ ์กฐํšŒ (๋ฉ”์ธ ํ…Œ์ด๋ธ” โ†’ ๋ฆฌํ”ผํ„ฐ ํ…Œ์ด๋ธ”) let joinRelationship: { mainColumn: string; detailColumn: string } | null = null; const cacheKey = `${tableName}:${repeaterTargetTable}`; if (tableName && repeaterTargetTable && tableName !== repeaterTargetTable) { // ์บ์‹œ์—์„œ ๋จผ์ € ํ™•์ธ if (cacheKey in joinRelationshipCache) { joinRelationship = joinRelationshipCache[cacheKey]; } else { try { const joinResponse = await apiClient.get( `/button-dataflow/join-relationship/${tableName}/${repeaterTargetTable}`, ); if (joinResponse.data?.success && joinResponse.data?.data?.found) { joinRelationship = { mainColumn: joinResponse.data.data.mainColumn, detailColumn: joinResponse.data.data.detailColumn, }; } } catch (joinError) { console.warn("โš ๏ธ [handleSave] ์กฐ์ธ ๊ด€๊ณ„ ์กฐํšŒ ์‹คํŒจ:", joinError); } // ๊ฒฐ๊ณผ๋ฅผ ์บ์‹œ์— ์ €์žฅ (์—†์–ด๋„ null๋กœ ์ €์žฅํ•˜์—ฌ ์žฌ์กฐํšŒ ๋ฐฉ์ง€) joinRelationshipCache[cacheKey] = joinRelationship; } } // ๐Ÿ†• ๋ฒ”์šฉ ํผ ๋ชจ๋‹ฌ์˜ ๊ณตํ†ต ํ•„๋“œ ์ถ”์ถœ (order_no, manager_id ๋“ฑ) // "๋ฒ”์šฉ_ํผ_๋ชจ๋‹ฌ" ํ‚ค์—์„œ ๊ณตํ†ต ํ•„๋“œ๋ฅผ ๊ฐ€์ ธ์˜ด const universalFormData = context.formData["๋ฒ”์šฉ_ํผ_๋ชจ๋‹ฌ"] as Record | undefined; const commonFields: Record = {}; if (universalFormData && typeof universalFormData === "object") { // ๊ณตํ†ต ํ•„๋“œ ๋ณต์‚ฌ (๋‚ด๋ถ€ ๋ฉ”ํƒ€ ํ•„๋“œ ์ œ์™ธ) for (const [key, value] of Object.entries(universalFormData)) { if (!key.startsWith("_") && !key.endsWith("_numberingRuleId") && value !== undefined && value !== "") { commonFields[key] = value; } } } // ๐Ÿ†• ๋ฃจํŠธ ๋ ˆ๋ฒจ formData์—์„œ RepeaterFieldGroup์— ์ „๋‹ฌํ•  ๊ณตํ†ต ํ•„๋“œ ์ถ”์ถœ // ๊ทœ์น™ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง: ํ•˜๋“œ์ฝ”๋”ฉ ๋Œ€์‹  ํŒจํ„ด์œผ๋กœ ์ œ์™ธํ•  ํ•„๋“œ๋ฅผ ์ •์˜ for (const [fieldName, value] of Object.entries(context.formData)) { // ์ œ์™ธ ๊ทœ์น™ 1: comp_๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ•„๋“œ (ํ•˜์œ„ ํ•ญ๋ชฉ ๋ฐฐ์—ด) if (fieldName.startsWith("comp_")) continue; // ์ œ์™ธ ๊ทœ์น™ 2: _numberingRuleId๋กœ ๋๋‚˜๋Š” ํ•„๋“œ (์ฑ„๋ฒˆ ๊ทœ์น™ ๋ฉ”ํƒ€ ์ •๋ณด) if (fieldName.endsWith("_numberingRuleId")) continue; // ์ œ์™ธ ๊ทœ์น™ 3: _๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ•„๋“œ (๋‚ด๋ถ€ ๋ฉ”ํƒ€ ํ•„๋“œ) if (fieldName.startsWith("_")) continue; // ์ œ์™ธ ๊ทœ์น™ 4: ๋ฐฐ์—ด ํƒ€์ž… (ํ•˜์œ„ ํ•ญ๋ชฉ ๋ฐ์ดํ„ฐ) if (Array.isArray(value)) continue; // ์ œ์™ธ ๊ทœ์น™ 5: ๊ฐ์ฒด ํƒ€์ž… (๋ณต์žกํ•œ ๊ตฌ์กฐ ๋ฐ์ดํ„ฐ) - null ์ œ์™ธ if (value !== null && typeof value === "object") continue; // ์ œ์™ธ ๊ทœ์น™ 6: ๋นˆ ๊ฐ’ if (value === undefined || value === "" || value === null) continue; // ์ œ์™ธ ๊ทœ์น™ 7: ์ด๋ฏธ commonFields์— ์žˆ๋Š” ํ•„๋“œ (๋ฒ”์šฉ ํผ ๋ชจ๋‹ฌ์—์„œ ๊ฐ€์ ธ์˜จ ํ•„๋“œ) if (fieldName in commonFields) continue; // ์œ„ ๊ทœ์น™์— ํ•ด๋‹นํ•˜์ง€ ์•Š๋Š” ๋‹จ์ˆœ ๊ฐ’(๋ฌธ์ž์—ด, ์ˆซ์ž, ๋‚ ์งœ ๋“ฑ)์€ ๊ณตํ†ต ํ•„๋“œ๋กœ ์ „๋‹ฌ commonFields[fieldName] = value; } // ๐Ÿ†• ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ์กฐ์ธ: ๋ฉ”์ธ ํ…Œ์ด๋ธ”์˜ ์กฐ์ธ ์ปฌ๋Ÿผ ๊ฐ’์„ commonFields์— ์ถ”๊ฐ€ if (joinRelationship) { const mainColumnValue = context.formData[joinRelationship.mainColumn]; if (mainColumnValue !== undefined && mainColumnValue !== null && mainColumnValue !== "") { // ๋ฆฌํ”ผํ„ฐ ํ…Œ์ด๋ธ”์˜ ์กฐ์ธ ์ปฌ๋Ÿผ์— ๋ฉ”์ธ ํ…Œ์ด๋ธ”์˜ ๊ฐ’ ์ฃผ์ž… commonFields[joinRelationship.detailColumn] = mainColumnValue; } else { console.warn(`โš ๏ธ [handleSave] ์กฐ์ธ ์ปฌ๋Ÿผ ๊ฐ’์ด ์—†์Œ: ${joinRelationship.mainColumn}`); } } for (const item of parsedData) { // ๋ฉ”ํƒ€ ํ•„๋“œ ์ œ๊ฑฐ (eslint ๊ฒฝ๊ณ  ๋ฌด์‹œ - ์˜๋„์ ์œผ๋กœ ๋ถ„๋ฆฌ) const { _targetTable: _, _isNewItem, _existingRecord: __, _originalItemIds: ___, _deletedItemIds: ____, ...dataToSave } = item; // ๐Ÿ†• ๋นˆ id ํ•„๋“œ ์ œ๊ฑฐ (์ƒˆ ํ•ญ๋ชฉ์ธ ๊ฒฝ์šฐ) if (!dataToSave.id || dataToSave.id === "" || dataToSave.id === null) { delete dataToSave.id; } // ๐Ÿ†• ๊ณตํ†ต ํ•„๋“œ ๋ณ‘ํ•ฉ + ์‚ฌ์šฉ์ž ์ •๋ณด ์ถ”๊ฐ€ // ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ๋ฐ์ดํ„ฐ๋ฅผ ๋จผ์ € ๋„ฃ๊ณ , ๊ณตํ†ต ํ•„๋“œ๋กœ ๋ฎ์–ด์”€ (๊ณตํ†ต ํ•„๋“œ๊ฐ€ ์šฐ์„ ) // ์ด์œ : ์‚ฌ์šฉ์ž๊ฐ€ ๊ณตํ†ต ํ•„๋“œ(์ถœ๊ณ ์ƒํƒœ ๋“ฑ)๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉด ๋ชจ๋“  ํ•ญ๋ชฉ์— ์ ์šฉ๋˜์–ด์•ผ ํ•จ const dataWithMeta: Record = { ...dataToSave, // RepeaterFieldGroup์˜ ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ๋ฐ์ดํ„ฐ ...commonFields, // ๋ฒ”์šฉ ํผ ๋ชจ๋‹ฌ์˜ ๊ณตํ†ต ํ•„๋“œ (outbound_status ๋“ฑ) - ๊ณตํ†ต ํ•„๋“œ๊ฐ€ ์šฐ์„ ! created_by: context.userId, updated_by: context.userId, company_code: context.companyCode, }; try { // ๐Ÿ†• ์ƒˆ ํ•ญ๋ชฉ ํŒ๋‹จ: _isNewItem ํ”Œ๋ž˜๊ทธ ๋˜๋Š” id๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋นˆ ๋ฌธ์ž์—ด์ธ ๊ฒฝ์šฐ const isNewRecord = _isNewItem || !item.id || item.id === "" || item.id === undefined; if (isNewRecord) { // INSERT (์ƒˆ ํ•ญ๋ชฉ) // id ํ•„๋“œ ์™„์ „ํžˆ ์ œ๊ฑฐ (์ž๋™ ์ƒ์„ฑ๋˜๋„๋ก) delete dataWithMeta.id; // ๋นˆ ๋ฌธ์ž์—ด id๋„ ์ œ๊ฑฐ if ("id" in dataWithMeta && (dataWithMeta.id === "" || dataWithMeta.id === null)) { delete dataWithMeta.id; } const insertResult = await apiClient.post( `/table-management/tables/${repeaterTargetTable}/add`, dataWithMeta, ); // ๋ฌด์‹œ๋œ ์ปฌ๋Ÿผ์ด ์žˆ์œผ๋ฉด ๊ฒฝ๊ณ  ์ถœ๋ ฅ if (insertResult.data?.data?.skippedColumns?.length > 0) { console.warn( `โš ๏ธ [${repeaterTargetTable}] ํ…Œ์ด๋ธ”์— ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ปฌ๋Ÿผ์ด ๋ฌด์‹œ๋จ:`, insertResult.data.data.skippedColumns, ); } } else if (item.id) { // UPDATE (๊ธฐ์กด ํ•ญ๋ชฉ) const originalData = { id: item.id }; const updatedData = { ...dataWithMeta, id: item.id }; const updateResult = await apiClient.put(`/table-management/tables/${repeaterTargetTable}/edit`, { originalData, updatedData, }); } } catch (err) { const error = err as { response?: { data?: unknown; status?: number }; message?: string }; console.error(`โŒ [handleSave] RepeaterFieldGroup ์ €์žฅ ์‹คํŒจ (${repeaterTargetTable}):`, { status: error.response?.status, data: error.response?.data, message: error.message, fullError: JSON.stringify(error.response?.data, null, 2), }); } } } // ๐Ÿ†• v3.9: RepeatScreenModal์˜ ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ €์žฅ ์ฒ˜๋ฆฌ const repeatScreenModalKeys = Object.keys(context.formData).filter( (key) => key.startsWith("_repeatScreenModal_") && key !== "_repeatScreenModal_aggregations", ); // RepeatScreenModal ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ํ…Œ์ด๋ธ”์— ๋Œ€ํ•œ ๋ฉ”์ธ ์ €์žฅ์€ ๊ฑด๋„ˆ๋œ€ const repeatScreenModalTables = repeatScreenModalKeys.map((key) => key.replace("_repeatScreenModal_", "")); // ๐Ÿ†• RepeaterFieldGroup ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์ˆ˜์ง‘ (๋ฉ”์ธ ์ €์žฅ ๊ฑด๋„ˆ๋›ฐ๊ธฐ ํŒ๋‹จ์šฉ) const repeaterFieldGroupTables: string[] = []; for (const [, fieldValue] of Object.entries(context.formData)) { let parsedData = fieldValue; if (typeof fieldValue === "string" && fieldValue.startsWith("[")) { try { parsedData = JSON.parse(fieldValue); } catch { continue; } } if (Array.isArray(parsedData) && parsedData.length > 0 && parsedData[0]?._targetTable) { repeaterFieldGroupTables.push(parsedData[0]._targetTable); } } // ๐Ÿ†• V2Repeater ์ „์—ญ ๋“ฑ๋ก ํ™•์ธ // @ts-ignore - window์— ๋™์  ์†์„ฑ ์‚ฌ์šฉ const v2RepeaterTables = Array.from(window.__v2RepeaterInstances || []); // ๋ฉ”์ธ ์ €์žฅ ๊ฑด๋„ˆ๋›ฐ๊ธฐ ์กฐ๊ฑด: // 1. RepeatScreenModal ๋˜๋Š” RepeaterFieldGroup์—์„œ ๊ฐ™์€ ํ…Œ์ด๋ธ” ์ฒ˜๋ฆฌ // 2. V2Repeater๊ฐ€ ๊ฐ™์€ ํ…Œ์ด๋ธ”์— ์กด์žฌ (๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ์— ๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ๋˜์–ด ์ €์žฅ๋จ) const shouldSkipMainSave = repeatScreenModalTables.includes(tableName) || repeaterFieldGroupTables.includes(tableName) || v2RepeaterTables.includes(tableName); if (shouldSkipMainSave) { saveResult = { success: true, message: "RepeaterFieldGroup/RepeatScreenModal/V2Repeater์—์„œ ์ฒ˜๋ฆฌ" }; } else { saveResult = await DynamicFormApi.saveFormData({ screenId, tableName, data: dataWithUserInfo, }); } if (repeatScreenModalKeys.length > 0) { // ๐Ÿ†• formData์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™์œผ๋กœ ์ƒ์„ฑ๋œ ๊ฐ’๋“ค ์ถ”์ถœ (์˜ˆ: shipment_plan_no) const numberingFields: Record = {}; for (const [fieldKey, value] of Object.entries(context.formData)) { // _numberingRuleId๋กœ ๋๋‚˜๋Š” ํ‚ค๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ํ•„๋“œ๋Š” ์ฑ„๋ฒˆ ๊ทœ์น™ ๊ฐ’ if (context.formData[`${fieldKey}_numberingRuleId`]) { numberingFields[fieldKey] = value; } } for (const key of repeatScreenModalKeys) { const targetTable = key.replace("_repeatScreenModal_", ""); const rows = context.formData[key] as any[]; if (!Array.isArray(rows) || rows.length === 0) continue; // ๐Ÿ†• ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ์กฐ์ธ ๊ด€๊ณ„ ์กฐํšŒ (๋ฉ”์ธ ํ…Œ์ด๋ธ” โ†’ RepeatScreenModal ํ…Œ์ด๋ธ”) let joinRelationship: { mainColumn: string; detailColumn: string } | null = null; if (tableName && targetTable && tableName !== targetTable) { const cacheKey = `${tableName}:${targetTable}`; if (cacheKey in joinRelationshipCache) { joinRelationship = joinRelationshipCache[cacheKey]; } else { try { const joinResponse = await apiClient.get( `/button-dataflow/join-relationship/${tableName}/${targetTable}`, ); if (joinResponse.data?.success && joinResponse.data?.data?.found) { joinRelationship = { mainColumn: joinResponse.data.data.mainColumn, detailColumn: joinResponse.data.data.detailColumn, }; } } catch (joinError) { console.warn("โš ๏ธ [handleSave] RepeatScreenModal ์กฐ์ธ ๊ด€๊ณ„ ์กฐํšŒ ์‹คํŒจ:", joinError); } joinRelationshipCache[cacheKey] = joinRelationship; } } // ๐Ÿ†• ์กฐ์ธ ์ปฌ๋Ÿผ ๊ฐ’ ์ค€๋น„ (๋ฉ”์ธ ํ…Œ์ด๋ธ”์—์„œ ๊ฐ€์ ธ์˜ด) const joinColumnData: Record = {}; if (joinRelationship) { const mainColumnValue = context.formData[joinRelationship.mainColumn]; if (mainColumnValue !== undefined && mainColumnValue !== null && mainColumnValue !== "") { joinColumnData[joinRelationship.detailColumn] = mainColumnValue; } } for (const row of rows) { const { _isNew, _targetTable, id, ...dataToSave } = row; // ์‚ฌ์šฉ์ž ์ •๋ณด ์ถ”๊ฐ€ + ์ฑ„๋ฒˆ ๊ทœ์น™ ๊ฐ’ ๋ณ‘ํ•ฉ + ์กฐ์ธ ์ปฌ๋Ÿผ ๊ฐ’ ์ถ”๊ฐ€ const dataWithMeta = { ...dataToSave, ...numberingFields, // ์ฑ„๋ฒˆ ๊ทœ์น™ ๊ฐ’ (shipment_plan_no ๋“ฑ) ...joinColumnData, // ๐Ÿ†• ์กฐ์ธ ์ปฌ๋Ÿผ ๊ฐ’ (๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ๊ด€๊ณ„) created_by: context.userId, updated_by: context.userId, company_code: context.companyCode, }; try { if (_isNew) { // INSERT const insertResult = await apiClient.post( `/table-management/tables/${targetTable}/add`, dataWithMeta, ); } else if (id) { // UPDATE const originalData = { id }; const updatedData = { ...dataWithMeta, id }; const updateResult = await apiClient.put(`/table-management/tables/${targetTable}/edit`, { originalData, updatedData, }); } } catch (error: any) { console.error(`โŒ [handleSave] ${targetTable} ์ €์žฅ ์‹คํŒจ:`, error.response?.data || error.message); // ๊ฐœ๋ณ„ ์‹คํŒจ๋Š” ์ „์ฒด ์ €์žฅ์„ ์ค‘๋‹จํ•˜์ง€ ์•Š์Œ } } } } // ๐Ÿ†• v2-repeat-container ๋ฐ์ดํ„ฐ ์ €์žฅ ์ฒ˜๋ฆฌ (_repeatContainerTables์— ๊ทธ๋ฃนํ™”๋œ ๋ฐ์ดํ„ฐ) const repeatContainerTables = context.formData._repeatContainerTables as Record | undefined; if (repeatContainerTables && Object.keys(repeatContainerTables).length > 0) { for (const [targetTable, rows] of Object.entries(repeatContainerTables)) { if (!Array.isArray(rows) || rows.length === 0) continue; // ๐Ÿ†• ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ์กฐ์ธ ๊ด€๊ณ„ ์กฐํšŒ let joinRelationship: { mainColumn: string; detailColumn: string } | null = null; if (tableName && targetTable && tableName !== targetTable) { const cacheKey = `${tableName}:${targetTable}`; if (cacheKey in joinRelationshipCache) { joinRelationship = joinRelationshipCache[cacheKey]; } else { try { const joinResponse = await apiClient.get( `/button-dataflow/join-relationship/${tableName}/${targetTable}`, ); if (joinResponse.data?.success && joinResponse.data?.data?.found) { joinRelationship = { mainColumn: joinResponse.data.data.mainColumn, detailColumn: joinResponse.data.data.detailColumn, }; } } catch (joinError) { console.warn("โš ๏ธ [handleSave] RepeatContainer ์กฐ์ธ ๊ด€๊ณ„ ์กฐํšŒ ์‹คํŒจ:", joinError); } joinRelationshipCache[cacheKey] = joinRelationship; } } // ์กฐ์ธ ์ปฌ๋Ÿผ ๊ฐ’ ์ค€๋น„ const joinColumnData: Record = {}; if (joinRelationship) { const mainColumnValue = context.formData[joinRelationship.mainColumn]; if (mainColumnValue !== undefined && mainColumnValue !== null && mainColumnValue !== "") { joinColumnData[joinRelationship.detailColumn] = mainColumnValue; } } for (const row of rows) { const { _isDirty, _sectionIndex, _targetTable, id, ...dataToSave } = row; // ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์€ ํ–‰์€ ๊ฑด๋„ˆ๋›ฐ๊ธฐ if (_isDirty === false) { continue; } // ์‚ฌ์šฉ์ž ์ •๋ณด ์ถ”๊ฐ€ + ์กฐ์ธ ์ปฌ๋Ÿผ ๊ฐ’ ์ถ”๊ฐ€ const dataWithMeta = { ...dataToSave, ...joinColumnData, // ์กฐ์ธ ์ปฌ๋Ÿผ ๊ฐ’ created_by: context.userId, updated_by: context.userId, company_code: context.companyCode, }; try { if (!id) { // INSERT (id๊ฐ€ ์—†์œผ๋ฉด ์ƒˆ ๋ ˆ์ฝ”๋“œ) const insertResult = await apiClient.post( `/table-management/tables/${targetTable}/add`, dataWithMeta, ); } else { // UPDATE (id๊ฐ€ ์žˆ์œผ๋ฉด ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ) const originalData = { id }; const updatedData = { ...dataWithMeta, id }; const updateResult = await apiClient.put(`/table-management/tables/${targetTable}/edit`, { originalData, updatedData, }); } } catch (error: any) { console.error( `โŒ [handleSave] ${targetTable} ์ €์žฅ ์‹คํŒจ (RepeatContainer):`, error.response?.data || error.message, ); } } } } // ๐Ÿ†• v3.9: RepeatScreenModal ์ง‘๊ณ„ ์ €์žฅ ์ฒ˜๋ฆฌ const aggregationConfigs = context.formData._repeatScreenModal_aggregations as Array<{ resultField: string; aggregatedValue: number; targetTable: string; targetColumn: string; joinKey: { sourceField: string; targetField: string }; sourceValue: any; }>; if (aggregationConfigs && aggregationConfigs.length > 0) { for (const config of aggregationConfigs) { const { targetTable, targetColumn, joinKey, aggregatedValue, sourceValue } = config; try { const originalData = { [joinKey.targetField]: sourceValue }; const updatedData = { [targetColumn]: aggregatedValue, [joinKey.targetField]: sourceValue, }; const updateResult = await apiClient.put(`/table-management/tables/${targetTable}/edit`, { originalData, updatedData, }); } catch (error: any) { console.error(`โŒ [handleSave] ${targetTable} ์ง‘๊ณ„ ์ €์žฅ ์‹คํŒจ:`, error.response?.data || error.message); } } } } if (!saveResult.success) { throw new Error(saveResult.message || "์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } // ๐Ÿ”ฅ ์ €์žฅ ์„ฑ๊ณต ํ›„ ์—ฐ๊ฒฐ๋œ ์ œ์–ด ์‹คํ–‰ (dataflowTiming์ด 'after'์ธ ๊ฒฝ์šฐ) if (config.enableDataflowControl && config.dataflowConfig) { // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ (comp_๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ•„๋“œ์— JSON ๋ฐฐ์—ด์ด ์žˆ๋Š” ๊ฒฝ์šฐ) // ์ž…๊ณ  ํ™”๋ฉด ๋“ฑ์—์„œ ํ’ˆ๋ชฉ ๋ชฉ๋ก์ด comp_xxx ํ•„๋“œ์— JSON ๋ฌธ์ž์—ด๋กœ ์ €์žฅ๋จ // ๐Ÿ”ง ์ˆ˜์ •: saveResult.data๊ฐ€ 3๋‹จ๊ณ„๋กœ ์ค‘์ฒฉ๋œ ๊ฒฝ์šฐ ์‹ค์ œ ํผ ๋ฐ์ดํ„ฐ ์ถ”์ถœ // saveResult.data = API ์‘๋‹ต { success, data, message } // saveResult.data.data = ์ €์žฅ๋œ ๋ ˆ์ฝ”๋“œ { id, screenId, tableName, data, createdAt... } // saveResult.data.data.data = ์‹ค์ œ ํผ ๋ฐ์ดํ„ฐ { sabun, user_name... } const savedRecord = saveResult?.data?.data || saveResult?.data || {}; const actualFormData = savedRecord?.data || savedRecord; const formData: Record = ( Object.keys(actualFormData).length > 0 ? actualFormData : context.formData || {} ) as Record; console.log("๐Ÿ“ฆ [executeAfterSaveControl] savedRecord ๊ตฌ์กฐ:", Object.keys(savedRecord)); console.log("๐Ÿ“ฆ [executeAfterSaveControl] actualFormData ์ถ”์ถœ:", Object.keys(formData)); console.log("๐Ÿ“ฆ [executeAfterSaveControl] formData.sabun:", formData.sabun); let parsedSectionData: any[] = []; // comp_๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ•„๋“œ์—์„œ ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ์ฐพ๊ธฐ const compFieldKey = Object.keys(formData).find( (key) => key.startsWith("comp_") && typeof formData[key] === "string", ); if (compFieldKey) { try { const sectionData = JSON.parse(formData[compFieldKey]); if (Array.isArray(sectionData) && sectionData.length > 0) { // ๊ณตํ†ต ํ•„๋“œ์™€ ์„น์…˜ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ parsedSectionData = sectionData.map((item: any) => { // ์„น์…˜ ๋ฐ์ดํ„ฐ์—์„œ ๋ถˆํ•„์š”ํ•œ ๋‚ด๋ถ€ ํ•„๋“œ ์ œ๊ฑฐ const { _isNewItem, _targetTable, _existingRecord, ...cleanItem } = item; // ๊ณตํ†ต ํ•„๋“œ(comp_ ํ•„๋“œ ์ œ์™ธ) + ์„น์…˜ ์•„์ดํ…œ ๋ณ‘ํ•ฉ const commonFields: Record = {}; Object.keys(formData).forEach((key) => { if (!key.startsWith("comp_") && !key.endsWith("_numberingRuleId")) { commonFields[key] = formData[key]; } }); return { ...commonFields, ...cleanItem }; }); } } catch (parseError) { console.warn("โš ๏ธ [handleSave] ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์‹คํŒจ:", parseError); } } // ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ context์— ์ถ”๊ฐ€ํ•˜์—ฌ ํ”Œ๋กœ์šฐ์— ์ „๋‹ฌ const contextWithSavedData = { ...context, savedData: formData, // ํŒŒ์‹ฑ๋œ ์„น์…˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด selectedRowsData๋กœ ์ „๋‹ฌ selectedRowsData: parsedSectionData.length > 0 ? parsedSectionData : context.selectedRowsData, }; await this.executeAfterSaveControl(config, contextWithSavedData); } } else { throw new Error("์ €์žฅ์— ํ•„์š”ํ•œ ์ •๋ณด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. (ํ…Œ์ด๋ธ”๋ช… ๋˜๋Š” ํ™”๋ฉดID ๋ˆ„๋ฝ)"); } // ํ…Œ์ด๋ธ”๊ณผ ํ”Œ๋กœ์šฐ ์ƒˆ๋กœ๊ณ ์นจ (๋ชจ๋‹ฌ ๋‹ซ๊ธฐ ์ „์— ์‹คํ–‰) context.onRefresh?.(); context.onFlowRefresh?.(); // ์ €์žฅ ์„ฑ๊ณต ํ›„ ์ด๋ฒคํŠธ ๋ฐœ์ƒ window.dispatchEvent(new CustomEvent("closeEditModal")); // EditModal ๋‹ซ๊ธฐ window.dispatchEvent(new CustomEvent("saveSuccessInModal")); // ScreenModal ์—ฐ์† ๋“ฑ๋ก ๋ชจ๋“œ ์ฒ˜๋ฆฌ // V2Repeater ์ €์žฅ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ + ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ ์ €์žฅ) // ๐Ÿ”ง formData๋ฅผ ๋ฆฌํ”ผํ„ฐ์— ์ „๋‹ฌํ•˜์—ฌ ๊ฐ ํ–‰์— ๋ณ‘ํ•ฉ ์ €์žฅ const savedId = saveResult?.data?.id || saveResult?.data?.data?.id || formData.id || context.formData?.id; // ๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ (์‚ฌ์šฉ์ž ์ •๋ณด ํฌํ•จ) const mainFormData = { ...formData, writer: formData.writer || context.userId, created_by: context.userId, updated_by: context.userId, company_code: formData.company_code || context.companyCode, }; // _numberingRuleId ๋“ฑ ๋ฉ”ํƒ€ ํ•„๋“œ ์ œ๊ฑฐ for (const key of Object.keys(mainFormData)) { if (key.endsWith("_numberingRuleId") || key.startsWith("_")) { delete mainFormData[key]; } } window.dispatchEvent( new CustomEvent("repeaterSave", { detail: { parentId: savedId, tableName: context.tableName, mainFormData, // ๐Ÿ†• ๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ masterRecordId: savedId, // ๐Ÿ†• ๋งˆ์Šคํ„ฐ ๋ ˆ์ฝ”๋“œ ID (FK ์ž๋™ ์—ฐ๊ฒฐ์šฉ) }, }), ); return true; } catch (error) { console.error("์ €์žฅ ์˜ค๋ฅ˜:", error); throw error; // ์—๋Ÿฌ๋ฅผ ๋‹ค์‹œ ๋˜์ ธ์„œ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ•จ } } /** * DB์—์„œ ์กฐํšŒํ•œ ์‹ค์ œ ๊ธฐ๋ณธํ‚ค๋กœ formData์—์„œ ๊ฐ’ ์ถ”์ถœ * @param formData ํผ ๋ฐ์ดํ„ฐ * @param primaryKeys DB์—์„œ ์กฐํšŒํ•œ ์‹ค์ œ ๊ธฐ๋ณธํ‚ค ์ปฌ๋Ÿผ๋ช… ๋ฐฐ์—ด * @returns ๊ธฐ๋ณธํ‚ค ๊ฐ’ (๋ณตํ•ฉํ‚ค์˜ ๊ฒฝ์šฐ ์ฒซ ๋ฒˆ์งธ ํ‚ค ๊ฐ’) */ private static extractPrimaryKeyValueFromDB(formData: Record, primaryKeys: string[]): any { if (!primaryKeys || primaryKeys.length === 0) { return null; } // ์ฒซ ๋ฒˆ์งธ ๊ธฐ๋ณธํ‚ค ์ปฌ๋Ÿผ์˜ ๊ฐ’์„ ์‚ฌ์šฉ (๋ณตํ•ฉํ‚ค์˜ ๊ฒฝ์šฐ) const primaryKeyColumn = primaryKeys[0]; if (formData.hasOwnProperty(primaryKeyColumn)) { const value = formData[primaryKeyColumn]; // ๋ณตํ•ฉํ‚ค์ธ ๊ฒฝ์šฐ ๋กœ๊ทธ ์ถœ๋ ฅ if (primaryKeys.length > 1) { console.log(`๐Ÿ“ ์ฒซ ๋ฒˆ์งธ ํ‚ค (${primaryKeyColumn}) ๊ฐ’์„ ์‚ฌ์šฉ: ${value}`); } return value; } // ๊ธฐ๋ณธํ‚ค ์ปฌ๋Ÿผ์ด formData์— ์—†๋Š” ๊ฒฝ์šฐ return null; } /** * @deprecated DB ๊ธฐ๋ฐ˜ ์กฐํšŒ๋กœ ๋Œ€์ฒด๋จ. extractPrimaryKeyValueFromDB ์‚ฌ์šฉ ๊ถŒ์žฅ * formData์—์„œ ๊ธฐ๋ณธ ํ‚ค๊ฐ’ ์ถ”์ถœ (์ถ”์ธก ๊ธฐ๋ฐ˜) */ private static extractPrimaryKeyValue(formData: Record): any { // ์ผ๋ฐ˜์ ์ธ ๊ธฐ๋ณธ ํ‚ค ํ•„๋“œ๋ช…๋“ค (์šฐ์„ ์ˆœ์œ„ ์ˆœ) const commonPrimaryKeys = [ "id", "ID", // ๊ฐ€์žฅ ์ผ๋ฐ˜์  "objid", "OBJID", // ์ด ํ”„๋กœ์ ํŠธ์—์„œ ์ž์ฃผ ์‚ฌ์šฉ "pk", "PK", // Primary Key ์ค„์ž„๋ง "_id", // MongoDB ์Šคํƒ€์ผ "uuid", "UUID", // UUID ๋ฐฉ์‹ "key", "KEY", // ๊ธฐํƒ€ ]; // ์šฐ์„ ์ˆœ์œ„์— ๋”ฐ๋ผ ๊ธฐ๋ณธ ํ‚ค๊ฐ’ ์ฐพ๊ธฐ for (const keyName of commonPrimaryKeys) { if (formData.hasOwnProperty(keyName)) { const value = formData[keyName]; return value; } } // ๊ธฐ๋ณธ ํ‚ค๋ฅผ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ return null; } /** * ์ œ์ถœ ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static async handleSubmit(config: ButtonActionConfig, context: ButtonActionContext): Promise { // ์ œ์ถœ์€ ์ €์žฅ๊ณผ ์œ ์‚ฌํ•˜์ง€๋งŒ ์ถ”๊ฐ€์ ์ธ ์ฒ˜๋ฆฌ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Œ return await this.handleSave(config, context); } /** * ๐Ÿ†• ๋ ‰ ๊ตฌ์กฐ ์ปดํฌ๋„ŒํŠธ ์ผ๊ด„ ์ €์žฅ ์ฒ˜๋ฆฌ * ๋ฏธ๋ฆฌ๋ณด๊ธฐ์—์„œ ์ƒ์„ฑ๋œ ์œ„์น˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ผ๊ด„ INSERT */ private static async handleRackStructureBatchSave( config: ButtonActionConfig, context: ButtonActionContext, locations: any[], rackStructureFieldKey: string = "_rackStructureLocations", ): Promise { const { tableName, screenId, userId, companyCode } = context; if (!tableName) { throw new Error("ํ…Œ์ด๋ธ”๋ช…์ด ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); } if (locations.length === 0) { throw new Error("์ €์žฅํ•  ์œ„์น˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋จผ์ € ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”."); } // ์ €์žฅ ์ „ ์ค‘๋ณต ์ฒดํฌ const firstLocation = locations[0]; const warehouseCode = firstLocation.warehouse_code || firstLocation.warehouse_id || firstLocation.warehouseCode; const floor = firstLocation.floor; const zone = firstLocation.zone; if (warehouseCode && floor && zone) { try { // search ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐฑ์—”๋“œ์—์„œ ํ•„ํ„ฐ๋ง (filters๋Š” ๋ฐฑ์—”๋“œ์—์„œ ์ฒ˜๋ฆฌ ์•ˆ๋จ) const existingResponse = await DynamicFormApi.getTableData(tableName, { search: { warehouse_code: { value: warehouseCode, operator: "equals" }, floor: { value: floor, operator: "equals" }, zone: { value: zone, operator: "equals" }, }, page: 1, pageSize: 1000, }); // API ์‘๋‹ต ๊ตฌ์กฐ์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ์ถ”์ถœ const responseData = existingResponse.data as any; const existingData = responseData?.data || responseData || []; if (Array.isArray(existingData) && existingData.length > 0) { // ์ค‘๋ณต๋˜๋Š” ์œ„์น˜ ํ™•์ธ const existingSet = new Set(existingData.map((loc: any) => `${loc.row_num}-${loc.level_num}`)); const duplicates = locations.filter((loc) => { const key = `${loc.row_num || loc.rowNum}-${loc.level_num || loc.levelNum}`; return existingSet.has(key); }); if (duplicates.length > 0) { const duplicateInfo = duplicates .slice(0, 5) .map((d) => `${d.row_num || d.rowNum}์—ด ${d.level_num || d.levelNum}๋‹จ`) .join(", "); const moreCount = duplicates.length > 5 ? ` ์™ธ ${duplicates.length - 5}๊ฐœ` : ""; alert( `์ด๋ฏธ ๋“ฑ๋ก๋œ ์œ„์น˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค!\n\n์ค‘๋ณต ์œ„์น˜: ${duplicateInfo}${moreCount}\n\nํ•ด๋‹น ์œ„์น˜๋ฅผ ์ œ์™ธํ•˜๊ฑฐ๋‚˜ ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•ด์ฃผ์„ธ์š”.`, ); return false; } } } catch (checkError) { console.warn("โš ๏ธ [handleRackStructureBatchSave] ์ค‘๋ณต ์ฒดํฌ ์‹คํŒจ (์ €์žฅ ๊ณ„์† ์ง„ํ–‰):", checkError); } } // ๊ฐ ์œ„์น˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์ €์žฅ (๋ ‰ ๊ตฌ์กฐ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ด๋ฏธ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ๋ช…์œผ๋กœ ์ƒ์„ฑ๋จ) const recordsToInsert = locations.map((loc) => { // ๋ ‰ ๊ตฌ์กฐ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ƒ์„ฑ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ // ์ƒˆ๋กœ์šด ํ˜•์‹(์Šค๋„ค์ดํฌ ์ผ€์ด์Šค)๊ณผ ๊ธฐ์กด ํ˜•์‹(์นด๋ฉœ ์ผ€์ด์Šค) ๋ชจ๋‘ ์ง€์› const record: Record = { // ๋ ‰ ๊ตฌ์กฐ์—์„œ ์ƒ์„ฑ๋œ ํ•„๋“œ (์ด๋ฏธ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ๋ช…๊ณผ ๋™์ผ) location_code: loc.location_code || loc.locationCode, location_name: loc.location_name || loc.locationName, row_num: loc.row_num || String(loc.rowNum), level_num: loc.level_num || String(loc.levelNum), // ์ฐฝ๊ณ  ์ •๋ณด (๋ ‰ ๊ตฌ์กฐ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ „๋‹ฌ) - DB ์ปฌ๋Ÿผ๋ช…์€ warehouse_code warehouse_code: loc.warehouse_code || loc.warehouse_id || loc.warehouseCode, warehouse_name: loc.warehouse_name || loc.warehouseName, // ์œ„์น˜ ์ •๋ณด (๋ ‰ ๊ตฌ์กฐ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ „๋‹ฌ) floor: loc.floor, zone: loc.zone, location_type: loc.location_type || loc.locationType, status: loc.status || "์‚ฌ์šฉ", // ์‚ฌ์šฉ์ž ์ •๋ณด ์ถ”๊ฐ€ writer: userId, company_code: companyCode, }; return record; }); // ์ผ๊ด„ INSERT ์‹คํ–‰ try { let successCount = 0; let errorCount = 0; const errors: string[] = []; for (let i = 0; i < recordsToInsert.length; i++) { const record = recordsToInsert[i]; try { const result = await DynamicFormApi.saveFormData({ screenId, tableName, data: record, }); if (result.success) { successCount++; } else { errorCount++; const errorMsg = result.message || result.error || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"; errors.push(errorMsg); console.error(`โŒ [handleRackStructureBatchSave] ์ €์žฅ ์‹คํŒจ (${i + 1}):`, errorMsg); } } catch (error: any) { errorCount++; const errorMsg = error.message || "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"; errors.push(errorMsg); console.error(`โŒ [handleRackStructureBatchSave] ์˜ˆ์™ธ ๋ฐœ์ƒ (${i + 1}):`, error); } } if (errorCount > 0) { if (successCount > 0) { alert(`${successCount}๊ฐœ ์ €์žฅ ์™„๋ฃŒ, ${errorCount}๊ฐœ ์ €์žฅ ์‹คํŒจ\n\n์˜ค๋ฅ˜: ${errors.slice(0, 3).join("\n")}`); } else { throw new Error(`์ €์žฅ ์‹คํŒจ: ${errors[0]}`); } } else { alert(`${successCount}๊ฐœ์˜ ์œ„์น˜๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); } // ์„ฑ๊ณต ํ›„ ์ƒˆ๋กœ๊ณ ์นจ if (context.onRefresh) { context.onRefresh(); } // ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ if (context.onClose) { context.onClose(); } return successCount > 0; } catch (error: any) { console.error("๐Ÿ—๏ธ [handleRackStructureBatchSave] ์ผ๊ด„ ์ €์žฅ ์˜ค๋ฅ˜:", error); throw error; } } /** * ๐Ÿ†• Universal Form Modal ํ…Œ์ด๋ธ” ์„น์…˜ ๋ณ‘ํ•ฉ ์ €์žฅ ์ฒ˜๋ฆฌ * ๋ฒ”์šฉ_ํผ_๋ชจ๋‹ฌ ๋‚ด๋ถ€์˜ ๊ณตํ†ต ํ•„๋“œ + _tableSection_ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ‘ํ•ฉํ•˜์—ฌ ํ’ˆ๋ชฉ๋ณ„๋กœ ์ €์žฅ * ์ˆ˜์ • ๋ชจ๋“œ: INSERT/UPDATE/DELETE ์ง€์› * ๐Ÿ†• ์„น์…˜๋ณ„ ์ €์žฅ ํ…Œ์ด๋ธ”(targetTable) ์ง€์› ์ถ”๊ฐ€ */ private static async handleUniversalFormModalTableSectionSave( config: ButtonActionConfig, context: ButtonActionContext, formData: Record, ): Promise<{ handled: boolean; success: boolean }> { const { tableName, screenId } = context; // ๋ฒ”์šฉ_ํผ_๋ชจ๋‹ฌ ํ‚ค ์ฐพ๊ธฐ (์ปฌ๋Ÿผ๋ช…์— ๋”ฐ๋ผ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ) const universalFormModalKey = Object.keys(formData).find((key) => { const value = formData[key]; if (!value || typeof value !== "object" || Array.isArray(value)) return false; // _tableSection_ ํ‚ค๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ return Object.keys(value).some((k) => k.startsWith("_tableSection_")); }); if (!universalFormModalKey) { return { handled: false, success: false }; } const modalData = formData[universalFormModalKey]; // ๐Ÿ†• universal-form-modal ์ปดํฌ๋„ŒํŠธ ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ // 1. componentConfigs์—์„œ ์ปดํฌ๋„ŒํŠธ ID๋กœ ์ฐพ๊ธฐ // 2. allComponents์—์„œ columnName์œผ๋กœ ์ฐพ๊ธฐ // 3. ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ API์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ let modalComponentConfig = context.componentConfigs?.[universalFormModalKey]; // componentConfigs์—์„œ ์ง์ ‘ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ, allComponents์—์„œ columnName์œผ๋กœ ์ฐพ๊ธฐ if (!modalComponentConfig && context.allComponents) { const modalComponent = context.allComponents.find( (comp: any) => comp.columnName === universalFormModalKey || comp.properties?.columnName === universalFormModalKey, ); if (modalComponent) { modalComponentConfig = modalComponent.componentConfig || modalComponent.properties?.componentConfig; } } // ๐Ÿ†• ์•„์ง๋„ ์„ค์ •์„ ์ฐพ์ง€ ๋ชปํ–ˆ์œผ๋ฉด ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ API์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ if (!modalComponentConfig && screenId) { try { const { screenApi } = await import("@/lib/api/screen"); const layoutData = await screenApi.getLayout(screenId); if (layoutData && layoutData.components) { // ๋ ˆ์ด์•„์›ƒ์—์„œ universal-form-modal ์ปดํฌ๋„ŒํŠธ ์ฐพ๊ธฐ const modalLayout = (layoutData.components as any[]).find( (comp) => comp.properties?.columnName === universalFormModalKey || comp.columnName === universalFormModalKey, ); if (modalLayout) { modalComponentConfig = modalLayout.properties?.componentConfig || modalLayout.componentConfig; } } } catch (error) { console.warn("โš ๏ธ [handleUniversalFormModalTableSectionSave] ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ ์‹คํŒจ:", error); } } const sections: any[] = modalComponentConfig?.sections || []; const saveConfig = modalComponentConfig?.saveConfig || {}; // _tableSection_ ๋ฐ์ดํ„ฐ ์ถ”์ถœ const tableSectionData: Record = {}; const commonFieldsData: Record = {}; // ๐Ÿ†• ์›๋ณธ ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์ถ”์ถœ (์ˆ˜์ • ๋ชจ๋“œ์—์„œ UPDATE/DELETE ์ถ”์ ์šฉ) // modalData ๋‚ด๋ถ€ ๋˜๋Š” ์ตœ์ƒ์œ„ formData์—์„œ ์ฐพ์Œ const originalGroupedData: any[] = modalData._originalGroupedData || formData._originalGroupedData || []; for (const [key, value] of Object.entries(modalData)) { if (key.startsWith("_tableSection_")) { const sectionId = key.replace("_tableSection_", ""); tableSectionData[sectionId] = value as any[]; } else if (!key.startsWith("_")) { // _๋กœ ์‹œ์ž‘ํ•˜์ง€ ์•Š๋Š” ํ•„๋“œ๋Š” ๊ณตํ†ต ํ•„๋“œ๋กœ ์ฒ˜๋ฆฌ commonFieldsData[key] = value; } } // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๊ณ  ์›๋ณธ ๋ฐ์ดํ„ฐ๋„ ์—†์œผ๋ฉด ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Œ const hasTableSectionData = Object.values(tableSectionData).some((arr) => arr.length > 0); if (!hasTableSectionData && originalGroupedData.length === 0) { console.log("โš ๏ธ [handleUniversalFormModalTableSectionSave] ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ์—†์Œ - ์ผ๋ฐ˜ ์ €์žฅ์œผ๋กœ ์ „ํ™˜"); return { handled: false, success: false }; } // ๐ŸŽฏ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒ˜๋ฆฌ (์ €์žฅ ์‹œ์ ์— ์‹ค์ œ ์ˆœ๋ฒˆ ์ฆ๊ฐ€) // ๐Ÿ”ง ์ˆ˜์ • ๋ชจ๋“œ ์ฒดํฌ: formData.id ๋˜๋Š” originalGroupedData๊ฐ€ ์žˆ์œผ๋ฉด UPDATE ๋ชจ๋“œ const isEditModeUniversal = (formData.id !== undefined && formData.id !== null && formData.id !== "") || originalGroupedData.length > 0; const fieldsWithNumbering: Record = {}; // commonFieldsData์™€ modalData์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์„ค์ •๋œ ํ•„๋“œ ์ฐพ๊ธฐ for (const [key, value] of Object.entries(modalData)) { if (key.endsWith("_numberingRuleId") && value) { const fieldName = key.replace("_numberingRuleId", ""); fieldsWithNumbering[fieldName] = value as string; } } // formData์—์„œ๋„ ํ™•์ธ (๋ชจ๋‹ฌ ์™ธ๋ถ€์— ์žˆ์„ ์ˆ˜ ์žˆ์Œ) for (const [key, value] of Object.entries(formData)) { if (key.endsWith("_numberingRuleId") && value && !fieldsWithNumbering[key.replace("_numberingRuleId", "")]) { const fieldName = key.replace("_numberingRuleId", ""); fieldsWithNumbering[fieldName] = value as string; } } // ๐Ÿ”ง ์ˆ˜์ • ๋ชจ๋“œ์—์„œ๋Š” ์ฑ„๋ฒˆ ์ฝ”๋“œ ํ• ๋‹น ๊ฑด๋„ˆ๋›ฐ๊ธฐ (๊ธฐ์กด ๋ฒˆํ˜ธ ์œ ์ง€) // ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ์—์„œ๋งŒ allocateCode ํ˜ธ์ถœํ•˜์—ฌ ์ƒˆ ๋ฒˆํ˜ธ ํ• ๋‹น if (Object.keys(fieldsWithNumbering).length > 0 && !isEditModeUniversal) { const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) { try { // ๐Ÿ†• ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘ํ•œ ๊ฐ’์„ ์ „๋‹ฌ (์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ถ”์ถœ์šฉ) const userInputCode = commonFieldsData[fieldName] as string; const allocateResult = await allocateNumberingCode(ruleId, userInputCode, formData); if (allocateResult.success && allocateResult.data?.generatedCode) { const newCode = allocateResult.data.generatedCode; commonFieldsData[fieldName] = newCode; } else { console.warn( `โš ๏ธ [handleUniversalFormModalTableSectionSave] ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์‹คํŒจ, ๊ธฐ์กด ๊ฐ’ ์œ ์ง€:`, allocateResult.error, ); } } catch (allocateError) { console.error(`โŒ [handleUniversalFormModalTableSectionSave] ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์˜ค๋ฅ˜:`, allocateError); // ์˜ค๋ฅ˜ ์‹œ ๊ธฐ์กด ๊ฐ’ ์œ ์ง€ } } } else if (isEditModeUniversal) { } try { // ์‚ฌ์šฉ์ž ์ •๋ณด ์ถ”๊ฐ€ if (!context.userId) { throw new Error("์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”."); } const userInfo = { writer: context.userId, created_by: context.userId, updated_by: context.userId, company_code: context.companyCode || "", }; let insertedCount = 0; let updatedCount = 0; let deletedCount = 0; let mainRecordId: number | null = null; // ๐Ÿ†• ๋จผ์ € ๋ฉ”์ธ ํ…Œ์ด๋ธ”์— ๊ณตํ†ต ๋ฐ์ดํ„ฐ ์ €์žฅ (๋ณ„๋„ ํ…Œ์ด๋ธ”์ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ) const hasSeparateTargetTable = sections.some( (s) => s.type === "table" && s.tableConfig?.saveConfig?.targetTable && s.tableConfig.saveConfig.targetTable !== tableName, ); if (hasSeparateTargetTable && Object.keys(commonFieldsData).length > 0) { const mainRowToSave = { ...commonFieldsData, ...userInfo }; // ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ œ๊ฑฐ Object.keys(mainRowToSave).forEach((key) => { if (key.startsWith("_")) { delete mainRowToSave[key]; } }); // ๐Ÿ†• ๋ฉ”์ธ ํ…Œ์ด๋ธ” UPDATE/INSERT ํŒ๋‹จ // - formData.id๊ฐ€ ์žˆ์œผ๋ฉด ํŽธ์ง‘ ๋ชจ๋“œ โ†’ UPDATE // - formData.id๊ฐ€ ์—†์œผ๋ฉด ์‹ ๊ทœ ๋“ฑ๋ก โ†’ INSERT const existingMainId = formData.id; const isMainUpdate = existingMainId !== undefined && existingMainId !== null && existingMainId !== ""; let mainSaveResult: { success: boolean; data?: any; message?: string }; if (isMainUpdate) { // ๐Ÿ”„ ํŽธ์ง‘ ๋ชจ๋“œ: UPDATE ์‹คํ–‰ mainSaveResult = await DynamicFormApi.updateFormData(existingMainId, { tableName: tableName!, data: mainRowToSave, }); mainRecordId = existingMainId; } else { // โž• ์‹ ๊ทœ ๋“ฑ๋ก: INSERT ์‹คํ–‰ console.log("โž• [handleUniversalFormModalTableSectionSave] ๋ฉ”์ธ ํ…Œ์ด๋ธ” INSERT ์‹คํ–‰"); mainSaveResult = await DynamicFormApi.saveFormData({ screenId: screenId!, tableName: tableName!, data: mainRowToSave, }); mainRecordId = mainSaveResult.data?.id || null; } if (!mainSaveResult.success) { throw new Error(mainSaveResult.message || "๋ฉ”์ธ ๋ฐ์ดํ„ฐ ์ €์žฅ ์‹คํŒจ"); } } // ๊ฐ ํ…Œ์ด๋ธ” ์„น์…˜ ์ฒ˜๋ฆฌ for (const [sectionId, currentItems] of Object.entries(tableSectionData)) { // ๐Ÿ†• ํ•ด๋‹น ์„น์…˜์˜ ์„ค์ • ์ฐพ๊ธฐ const sectionConfig = sections.find((s) => s.id === sectionId); const targetTableName = sectionConfig?.tableConfig?.saveConfig?.targetTable; // ๐Ÿ†• ์‹ค์ œ ์ €์žฅํ•  ํ…Œ์ด๋ธ” ๊ฒฐ์ • // - targetTable์ด ์žˆ์œผ๋ฉด ํ•ด๋‹น ํ…Œ์ด๋ธ”์— ์ €์žฅ // - targetTable์ด ์—†์œผ๋ฉด ๋ฉ”์ธ ํ…Œ์ด๋ธ”์— ์ €์žฅ const saveTableName = targetTableName || tableName!; // 1๏ธโƒฃ ์‹ ๊ทœ ํ’ˆ๋ชฉ INSERT (id๊ฐ€ ์—†๋Š” ํ•ญ๋ชฉ) const newItems = currentItems.filter((item) => !item.id); for (const item of newItems) { const rowToSave = { ...commonFieldsData, ...item, ...userInfo }; // ๋‚ด๋ถ€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ œ๊ฑฐ Object.keys(rowToSave).forEach((key) => { if (key.startsWith("_")) { delete rowToSave[key]; } }); // ๐Ÿ†• ๋ฉ”์ธ ๋ ˆ์ฝ”๋“œ ID ์—ฐ๊ฒฐ (๋ณ„๋„ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•˜๋Š” ๊ฒฝ์šฐ) if (targetTableName && mainRecordId && saveConfig.primaryKeyColumn) { rowToSave[saveConfig.primaryKeyColumn] = mainRecordId; } console.log("โž• [INSERT] ์‹ ๊ทœ ํ’ˆ๋ชฉ:", { tableName: saveTableName, data: rowToSave }); const saveResult = await DynamicFormApi.saveFormData({ screenId: screenId!, tableName: saveTableName, data: rowToSave, }); if (!saveResult.success) { throw new Error(saveResult.message || "์‹ ๊ทœ ํ’ˆ๋ชฉ ์ €์žฅ ์‹คํŒจ"); } insertedCount++; } // 2๏ธโƒฃ ๊ธฐ์กด ํ’ˆ๋ชฉ UPDATE (id๊ฐ€ ์žˆ๋Š” ํ•ญ๋ชฉ, ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ๋งŒ) const existingItems = currentItems.filter((item) => item.id); for (const item of existingItems) { const originalItem = originalGroupedData.find((orig) => orig.id === item.id); if (!originalItem) { // ๐Ÿ†• ํด๋ฐฑ ๋กœ์ง: ์›๋ณธ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์–ด๋„ id๊ฐ€ ์žˆ์œผ๋ฉด UPDATE ์‹œ๋„ // originalGroupedData ์ „๋‹ฌ์ด ๋ˆ„๋ฝ๋œ ๊ฒฝ์šฐ๋ฅผ ์ฒ˜๋ฆฌ console.warn(`โš ๏ธ [UPDATE] ์›๋ณธ ๋ฐ์ดํ„ฐ ์—†์Œ - id๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ UPDATE ์‹œ๋„ (ํด๋ฐฑ): id=${item.id}`); // โš ๏ธ ์ค‘์š”: commonFieldsData๊ฐ€ item๋ณด๋‹ค ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์•„์•ผ ํ•จ // item์— ์žˆ๋Š” ๊ธฐ์กด ๊ฐ’(์˜ˆ: manager_id=123)์ด commonFieldsData์˜ ์ƒˆ ๊ฐ’(manager_id=234)์„ ๋ฎ์–ด์“ฐ์ง€ ์•Š๋„๋ก // ์ˆœ์„œ: item(๊ธฐ์กด) โ†’ commonFieldsData(์ƒˆ๋กœ ์ž…๋ ฅ) โ†’ userInfo(๋ฉ”ํƒ€๋ฐ์ดํ„ฐ) const rowToUpdate = { ...item, ...commonFieldsData, ...userInfo }; Object.keys(rowToUpdate).forEach((key) => { if (key.startsWith("_")) { delete rowToUpdate[key]; } }); // id๋ฅผ ์œ ์ง€ํ•˜๊ณ  UPDATE ์‹คํ–‰ const updateResult = await DynamicFormApi.updateFormData(item.id, { tableName: saveTableName, data: rowToUpdate, }); if (!updateResult.success) { throw new Error(updateResult.message || "ํ’ˆ๋ชฉ ์ˆ˜์ • ์‹คํŒจ"); } updatedCount++; continue; } // ๋ณ€๊ฒฝ ์‚ฌํ•ญ ํ™•์ธ (๊ณตํ†ต ํ•„๋“œ ํฌํ•จ) // โš ๏ธ ์ค‘์š”: commonFieldsData๊ฐ€ item๋ณด๋‹ค ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์•„์•ผ ํ•จ (์ƒˆ๋กœ ์ž…๋ ฅํ•œ ๊ฐ’์ด ๊ธฐ์กด ๊ฐ’์„ ๋ฎ์–ด์”€) const currentDataWithCommon = { ...item, ...commonFieldsData }; const hasChanges = this.checkForChanges(originalItem, currentDataWithCommon); if (hasChanges) { // ๋ณ€๊ฒฝ๋œ ํ•„๋“œ๋งŒ ์ถ”์ถœํ•˜์—ฌ ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ const updateResult = await DynamicFormApi.updateFormDataPartial( item.id, originalItem, currentDataWithCommon, saveTableName, ); if (!updateResult.success) { throw new Error(updateResult.message || "ํ’ˆ๋ชฉ ์ˆ˜์ • ์‹คํŒจ"); } updatedCount++; } else { } } // 3๏ธโƒฃ ์‚ญ์ œ๋œ ํ’ˆ๋ชฉ DELETE (์›๋ณธ์—๋Š” ์žˆ์ง€๋งŒ ํ˜„์žฌ์—๋Š” ์—†๋Š” ํ•ญ๋ชฉ) // ๐Ÿ†• ํ…Œ์ด๋ธ” ์„น์…˜๋ณ„ ์›๋ณธ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ (์šฐ์„ ), ์—†์œผ๋ฉด ์ „์—ญ originalGroupedData ์‚ฌ์šฉ const sectionOriginalKey = `_originalTableSectionData_${sectionId}`; const sectionOriginalData: any[] = modalData[sectionOriginalKey] || formData[sectionOriginalKey] || []; // ์„น์…˜๋ณ„ ์›๋ณธ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์ „์—ญ originalGroupedData ์‚ฌ์šฉ const originalDataForDelete = sectionOriginalData.length > 0 ? sectionOriginalData : originalGroupedData; // โš ๏ธ id ํƒ€์ž… ํ†ต์ผ: ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋น„๊ต (์ˆซ์ž vs ๋ฌธ์ž์—ด ๋ถˆ์ผ์น˜ ๋ฐฉ์ง€) const currentIds = new Set(currentItems.map((item) => String(item.id)).filter(Boolean)); const deletedItems = originalDataForDelete.filter((orig) => orig.id && !currentIds.has(String(orig.id))); for (const deletedItem of deletedItems) { // screenId ์ „๋‹ฌํ•˜์—ฌ ์ œ์–ด๊ด€๋ฆฌ ์‹คํ–‰ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•จ const deleteResult = await DynamicFormApi.deleteFormDataFromTable( deletedItem.id, saveTableName, context.screenId, ); if (!deleteResult.success) { throw new Error(deleteResult.message || "ํ’ˆ๋ชฉ ์‚ญ์ œ ์‹คํŒจ"); } deletedCount++; } } // ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ const resultParts: string[] = []; if (mainRecordId) resultParts.push("๋ฉ”์ธ ๋ฐ์ดํ„ฐ ์ €์žฅ"); if (insertedCount > 0) resultParts.push(`${insertedCount}๊ฐœ ์ถ”๊ฐ€`); if (updatedCount > 0) resultParts.push(`${updatedCount}๊ฐœ ์ˆ˜์ •`); if (deletedCount > 0) resultParts.push(`${deletedCount}๊ฐœ ์‚ญ์ œ`); const resultMessage = resultParts.length > 0 ? resultParts.join(", ") : "๋ณ€๊ฒฝ ์‚ฌํ•ญ ์—†์Œ"; toast.success(`์ €์žฅ ์™„๋ฃŒ: ${resultMessage}`); // ๐Ÿ†• ์ €์žฅ ์„ฑ๊ณต ํ›„ ์ œ์–ด ๊ด€๋ฆฌ ์‹คํ–‰ (๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ €์žฅ ์‹œ ์†Œ์Šค ํ…Œ์ด๋ธ”๊ณผ ์ผ์น˜ํ•˜๋Š” ์„น์…˜๋งŒ ์‹คํ–‰) if (config.enableDataflowControl && config.dataflowConfig?.flowConfig?.flowId) { const flowId = config.dataflowConfig.flowConfig.flowId; try { // ํ”Œ๋กœ์šฐ ์†Œ์Šค ํ…Œ์ด๋ธ” ์กฐํšŒ const { getFlowSourceTable } = await import("@/lib/api/nodeFlows"); const flowSourceInfo = await getFlowSourceTable(flowId); if (flowSourceInfo.sourceTable) { // ๊ฐ ์„น์…˜ ํ™•์ธํ•˜์—ฌ ์†Œ์Šค ํ…Œ์ด๋ธ”๊ณผ ์ผ์น˜ํ•˜๋Š” ์„น์…˜ ์ฐพ๊ธฐ let controlExecuted = false; for (const [sectionId, sectionItems] of Object.entries(tableSectionData)) { const sectionConfig = sections.find((s: any) => s.id === sectionId); const sectionTargetTable = sectionConfig?.tableConfig?.saveConfig?.targetTable || tableName; // ์†Œ์Šค ํ…Œ์ด๋ธ”๊ณผ ์ผ์น˜ํ•˜๋Š” ์„น์…˜๋งŒ ์ œ์–ด ์‹คํ–‰ if (sectionTargetTable === flowSourceInfo.sourceTable && sectionItems.length > 0) { // ๊ณตํ†ต ํ•„๋“œ + ํ•ด๋‹น ์„น์…˜ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉํ•˜์—ฌ sourceData ์ƒ์„ฑ const sourceData = sectionItems.map((item: any) => ({ ...commonFieldsData, ...item, })); // ์ œ์–ด ๊ด€๋ฆฌ์šฉ ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ const controlContext: ButtonActionContext = { ...context, selectedRowsData: sourceData, formData: commonFieldsData, }; // ์ œ์–ด ๊ด€๋ฆฌ ์‹คํ–‰ await this.executeAfterSaveControl(config, controlContext); controlExecuted = true; break; // ์ฒซ ๋ฒˆ์งธ ๋งค์นญ ์„น์…˜๋งŒ ์‹คํ–‰ } } // ๋งค์นญ๋˜๋Š” ์„น์…˜์ด ์—†์œผ๋ฉด ๋ฉ”์ธ ํ…Œ์ด๋ธ” ํ™•์ธ if (!controlExecuted && tableName === flowSourceInfo.sourceTable) { const controlContext: ButtonActionContext = { ...context, selectedRowsData: [commonFieldsData], formData: commonFieldsData, }; await this.executeAfterSaveControl(config, controlContext); } } else { console.log("โš ๏ธ [handleUniversalFormModalTableSectionSave] ํ”Œ๋กœ์šฐ ์†Œ์Šค ํ…Œ์ด๋ธ” ์—†์Œ - ์ œ์–ด ์Šคํ‚ต"); } } catch (controlError) { console.error("โŒ [handleUniversalFormModalTableSectionSave] ์ œ์–ด ๊ด€๋ฆฌ ์‹คํ–‰ ์˜ค๋ฅ˜:", controlError); // ์ œ์–ด ๊ด€๋ฆฌ ์‹คํŒจ๋Š” ์ €์žฅ ์„ฑ๊ณต์— ์˜ํ–ฅ์ฃผ์ง€ ์•Š์Œ } } // ์ €์žฅ ์„ฑ๊ณต ์ด๋ฒคํŠธ ๋ฐœ์ƒ window.dispatchEvent(new CustomEvent("saveSuccess")); window.dispatchEvent(new CustomEvent("refreshTable")); // EditModal ๋‹ซ๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์ƒ window.dispatchEvent(new CustomEvent("closeEditModal")); return { handled: true, success: true }; } catch (error: any) { console.error("โŒ [handleUniversalFormModalTableSectionSave] ์ €์žฅ ์˜ค๋ฅ˜:", error); toast.error(error.message || "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return { handled: true, success: false }; } } /** * ๋‘ ๊ฐ์ฒด ๊ฐ„ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ํ™•์ธ */ private static checkForChanges(original: Record, current: Record): boolean { // ๋น„๊ตํ•  ํ•„๋“œ ๋ชฉ๋ก (๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ œ์™ธ) const fieldsToCompare = new Set([ ...Object.keys(original).filter((k) => !k.startsWith("_")), ...Object.keys(current).filter((k) => !k.startsWith("_")), ]); for (const field of fieldsToCompare) { // ์‹œ์Šคํ…œ ํ•„๋“œ๋Š” ๋น„๊ต์—์„œ ์ œ์™ธ if (["created_date", "updated_date", "created_by", "updated_by", "writer"].includes(field)) { continue; } const originalValue = original[field]; const currentValue = current[field]; // null/undefined ํ†ต์ผ ์ฒ˜๋ฆฌ const normalizedOriginal = originalValue === null || originalValue === undefined ? "" : String(originalValue); const normalizedCurrent = currentValue === null || currentValue === undefined ? "" : String(currentValue); if (normalizedOriginal !== normalizedCurrent) { console.log(` ๐Ÿ“ ๋ณ€๊ฒฝ ๊ฐ์ง€: ${field} = "${normalizedOriginal}" โ†’ "${normalizedCurrent}"`); return true; } } return false; } /** * ๐Ÿ†• ๋ฐฐ์น˜ ์ €์žฅ ์•ก์…˜ ์ฒ˜๋ฆฌ (SelectedItemsDetailInput์šฉ - ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ) * ItemData[] โ†’ ๊ฐ ํ’ˆ๋ชฉ์˜ details ๋ฐฐ์—ด์„ ๊ฐœ๋ณ„ ๋ ˆ์ฝ”๋“œ๋กœ ์ €์žฅ */ private static async handleBatchSave( config: ButtonActionConfig, context: ButtonActionContext, selectedItemsKeys: string[], ): Promise { const { formData, tableName, screenId, selectedRowsData, originalData } = context; if (!tableName || !screenId) { toast.error("์ €์žฅ์— ํ•„์š”ํ•œ ์ •๋ณด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. (ํ…Œ์ด๋ธ”๋ช… ๋˜๋Š” ํ™”๋ฉดID ๋ˆ„๋ฝ)"); return false; } try { let successCount = 0; let failCount = 0; const errors: string[] = []; // ๐Ÿ†• ๋ถ€๋ชจ ํ™”๋ฉด ๋ฐ์ดํ„ฐ ์ค€๋น„ (parentDataMapping์šฉ) // selectedRowsData ๋˜๋Š” originalData๋ฅผ parentData๋กœ ์‚ฌ์šฉ const parentData = selectedRowsData?.[0] || originalData || {}; // ๐Ÿ†• modalDataStore์—์„œ ๋ˆ„์ ๋œ ๋ชจ๋“  ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ // (์—ฌ๋Ÿฌ ๋‹จ๊ณ„ ๋ชจ๋‹ฌ์—์„œ ์ „๋‹ฌ๋œ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ์šฉ) let modalDataStoreRegistry: Record = {}; if (typeof window !== "undefined") { try { // Zustand store์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ const { useModalDataStore } = await import("@/stores/modalDataStore"); modalDataStoreRegistry = useModalDataStore.getState().dataRegistry; } catch (error) { console.warn("โš ๏ธ modalDataStore ๋กœ๋“œ ์‹คํŒจ:", error); } } // ๊ฐ ํ…Œ์ด๋ธ”์˜ ์ฒซ ๋ฒˆ์งธ ํ•ญ๋ชฉ์„ modalDataStore๋กœ ๋ณ€ํ™˜ const modalDataStore: Record = {}; Object.entries(modalDataStoreRegistry).forEach(([key, items]) => { if (Array.isArray(items) && items.length > 0) { // ModalDataItem[] โ†’ originalData ์ถ”์ถœ modalDataStore[key] = items.map((item) => item.originalData || item); } }); // ๊ฐ SelectedItemsDetailInput ์ปดํฌ๋„ŒํŠธ์˜ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ for (const key of selectedItemsKeys) { // ๐Ÿ†• ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ: ItemData[] with fieldGroups const items = formData[key] as Array<{ id: string; originalData: any; fieldGroups: Record>; }>; // ๐Ÿ†• ์ด ์ปดํฌ๋„ŒํŠธ์˜ parentDataMapping ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ const componentConfig = context.componentConfigs?.[key]; const parentDataMapping = componentConfig?.parentDataMapping || []; // ๐Ÿ†• ๊ฐ ํ’ˆ๋ชฉ์˜ ๊ทธ๋ฃน ๊ฐ„ ์กฐํ•ฉ(์นดํ‹ฐ์…˜ ๊ณฑ) ์ƒ์„ฑ for (const item of items) { const groupKeys = Object.keys(item.fieldGroups); // ๊ฐ ๊ทธ๋ฃน์˜ ํ•ญ๋ชฉ ๋ฐฐ์—ด ๊ฐ€์ ธ์˜ค๊ธฐ const groupArrays = groupKeys.map((groupKey) => ({ groupKey, entries: item.fieldGroups[groupKey] || [], })); // ์นดํ‹ฐ์…˜ ๊ณฑ ๊ณ„์‚ฐ ํ•จ์ˆ˜ const cartesianProduct = (arrays: any[][]): any[][] => { if (arrays.length === 0) return [[]]; if (arrays.length === 1) return arrays[0].map((item) => [item]); const [first, ...rest] = arrays; const restProduct = cartesianProduct(rest); return first.flatMap((item) => restProduct.map((combination) => [item, ...combination])); }; // ๋ชจ๋“  ๊ทธ๋ฃน์˜ ์นดํ‹ฐ์…˜ ๊ณฑ ์ƒ์„ฑ const entryArrays = groupArrays.map((g) => g.entries); // ๐Ÿ†• ๋ชจ๋“  ๊ทธ๋ฃน์ด ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธ const allGroupsEmpty = entryArrays.every((arr) => arr.length === 0); let combinations: any[][]; if (allGroupsEmpty) { // ๐Ÿ†• ๋ชจ๋“  ๊ทธ๋ฃน์ด ๋น„์–ด์žˆ์œผ๋ฉด ๋นˆ ์กฐํ•ฉ ํ•˜๋‚˜ ์ƒ์„ฑ (ํ’ˆ๋ชฉ ๊ธฐ๋ณธ ์ •๋ณด๋งŒ์œผ๋กœ ์ €์žฅ) combinations = [[]]; } else { // ๋นˆ ๊ทธ๋ฃน์„ ํ•„ํ„ฐ๋งํ•˜์—ฌ ์นดํ‹ฐ์…˜ ๊ณฑ ๊ณ„์‚ฐ (๋นˆ ๊ทธ๋ฃน์€ ๋ฌด์‹œ) const nonEmptyArrays = entryArrays.filter((arr) => arr.length > 0); combinations = nonEmptyArrays.length > 0 ? cartesianProduct(nonEmptyArrays) : [[]]; } // ๊ฐ ์กฐํ•ฉ์„ ๊ฐœ๋ณ„ ๋ ˆ์ฝ”๋“œ๋กœ ์ €์žฅ for (let i = 0; i < combinations.length; i++) { const combination = combinations[i]; try { // ๐Ÿ†• ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ ์šฉ const mappedData: any = {}; // 1. parentDataMapping ์„ค์ •์ด ์žˆ์œผ๋ฉด ์ ์šฉ if (parentDataMapping.length > 0) { for (const mapping of parentDataMapping) { let sourceData: any; const sourceTableName = mapping.sourceTable; const selectedItemTable = componentConfig?.sourceTable; if (sourceTableName === selectedItemTable) { sourceData = item.originalData; } else { const tableData = modalDataStore[sourceTableName]; if (tableData && Array.isArray(tableData) && tableData.length > 0) { sourceData = tableData[0]; } else { sourceData = parentData; } } const sourceValue = sourceData[mapping.sourceField]; if (sourceValue !== undefined && sourceValue !== null) { mappedData[mapping.targetField] = sourceValue; } else if (mapping.defaultValue !== undefined) { mappedData[mapping.targetField] = mapping.defaultValue; } } } else { // ๐Ÿ”ง parentDataMapping ์„ค์ •์ด ์—†๋Š” ๊ฒฝ์šฐ ๊ธฐ๋ณธ ๋งคํ•‘ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ) if (item.originalData.id) { mappedData.item_id = item.originalData.id; } if (parentData.id || parentData.customer_id) { mappedData.customer_id = parentData.customer_id || parentData.id; } } // ๊ณตํ†ต ํ•„๋“œ ๋ณต์‚ฌ (company_code, currency_code ๋“ฑ) if (item.originalData.company_code && !mappedData.company_code) { mappedData.company_code = item.originalData.company_code; } if (item.originalData.currency_code && !mappedData.currency_code) { mappedData.currency_code = item.originalData.currency_code; } // ์›๋ณธ ๋ฐ์ดํ„ฐ๋กœ ์‹œ์ž‘ (๋งคํ•‘๋œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ) let mergedData = { ...mappedData }; // ๊ฐ ๊ทธ๋ฃน์˜ ํ•ญ๋ชฉ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ๋ณ‘ํ•ฉ for (let j = 0; j < combination.length; j++) { const entry = combination[j]; const { id, ...entryData } = entry; // id ์ œ์™ธ mergedData = { ...mergedData, ...entryData }; } // ๐Ÿ†• ์กฐํ•ฉ ์ €์žฅ ์‹œ id ํ•„๋“œ ์ œ๊ฑฐ (๊ฐ ์กฐํ•ฉ์ด ๋…๋ฆฝ๋œ ์ƒˆ ๋ ˆ์ฝ”๋“œ๊ฐ€ ๋˜๋„๋ก) const { id: _removedId, ...dataWithoutId } = mergedData; // ์‚ฌ์šฉ์ž ์ •๋ณด ์ถ”๊ฐ€ if (!context.userId) { throw new Error("์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } const writerValue = context.userId; const companyCodeValue = context.companyCode || ""; const dataWithUserInfo = { ...dataWithoutId, writer: dataWithoutId.writer || writerValue, created_by: writerValue, updated_by: writerValue, company_code: dataWithoutId.company_code || companyCodeValue, }; // INSERT ์‹คํ–‰ const { DynamicFormApi } = await import("@/lib/api/dynamicForm"); const saveResult = await DynamicFormApi.saveFormData({ screenId, tableName, data: dataWithUserInfo, }); if (saveResult.success) { successCount++; } else { failCount++; errors.push(`ํ’ˆ๋ชฉ ${item.id} > ์กฐํ•ฉ ${i + 1}: ${saveResult.message}`); } } catch (error: any) { failCount++; errors.push(`ํ’ˆ๋ชฉ ${item.id} > ์กฐํ•ฉ ${i + 1}: ${error.message}`); } } } } // ๊ฒฐ๊ณผ ํ† ์ŠคํŠธ if (failCount === 0) { toast.success(`${successCount}๊ฐœ ํ•ญ๋ชฉ์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); } else if (successCount === 0) { toast.error(`์ €์žฅ ์‹คํŒจ: ${errors.join(", ")}`); return false; } else { toast.warning(`${successCount}๊ฐœ ์„ฑ๊ณต, ${failCount}๊ฐœ ์‹คํŒจ: ${errors.join(", ")}`); } // ํ…Œ์ด๋ธ”๊ณผ ํ”Œ๋กœ์šฐ ์ƒˆ๋กœ๊ณ ์นจ context.onRefresh?.(); context.onFlowRefresh?.(); // ์ €์žฅ ์„ฑ๊ณต ํ›„ ์ด๋ฒคํŠธ ๋ฐœ์ƒ window.dispatchEvent(new CustomEvent("closeEditModal")); window.dispatchEvent(new CustomEvent("saveSuccessInModal")); return true; } catch (error: any) { console.error("๋ฐฐ์น˜ ์ €์žฅ ์˜ค๋ฅ˜:", error); toast.error(`์ €์žฅ ์˜ค๋ฅ˜: ${error.message}`); return false; } } /** * ์‚ญ์ œ ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static async handleDelete(config: ButtonActionConfig, context: ButtonActionContext): Promise { const { formData, tableName, screenId, selectedRowsData, flowSelectedData } = context; try { // ํ”Œ๋กœ์šฐ ์„ ํƒ ๋ฐ์ดํ„ฐ ์šฐ์„  ์‚ฌ์šฉ const dataToDelete = flowSelectedData && flowSelectedData.length > 0 ? flowSelectedData : selectedRowsData; // ๋‹ค์ค‘ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ if (dataToDelete && dataToDelete.length > 0) { console.log(`๋‹ค์ค‘ ์‚ญ์ œ ์•ก์…˜ ์‹คํ–‰: ${dataToDelete.length}๊ฐœ ํ•ญ๋ชฉ`, dataToDelete); // ํ…Œ์ด๋ธ”์˜ ๊ธฐ๋ณธํ‚ค ์กฐํšŒ let primaryKeys: string[] = []; if (tableName) { try { const primaryKeysResult = await DynamicFormApi.getTablePrimaryKeys(tableName); if (primaryKeysResult.success && primaryKeysResult.data) { primaryKeys = primaryKeysResult.data; } } catch (error) { console.warn("๊ธฐ๋ณธํ‚ค ์กฐํšŒ ์‹คํŒจ, ํด๋ฐฑ ๋ฐฉ๋ฒ• ์‚ฌ์šฉ:", error); } } // ๊ฐ ์„ ํƒ๋œ ํ•ญ๋ชฉ์„ ์‚ญ์ œ for (const rowData of dataToDelete) { let deleteId: any = null; // 1์ˆœ์œ„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์กฐํšŒํ•œ ๊ธฐ๋ณธํ‚ค ์‚ฌ์šฉ if (primaryKeys.length > 0) { const primaryKey = primaryKeys[0]; // ์ฒซ ๋ฒˆ์งธ ๊ธฐ๋ณธํ‚ค ์‚ฌ์šฉ deleteId = rowData[primaryKey]; } // 2์ˆœ์œ„: ํด๋ฐฑ - ์ผ๋ฐ˜์ ์ธ ID ํ•„๋“œ๋ช…๋“ค ์‹œ๋„ if (!deleteId) { deleteId = rowData.id || rowData.objid || rowData.pk || rowData.ID || rowData.OBJID || rowData.PK || // ํ…Œ์ด๋ธ”๋ณ„ ๊ธฐ๋ณธํ‚ค ํŒจํ„ด๋“ค rowData.sales_no || rowData.contract_no || rowData.order_no || rowData.seq_no || rowData.code || rowData.code_id || rowData.user_id || rowData.menu_id; // _no๋กœ ๋๋‚˜๋Š” ํ•„๋“œ๋“ค ์ฐพ๊ธฐ if (!deleteId) { const noField = Object.keys(rowData).find((key) => key.endsWith("_no") && rowData[key]); if (noField) deleteId = rowData[noField]; } // _id๋กœ ๋๋‚˜๋Š” ํ•„๋“œ๋“ค ์ฐพ๊ธฐ if (!deleteId) { const idField = Object.keys(rowData).find((key) => key.endsWith("_id") && rowData[key]); if (idField) deleteId = rowData[idField]; } } console.log("์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ:", rowData); console.log("์ตœ์ข… ์ถ”์ถœ๋œ deleteId:", deleteId); if (deleteId) { console.log("๋‹ค์ค‘ ๋ฐ์ดํ„ฐ ์‚ญ์ œ:", { tableName, screenId, id: deleteId }); // screenId ์ „๋‹ฌํ•˜์—ฌ ์ œ์–ด๊ด€๋ฆฌ ์‹คํ–‰ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•จ const deleteResult = await DynamicFormApi.deleteFormDataFromTable(deleteId, tableName, screenId); if (!deleteResult.success) { throw new Error(`ID ${deleteId} ์‚ญ์ œ ์‹คํŒจ: ${deleteResult.message}`); } } else { console.error("์‚ญ์ œ ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํ–‰ ๋ฐ์ดํ„ฐ:", rowData); throw new Error( `์‚ญ์ œ ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธํ‚ค: ${primaryKeys.join(", ")}, ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ•„๋“œ: ${Object.keys(rowData).join(", ")}`, ); } } // ๋ฐ์ดํ„ฐ ์†Œ์Šค์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ์ƒˆ๋กœ๊ณ ์นจ ํ˜ธ์ถœ if (flowSelectedData && flowSelectedData.length > 0) { context.onFlowRefresh?.(); // ํ”Œ๋กœ์šฐ ์ƒˆ๋กœ๊ณ ์นจ } else { context.onRefresh?.(); // ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋“ฑ ์ „์—ญ ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฐœ์ƒ window.dispatchEvent(new CustomEvent("refreshTable")); } toast.success(config.successMessage || `${dataToDelete.length}๊ฐœ ํ•ญ๋ชฉ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); return true; } // ๋‹จ์ผ ์‚ญ์ œ (๊ธฐ์กด ๋กœ์ง) if (tableName && screenId && formData.id) { console.log("๋‹จ์ผ ๋ฐ์ดํ„ฐ ์‚ญ์ œ:", { tableName, screenId, id: formData.id }); // ์‹ค์ œ ์‚ญ์ œ API ํ˜ธ์ถœ - screenId ์ „๋‹ฌํ•˜์—ฌ ์ œ์–ด๊ด€๋ฆฌ ์‹คํ–‰ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•จ const deleteResult = await DynamicFormApi.deleteFormDataFromTable(formData.id, tableName, screenId); if (!deleteResult.success) { throw new Error(deleteResult.message || "์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } } else { throw new Error("์‚ญ์ œ์— ํ•„์š”ํ•œ ์ •๋ณด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. (ID, ํ…Œ์ด๋ธ”๋ช… ๋˜๋Š” ํ™”๋ฉดID ๋ˆ„๋ฝ)"); } context.onRefresh?.(); // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋“ฑ ์ „์—ญ ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฐœ์ƒ window.dispatchEvent(new CustomEvent("refreshTable")); toast.success(config.successMessage || "์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); return true; } catch (error) { console.error("์‚ญ์ œ ์˜ค๋ฅ˜:", error); throw error; // ์—๋Ÿฌ๋ฅผ ๋‹ค์‹œ ๋˜์ ธ์„œ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ•จ } } /** * ์ดˆ๊ธฐํ™” ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static handleReset(config: ButtonActionConfig, context: ButtonActionContext): boolean { const { formData, onFormDataChange } = context; // ํผ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” - ๊ฐ ํ•„๋“œ๋ฅผ ๊ฐœ๋ณ„์ ์œผ๋กœ ์ดˆ๊ธฐํ™” if (onFormDataChange && formData) { Object.keys(formData).forEach((key) => { onFormDataChange(key, ""); }); } toast.success(config.successMessage || "์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); return true; } /** * ์ทจ์†Œ ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static handleCancel(config: ButtonActionConfig, context: ButtonActionContext): boolean { const { onClose } = context; onClose?.(); return true; } /** * ๋„ค๋น„๊ฒŒ์ด์…˜ ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static handleNavigate(config: ButtonActionConfig, context: ButtonActionContext): boolean { let targetUrl = config.targetUrl; // ํ™”๋ฉด ID๊ฐ€ ์ง€์ •๋œ ๊ฒฝ์šฐ URL ์ƒ์„ฑ if (config.targetScreenId) { targetUrl = `/screens/${config.targetScreenId}`; } if (targetUrl) { window.location.href = targetUrl; return true; } toast.error("์ด๋™ํ•  ํŽ˜์ด์ง€๊ฐ€ ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); return false; } /** * ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ ๋ฒ„ํŠผ์˜ ์„ ํƒ ๋ฐ์ดํ„ฐ๋กœ ๋ชจ๋‹ฌ ์—ด๊ธฐ * RelatedDataButtons ์ปดํฌ๋„ŒํŠธ์—์„œ ์„ ํƒ๋œ ๋ฒ„ํŠผ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‹ฌ๋กœ ์ „๋‹ฌ */ private static async handleOpenRelatedModal( config: ButtonActionConfig, context: ButtonActionContext, ): Promise { // ๋ฒ„ํŠผ ์„ค์ •์—์„œ targetScreenId ๊ฐ€์ ธ์˜ค๊ธฐ (์—ฌ๋Ÿฌ ์œ„์น˜์—์„œ ํ™•์ธ) const targetScreenId = config.relatedModalConfig?.targetScreenId || config.targetScreenId; if (!targetScreenId) { console.error("โŒ [openRelatedModal] targetScreenId๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); toast.error("๋ชจ๋‹ฌ ํ™”๋ฉด ID๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); return false; } // RelatedDataButtons์—์„œ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ const relatedData = window.__relatedButtonsSelectedData; if (!relatedData?.selectedItem) { console.warn("โš ๏ธ [openRelatedModal] ์„ ํƒ๋œ ๋ฒ„ํŠผ์ด ์—†์Šต๋‹ˆ๋‹ค."); toast.warning("๋จผ์ € ๋ฒ„ํŠผ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return false; } const { selectedItem, config: relatedConfig } = relatedData; // ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ ์šฉ const initialData: Record = {}; if (relatedConfig?.modalLink?.dataMapping && relatedConfig.modalLink.dataMapping.length > 0) { relatedConfig.modalLink.dataMapping.forEach((mapping) => { if (mapping.sourceField === "value") { initialData[mapping.targetField] = selectedItem.value; } else if (mapping.sourceField === "id") { initialData[mapping.targetField] = selectedItem.id; } else if (selectedItem.rawData[mapping.sourceField] !== undefined) { initialData[mapping.targetField] = selectedItem.rawData[mapping.sourceField]; } }); } else { // ๊ธฐ๋ณธ ๋งคํ•‘: id๋ฅผ routing_version_id๋กœ ์ „๋‹ฌ initialData["routing_version_id"] = selectedItem.value || selectedItem.id; } // ๋ชจ๋‹ฌ ์—ด๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (ScreenModal์€ editData๋ฅผ ์‚ฌ์šฉ) window.dispatchEvent( new CustomEvent("openScreenModal", { detail: { screenId: targetScreenId, title: config.modalTitle, description: config.modalDescription, editData: initialData, // ScreenModal์€ editData๋กœ ํผ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์Œ onSuccess: () => { // ์„ฑ๊ณต ํ›„ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ window.dispatchEvent(new CustomEvent("refreshTableData")); }, }, }), ); return true; } /** * ๋ชจ๋‹ฌ ์•ก์…˜ ์ฒ˜๋ฆฌ (ํ†ตํ•ฉ) * - passSelectedData: true (๊ธฐ๋ณธ) โ†’ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‹ฌ์— ์ „๋‹ฌ * - autoDetectDataSource: true โ†’ TableList/SplitPanel ๋“ฑ์—์„œ ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ž๋™ ๊ฐ์ง€ * - fieldMappings โ†’ ํ•„๋“œ ์ด๋ฆ„ ๋งคํ•‘ ์ง€์› */ private static async handleModal(config: ButtonActionConfig, context: ButtonActionContext): Promise { const passSelectedData = config.passSelectedData !== false; // ๊ธฐ๋ณธ: true const autoDetectDataSource = config.autoDetectDataSource === true; if (!config.targetScreenId) { toast.error("๋ชจ๋‹ฌ๋กœ ์—ด ํ™”๋ฉด์ด ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); return false; } // 1. ํ™”๋ฉด ์„ค๋ช… ๊ฐ€์ ธ์˜ค๊ธฐ let description = config.modalDescription || ""; if (!description) { try { const screenInfo = await screenApi.getScreen(config.targetScreenId); description = screenInfo?.description || ""; } catch (error) { console.warn("ํ™”๋ฉด ์„ค๋ช…์„ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค:", error); } } // 2. ๋ฐ์ดํ„ฐ ์†Œ์Šค ๋ฐ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ let selectedData: any[] = []; let parentData: Record = {}; let dataSourceId: string | undefined; if (passSelectedData) { // 2-1. ์ž๋™ ๊ฐ์ง€ ๋ชจ๋“œ if (autoDetectDataSource) { dataSourceId = config.dataSourceId; // TableList, V2List ๋˜๋Š” SplitPanelLayout์—์„œ ์ž๋™ ๊ฐ์ง€ if (!dataSourceId && context.allComponents) { // 1. table-list ์ปดํฌ๋„ŒํŠธ ์ฐพ๊ธฐ const tableListComponent = context.allComponents.find( (comp: any) => comp.componentType === "table-list" && comp.componentConfig?.tableName, ); if (tableListComponent) { dataSourceId = tableListComponent.componentConfig.tableName; } else { // 2. v2-list ์ปดํฌ๋„ŒํŠธ ์ฐพ๊ธฐ const v2ListComponent = context.allComponents.find( (comp: any) => comp.componentType === "v2-list" && (comp.componentConfig?.dataSource?.table || comp.componentConfig?.tableName), ); if (v2ListComponent) { dataSourceId = v2ListComponent.componentConfig.dataSource?.table || v2ListComponent.componentConfig.tableName; console.log("โœจ V2List ์ž๋™ ๊ฐ์ง€:", { componentId: v2ListComponent.id, tableName: dataSourceId, }); } else { // 3. split-panel-layout ์ปดํฌ๋„ŒํŠธ ์ฐพ๊ธฐ const splitPanelComponent = context.allComponents.find( (comp: any) => comp.componentType === "split-panel-layout" && comp.componentConfig?.leftPanel?.tableName, ); if (splitPanelComponent) { dataSourceId = splitPanelComponent.componentConfig.leftPanel.tableName; } } } } dataSourceId = dataSourceId || context.tableName || "default"; // modalDataStore์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ try { const { useModalDataStore } = await import("@/stores/modalDataStore"); const dataRegistry = useModalDataStore.getState().dataRegistry; const modalData = dataRegistry[dataSourceId] || []; if (modalData.length === 0) { console.warn("โš ๏ธ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค:", dataSourceId); toast.warning("์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋จผ์ € ํ•ญ๋ชฉ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return false; } selectedData = modalData.map((item: any) => item.originalData || item); const rawParentData = modalData[0]?.originalData || modalData[0] || {}; // ํ•„๋“œ ๋งคํ•‘ ์ ์šฉ if (config.fieldMappings?.length) { // ๋งคํ•‘์ด ์žˆ์œผ๋ฉด ๋งคํ•‘๋œ ํ•„๋“œ๋งŒ ์ „๋‹ฌ parentData = {}; config.fieldMappings.forEach((mapping) => { if (mapping.sourceField && mapping.targetField && rawParentData[mapping.sourceField] !== undefined) { parentData[mapping.targetField] = rawParentData[mapping.sourceField]; } }); } else { // ๋งคํ•‘์ด ์—†์œผ๋ฉด ๋ชจ๋“  ํ•„๋“œ ์ „๋‹ฌ (๋™์ผ ์ปฌ๋Ÿผ๋ช… ์ž๋™ ๋งคํ•‘) parentData = { ...rawParentData }; } } catch (error) { console.error("โŒ ๋ฐ์ดํ„ฐ ํ™•์ธ ์‹คํŒจ:", error); toast.error("๋ฐ์ดํ„ฐ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } else { // 2-2. ๊ธฐ๋ณธ ๋ชจ๋“œ: context์—์„œ ์ง์ ‘ ๊ฐ€์ ธ์˜ค๊ธฐ selectedData = context.selectedRowsData || []; const rawParentData = context.splitPanelParentData || selectedData[0] || {}; // ํ•„๋“œ ๋งคํ•‘ ์ ์šฉ if (config.fieldMappings?.length && selectedData.length > 0) { // ๋งคํ•‘์ด ์žˆ์œผ๋ฉด ๋งคํ•‘๋œ ํ•„๋“œ๋งŒ ์ „๋‹ฌ parentData = {}; config.fieldMappings.forEach((mapping) => { if (mapping.sourceField && mapping.targetField && rawParentData[mapping.sourceField] !== undefined) { parentData[mapping.targetField] = rawParentData[mapping.sourceField]; } }); } else { // ๋งคํ•‘์ด ์—†์œผ๋ฉด ๋ชจ๋“  ํ•„๋“œ ์ „๋‹ฌ (๋™์ผ ์ปฌ๋Ÿผ๋ช… ์ž๋™ ๋งคํ•‘) parentData = { ...rawParentData }; } } } // 3. ๋™์  ๋ชจ๋‹ฌ ์ œ๋ชฉ ์ƒ์„ฑ let finalTitle = config.modalTitle || "ํ™”๋ฉด"; // ๋ธ”๋ก ๊ธฐ๋ฐ˜ ์ œ๋ชฉ ์ฒ˜๋ฆฌ if (config.modalTitleBlocks?.length) { try { const { useModalDataStore } = await import("@/stores/modalDataStore"); const dataRegistry = useModalDataStore.getState().dataRegistry; const titleParts: string[] = []; config.modalTitleBlocks.forEach((block) => { if (block.type === "text") { titleParts.push(block.value); } else if (block.type === "field" && block.tableName && block.value) { const tableData = dataRegistry[block.tableName]; if (tableData?.length > 0) { const firstItem = tableData[0].originalData || tableData[0]; const value = firstItem[block.value]; titleParts.push(value !== undefined && value !== null ? String(value) : block.label || block.value); } else { titleParts.push(block.label || block.value); } } }); finalTitle = titleParts.join(""); } catch (error) { console.warn("๋™์  ์ œ๋ชฉ ์ƒ์„ฑ ์‹คํŒจ:", error); } } // {tableName.columnName} ํŒจํ„ด ์ฒ˜๋ฆฌ else if (finalTitle.includes("{")) { try { const { useModalDataStore } = await import("@/stores/modalDataStore"); const dataRegistry = useModalDataStore.getState().dataRegistry; const matches = finalTitle.match(/\{([^}]+)\}/g); if (matches) { matches.forEach((match) => { const path = match.slice(1, -1); const [tableName, columnName] = path.split("."); if (tableName && columnName) { const tableData = dataRegistry[tableName]; if (tableData?.length > 0) { const firstItem = tableData[0].originalData || tableData[0]; const value = firstItem[columnName]; if (value !== undefined && value !== null) { finalTitle = finalTitle.replace(match, String(value)); } } } }); } } catch { // ๋™์  ์ œ๋ชฉ ๋ณ€ํ™˜ ์‹คํŒจ ์‹œ ๋ฌด์‹œ } } // 4. ๋ชจ๋‹ฌ ์—ด๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์ƒ // ๐Ÿ”ง ์ˆ˜์ •: openModalWithData๋Š” "์‹ ๊ทœ ๋“ฑ๋ก + ์—ฐ๊ฒฐ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ"์šฉ์ด๋ฏ€๋กœ // editData๊ฐ€ ์•„๋‹Œ splitPanelParentData๋กœ ์ „๋‹ฌํ•ด์•ผ ์ฑ„๋ฒˆ ๋“ฑ์ด ์ •์ƒ ์ž‘๋™ํ•จ const isPassDataMode = passSelectedData && selectedData.length > 0; // ๐Ÿ”ง isEditMode ์˜ต์…˜์ด ๋ช…์‹œ์ ์œผ๋กœ true์ธ ๊ฒฝ์šฐ์—๋งŒ ์ˆ˜์ • ๋ชจ๋“œ๋กœ ์ฒ˜๋ฆฌ const useAsEditData = config.isEditMode === true; const modalEvent = new CustomEvent("openScreenModal", { detail: { screenId: config.targetScreenId, title: finalTitle, description: description, size: config.modalSize || "md", selectedData: selectedData, selectedIds: selectedData.map((row: any) => row.id).filter(Boolean), // ๐Ÿ”ง ์ˆ˜์ •: isEditMode๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ true์ธ ๊ฒฝ์šฐ์—๋งŒ editData๋กœ ์ „๋‹ฌ // ๊ธฐ๋ณธ์ ์œผ๋กœ๋Š” splitPanelParentData๋กœ ์ „๋‹ฌํ•˜์—ฌ ์‹ ๊ทœ ๋“ฑ๋ก + ์—ฐ๊ฒฐ ๋ฐ์ดํ„ฐ ๋ชจ๋“œ editData: useAsEditData && isPassDataMode ? parentData : undefined, splitPanelParentData: isPassDataMode ? parentData : undefined, urlParams: dataSourceId ? { dataSourceId } : undefined, // ๐Ÿ†• ํ•„๋“œ ๋งคํ•‘ ์ •๋ณด ์ „๋‹ฌ - ScreenModal์—์„œ ๋งคํ•‘๋œ ํ•„๋“œ๋ฅผ ํ•„ํ„ฐ๋งํ•˜์ง€ ์•Š๋„๋ก fieldMappings: config.fieldMappings, }, }); window.dispatchEvent(modalEvent); // ๋ชจ๋‹ฌ ์—ด๊ธฐ๋Š” UI ์ „ํ™˜์ด๋ฏ€๋กœ ์„ฑ๊ณต ํ† ์ŠคํŠธ๋ฅผ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ // (์ €์žฅ ๋“ฑ ์‹ค์ œ ์•ก์…˜ ์™„๋ฃŒ ์‹œ์—๋งŒ ํ† ์ŠคํŠธ ํ‘œ์‹œ) return true; } /** * ๐Ÿ†• ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋ฉด์„œ ๋ชจ๋‹ฌ ์—ด๊ธฐ ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static async handleOpenModalWithData( config: ButtonActionConfig, context: ButtonActionContext, ): Promise { // ๐Ÿ†• 1. ํ˜„์žฌ ํ™”๋ฉด์˜ TableList ๋˜๋Š” SplitPanelLayout ์ž๋™ ๊ฐ์ง€ let dataSourceId = config.dataSourceId; if (!dataSourceId && context.allComponents) { // TableList ์šฐ์„  ๊ฐ์ง€ const tableListComponent = context.allComponents.find( (comp: any) => comp.componentType === "table-list" && comp.componentConfig?.tableName, ); if (tableListComponent) { dataSourceId = tableListComponent.componentConfig.tableName; console.log("โœจ TableList ์ž๋™ ๊ฐ์ง€:", { componentId: tableListComponent.id, tableName: dataSourceId, }); } else { // TableList๊ฐ€ ์—†์œผ๋ฉด SplitPanelLayout์˜ ์ขŒ์ธก ํŒจ๋„ ๊ฐ์ง€ const splitPanelComponent = context.allComponents.find( (comp: any) => comp.componentType === "split-panel-layout" && comp.componentConfig?.leftPanel?.tableName, ); if (splitPanelComponent) { dataSourceId = splitPanelComponent.componentConfig.leftPanel.tableName; console.log("โœจ ๋ถ„ํ•  ํŒจ๋„ ์ขŒ์ธก ํ…Œ์ด๋ธ” ์ž๋™ ๊ฐ์ง€:", { componentId: splitPanelComponent.id, tableName: dataSourceId, }); } } } // ์—ฌ์ „ํžˆ ์—†์œผ๋ฉด context.tableName ๋˜๋Š” "default" ์‚ฌ์šฉ if (!dataSourceId) { dataSourceId = context.tableName || "default"; } // ๐Ÿ†• 2. modalDataStore์—์„œ ํ˜„์žฌ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ํ™•์ธ try { const { useModalDataStore } = await import("@/stores/modalDataStore"); const dataRegistry = useModalDataStore.getState().dataRegistry; const modalData = dataRegistry[dataSourceId] || []; if (modalData.length === 0) { console.warn("โš ๏ธ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค:", dataSourceId); toast.warning("์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋จผ์ € ํ•ญ๋ชฉ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return false; } } catch (error) { console.error("โŒ ๋ฐ์ดํ„ฐ ํ™•์ธ ์‹คํŒจ:", error); toast.error("๋ฐ์ดํ„ฐ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } // 6. ๋™์  ๋ชจ๋‹ฌ ์ œ๋ชฉ ์ƒ์„ฑ const { useModalDataStore } = await import("@/stores/modalDataStore"); const dataRegistry = useModalDataStore.getState().dataRegistry; let finalTitle = "๋ฐ์ดํ„ฐ ์ž…๋ ฅ"; // ๐Ÿ†• ๋ธ”๋ก ๊ธฐ๋ฐ˜ ์ œ๋ชฉ (์šฐ์„ ์ˆœ์œ„ 1) if (config.modalTitleBlocks && config.modalTitleBlocks.length > 0) { const titleParts: string[] = []; config.modalTitleBlocks.forEach((block) => { if (block.type === "text") { // ํ…์ŠคํŠธ ๋ธ”๋ก: ๊ทธ๋Œ€๋กœ ์ถ”๊ฐ€ titleParts.push(block.value); } else if (block.type === "field") { // ํ•„๋“œ ๋ธ”๋ก: ๋ฐ์ดํ„ฐ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ const tableName = block.tableName; const columnName = block.value; if (tableName && columnName) { const tableData = dataRegistry[tableName]; if (tableData && tableData.length > 0) { const firstItem = tableData[0].originalData || tableData[0]; const value = firstItem[columnName]; if (value !== undefined && value !== null) { titleParts.push(String(value)); console.log(`โœจ ๋™์  ํ•„๋“œ: ${tableName}.${columnName} โ†’ ${value}`); } else { // ๋ฐ์ดํ„ฐ ์—†์œผ๋ฉด ๋ผ๋ฒจ ํ‘œ์‹œ titleParts.push(block.label || columnName); } } else { // ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์—†์œผ๋ฉด ๋ผ๋ฒจ ํ‘œ์‹œ titleParts.push(block.label || columnName); } } } }); finalTitle = titleParts.join(""); } // ๊ธฐ์กด ๋ฐฉ์‹: {tableName.columnName} ํŒจํ„ด (์šฐ์„ ์ˆœ์œ„ 2) else if (config.modalTitle) { finalTitle = config.modalTitle; if (finalTitle.includes("{")) { const matches = finalTitle.match(/\{([^}]+)\}/g); if (matches) { matches.forEach((match) => { const path = match.slice(1, -1); // {item_info.item_name} โ†’ item_info.item_name const [tableName, columnName] = path.split("."); if (tableName && columnName) { const tableData = dataRegistry[tableName]; if (tableData && tableData.length > 0) { const firstItem = tableData[0].originalData || tableData[0]; const value = firstItem[columnName]; if (value !== undefined && value !== null) { finalTitle = finalTitle.replace(match, String(value)); console.log(`โœจ ๋™์  ์ œ๋ชฉ: ${match} โ†’ ${value}`); } } } }); } } } // 7. ๋ชจ๋‹ฌ ์—ด๊ธฐ + URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ dataSourceId ์ „๋‹ฌ if (config.targetScreenId) { // config์— modalDescription์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ let description = config.modalDescription || ""; // config์— ์—†์œผ๋ฉด ํ™”๋ฉด ์ •๋ณด์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ if (!description) { try { const screenInfo = await screenApi.getScreen(config.targetScreenId); description = screenInfo?.description || ""; } catch (error) { console.warn("ํ™”๋ฉด ์„ค๋ช…์„ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค:", error); } } // ๐Ÿ†• ๋ถ€๋ชจ ํ™”๋ฉด์˜ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (excludeFilter์—์„œ ์‚ฌ์šฉ) const rawParentData = dataRegistry[dataSourceId]?.[0]?.originalData || dataRegistry[dataSourceId]?.[0] || {}; // ๐Ÿ†• ํ•„๋“œ ๋งคํ•‘ ์ ์šฉ (์†Œ์Šค ์ปฌ๋Ÿผ โ†’ ํƒ€๊ฒŸ ์ปฌ๋Ÿผ) const parentData = { ...rawParentData }; if (config.fieldMappings && Array.isArray(config.fieldMappings) && config.fieldMappings.length > 0) { config.fieldMappings.forEach((mapping: { sourceField: string; targetField: string }) => { if (mapping.sourceField && mapping.targetField && rawParentData[mapping.sourceField] !== undefined) { // ํƒ€๊ฒŸ ํ•„๋“œ์— ์†Œ์Šค ํ•„๋“œ ๊ฐ’ ๋ณต์‚ฌ parentData[mapping.targetField] = rawParentData[mapping.sourceField]; console.log(` โœ… ${mapping.sourceField} โ†’ ${mapping.targetField}: ${rawParentData[mapping.sourceField]}`); } }); } // ๐Ÿ†• modalDataStore์—์„œ ์„ ํƒ๋œ ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (RepeatScreenModal์—์„œ ์‚ฌ์šฉ) const modalData = dataRegistry[dataSourceId] || []; const selectedData = modalData.map((item: any) => item.originalData || item); const selectedIds = selectedData.map((row: any) => row.id).filter(Boolean); // ๐Ÿ†• ์ „์—ญ ๋ชจ๋‹ฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋ฅผ ์œ„ํ•œ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (URL ํŒŒ๋ผ๋ฏธํ„ฐ ํฌํ•จ) const modalEvent = new CustomEvent("openScreenModal", { detail: { screenId: config.targetScreenId, title: finalTitle, // ๐Ÿ†• ๋™์  ์ œ๋ชฉ ์‚ฌ์šฉ description: description, size: config.modalSize || "lg", // ๋ฐ์ดํ„ฐ ์ž…๋ ฅ ํ™”๋ฉด์€ ๊ธฐ๋ณธ large urlParams: { dataSourceId }, // ๐Ÿ†• ์ฃผ ๋ฐ์ดํ„ฐ ์†Œ์Šค๋งŒ ์ „๋‹ฌ (๋‚˜๋จธ์ง€๋Š” modalDataStore์—์„œ ์ž๋™์œผ๋กœ ์ฐพ์Œ) splitPanelParentData: parentData, // ๐Ÿ†• ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (excludeFilter์—์„œ ์‚ฌ์šฉ) // ๐Ÿ†• ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (RepeatScreenModal์—์„œ groupedData๋กœ ์‚ฌ์šฉ) selectedData: selectedData, selectedIds: selectedIds, }, }); window.dispatchEvent(modalEvent); // ๋ชจ๋‹ฌ ์—ด๊ธฐ๋Š” UI ์ „ํ™˜์ด๋ฏ€๋กœ ์„ฑ๊ณต ํ† ์ŠคํŠธ๋ฅผ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ return true; } else { console.error("๋ชจ๋‹ฌ๋กœ ์—ด ํ™”๋ฉด์ด ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); toast.error("๋Œ€์ƒ ํ™”๋ฉด์ด ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); return false; } } /** * ์ƒˆ ์ฐฝ ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static handleNewWindow(config: ButtonActionConfig, context: ButtonActionContext): boolean { let targetUrl = config.targetUrl; // ํ™”๋ฉด ID๊ฐ€ ์ง€์ •๋œ ๊ฒฝ์šฐ URL ์ƒ์„ฑ if (config.targetScreenId) { targetUrl = `/screens/${config.targetScreenId}`; } if (targetUrl) { const windowFeatures = `width=${config.popupWidth || 800},height=${config.popupHeight || 600},scrollbars=yes,resizable=yes`; window.open(targetUrl, "_blank", windowFeatures); return true; } toast.error("์—ด ํŽ˜์ด์ง€๊ฐ€ ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); return false; } /** * ํŒ์—… ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static handlePopup(config: ButtonActionConfig, context: ButtonActionContext): boolean { // ํŒ์—…์€ ์ƒˆ ์ฐฝ๊ณผ ์œ ์‚ฌํ•˜์ง€๋งŒ ๋” ์ž‘์€ ํฌ๊ธฐ return this.handleNewWindow( { ...config, popupWidth: config.popupWidth || 600, popupHeight: config.popupHeight || 400, }, context, ); } /** * ๊ฒ€์ƒ‰ ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static handleSearch(config: ButtonActionConfig, context: ButtonActionContext): boolean { const { formData, onRefresh } = context; console.log("๊ฒ€์ƒ‰ ์‹คํ–‰:", formData); // ๊ฒ€์ƒ‰ ์กฐ๊ฑด ๊ฒ€์ฆ const hasSearchCriteria = Object.values(formData).some( (value) => value !== null && value !== undefined && value !== "", ); if (!hasSearchCriteria) { toast.warning("๊ฒ€์ƒ‰ ์กฐ๊ฑด์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."); return false; } // ๊ฒ€์ƒ‰ ์‹คํ–‰ (๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ) onRefresh?.(); // ๊ฒ€์ƒ‰ ์กฐ๊ฑด์„ URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ถ”๊ฐ€ (์„ ํƒ์‚ฌํ•ญ) const searchParams = new URLSearchParams(); Object.entries(formData).forEach(([key, value]) => { if (value !== null && value !== undefined && value !== "") { searchParams.set(key, String(value)); } }); // URL ์—…๋ฐ์ดํŠธ (ํžˆ์Šคํ† ๋ฆฌ์— ์ถ”๊ฐ€ํ•˜์ง€ ์•Š์Œ) if (searchParams.toString()) { const newUrl = `${window.location.pathname}?${searchParams.toString()}`; window.history.replaceState({}, "", newUrl); } toast.success(config.successMessage || "๊ฒ€์ƒ‰์„ ์‹คํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค."); return true; } /** * ์ถ”๊ฐ€ ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static handleAdd(config: ButtonActionConfig, context: ButtonActionContext): boolean { console.log("์ถ”๊ฐ€ ์•ก์…˜ ์‹คํ–‰:", context); // ์ถ”๊ฐ€ ๋กœ์ง ๊ตฌํ˜„ (์˜ˆ: ์ƒˆ ๋ ˆ์ฝ”๋“œ ์ƒ์„ฑ ํผ ์—ด๊ธฐ) return true; } /** * ํŽธ์ง‘ ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static async handleEdit(config: ButtonActionConfig, context: ButtonActionContext): Promise { const { selectedRowsData, flowSelectedData } = context; // ํ”Œ๋กœ์šฐ ์„ ํƒ ๋ฐ์ดํ„ฐ ์šฐ์„  ์‚ฌ์šฉ const dataToEdit = flowSelectedData && flowSelectedData.length > 0 ? flowSelectedData : selectedRowsData; // ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ if (!dataToEdit || dataToEdit.length === 0) { toast.error("์ˆ˜์ •ํ•  ํ•ญ๋ชฉ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return false; } // ํŽธ์ง‘ ํ™”๋ฉด์ด ์„ค์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ if (!config.targetScreenId) { toast.error("์ˆ˜์ • ํผ ํ™”๋ฉด์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋ฒ„ํŠผ ์„ค์ •์—์„œ ์ˆ˜์ • ํผ ํ™”๋ฉด์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return false; } if (dataToEdit.length === 1) { // ๋‹จ์ผ ํ•ญ๋ชฉ ํŽธ์ง‘ const rowData = dataToEdit[0]; await this.openEditForm(config, rowData, context); } else { // ๋‹ค์ค‘ ํ•ญ๋ชฉ ํŽธ์ง‘ - ํ˜„์žฌ๋Š” ๋‹จ์ผ ํŽธ์ง‘๋งŒ ์ง€์› toast.error("ํ˜„์žฌ ๋‹จ์ผ ํ•ญ๋ชฉ ํŽธ์ง‘๋งŒ ์ง€์›๋ฉ๋‹ˆ๋‹ค. ํ•˜๋‚˜์˜ ํ•ญ๋ชฉ๋งŒ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return false; } return true; } /** * ํŽธ์ง‘ ํผ ์—ด๊ธฐ (๋‹จ์ผ ํ•ญ๋ชฉ) */ private static async openEditForm( config: ButtonActionConfig, rowData: any, context: ButtonActionContext, ): Promise { const editMode = config.editMode || "modal"; switch (editMode) { case "modal": // ๋ชจ๋‹ฌ๋กœ ํŽธ์ง‘ ํผ ์—ด๊ธฐ await this.openEditModal(config, rowData, context); break; case "navigate": // ์ƒˆ ํŽ˜์ด์ง€๋กœ ์ด๋™ this.navigateToEditScreen(config, rowData, context); break; case "inline": // ํ˜„์žฌ ํ™”๋ฉด์—์„œ ์ธ๋ผ์ธ ํŽธ์ง‘ (ํ–ฅํ›„ ๊ตฌํ˜„) toast.info("์ธ๋ผ์ธ ํŽธ์ง‘ ๊ธฐ๋Šฅ์€ ํ–ฅํ›„ ์ง€์› ์˜ˆ์ •์ž…๋‹ˆ๋‹ค."); break; default: // ๊ธฐ๋ณธ๊ฐ’: ๋ชจ๋‹ฌ await this.openEditModal(config, rowData, context); } } /** * ํŽธ์ง‘ ๋ชจ๋‹ฌ ์—ด๊ธฐ */ private static async openEditModal( config: ButtonActionConfig, rowData: any, context: ButtonActionContext, isCreateMode: boolean = false, // ๐Ÿ†• ๋ณต์‚ฌ ๋ชจ๋“œ์—์„œ true๋กœ ์ „๋‹ฌ ): Promise { const { groupByColumns = [] } = config; // PK ๊ฐ’ ์ถ”์ถœ (์šฐ์„ ์ˆœ์œ„: id > ID > ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ) let primaryKeyValue: any; if (rowData.id !== undefined && rowData.id !== null) { primaryKeyValue = rowData.id; } else if (rowData.ID !== undefined && rowData.ID !== null) { primaryKeyValue = rowData.ID; } else { primaryKeyValue = Object.values(rowData)[0]; } // 1. config์— editModalDescription์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ let description = config.editModalDescription || ""; // 2. config์— ์—†์œผ๋ฉด ํ™”๋ฉด ์ •๋ณด์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ let screenInfo: any = null; if (config.targetScreenId) { try { screenInfo = await screenApi.getScreen(config.targetScreenId); if (!description) { description = screenInfo?.description || ""; } } catch (error) { console.warn("ํ™”๋ฉด ์„ค๋ช…์„ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค:", error); } } // ๐Ÿ†• ํ™”๋ฉด์ด ๋ถ„ํ•  ํŒจ๋„์„ ํฌํ•จํ•˜๋Š”์ง€ ํ™•์ธ (๋ ˆ์ด์•„์›ƒ์— screen-split-panel ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ๋Š”์ง€) let hasSplitPanel = false; if (config.targetScreenId) { try { const layoutData = await screenApi.getLayout(config.targetScreenId); if (layoutData?.components) { hasSplitPanel = layoutData.components.some( (comp: any) => comp.type === "screen-split-panel" || comp.componentType === "screen-split-panel" || comp.type === "split-panel-layout" || comp.componentType === "split-panel-layout", ); } // console.log("๐Ÿ” [openEditModal] ๋ถ„ํ•  ํŒจ๋„ ํ™•์ธ:", { // targetScreenId: config.targetScreenId, // hasSplitPanel, // componentTypes: layoutData?.components?.map((c: any) => c.type || c.componentType) || [], // }); } catch (error) { console.warn("๋ ˆ์ด์•„์›ƒ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค:", error); } } // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ํ™”๋ฉด์ธ ๊ฒฝ์šฐ ScreenModal ์‚ฌ์šฉ (editData ์ „๋‹ฌ) if (hasSplitPanel) { const screenModalEvent = new CustomEvent("openScreenModal", { detail: { screenId: config.targetScreenId, title: isCreateMode ? config.editModalTitle || "๋ฐ์ดํ„ฐ ๋ณต์‚ฌ" : config.editModalTitle || "๋ฐ์ดํ„ฐ ์ˆ˜์ •", description: description, size: config.modalSize || "lg", // ๐Ÿ”ง ๋ณต์‚ฌ ๋ชจ๋“œ์—์„œ๋Š” editData ๋Œ€์‹  splitPanelParentData๋กœ ์ „๋‹ฌํ•˜์—ฌ ์ฑ„๋ฒˆ์ด ์ƒ์„ฑ๋˜๋„๋ก ํ•จ editData: isCreateMode ? undefined : rowData, splitPanelParentData: isCreateMode ? rowData : undefined, isCreateMode: isCreateMode, // ๐Ÿ†• ๋ณต์‚ฌ ๋ชจ๋“œ ํ”Œ๋ž˜๊ทธ ์ „๋‹ฌ }, }); window.dispatchEvent(screenModalEvent); return; } // ๐Ÿ”ง ์ผ๋ฐ˜ ํ™”๋ฉด์€ EditModal ์‚ฌ์šฉ (groupByColumns๋Š” EditModal์—์„œ ์ฒ˜๋ฆฌ) const modalEvent = new CustomEvent("openEditModal", { detail: { screenId: config.targetScreenId, title: isCreateMode ? config.editModalTitle || "๋ฐ์ดํ„ฐ ๋ณต์‚ฌ" : config.editModalTitle || "๋ฐ์ดํ„ฐ ์ˆ˜์ •", description: description, modalSize: config.modalSize || "lg", editData: rowData, isCreateMode: isCreateMode, // ๐Ÿ†• ๋ณต์‚ฌ ๋ชจ๋“œ์—์„œ INSERT๋กœ ์ฒ˜๋ฆฌ๋˜๋„๋ก groupByColumns: groupByColumns.length > 0 ? groupByColumns : undefined, // ๐Ÿ†• ๊ทธ๋ฃนํ•‘ ์ปฌ๋Ÿผ ์ „๋‹ฌ tableName: context.tableName, // ๐Ÿ†• ํ…Œ์ด๋ธ”๋ช… ์ „๋‹ฌ buttonConfig: config, // ๐Ÿ†• ๋ฒ„ํŠผ ์„ค์ • ์ „๋‹ฌ (์ œ์–ด๋กœ์ง ์‹คํ–‰์šฉ) buttonContext: context, // ๐Ÿ†• ๋ฒ„ํŠผ ์ปจํ…์ŠคํŠธ ์ „๋‹ฌ (screenId, userId ๋“ฑ) onSave: () => { context.onRefresh?.(); }, }, }); window.dispatchEvent(modalEvent); } /** * ํŽธ์ง‘ ํ™”๋ฉด์œผ๋กœ ์ด๋™ */ private static navigateToEditScreen(config: ButtonActionConfig, rowData: any, context: ButtonActionContext): void { const rowId = rowData.id || rowData.objid || rowData.pk || rowData.ID || rowData.OBJID || rowData.PK; if (!rowId) { toast.error("์ˆ˜์ •ํ•  ํ•ญ๋ชฉ์˜ ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); return; } const editUrl = `/screens/${config.targetScreenId}?mode=edit&id=${rowId}`; window.location.href = editUrl; } /** * ๋ณต์‚ฌ ์•ก์…˜ ์ฒ˜๋ฆฌ (ํ’ˆ๋ชฉ์ฝ”๋“œ ์ดˆ๊ธฐํ™”) */ private static async handleCopy(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { const { selectedRowsData, flowSelectedData } = context; // ํ”Œ๋กœ์šฐ ์„ ํƒ ๋ฐ์ดํ„ฐ ์šฐ์„  ์‚ฌ์šฉ const dataToCopy = flowSelectedData && flowSelectedData.length > 0 ? flowSelectedData : selectedRowsData; // ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ if (!dataToCopy || dataToCopy.length === 0) { toast.error("๋ณต์‚ฌํ•  ํ•ญ๋ชฉ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return false; } // ๋ณต์‚ฌ ํ™”๋ฉด์ด ์„ค์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ if (!config.targetScreenId) { toast.error("๋ณต์‚ฌ ํผ ํ™”๋ฉด์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋ฒ„ํŠผ ์„ค์ •์—์„œ ๋ณต์‚ฌ ํผ ํ™”๋ฉด์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return false; } if (dataToCopy.length === 1) { // ๋‹จ์ผ ํ•ญ๋ชฉ ๋ณต์‚ฌ const rowData = dataToCopy[0]; // ๋ณต์‚ฌ ์‹œ ์ œ๊ฑฐํ•  ํ•„๋“œ๋“ค const copiedData = { ...rowData }; const fieldsToRemove = [ // ID ํ•„๋“œ (์ƒˆ ๋ ˆ์ฝ”๋“œ ์ƒ์„ฑ) "id", "ID", // ๋‚ ์งœ ํ•„๋“œ (์ž๋™ ์ƒ์„ฑ) "created_date", "createdDate", "updated_date", "updatedDate", "created_at", "createdAt", "updated_at", "updatedAt", "reg_date", "regDate", "mod_date", "modDate", ]; // ์ œ๊ฑฐํ•  ํ•„๋“œ ์‚ญ์ œ fieldsToRemove.forEach((field) => { if (copiedData[field] !== undefined) { delete copiedData[field]; } }); // ํ’ˆ๋ชฉ์ฝ”๋“œ ํ•„๋“œ ์ดˆ๊ธฐํ™” (์—ฌ๋Ÿฌ ๊ฐ€๋Šฅํ•œ ํ•„๋“œ๋ช… ํ™•์ธ) const itemCodeFields = [ "item_code", "itemCode", "item_no", "itemNo", "item_number", "itemNumber", "ํ’ˆ๋ชฉ์ฝ”๋“œ", "ํ’ˆ๋ฒˆ", "code", ]; // ๐Ÿ†• ํ™”๋ฉด ์„ค์ •์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™ ๊ฐ€์ ธ์˜ค๊ธฐ const screenNumberingRules: Record = {}; if (config.targetScreenId) { try { const { screenApi } = await import("@/lib/api/screen"); const layout = await screenApi.getLayout(config.targetScreenId); // ๋ ˆ์ด์•„์›ƒ์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์„ค์ •๋œ ์ปดํฌ๋„ŒํŠธ ์ฐพ๊ธฐ const findNumberingRules = (components: any[]): void => { for (const comp of components) { const compConfig = comp.componentConfig || {}; // text-input ์ปดํฌ๋„ŒํŠธ์˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ™•์ธ if ( compConfig.autoGeneration?.type === "numbering_rule" && compConfig.autoGeneration?.options?.numberingRuleId ) { const columnName = compConfig.columnName || comp.columnName; if (columnName) { screenNumberingRules[columnName] = compConfig.autoGeneration.options.numberingRuleId; } } // ์ค‘์ฒฉ๋œ ์ปดํฌ๋„ŒํŠธ ํ™•์ธ if (comp.children && Array.isArray(comp.children)) { findNumberingRules(comp.children); } } }; if (layout?.components) { findNumberingRules(layout.components); } } catch (error) { console.warn("โš ๏ธ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ ์‹คํŒจ:", error); } } // ํ’ˆ๋ชฉ์ฝ”๋“œ ํ•„๋“œ๋ฅผ ์ฐพ์•„์„œ ๋ฌด์กฐ๊ฑด ๊ณต๋ฐฑ์œผ๋กœ ์ดˆ๊ธฐํ™” let resetFieldName = ""; for (const field of itemCodeFields) { if (copiedData[field] !== undefined) { const originalValue = copiedData[field]; const ruleIdKey = `${field}_numberingRuleId`; // 1์ˆœ์œ„: ์›๋ณธ ๋ฐ์ดํ„ฐ์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™ ID ํ™•์ธ // 2์ˆœ์œ„: ํ™”๋ฉด ์„ค์ •์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™ ID ํ™•์ธ const numberingRuleId = rowData[ruleIdKey] || screenNumberingRules[field]; const hasNumberingRule = numberingRuleId !== undefined && numberingRuleId !== null && numberingRuleId !== ""; // ํ’ˆ๋ชฉ์ฝ”๋“œ๋ฅผ ๋ฌด์กฐ๊ฑด ๊ณต๋ฐฑ์œผ๋กœ ์ดˆ๊ธฐํ™” copiedData[field] = ""; // ์ฑ„๋ฒˆ ๊ทœ์น™ ID๊ฐ€ ์žˆ์œผ๋ฉด ๋ณต์‚ฌ (์ €์žฅ ์‹œ ์ž๋™ ์ƒ์„ฑ) if (hasNumberingRule) { copiedData[ruleIdKey] = numberingRuleId; } else { } resetFieldName = field; break; } } // ์ž‘์„ฑ์ž ์ •๋ณด๋ฅผ ํ˜„์žฌ ์‚ฌ์šฉ์ž๋กœ ๋ณ€๊ฒฝ const writerFields = ["writer", "creator", "reg_user", "regUser", "created_by", "createdBy"]; writerFields.forEach((field) => { if (copiedData[field] !== undefined && context.userId) { copiedData[field] = context.userId; } }); if (resetFieldName) { toast.success("๋ณต์‚ฌ๋ณธ์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ’ˆ๋ชฉ์ฝ”๋“œ๋Š” ์ €์žฅ ์‹œ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค."); } else { console.warn("โš ๏ธ ํ’ˆ๋ชฉ์ฝ”๋“œ ํ•„๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค."); console.warn("โš ๏ธ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ•„๋“œ:", Object.keys(copiedData)); toast.info("๋ณต์‚ฌ๋ณธ์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค."); } await this.openCopyForm(config, copiedData, context); } else { // ๋‹ค์ค‘ ํ•ญ๋ชฉ ๋ณต์‚ฌ - ํ˜„์žฌ๋Š” ๋‹จ์ผ ๋ณต์‚ฌ๋งŒ ์ง€์› toast.error("ํ˜„์žฌ ๋‹จ์ผ ํ•ญ๋ชฉ ๋ณต์‚ฌ๋งŒ ์ง€์›๋ฉ๋‹ˆ๋‹ค. ํ•˜๋‚˜์˜ ํ•ญ๋ชฉ๋งŒ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return false; } return true; } catch (error: any) { console.error("โŒ ๋ณต์‚ฌ ์•ก์…˜ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜:", error); toast.error(`๋ณต์‚ฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ${error.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"}`); return false; } } /** * ๋ณต์‚ฌ ํผ ์—ด๊ธฐ (๋‹จ์ผ ํ•ญ๋ชฉ) */ private static async openCopyForm( config: ButtonActionConfig, rowData: any, context: ButtonActionContext, ): Promise { try { const editMode = config.editMode || "modal"; switch (editMode) { case "modal": // ๋ชจ๋‹ฌ๋กœ ๋ณต์‚ฌ ํผ ์—ด๊ธฐ (ํŽธ์ง‘ ๋ชจ๋‹ฌ ์žฌ์‚ฌ์šฉ, INSERT ๋ชจ๋“œ๋กœ) await this.openEditModal(config, rowData, context, true); // ๐Ÿ†• isCreateMode: true break; case "navigate": // ์ƒˆ ํŽ˜์ด์ง€๋กœ ์ด๋™ this.navigateToCopyScreen(config, rowData, context); break; default: // ๊ธฐ๋ณธ๊ฐ’: ๋ชจ๋‹ฌ this.openEditModal(config, rowData, context, true); // ๐Ÿ†• isCreateMode: true } } catch (error: any) { console.error("โŒ openCopyForm ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜:", error); throw error; } } /** * ๋ณต์‚ฌ ํ™”๋ฉด์œผ๋กœ ๋„ค๋น„๊ฒŒ์ด์…˜ */ private static navigateToCopyScreen(config: ButtonActionConfig, rowData: any, context: ButtonActionContext): void { const copyUrl = `/screens/${config.targetScreenId}?mode=copy`; // ๋ณต์‚ฌํ•  ๋ฐ์ดํ„ฐ๋ฅผ sessionStorage์— ์ €์žฅ sessionStorage.setItem("copyData", JSON.stringify(rowData)); window.location.href = copyUrl; } /** * ๋‹ซ๊ธฐ ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static handleClose(config: ButtonActionConfig, context: ButtonActionContext): boolean { console.log("๋‹ซ๊ธฐ ์•ก์…˜ ์‹คํ–‰:", context); context.onClose?.(); return true; } /** * ์ œ์–ด ์ „์šฉ ์•ก์…˜ ์ฒ˜๋ฆฌ (์กฐ๊ฑด ์ฒดํฌ๋งŒ ์ˆ˜ํ–‰) */ private static async handleControl(config: ButtonActionConfig, context: ButtonActionContext): Promise { // ๐Ÿ”ฅ ์ œ์–ด ์กฐ๊ฑด์ด ์„ค์ •๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ if (!config.dataflowConfig || !config.enableDataflowControl) { console.warn("โš ๏ธ ์ œ์–ด๊ด€๋ฆฌ๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:", { enableDataflowControl: config.enableDataflowControl, hasDataflowConfig: !!config.dataflowConfig, }); toast.warning( "์ œ์–ด๊ด€๋ฆฌ๊ฐ€ ํ™œ์„ฑํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋ฒ„ํŠผ ์„ค์ •์—์„œ '์ œ์–ด๊ด€๋ฆฌ ํ™œ์„ฑํ™”'๋ฅผ ์ฒดํฌํ•˜๊ณ  ์กฐ๊ฑด์„ ์„ค์ •ํ•ด์ฃผ์„ธ์š”.", ); return false; } try { // ๐Ÿ”ฅ ํ™•์žฅ๋œ ์ œ์–ด ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ // ์ž๋™์œผ๋กœ ์ ์ ˆํ•œ controlDataSource ๊ฒฐ์ • let controlDataSource = config.dataflowConfig.controlDataSource; if (!controlDataSource) { // ์„ค์ •์ด ์—†์œผ๋ฉด ์ž๋™ ํŒ๋‹จ (์šฐ์„ ์ˆœ์œ„ ์ˆœ์„œ๋Œ€๋กœ) if (context.flowSelectedData && context.flowSelectedData.length > 0) { controlDataSource = "flow-selection"; } else if (context.selectedRowsData && context.selectedRowsData.length > 0) { controlDataSource = "table-selection"; } else if (context.formData && Object.keys(context.formData).length > 0) { controlDataSource = "form"; } else { controlDataSource = "form"; // ๊ธฐ๋ณธ๊ฐ’ } } const extendedContext: ExtendedControlContext = { formData: context.formData || {}, selectedRows: context.selectedRows || [], selectedRowsData: context.selectedRowsData || [], flowSelectedData: context.flowSelectedData || [], flowSelectedStepId: context.flowSelectedStepId, controlDataSource, }; // ๐Ÿ”ฅ ์ƒˆ๋กœ์šด ๋ฒ„ํŠผ ์•ก์…˜ ์‹คํ–‰ ์‹œ์Šคํ…œ ์‚ฌ์šฉ // flowConfig๊ฐ€ ์žˆ์œผ๋ฉด controlMode๊ฐ€ ๋ช…์‹œ๋˜์ง€ ์•Š์•„๋„ ํ”Œ๋กœ์šฐ ๋ชจ๋“œ๋กœ ๊ฐ„์ฃผ const hasFlowConfig = config.dataflowConfig?.flowConfig && config.dataflowConfig.flowConfig.flowId; const isFlowMode = config.dataflowConfig?.controlMode === "flow" || hasFlowConfig; if (isFlowMode && config.dataflowConfig?.flowConfig) { const { flowId, executionTiming } = config.dataflowConfig.flowConfig; if (!flowId) { console.error("โŒ ํ”Œ๋กœ์šฐ ID๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"); toast.error("ํ”Œ๋กœ์šฐ๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); return false; } try { // ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์‹คํ–‰ API ํ˜ธ์ถœ (API ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ) const { executeNodeFlow } = await import("@/lib/api/nodeFlows"); // ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ค€๋น„: controlDataSource ์„ค์ • ๊ธฐ๋ฐ˜ let sourceData: any = null; let dataSourceType: string = controlDataSource || "none"; // controlDataSource ์„ค์ •์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ์„ ํƒ switch (controlDataSource) { case "flow-selection": if (context.flowSelectedData && context.flowSelectedData.length > 0) { sourceData = context.flowSelectedData; console.log("๐ŸŒŠ ํ”Œ๋กœ์šฐ ์„ ํƒ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ:", { stepId: context.flowSelectedStepId, dataCount: sourceData.length, sourceData, }); } else { console.warn("โš ๏ธ flow-selection ๋ชจ๋“œ์ด์ง€๋งŒ ์„ ํƒ๋œ ํ”Œ๋กœ์šฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); toast.error("ํ”Œ๋กœ์šฐ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋จผ์ € ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return false; } break; case "table-selection": if (context.selectedRowsData && context.selectedRowsData.length > 0) { sourceData = context.selectedRowsData; } else { console.warn("โš ๏ธ table-selection ๋ชจ๋“œ์ด์ง€๋งŒ ์„ ํƒ๋œ ํ–‰์ด ์—†์Šต๋‹ˆ๋‹ค."); toast.error("ํ…Œ์ด๋ธ”์—์„œ ์ฒ˜๋ฆฌํ•  ํ•ญ๋ชฉ์„ ๋จผ์ € ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return false; } break; case "form": if (context.formData && Object.keys(context.formData).length > 0) { // ๐Ÿ”ง ๋ฐฐ์—ด ๊ฐ’์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ sourceData = [normalizeFormDataArrays(context.formData)]; } else { console.warn("โš ๏ธ form ๋ชจ๋“œ์ด์ง€๋งŒ ํผ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); } break; case "both": // ํผ + ํ…Œ์ด๋ธ” ์„ ํƒ (๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ) // ๐Ÿ”ฅ ๊ฐ selectedRowsData ํ•ญ๋ชฉ์— formData๋ฅผ ๋ณ‘ํ•ฉํ•˜์—ฌ ์ „๋‹ฌ // ์ด๋ ‡๊ฒŒ ํ•ด์•ผ ๋ฉ”์ผ ๋ฐœ์†ก ์‹œ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ(์ƒํ’ˆ๋ช… ๋“ฑ)์™€ ํผ ๋ฐ์ดํ„ฐ(์ˆ˜์‹ ์ž ๋“ฑ)๊ฐ€ ๋ชจ๋‘ ๋ณ€์ˆ˜๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ if (context.selectedRowsData && context.selectedRowsData.length > 0) { sourceData = context.selectedRowsData.map((row: any) => ({ ...row, ...(context.formData || {}), })); console.log("๐Ÿ”€ ํผ + ํ…Œ์ด๋ธ” ์„ ํƒ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ:", { dataCount: sourceData.length, sourceData, }); } else if (context.formData && Object.keys(context.formData).length > 0) { sourceData = [context.formData]; console.log("๐Ÿ”€ ํผ ๋ฐ์ดํ„ฐ๋งŒ ์‚ฌ์šฉ (์„ ํƒ๋œ ํ–‰ ์—†์Œ):", sourceData); } break; default: // ์ž๋™ ํŒ๋‹จ (์„ค์ •์ด ์—†๋Š” ๊ฒฝ์šฐ) if (context.flowSelectedData && context.flowSelectedData.length > 0) { sourceData = context.flowSelectedData; dataSourceType = "flow-selection"; console.log("๐ŸŒŠ [์ž๋™] ํ”Œ๋กœ์šฐ ์„ ํƒ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ"); } else if (context.selectedRowsData && context.selectedRowsData.length > 0) { // ๐Ÿ”ฅ selectedRowsData๊ฐ€ ์žˆ์œผ๋ฉด formData๋„ ํ•จ๊ป˜ ๋ณ‘ํ•ฉ // ๋ชจ๋‹ฌ์—์„œ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ(selectedRowsData)์™€ ํผ ์ž…๋ ฅ(formData)์„ ๋ชจ๋‘ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก if (context.formData && Object.keys(context.formData).length > 0) { sourceData = context.selectedRowsData.map((row: any) => ({ ...row, ...context.formData, })); dataSourceType = "both"; } else { sourceData = context.selectedRowsData; dataSourceType = "table-selection"; } } else if (context.formData && Object.keys(context.formData).length > 0) { // ๐Ÿ”ง ๋ฐฐ์—ด ๊ฐ’์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ sourceData = [normalizeFormDataArrays(context.formData)]; dataSourceType = "form"; } break; } const result = await executeNodeFlow(flowId, { dataSourceType, sourceData, context, }); if (result.success) { toast.success("ํ”Œ๋กœ์šฐ ์‹คํ–‰์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); // ํ”Œ๋กœ์šฐ ์ƒˆ๋กœ๊ณ ์นจ (ํ”Œ๋กœ์šฐ ์œ„์ ฏ์šฉ) if (context.onFlowRefresh) { context.onFlowRefresh(); } // ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ (์ผ๋ฐ˜ ํ…Œ์ด๋ธ”์šฉ) if (context.onRefresh) { context.onRefresh(); } return true; } else { console.error("โŒ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์‹คํ–‰ ์‹คํŒจ:", result); toast.error(config.errorMessage || result.message || "ํ”Œ๋กœ์šฐ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } catch (error) { console.error("โŒ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์‹คํ–‰ ์˜ค๋ฅ˜:", error); toast.error("ํ”Œ๋กœ์šฐ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } else if (config.dataflowConfig?.controlMode === "relationship" && config.dataflowConfig?.relationshipConfig) { // ๐Ÿ”ฅ table-selection ๋ชจ๋“œ์ผ ๋•Œ ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ๋ฅผ formData์— ๋ณ‘ํ•ฉ let mergedFormData = { ...context.formData } || {}; if ( controlDataSource === "table-selection" && context.selectedRowsData && context.selectedRowsData.length > 0 ) { // ์„ ํƒ๋œ ์ฒซ ๋ฒˆ์งธ ํ–‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ formData์— ๋ณ‘ํ•ฉ const selectedRowData = context.selectedRowsData[0]; mergedFormData = { ...mergedFormData, ...selectedRowData }; } // ์ƒˆ๋กœ์šด ImprovedButtonActionExecutor ์‚ฌ์šฉ const buttonConfig = { actionType: config.type, dataflowConfig: config.dataflowConfig, enableDataflowControl: true, // ๊ด€๊ณ„ ๊ธฐ๋ฐ˜ ์ œ์–ด๊ฐ€ ์„ค์ •๋œ ๊ฒฝ์šฐ ํ™œ์„ฑํ™” }; const executionResult = await ImprovedButtonActionExecutor.executeButtonAction(buttonConfig, mergedFormData, { buttonId: context.buttonId || "unknown", screenId: context.screenId || "unknown", userId: context.userId || "unknown", companyCode: context.companyCode || "*", startTime: Date.now(), contextData: context, }); if (executionResult.success) { toast.success(config.successMessage || "๊ด€๊ณ„ ์‹คํ–‰์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); // ์ƒˆ๋กœ๊ณ ์นจ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ if (context.onRefresh) { context.onRefresh(); } return true; } else { console.error("โŒ ๊ด€๊ณ„ ์‹คํ–‰ ์‹คํŒจ:", executionResult); toast.error(config.errorMessage || "๊ด€๊ณ„ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } else { // ์ œ์–ด ์—†์Œ - ์„ฑ๊ณต ์ฒ˜๋ฆฌ // ์ƒˆ๋กœ๊ณ ์นจ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ if (context.onRefresh) { context.onRefresh(); } return true; } } catch (error) { console.error("์ œ์–ด ์กฐ๊ฑด ๊ฒ€์ฆ ์ค‘ ์˜ค๋ฅ˜:", error); toast.error("์ œ์–ด ์กฐ๊ฑด ๊ฒ€์ฆ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } /** * ์ €์žฅ ํ›„ ์ œ์–ด ์‹คํ–‰ (After Timing) * EditModal ๋“ฑ ์™ธ๋ถ€์—์„œ๋„ ํ˜ธ์ถœ ๊ฐ€๋Šฅํ•˜๋„๋ก public์œผ๋กœ ๋ณ€๊ฒฝ * ๋‹ค์ค‘ ์ œ์–ด ์ˆœ์ฐจ ์‹คํ–‰ ์ง€์› */ public static async executeAfterSaveControl(config: ButtonActionConfig, context: ButtonActionContext): Promise { // ์ œ์–ด ๋ฐ์ดํ„ฐ ์†Œ์Šค ๊ฒฐ์ • let controlDataSource = config.dataflowConfig?.controlDataSource; if (!controlDataSource) { controlDataSource = "form"; // ์ €์žฅ ํ›„์—๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ form ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ } const extendedContext: ExtendedControlContext = { formData: context.formData || {}, selectedRows: context.selectedRows || [], selectedRowsData: context.selectedRowsData || [], controlDataSource, }; // ๐Ÿ”ฅ ๋‹ค์ค‘ ์ œ์–ด ์ง€์› (flowControls ๋ฐฐ์—ด) const flowControls = config.dataflowConfig?.flowControls || []; if (flowControls.length > 0) { // ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ const sortedControls = [...flowControls].sort((a: any, b: any) => (a.order || 0) - (b.order || 0)); // ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์‹คํ–‰ API const { executeNodeFlow } = await import("@/lib/api/nodeFlows"); // ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ค€๋น„: context-data ๋ชจ๋“œ๋Š” ๋ฐฐ์—ด์„ ๊ธฐ๋Œ€ํ•จ // ๐Ÿ”ง ์ €์žฅ ํ›„ ์ œ์–ด: savedData > formData > selectedRowsData // - ์ €์žฅ ํ›„ ์ œ์–ด์—์„œ๋Š” ๋ฐฉ๊ธˆ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ(savedData)๊ฐ€ ๊ฐ€์žฅ ์ค‘์š”! // - selectedRowsData๋Š” ์™ผ์ชฝ ํŒจ๋„ ์„ ํƒ ๋ฐ์ดํ„ฐ์ผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋งˆ์ง€๋ง‰ ์ˆœ์œ„ let sourceData: any[]; if (context.savedData) { // ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ (์ €์žฅ API ์‘๋‹ต) sourceData = Array.isArray(context.savedData) ? context.savedData : [context.savedData]; console.log("๐Ÿ“ฆ [executeAfterSaveControl] savedData ์‚ฌ์šฉ:", sourceData); console.log("๐Ÿ“ฆ [executeAfterSaveControl] savedData ํ•„๋“œ:", Object.keys(context.savedData)); console.log("๐Ÿ“ฆ [executeAfterSaveControl] savedData.sabun:", context.savedData.sabun); } else if (context.formData && Object.keys(context.formData).length > 0) { // ํผ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ (๐Ÿ”ง ๋ฐฐ์—ด ๊ฐ’์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜) sourceData = [normalizeFormDataArrays(context.formData)]; console.log("๐Ÿ“ฆ [executeAfterSaveControl] formData ์‚ฌ์šฉ:", sourceData); } else if (context.selectedRowsData && context.selectedRowsData.length > 0) { // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ (๋งˆ์ง€๋ง‰ ์ˆœ์œ„) sourceData = context.selectedRowsData; console.log("๐Ÿ“ฆ [executeAfterSaveControl] selectedRowsData ์‚ฌ์šฉ:", sourceData); } else { sourceData = []; console.warn("โš ๏ธ [executeAfterSaveControl] ๋ฐ์ดํ„ฐ ์†Œ์Šค ์—†์Œ!"); } let allSuccess = true; const results: Array<{ flowId: number; flowName: string; success: boolean; message?: string }> = []; for (let i = 0; i < sortedControls.length; i++) { const control = sortedControls[i]; // ์œ ํšจํ•˜์ง€ ์•Š์€ flowId ์Šคํ‚ต if (!control.flowId || control.flowId <= 0) { console.warn(`โš ๏ธ [${i + 1}/${sortedControls.length}] ์œ ํšจํ•˜์ง€ ์•Š์€ flowId, ์Šคํ‚ต:`, control); continue; } // executionTiming ์ฒดํฌ (after๋งŒ ์‹คํ–‰) if (control.executionTiming && control.executionTiming !== "after") { continue; } console.log( `\n๐Ÿ“ [${i + 1}/${sortedControls.length}] ์ œ์–ด ์‹คํ–‰: ${control.flowName} (flowId: ${control.flowId})`, ); try { const result = await executeNodeFlow(control.flowId, { dataSourceType: controlDataSource, sourceData, context: extendedContext, }); results.push({ flowId: control.flowId, flowName: control.flowName, success: result.success, message: result.message, }); if (result.success) { } else { console.error(`โŒ [${i + 1}/${sortedControls.length}] ์ œ์–ด ์‹คํŒจ: ${control.flowName} - ${result.message}`); allSuccess = false; // ์ด์ „ ์ œ์–ด ์‹คํŒจ ์‹œ ๋‹ค์Œ ์ œ์–ด ์‹คํ–‰ ์ค‘๋‹จ console.warn("โš ๏ธ ์ด์ „ ์ œ์–ด ์‹คํŒจ๋กœ ์ธํ•ด ๋‚˜๋จธ์ง€ ์ œ์–ด ์‹คํ–‰ ์ค‘๋‹จ"); break; } } catch (error: any) { console.error(`โŒ [${i + 1}/${sortedControls.length}] ์ œ์–ด ์‹คํ–‰ ์˜ค๋ฅ˜: ${control.flowName}`, error); results.push({ flowId: control.flowId, flowName: control.flowName, success: false, message: error.message, }); allSuccess = false; break; } } // ๊ฒฐ๊ณผ ์š”์•ฝ const successCount = results.filter((r) => r.success).length; const failCount = results.filter((r) => !r.success).length; console.log("\n๐Ÿ“Š ๋‹ค์ค‘ ์ œ์–ด ์‹คํ–‰ ์™„๋ฃŒ:", { total: sortedControls.length, executed: results.length, success: successCount, failed: failCount, }); if (allSuccess) { toast.success(`${successCount}๊ฐœ ์ œ์–ด ์‹คํ–‰ ์™„๋ฃŒ`); } else { toast.error(`์ œ์–ด ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ (${successCount}/${results.length} ์„ฑ๊ณต)`); } return; } // ๐Ÿ”ฅ ๊ธฐ์กด ๋‹จ์ผ ์ œ์–ด ์‹คํ–‰ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ) // dataflowTiming์ด 'after'๊ฐ€ ์•„๋‹ˆ๋ฉด ์‹คํ–‰ํ•˜์ง€ ์•Š์Œ if (config.dataflowTiming && config.dataflowTiming !== "after") { return; } // ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ๋ฐฉ์‹ ์‹คํ–‰ (flowConfig๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ) const hasFlowConfig = config.dataflowConfig?.flowConfig && config.dataflowConfig.flowConfig.flowId; if (hasFlowConfig) { // executionTiming ์ฒดํฌ const flowTiming = config.dataflowConfig.flowConfig.executionTiming; if (flowTiming && flowTiming !== "after") { return; } const { flowId } = config.dataflowConfig.flowConfig; try { // ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์‹คํ–‰ API ํ˜ธ์ถœ const { executeNodeFlow } = await import("@/lib/api/nodeFlows"); // ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ค€๋น„: context-data ๋ชจ๋“œ๋Š” ๋ฐฐ์—ด์„ ๊ธฐ๋Œ€ํ•จ // ๐Ÿ”ง ์ €์žฅ ํ›„ ์ œ์–ด: savedData > formData > selectedRowsData // - ์ €์žฅ ํ›„ ์ œ์–ด์—์„œ๋Š” ๋ฐฉ๊ธˆ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ(savedData)๊ฐ€ ๊ฐ€์žฅ ์ค‘์š”! // - selectedRowsData๋Š” ์™ผ์ชฝ ํŒจ๋„ ์„ ํƒ ๋ฐ์ดํ„ฐ์ผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋งˆ์ง€๋ง‰ ์ˆœ์œ„ let sourceData: any[]; if (context.savedData) { // ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ (์ €์žฅ API ์‘๋‹ต) sourceData = Array.isArray(context.savedData) ? context.savedData : [context.savedData]; console.log("๐Ÿ“ฆ [executeSingleFlowControl] savedData ์‚ฌ์šฉ:", sourceData); } else if (context.formData && Object.keys(context.formData).length > 0) { // ํผ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ (๐Ÿ”ง ๋ฐฐ์—ด ๊ฐ’์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜) sourceData = [normalizeFormDataArrays(context.formData)]; console.log("๐Ÿ“ฆ [executeSingleFlowControl] formData ์‚ฌ์šฉ:", sourceData); } else if (context.selectedRowsData && context.selectedRowsData.length > 0) { // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ (๋งˆ์ง€๋ง‰ ์ˆœ์œ„) sourceData = context.selectedRowsData; console.log("๐Ÿ“ฆ [executeSingleFlowControl] selectedRowsData ์‚ฌ์šฉ:", sourceData); } else { sourceData = []; console.warn("โš ๏ธ [executeSingleFlowControl] ๋ฐ์ดํ„ฐ ์†Œ์Šค ์—†์Œ!"); } // repeat-screen-modal ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋ณ‘ํ•ฉ const repeatScreenModalKeys = Object.keys(context.formData || {}).filter((key) => key.startsWith("_repeatScreenModal_"), ); if (repeatScreenModalKeys.length > 0) { } const result = await executeNodeFlow(flowId, { dataSourceType: controlDataSource, sourceData, context: extendedContext, }); if (result.success) { toast.success("์ œ์–ด ๋กœ์ง ์‹คํ–‰์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); } else { console.error("โŒ ์ €์žฅ ํ›„ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์‹คํ–‰ ์‹คํŒจ:", result); toast.error("์ €์žฅ์€ ์™„๋ฃŒ๋˜์—ˆ์œผ๋‚˜ ์ œ์–ด ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); } } catch (error: any) { console.error("โŒ ์ €์žฅ ํ›„ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์‹คํ–‰ ์˜ค๋ฅ˜:", error); toast.error(`์ œ์–ด ์‹คํ–‰ ์˜ค๋ฅ˜: ${error.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"}`); } return; // ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์‹คํ–‰ ํ›„ ์ข…๋ฃŒ } // ๊ด€๊ณ„ ๊ธฐ๋ฐ˜ ์ œ์–ด ์‹คํ–‰ if (config.dataflowConfig?.controlMode === "relationship" && config.dataflowConfig?.relationshipConfig) { const buttonConfig = { actionType: config.type, dataflowConfig: config.dataflowConfig, enableDataflowControl: true, }; const executionResult = await ImprovedButtonActionExecutor.executeButtonAction( buttonConfig, context.formData || {}, { buttonId: context.buttonId || "unknown", screenId: context.screenId || "unknown", userId: context.userId || "unknown", companyCode: context.companyCode || "*", startTime: Date.now(), contextData: context, }, ); if (executionResult.success) { // ์„ฑ๊ณต ํ† ์ŠคํŠธ๋Š” save ์•ก์…˜์—์„œ ์ด๋ฏธ ํ‘œ์‹œํ–ˆ์œผ๋ฏ€๋กœ ์ถ”๊ฐ€๋กœ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ } else { console.error("โŒ ์ €์žฅ ํ›„ ์ œ์–ด ์‹คํ–‰ ์‹คํŒจ:", executionResult); toast.error("์ €์žฅ์€ ์™„๋ฃŒ๋˜์—ˆ์œผ๋‚˜ ์—ฐ๊ฒฐ๋œ ์ œ์–ด ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); } } } /** * ๊ด€๊ณ„๋„์—์„œ ๊ฐ€์ ธ์˜จ ์•ก์…˜๋“ค์„ ์‹คํ–‰ */ private static async executeRelationshipActions(actions: any[], context: ButtonActionContext): Promise { for (let i = 0; i < actions.length; i++) { const action = actions[i]; try { const actionType = action.actionType || action.type; // actionType ์šฐ์„ , type ํด๋ฐฑ switch (actionType) { case "save": await this.executeActionSave(action, context); break; case "update": await this.executeActionUpdate(action, context); break; case "delete": await this.executeActionDelete(action, context); break; case "insert": await this.executeActionInsert(action, context); break; default: console.warn(`โŒ ์ง€์›๋˜์ง€ ์•Š๋Š” ์•ก์…˜ ํƒ€์ž… (${i + 1}/${actions.length}):`, { actionType, actionName: action.name, fullAction: action, }); // ์ง€์›๋˜์ง€ ์•Š๋Š” ์•ก์…˜์€ ์˜ค๋ฅ˜๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ ์ค‘๋‹จ throw new Error(`์ง€์›๋˜์ง€ ์•Š๋Š” ์•ก์…˜ ํƒ€์ž…: ${actionType}`); } // ์„ฑ๊ณต ํ† ์ŠคํŠธ (๊ฐœ๋ณ„ ์•ก์…˜๋ณ„) toast.success(`${action.name || `์•ก์…˜ ${i + 1}`} ์™„๋ฃŒ`); } catch (error) { const actionType = action.actionType || action.type; console.error(`โŒ ์•ก์…˜ ${i + 1}/${actions.length} ์‹คํ–‰ ์‹คํŒจ:`, action.name, error); // ์‹คํŒจ ํ† ์ŠคํŠธ toast.error( `${action.name || `์•ก์…˜ ${i + 1}`} ์‹คํ–‰ ์‹คํŒจ: ${error instanceof Error ? error.message : "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"}`, ); // ๐Ÿšจ ์ˆœ์ฐจ ์‹คํ–‰ ์ค‘๋‹จ: ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจํ•˜๋ฉด ์ „์ฒด ์ค‘๋‹จ throw new Error( `์•ก์…˜ ${i + 1}(${action.name})์—์„œ ์‹คํŒจํ•˜์—ฌ ์ œ์–ด ํ”„๋กœ์„ธ์Šค๋ฅผ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค: ${error instanceof Error ? error.message : error}`, ); } } toast.success(`์ด ${actions.length}๊ฐœ ์•ก์…˜์ด ๋ชจ๋‘ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); } /** * ์ €์žฅ ์•ก์…˜ ์‹คํ–‰ */ private static async executeActionSave(action: any, context: ButtonActionContext): Promise { // ๐ŸŽฏ ํ•„๋“œ ๋งคํ•‘ ์ •๋ณด ์‚ฌ์šฉํ•˜์—ฌ ์ €์žฅ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ let saveData: Record = {}; // ์•ก์…˜์— ํ•„๋“œ ๋งคํ•‘ ์ •๋ณด๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ if (action.fieldMappings && Array.isArray(action.fieldMappings)) { // ํ•„๋“œ ๋งคํ•‘์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ action.fieldMappings.forEach((mapping: any) => { const { sourceField, targetField, defaultValue, valueType } = mapping; let value: any; // ๊ฐ’ ์†Œ์Šค์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ if (valueType === "form" && context.formData && sourceField) { value = context.formData[sourceField]; } else if (valueType === "selected" && context.selectedRowsData?.[0] && sourceField) { value = context.selectedRowsData[0][sourceField]; } else if (valueType === "default" || !sourceField) { value = defaultValue; } // ํƒ€๊ฒŸ ํ•„๋“œ์— ๊ฐ’ ์„ค์ • if (targetField && value !== undefined) { saveData[targetField] = value; } }); } else { console.log("โš ๏ธ ํ•„๋“œ ๋งคํ•‘ ์ •๋ณด๊ฐ€ ์—†์Œ, ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ"); // ํด๋ฐฑ: ๊ธฐ์กด ๋ฐฉ์‹ saveData = { ...context.formData, ...context.selectedRowsData?.[0], // ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๋„ ํฌํ•จ }; } try { // ๐Ÿ”ฅ ์‹ค์ œ ์ €์žฅ API ํ˜ธ์ถœ if (!context.tableName) { throw new Error("ํ…Œ์ด๋ธ”๋ช…์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); } const result = await DynamicFormApi.saveFormData({ screenId: 0, // ์ž„์‹œ๊ฐ’ tableName: context.tableName, data: saveData, }); if (result.success) { toast.success("๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); } else { throw new Error(result.message || "์ €์žฅ ์‹คํŒจ"); } } catch (error) { console.error("โŒ ์ €์žฅ ์‹คํŒจ:", error); toast.error(`์ €์žฅ ์‹คํŒจ: ${error.message}`); throw error; } } /** * ์—…๋ฐ์ดํŠธ ์•ก์…˜ ์‹คํ–‰ */ private static async executeActionUpdate(action: any, context: ButtonActionContext): Promise { // ๐ŸŽฏ ํ•„๋“œ ๋งคํ•‘ ์ •๋ณด ์‚ฌ์šฉํ•˜์—ฌ ์—…๋ฐ์ดํŠธ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ let updateData: Record = {}; // ์•ก์…˜์— ํ•„๋“œ ๋งคํ•‘ ์ •๋ณด๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ if (action.fieldMappings && Array.isArray(action.fieldMappings)) { // ๐Ÿ”‘ ๋จผ์ € ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ์˜ ๋ชจ๋“  ํ•„๋“œ๋ฅผ ๊ธฐ๋ณธ์œผ๋กœ ํฌํ•จ (๊ธฐ๋ณธํ‚ค ๋ณด์กด) if (context.selectedRowsData?.[0]) { updateData = { ...context.selectedRowsData[0] }; } // ํ•„๋“œ ๋งคํ•‘์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ (๋ฎ์–ด์“ฐ๊ธฐ) action.fieldMappings.forEach((mapping: any) => { const { sourceField, targetField, defaultValue, valueType } = mapping; let value: any; // ๊ฐ’ ์†Œ์Šค์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ if (valueType === "form" && context.formData && sourceField) { value = context.formData[sourceField]; } else if (valueType === "selected" && context.selectedRowsData?.[0] && sourceField) { value = context.selectedRowsData[0][sourceField]; } else if (valueType === "default" || !sourceField) { value = defaultValue; } // ํƒ€๊ฒŸ ํ•„๋“œ์— ๊ฐ’ ์„ค์ • (๋ฎ์–ด์“ฐ๊ธฐ) if (targetField && value !== undefined) { updateData[targetField] = value; } }); } else { console.log("โš ๏ธ ํ•„๋“œ ๋งคํ•‘ ์ •๋ณด๊ฐ€ ์—†์Œ, ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ"); // ํด๋ฐฑ: ๊ธฐ์กด ๋ฐฉ์‹ updateData = { ...context.formData, ...context.selectedRowsData?.[0], }; } try { // ๐Ÿ”ฅ ์‹ค์ œ ์—…๋ฐ์ดํŠธ API ํ˜ธ์ถœ if (!context.tableName) { throw new Error("ํ…Œ์ด๋ธ”๋ช…์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); } // ๋จผ์ € ID ์ฐพ๊ธฐ const primaryKeysResult = await DynamicFormApi.getTablePrimaryKeys(context.tableName); let updateId: string | undefined; if (primaryKeysResult.success && primaryKeysResult.data && primaryKeysResult.data.length > 0) { updateId = updateData[primaryKeysResult.data[0]]; } if (!updateId) { // ํด๋ฐฑ: ์ผ๋ฐ˜์ ์ธ ID ํ•„๋“œ๋“ค ํ™•์ธ const commonIdFields = ["id", "objid", "pk", "sales_no", "contract_no"]; for (const field of commonIdFields) { if (updateData[field]) { updateId = updateData[field]; break; } } } if (!updateId) { throw new Error("์—…๋ฐ์ดํŠธํ•  ํ•ญ๋ชฉ์˜ ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } const result = await DynamicFormApi.updateFormData(updateId, { tableName: context.tableName, data: updateData, }); if (result.success) { toast.success("๋ฐ์ดํ„ฐ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); } else { throw new Error(result.message || "์—…๋ฐ์ดํŠธ ์‹คํŒจ"); } } catch (error) { console.error("โŒ ์—…๋ฐ์ดํŠธ ์‹คํŒจ:", error); toast.error(`์—…๋ฐ์ดํŠธ ์‹คํŒจ: ${error.message}`); throw error; } } /** * ์‚ญ์ œ ์•ก์…˜ ์‹คํ–‰ */ private static async executeActionDelete(action: any, context: ButtonActionContext): Promise { // ์‹ค์ œ ์‚ญ์ œ ๋กœ์ง (๊ธฐ์กด handleDelete์™€ ์œ ์‚ฌ) if (!context.selectedRowsData || context.selectedRowsData.length === 0) { throw new Error("์‚ญ์ œํ•  ํ•ญ๋ชฉ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); } const deleteData = context.selectedRowsData[0]; console.log("์‚ญ์ œํ•  ๋ฐ์ดํ„ฐ:", deleteData); try { // ๐Ÿ”ฅ ์‹ค์ œ ์‚ญ์ œ API ํ˜ธ์ถœ if (!context.tableName) { throw new Error("ํ…Œ์ด๋ธ”๋ช…์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); } // ๊ธฐ์กด handleDelete์™€ ๋™์ผํ•œ ๋กœ์ง์œผ๋กœ ID ์ฐพ๊ธฐ const primaryKeysResult = await DynamicFormApi.getTablePrimaryKeys(context.tableName); let deleteId: string | undefined; if (primaryKeysResult.success && primaryKeysResult.data && primaryKeysResult.data.length > 0) { deleteId = deleteData[primaryKeysResult.data[0]]; } if (!deleteId) { // ํด๋ฐฑ: ์ผ๋ฐ˜์ ์ธ ID ํ•„๋“œ๋“ค ํ™•์ธ const commonIdFields = ["id", "objid", "pk", "sales_no", "contract_no"]; for (const field of commonIdFields) { if (deleteData[field]) { deleteId = deleteData[field]; break; } } } if (!deleteId) { throw new Error("์‚ญ์ œํ•  ํ•ญ๋ชฉ์˜ ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } // screenId ์ „๋‹ฌํ•˜์—ฌ ์ œ์–ด๊ด€๋ฆฌ ์‹คํ–‰ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•จ const result = await DynamicFormApi.deleteFormDataFromTable(deleteId, context.tableName, context.screenId); if (result.success) { toast.success("๋ฐ์ดํ„ฐ๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); } else { throw new Error(result.message || "์‚ญ์ œ ์‹คํŒจ"); } } catch (error) { console.error("โŒ ์‚ญ์ œ ์‹คํŒจ:", error); toast.error(`์‚ญ์ œ ์‹คํŒจ: ${error.message}`); throw error; } } /** * ์‚ฝ์ž… ์•ก์…˜ ์‹คํ–‰ (์ฒดํฌ๋ฐ•์Šค ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ํ•„๋“œ๋งคํ•‘์— ๋”ฐ๋ผ ์ƒˆ ํ…Œ์ด๋ธ”์— ์‚ฝ์ž…) */ private static async executeActionInsert(action: any, context: ButtonActionContext): Promise { console.log("โž• ์‚ฝ์ž… ์•ก์…˜ ์‹คํ–‰:", action); let insertData: Record = {}; // ์•ก์…˜์— ํ•„๋“œ ๋งคํ•‘ ์ •๋ณด๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ if (action.fieldMappings && Array.isArray(action.fieldMappings)) { // ๐ŸŽฏ ์ฒดํฌ๋ฐ•์Šค๋กœ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ if (!context.selectedRowsData || context.selectedRowsData.length === 0) { throw new Error("์‚ฝ์ž…ํ•  ์†Œ์Šค ๋ฐ์ดํ„ฐ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”. (ํ…Œ์ด๋ธ”์—์„œ ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ ํ•„์š”)"); } const sourceData = context.selectedRowsData[0]; // ์ฒซ ๋ฒˆ์งธ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ // ํ•„๋“œ ๋งคํ•‘์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ action.fieldMappings.forEach((mapping: any) => { const { sourceField, targetField, defaultValue } = mapping; // valueType์ด ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’์„ "selection"์œผ๋กœ ์„ค์ • const valueType = mapping.valueType || "selection"; let value: any; // ๊ฐ’ ์†Œ์Šค์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ if (valueType === "form" && context.formData && sourceField) { // ํผ ๋ฐ์ดํ„ฐ์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ value = context.formData[sourceField]; } else if (valueType === "selection" && sourceField) { // ์„ ํƒ๋œ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ (๋‹ค์–‘ํ•œ ํ•„๋“œ๋ช… ์‹œ๋„) value = sourceData[sourceField] || sourceData[sourceField + "_name"] || // ์กฐ์ธ๋œ ํ•„๋“œ (_name ์ ‘๋ฏธ์‚ฌ) sourceData[sourceField + "Name"]; // ์นด๋ฉœ์ผ€์ด์Šค } else if (valueType === "default" || (defaultValue !== undefined && defaultValue !== "")) { // ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ (valueType์ด "default"์ด๊ฑฐ๋‚˜ defaultValue๊ฐ€ ์žˆ์„ ๋•Œ) value = defaultValue; } else { console.warn(`โš ๏ธ ๋งคํ•‘ ์‹คํŒจ: ${sourceField} โ†’ ${targetField} (๊ฐ’์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ)`); console.warn(` - valueType: ${valueType}, defaultValue: ${defaultValue}`); console.warn(" - ์†Œ์Šค ๋ฐ์ดํ„ฐ ํ‚ค๋“ค:", Object.keys(sourceData)); console.warn(` - sourceData[${sourceField}] =`, sourceData[sourceField]); return; // ๊ฐ’์ด ์—†์œผ๋ฉด ํ•ด๋‹น ํ•„๋“œ๋Š” ์Šคํ‚ต } // ๋Œ€์ƒ ํ•„๋“œ์— ๊ฐ’ ์„ค์ • if (targetField && value !== undefined && value !== null) { insertData[targetField] = value; } }); } else { // ํ•„๋“œ ๋งคํ•‘์ด ์—†์œผ๋ฉด ํผ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ณธ์œผ๋กœ ์‚ฌ์šฉ insertData = { ...context.formData }; } try { // ๐Ÿ”ฅ ์‹ค์ œ ์‚ฝ์ž… API ํ˜ธ์ถœ - ํ•„์ˆ˜ ๋งค๊ฐœ๋ณ€์ˆ˜ ํฌํ•จ // ํ•„๋“œ ๋งคํ•‘์—์„œ ์ฒซ ๋ฒˆ์งธ targetTable์„ ์ฐพ๊ฑฐ๋‚˜ ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ const targetTable = action.fieldMappings?.[0]?.targetTable || action.targetTable || "test_project_info"; const formDataPayload = { screenId: 0, // ์ œ์–ด ๊ด€๋ฆฌ์—์„œ๋Š” screenId๊ฐ€ ์—†์œผ๋ฏ€๋กœ 0 ์‚ฌ์šฉ tableName: targetTable, // ํ•„๋“œ ๋งคํ•‘์—์„œ ๋Œ€์ƒ ํ…Œ์ด๋ธ”๋ช… ๊ฐ€์ ธ์˜ค๊ธฐ data: insertData, }; const result = await DynamicFormApi.saveFormData(formDataPayload); if (result.success) { toast.success("๋ฐ์ดํ„ฐ๊ฐ€ ํƒ€๊ฒŸ ํ…Œ์ด๋ธ”์— ์„ฑ๊ณต์ ์œผ๋กœ ์‚ฝ์ž…๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); } else { throw new Error(result.message || "์‚ฝ์ž… ์‹คํŒจ"); } } catch (error) { console.error("โŒ ์‚ฝ์ž… ์‹คํŒจ:", error); toast.error(`์‚ฝ์ž… ์‹คํŒจ: ${error instanceof Error ? error.message : "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"}`); throw error; } } /** * ํ…Œ์ด๋ธ” ์ด๋ ฅ ๋ณด๊ธฐ ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static async handleViewTableHistory( config: ButtonActionConfig, context: ButtonActionContext, ): Promise { console.log("๐Ÿ“œ ํ…Œ์ด๋ธ” ์ด๋ ฅ ๋ณด๊ธฐ ์•ก์…˜ ์‹คํ–‰:", { config, context }); // ํ…Œ์ด๋ธ”๋ช… ๊ฒฐ์ • (์„ค์ • > ์ปจํ…์ŠคํŠธ > ํผ ๋ฐ์ดํ„ฐ) const tableName = config.historyTableName || context.tableName; if (!tableName) { toast.error("ํ…Œ์ด๋ธ”๋ช…์ด ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); return false; } // ๋ ˆ์ฝ”๋“œ ID ๊ฐ€์ ธ์˜ค๊ธฐ (์„ ํƒ์‚ฌํ•ญ - ์—†์œผ๋ฉด ์ „์ฒด ํ…Œ์ด๋ธ” ์ด๋ ฅ) const recordIdField = config.historyRecordIdField || "id"; const recordIdSource = config.historyRecordIdSource || "selected_row"; let recordId: any = null; let recordLabel: string | undefined; switch (recordIdSource) { case "selected_row": // ์„ ํƒ๋œ ํ–‰์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ (์„ ํƒ์‚ฌํ•ญ) if (context.selectedRowsData && context.selectedRowsData.length > 0) { const selectedRow = context.selectedRowsData[0]; recordId = selectedRow[recordIdField]; // ๋ผ๋ฒจ ํ•„๋“œ๊ฐ€ ์ง€์ •๋˜์–ด ์žˆ์œผ๋ฉด ์‚ฌ์šฉ if (config.historyRecordLabelField) { recordLabel = selectedRow[config.historyRecordLabelField]; } } else if (context.flowSelectedData && context.flowSelectedData.length > 0) { // ํ”Œ๋กœ์šฐ ์„ ํƒ ๋ฐ์ดํ„ฐ ํด๋ฐฑ const selectedRow = context.flowSelectedData[0]; recordId = selectedRow[recordIdField]; if (config.historyRecordLabelField) { recordLabel = selectedRow[config.historyRecordLabelField]; } } break; case "form_field": // ํผ ํ•„๋“œ์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ recordId = context.formData?.[recordIdField]; if (config.historyRecordLabelField) { recordLabel = context.formData?.[config.historyRecordLabelField]; } break; case "context": // ์›๋ณธ ๋ฐ์ดํ„ฐ์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ recordId = context.originalData?.[recordIdField]; if (config.historyRecordLabelField) { recordLabel = context.originalData?.[config.historyRecordLabelField]; } break; } // recordId๊ฐ€ ์—†์–ด๋„ ๊ดœ์ฐฎ์Œ - ์ „์ฒด ํ…Œ์ด๋ธ” ์ด๋ ฅ ๋ณด๊ธฐ // ์ด๋ ฅ ๋ชจ๋‹ฌ ์—ด๊ธฐ (๋™์  import) try { const { TableHistoryModal } = await import("@/components/common/TableHistoryModal"); const { createRoot } = await import("react-dom/client"); // ๋ชจ๋‹ฌ ์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ const modalContainer = document.createElement("div"); document.body.appendChild(modalContainer); const root = createRoot(modalContainer); const closeModal = () => { root.unmount(); document.body.removeChild(modalContainer); }; root.render( React.createElement(TableHistoryModal, { open: true, onOpenChange: (open: boolean) => { if (!open) closeModal(); }, tableName, recordId, recordLabel, displayColumn: config.historyDisplayColumn, }), ); return true; } catch (error) { console.error("โŒ ์ด๋ ฅ ๋ชจ๋‹ฌ ์—ด๊ธฐ ์‹คํŒจ:", error); toast.error("์ด๋ ฅ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } /** * ์—‘์…€ ๋‹ค์šด๋กœ๋“œ ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static async handleExcelDownload(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { // ๋™์  import๋กœ ์—‘์…€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋กœ๋“œ const { exportToExcel } = await import("@/lib/utils/excelExport"); let dataToExport: any[] = []; let visibleColumns: string[] | undefined = undefined; let columnLabels: Record | undefined = undefined; // ๐Ÿ†• ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ๊ตฌ์กฐ ํ™•์ธ ๋ฐ ์ฒ˜๋ฆฌ if (context.screenId) { const { DynamicFormApi } = await import("@/lib/api/dynamicForm"); const relationResponse = await DynamicFormApi.getMasterDetailRelation(context.screenId); if (relationResponse.success && relationResponse.data) { // ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ๊ตฌ์กฐ์ธ ๊ฒฝ์šฐ ์ „์šฉ API ์‚ฌ์šฉ const downloadResponse = await DynamicFormApi.getMasterDetailDownloadData( context.screenId, context.filterConditions, ); if (downloadResponse.success && downloadResponse.data) { dataToExport = downloadResponse.data.data; visibleColumns = downloadResponse.data.columns; // ํ—ค๋”์™€ ์ปฌ๋Ÿผ ๋งคํ•‘ columnLabels = {}; downloadResponse.data.columns.forEach((col: string, index: number) => { columnLabels![col] = downloadResponse.data.headers[index] || col; }); } else { toast.error("๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ๋ฐ์ดํ„ฐ ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } // ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๋ฐ ๋‹ค์šด๋กœ๋“œ if (visibleColumns && visibleColumns.length > 0 && dataToExport.length > 0) { dataToExport = dataToExport.map((row: any) => { const filteredRow: Record = {}; visibleColumns!.forEach((columnName: string) => { const label = columnLabels?.[columnName] || columnName; let value = row[columnName]; // ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ๋ฅผ ๋ผ๋ฒจ๋กœ ๋ณ€ํ™˜ (CATEGORY_๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฐ’) if (value && typeof value === "string" && value.includes("CATEGORY_")) { // ๋จผ์ € _label ํ•„๋“œ ํ™•์ธ (API์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ฒฝ์šฐ) const labelFieldName = `${columnName}_label`; if (row[labelFieldName]) { value = row[labelFieldName]; } else { // _value_label ํ•„๋“œ ํ™•์ธ const valueLabelFieldName = `${columnName}_value_label`; if (row[valueLabelFieldName]) { value = row[valueLabelFieldName]; } } } filteredRow[label] = value; }); return filteredRow; }); } // ํŒŒ์ผ๋ช… ์ƒ์„ฑ let defaultFileName = relationResponse.data.masterTable || "๋ฐ์ดํ„ฐ"; if (typeof window !== "undefined") { const menuName = localStorage.getItem("currentMenuName"); if (menuName) defaultFileName = menuName; } const fileName = config.excelFileName || `${defaultFileName}_${new Date().toISOString().split("T")[0]}.xlsx`; const sheetName = config.excelSheetName || "Sheet1"; await exportToExcel(dataToExport, fileName, sheetName, true); toast.success(`${dataToExport.length}๊ฐœ ํ–‰์ด ๋‹ค์šด๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); return true; } } // โœ… ๊ธฐ์กด ๋กœ์ง: ๋‹จ์ผ ํ…Œ์ด๋ธ” ์ฒ˜๋ฆฌ if (context.tableName) { const { tableDisplayStore } = await import("@/stores/tableDisplayStore"); const storedData = tableDisplayStore.getTableData(context.tableName); // ํ•„ํ„ฐ ์กฐ๊ฑด์€ ์ €์žฅ์†Œ ๋˜๋Š” context์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ const filterConditions = storedData?.filterConditions || context.filterConditions; const searchTerm = storedData?.searchTerm || context.searchTerm; try { const { entityJoinApi } = await import("@/lib/api/entityJoin"); const apiParams = { page: 1, size: 10000, // ์ตœ๋Œ€ 10,000๊ฐœ sortBy: context.sortBy || storedData?.sortBy || "id", sortOrder: (context.sortOrder || storedData?.sortOrder || "asc") as "asc" | "desc", search: filterConditions, // โœ… ํ•„ํ„ฐ ์กฐ๊ฑด enableEntityJoin: true, // โœ… Entity ์กฐ์ธ // autoFilter๋Š” entityJoinApi.getTableDataWithJoins ๋‚ด๋ถ€์—์„œ ์ž๋™์œผ๋กœ ์ ์šฉ๋จ }; // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ค€์ˆ˜: autoFilter๋กœ company_code ์ž๋™ ์ ์šฉ const response = await entityJoinApi.getTableDataWithJoins(context.tableName, apiParams); // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ™•์ธ const allData = Array.isArray(response) ? response : response?.data || []; const companyCodesInData = [...new Set(allData.map((row: any) => row.company_code))]; if (companyCodesInData.length > 1) { console.error("โŒ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์œ„๋ฐ˜! ์—ฌ๋Ÿฌ ํšŒ์‚ฌ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์„ž์—ฌ์žˆ์Šต๋‹ˆ๋‹ค:", companyCodesInData); } // entityJoinApi๋Š” EntityJoinResponse ๋˜๋Š” data ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ if (Array.isArray(response)) { // ๋ฐฐ์—ด๋กœ ์ง์ ‘ ๋ฐ˜ํ™˜๋œ ๊ฒฝ์šฐ dataToExport = response; } else if (response && "data" in response) { // EntityJoinResponse ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ dataToExport = response.data; } else { console.error("โŒ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์‘๋‹ต ํ˜•์‹:", response); toast.error("๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } catch (error) { console.error("์—‘์…€ ๋‹ค์šด๋กœ๋“œ: ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ:", error); toast.error("๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } // ํด๋ฐฑ: ํผ ๋ฐ์ดํ„ฐ else if (context.formData && Object.keys(context.formData).length > 0) { dataToExport = [context.formData]; } // ํ…Œ์ด๋ธ”๋ช…๋„ ์—†๊ณ  ํผ ๋ฐ์ดํ„ฐ๋„ ์—†์œผ๋ฉด ์—๋Ÿฌ else { toast.error("๋‹ค์šด๋กœ๋“œํ•  ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); return false; } // ๋ฐฐ์—ด์ด ์•„๋‹ˆ๋ฉด ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ if (!Array.isArray(dataToExport)) { if (typeof dataToExport === "object" && dataToExport !== null) { dataToExport = [dataToExport]; } else { toast.error("๋‹ค์šด๋กœ๋“œํ•  ๋ฐ์ดํ„ฐ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); return false; } } if (dataToExport.length === 0) { toast.error("๋‹ค์šด๋กœ๋“œํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); return false; } // ํŒŒ์ผ๋ช… ์ƒ์„ฑ (๋ฉ”๋‰ด ์ด๋ฆ„ ์šฐ์„  ์‚ฌ์šฉ) let defaultFileName = context.tableName || "๋ฐ์ดํ„ฐ"; // localStorage์—์„œ ๋ฉ”๋‰ด ์ด๋ฆ„ ๊ฐ€์ ธ์˜ค๊ธฐ if (typeof window !== "undefined") { const menuName = localStorage.getItem("currentMenuName"); if (menuName) { defaultFileName = menuName; } } const fileName = config.excelFileName || `${defaultFileName}_${new Date().toISOString().split("T")[0]}.xlsx`; const sheetName = config.excelSheetName || "Sheet1"; const includeHeaders = config.excelIncludeHeaders !== false; // ๐ŸŽจ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์—์„œ ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ์˜ ์ปฌ๋Ÿผ ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ // visibleColumns, columnLabels๋Š” ํ•จ์ˆ˜ ์ƒ๋‹จ์—์„œ ์ด๋ฏธ ์„ ์–ธ๋จ try { // ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (๋ณ„๋„ API ์‚ฌ์šฉ) const { apiClient } = await import("@/lib/api/client"); const layoutResponse = await apiClient.get(`/screen-management/screens/${context.screenId}/layout`); if (layoutResponse.data?.success && layoutResponse.data?.data) { const layoutData = layoutResponse.data.data; // components๊ฐ€ ๋ฌธ์ž์—ด์ด๋ฉด ํŒŒ์‹ฑ if (typeof layoutData.components === "string") { layoutData.components = JSON.parse(layoutData.components); } // ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ ์ฐพ๊ธฐ const findTableListComponent = (components: any[]): any => { if (!Array.isArray(components)) return null; for (const comp of components) { // componentType์ด 'table-list'์ธ์ง€ ํ™•์ธ const isTableList = comp.componentType === "table-list"; // componentConfig ์•ˆ์—์„œ ํ…Œ์ด๋ธ”๋ช… ํ™•์ธ const matchesTable = comp.componentConfig?.selectedTable === context.tableName || comp.componentConfig?.tableName === context.tableName; if (isTableList && matchesTable) { return comp; } if (comp.children && comp.children.length > 0) { const found = findTableListComponent(comp.children); if (found) return found; } } return null; }; const tableListComponent = findTableListComponent(layoutData.components || []); if (tableListComponent && tableListComponent.componentConfig?.columns) { const columns = tableListComponent.componentConfig.columns; // visible์ด true์ธ ์ปฌ๋Ÿผ๋งŒ ์ถ”์ถœ visibleColumns = columns.filter((col: any) => col.visible !== false).map((col: any) => col.columnName); // column_labels ํ…Œ์ด๋ธ”์—์„œ ์‹ค์ œ ๋ผ๋ฒจ ๊ฐ€์ ธ์˜ค๊ธฐ try { const columnsResponse = await apiClient.get(`/table-management/tables/${context.tableName}/columns`, { params: { page: 1, size: 9999 }, }); if (columnsResponse.data?.success && columnsResponse.data?.data) { let columnData = columnsResponse.data.data; // data๊ฐ€ ๊ฐ์ฒด์ด๊ณ  columns ํ•„๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์ถ”์ถœ if (columnData.columns && Array.isArray(columnData.columns)) { columnData = columnData.columns; } if (Array.isArray(columnData)) { columnLabels = {}; // API์—์„œ ๊ฐ€์ ธ์˜จ ๋ผ๋ฒจ๋กœ ๋งคํ•‘ columnData.forEach((colData: any) => { const colName = colData.column_name || colData.columnName; // ์šฐ์„ ์ˆœ์œ„: column_label > label > displayName > columnName const labelValue = colData.column_label || colData.label || colData.displayName || colName; if (colName && labelValue) { columnLabels![colName] = labelValue; } }); } } } catch (error) { // ์‹คํŒจ ์‹œ ์ปดํฌ๋„ŒํŠธ ์„ค์ •์˜ displayName ์‚ฌ์šฉ columnLabels = {}; columns.forEach((col: any) => { if (col.columnName) { columnLabels![col.columnName] = col.displayName || col.label || col.columnName; } }); } } } } catch (error) { console.error("โŒ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ ์‹คํŒจ:", error); } // Fallback: ๋ ˆ์ด์•„์›ƒ์—์„œ ์ปฌ๋Ÿผ ์ •๋ณด๋ฅผ ๋ชป ๊ฐ€์ ธ์˜จ ๊ฒฝ์šฐ, table_type_columns์—์„œ ์ง์ ‘ ์กฐํšŒ // ์‹œ์Šคํ…œ ์ปฌ๋Ÿผ ์ œ์™ธ + ๋ผ๋ฒจ ์ ์šฉ์œผ๋กœ raw ์ปฌ๋Ÿผ๋ช… ๋…ธ์ถœ ๋ฐฉ์ง€ const SYSTEM_COLUMNS = ["id", "company_code", "created_date", "updated_date", "writer"]; if ((!visibleColumns || visibleColumns.length === 0) && context.tableName && dataToExport.length > 0) { console.log("โš ๏ธ ๋ ˆ์ด์•„์›ƒ์—์„œ ์ปฌ๋Ÿผ ์„ค์ •์„ ์ฐพ์ง€ ๋ชปํ•จ โ†’ table_type_columns์—์„œ fallback ์กฐํšŒ"); try { const { apiClient } = await import("@/lib/api/client"); const columnsResponse = await apiClient.get(`/table-management/tables/${context.tableName}/columns`, { params: { page: 1, size: 9999 }, }); if (columnsResponse.data?.success && columnsResponse.data?.data) { let columnData = columnsResponse.data.data; if (columnData.columns && Array.isArray(columnData.columns)) { columnData = columnData.columns; } if (Array.isArray(columnData) && columnData.length > 0) { // visible์ด false๊ฐ€ ์•„๋‹Œ ์ปฌ๋Ÿผ๋งŒ + ์‹œ์Šคํ…œ ์ปฌ๋Ÿผ ์ œ์™ธ const filteredCols = columnData.filter((col: any) => { const colName = (col.column_name || col.columnName || "").toLowerCase(); if (SYSTEM_COLUMNS.includes(colName)) return false; if (col.isVisible === false || col.is_visible === false) return false; return true; }); visibleColumns = filteredCols.map((col: any) => col.column_name || col.columnName); columnLabels = {}; filteredCols.forEach((col: any) => { const colName = col.column_name || col.columnName; const labelValue = col.column_label || col.label || col.displayName || colName; if (colName) { columnLabels![colName] = labelValue; } }); console.log(`โœ… Fallback ์ปฌ๋Ÿผ ${visibleColumns.length}๊ฐœ ๋กœ๋“œ ์™„๋ฃŒ`); } } } catch (fallbackError) { console.error("โŒ Fallback ์ปฌ๋Ÿผ ์กฐํšŒ ์‹คํŒจ:", fallbackError); } } // ์ตœ์ข… ์•ˆ์ „์žฅ์น˜: ์—ฌ์ „ํžˆ ์ปฌ๋Ÿผ ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด ๋ฐ์ดํ„ฐ์˜ ํ‚ค์—์„œ ์‹œ์Šคํ…œ ์ปฌ๋Ÿผ๋งŒ ์ œ์™ธ if ((!visibleColumns || visibleColumns.length === 0) && dataToExport.length > 0) { console.log("โš ๏ธ ์ตœ์ข… fallback: ๋ฐ์ดํ„ฐ ํ‚ค์—์„œ ์‹œ์Šคํ…œ ์ปฌ๋Ÿผ ์ œ์™ธ"); const allKeys = Object.keys(dataToExport[0]); visibleColumns = allKeys.filter((key) => { const lowerKey = key.toLowerCase(); // ์‹œ์Šคํ…œ ์ปฌ๋Ÿผ ์ œ์™ธ if (SYSTEM_COLUMNS.includes(lowerKey)) return false; // _name, _label ๋“ฑ ์กฐ์ธ๋œ ๋ณด์กฐ ํ•„๋“œ ์ œ์™ธ if (lowerKey.endsWith("_name") || lowerKey.endsWith("_label") || lowerKey.endsWith("_value_label")) return false; return true; }); // ๋ผ๋ฒจ์ด ์—†์œผ๋ฏ€๋กœ ์ตœ์†Œํ•œ column_labels ๋น„์›Œ๋‘์ง€ ์•Š์Œ (์ปฌ๋Ÿผ๋ช… ๊ทธ๋Œ€๋กœ ํ‘œ์‹œ๋˜์ง€๋งŒ ์‹œ์Šคํ…œ ์ปฌ๋Ÿผ์€ ์ œ์™ธ๋จ) if (!columnLabels) { columnLabels = {}; } } // ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’๋“ค ์กฐํšŒ (ํ•œ ๋ฒˆ๋งŒ) const categoryMap: Record> = {}; let categoryColumns: string[] = []; if (context.tableName) { try { const { getCategoryColumns, getCategoryValues } = await import("@/lib/api/tableCategoryValue"); const categoryColumnsResponse = await getCategoryColumns(context.tableName); if (categoryColumnsResponse.success && categoryColumnsResponse.data) { // ๋ฐฑ์—”๋“œ์—์„œ ์ •์˜๋œ ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ๋“ค categoryColumns = categoryColumnsResponse.data .map((col: any) => col.column_name || col.columnName || col.name) .filter(Boolean); // undefined ์ œ๊ฑฐ // ๊ฐ ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ์˜ ๊ฐ’๋“ค ์กฐํšŒ for (const columnName of categoryColumns) { try { const valuesResponse = await getCategoryValues(context.tableName, columnName, false); if (valuesResponse.success && valuesResponse.data) { // valueCode โ†’ valueLabel ๋งคํ•‘ categoryMap[columnName] = {}; valuesResponse.data.forEach((catValue: any) => { const code = catValue.valueCode || catValue.category_value_id; const label = catValue.valueLabel || catValue.label || code; if (code) { categoryMap[columnName][code] = label; } }); } } catch (error) { console.error(`โŒ ์นดํ…Œ๊ณ ๋ฆฌ "${columnName}" ์กฐํšŒ ์‹คํŒจ:`, error); } } } } catch (error) { console.error("โŒ ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด ์กฐํšŒ ์‹คํŒจ:", error); } } // ์ปฌ๋Ÿผ ํ•„ํ„ฐ๋ง ๋ฐ ๋ผ๋ฒจ ์ ์šฉ if (visibleColumns && visibleColumns.length > 0 && dataToExport.length > 0) { dataToExport = dataToExport.map((row: any) => { const filteredRow: Record = {}; visibleColumns.forEach((columnName: string) => { // __checkbox__ ์ปฌ๋Ÿผ์€ ์ œ์™ธ if (columnName === "__checkbox__") return; if (columnName in row) { // ๋ผ๋ฒจ ์šฐ์„  ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์ปฌ๋Ÿผ๋ช… ์‚ฌ์šฉ const label = columnLabels?.[columnName] || columnName; // ๐ŸŽฏ Entity ์กฐ์ธ๋œ ๊ฐ’ ์šฐ์„  ์‚ฌ์šฉ let value = row[columnName]; // writer โ†’ writer_name ์‚ฌ์šฉ if (columnName === "writer" && row["writer_name"]) { value = row["writer_name"]; } // ๋‹ค๋ฅธ ์—”ํ‹ฐํ‹ฐ ํ•„๋“œ๋“ค๋„ _name ์šฐ์„  ์‚ฌ์šฉ else if (row[`${columnName}_name`]) { value = row[`${columnName}_name`]; } // ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ํ•„๋“œ๋Š” ๋ผ๋ฒจ๋กœ ๋ณ€ํ™˜ (๋ฐฑ์—”๋“œ์—์„œ ์ •์˜๋œ ์ปฌ๋Ÿผ๋งŒ) else if (categoryMap[columnName] && typeof value === "string") { // ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ๋‹ค์ค‘ ๊ฐ’ ์ฒ˜๋ฆฌ if (value.includes(",")) { const values = value .split(",") .map((v) => v.trim()) .filter((v) => v); const labels = values.map((v) => categoryMap[columnName][v] || v); value = labels.join(", "); } else if (categoryMap[columnName][value]) { value = categoryMap[columnName][value]; } } filteredRow[label] = value; } }); return filteredRow; }); } // ์ตœ๋Œ€ ํ–‰ ์ˆ˜ ์ œํ•œ const MAX_ROWS = 10000; if (dataToExport.length > MAX_ROWS) { toast.warning(`์ตœ๋Œ€ ${MAX_ROWS.toLocaleString()}๊ฐœ ํ–‰๊นŒ์ง€๋งŒ ๋‹ค์šด๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค.`); dataToExport = dataToExport.slice(0, MAX_ROWS); } // ์—‘์…€ ๋‹ค์šด๋กœ๋“œ ์‹คํ–‰ await exportToExcel(dataToExport, fileName, sheetName, includeHeaders); toast.success(`${dataToExport.length}๊ฐœ ํ–‰์ด ๋‹ค์šด๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); return true; } catch (error) { console.error("โŒ ์—‘์…€ ๋‹ค์šด๋กœ๋“œ ์‹คํŒจ:", error); toast.error(config.errorMessage || "์—‘์…€ ๋‹ค์šด๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } /** * ์—‘์…€ ์—…๋กœ๋“œ ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static async handleExcelUpload(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { // ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ๊ตฌ์กฐ ํ™•์ธ (ํ™”๋ฉด์— ๋ถ„ํ•  ํŒจ๋„์ด ์žˆ์œผ๋ฉด ์ž๋™ ๊ฐ์ง€) let isMasterDetail = false; let masterDetailRelation: any = null; let masterDetailExcelConfig: any = undefined; // ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์—์„œ ๋ถ„ํ•  ํŒจ๋„ ์ž๋™ ๊ฐ์ง€ if (context.screenId) { const { DynamicFormApi } = await import("@/lib/api/dynamicForm"); const relationResponse = await DynamicFormApi.getMasterDetailRelation(context.screenId); if (relationResponse.success && relationResponse.data) { isMasterDetail = true; masterDetailRelation = relationResponse.data; // ๋ฒ„ํŠผ ์„ค์ •์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋“ฑ ์ถ”๊ฐ€ ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ // ์—…๋กœ๋“œ ํ›„ ์ œ์–ด: excelAfterUploadFlows๋ฅผ ์šฐ์„  ์‚ฌ์šฉ (ํ†ตํ•ฉ๋œ ์„ค์ •) // masterDetailExcel.afterUploadFlows๋Š” ๋ ˆ๊ฑฐ์‹œ ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•ด fallback์œผ๋กœ๋งŒ ์‚ฌ์šฉ const afterUploadFlows = config.excelAfterUploadFlows?.length > 0 ? config.excelAfterUploadFlows : config.masterDetailExcel?.afterUploadFlows; // masterDetailExcel ์„ค์ •์ด ๋ช…์‹œ์ ์œผ๋กœ ์žˆ์„ ๋•Œ๋งŒ ๊ฐ„๋‹จ ๋ชจ๋“œ (๋””ํ…Œ์ผ๋งŒ ์—…๋กœ๋“œ) // ์„ค์ •์ด ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋ชจ๋“œ (๋งˆ์Šคํ„ฐ+๋””ํ…Œ์ผ ๋‘˜ ๋‹ค ์—…๋กœ๋“œ) if (config.masterDetailExcel) { masterDetailExcelConfig = { ...config.masterDetailExcel, // ๋ถ„ํ•  ํŒจ๋„์—์„œ ๊ฐ์ง€ํ•œ ํ…Œ์ด๋ธ” ์ •๋ณด๋กœ ๋ฎ์–ด์“ฐ๊ธฐ masterTable: relationResponse.data.masterTable, detailTable: relationResponse.data.detailTable, masterKeyColumn: relationResponse.data.masterKeyColumn, detailFkColumn: relationResponse.data.detailFkColumn, // ์ฑ„๋ฒˆ์€ ExcelUploadModal์—์„œ ๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ์ž๋™ ๊ฐ์ง€ // ์—…๋กœ๋“œ ํ›„ ์ œ์–ด ์„ค์ • (ํ†ตํ•ฉ: excelAfterUploadFlows ์šฐ์„ ) afterUploadFlows, }; } // masterDetailExcel ์„ค์ • ์—†์œผ๋ฉด masterDetailExcelConfig๋Š” undefined ์œ ์ง€ // โ†’ ExcelUploadModal์—์„œ ๊ธฐ๋ณธ ๋ชจ๋“œ๋กœ ๋™์ž‘ (๋งˆ์Šคํ„ฐ+๋””ํ…Œ์ผ ๋‘˜ ๋‹ค ๋งคํ•‘/์—…๋กœ๋“œ) } } // ๋™์  import๋กœ ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ ๋กœ๋“œ const { ExcelUploadModal } = await import("@/components/common/ExcelUploadModal"); const { createRoot } = await import("react-dom/client"); // ๋ชจ๋‹ฌ ์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ const modalContainer = document.createElement("div"); document.body.appendChild(modalContainer); const root = createRoot(modalContainer); const closeModal = () => { root.unmount(); document.body.removeChild(modalContainer); }; // localStorage ๋””๋ฒ„๊น… const modalId = `excel-upload-${context.tableName || ""}`; const storageKey = `modal_size_${modalId}_${context.userId || "guest"}`; root.render( React.createElement(ExcelUploadModal, { open: true, onOpenChange: (open: boolean) => { if (!open) { // ๋ชจ๋‹ฌ ๋‹ซ์„ ๋•Œ localStorage ํ™•์ธ closeModal(); } }, tableName: context.tableName || "", uploadMode: config.excelUploadMode || "insert", keyColumn: config.excelKeyColumn, userId: context.userId, // ๐Ÿ†• ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ๊ด€๋ จ props screenId: context.screenId, isMasterDetail, masterDetailRelation, masterDetailExcelConfig, // ์ฑ„๋ฒˆ์€ ExcelUploadModal์—์„œ ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ ๊ธฐ๋ฐ˜ ์ž๋™ ๊ฐ์ง€ // ๐Ÿ†• ์—…๋กœ๋“œ ํ›„ ์ œ์–ด ์‹คํ–‰ ์„ค์ • afterUploadFlows: config.excelAfterUploadFlows, onSuccess: () => { // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€๋Š” ExcelUploadModal ๋‚ด๋ถ€์—์„œ ์ด๋ฏธ ํ‘œ์‹œ๋จ context.onRefresh?.(); closeModal(); }, }), ); return true; } catch (error) { console.error("โŒ ์—‘์…€ ์—…๋กœ๋“œ ๋ชจ๋‹ฌ ์—ด๊ธฐ ์‹คํŒจ:", error); toast.error(config.errorMessage || "์—‘์…€ ์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } /** * ๋ฐ”์ฝ”๋“œ ์Šค์บ” ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static async handleBarcodeScan(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { console.log("๐Ÿ“ท ๋ฐ”์ฝ”๋“œ ์Šค์บ” ๋ชจ๋‹ฌ ์—ด๊ธฐ:", { config, context }); // ๋™์  import๋กœ ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ ๋กœ๋“œ const { BarcodeScanModal } = await import("@/components/common/BarcodeScanModal"); const { createRoot } = await import("react-dom/client"); // ๋ชจ๋‹ฌ ์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ const modalContainer = document.createElement("div"); document.body.appendChild(modalContainer); const root = createRoot(modalContainer); const closeModal = () => { root.unmount(); document.body.removeChild(modalContainer); }; root.render( React.createElement(BarcodeScanModal, { open: true, onOpenChange: (open: boolean) => { if (!open) closeModal(); }, targetField: config.barcodeTargetField, barcodeFormat: config.barcodeFormat || "all", autoSubmit: config.barcodeAutoSubmit || false, userId: context.userId, onScanSuccess: (barcode: string) => { // ๋Œ€์ƒ ํ•„๋“œ์— ๊ฐ’ ์ž…๋ ฅ if (config.barcodeTargetField && context.onFormDataChange) { context.onFormDataChange({ ...context.formData, [config.barcodeTargetField]: barcode, }); } toast.success(`๋ฐ”์ฝ”๋“œ ์Šค์บ” ์™„๋ฃŒ: ${barcode}`); // ์ž๋™ ์ œ์ถœ ์˜ต์…˜์ด ์ผœ์ ธ์žˆ์œผ๋ฉด ์ €์žฅ if (config.barcodeAutoSubmit) { this.handleSave(config, context); } closeModal(); }, }), ); return true; } catch (error) { console.error("โŒ ๋ฐ”์ฝ”๋“œ ์Šค์บ” ๋ชจ๋‹ฌ ์—ด๊ธฐ ์‹คํŒจ:", error); toast.error("๋ฐ”์ฝ”๋“œ ์Šค์บ” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } /** * ์ฝ”๋“œ ๋ณ‘ํ•ฉ ์•ก์…˜ ์ฒ˜๋ฆฌ */ private static async handleCodeMerge(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { console.log("๐Ÿ”€ ์ฝ”๋“œ ๋ณ‘ํ•ฉ ์•ก์…˜ ์‹คํ–‰:", { config, context }); // ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ ํ™•์ธ const selectedRows = context.selectedRowsData || context.flowSelectedData; if (!selectedRows || selectedRows.length !== 2) { toast.error("๋ณ‘ํ•ฉํ•  ๋‘ ๊ฐœ์˜ ํ•ญ๋ชฉ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return false; } // ๋ณ‘ํ•ฉํ•  ์ปฌ๋Ÿผ๋ช… ํ™•์ธ const columnName = config.mergeColumnName; if (!columnName) { toast.error("๋ณ‘ํ•ฉํ•  ์ปฌ๋Ÿผ๋ช…์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); return false; } // ๋‘ ๊ฐœ์˜ ์„ ํƒ๋œ ํ–‰์—์„œ ์ปฌ๋Ÿผ ๊ฐ’ ์ถ”์ถœ const [row1, row2] = selectedRows; const value1 = row1[columnName]; const value2 = row2[columnName]; if (!value1 || !value2) { toast.error(`์„ ํƒํ•œ ํ•ญ๋ชฉ์— "${columnName}" ๊ฐ’์ด ์—†์Šต๋‹ˆ๋‹ค.`); return false; } if (value1 === value2) { toast.error("๊ฐ™์€ ๊ฐ’์€ ๋ณ‘ํ•ฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); return false; } // ๋ณ‘ํ•ฉ ๋ฐฉํ–ฅ ์„ ํƒ ๋ชจ๋‹ฌ ํ‘œ์‹œ const confirmed = await new Promise<{ confirmed: boolean; oldValue: string; newValue: string }>((resolve) => { const modalHtml = `

์ฝ”๋“œ ๋ณ‘ํ•ฉ ๋ฐฉํ–ฅ ์„ ํƒ

์–ด๋А ์ฝ”๋“œ๋กœ ๋ณ‘ํ•ฉํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?

`; const modalContainer = document.createElement("div"); modalContainer.innerHTML = modalHtml; document.body.appendChild(modalContainer); const option1Btn = modalContainer.querySelector("#merge-option-1") as HTMLButtonElement; const option2Btn = modalContainer.querySelector("#merge-option-2") as HTMLButtonElement; const cancelBtn = modalContainer.querySelector("#merge-cancel") as HTMLButtonElement; // ํ˜ธ๋ฒ„ ํšจ๊ณผ [option1Btn, option2Btn].forEach((btn) => { btn.addEventListener("mouseenter", () => { btn.style.borderColor = "#3b82f6"; btn.style.background = "#eff6ff"; }); btn.addEventListener("mouseleave", () => { btn.style.borderColor = "#e5e7eb"; btn.style.background = "white"; }); }); option1Btn.addEventListener("click", () => { document.body.removeChild(modalContainer); resolve({ confirmed: true, oldValue: value2, newValue: value1 }); }); option2Btn.addEventListener("click", () => { document.body.removeChild(modalContainer); resolve({ confirmed: true, oldValue: value1, newValue: value2 }); }); cancelBtn.addEventListener("click", () => { document.body.removeChild(modalContainer); resolve({ confirmed: false, oldValue: "", newValue: "" }); }); }); if (!confirmed.confirmed) { return false; } const { oldValue, newValue } = confirmed; // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ‘œ์‹œ (๊ฐ’ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ - ๋ชจ๋“  ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ์—์„œ ๊ฒ€์ƒ‰) if (config.mergeShowPreview !== false) { const { apiClient } = await import("@/lib/api/client"); toast.loading("์˜ํ–ฅ๋ฐ›๋Š” ๋ฐ์ดํ„ฐ ๊ฒ€์ƒ‰ ์ค‘...", { duration: Infinity }); const previewResponse = await apiClient.post("/code-merge/preview-by-value", { oldValue, }); toast.dismiss(); if (previewResponse.data.success) { const preview = previewResponse.data.data; const totalRows = preview.totalAffectedRows; // ์ƒ์„ธ ์ •๋ณด ์ƒ์„ฑ const detailList = preview.preview .map((p: any) => ` - ${p.tableName}.${p.columnName}: ${p.affectedRows}๊ฑด`) .join("\n"); const confirmMerge = confirm( "์ฝ”๋“œ ๋ณ‘ํ•ฉ ํ™•์ธ\n\n" + `${oldValue} โ†’ ${newValue}\n\n` + "์˜ํ–ฅ๋ฐ›๋Š” ๋ฐ์ดํ„ฐ:\n" + `- ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ ์ˆ˜: ${preview.preview.length}๊ฐœ\n` + `- ์ด ํ–‰ ์ˆ˜: ${totalRows}๊ฐœ\n\n` + (preview.preview.length <= 10 ? `์ƒ์„ธ:\n${detailList}\n\n` : "") + "๋ชจ๋“  ํ…Œ์ด๋ธ”์—์„œ ํ•ด๋‹น ๊ฐ’์ด ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค.\n\n" + "๊ณ„์†ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?", ); if (!confirmMerge) { return false; } } } // ๋ณ‘ํ•ฉ ์‹คํ–‰ (๊ฐ’ ๊ธฐ๋ฐ˜ - ๋ชจ๋“  ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ) toast.loading("์ฝ”๋“œ ๋ณ‘ํ•ฉ ์ค‘...", { duration: Infinity }); const { apiClient } = await import("@/lib/api/client"); const response = await apiClient.post("/code-merge/merge-by-value", { oldValue, newValue, }); toast.dismiss(); if (response.data.success) { const data = response.data.data; // ๋ณ€๊ฒฝ๋œ ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ ๋ชฉ๋ก ์ƒ์„ฑ const changedList = data.affectedData .map((d: any) => `${d.tableName}.${d.columnName}: ${d.rowsUpdated}๊ฑด`) .join(", "); toast.success( `์ฝ”๋“œ ๋ณ‘ํ•ฉ ์™„๋ฃŒ! ${data.affectedData.length}๊ฐœ ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ, ${data.totalRowsUpdated}๊ฐœ ํ–‰ ์—…๋ฐ์ดํŠธ`, ); console.log("์ฝ”๋“œ ๋ณ‘ํ•ฉ ๊ฒฐ๊ณผ:", data.affectedData); // ํ™”๋ฉด ์ƒˆ๋กœ๊ณ ์นจ context.onRefresh?.(); context.onFlowRefresh?.(); return true; } else { toast.error(response.data.message || "์ฝ”๋“œ ๋ณ‘ํ•ฉ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } catch (error: any) { console.error("โŒ ์ฝ”๋“œ ๋ณ‘ํ•ฉ ์‹คํŒจ:", error); toast.dismiss(); toast.error(error.response?.data?.message || "์ฝ”๋“œ ๋ณ‘ํ•ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } // ๐Ÿ†• ์—ฐ์† ์œ„์น˜ ์ถ”์  ์ƒํƒœ ์ €์žฅ (์ „์—ญ) private static trackingIntervalId: NodeJS.Timeout | null = null; private static currentTripId: string | null = null; private static trackingContext: ButtonActionContext | null = null; private static trackingConfig: ButtonActionConfig | null = null; /** * ์—ฐ์† ์œ„์น˜ ์ถ”์  ์‹œ์ž‘ */ private static async handleTrackingStart(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { // ์ด๋ฏธ ์ถ”์  ์ค‘์ธ์ง€ ํ™•์ธ if (this.trackingIntervalId) { toast.warning("์ด๋ฏธ ์œ„์น˜ ์ถ”์ ์ด ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค."); return false; } // ์œ„์น˜ ๊ถŒํ•œ ํ™•์ธ if (!navigator.geolocation) { toast.error("์ด ๋ธŒ๋ผ์šฐ์ €๋Š” ์œ„์น˜ ์ •๋ณด๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); return false; } // Trip ID ์ƒ์„ฑ const tripId = config.trackingAutoGenerateTripId !== false ? `TRIP_${Date.now()}_${context.userId || "unknown"}` : context.formData?.[config.trackingTripIdField || "trip_id"] || `TRIP_${Date.now()}`; this.currentTripId = tripId; this.trackingContext = context; this.trackingConfig = config; // ์ถœ๋ฐœ์ง€/๋„์ฐฉ์ง€ ์ •๋ณด const departure = context.formData?.[config.trackingDepartureField || "departure"] || null; const arrival = context.formData?.[config.trackingArrivalField || "arrival"] || null; const departureName = context.formData?.["departure_name"] || null; const destinationName = context.formData?.["destination_name"] || null; const vehicleId = context.formData?.[config.trackingVehicleIdField || "vehicle_id"] || null; console.log("๐Ÿ“ [handleTrackingStart] ์šดํ–‰ ์ •๋ณด:", { tripId, departure, arrival, departureName, destinationName, vehicleId, }); // ์ƒํƒœ ๋ณ€๊ฒฝ (vehicles ํ…Œ์ด๋ธ” ๋“ฑ) if (config.trackingStatusOnStart && config.trackingStatusField) { try { const { apiClient } = await import("@/lib/api/client"); const statusTableName = config.trackingStatusTableName || context.tableName; const keyField = config.trackingStatusKeyField || "user_id"; const keyValue = resolveSpecialKeyword(config.trackingStatusKeySourceField || "__userId__", context); if (keyValue) { // ์ƒํƒœ ์—…๋ฐ์ดํŠธ await apiClient.put("/dynamic-form/update-field", { tableName: statusTableName, keyField: keyField, keyValue: keyValue, updateField: config.trackingStatusField, updateValue: config.trackingStatusOnStart, }); // ๐Ÿ†• ์ถœ๋ฐœ์ง€/๋„์ฐฉ์ง€๋„ vehicles ํ…Œ์ด๋ธ”์— ์ €์žฅ if (departure) { try { await apiClient.put("/dynamic-form/update-field", { tableName: statusTableName, keyField: keyField, keyValue: keyValue, updateField: "departure", updateValue: departure, }); } catch { // ์ปฌ๋Ÿผ์ด ์—†์œผ๋ฉด ๋ฌด์‹œ } } if (arrival) { try { await apiClient.put("/dynamic-form/update-field", { tableName: statusTableName, keyField: keyField, keyValue: keyValue, updateField: "arrival", updateValue: arrival, }); } catch { // ์ปฌ๋Ÿผ์ด ์—†์œผ๋ฉด ๋ฌด์‹œ } } } } catch (statusError) { console.warn("โš ๏ธ ์ƒํƒœ ๋ณ€๊ฒฝ ์‹คํŒจ:", statusError); } } // ์ฒซ ๋ฒˆ์งธ ์œ„์น˜ ์ €์žฅ await this.saveLocationToHistory(tripId, departure, arrival, departureName, destinationName, vehicleId); // ์ฃผ๊ธฐ์  ์œ„์น˜ ์ €์žฅ ์‹œ์ž‘ const interval = config.trackingInterval || 10000; // ๊ธฐ๋ณธ 10์ดˆ this.trackingIntervalId = setInterval(async () => { await this.saveLocationToHistory(tripId, departure, arrival, departureName, destinationName, vehicleId); }, interval); toast.success(config.successMessage || `์œ„์น˜ ์ถ”์ ์ด ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (${interval / 1000}์ดˆ ๊ฐ„๊ฒฉ)`); // ์ถ”์  ์‹œ์ž‘ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (UI ์—…๋ฐ์ดํŠธ์šฉ) window.dispatchEvent( new CustomEvent("trackingStarted", { detail: { tripId, interval }, }), ); return true; } catch (error: any) { console.error("โŒ ์œ„์น˜ ์ถ”์  ์‹œ์ž‘ ์‹คํŒจ:", error); toast.error(config.errorMessage || "์œ„์น˜ ์ถ”์  ์‹œ์ž‘ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } /** * ์—ฐ์† ์œ„์น˜ ์ถ”์  ์ข…๋ฃŒ */ private static async handleTrackingStop(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { console.log("๐Ÿ›‘ [handleTrackingStop] ์œ„์น˜ ์ถ”์  ์ข…๋ฃŒ:", { config, context }); // ์ถ”์  ์ค‘์ธ์ง€ ํ™•์ธ (์ƒˆ๋กœ๊ณ ์นจ ํ›„์—๋„ DB ์ƒํƒœ ๊ธฐ๋ฐ˜ ์ข…๋ฃŒ ๊ฐ€๋Šฅํ•˜๋„๋ก ์ˆ˜์ •) const isTrackingActive = !!this.trackingIntervalId; if (!isTrackingActive) { // ์ถ”์  ์ค‘์ด ์•„๋‹ˆ์–ด๋„ DB ์ƒํƒœ ๋ณ€๊ฒฝ์€ ์ง„ํ–‰ (์ƒˆ๋กœ๊ณ ์นจ ํ›„ ์ข…๋ฃŒ ์ง€์›) console.log("โš ๏ธ [handleTrackingStop] trackingIntervalId ์—†์Œ - DB ์ƒํƒœ ๊ธฐ๋ฐ˜ ์ข…๋ฃŒ ์ง„ํ–‰"); } else { // ํƒ€์ด๋จธ ์ •๋ฆฌ (์ถ”์  ์ค‘์ธ ๊ฒฝ์šฐ์—๋งŒ) clearInterval(this.trackingIntervalId); this.trackingIntervalId = null; } 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) { // DB ๊ฐ’ ์šฐ์„ , ์—†์œผ๋ฉด formData ์‚ฌ์šฉ const departure = dbDeparture || this.trackingContext?.formData?.[this.trackingConfig?.trackingDepartureField || "departure"] || 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 = dbVehicleId || this.trackingContext?.formData?.[this.trackingConfig?.trackingVehicleIdField || "vehicle_id"] || null; await this.saveLocationToHistory( tripId, departure, arrival, departureName, destinationName, vehicleId, "completed", ); } // ๐Ÿ†• ๊ฑฐ๋ฆฌ/์‹œ๊ฐ„ ๊ณ„์‚ฐ ๋ฐ ์ €์žฅ (์ถ”์  ์ค‘์ด์—ˆ๋˜ ๊ฒฝ์šฐ์—๋งŒ) if (isTrackingActive && tripId) { try { const tripStats = await this.calculateTripStats(tripId); // ์šดํ–‰ ํ†ต๊ณ„๋ฅผ ๋‘ ํ…Œ์ด๋ธ”์— ์ €์žฅ if (tripStats) { const distanceMeters = Math.round(tripStats.totalDistanceKm * 1000); // km โ†’ m const timeMinutes = tripStats.totalTimeMinutes; const userId = this.trackingUserId || context.userId; const { apiClient } = await import("@/lib/api/client"); // 1๏ธโƒฃ vehicle_location_history ๋งˆ์ง€๋ง‰ ๋ ˆ์ฝ”๋“œ์— ํ†ต๊ณ„ ์ €์žฅ (์ด๋ ฅ์šฉ) try { const lastRecordResponse = await apiClient.post( "/table-management/tables/vehicle_location_history/data", { page: 1, size: 1, search: { trip_id: tripId }, sortBy: "recorded_at", sortOrder: "desc", autoFilter: true, }, ); const lastRecordData = lastRecordResponse.data?.data?.data || lastRecordResponse.data?.data?.rows || []; if (lastRecordData.length > 0) { const lastRecordId = lastRecordData[0].id; console.log("๐Ÿ“ ๋งˆ์ง€๋ง‰ ๋ ˆ์ฝ”๋“œ ID:", lastRecordId); const historyUpdates = [ { field: "trip_distance", value: distanceMeters }, { field: "trip_time", value: timeMinutes }, { field: "trip_start", value: tripStats.startTime }, { field: "trip_end", value: tripStats.endTime }, ]; for (const update of historyUpdates) { await apiClient.put("/dynamic-form/update-field", { tableName: "vehicle_location_history", keyField: "id", keyValue: lastRecordId, updateField: update.field, updateValue: update.value, }); } } else { console.warn("โš ๏ธ trip_id์— ํ•ด๋‹นํ•˜๋Š” ๋ ˆ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ:", tripId); } } catch (historyError) { console.warn("โš ๏ธ vehicle_location_history ์ €์žฅ ์‹คํŒจ:", historyError); } // 2๏ธโƒฃ vehicles ํ…Œ์ด๋ธ”์—๋„ ๋งˆ์ง€๋ง‰ ์šดํ–‰ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ (์ตœ์‹  ์ •๋ณด์šฉ) if (userId) { try { const vehicleUpdates = [ { field: "last_trip_distance", value: distanceMeters }, { field: "last_trip_time", value: timeMinutes }, { field: "last_trip_start", value: tripStats.startTime }, { field: "last_trip_end", value: tripStats.endTime }, ]; for (const update of vehicleUpdates) { await apiClient.put("/dynamic-form/update-field", { tableName: "vehicles", keyField: "user_id", keyValue: userId, updateField: update.field, updateValue: update.value, }); } } catch (vehicleError) { console.warn("โš ๏ธ vehicles ํ…Œ์ด๋ธ” ์ €์žฅ ์‹คํŒจ:", vehicleError); } } // ์ด๋ฒคํŠธ๋กœ ํ†ต๊ณ„ ์ „๋‹ฌ (UI์—์„œ ํ‘œ์‹œ์šฉ) window.dispatchEvent( new CustomEvent("tripCompleted", { detail: { tripId, totalDistanceKm: tripStats.totalDistanceKm, totalTimeMinutes: tripStats.totalTimeMinutes, startTime: tripStats.startTime, endTime: tripStats.endTime, }, }), ); toast.success( `์šดํ–‰ ์ข…๋ฃŒ! ์ด ${tripStats.totalDistanceKm.toFixed(1)}km, ${tripStats.totalTimeMinutes}๋ถ„ ์†Œ์š”`, ); } } catch (statsError) { console.warn("โš ๏ธ ์šดํ–‰ ํ†ต๊ณ„ ๊ณ„์‚ฐ ์‹คํŒจ:", statsError); } } // ์ƒํƒœ ๋ณ€๊ฒฝ (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 { const { apiClient } = await import("@/lib/api/client"); const statusTableName = effectiveConfig.trackingStatusTableName || effectiveContext.tableName; const keyField = effectiveConfig.trackingStatusKeyField || "user_id"; const keyValue = resolveSpecialKeyword( effectiveConfig.trackingStatusKeySourceField || "__userId__", effectiveContext, ); if (keyValue) { await apiClient.put("/dynamic-form/update-field", { tableName: statusTableName, keyField: keyField, keyValue: keyValue, updateField: effectiveConfig.trackingStatusField, updateValue: effectiveConfig.trackingStatusOnStop, }); // ๐Ÿ†• ์šดํ–‰ ์ข…๋ฃŒ ์‹œ vehicles ํ…Œ์ด๋ธ”์˜ ์ถœ๋ฐœ์ง€/๋„์ฐฉ์ง€/์œ„๋„/๊ฒฝ๋„๋ฅผ null๋กœ ์ดˆ๊ธฐํ™” const fieldsToReset = ["departure", "arrival", "latitude", "longitude"]; for (const field of fieldsToReset) { try { await apiClient.put("/dynamic-form/update-field", { tableName: statusTableName, keyField: keyField, keyValue: keyValue, updateField: field, updateValue: null, }); } catch { // ์ปฌ๋Ÿผ์ด ์—†์œผ๋ฉด ๋ฌด์‹œ } } } } catch (statusError) { console.warn("โš ๏ธ ์ƒํƒœ ๋ณ€๊ฒฝ ์‹คํŒจ:", statusError); } } // ์ปจํ…์ŠคํŠธ ์ •๋ฆฌ this.currentTripId = null; this.trackingContext = null; this.trackingConfig = null; toast.success(config.successMessage || "์œ„์น˜ ์ถ”์ ์ด ์ข…๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); // ์ถ”์  ์ข…๋ฃŒ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (UI ์—…๋ฐ์ดํŠธ์šฉ) window.dispatchEvent( new CustomEvent("trackingStopped", { detail: { tripId }, }), ); // ํ™”๋ฉด ์ƒˆ๋กœ๊ณ ์นจ context.onRefresh?.(); return true; } catch (error: any) { console.error("โŒ ์œ„์น˜ ์ถ”์  ์ข…๋ฃŒ ์‹คํŒจ:", error); toast.error(config.errorMessage || "์œ„์น˜ ์ถ”์  ์ข…๋ฃŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } /** * ์šดํ–‰ ํ†ต๊ณ„ ๊ณ„์‚ฐ (๊ฑฐ๋ฆฌ, ์‹œ๊ฐ„) */ private static async calculateTripStats(tripId: string): Promise<{ totalDistanceKm: number; totalTimeMinutes: number; startTime: string | null; endTime: string | null; } | null> { try { // vehicle_location_history์—์„œ ํ•ด๋‹น trip์˜ ๋ชจ๋“  ์œ„์น˜ ์กฐํšŒ const { apiClient } = await import("@/lib/api/client"); const response = await apiClient.post("/table-management/tables/vehicle_location_history/data", { page: 1, size: 10000, search: { trip_id: tripId }, sortBy: "recorded_at", sortOrder: "asc", }); if (!response.data?.success) { return null; } // ์‘๋‹ต ํ˜•์‹: data.data.data ๋˜๋Š” data.data.rows const rows = response.data?.data?.data || response.data?.data?.rows || []; if (!rows.length) { return null; } const locations = rows; // ์‹œ๊ฐ„ ๊ณ„์‚ฐ const startTime = locations[0].recorded_at; const endTime = locations[locations.length - 1].recorded_at; const totalTimeMs = new Date(endTime).getTime() - new Date(startTime).getTime(); const totalTimeMinutes = Math.round(totalTimeMs / 60000); // ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ (Haversine ๊ณต์‹) let totalDistanceM = 0; for (let i = 1; i < locations.length; i++) { const prev = locations[i - 1]; const curr = locations[i]; if (prev.latitude && prev.longitude && curr.latitude && curr.longitude) { const distance = this.calculateDistance( parseFloat(prev.latitude), parseFloat(prev.longitude), parseFloat(curr.latitude), parseFloat(curr.longitude), ); totalDistanceM += distance; } } const totalDistanceKm = totalDistanceM / 1000; return { totalDistanceKm, totalTimeMinutes, startTime, endTime, }; } catch (error) { console.error("โŒ ์šดํ–‰ ํ†ต๊ณ„ ๊ณ„์‚ฐ ์˜ค๋ฅ˜:", error); return null; } } /** * ๋‘ ์ขŒํ‘œ ๊ฐ„ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ (Haversine ๊ณต์‹, ๋ฏธํ„ฐ ๋‹จ์œ„) */ private static calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { const R = 6371000; // ์ง€๊ตฌ ๋ฐ˜๊ฒฝ (๋ฏธํ„ฐ) const dLat = ((lat2 - lat1) * Math.PI) / 180; const dLon = ((lon2 - lon1) * Math.PI) / 180; const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } /** * ์œ„์น˜ ์ด๋ ฅ ํ…Œ์ด๋ธ”์— ์ €์žฅ (๋‚ด๋ถ€ ํ—ฌํผ) * + vehicles ํ…Œ์ด๋ธ”์˜ latitude/longitude๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ */ private static async saveLocationToHistory( tripId: string | null, departure: string | null, arrival: string | null, departureName: string | null, destinationName: string | null, vehicleId: number | null, tripStatus: string = "active", ): Promise { return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( async (position) => { try { const { apiClient } = await import("@/lib/api/client"); const { latitude, longitude, accuracy, altitude, speed, heading } = position.coords; const locationData = { latitude, longitude, accuracy, altitude, speed, heading, tripId, tripStatus, departure, arrival, departureName, destinationName, recordedAt: new Date(position.timestamp).toISOString(), vehicleId, }; console.log("๐Ÿ“ [saveLocationToHistory] ์œ„์น˜ ์ €์žฅ:", locationData); // 1. vehicle_location_history์— ์ €์žฅ const response = await apiClient.post("/dynamic-form/location-history", locationData); if (response.data?.success) { } else { console.warn("โš ๏ธ ์œ„์น˜ ์ด๋ ฅ ์ €์žฅ ์‹คํŒจ:", response.data); } // 2. vehicles ํ…Œ์ด๋ธ”์˜ latitude/longitude๋„ ์—…๋ฐ์ดํŠธ (์‹ค์‹œ๊ฐ„ ์œ„์น˜ ๋ฐ˜์˜) if (this.trackingContext && this.trackingConfig) { const keyField = this.trackingConfig.trackingStatusKeyField || "user_id"; const keySourceField = this.trackingConfig.trackingStatusKeySourceField || "__userId__"; const keyValue = resolveSpecialKeyword(keySourceField, this.trackingContext); const vehiclesTableName = this.trackingConfig.trackingStatusTableName || "vehicles"; if (keyValue) { try { // latitude ์—…๋ฐ์ดํŠธ await apiClient.put("/dynamic-form/update-field", { tableName: vehiclesTableName, keyField, keyValue, updateField: "latitude", updateValue: latitude, }); // longitude ์—…๋ฐ์ดํŠธ await apiClient.put("/dynamic-form/update-field", { tableName: vehiclesTableName, keyField, keyValue, updateField: "longitude", updateValue: longitude, }); } catch (vehicleUpdateError) { // ์ปฌ๋Ÿผ์ด ์—†์œผ๋ฉด ์กฐ์šฉํžˆ ๋ฌด์‹œ console.warn("โš ๏ธ vehicles ํ…Œ์ด๋ธ” ์œ„์น˜ ์—…๋ฐ์ดํŠธ ์‹คํŒจ (๋ฌด์‹œ):", vehicleUpdateError); } } } resolve(); } catch (error) { console.error("โŒ ์œ„์น˜ ์ด๋ ฅ ์ €์žฅ ์˜ค๋ฅ˜:", error); reject(error); } }, (error) => { console.error("โŒ ์œ„์น˜ ํš๋“ ์‹คํŒจ:", error.message); reject(error); }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0, }, ); }); } /** * ํ˜„์žฌ ์ถ”์  ์ƒํƒœ ํ™•์ธ (์™ธ๋ถ€์—์„œ ํ˜ธ์ถœ ๊ฐ€๋Šฅ) */ static isTracking(): boolean { return this.trackingIntervalId !== null; } /** * ํ˜„์žฌ Trip ID ๊ฐ€์ ธ์˜ค๊ธฐ (์™ธ๋ถ€์—์„œ ํ˜ธ์ถœ ๊ฐ€๋Šฅ) */ static getCurrentTripId(): string | null { return this.currentTripId; } /** * ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์•ก์…˜ ์ฒ˜๋ฆฌ (๋ถ„ํ•  ํŒจ๋„์—์„œ ์ขŒ์ธก โ†’ ์šฐ์ธก ๋ฐ์ดํ„ฐ ์ „๋‹ฌ) */ private static async handleTransferData(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { // ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ ํ™•์ธ const selectedRows = context.selectedRowsData || context.flowSelectedData || []; if (!selectedRows || selectedRows.length === 0) { toast.error("์ „๋‹ฌํ•  ๋ฐ์ดํ„ฐ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return false; } // dataTransfer ์„ค์ • ํ™•์ธ const dataTransfer = config.dataTransfer; if (!dataTransfer) { // dataTransfer ์„ค์ •์ด ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋™์ž‘: ์ „์—ญ ์ด๋ฒคํŠธ๋กœ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ const transferEvent = new CustomEvent("splitPanelDataTransfer", { detail: { data: selectedRows, mode: "append", sourcePosition: "left", }, }); window.dispatchEvent(transferEvent); toast.success(`${selectedRows.length}๊ฐœ ํ•ญ๋ชฉ์ด ์ „๋‹ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); return true; } // dataTransfer ์„ค์ •์ด ์žˆ๋Š” ๊ฒฝ์šฐ const { targetType, targetComponentId, targetScreenId, mappingRules, receiveMode } = dataTransfer; if (targetType === "component" && targetComponentId) { // ๊ฐ™์€ ํ™”๋ฉด ๋‚ด ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌ const transferEvent = new CustomEvent("componentDataTransfer", { detail: { targetComponentId, data: selectedRows, mappingRules, mode: receiveMode || "append", }, }); window.dispatchEvent(transferEvent); toast.success(`${selectedRows.length}๊ฐœ ํ•ญ๋ชฉ์ด ์ „๋‹ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); return true; } else if (targetType === "screen" && targetScreenId) { // ๋‹ค๋ฅธ ํ™”๋ฉด์œผ๋กœ ์ „๋‹ฌ (๋ถ„ํ•  ํŒจ๋„ ๋“ฑ) const transferEvent = new CustomEvent("screenDataTransfer", { detail: { targetScreenId, data: selectedRows, mappingRules, mode: receiveMode || "append", }, }); window.dispatchEvent(transferEvent); toast.success(`${selectedRows.length}๊ฐœ ํ•ญ๋ชฉ์ด ์ „๋‹ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); return true; } else { // ๊ธฐ๋ณธ: ๋ถ„ํ•  ํŒจ๋„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ด๋ฒคํŠธ const transferEvent = new CustomEvent("splitPanelDataTransfer", { detail: { data: selectedRows, mappingRules, mode: receiveMode || "append", sourcePosition: "left", }, }); window.dispatchEvent(transferEvent); toast.success(`${selectedRows.length}๊ฐœ ํ•ญ๋ชฉ์ด ์ „๋‹ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); return true; } } catch (error: any) { console.error("โŒ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์‹คํŒจ:", error); toast.error(error.message || "๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } // ๊ณต์ฐจ ์ถ”์ ์šฉ watchId ์ €์žฅ private static emptyVehicleWatchId: number | null = null; private static emptyVehicleTripId: string | null = null; /** * ๊ณต์ฐจ๋“ฑ๋ก ์•ก์…˜ ์ฒ˜๋ฆฌ * - ์œ„์น˜ ์ˆ˜์ง‘ + ์ƒํƒœ ๋ณ€๊ฒฝ (์˜ˆ: status โ†’ inactive) * - ์—ฐ์† ์œ„์น˜ ์ถ”์  ์‹œ์ž‘ (vehicle_location_history์— ์ €์žฅ) */ private static async handleEmptyVehicle(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { console.log("๐Ÿ“ ๊ณต์ฐจ๋“ฑ๋ก ์•ก์…˜ ์‹คํ–‰:", { config, context }); // ๋ธŒ๋ผ์šฐ์ € Geolocation API ์ง€์› ํ™•์ธ if (!navigator.geolocation) { toast.error("์ด ๋ธŒ๋ผ์šฐ์ €๋Š” ์œ„์น˜์ •๋ณด๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); return false; } // ์œ„๋„/๊ฒฝ๋„ ์ €์žฅ ํ•„๋“œ ํ™•์ธ const latField = config.geolocationLatField || "latitude"; const lngField = config.geolocationLngField || "longitude"; // ๋กœ๋”ฉ ํ† ์ŠคํŠธ ํ‘œ์‹œ const loadingToastId = toast.loading("์œ„์น˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘..."); // Geolocation ์˜ต์…˜ ์„ค์ • const options: PositionOptions = { enableHighAccuracy: config.geolocationHighAccuracy !== false, timeout: config.geolocationTimeout || 10000, maximumAge: config.geolocationMaxAge || 0, }; // ์œ„์น˜ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ const position = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition(resolve, reject, options); }); toast.dismiss(loadingToastId); const { latitude, longitude, accuracy, speed, heading, altitude } = position.coords; const timestamp = new Date(position.timestamp); console.log("๐Ÿ“ ์œ„์น˜์ •๋ณด ํš๋“ ์„ฑ๊ณต:", { latitude, longitude, accuracy }); // ํผ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ const updates: Record = { [latField]: latitude, [lngField]: longitude, }; if (config.geolocationAccuracyField && accuracy !== null) { updates[config.geolocationAccuracyField] = accuracy; } if (config.geolocationTimestampField) { updates[config.geolocationTimestampField] = timestamp.toISOString(); } // onFormDataChange๋กœ ํผ ์—…๋ฐ์ดํŠธ if (context.onFormDataChange) { Object.entries(updates).forEach(([field, value]) => { context.onFormDataChange!(field, value); }); } // ๐Ÿ†• ์ž๋™ ์ €์žฅ ์˜ต์…˜์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ DB UPDATE if (config.geolocationAutoSave) { const keyField = config.geolocationKeyField || "user_id"; const keySourceField = config.geolocationKeySourceField || "__userId__"; const keyValue = resolveSpecialKeyword(keySourceField, context); const targetTableName = config.geolocationTableName || context.tableName; if (keyValue && targetTableName) { try { const { apiClient } = await import("@/lib/api/client"); // ์œ„์น˜ ์ •๋ณด ํ•„๋“œ๋“ค ์—…๋ฐ์ดํŠธ (์œ„๋„, ๊ฒฝ๋„, ์ •ํ™•๋„, ํƒ€์ž„์Šคํƒฌํ”„) const fieldsToUpdate = { ...updates }; // formData์—์„œ departure, arrival๋งŒ ํฌํ•จ (ํ…Œ์ด๋ธ”์— ์žˆ์„ ๊ฐ€๋Šฅ์„ฑ ๋†’์€ ํ•„๋“œ๋งŒ) if (context.formData?.departure) fieldsToUpdate.departure = context.formData.departure; if (context.formData?.arrival) fieldsToUpdate.arrival = context.formData.arrival; // ์ถ”๊ฐ€ ํ•„๋“œ ๋ณ€๊ฒฝ (status ๋“ฑ) if (config.geolocationExtraField && config.geolocationExtraValue !== undefined) { fieldsToUpdate[config.geolocationExtraField] = config.geolocationExtraValue; } console.log("๐Ÿ“ DB UPDATE ์‹œ์ž‘:", { targetTableName, keyField, keyValue, fieldsToUpdate }); // ๊ฐ ํ•„๋“œ๋ฅผ ๊ฐœ๋ณ„์ ์œผ๋กœ UPDATE (์—๋Ÿฌ ๋ฌด์‹œ) let successCount = 0; for (const [field, value] of Object.entries(fieldsToUpdate)) { try { const response = await apiClient.put("/dynamic-form/update-field", { tableName: targetTableName, keyField, keyValue, updateField: field, updateValue: value, }); if (response.data?.success) { successCount++; } } catch { // ์ปฌ๋Ÿผ์ด ์—†์œผ๋ฉด ์กฐ์šฉํžˆ ๋ฌด์‹œ (์—๋Ÿฌ ๋กœ๊ทธ ์•ˆ ์ฐ์Œ) } } console.log(`๐Ÿ“ DB UPDATE ์™„๋ฃŒ: ${successCount}/${Object.keys(fieldsToUpdate).length} ํ•„๋“œ ์ €์žฅ๋จ`); // ๐Ÿ†• ์—ฐ์† ์œ„์น˜ ์ถ”์  ์‹œ์ž‘ (๊ณต์ฐจ ์ƒํƒœ์—์„œ๋„ ์œ„์น˜ ๊ธฐ๋ก) if (config.emptyVehicleTracking !== false) { await this.startEmptyVehicleTracking(config, context, { latitude, longitude, accuracy, speed, heading, altitude, }); } toast.success(config.successMessage || "๊ณต์ฐจ ๋“ฑ๋ก์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์œ„์น˜ ์ถ”์ ์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค."); } catch (saveError) { console.error("โŒ ์œ„์น˜์ •๋ณด ์ž๋™ ์ €์žฅ ์‹คํŒจ:", saveError); toast.error("์œ„์น˜ ์ •๋ณด ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } else { console.warn("โš ๏ธ ํ‚ค ๊ฐ’ ๋˜๋Š” ํ…Œ์ด๋ธ”๋ช…์ด ์—†์–ด์„œ ์ž๋™ ์ €์žฅ์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค:", { keyValue, targetTableName }); toast.success(config.successMessage || `์œ„์น˜: ${latitude.toFixed(6)}, ${longitude.toFixed(6)}`); } } else { // ์ž๋™ ์ €์žฅ ์—†์ด ์„ฑ๊ณต ๋ฉ”์‹œ์ง€๋งŒ toast.success(config.successMessage || `์œ„์น˜: ${latitude.toFixed(6)}, ${longitude.toFixed(6)}`); } return true; } catch (error: any) { console.error("โŒ ๊ณต์ฐจ๋“ฑ๋ก ์‹คํŒจ:", error); toast.dismiss(); // GeolocationPositionError ์ฒ˜๋ฆฌ if (error.code) { switch (error.code) { case 1: // PERMISSION_DENIED toast.error("์œ„์น˜ ์ •๋ณด ์ ‘๊ทผ์ด ๊ฑฐ๋ถ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\n๋ธŒ๋ผ์šฐ์ € ์„ค์ •์—์„œ ์œ„์น˜ ๊ถŒํ•œ์„ ํ—ˆ์šฉํ•ด์ฃผ์„ธ์š”."); break; case 2: // POSITION_UNAVAILABLE toast.error("์œ„์น˜ ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.\nGPS ์‹ ํ˜ธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”."); break; case 3: // TIMEOUT toast.error("์œ„์น˜ ์ •๋ณด ์š”์ฒญ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\n๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."); break; default: toast.error(config.errorMessage || "์œ„์น˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); } } else { toast.error(config.errorMessage || "์œ„์น˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); } return false; } } /** * ๊ณต์ฐจ ์ƒํƒœ์—์„œ ์—ฐ์† ์œ„์น˜ ์ถ”์  ์‹œ์ž‘ */ private static async startEmptyVehicleTracking( config: ButtonActionConfig, context: ButtonActionContext, initialPosition: { latitude: number; longitude: number; accuracy: number | null; speed: number | null; heading: number | null; altitude: number | null; }, ): Promise { try { // ๊ธฐ์กด ์ถ”์ ์ด ์žˆ์œผ๋ฉด ์ค‘์ง€ if (this.emptyVehicleWatchId !== null) { navigator.geolocation.clearWatch(this.emptyVehicleWatchId); this.emptyVehicleWatchId = null; } const { apiClient } = await import("@/lib/api/client"); // Trip ID ์ƒ์„ฑ (๊ณต์ฐจ์šฉ) const tripId = `EMPTY-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`; this.emptyVehicleTripId = tripId; const userId = context.userId || ""; const companyCode = context.companyCode || ""; const departure = context.formData?.departure || ""; const arrival = context.formData?.arrival || ""; const departureName = context.formData?.departure_name || ""; const destinationName = context.formData?.destination_name || ""; // ์‹œ์ž‘ ์œ„์น˜ ๊ธฐ๋ก try { await apiClient.post("/dynamic-form/location-history", { tripId, userId, latitude: initialPosition.latitude, longitude: initialPosition.longitude, accuracy: initialPosition.accuracy, speed: initialPosition.speed, heading: initialPosition.heading, altitude: initialPosition.altitude, tripStatus: "empty_start", // ๊ณต์ฐจ ์‹œ์ž‘ departure, arrival, departureName, destinationName, companyCode, }); console.log("๐Ÿ“ ๊ณต์ฐจ ์‹œ์ž‘ ์œ„์น˜ ๊ธฐ๋ก ์™„๋ฃŒ:", tripId); } catch (err) { console.warn("โš ๏ธ ๊ณต์ฐจ ์‹œ์ž‘ ์œ„์น˜ ๊ธฐ๋ก ์‹คํŒจ (ํ…Œ์ด๋ธ” ์—†์„ ์ˆ˜ ์žˆ์Œ):", err); } // ์ถ”์  ๊ฐ„๊ฒฉ (๊ธฐ๋ณธ 10์ดˆ) const trackingInterval = config.emptyVehicleTrackingInterval || 10000; // watchPosition์œผ๋กœ ์—ฐ์† ์ถ”์  this.emptyVehicleWatchId = navigator.geolocation.watchPosition( async (position) => { const { latitude, longitude, accuracy, speed, heading, altitude } = position.coords; try { await apiClient.post("/dynamic-form/location-history", { tripId: this.emptyVehicleTripId, userId, latitude, longitude, accuracy, speed, heading, altitude, tripStatus: "empty_tracking", // ๊ณต์ฐจ ์ถ”์  ์ค‘ departure, arrival, departureName, destinationName, companyCode, }); console.log("๐Ÿ“ ๊ณต์ฐจ ์œ„์น˜ ๊ธฐ๋ก:", { latitude: latitude.toFixed(6), longitude: longitude.toFixed(6) }); } catch (err) { console.warn("โš ๏ธ ๊ณต์ฐจ ์œ„์น˜ ๊ธฐ๋ก ์‹คํŒจ:", err); } }, (error) => { console.error("โŒ ๊ณต์ฐจ ์œ„์น˜ ์ถ”์  ์˜ค๋ฅ˜:", error.message); }, { enableHighAccuracy: true, timeout: trackingInterval, maximumAge: 0, }, ); console.log("๐Ÿš— ๊ณต์ฐจ ์œ„์น˜ ์ถ”์  ์‹œ์ž‘:", { tripId, watchId: this.emptyVehicleWatchId }); } catch (error) { console.error("โŒ ๊ณต์ฐจ ์œ„์น˜ ์ถ”์  ์‹œ์ž‘ ์‹คํŒจ:", error); } } /** * ๊ณต์ฐจ ์œ„์น˜ ์ถ”์  ์ค‘์ง€ (์šดํ–‰ ์ „ํ™˜ ์‹œ ํ˜ธ์ถœ) */ public static stopEmptyVehicleTracking(): void { if (this.emptyVehicleWatchId !== null) { navigator.geolocation.clearWatch(this.emptyVehicleWatchId); console.log("๐Ÿ›‘ ๊ณต์ฐจ ์œ„์น˜ ์ถ”์  ์ค‘์ง€:", { tripId: this.emptyVehicleTripId, watchId: this.emptyVehicleWatchId }); this.emptyVehicleWatchId = null; this.emptyVehicleTripId = null; } } /** * ํ˜„์žฌ ๊ณต์ฐจ ์ถ”์  Trip ID ๋ฐ˜ํ™˜ */ public static getEmptyVehicleTripId(): string | null { return this.emptyVehicleTripId; } /** * ํ•„๋“œ ๊ฐ’ ๊ตํ™˜ ์•ก์…˜ ์ฒ˜๋ฆฌ (์˜ˆ: ์ถœ๋ฐœ์ง€ โ†” ๋„์ฐฉ์ง€) */ private static async handleSwapFields(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { const { formData, onFormDataChange } = context; // ๊ตํ™˜ํ•  ํ•„๋“œ ํ™•์ธ const fieldA = config.swapFieldA; const fieldB = config.swapFieldB; if (!fieldA || !fieldB) { toast.error("๊ตํ™˜ํ•  ํ•„๋“œ๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); return false; } // ํ˜„์žฌ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ const valueA = formData?.[fieldA]; const valueB = formData?.[fieldB]; // ๊ฐ’ ๊ตํ™˜ if (onFormDataChange) { onFormDataChange(fieldA, valueB); onFormDataChange(fieldB, valueA); } // ๊ด€๋ จ ํ•„๋“œ๋„ ํ•จ๊ป˜ ๊ตํ™˜ (์˜ˆ: ์œ„๋„/๊ฒฝ๋„) if (config.swapRelatedFields && config.swapRelatedFields.length > 0) { for (const related of config.swapRelatedFields) { const relatedValueA = formData?.[related.fieldA]; const relatedValueB = formData?.[related.fieldB]; if (onFormDataChange) { onFormDataChange(related.fieldA, relatedValueB); onFormDataChange(related.fieldB, relatedValueA); } } } toast.success(config.successMessage || "๊ฐ’์ด ๊ตํ™˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); return true; } catch (error) { console.error("โŒ ํ•„๋“œ ๊ฐ’ ๊ตํ™˜ ์˜ค๋ฅ˜:", error); toast.error(config.errorMessage || "๊ฐ’ ๊ตํ™˜ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } /** * ์ฆ‰์‹œ ์ €์žฅ (Quick Insert) ์•ก์…˜ ์ฒ˜๋ฆฌ * ํ™”๋ฉด์—์„œ ์„ ํƒํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ํŠน์ • ํ…Œ์ด๋ธ”์— ์ฆ‰์‹œ ์ €์žฅ */ private static async handleQuickInsert(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { const quickInsertConfig = config.quickInsertConfig; if (!quickInsertConfig?.targetTable) { toast.error("๋Œ€์ƒ ํ…Œ์ด๋ธ”์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); return false; } // โœ… allComponents๊ฐ€ ์žˆ์œผ๋ฉด ๊ธฐ์กด ํ•„์ˆ˜ ํ•ญ๋ชฉ ๊ฒ€์ฆ ์ˆ˜ํ–‰ if (context.allComponents && context.allComponents.length > 0) { const requiredValidation = this.validateRequiredFields(context); if (!requiredValidation.isValid) { console.log("โŒ [handleQuickInsert] ํ•„์ˆ˜ ํ•ญ๋ชฉ ๋ˆ„๋ฝ:", requiredValidation.missingFields); toast.error(`ํ•„์ˆ˜ ํ•ญ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”: ${requiredValidation.missingFields.join(", ")}`); return false; } } // โœ… quickInsert ์ „์šฉ ๊ฒ€์ฆ: component ํƒ€์ž… ๋งคํ•‘์—์„œ ๊ฐ’์ด ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธ const mappingsForValidation = quickInsertConfig.columnMappings || []; const missingMappingFields: string[] = []; for (const mapping of mappingsForValidation) { // component ํƒ€์ž… ๋งคํ•‘์€ ํ•„์ˆ˜ ์ž…๋ ฅ์œผ๋กœ ๊ฐ„์ฃผ if (mapping.sourceType === "component" && mapping.sourceComponentId) { let value: any = undefined; // ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (formData์—์„œ) if (mapping.sourceColumnName) { value = context.formData?.[mapping.sourceColumnName]; } if (value === undefined || value === null) { value = context.formData?.[mapping.sourceComponentId]; } // allComponents์—์„œ ์ปดํฌ๋„ŒํŠธ ์ฐพ์•„์„œ columnName์œผ๋กœ ์‹œ๋„ if ((value === undefined || value === null) && context.allComponents) { const comp = context.allComponents.find((c: any) => c.id === mapping.sourceComponentId); if (comp?.columnName) { value = context.formData?.[comp.columnName]; } } // targetColumn์œผ๋กœ ํด๋ฐฑ if ((value === undefined || value === null) && mapping.targetColumn) { value = context.formData?.[mapping.targetColumn]; } // ๊ฐ’์ด ๋น„์–ด์žˆ์œผ๋ฉด ํ•„์ˆ˜ ๋ˆ„๋ฝ์œผ๋กœ ์ฒ˜๋ฆฌ if (value === undefined || value === null || (typeof value === "string" && value.trim() === "")) { console.log("โŒ [handleQuickInsert] component ๋งคํ•‘ ๊ฐ’ ๋ˆ„๋ฝ:", { targetColumn: mapping.targetColumn, sourceComponentId: mapping.sourceComponentId, sourceColumnName: mapping.sourceColumnName, value, }); missingMappingFields.push(mapping.targetColumn); } } } if (missingMappingFields.length > 0) { console.log("โŒ [handleQuickInsert] ํ•„์ˆ˜ ์ž…๋ ฅ ํ•ญ๋ชฉ ๋ˆ„๋ฝ:", missingMappingFields); toast.error(`๋‹ค์Œ ํ•ญ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”: ${missingMappingFields.join(", ")}`); return false; } const { formData, splitPanelContext, userId, userName, companyCode } = context; // ์ปฌ๋Ÿผ ๋งคํ•‘์— ๋”ฐ๋ผ ์ €์žฅํ•  ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ const insertData: Record = {}; const columnMappings = quickInsertConfig.columnMappings || []; for (const mapping of columnMappings) { console.log("๐Ÿ“ ๋งคํ•‘ ์ฒ˜๋ฆฌ ์‹œ์ž‘:", mapping); if (!mapping.targetColumn) { console.log("๐Ÿ“ targetColumn ์—†์Œ, ์Šคํ‚ต"); continue; } let value: any = undefined; switch (mapping.sourceType) { case "component": console.log("๐Ÿ“ component ํƒ€์ž… ์ฒ˜๋ฆฌ:", { sourceComponentId: mapping.sourceComponentId, sourceColumnName: mapping.sourceColumnName, targetColumn: mapping.targetColumn, }); // ์ปดํฌ๋„ŒํŠธ์˜ ํ˜„์žฌ ๊ฐ’ if (mapping.sourceComponentId) { // 1. sourceColumnName์ด ์žˆ์œผ๋ฉด ์ง์ ‘ ์‚ฌ์šฉ (๊ฐ€์žฅ ํ™•์‹คํ•œ ๋ฐฉ๋ฒ•) if (mapping.sourceColumnName) { value = formData?.[mapping.sourceColumnName]; console.log(`๐Ÿ“ ๋ฐฉ๋ฒ•1 (sourceColumnName): ${mapping.sourceColumnName} = ${value}`); } // 2. ์—†์œผ๋ฉด ์ปดํฌ๋„ŒํŠธ ID๋กœ ์ง์ ‘ ์ฐพ๊ธฐ if (value === undefined) { value = formData?.[mapping.sourceComponentId]; console.log(`๐Ÿ“ ๋ฐฉ๋ฒ•2 (sourceComponentId): ${mapping.sourceComponentId} = ${value}`); } // 3. ์—†์œผ๋ฉด allComponents์—์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ฐพ์•„ columnName์œผ๋กœ ์‹œ๋„ if (value === undefined && context.allComponents) { const comp = context.allComponents.find((c: any) => c.id === mapping.sourceComponentId); console.log("๐Ÿ“ ๋ฐฉ๋ฒ•3 ์ฐพ์€ ์ปดํฌ๋„ŒํŠธ:", comp); if (comp?.columnName) { value = formData?.[comp.columnName]; console.log(`๐Ÿ“ ๋ฐฉ๋ฒ•3 (allComponents): ${mapping.sourceComponentId} โ†’ ${comp.columnName} = ${value}`); } } // 4. targetColumn๊ณผ ๊ฐ™์€ ์ด๋ฆ„์˜ ํ‚ค๊ฐ€ formData์— ์žˆ์œผ๋ฉด ์‚ฌ์šฉ (ํด๋ฐฑ) if (value === undefined && mapping.targetColumn && formData?.[mapping.targetColumn] !== undefined) { value = formData[mapping.targetColumn]; console.log(`๐Ÿ“ ๋ฐฉ๋ฒ•4 (targetColumn ํด๋ฐฑ): ${mapping.targetColumn} = ${value}`); } // 5. ๊ทธ๋ž˜๋„ ์—†์œผ๋ฉด formData์˜ ๋ชจ๋“  ํ‚ค๋ฅผ ํ™•์ธํ•˜๊ณ  ๋กœ๊น… if (value === undefined) { console.log("๐Ÿ“ ๋ฐฉ๋ฒ•5: formData์—์„œ ๊ฐ’์„ ์ฐพ์ง€ ๋ชปํ•จ. formData ํ‚ค๋“ค:", Object.keys(formData || {})); } // sourceColumn์ด ์ง€์ •๋œ ๊ฒฝ์šฐ ํ•ด๋‹น ์†์„ฑ ์ถ”์ถœ if (mapping.sourceColumn && value && typeof value === "object") { value = value[mapping.sourceColumn]; console.log(`๐Ÿ“ sourceColumn ์ถ”์ถœ: ${mapping.sourceColumn} = ${value}`); } } break; case "leftPanel": console.log("๐Ÿ“ leftPanel ํƒ€์ž… ์ฒ˜๋ฆฌ:", { sourceColumn: mapping.sourceColumn, selectedLeftData: splitPanelContext?.selectedLeftData, }); // ์ขŒ์ธก ํŒจ๋„ ์„ ํƒ ๋ฐ์ดํ„ฐ if (mapping.sourceColumn && splitPanelContext?.selectedLeftData) { value = splitPanelContext.selectedLeftData[mapping.sourceColumn]; console.log(`๐Ÿ“ leftPanel ๊ฐ’: ${mapping.sourceColumn} = ${value}`); } break; case "fixed": console.log(`๐Ÿ“ fixed ํƒ€์ž… ์ฒ˜๋ฆฌ: fixedValue = ${mapping.fixedValue}`); // ๊ณ ์ •๊ฐ’ value = mapping.fixedValue; break; case "currentUser": console.log(`๐Ÿ“ currentUser ํƒ€์ž… ์ฒ˜๋ฆฌ: userField = ${mapping.userField}`); // ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ •๋ณด switch (mapping.userField) { case "userId": value = userId; break; case "userName": value = userName; break; case "companyCode": value = companyCode; break; } console.log(`๐Ÿ“ currentUser ๊ฐ’: ${value}`); break; default: console.log(`๐Ÿ“ ์•Œ ์ˆ˜ ์—†๋Š” sourceType: ${mapping.sourceType}`); } console.log(`๐Ÿ“ ๋งคํ•‘ ๊ฒฐ๊ณผ: targetColumn=${mapping.targetColumn}, value=${value}, type=${typeof value}`); if (value !== undefined && value !== null && value !== "") { insertData[mapping.targetColumn] = value; console.log(`๐Ÿ“ insertData์— ์ถ”๊ฐ€๋จ: ${mapping.targetColumn} = ${value}`); } else { console.log("๐Ÿ“ ๊ฐ’์ด ๋น„์–ด์žˆ์–ด์„œ insertData์— ์ถ”๊ฐ€ ์•ˆ๋จ"); } } // ๐Ÿ†• ์ขŒ์ธก ํŒจ๋„ ์„ ํƒ ๋ฐ์ดํ„ฐ์—์„œ ์ž๋™ ๋งคํ•‘ (๋Œ€์ƒ ํ…Œ์ด๋ธ”์— ์กด์žฌํ•˜๋Š” ์ปฌ๋Ÿผ๋งŒ) if (splitPanelContext?.selectedLeftData) { const leftData = splitPanelContext.selectedLeftData; console.log("๐Ÿ“ ์ขŒ์ธก ํŒจ๋„ ์ž๋™ ๋งคํ•‘ ์‹œ์ž‘:", leftData); // ๋Œ€์ƒ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ๋ชฉ๋ก ์กฐํšŒ let targetTableColumns: string[] = []; try { const columnsResponse = await apiClient.get( `/table-management/tables/${quickInsertConfig.targetTable}/columns`, ); if (columnsResponse.data?.success && columnsResponse.data?.data) { const columnsData = columnsResponse.data.data.columns || columnsResponse.data.data; targetTableColumns = columnsData.map((col: any) => col.columnName || col.column_name || col.name); console.log("๐Ÿ“ ๋Œ€์ƒ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋ชฉ๋ก:", targetTableColumns); } } catch (error) { console.error("๋Œ€์ƒ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์กฐํšŒ ์‹คํŒจ:", error); } for (const [key, val] of Object.entries(leftData)) { // ์ด๋ฏธ ๋งคํ•‘๋œ ์ปฌ๋Ÿผ์€ ์Šคํ‚ต if (insertData[key] !== undefined) { console.log(`๐Ÿ“ ์ž๋™ ๋งคํ•‘ ์Šคํ‚ต (์ด๋ฏธ ์กด์žฌ): ${key}`); continue; } // ๋Œ€์ƒ ํ…Œ์ด๋ธ”์— ํ•ด๋‹น ์ปฌ๋Ÿผ์ด ์—†์œผ๋ฉด ์Šคํ‚ต if (targetTableColumns.length > 0 && !targetTableColumns.includes(key)) { console.log(`๐Ÿ“ ์ž๋™ ๋งคํ•‘ ์Šคํ‚ต (๋Œ€์ƒ ํ…Œ์ด๋ธ”์— ์—†๋Š” ์ปฌ๋Ÿผ): ${key}`); continue; } // ์‹œ์Šคํ…œ ์ปฌ๋Ÿผ ์ œ์™ธ (id, created_date, updated_date, writer ๋“ฑ) const systemColumns = ["id", "created_date", "updated_date", "writer", "writer_name"]; if (systemColumns.includes(key)) { console.log(`๐Ÿ“ ์ž๋™ ๋งคํ•‘ ์Šคํ‚ต (์‹œ์Šคํ…œ ์ปฌ๋Ÿผ): ${key}`); continue; } // _label, _name ์œผ๋กœ ๋๋‚˜๋Š” ํ‘œ์‹œ์šฉ ์ปฌ๋Ÿผ ์ œ์™ธ if (key.endsWith("_label") || key.endsWith("_name")) { console.log(`๐Ÿ“ ์ž๋™ ๋งคํ•‘ ์Šคํ‚ต (ํ‘œ์‹œ์šฉ ์ปฌ๋Ÿผ): ${key}`); continue; } // ๊ฐ’์ด ์žˆ์œผ๋ฉด ์ž๋™ ์ถ”๊ฐ€ if (val !== undefined && val !== null && val !== "") { insertData[key] = val; console.log(`๐Ÿ“ ์ž๋™ ๋งคํ•‘ ์ถ”๊ฐ€: ${key} = ${val}`); } } } // ํ•„์ˆ˜ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ if (Object.keys(insertData).length === 0) { toast.error("์ €์žฅํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); return false; } // ์ค‘๋ณต ์ฒดํฌ console.log("๐Ÿ“ ์ค‘๋ณต ์ฒดํฌ ์„ค์ •:", { enabled: quickInsertConfig.duplicateCheck?.enabled, columns: quickInsertConfig.duplicateCheck?.columns, }); if (quickInsertConfig.duplicateCheck?.enabled && quickInsertConfig.duplicateCheck?.columns?.length > 0) { const duplicateCheckData: Record = {}; for (const col of quickInsertConfig.duplicateCheck.columns) { if (insertData[col] !== undefined) { // ๋ฐฑ์—”๋“œ๊ฐ€ { value, operator } ํ˜•์‹์„ ๊ธฐ๋Œ€ํ•˜๋ฏ€๋กœ ๋ณ€ํ™˜ duplicateCheckData[col] = { value: insertData[col], operator: "equals" }; } } console.log("๐Ÿ“ ์ค‘๋ณต ์ฒดํฌ ์กฐ๊ฑด:", duplicateCheckData); if (Object.keys(duplicateCheckData).length > 0) { try { const checkResponse = await apiClient.post( `/table-management/tables/${quickInsertConfig.targetTable}/data`, { page: 1, pageSize: 1, search: duplicateCheckData, }, ); console.log("๐Ÿ“ ์ค‘๋ณต ์ฒดํฌ ์‘๋‹ต:", checkResponse.data); // ์‘๋‹ต ๊ตฌ์กฐ: { success: true, data: { data: [...], total: N } } ๋˜๋Š” { success: true, data: [...] } const existingData = checkResponse.data?.data?.data || checkResponse.data?.data || []; console.log( "๐Ÿ“ ๊ธฐ์กด ๋ฐ์ดํ„ฐ:", existingData, "๊ธธ์ด:", Array.isArray(existingData) ? existingData.length : 0, ); if (Array.isArray(existingData) && existingData.length > 0) { toast.error(quickInsertConfig.duplicateCheck.errorMessage || "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค."); return false; } } catch (error) { console.error("์ค‘๋ณต ์ฒดํฌ ์˜ค๋ฅ˜:", error); // ์ค‘๋ณต ์ฒดํฌ ์‹คํŒจํ•ด๋„ ์ €์žฅ์€ ์‹œ๋„ } } } else { console.log("๐Ÿ“ ์ค‘๋ณต ์ฒดํฌ ๋น„ํ™œ์„ฑํ™” ๋˜๋Š” ์ปฌ๋Ÿผ ๋ฏธ์„ค์ •"); } // ๋ฐ์ดํ„ฐ ์ €์žฅ const response = await apiClient.post( `/table-management/tables/${quickInsertConfig.targetTable}/add`, insertData, ); if (response.data?.success) { // ์ €์žฅ ํ›„ ๋™์ž‘ ์„ค์ • ๋กœ๊ทธ console.log("๐Ÿ“ afterInsert ์„ค์ •:", quickInsertConfig.afterInsert); // ๐Ÿ†• ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ (ํ…Œ์ด๋ธ”๋ฆฌ์ŠคํŠธ, ์นด๋“œ ๋””์Šคํ”Œ๋ ˆ์ด ์ปดํฌ๋„ŒํŠธ ์ƒˆ๋กœ๊ณ ์นจ) // refreshData๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ false๊ฐ€ ์•„๋‹ˆ๋ฉด ๊ธฐ๋ณธ์ ์œผ๋กœ ์ƒˆ๋กœ๊ณ ์นจ ์‹คํ–‰ const shouldRefresh = quickInsertConfig.afterInsert?.refreshData !== false; console.log("๐Ÿ“ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ ์—ฌ๋ถ€:", shouldRefresh); if (shouldRefresh) { console.log("๐Ÿ“ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฐœ์†ก"); // ์ „์—ญ ์ด๋ฒคํŠธ๋กœ ํ…Œ์ด๋ธ”/์นด๋“œ ์ปดํฌ๋„ŒํŠธ๋“ค์—๊ฒŒ ์ƒˆ๋กœ๊ณ ์นจ ์•Œ๋ฆผ if (typeof window !== "undefined") { window.dispatchEvent(new CustomEvent("refreshTable")); window.dispatchEvent(new CustomEvent("refreshCardDisplay")); } } // ์ปดํฌ๋„ŒํŠธ ๊ฐ’ ์ดˆ๊ธฐํ™” if (quickInsertConfig.afterInsert?.clearComponents && context.onFormDataChange) { for (const mapping of columnMappings) { if (mapping.sourceType === "component" && mapping.sourceComponentId) { // sourceColumnName์ด ์žˆ์œผ๋ฉด ๊ทธ๊ฒƒ์„ ์‚ฌ์šฉ, ์—†์œผ๋ฉด sourceComponentId ์‚ฌ์šฉ const fieldName = mapping.sourceColumnName || mapping.sourceComponentId; context.onFormDataChange(fieldName, null); console.log(`๐Ÿ“ ์ปดํฌ๋„ŒํŠธ ๊ฐ’ ์ดˆ๊ธฐํ™”: ${fieldName}`); } } } if (quickInsertConfig.afterInsert?.showSuccessMessage !== false) { toast.success(quickInsertConfig.afterInsert?.successMessage || "์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); } return true; } else { toast.error(response.data?.message || "์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } catch (error: any) { console.error("โŒ Quick Insert ์˜ค๋ฅ˜:", error); toast.error(error.response?.data?.message || "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } /** * ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ ์•ก์…˜ ์ฒ˜๋ฆฌ (์˜ˆ: status๋ฅผ active๋กœ ๋ณ€๊ฒฝ) * ๐Ÿ†• ์œ„์น˜์ •๋ณด ์ˆ˜์ง‘ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ * ๐Ÿ†• ์—ฐ์† ์œ„์น˜ ์ถ”์  ๊ธฐ๋Šฅ ์ถ”๊ฐ€ */ /** * ์šดํ–‰์•Œ๋ฆผ ๋ฐ ์ข…๋ฃŒ ์•ก์…˜ ์ฒ˜๋ฆฌ * - ์œ„์น˜ ์ˆ˜์ง‘ + ์ƒํƒœ ๋ณ€๊ฒฝ + ์—ฐ์† ์ถ”์  (์‹œ์ž‘/์ข…๋ฃŒ) */ private static async handleOperationControl( config: ButtonActionConfig, context: ButtonActionContext, ): Promise { try { // ๐Ÿ†• ์ถœ๋ฐœ์ง€/๋„์ฐฉ์ง€ ํ•„์ˆ˜ ์ฒดํฌ (์šดํ–‰ ์‹œ์ž‘ ๋ชจ๋“œ์ผ ๋•Œ๋งŒ) // updateTrackingMode๊ฐ€ "start"์ด๊ฑฐ๋‚˜ updateTargetValue๊ฐ€ "active"/"inactive"์ธ ๊ฒฝ์šฐ const isStartMode = config.updateTrackingMode === "start" || config.updateTargetValue === "active" || config.updateTargetValue === "inactive"; if (isStartMode) { // ์ถœ๋ฐœ์ง€/๋„์ฐฉ์ง€ ํ•„๋“œ๋ช… (๊ธฐ๋ณธ๊ฐ’: departure, destination) const departureField = config.trackingDepartureField || "departure"; const destinationField = config.trackingArrivalField || "destination"; const departure = context.formData?.[departureField]; const destination = context.formData?.[destinationField]; console.log("๐Ÿ“ ์ถœ๋ฐœ์ง€/๋„์ฐฉ์ง€ ์ฒดํฌ:", { departureField, destinationField, departure, destination }); if (!departure || departure === "" || !destination || destination === "") { toast.error("์ถœ๋ฐœ์ง€์™€ ๋„์ฐฉ์ง€๋ฅผ ๋จผ์ € ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return false; } } // ๐Ÿ†• ๊ณต์ฐจ ์ถ”์  ์ค‘์ง€ (์šดํ–‰ ์‹œ์ž‘ ์‹œ ๊ณต์ฐจ ์ถ”์  ์ข…๋ฃŒ) if (this.emptyVehicleWatchId !== null) { this.stopEmptyVehicleTracking(); console.log("๐Ÿ›‘ ๊ณต์ฐจ ์ถ”์  ์ข…๋ฃŒ ํ›„ ์šดํ–‰ ์‹œ์ž‘"); } // ๐Ÿ†• ์—ฐ์† ์œ„์น˜ ์ถ”์  ๋ชจ๋“œ ์ฒ˜๋ฆฌ if (config.updateWithTracking) { const trackingConfig: ButtonActionConfig = { ...config, trackingInterval: config.updateTrackingInterval || config.trackingInterval || 10000, trackingStatusField: config.updateTargetField, trackingStatusTableName: config.updateTableName || context.tableName, trackingStatusKeyField: config.updateKeyField, trackingStatusKeySourceField: config.updateKeySourceField, }; if (config.updateTrackingMode === "start") { trackingConfig.trackingStatusOnStart = config.updateTargetValue as string; return await this.handleTrackingStart(trackingConfig, context); } else if (config.updateTrackingMode === "stop") { trackingConfig.trackingStatusOnStop = config.updateTargetValue as string; return await this.handleTrackingStop(trackingConfig, context); } } const { formData, tableName, onFormDataChange, onSave } = context; // ๋ณ€๊ฒฝํ•  ํ•„๋“œ ํ™•์ธ const targetField = config.updateTargetField; const targetValue = config.updateTargetValue; const multipleFields = config.updateMultipleFields || []; // ๋‹จ์ผ ํ•„๋“œ ๋ณ€๊ฒฝ์ด๋‚˜ ๋‹ค์ค‘ ํ•„๋“œ ๋ณ€๊ฒฝ ์ค‘ ํ•˜๋‚˜๋Š” ์žˆ์–ด์•ผ ํ•จ if (!targetField && multipleFields.length === 0 && !config.updateWithGeolocation) { toast.error("๋ณ€๊ฒฝํ•  ํ•„๋“œ๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); return false; } // ํ™•์ธ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ (์„ค์ •๋œ ๊ฒฝ์šฐ) if (config.confirmMessage) { const confirmed = window.confirm(config.confirmMessage); if (!confirmed) { return false; } } // ๋ณ€๊ฒฝํ•  ํ•„๋“œ ๋ชฉ๋ก ๊ตฌ์„ฑ const updates: Record = {}; // ๋‹จ์ผ ํ•„๋“œ ๋ณ€๊ฒฝ if (targetField && targetValue !== undefined) { updates[targetField] = targetValue; } // ๋‹ค์ค‘ ํ•„๋“œ ๋ณ€๊ฒฝ multipleFields.forEach(({ field, value }) => { 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; } } // formData ์—…๋ฐ์ดํŠธ if (onFormDataChange) { Object.entries(updates).forEach(([field, value]) => { onFormDataChange(field, value); }); } // ์ž๋™ ์ €์žฅ (๊ธฐ๋ณธ๊ฐ’: true) 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); 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)) { 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; } } 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) { try { await onSave(); toast.success(config.successMessage || "์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); return true; } catch (saveError) { console.error("โŒ ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ ์ €์žฅ ์‹คํŒจ:", saveError); toast.error(config.errorMessage || "์ƒํƒœ ๋ณ€๊ฒฝ ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } // API๋ฅผ ํ†ตํ•œ ์ง์ ‘ ์ €์žฅ (๊ธฐ์กด ๋ฐฉ์‹: formData์— PK๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ) if (tableName && formData) { try { // PK ํ•„๋“œ ์ฐพ๊ธฐ (id ๋˜๋Š” ํ…Œ์ด๋ธ”๋ช…_id) const pkField = formData.id !== undefined ? "id" : `${tableName}_id`; const pkValue = formData[pkField] || formData.id; if (!pkValue) { toast.error("๋ ˆ์ฝ”๋“œ ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํ‚ค ํ•„๋“œ๋ฅผ ์„ค์ •ํ•ด์ฃผ์„ธ์š”."); return false; } // ์—…๋ฐ์ดํŠธํ•  ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ (๋ณ€๊ฒฝํ•  ํ•„๋“œ๋“ค๋งŒ) const updateData = { ...updates, [pkField]: pkValue, // PK ํฌํ•จ }; const response = await DynamicFormApi.updateData(tableName, updateData); if (response.success) { toast.success(config.successMessage || "์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); // ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฐœ์ƒ window.dispatchEvent( new CustomEvent("refreshTableData", { detail: { tableName }, }), ); return true; } else { toast.error(response.message || config.errorMessage || "์ƒํƒœ ๋ณ€๊ฒฝ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } catch (apiError) { console.error("โŒ ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ API ํ˜ธ์ถœ ์‹คํŒจ:", apiError); toast.error(config.errorMessage || "์ƒํƒœ ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } } // ์ž๋™ ์ €์žฅ์ด ๋น„ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ ํผ ๋ฐ์ดํ„ฐ๋งŒ ๋ณ€๊ฒฝ toast.success(config.successMessage || "ํ•„๋“œ ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ €์žฅ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์ €์žฅํ•˜์„ธ์š”."); return true; } catch (error) { console.error("โŒ ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ ์‹คํŒจ:", error); toast.error(config.errorMessage || "ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } /** * ํผ ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ */ private static validateFormData(formData: Record): { isValid: boolean; errors: string[]; } { const errors: string[] = []; // ๊ธฐ๋ณธ์ ์ธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋กœ์ง Object.entries(formData).forEach(([key, value]) => { // ๋นˆ ๊ฐ’ ์ฒดํฌ (null, undefined, ๋นˆ ๋ฌธ์ž์—ด) if (value === null || value === undefined || value === "") { // ํ•„์ˆ˜ ํ•„๋“œ๋Š” ํ–ฅํ›„ ์ปดํฌ๋„ŒํŠธ ์„ค์ •์—์„œ ํ™•์ธ ๊ฐ€๋Šฅ console.warn(`ํ•„๋“œ '${key}'๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค.`); } // ๊ธฐ๋ณธ ํƒ€์ž… ๊ฒ€์ฆ if (typeof value === "string" && value.trim() === "") { console.warn(`ํ•„๋“œ '${key}'๊ฐ€ ๊ณต๋ฐฑ๋งŒ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.`); } }); // ์ตœ์†Œํ•œ ํ•˜๋‚˜์˜ ํ•„๋“œ๋Š” ์žˆ์–ด์•ผ ํ•จ if (Object.keys(formData).length === 0) { errors.push("์ €์žฅํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); } return { isValid: errors.length === 0, errors, }; } /** * ์ด๋ฒคํŠธ ๋ฒ„์Šค๋กœ ์ด๋ฒคํŠธ ๋ฐœ์†ก (์Šค์ผ€์ค„ ์ƒ์„ฑ ๋“ฑ) */ private static async handleEvent(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { const { eventConfig } = config; if (!eventConfig?.eventName) { toast.error("์ด๋ฒคํŠธ ์„ค์ •์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); console.error("[handleEvent] eventName์ด ์„ค์ •๋˜์ง€ ์•Š์Œ", { config }); return false; } // V2_EVENTS์—์„œ ์ด๋ฒคํŠธ ์ด๋ฆ„ ๊ฐ€์ ธ์˜ค๊ธฐ const { v2EventBus, V2_EVENTS } = await import("@/lib/v2-core"); // ์ด๋ฒคํŠธ ์ด๋ฆ„ ๊ฒ€์ฆ const eventName = eventConfig.eventName as keyof typeof V2_EVENTS; if (!V2_EVENTS[eventName]) { toast.error(`์•Œ ์ˆ˜ ์—†๋Š” ์ด๋ฒคํŠธ: ${eventConfig.eventName}`); console.error("[handleEvent] ์•Œ ์ˆ˜ ์—†๋Š” ์ด๋ฒคํŠธ", { eventName, V2_EVENTS }); return false; } // ํŽ˜์ด๋กœ๋“œ ๊ตฌ์„ฑ const eventPayload = { requestId: crypto.randomUUID(), ...eventConfig.eventPayload, }; console.log("[handleEvent] ์ด๋ฒคํŠธ ๋ฐœ์†ก:", { eventName: V2_EVENTS[eventName], payload: eventPayload, }); // ์ด๋ฒคํŠธ ๋ฐœ์†ก v2EventBus.emit(V2_EVENTS[eventName], eventPayload); return true; } catch (error) { console.error("[handleEvent] ์ด๋ฒคํŠธ ๋ฐœ์†ก ์˜ค๋ฅ˜:", error); toast.error("์ด๋ฒคํŠธ ๋ฐœ์†ก ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); return false; } } } /** * ๊ธฐ๋ณธ ๋ฒ„ํŠผ ์•ก์…˜ ์„ค์ •๋“ค */ export const DEFAULT_BUTTON_ACTIONS: Record> = { save: { type: "save", validateForm: true, confirmMessage: "์ €์žฅํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?", successMessage: "์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", errorMessage: "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", }, delete: { type: "delete", confirmMessage: "์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?", successMessage: "์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", errorMessage: "์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", }, navigate: { type: "navigate", }, openModalWithData: { // deprecated: modal๋กœ ํ†ตํ•ฉ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€) type: "openModalWithData", modalSize: "md", passSelectedData: true, autoDetectDataSource: true, // ๋ชจ๋‹ฌ ์—ด๊ธฐ๋Š” UI ์ „ํ™˜์ด๋ฏ€๋กœ successMessage ์ œ๊ฑฐ }, modal: { type: "modal", modalSize: "md", passSelectedData: true, // ๊ธฐ๋ณธ: ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ autoDetectDataSource: false, // ๊ธฐ๋ณธ: ์ž๋™ ๊ฐ์ง€ ๋น„ํ™œ์„ฑํ™” }, edit: { type: "edit", successMessage: "ํŽธ์ง‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", }, copy: { type: "copy", confirmMessage: "๋ณต์‚ฌํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?", successMessage: "๋ณต์‚ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", errorMessage: "๋ณต์‚ฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", }, control: { type: "control", }, view_table_history: { type: "view_table_history", historyRecordIdField: "id", historyRecordIdSource: "selected_row", }, excel_download: { type: "excel_download", excelIncludeHeaders: true, successMessage: "์—‘์…€ ํŒŒ์ผ์ด ๋‹ค์šด๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", errorMessage: "์—‘์…€ ๋‹ค์šด๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", }, excel_upload: { type: "excel_upload", excelUploadMode: "insert", confirmMessage: "์—‘์…€ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?", successMessage: "์—‘์…€ ํŒŒ์ผ์ด ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", errorMessage: "์—‘์…€ ์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", }, barcode_scan: { type: "barcode_scan", barcodeFormat: "all", barcodeAutoSubmit: false, }, code_merge: { type: "code_merge", mergeShowPreview: true, confirmMessage: "์„ ํƒํ•œ ๋‘ ํ•ญ๋ชฉ์„ ๋ณ‘ํ•ฉํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?", successMessage: "์ฝ”๋“œ ๋ณ‘ํ•ฉ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", errorMessage: "์ฝ”๋“œ ๋ณ‘ํ•ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", }, transferData: { type: "transferData", successMessage: "๋ฐ์ดํ„ฐ๊ฐ€ ์ „๋‹ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", errorMessage: "๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", }, empty_vehicle: { type: "empty_vehicle", geolocationHighAccuracy: true, geolocationTimeout: 10000, geolocationMaxAge: 0, geolocationLatField: "latitude", geolocationLngField: "longitude", geolocationAutoSave: true, geolocationKeyField: "user_id", geolocationKeySourceField: "__userId__", geolocationExtraField: "status", geolocationExtraValue: "inactive", successMessage: "๊ณต์ฐจ๋“ฑ๋ก์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", errorMessage: "๊ณต์ฐจ๋“ฑ๋ก ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", }, operation_control: { type: "operation_control", updateAutoSave: true, updateWithGeolocation: true, updateGeolocationLatField: "latitude", updateGeolocationLngField: "longitude", updateKeyField: "user_id", updateKeySourceField: "__userId__", confirmMessage: "์šดํ–‰ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?", successMessage: "์šดํ–‰ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", errorMessage: "์šดํ–‰ ์ƒํƒœ ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", }, swap_fields: { type: "swap_fields", successMessage: "ํ•„๋“œ ๊ฐ’์ด ๊ตํ™˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", errorMessage: "ํ•„๋“œ ๊ฐ’ ๊ตํ™˜ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", }, quickInsert: { type: "quickInsert", successMessage: "์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", errorMessage: "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", }, event: { type: "event", }, };