From c74e97d66e1dcdc9d36a72c60d6c6f425bc77f35 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 13 Jan 2026 09:35:01 +0900 Subject: [PATCH 1/5] chore: update react-is to version 18.3.1 and downgrade recharts to version 3.2.1 in package.json and package-lock.json --- frontend/package-lock.json | 52 ++++++++++++++++++++++++++++++-------- frontend/package.json | 1 + 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f0ef7c70..6fcd209b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -77,6 +77,7 @@ "react-dom": "19.1.0", "react-hook-form": "^7.62.0", "react-hot-toast": "^2.6.0", + "react-is": "^18.3.1", "react-leaflet": "^5.0.0", "react-resizable-panels": "^3.0.6", "react-webcam": "^7.2.0", @@ -256,6 +257,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -297,6 +299,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -330,6 +333,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -2632,6 +2636,7 @@ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz", "integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.17.8", "@types/react-reconciler": "^0.32.0", @@ -3285,6 +3290,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.6.tgz", "integrity": "sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.90.6" }, @@ -3352,6 +3358,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.1.tgz", "integrity": "sha512-nkerkl8syHj44ZzAB7oA2GPmmZINKBKCa79FuNvmGJrJ4qyZwlkDzszud23YteFZEytbc87kVd/fP76ROS6sLg==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -3665,6 +3672,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.1.tgz", "integrity": "sha512-ijKo3+kIjALthYsnBmkRXAuw2Tswd9gd7BUR5OMfIcjGp8v576vKxOxrRfuYiUM78GPt//P0sVc1WV82H5N0PQ==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", @@ -6165,6 +6173,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -6175,6 +6184,7 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6208,6 +6218,7 @@ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz", "integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==", "license": "MIT", + "peer": true, "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", @@ -6290,6 +6301,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -6922,6 +6934,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8072,7 +8085,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/d3": { "version": "7.9.0", @@ -8394,6 +8408,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -9153,6 +9168,7 @@ "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9241,6 +9257,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9342,6 +9359,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -10492,6 +10510,7 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -11273,7 +11292,8 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/levn": { "version": "0.4.1", @@ -12572,6 +12592,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12867,6 +12888,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "license": "MIT", + "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -12896,6 +12918,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -12944,6 +12967,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz", "integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -13070,6 +13094,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13139,6 +13164,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -13157,6 +13183,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -13186,9 +13213,9 @@ } }, "node_modules/react-is": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.1.tgz", - "integrity": "sha512-L7BnWgRbMwzMAubQcS7sXdPdNLmKlucPlopgAzx7FtYbksWZgEWiuYM5x9T6UqS2Ne0rsgQTq5kY2SGqpzUkYA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT", "peer": true }, @@ -13413,9 +13440,9 @@ } }, "node_modules/recharts": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.3.0.tgz", - "integrity": "sha512-Vi0qmTB0iz1+/Cz9o5B7irVyUjX2ynvEgImbgMt/3sKRREcUM07QiYjS1QpAVrkmVlXqy5gykq4nGWMz9AS4Rg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.1.tgz", + "integrity": "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw==", "license": "MIT", "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", @@ -13470,6 +13497,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -13492,7 +13520,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/recharts/node_modules/redux-thunk": { "version": "3.1.0", @@ -14516,7 +14545,8 @@ "version": "0.180.0", "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", "integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/three-mesh-bvh": { "version": "0.8.3", @@ -14604,6 +14634,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14952,6 +14983,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/frontend/package.json b/frontend/package.json index 1dc6c6fe..1299b81c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -85,6 +85,7 @@ "react-dom": "19.1.0", "react-hook-form": "^7.62.0", "react-hot-toast": "^2.6.0", + "react-is": "^18.3.1", "react-leaflet": "^5.0.0", "react-resizable-panels": "^3.0.6", "react-webcam": "^7.2.0", From 1b5ae5fe1c9a6db859fbe4c873dba202b014dea5 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Tue, 13 Jan 2026 15:43:04 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=EC=9D=BC=EB=8B=A8=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UniversalFormModalComponent.tsx | 8 +++++ frontend/lib/utils/buttonActions.ts | 36 +++++++++++++++---- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index 3e043331..c03f4cce 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -308,6 +308,14 @@ export function UniversalFormModalComponent({ console.log("[UniversalFormModal] beforeFormSave 이벤트 수신"); console.log("[UniversalFormModal] 설정된 필드 목록:", Array.from(configuredFields)); + // 🆕 시스템 필드 병합: id는 설정 여부와 관계없이 항상 전달 (UPDATE/INSERT 판단용) + // - 신규 등록: formData.id가 없으므로 영향 없음 + // - 편집 모드: formData.id가 있으면 메인 테이블 UPDATE에 사용 + if (formData.id !== undefined && formData.id !== null && formData.id !== "") { + event.detail.formData.id = formData.id; + console.log(`[UniversalFormModal] 시스템 필드 병합: id =`, formData.id); + } + // UniversalFormModal에 설정된 필드만 병합 (채번 규칙 포함) // 외부 formData에 이미 값이 있어도 UniversalFormModal 값으로 덮어씀 // (UniversalFormModal이 해당 필드의 주인이므로) diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 8bc65cca..e9d6eede 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -1971,19 +1971,43 @@ export class ButtonActionExecutor { } }); - console.log("📦 [handleUniversalFormModalTableSectionSave] 메인 테이블 저장 데이터:", mainRowToSave); + // 🆕 메인 테이블 UPDATE/INSERT 판단 + // - formData.id가 있으면 편집 모드 → UPDATE + // - formData.id가 없으면 신규 등록 → INSERT + const existingMainId = formData.id; + const isMainUpdate = existingMainId !== undefined && existingMainId !== null && existingMainId !== ""; - const mainSaveResult = await DynamicFormApi.saveFormData({ - screenId: screenId!, - tableName: tableName!, - data: mainRowToSave, + console.log("📦 [handleUniversalFormModalTableSectionSave] 메인 테이블 저장 데이터:", mainRowToSave); + console.log("📦 [handleUniversalFormModalTableSectionSave] UPDATE/INSERT 판단:", { + existingMainId, + isMainUpdate, }); + let mainSaveResult: { success: boolean; data?: any; message?: string }; + + if (isMainUpdate) { + // 🔄 편집 모드: UPDATE 실행 + console.log("🔄 [handleUniversalFormModalTableSectionSave] 메인 테이블 UPDATE 실행, ID:", existingMainId); + 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 || "메인 데이터 저장 실패"); } - mainRecordId = mainSaveResult.data?.id || null; console.log("✅ [handleUniversalFormModalTableSectionSave] 메인 테이블 저장 완료, ID:", mainRecordId); } From d7d7dabe842d7ad7d341257cf4dd5571411db970 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 13 Jan 2026 17:05:16 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20Select=20=EC=98=B5=EC=85=98=EC=9D=98?= =?UTF-8?q?=20=EC=B0=B8=EC=A1=B0=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20=EC=9E=90=EB=8F=99=20=EB=A1=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FieldDetailSettingsModal에서 모달 열릴 때 Select 옵션의 참조 테이블 컬럼을 자동으로 로드하도록 useEffect 추가 - field.selectOptions.tableName이 설정되어 있고 해당 테이블의 컬럼이 로드되지 않은 경우 onLoadTableColumns 호출 - 기존 linkedFieldGroup의 sourceTable 로드 로직과 동일한 패턴 적용 --- .../modals/FieldDetailSettingsModal.tsx | 10 +++++ frontend/package-lock.json | 42 +++---------------- 2 files changed, 15 insertions(+), 37 deletions(-) diff --git a/frontend/lib/registry/components/universal-form-modal/modals/FieldDetailSettingsModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/FieldDetailSettingsModal.tsx index 453429e7..9148a9fb 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/FieldDetailSettingsModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/FieldDetailSettingsModal.tsx @@ -115,6 +115,16 @@ export function FieldDetailSettingsModal({ } } }, [open, field.linkedFieldGroup?.sourceTable, tableColumns, onLoadTableColumns]); + + // 모달이 열릴 때 Select 옵션의 참조 테이블 컬럼 자동 로드 + useEffect(() => { + if (open && field.selectOptions?.tableName) { + // tableColumns에 해당 테이블 컬럼이 없으면 로드 + if (!tableColumns[field.selectOptions.tableName] || tableColumns[field.selectOptions.tableName].length === 0) { + onLoadTableColumns(field.selectOptions.tableName); + } + } + }, [open, field.selectOptions?.tableName, tableColumns, onLoadTableColumns]); // 모든 카테고리 컬럼 목록 로드 (모달 열릴 때) useEffect(() => { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6fcd209b..7bad1f96 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -257,7 +257,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -299,7 +298,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -333,7 +331,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -2636,7 +2633,6 @@ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz", "integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.17.8", "@types/react-reconciler": "^0.32.0", @@ -3290,7 +3286,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.6.tgz", "integrity": "sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "5.90.6" }, @@ -3358,7 +3353,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.1.tgz", "integrity": "sha512-nkerkl8syHj44ZzAB7oA2GPmmZINKBKCa79FuNvmGJrJ4qyZwlkDzszud23YteFZEytbc87kVd/fP76ROS6sLg==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -3672,7 +3666,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.1.tgz", "integrity": "sha512-ijKo3+kIjALthYsnBmkRXAuw2Tswd9gd7BUR5OMfIcjGp8v576vKxOxrRfuYiUM78GPt//P0sVc1WV82H5N0PQ==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", @@ -6173,7 +6166,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -6184,7 +6176,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6218,7 +6209,6 @@ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz", "integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==", "license": "MIT", - "peer": true, "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", @@ -6301,7 +6291,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -6934,7 +6923,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8085,8 +8073,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/d3": { "version": "7.9.0", @@ -8408,7 +8395,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -9168,7 +9154,6 @@ "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9257,7 +9242,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9359,7 +9343,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -10510,7 +10493,6 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -11292,8 +11274,7 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/levn": { "version": "0.4.1", @@ -12592,7 +12573,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12888,7 +12868,6 @@ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "license": "MIT", - "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -12918,7 +12897,6 @@ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -12967,7 +12945,6 @@ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz", "integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -13094,7 +13071,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13164,7 +13140,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -13183,7 +13158,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -13216,8 +13190,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-leaflet": { "version": "5.0.0", @@ -13497,7 +13470,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -13520,8 +13492,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/recharts/node_modules/redux-thunk": { "version": "3.1.0", @@ -14545,8 +14516,7 @@ "version": "0.180.0", "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", "integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/three-mesh-bvh": { "version": "0.8.3", @@ -14634,7 +14604,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14983,7 +14952,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From ef27e0e38f52c95a8c34a73d3274ecc6f9697aa5 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 13 Jan 2026 18:26:41 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat(universal-form-modal):=20=EC=97=B0?= =?UTF-8?q?=EC=87=84=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4(Cascading=20Dr?= =?UTF-8?q?opdown)=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SelectOptionConfig에 cascading 타입 및 설정 객체 추가 - FieldDetailSettingsModal에 연쇄 드롭다운 설정 UI 구현 - 부모 필드 선택 (섹션별 그룹핑 콤보박스) - 관계 코드 선택 시 상세 설정 자동 채움 - 소스 테이블, 부모 키 컬럼, 값/라벨 컬럼 설정 - UniversalFormModalComponent에 자식 필드 초기화 로직 추가 - selectOptions.cascading 방식 CascadingSelectField 렌더링 지원 --- .../UniversalFormModalComponent.tsx | 61 +- .../UniversalFormModalConfigPanel.tsx | 8 + .../modals/FieldDetailSettingsModal.tsx | 580 +++++++++++++++++- .../components/universal-form-modal/types.ts | 16 +- 4 files changed, 653 insertions(+), 12 deletions(-) diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index 3e043331..e6976844 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -84,7 +84,7 @@ const CascadingSelectField: React.FC = ({ return ( - updateField({ - selectOptions: { - ...localField.selectOptions, - type: value as "static" | "code", - }, - }) - } + onValueChange={(value) => { + // 타입 변경 시 관련 설정 초기화 + if (value === "cascading") { + updateField({ + selectOptions: { + type: "cascading", + cascading: { + parentField: "", + clearOnParentChange: true, + }, + }, + }); + } else { + updateField({ + selectOptions: { + ...localField.selectOptions, + type: value as "static" | "table" | "code", + cascading: undefined, + }, + }); + } + }} > @@ -382,6 +473,11 @@ export function FieldDetailSettingsModal({ ))} + + {localField.selectOptions?.type === "cascading" + ? "연쇄 드롭다운: 부모 필드 선택에 따라 옵션이 동적으로 변경됩니다" + : "테이블 참조: DB 테이블에서 옵션 목록을 가져옵니다."} + {localField.selectOptions?.type === "table" && ( @@ -594,6 +690,472 @@ export function FieldDetailSettingsModal({ )} + + {localField.selectOptions?.type === "cascading" && ( +
+ + 연쇄 드롭다운: 부모 필드의 값에 따라 옵션이 동적으로 필터링됩니다. +
+ 예: 거래처 선택 → 해당 거래처의 납품처만 표시 +
+ + {/* 부모 필드 선택 - 콤보박스 (섹션별 그룹핑) */} +
+ + {allFieldsWithSections.length > 0 ? ( + + + + + + + + + + 선택 가능한 필드가 없습니다. + + {allFieldsWithSections.map((section) => { + // 자기 자신 제외한 필드 목록 + const availableFields = section.fields.filter( + (f) => f.columnName !== field.columnName + ); + if (availableFields.length === 0) return null; + + return ( + + {availableFields.map((f) => ( + { + updateField({ + selectOptions: { + ...localField.selectOptions, + cascading: { + ...localField.selectOptions?.cascading, + parentField: f.columnName, + }, + }, + }); + setParentFieldOpen(false); + }} + className="text-xs" + > + +
+ {f.label} + + {f.columnName} ({f.fieldType}) + +
+
+ ))} +
+ ); + })} +
+
+
+
+ ) : ( + + updateField({ + selectOptions: { + ...localField.selectOptions, + cascading: { + ...localField.selectOptions?.cascading, + parentField: e.target.value, + }, + }, + }) + } + placeholder="customer_code" + className="h-7 text-xs mt-1" + /> + )} + + 이 드롭다운의 옵션을 결정할 부모 필드를 선택하세요 +
+ 예: 거래처 선택 → 납품처 필터링 +
+
+ + {/* 관계 코드 선택 */} +
+ + + + + + + + + + + 등록된 연쇄 관계가 없습니다. + + + {/* 직접 설정 옵션 */} + { + updateField({ + selectOptions: { + ...localField.selectOptions, + cascading: { + ...localField.selectOptions?.cascading, + relationCode: undefined, + }, + }, + }); + setCascadingRelationOpen(false); + }} + className="text-xs" + > + + 직접 설정 + + + {cascadingRelations.map((relation) => ( + { + handleRelationCodeSelect(relation.relation_code); + setCascadingRelationOpen(false); + }} + className="text-xs" + > + +
+ {relation.relation_name} + + {relation.parent_table} → {relation.child_table} + +
+
+ ))} +
+
+
+
+
+ + 미리 등록된 관계를 선택하면 설정이 자동으로 채워집니다. +
+ 직접 설정을 선택하면 아래에서 수동으로 입력할 수 있습니다. +
+
+ + + + {/* 상세 설정 (수정 가능) */} +
+
+ + 상세 설정 (수정 가능) +
+ +
+ + + 옵션을 가져올 테이블 (예: delivery_destination) +
+ +
+ + {selectTableColumns.length > 0 ? ( + + ) : ( + + updateField({ + selectOptions: { + ...localField.selectOptions, + cascading: { + ...localField.selectOptions?.cascading, + parentKeyColumn: e.target.value, + }, + }, + }) + } + placeholder="customer_code" + className="h-7 text-xs mt-1" + /> + )} + 부모 값과 매칭할 컬럼 (예: customer_code) +
+ +
+ + {selectTableColumns.length > 0 ? ( + + ) : ( + + updateField({ + selectOptions: { + ...localField.selectOptions, + valueColumn: e.target.value, + }, + }) + } + placeholder="destination_code" + className="h-7 text-xs mt-1" + /> + )} + 드롭다운 value로 사용할 컬럼 +
+ +
+ + {selectTableColumns.length > 0 ? ( + + ) : ( + + updateField({ + selectOptions: { + ...localField.selectOptions, + labelColumn: e.target.value, + }, + }) + } + placeholder="destination_name" + className="h-7 text-xs mt-1" + /> + )} + 드롭다운에 표시할 컬럼 +
+ + + +
+ + + updateField({ + selectOptions: { + ...localField.selectOptions, + cascading: { + ...localField.selectOptions?.cascading, + emptyParentMessage: e.target.value, + }, + }, + }) + } + placeholder="상위 항목을 먼저 선택하세요" + className="h-7 text-xs mt-1" + /> +
+ +
+ + + updateField({ + selectOptions: { + ...localField.selectOptions, + cascading: { + ...localField.selectOptions?.cascading, + noOptionsMessage: e.target.value, + }, + }, + }) + } + placeholder="선택 가능한 항목이 없습니다" + className="h-7 text-xs mt-1" + /> +
+ +
+ 부모 변경 시 값 초기화 + + updateField({ + selectOptions: { + ...localField.selectOptions, + cascading: { + ...localField.selectOptions?.cascading, + clearOnParentChange: checked, + }, + }, + }) + } + /> +
+ 부모 필드 값이 변경되면 이 필드의 값을 자동으로 초기화합니다 +
+
+ )} )} @@ -929,7 +1491,7 @@ export function FieldDetailSettingsModal({ preview = `${subLabel} - ${mainLabel}`; } else if (format === "name_code" && subCol) { preview = `${mainLabel} (${subLabel})`; - } else if (format !== "name_only" && !subCol) { + } else if (!subCol) { preview = `${mainLabel} (서브 컬럼을 선택하세요)`; } else { preview = mainLabel; diff --git a/frontend/lib/registry/components/universal-form-modal/types.ts b/frontend/lib/registry/components/universal-form-modal/types.ts index c5ecb1cc..72e29d58 100644 --- a/frontend/lib/registry/components/universal-form-modal/types.ts +++ b/frontend/lib/registry/components/universal-form-modal/types.ts @@ -7,7 +7,7 @@ // Select 옵션 설정 export interface SelectOptionConfig { - type?: "static" | "table" | "code"; // 옵션 타입 (기본: static) + type?: "static" | "table" | "code" | "cascading"; // 옵션 타입 (기본: static) // 정적 옵션 staticOptions?: { value: string; label: string }[]; // 테이블 기반 옵션 @@ -19,6 +19,19 @@ export interface SelectOptionConfig { // 카테고리 컬럼 기반 옵션 (table_column_category_values 테이블) // 형식: "tableName.columnName" (예: "sales_order_mng.incoterms") categoryKey?: string; + + // 연쇄 드롭다운 설정 (type이 "cascading"일 때 사용) + cascading?: { + parentField?: string; // 부모 필드명 (같은 폼 내) + relationCode?: string; // 관계 코드 (cascading_relation 테이블) + // 직접 설정 또는 관계 코드에서 가져온 값 수정 시 사용 + sourceTable?: string; // 옵션을 조회할 테이블 + parentKeyColumn?: string; // 부모 값과 매칭할 컬럼 + // valueColumn, labelColumn은 상위 속성 사용 + emptyParentMessage?: string; // 부모 미선택 시 메시지 + noOptionsMessage?: string; // 옵션 없음 메시지 + clearOnParentChange?: boolean; // 부모 변경 시 값 초기화 (기본: true) + }; } // 채번규칙 설정 @@ -873,6 +886,7 @@ export const SELECT_OPTION_TYPE_OPTIONS = [ { value: "static", label: "직접 입력" }, { value: "table", label: "테이블 참조" }, { value: "code", label: "공통코드" }, + { value: "cascading", label: "연쇄 드롭다운" }, ] as const; // 연동 필드 표시 형식 옵션 From cf97db7fbf949b744587486663971b20c2a88dec Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 13 Jan 2026 18:44:59 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat(universal-form-modal):=20Select=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=A7=81=EC=A0=91=20=EC=9E=85=EB=A0=A5(Co?= =?UTF-8?q?mbobox)=20=EB=AA=A8=EB=93=9C=20=EC=B6=94=EA=B0=80=20SelectOptio?= =?UTF-8?q?nConfig=EC=97=90=20allowCustomInput=20=EC=98=B5=EC=85=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20FieldDetailSettingsModal=EC=97=90=20"?= =?UTF-8?q?=EC=A7=81=EC=A0=91=20=EC=9E=85=EB=A0=A5=20=ED=97=88=EC=9A=A9"?= =?UTF-8?q?=20Switch=20UI=20=EC=B6=94=EA=B0=80=20CascadingSelectField?= =?UTF-8?q?=EC=97=90=20Combobox=20=EB=AA=A8=EB=93=9C=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(Popover+Command)=20SelectField=EC=97=90=20Combobox=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=20=EA=B5=AC=ED=98=84=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=EA=B3=BC=20=EC=A7=81=EC=A0=91=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EB=8F=99=EC=8B=9C=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UniversalFormModalComponent.tsx | 170 +++++++++++++++++- .../modals/FieldDetailSettingsModal.tsx | 25 +++ .../components/universal-form-modal/types.ts | 5 + 3 files changed, 199 insertions(+), 1 deletion(-) diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index e6976844..662f6379 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -19,7 +19,9 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { ChevronDown, ChevronUp, ChevronRight, Plus, Trash2, Loader2 } from "lucide-react"; +import { ChevronDown, ChevronUp, ChevronRight, Plus, Trash2, Loader2, Check, ChevronsUpDown } from "lucide-react"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; @@ -42,6 +44,7 @@ import { TableSectionRenderer } from "./TableSectionRenderer"; /** * 🔗 연쇄 드롭다운 Select 필드 컴포넌트 + * allowCustomInput이 true이면 Combobox 형태로 직접 입력 가능 */ interface CascadingSelectFieldProps { fieldId: string; @@ -51,6 +54,7 @@ interface CascadingSelectFieldProps { onChange: (value: string) => void; placeholder?: string; disabled?: boolean; + allowCustomInput?: boolean; } const CascadingSelectField: React.FC = ({ @@ -61,12 +65,20 @@ const CascadingSelectField: React.FC = ({ onChange, placeholder, disabled, + allowCustomInput = false, }) => { + const [open, setOpen] = useState(false); + const [inputValue, setInputValue] = useState(value || ""); const { options, loading } = useCascadingDropdown({ config, parentValue, }); + // value가 외부에서 변경되면 inputValue도 동기화 + useEffect(() => { + setInputValue(value || ""); + }, [value]); + const getPlaceholder = () => { if (!parentValue) { return config.emptyParentMessage || "상위 항목을 먼저 선택하세요"; @@ -82,6 +94,79 @@ const CascadingSelectField: React.FC = ({ const isDisabled = disabled || !parentValue || loading; + // Combobox 형태 (직접 입력 허용) + if (allowCustomInput) { + return ( + + +
+ { + setInputValue(e.target.value); + onChange(e.target.value); + }} + placeholder={getPlaceholder()} + disabled={isDisabled} + className="w-full pr-8" + /> + +
+
+ + + + + + {!parentValue + ? config.emptyParentMessage || "상위 항목을 먼저 선택하세요" + : config.noOptionsMessage || "선택 가능한 항목이 없습니다"} + + + {options + .filter((option) => option.value && option.value !== "") + .map((option) => ( + { + setInputValue(option.label); + onChange(option.value); + setOpen(false); + }} + > + + {option.label} + + ))} + + + + +
+ ); + } + + // 기본 Select 형태 (목록에서만 선택) return ( { + setInputValue(e.target.value); + onChange(e.target.value); + }} + placeholder={loading ? "로딩 중..." : placeholder} + disabled={disabled || loading} + className="w-full pr-8" + /> + + + + + + + + 선택 가능한 항목이 없습니다 + + {options + .filter((option) => option.value && option.value !== "") + .map((option) => ( + { + setInputValue(option.label); + onChange(option.value); + setOpen(false); + }} + > + + {option.label} + + ))} + + + + + + ); + } + + // 기본 Select 형태 (목록에서만 선택) return (