From 808c23b3410d83e89051c836c14fce0625ee93bb Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 12 Mar 2026 15:12:26 +0900 Subject: [PATCH 1/4] chore: remove peer dependencies from package-lock.json - Removed unnecessary "peer" entries from various packages in package-lock.json to streamline dependency management and avoid potential conflicts. - This cleanup helps maintain a cleaner and more efficient package structure. Made-with: Cursor --- frontend/package-lock.json | 42 +++++--------------------------------- 1 file changed, 5 insertions(+), 37 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 614e0d47..f38af595 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -265,7 +265,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -307,7 +306,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -341,7 +339,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", @@ -3057,7 +3054,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", @@ -3711,7 +3707,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" }, @@ -3779,7 +3774,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" @@ -4093,7 +4087,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", @@ -6594,7 +6587,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" } @@ -6605,7 +6597,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6648,7 +6639,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", @@ -6731,7 +6721,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", @@ -7364,7 +7353,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8515,8 +8503,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", @@ -8838,7 +8825,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" } @@ -9598,7 +9584,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", @@ -9687,7 +9672,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9789,7 +9773,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -10961,7 +10944,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" @@ -11742,8 +11724,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", @@ -13082,7 +13063,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -13376,7 +13356,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" } @@ -13406,7 +13385,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", @@ -13455,7 +13433,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", @@ -13659,7 +13636,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" } @@ -13729,7 +13705,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" }, @@ -13780,7 +13755,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" }, @@ -13813,8 +13787,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", @@ -14122,7 +14095,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" @@ -14145,8 +14117,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", @@ -15176,8 +15147,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", @@ -15265,7 +15235,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15614,7 +15583,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From 7da04c6a095f5f88d336986db206126a0b5056ab Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 12 Mar 2026 15:43:06 +0900 Subject: [PATCH 2/4] feat: enhance V2InputConfigPanel with additional UI components - Added Separator and Checkbox components to the V2InputConfigPanel for improved layout and functionality. - This enhancement aims to provide better user interaction and organization within the input configuration settings. Made-with: Cursor --- frontend/components/v2/config-panels/V2InputConfigPanel.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/components/v2/config-panels/V2InputConfigPanel.tsx b/frontend/components/v2/config-panels/V2InputConfigPanel.tsx index 60cec12e..f4593e14 100644 --- a/frontend/components/v2/config-panels/V2InputConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2InputConfigPanel.tsx @@ -10,6 +10,8 @@ import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; +import { Separator } from "@/components/ui/separator"; +import { Checkbox } from "@/components/ui/checkbox"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Settings, ChevronDown, Loader2, Type, Hash, Lock, AlignLeft, SlidersHorizontal, Palette, ListOrdered } from "lucide-react"; import { cn } from "@/lib/utils"; From 772a10258cadb9d64159ab51f194859393039c09 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 12 Mar 2026 15:46:54 +0900 Subject: [PATCH 3/4] fix: update V2PropertiesPanel to use V2FieldConfigPanel for input and select components - Replaced references to V2InputConfigPanel and V2SelectConfigPanel with V2FieldConfigPanel in the V2PropertiesPanel. - This change ensures consistent configuration handling for both input and select components, improving maintainability and usability. Made-with: Cursor --- .../screen/panels/V2PropertiesPanel.tsx | 175 ++++++++++++------ 1 file changed, 122 insertions(+), 53 deletions(-) diff --git a/frontend/components/screen/panels/V2PropertiesPanel.tsx b/frontend/components/screen/panels/V2PropertiesPanel.tsx index 1f97230c..cf148e6e 100644 --- a/frontend/components/screen/panels/V2PropertiesPanel.tsx +++ b/frontend/components/screen/panels/V2PropertiesPanel.tsx @@ -183,6 +183,62 @@ export const V2PropertiesPanel: React.FC = ({ selectedComponent.componentConfig?.id || (selectedComponent.type === "component" ? selectedComponent.id : null); // πŸ†• 독립 μ»΄ν¬λ„ŒνŠΈ (table-search-widget λ“±) + // πŸ†• V2 μ»΄ν¬λ„ŒνŠΈ 직접 감지 및 μ„€μ • νŒ¨λ„ λ Œλ”λ§ + if (componentId?.startsWith("v2-")) { + const v2ConfigPanels: Record void }>> = { + "v2-input": require("@/components/v2/config-panels/V2FieldConfigPanel").V2FieldConfigPanel, + "v2-select": require("@/components/v2/config-panels/V2FieldConfigPanel").V2FieldConfigPanel, + "v2-date": require("@/components/v2/config-panels/V2DateConfigPanel").V2DateConfigPanel, + "v2-list": require("@/components/v2/config-panels/V2ListConfigPanel").V2ListConfigPanel, + "v2-layout": require("@/components/v2/config-panels/V2LayoutConfigPanel").V2LayoutConfigPanel, + "v2-group": require("@/components/v2/config-panels/V2GroupConfigPanel").V2GroupConfigPanel, + "v2-media": require("@/components/v2/config-panels/V2MediaConfigPanel").V2MediaConfigPanel, + "v2-biz": require("@/components/v2/config-panels/V2BizConfigPanel").V2BizConfigPanel, + "v2-hierarchy": require("@/components/v2/config-panels/V2HierarchyConfigPanel").V2HierarchyConfigPanel, + "v2-bom-item-editor": require("@/components/v2/config-panels/V2BomItemEditorConfigPanel") + .V2BomItemEditorConfigPanel, + "v2-bom-tree": require("@/components/v2/config-panels/V2BomTreeConfigPanel").V2BomTreeConfigPanel, + }; + + const V2ConfigPanel = v2ConfigPanels[componentId]; + if (V2ConfigPanel) { + const currentConfig = selectedComponent.componentConfig || {}; + const handleV2ConfigChange = (newConfig: any) => { + onUpdateProperty(selectedComponent.id, "componentConfig", { ...currentConfig, ...newConfig }); + }; + + // 컬럼의 inputType κ°€μ Έμ˜€κΈ° (entity νƒ€μž…μΈμ§€ ν™•μΈμš©) + const inputType = currentConfig.inputType || currentConfig.webType || (selectedComponent as any).inputType; + + // ν˜„μž¬ ν™”λ©΄μ˜ ν…Œμ΄λΈ”λͺ… κ°€μ Έμ˜€κΈ° + const currentTableName = tables?.[0]?.tableName; + + // μ»΄ν¬λ„ŒνŠΈλ³„ μΆ”κ°€ props + const extraProps: Record = {}; + if (componentId === "v2-select") { + extraProps.inputType = inputType; + extraProps.tableName = selectedComponent.tableName || currentTable?.tableName || currentTableName; + extraProps.columnName = selectedComponent.columnName || currentConfig.fieldKey || currentConfig.columnName; + } + if (componentId === "v2-list") { + extraProps.currentTableName = currentTableName; + } + if (componentId === "v2-bom-item-editor" || componentId === "v2-bom-tree") { + extraProps.currentTableName = currentTableName; + extraProps.screenTableName = selectedComponent.tableName || currentTable?.tableName || currentTableName; + } + if (componentId === "v2-input") { + extraProps.allComponents = allComponents; + } + + return ( +
+ +
+ ); + } + } + if (componentId) { const definition = ComponentRegistry.getComponent(componentId); @@ -219,7 +275,9 @@ export const V2PropertiesPanel: React.FC = ({ allTables={allTables} screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} tableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} - columnName={(selectedComponent as any).columnName || currentConfig?.columnName || currentConfig?.fieldName} + columnName={ + (selectedComponent as any).columnName || currentConfig?.columnName || currentConfig?.fieldName + } inputType={(selectedComponent as any).inputType || currentConfig?.inputType} componentType={componentType} tableColumns={currentTable?.columns || []} @@ -334,11 +392,11 @@ export const V2PropertiesPanel: React.FC = ({ return (
{/* DIMENSIONS μ„Ήμ…˜ */} -
-

DIMENSIONS

+
+

DIMENSIONS

- + = ({ />
- + = ({ />
- + = ({ {/* Title (group/area) */} {(selectedComponent.type === "group" || selectedComponent.type === "area") && ( -
-

CONTENT

+
+

CONTENT

- 제λͺ© + 제λͺ©
= ({
{selectedComponent.type === "area" && (
- μ„€λͺ… + μ„€λͺ…
= ({ )} {/* OPTIONS μ„Ήμ…˜ */} -
-

OPTIONS

- {(isInputField || widget.required !== undefined) && (() => { - const colName = widget.columnName || selectedComponent?.columnName; - const colMeta = colName ? currentTable?.columns?.find( - (c: any) => (c.columnName || c.column_name || "").toLowerCase() === colName.toLowerCase() - ) : null; - const isNotNull = colMeta && ((colMeta as any).isNullable === "NO" || (colMeta as any).isNullable === "N" || (colMeta as any).is_nullable === "NO" || (colMeta as any).is_nullable === "N"); - return ( -
- - ν•„μˆ˜ - {isNotNull && (NOT NULL)} - - { - if (isNotNull) return; - handleUpdate("required", checked); - handleUpdate("componentConfig.required", checked); - }} - disabled={!!isNotNull} - className="h-4 w-4" - /> -
- ); - })()} +
+

OPTIONS

+ {(isInputField || widget.required !== undefined) && + (() => { + const colName = widget.columnName || selectedComponent?.columnName; + const colMeta = colName + ? currentTable?.columns?.find( + (c: any) => (c.columnName || c.column_name || "").toLowerCase() === colName.toLowerCase(), + ) + : null; + const isNotNull = + colMeta && + ((colMeta as any).isNullable === "NO" || + (colMeta as any).isNullable === "N" || + (colMeta as any).is_nullable === "NO" || + (colMeta as any).is_nullable === "N"); + return ( +
+ + ν•„μˆ˜ + {isNotNull && (NOT NULL)} + + { + if (isNotNull) return; + handleUpdate("required", checked); + handleUpdate("componentConfig.required", checked); + }} + disabled={!!isNotNull} + className="h-4 w-4" + /> +
+ ); + })()} {(isInputField || widget.readonly !== undefined) && (
- μ½κΈ°μ „μš© + μ½κΈ°μ „μš© { @@ -489,7 +557,7 @@ export const V2PropertiesPanel: React.FC = ({
)}
- μˆ¨κΉ€ + μˆ¨κΉ€ { @@ -505,13 +573,13 @@ export const V2PropertiesPanel: React.FC = ({ {isInputField && ( - LABEL - + LABEL + {/* 라벨 ν…μŠ€νŠΈ */}
- ν…μŠ€νŠΈ + ν…μŠ€νŠΈ
= ({ {/* μœ„μΉ˜ + 간격 */}
- +
- + { const pos = selectedComponent.style?.labelPosition; @@ -570,7 +639,7 @@ export const V2PropertiesPanel: React.FC = ({ {/* 크기 + 색상 */}
- + handleUpdate("style.labelFontSize", e.target.value)} @@ -578,7 +647,7 @@ export const V2PropertiesPanel: React.FC = ({ />
- + handleUpdate("style.labelColor", value)} @@ -589,7 +658,7 @@ export const V2PropertiesPanel: React.FC = ({
{/* κ΅΅κΈ° */}
- κ΅΅κΈ° + κ΅΅κΈ°
updateColumn(idx, "label", e.target.value)} className="h-7 text-xs" /> +
+
+ λ„ˆλΉ„ + updateColumn(idx, "width", parseInt(e.target.value) || 100)} className="h-7 text-xs" /> +
+
+ μ •λ ¬ + +
+
+ +
+ + ))} + +
+ + + ); +} + // ─── 메인 μ»΄ν¬λ„ŒνŠΈ ─── -export const V2ItemRoutingConfigPanel: React.FC = ({ - config: configProp, - onChange, -}) => { +export const V2ItemRoutingConfigPanel: React.FC = ({ config: configProp, onChange }) => { const [tables, setTables] = useState([]); const [loadingTables, setLoadingTables] = useState(false); const [modalOpen, setModalOpen] = useState(false); - const [columnsOpen, setColumnsOpen] = useState(false); const [dataSourceOpen, setDataSourceOpen] = useState(false); const [layoutOpen, setLayoutOpen] = useState(false); + const [filterOpen, setFilterOpen] = useState(false); const config: ItemRoutingConfig = { ...defaultConfig, @@ -278,6 +284,9 @@ export const V2ItemRoutingConfigPanel: React.FC = dataSource: { ...defaultConfig.dataSource, ...configProp?.dataSource }, modals: { ...defaultConfig.modals, ...configProp?.modals }, processColumns: configProp?.processColumns?.length ? configProp.processColumns : defaultConfig.processColumns, + itemDisplayColumns: configProp?.itemDisplayColumns?.length ? configProp.itemDisplayColumns : defaultConfig.itemDisplayColumns, + modalDisplayColumns: configProp?.modalDisplayColumns?.length ? configProp.modalDisplayColumns : defaultConfig.modalDisplayColumns, + itemFilterConditions: configProp?.itemFilterConditions || [], }; useEffect(() => { @@ -287,12 +296,7 @@ export const V2ItemRoutingConfigPanel: React.FC = const { tableManagementApi } = await import("@/lib/api/tableManagement"); const res = await tableManagementApi.getTableList(); if (res.success && res.data) { - setTables( - res.data.map((t: any) => ({ - tableName: t.tableName, - displayName: t.displayName || t.tableName, - })) - ); + setTables(res.data.map((t: any) => ({ tableName: t.tableName, displayName: t.displayName || t.tableName }))); } } catch { /* ignore */ } finally { setLoadingTables(false); } }; @@ -301,11 +305,7 @@ export const V2ItemRoutingConfigPanel: React.FC = const dispatchConfigEvent = (newConfig: Partial) => { if (typeof window !== "undefined") { - window.dispatchEvent( - new CustomEvent("componentConfigChanged", { - detail: { config: { ...config, ...newConfig } }, - }) - ); + window.dispatchEvent(new CustomEvent("componentConfigChanged", { detail: { config: { ...config, ...newConfig } } })); } }; @@ -316,61 +316,141 @@ export const V2ItemRoutingConfigPanel: React.FC = }; const updateDataSource = (field: string, value: string) => { - const newDataSource = { ...config.dataSource, [field]: value }; - const partial = { dataSource: newDataSource }; - onChange({ ...configProp, ...partial }); - dispatchConfigEvent(partial); + const newDS = { ...config.dataSource, [field]: value }; + onChange({ ...configProp, dataSource: newDS }); + dispatchConfigEvent({ dataSource: newDS }); }; const updateModals = (field: string, value?: number) => { - const newModals = { ...config.modals, [field]: value }; - const partial = { modals: newModals }; - onChange({ ...configProp, ...partial }); - dispatchConfigEvent(partial); + const newM = { ...config.modals, [field]: value }; + onChange({ ...configProp, modals: newM }); + dispatchConfigEvent({ modals: newM }); }; - // 곡정 컬럼 관리 - const addColumn = () => { - update({ - processColumns: [ - ...config.processColumns, - { name: "", label: "μƒˆ 컬럼", width: 100, align: "left" as const }, - ], - }); - }; - - const removeColumn = (idx: number) => { - update({ processColumns: config.processColumns.filter((_, i) => i !== idx) }); - }; - - const updateColumn = (idx: number, field: keyof ProcessColumnDef, value: string | number) => { - const next = [...config.processColumns]; - next[idx] = { ...next[idx], [field]: value }; - update({ processColumns: next }); + // ν•„ν„° 쑰건 관리 + const filters = config.itemFilterConditions || []; + const addFilter = () => update({ itemFilterConditions: [...filters, { column: "", operator: "equals", value: "" }] }); + const removeFilter = (idx: number) => update({ itemFilterConditions: filters.filter((_, i) => i !== idx) }); + const updateFilter = (idx: number, field: keyof ItemFilterCondition, val: string) => { + const next = [...filters]; + next[idx] = { ...next[idx], [field]: val }; + update({ itemFilterConditions: next }); }; return (
- {/* ─── 1단계: λͺ¨λ‹¬ 연동 (Collapsible) ─── */} + {/* ─── ν’ˆλͺ© λͺ©λ‘ λͺ¨λ“œ ─── */} +
+
+ + ν’ˆλͺ© λͺ©λ‘ λͺ¨λ“œ +
+

쒌츑 ν’ˆλͺ© λͺ©λ‘μ— ν‘œμ‹œν•  방식을 μ„ νƒν•˜μ„Έμš”

+
+ + +
+ {config.itemListMode === "registered" && ( +

+ ν˜„μž¬ ν™”λ©΄ IDλ₯Ό κΈ°μ€€μœΌλ‘œ ν’ˆλͺ© λͺ©λ‘μ΄ μžλ™ κ΄€λ¦¬λ©λ‹ˆλ‹€. +

+ )} +
+ + {/* ─── ν’ˆλͺ© ν‘œμ‹œ 컬럼 ─── */} + update({ itemDisplayColumns: cols })} + tableName={config.dataSource.itemTable} + title="ν’ˆλͺ© λͺ©λ‘ 컬럼" + icon={} + /> + + {/* ─── λͺ¨λ‹¬ ν‘œμ‹œ 컬럼 (등둝 λͺ¨λ“œμ—μ„œλ§Œ 의미 μžˆμ§€λ§Œ 항상 μ„€μ • κ°€λŠ₯) ─── */} + update({ modalDisplayColumns: cols })} + tableName={config.dataSource.itemTable} + title="ν’ˆλͺ© μΆ”κ°€ λͺ¨λ‹¬ 컬럼" + icon={} + /> + + {/* ─── ν’ˆλͺ© ν•„ν„° 쑰건 ─── */} + + + + + +
+

ν’ˆλͺ© 쑰회 μ‹œ μžλ™μœΌλ‘œ μ μš©λ˜λŠ” ν•„ν„° μ‘°κ±΄μž…λ‹ˆλ‹€

+ {filters.map((f, idx) => ( +
+
+ 컬럼 + updateFilter(idx, "column", v)} + tableName={config.dataSource.itemTable} placeholder="ν•„ν„° 컬럼" /> +
+
+ 쑰건 + +
+
+ κ°’ + updateFilter(idx, "value", e.target.value)} + placeholder="ν•„ν„°κ°’" className="h-7 text-xs" /> +
+ +
+ ))} + +
+
+
+ + {/* ─── λͺ¨λ‹¬ 연동 ─── */} - @@ -379,291 +459,103 @@ export const V2ItemRoutingConfigPanel: React.FC =
버전 μΆ”κ°€ - updateModals("versionAddScreenId", v)} - /> + updateModals("versionAddScreenId", v)} />
곡정 μΆ”κ°€ - updateModals("processAddScreenId", v)} - /> + updateModals("processAddScreenId", v)} />
곡정 μˆ˜μ • - updateModals("processEditScreenId", v)} - /> + updateModals("processEditScreenId", v)} />
- {/* ─── 2단계: 곡정 ν…Œμ΄λΈ” 컬럼 (Collapsible + 접이식 μΉ΄λ“œ) ─── */} - - - - - -
-

곡정 μˆœμ„œ ν…Œμ΄λΈ”μ— ν‘œμ‹œν•  컬럼

-
- {config.processColumns.map((col, idx) => ( - -
- - - - - -
-
- 컬럼λͺ… - updateColumn(idx, "name", e.target.value)} - className="h-7 text-xs" - placeholder="컬럼λͺ…" - /> -
-
- ν‘œμ‹œλͺ… - updateColumn(idx, "label", e.target.value)} - className="h-7 text-xs" - placeholder="ν‘œμ‹œλͺ…" - /> -
-
- λ„ˆλΉ„ - updateColumn(idx, "width", parseInt(e.target.value) || 100)} - className="h-7 text-xs" - placeholder="100" - /> -
-
- μ •λ ¬ - -
-
-
-
-
- ))} -
- -
-
-
+ {/* ─── 곡정 ν…Œμ΄λΈ” 컬럼 ─── */} + update({ processColumns: cols })} + tableName={config.dataSource.routingDetailTable} + title="곡정 ν…Œμ΄λΈ” 컬럼" + icon={} + /> - {/* ─── 3단계: 데이터 μ†ŒμŠ€ (Collapsible) ─── */} + {/* ─── 데이터 μ†ŒμŠ€ ─── */} -
ν’ˆλͺ© ν…Œμ΄λΈ” - updateDataSource("itemTable", v)} - tables={tables} - loading={loadingTables} - /> + updateDataSource("itemTable", v)} tables={tables} loading={loadingTables} />
ν’ˆλͺ©λͺ… 컬럼 - updateDataSource("itemNameColumn", v)} - tableName={config.dataSource.itemTable} - placeholder="ν’ˆλͺ©λͺ…" - /> + updateDataSource("itemNameColumn", v)} tableName={config.dataSource.itemTable} placeholder="ν’ˆλͺ©λͺ…" />
ν’ˆλͺ©μ½”λ“œ 컬럼 - updateDataSource("itemCodeColumn", v)} - tableName={config.dataSource.itemTable} - placeholder="ν’ˆλͺ©μ½”λ“œ" - /> + updateDataSource("itemCodeColumn", v)} tableName={config.dataSource.itemTable} placeholder="ν’ˆλͺ©μ½”λ“œ" />
λΌμš°νŒ… 버전 ν…Œμ΄λΈ” - updateDataSource("routingVersionTable", v)} - tables={tables} - loading={loadingTables} - /> + updateDataSource("routingVersionTable", v)} tables={tables} loading={loadingTables} />
ν’ˆλͺ© FK 컬럼 - updateDataSource("routingVersionFkColumn", v)} - tableName={config.dataSource.routingVersionTable} - placeholder="FK 컬럼" - /> + updateDataSource("routingVersionFkColumn", v)} tableName={config.dataSource.routingVersionTable} placeholder="FK 컬럼" />
버전λͺ… 컬럼 - updateDataSource("routingVersionNameColumn", v)} - tableName={config.dataSource.routingVersionTable} - placeholder="버전λͺ…" - /> + updateDataSource("routingVersionNameColumn", v)} tableName={config.dataSource.routingVersionTable} placeholder="버전λͺ…" />
λΌμš°νŒ… 상세 ν…Œμ΄λΈ” - updateDataSource("routingDetailTable", v)} - tables={tables} - loading={loadingTables} - /> + updateDataSource("routingDetailTable", v)} tables={tables} loading={loadingTables} />
버전 FK 컬럼 - updateDataSource("routingDetailFkColumn", v)} - tableName={config.dataSource.routingDetailTable} - placeholder="FK 컬럼" - /> + updateDataSource("routingDetailFkColumn", v)} tableName={config.dataSource.routingDetailTable} placeholder="FK 컬럼" />
곡정 λ§ˆμŠ€ν„° ν…Œμ΄λΈ” - updateDataSource("processTable", v)} - tables={tables} - loading={loadingTables} - /> + updateDataSource("processTable", v)} tables={tables} loading={loadingTables} />
곡정λͺ… 컬럼 - updateDataSource("processNameColumn", v)} - tableName={config.dataSource.processTable} - placeholder="곡정λͺ…" - /> + updateDataSource("processNameColumn", v)} tableName={config.dataSource.processTable} placeholder="곡정λͺ…" />
κ³΅μ •μ½”λ“œ 컬럼 - updateDataSource("processCodeColumn", v)} - tableName={config.dataSource.processTable} - placeholder="κ³΅μ •μ½”λ“œ" - /> + updateDataSource("processCodeColumn", v)} tableName={config.dataSource.processTable} placeholder="κ³΅μ •μ½”λ“œ" />
- {/* ─── 4단계: λ ˆμ΄μ•„μ›ƒ & 기타 (Collapsible) ─── */} + {/* ─── λ ˆμ΄μ•„μ›ƒ & 기타 ─── */} - @@ -673,76 +565,38 @@ export const V2ItemRoutingConfigPanel: React.FC = 쒌츑 νŒ¨λ„ λΉ„μœ¨ (%)

ν’ˆλͺ© λͺ©λ‘ νŒ¨λ„μ˜ λ„ˆλΉ„

- update({ splitRatio: parseInt(e.target.value) || 40 })} - className="h-7 w-[80px] text-xs" - /> + update({ splitRatio: parseInt(e.target.value) || 40 })} className="h-7 w-[80px] text-xs" />
-
쒌츑 νŒ¨λ„ 제λͺ© - update({ leftPanelTitle: e.target.value })} - placeholder="ν’ˆλͺ© λͺ©λ‘" - className="h-7 w-[140px] text-xs" - /> + update({ leftPanelTitle: e.target.value })} placeholder="ν’ˆλͺ© λͺ©λ‘" className="h-7 w-[140px] text-xs" />
-
우츑 νŒ¨λ„ 제λͺ© - update({ rightPanelTitle: e.target.value })} - placeholder="곡정 μˆœμ„œ" - className="h-7 w-[140px] text-xs" - /> + update({ rightPanelTitle: e.target.value })} placeholder="곡정 μˆœμ„œ" className="h-7 w-[140px] text-xs" />
-
버전 μΆ”κ°€ λ²„νŠΌ ν…μŠ€νŠΈ - update({ versionAddButtonText: e.target.value })} - placeholder="+ λΌμš°νŒ… 버전 μΆ”κ°€" - className="h-7 w-[140px] text-xs" - /> + update({ versionAddButtonText: e.target.value })} placeholder="+ λΌμš°νŒ… 버전 μΆ”κ°€" className="h-7 w-[140px] text-xs" />
-
곡정 μΆ”κ°€ λ²„νŠΌ ν…μŠ€νŠΈ - update({ processAddButtonText: e.target.value })} - placeholder="+ 곡정 μΆ”κ°€" - className="h-7 w-[140px] text-xs" - /> + update({ processAddButtonText: e.target.value })} placeholder="+ 곡정 μΆ”κ°€" className="h-7 w-[140px] text-xs" />
-

첫 번째 버전 μžλ™ 선택

ν’ˆλͺ© 선택 μ‹œ 첫 버전을 μžλ™μœΌλ‘œ μ„ νƒν•΄μš”

- update({ autoSelectFirstVersion: checked })} - /> + update({ autoSelectFirstVersion: checked })} />
-

읽기 μ „μš©

μΆ”κ°€/μˆ˜μ •/μ‚­μ œ λ²„νŠΌμ„ μˆ¨κ²¨μš”

- update({ readonly: checked })} - /> + update({ readonly: checked })} />
@@ -752,5 +606,4 @@ export const V2ItemRoutingConfigPanel: React.FC = }; V2ItemRoutingConfigPanel.displayName = "V2ItemRoutingConfigPanel"; - export default V2ItemRoutingConfigPanel; diff --git a/frontend/lib/registry/components/v2-item-routing/ItemRoutingComponent.tsx b/frontend/lib/registry/components/v2-item-routing/ItemRoutingComponent.tsx index a8f752f9..d0144d0b 100644 --- a/frontend/lib/registry/components/v2-item-routing/ItemRoutingComponent.tsx +++ b/frontend/lib/registry/components/v2-item-routing/ItemRoutingComponent.tsx @@ -1,217 +1,203 @@ "use client"; -import React, { useState, useEffect, useCallback, useMemo } from "react"; -import { Search, Plus, Trash2, Edit, ListOrdered, Package, Star } from "lucide-react"; +import React, { useState, useEffect, useCallback } from "react"; +import { Search, Plus, Trash2, Edit, ListOrdered, Package, Star, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, + Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, + AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, + AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; +import { + Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, +} from "@/components/ui/dialog"; +import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; import { useToast } from "@/hooks/use-toast"; -import { ItemRoutingConfig, ItemRoutingComponentProps } from "./types"; -import { defaultConfig } from "./config"; +import { ItemRoutingComponentProps, ColumnDef } from "./types"; import { useItemRouting } from "./hooks/useItemRouting"; +const DEFAULT_ITEM_COLS: ColumnDef[] = [ + { name: "item_name", label: "ν’ˆλͺ…" }, + { name: "item_code", label: "ν’ˆλ²ˆ", width: 100 }, +]; + export function ItemRoutingComponent({ config: configProp, isPreview, + screenId, }: ItemRoutingComponentProps) { const { toast } = useToast(); + const resolvedConfig = React.useMemo(() => { + if (configProp?.itemListMode === "registered" && !configProp?.screenCode && screenId) { + return { ...configProp, screenCode: `screen_${screenId}` }; + } + return configProp; + }, [configProp, screenId]); + const { - config, - items, - versions, - details, - loading, - selectedItemCode, - selectedItemName, - selectedVersionId, - fetchItems, - selectItem, - selectVersion, - refreshVersions, - refreshDetails, - deleteDetail, - deleteVersion, - setDefaultVersion, - unsetDefaultVersion, - } = useItemRouting(configProp || {}); + config, items, allItems, versions, details, loading, + selectedItemCode, selectedItemName, selectedVersionId, isRegisteredMode, + fetchItems, fetchRegisteredItems, fetchAllItems, + registerItemsBatch, unregisterItem, + selectItem, selectVersion, refreshVersions, refreshDetails, + deleteDetail, deleteVersion, setDefaultVersion, unsetDefaultVersion, + } = useItemRouting(resolvedConfig || {}); const [searchText, setSearchText] = useState(""); const [deleteTarget, setDeleteTarget] = useState<{ - type: "version" | "detail"; - id: string; - name: string; + type: "version" | "detail"; id: string; name: string; } | null>(null); - // 초기 λ‘œλ”© (마운트 μ‹œ 1회만) + // ν’ˆλͺ© μΆ”κ°€ λ‹€μ΄μ–Όλ‘œκ·Έ + const [addDialogOpen, setAddDialogOpen] = useState(false); + const [addSearchText, setAddSearchText] = useState(""); + const [selectedAddItems, setSelectedAddItems] = useState>(new Set()); + const [addLoading, setAddLoading] = useState(false); + + const itemDisplayCols = config.itemDisplayColumns?.length + ? config.itemDisplayColumns : DEFAULT_ITEM_COLS; + const modalDisplayCols = config.modalDisplayColumns?.length + ? config.modalDisplayColumns : DEFAULT_ITEM_COLS; + + // 초기 λ‘œλ”© const mountedRef = React.useRef(false); useEffect(() => { if (!mountedRef.current) { mountedRef.current = true; - fetchItems(); + if (isRegisteredMode) fetchRegisteredItems(); + else fetchItems(); } - }, [fetchItems]); + }, [fetchItems, fetchRegisteredItems, isRegisteredMode]); - // λͺ¨λ‹¬ μ €μž₯ 성곡 감지 -> 데이터 μƒˆλ‘œκ³ μΉ¨ + // λͺ¨λ‹¬ μ €μž₯ 성곡 감지 const refreshVersionsRef = React.useRef(refreshVersions); const refreshDetailsRef = React.useRef(refreshDetails); refreshVersionsRef.current = refreshVersions; refreshDetailsRef.current = refreshDetails; - useEffect(() => { - const handleSaveSuccess = () => { - refreshVersionsRef.current(); - refreshDetailsRef.current(); - }; - window.addEventListener("saveSuccessInModal", handleSaveSuccess); - return () => { - window.removeEventListener("saveSuccessInModal", handleSaveSuccess); - }; + const h = () => { refreshVersionsRef.current(); refreshDetailsRef.current(); }; + window.addEventListener("saveSuccessInModal", h); + return () => window.removeEventListener("saveSuccessInModal", h); }, []); - // ν’ˆλͺ© 검색 + // 검색 const handleSearch = useCallback(() => { - fetchItems(searchText || undefined); - }, [fetchItems, searchText]); + if (isRegisteredMode) fetchRegisteredItems(searchText || undefined); + else fetchItems(searchText || undefined); + }, [fetchItems, fetchRegisteredItems, isRegisteredMode, searchText]); - const handleSearchKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === "Enter") handleSearch(); + // ──── ν’ˆλͺ© μΆ”κ°€ λͺ¨λ‹¬ ──── + const handleOpenAddDialog = useCallback(() => { + setAddSearchText(""); setSelectedAddItems(new Set()); setAddDialogOpen(true); + fetchAllItems(); + }, [fetchAllItems]); + + const handleToggleAddItem = useCallback((itemId: string) => { + setSelectedAddItems((prev) => { + const next = new Set(prev); + next.has(itemId) ? next.delete(itemId) : next.add(itemId); + return next; + }); + }, []); + + const handleConfirmAdd = useCallback(async () => { + if (selectedAddItems.size === 0) return; + setAddLoading(true); + const itemList = allItems + .filter((item) => selectedAddItems.has(item.id)) + .map((item) => ({ + itemId: item.id, + itemCode: item.item_code || item[config.dataSource.itemCodeColumn] || "", + })); + const success = await registerItemsBatch(itemList); + setAddLoading(false); + if (success) { + toast({ title: `${itemList.length}개 ν’ˆλͺ©μ΄ λ“±λ‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€` }); + setAddDialogOpen(false); + } else { + toast({ title: "ν’ˆλͺ© 등둝 μ‹€νŒ¨", variant: "destructive" }); + } + }, [selectedAddItems, allItems, config.dataSource.itemCodeColumn, registerItemsBatch, toast]); + + const handleUnregisterItem = useCallback( + async (registeredId: string, itemName: string) => { + const success = await unregisterItem(registeredId); + if (success) toast({ title: `${itemName} 등둝 ν•΄μ œλ¨` }); + else toast({ title: "등둝 ν•΄μ œ μ‹€νŒ¨", variant: "destructive" }); }, - [handleSearch] + [unregisterItem, toast] ); - // 버전 μΆ”κ°€ λͺ¨λ‹¬ + // ──── κΈ°μ‘΄ ν•Έλ“€λŸ¬ ──── const handleAddVersion = useCallback(() => { - if (!selectedItemCode) { - toast({ title: "ν’ˆλͺ©μ„ λ¨Όμ € μ„ νƒν•΄μ£Όμ„Έμš”", variant: "destructive" }); - return; - } - const screenId = config.modals.versionAddScreenId; - if (!screenId) return; - - window.dispatchEvent( - new CustomEvent("openScreenModal", { - detail: { - screenId, - urlParams: { mode: "add", tableName: config.dataSource.routingVersionTable }, - splitPanelParentData: { - [config.dataSource.routingVersionFkColumn]: selectedItemCode, - }, - }, - }) - ); + if (!selectedItemCode) { toast({ title: "ν’ˆλͺ©μ„ λ¨Όμ € μ„ νƒν•΄μ£Όμ„Έμš”", variant: "destructive" }); return; } + const sid = config.modals.versionAddScreenId; + if (!sid) return; + window.dispatchEvent(new CustomEvent("openScreenModal", { + detail: { screenId: sid, urlParams: { mode: "add", tableName: config.dataSource.routingVersionTable }, + splitPanelParentData: { [config.dataSource.routingVersionFkColumn]: selectedItemCode } }, + })); }, [selectedItemCode, config, toast]); - // 곡정 μΆ”κ°€ λͺ¨λ‹¬ const handleAddProcess = useCallback(() => { - if (!selectedVersionId) { - toast({ title: "λΌμš°νŒ… 버전을 λ¨Όμ € μ„ νƒν•΄μ£Όμ„Έμš”", variant: "destructive" }); - return; - } - const screenId = config.modals.processAddScreenId; - if (!screenId) return; - - window.dispatchEvent( - new CustomEvent("openScreenModal", { - detail: { - screenId, - urlParams: { mode: "add", tableName: config.dataSource.routingDetailTable }, - splitPanelParentData: { - [config.dataSource.routingDetailFkColumn]: selectedVersionId, - }, - }, - }) - ); + if (!selectedVersionId) { toast({ title: "λΌμš°νŒ… 버전을 λ¨Όμ € μ„ νƒν•΄μ£Όμ„Έμš”", variant: "destructive" }); return; } + const sid = config.modals.processAddScreenId; + if (!sid) return; + window.dispatchEvent(new CustomEvent("openScreenModal", { + detail: { screenId: sid, urlParams: { mode: "add", tableName: config.dataSource.routingDetailTable }, + splitPanelParentData: { [config.dataSource.routingDetailFkColumn]: selectedVersionId } }, + })); }, [selectedVersionId, config, toast]); - // 곡정 μˆ˜μ • λͺ¨λ‹¬ const handleEditProcess = useCallback( (detail: Record) => { - const screenId = config.modals.processEditScreenId; - if (!screenId) return; - - window.dispatchEvent( - new CustomEvent("openScreenModal", { - detail: { - screenId, - urlParams: { mode: "edit", tableName: config.dataSource.routingDetailTable }, - editData: detail, - }, - }) - ); - }, - [config] + const sid = config.modals.processEditScreenId; + if (!sid) return; + window.dispatchEvent(new CustomEvent("openScreenModal", { + detail: { screenId: sid, urlParams: { mode: "edit", tableName: config.dataSource.routingDetailTable }, editData: detail }, + })); + }, [config] ); - // κΈ°λ³Έ 버전 ν† κΈ€ const handleToggleDefault = useCallback( async (versionId: string, currentIsDefault: boolean) => { - let success: boolean; - if (currentIsDefault) { - success = await unsetDefaultVersion(versionId); - if (success) toast({ title: "κΈ°λ³Έ 버전이 ν•΄μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€" }); - } else { - success = await setDefaultVersion(versionId); - if (success) toast({ title: "κΈ°λ³Έ λ²„μ „μœΌλ‘œ μ„€μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€" }); - } - if (!success) { - toast({ title: "κΈ°λ³Έ 버전 λ³€κ²½ μ‹€νŒ¨", variant: "destructive" }); - } + const success = currentIsDefault ? await unsetDefaultVersion(versionId) : await setDefaultVersion(versionId); + if (success) toast({ title: currentIsDefault ? "κΈ°λ³Έ 버전이 ν•΄μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€" : "κΈ°λ³Έ λ²„μ „μœΌλ‘œ μ„€μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€" }); + else toast({ title: "κΈ°λ³Έ 버전 λ³€κ²½ μ‹€νŒ¨", variant: "destructive" }); }, [setDefaultVersion, unsetDefaultVersion, toast] ); - // μ‚­μ œ 확인 const handleConfirmDelete = useCallback(async () => { if (!deleteTarget) return; - - let success = false; - if (deleteTarget.type === "version") { - success = await deleteVersion(deleteTarget.id); - } else { - success = await deleteDetail(deleteTarget.id); - } - - if (success) { - toast({ title: `${deleteTarget.name} μ‚­μ œ μ™„λ£Œ` }); - } else { - toast({ title: "μ‚­μ œ μ‹€νŒ¨", variant: "destructive" }); - } + const success = deleteTarget.type === "version" + ? await deleteVersion(deleteTarget.id) : await deleteDetail(deleteTarget.id); + toast({ title: success ? `${deleteTarget.name} μ‚­μ œ μ™„λ£Œ` : "μ‚­μ œ μ‹€νŒ¨", variant: success ? undefined : "destructive" }); setDeleteTarget(null); }, [deleteTarget, deleteVersion, deleteDetail, toast]); const splitRatio = config.splitRatio || 40; + const registeredItemIds = React.useMemo(() => new Set(items.map((i) => i.id)), [items]); + + // ──── μ…€ κ°’ μΆ”μΆœ 헬퍼 ──── + const getCellValue = (item: Record, colName: string) => { + return item[colName] ?? item[`item_${colName}`] ?? "-"; + }; if (isPreview) { return (
-

- ν’ˆλͺ©λ³„ λΌμš°νŒ… 관리 -

+

ν’ˆλͺ©λ³„ λΌμš°νŒ… 관리

- ν’ˆλͺ© 선택 - λΌμš°νŒ… 버전 - 곡정 μˆœμ„œ + {isRegisteredMode ? "등둝 ν’ˆλͺ© λͺ¨λ“œ" : "전체 ν’ˆλͺ© λͺ¨λ“œ"}

@@ -221,94 +207,111 @@ export function ItemRoutingComponent({ return (
- {/* 쒌츑 νŒ¨λ„: ν’ˆλͺ© λͺ©λ‘ */} -
-
+ {/* ════ 쒌츑 νŒ¨λ„: ν’ˆλͺ© λͺ©λ‘ (ν…Œμ΄λΈ”) ════ */} +
+

{config.leftPanelTitle || "ν’ˆλͺ© λͺ©λ‘"} + {isRegisteredMode && ( + (등둝 λͺ¨λ“œ) + )}

+ {isRegisteredMode && !config.readonly && ( + + )}
- {/* 검색 */}
- setSearchText(e.target.value)} - onKeyDown={handleSearchKeyDown} - placeholder="ν’ˆλͺ©λͺ…/ν’ˆλ²ˆ 검색" - className="h-8 text-xs" - /> + setSearchText(e.target.value)} + onKeyDown={(e) => { if (e.key === "Enter") handleSearch(); }} + placeholder="ν’ˆλͺ©λͺ…/ν’ˆλ²ˆ 검색" className="h-8 text-xs" />
- {/* ν’ˆλͺ© 리슀트 */} -
+ {/* ν’ˆλͺ© ν…Œμ΄λΈ” */} +
{items.length === 0 ? ( -
+

- {loading ? "λ‘œλ”© 쀑..." : "ν’ˆλͺ©μ΄ μ—†μŠ΅λ‹ˆλ‹€"} + {loading ? "λ‘œλ”© 쀑..." : isRegisteredMode ? "λ“±λ‘λœ ν’ˆλͺ©μ΄ μ—†μŠ΅λ‹ˆλ‹€" : "ν’ˆλͺ©μ΄ μ—†μŠ΅λ‹ˆλ‹€"}

+ {isRegisteredMode && !loading && !config.readonly && ( + + )}
) : ( -
- {items.map((item) => { - const itemCode = - item[config.dataSource.itemCodeColumn] || item.item_code || item.item_number; - const itemName = - item[config.dataSource.itemNameColumn] || item.item_name; - const isSelected = selectedItemCode === itemCode; + + + + {itemDisplayCols.map((col) => ( + + {col.label} + + ))} + {isRegisteredMode && !config.readonly && ( + + )} + + + + {items.map((item) => { + const itemCode = item[config.dataSource.itemCodeColumn] || item.item_code || item.item_number; + const itemName = item[config.dataSource.itemNameColumn] || item.item_name; + const isSelected = selectedItemCode === itemCode; - return ( - - ); - })} - + return ( + selectItem(itemCode, itemName)}> + {itemDisplayCols.map((col) => ( + + {getCellValue(item, col.name)} + + ))} + {isRegisteredMode && !config.readonly && item.registered_id && ( + + + + )} + + ); + })} + +
)}
- {/* 우츑 νŒ¨λ„: 버전 + 곡정 */} + {/* ════ 우츑 νŒ¨λ„: 버전 + 곡정 ════ */}
{selectedItemCode ? ( <> - {/* 헀더: μ„ νƒλœ ν’ˆλͺ© + 버전 μΆ”κ°€ */}

{selectedItemName}

{selectedItemCode}

{!config.readonly && ( - )}
- {/* 버전 선택 λ²„νŠΌλ“€ */} {versions.length > 0 ? (
버전: @@ -317,50 +320,24 @@ export function ItemRoutingComponent({ const isDefault = ver.is_default === true; return (
- selectVersion(ver.id)} - > + isDefault && !isActive && "border-amber-400 bg-amber-50 text-amber-700")} + onClick={() => selectVersion(ver.id)}> {isDefault && } {ver[config.dataSource.routingVersionNameColumn] || ver.version_name || ver.id} {!config.readonly && ( <> - - @@ -371,112 +348,65 @@ export function ItemRoutingComponent({
) : (
-

- λΌμš°νŒ… 버전이 μ—†μŠ΅λ‹ˆλ‹€. 버전을 μΆ”κ°€ν•΄μ£Όμ„Έμš”. -

+

λΌμš°νŒ… 버전이 μ—†μŠ΅λ‹ˆλ‹€. 버전을 μΆ”κ°€ν•΄μ£Όμ„Έμš”.

)} - {/* 곡정 ν…Œμ΄λΈ” */} {selectedVersionId ? (
- {/* 곡정 ν…Œμ΄λΈ” 헀더 */}

{config.rightPanelTitle || "곡정 μˆœμ„œ"} ({details.length}건)

{!config.readonly && ( - )}
- - {/* ν…Œμ΄λΈ” */}
{details.length === 0 ? (
-

- {loading ? "λ‘œλ”© 쀑..." : "λ“±λ‘λœ 곡정이 μ—†μŠ΅λ‹ˆλ‹€"} -

+

{loading ? "λ‘œλ”© 쀑..." : "λ“±λ‘λœ 곡정이 μ—†μŠ΅λ‹ˆλ‹€"}

) : ( {config.processColumns.map((col) => ( - + className={cn("text-xs", col.align === "center" && "text-center", col.align === "right" && "text-right")}> {col.label} ))} - {!config.readonly && ( - - 관리 - - )} + {!config.readonly && 관리} {details.map((detail) => ( {config.processColumns.map((col) => { - let cellValue = detail[col.name]; - if (cellValue == null) { - const aliasKey = Object.keys(detail).find( - (k) => k.endsWith(`_${col.name}`) - ); - if (aliasKey) cellValue = detail[aliasKey]; + let v = detail[col.name]; + if (v == null) { + const ak = Object.keys(detail).find((k) => k.endsWith(`_${col.name}`)); + if (ak) v = detail[ak]; } return ( - - {cellValue ?? "-"} + + {v ?? "-"} ); })} {!config.readonly && (
- -
@@ -492,9 +422,7 @@ export function ItemRoutingComponent({ ) : ( versions.length > 0 && (
-

- λΌμš°νŒ… 버전을 μ„ νƒν•΄μ£Όμ„Έμš” -

+

λΌμš°νŒ… 버전을 μ„ νƒν•΄μ£Όμ„Έμš”

) )} @@ -502,43 +430,121 @@ export function ItemRoutingComponent({ ) : (
-

- μ’ŒμΈ‘μ—μ„œ ν’ˆλͺ©μ„ μ„ νƒν•˜μ„Έμš” -

-

- ν’ˆλͺ©μ„ μ„ νƒν•˜λ©΄ λΌμš°νŒ… 버전별 곡정 μˆœμ„œλ₯Ό 관리할 수 μžˆμŠ΅λ‹ˆλ‹€ -

+

μ’ŒμΈ‘μ—μ„œ ν’ˆλͺ©μ„ μ„ νƒν•˜μ„Έμš”

+

ν’ˆλͺ©μ„ μ„ νƒν•˜λ©΄ λΌμš°νŒ… 버전별 곡정 μˆœμ„œλ₯Ό 관리할 수 μžˆμŠ΅λ‹ˆλ‹€

)} - {/* μ‚­μ œ 확인 λ‹€μ΄μ–Όλ‘œκ·Έ */} + {/* ════ μ‚­μ œ 확인 ════ */} setDeleteTarget(null)}> μ‚­μ œ 확인 {deleteTarget?.name}을(λ₯Ό) μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ? - {deleteTarget?.type === "version" && ( - <> -
- ν•΄λ‹Ή 버전에 ν¬ν•¨λœ λͺ¨λ“  곡정 정보도 ν•¨κ»˜ μ‚­μ œλ©λ‹ˆλ‹€. - - )} + {deleteTarget?.type === "version" && (<>
ν•΄λ‹Ή 버전에 ν¬ν•¨λœ λͺ¨λ“  곡정 정보도 ν•¨κ»˜ μ‚­μ œλ©λ‹ˆλ‹€.)}
μ·¨μ†Œ - + μ‚­μ œ
+ + {/* ════ ν’ˆλͺ© μΆ”κ°€ λ‹€μ΄μ–Όλ‘œκ·Έ (ν…Œμ΄λΈ” ν˜•νƒœ + 검색) ════ */} + + + + ν’ˆλͺ© μΆ”κ°€ + + 쒌츑 λͺ©λ‘μ— ν‘œμ‹œν•  ν’ˆλͺ©μ„ μ„ νƒν•˜μ„Έμš” + {(config.itemFilterConditions?.length ?? 0) > 0 && ( + + (ν•„ν„° {config.itemFilterConditions!.length}건 적용됨) + + )} + + + +
+ setAddSearchText(e.target.value)} + onKeyDown={(e) => { if (e.key === "Enter") fetchAllItems(addSearchText || undefined); }} + placeholder="ν’ˆλͺ©λͺ…/ν’ˆλ²ˆ 검색" className="h-8 text-xs sm:h-10 sm:text-sm" /> + +
+ +
+ {allItems.length === 0 ? ( +
+

ν’ˆλͺ©μ΄ μ—†μŠ΅λ‹ˆλ‹€

+
+ ) : ( +
+ + + + {modalDisplayCols.map((col) => ( + + {col.label} + + ))} + μƒνƒœ + + + + {allItems.map((item) => { + const isAlreadyRegistered = registeredItemIds.has(item.id); + const isChecked = selectedAddItems.has(item.id); + return ( + { if (!isAlreadyRegistered) handleToggleAddItem(item.id); }}> + + + + {modalDisplayCols.map((col) => ( + + {getCellValue(item, col.name)} + + ))} + + {isAlreadyRegistered && ( + 등둝됨 + )} + + + ); + })} + +
+ )} +
+ + {selectedAddItems.size > 0 && ( +

{selectedAddItems.size}개 선택됨

+ )} + + + + + + +
); } diff --git a/frontend/lib/registry/components/v2-item-routing/ItemRoutingRenderer.tsx b/frontend/lib/registry/components/v2-item-routing/ItemRoutingRenderer.tsx index 7a9fa624..653d351d 100644 --- a/frontend/lib/registry/components/v2-item-routing/ItemRoutingRenderer.tsx +++ b/frontend/lib/registry/components/v2-item-routing/ItemRoutingRenderer.tsx @@ -9,7 +9,7 @@ export class ItemRoutingRenderer extends AutoRegisteringComponentRenderer { static componentDefinition = V2ItemRoutingDefinition; render(): React.ReactElement { - const { formData, isPreview, config, tableName } = this.props as Record< + const { formData, isPreview, config, tableName, screenId } = this.props as Record< string, unknown >; @@ -20,6 +20,7 @@ export class ItemRoutingRenderer extends AutoRegisteringComponentRenderer { formData={formData as Record} tableName={tableName as string} isPreview={isPreview as boolean} + screenId={screenId as number | string} /> ); } diff --git a/frontend/lib/registry/components/v2-item-routing/config.ts b/frontend/lib/registry/components/v2-item-routing/config.ts index a84ff23e..42ae1479 100644 --- a/frontend/lib/registry/components/v2-item-routing/config.ts +++ b/frontend/lib/registry/components/v2-item-routing/config.ts @@ -35,4 +35,15 @@ export const defaultConfig: ItemRoutingConfig = { autoSelectFirstVersion: true, versionAddButtonText: "+ λΌμš°νŒ… 버전 μΆ”κ°€", processAddButtonText: "+ 곡정 μΆ”κ°€", + itemListMode: "all", + screenCode: "", + itemDisplayColumns: [ + { name: "item_name", label: "ν’ˆλͺ…" }, + { name: "item_code", label: "ν’ˆλ²ˆ", width: 100 }, + ], + modalDisplayColumns: [ + { name: "item_name", label: "ν’ˆλͺ…" }, + { name: "item_code", label: "ν’ˆλ²ˆ", width: 100 }, + ], + itemFilterConditions: [], }; diff --git a/frontend/lib/registry/components/v2-item-routing/hooks/useItemRouting.ts b/frontend/lib/registry/components/v2-item-routing/hooks/useItemRouting.ts index 97f6be4f..bd1f551b 100644 --- a/frontend/lib/registry/components/v2-item-routing/hooks/useItemRouting.ts +++ b/frontend/lib/registry/components/v2-item-routing/hooks/useItemRouting.ts @@ -1,12 +1,21 @@ "use client"; -import { useState, useCallback, useEffect, useMemo, useRef } from "react"; +import { useState, useCallback, useMemo, useRef } from "react"; import { apiClient } from "@/lib/api/client"; -import { ItemRoutingConfig, ItemData, RoutingVersionData, RoutingDetailData } from "../types"; +import { ItemRoutingConfig, ItemData, RoutingVersionData, RoutingDetailData, ColumnDef } from "../types"; import { defaultConfig } from "../config"; const API_BASE = "/process-work-standard"; +/** ν‘œμ‹œ 컬럼 λͺ©λ‘μ—μ„œ κΈ°λ³Έ(item_name, item_code) μ™Έ μΆ”κ°€ 컬럼만 μΆ”μΆœ */ +function getExtraColumnNames(columns?: ColumnDef[]): string { + if (!columns || columns.length === 0) return ""; + return columns + .map((c) => c.name) + .filter((n) => n && n !== "item_name" && n !== "item_code") + .join(","); +} + export function useItemRouting(configPartial: Partial) { const configKey = useMemo( () => JSON.stringify(configPartial), @@ -27,21 +36,81 @@ export function useItemRouting(configPartial: Partial) { configRef.current = config; const [items, setItems] = useState([]); + const [allItems, setAllItems] = useState([]); const [versions, setVersions] = useState([]); const [details, setDetails] = useState([]); const [loading, setLoading] = useState(false); - // 선택 μƒνƒœ const [selectedItemCode, setSelectedItemCode] = useState(null); const [selectedItemName, setSelectedItemName] = useState(null); const [selectedVersionId, setSelectedVersionId] = useState(null); - // ν’ˆλͺ© λͺ©λ‘ 쑰회 + const isRegisteredMode = config.itemListMode === "registered"; + + /** API κΈ°λ³Έ νŒŒλΌλ―Έν„° 생성 */ + const buildBaseParams = useCallback((search?: string, columns?: ColumnDef[]) => { + const ds = configRef.current.dataSource; + const extra = getExtraColumnNames(columns); + const filters = configRef.current.itemFilterConditions; + const params: Record = { + tableName: ds.itemTable, + nameColumn: ds.itemNameColumn, + codeColumn: ds.itemCodeColumn, + routingTable: ds.routingVersionTable, + routingFkColumn: ds.routingVersionFkColumn, + }; + if (search) params.search = search; + if (extra) params.extraColumns = extra; + if (filters && filters.length > 0) { + params.filterConditions = JSON.stringify(filters); + } + return new URLSearchParams(params); + }, []); + + // ──────────────────────────────────────── + // ν’ˆλͺ© λͺ©λ‘ 쑰회 (all λͺ¨λ“œ) + // ──────────────────────────────────────── const fetchItems = useCallback( async (search?: string) => { + try { + setLoading(true); + const cols = configRef.current.itemDisplayColumns; + const params = buildBaseParams(search, cols); + const res = await apiClient.get(`${API_BASE}/items?${params}`); + if (res.data?.success) { + const data = res.data.data || []; + if (configRef.current.itemListMode !== "registered") { + setItems(data); + } + return data; + } + } catch (err) { + console.error("ν’ˆλͺ© 쑰회 μ‹€νŒ¨", err); + } finally { + setLoading(false); + } + return []; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [configKey, buildBaseParams] + ); + + // ──────────────────────────────────────── + // 등둝 ν’ˆλͺ© 쑰회 (registered λͺ¨λ“œ) + // ──────────────────────────────────────── + const fetchRegisteredItems = useCallback( + async (search?: string) => { + const screenCode = configRef.current.screenCode; + if (!screenCode) { + console.warn("screenCodeκ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€"); + setItems([]); + return; + } try { setLoading(true); const ds = configRef.current.dataSource; + const cols = configRef.current.itemDisplayColumns; + const extra = getExtraColumnNames(cols); const params = new URLSearchParams({ tableName: ds.itemTable, nameColumn: ds.itemNameColumn, @@ -49,13 +118,16 @@ export function useItemRouting(configPartial: Partial) { routingTable: ds.routingVersionTable, routingFkColumn: ds.routingVersionFkColumn, ...(search ? { search } : {}), + ...(extra ? { extraColumns: extra } : {}), }); - const res = await apiClient.get(`${API_BASE}/items?${params}`); + const res = await apiClient.get( + `${API_BASE}/registered-items/${encodeURIComponent(screenCode)}?${params}` + ); if (res.data?.success) { setItems(res.data.data || []); } } catch (err) { - console.error("ν’ˆλͺ© 쑰회 μ‹€νŒ¨", err); + console.error("등둝 ν’ˆλͺ© 쑰회 μ‹€νŒ¨", err); } finally { setLoading(false); } @@ -64,7 +136,104 @@ export function useItemRouting(configPartial: Partial) { [configKey] ); - // λΌμš°νŒ… 버전 λͺ©λ‘ 쑰회 + // ──────────────────────────────────────── + // 전체 ν’ˆλͺ© 쑰회 (등둝 νŒμ—…μš© - ν•„ν„°+μΆ”κ°€μ»¬λŸΌ 적용) + // ──────────────────────────────────────── + const fetchAllItems = useCallback( + async (search?: string) => { + try { + const cols = configRef.current.modalDisplayColumns; + const params = buildBaseParams(search, cols); + const res = await apiClient.get(`${API_BASE}/items?${params}`); + if (res.data?.success) { + setAllItems(res.data.data || []); + } + } catch (err) { + console.error("전체 ν’ˆλͺ© 쑰회 μ‹€νŒ¨", err); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [configKey, buildBaseParams] + ); + + // ──────────────────────────────────────── + // ν’ˆλͺ© 등둝/제거 (registered λͺ¨λ“œ) + // ──────────────────────────────────────── + const registerItem = useCallback( + async (itemId: string, itemCode: string) => { + const screenCode = configRef.current.screenCode; + if (!screenCode) return false; + try { + const res = await apiClient.post(`${API_BASE}/registered-items`, { + screenCode, + itemId, + itemCode, + }); + if (res.data?.success) { + await fetchRegisteredItems(); + return true; + } + } catch (err) { + console.error("ν’ˆλͺ© 등둝 μ‹€νŒ¨", err); + } + return false; + }, + [fetchRegisteredItems] + ); + + const registerItemsBatch = useCallback( + async (itemList: { itemId: string; itemCode: string }[]) => { + const screenCode = configRef.current.screenCode; + if (!screenCode) return false; + try { + const res = await apiClient.post(`${API_BASE}/registered-items/batch`, { + screenCode, + items: itemList, + }); + if (res.data?.success) { + await fetchRegisteredItems(); + return true; + } + } catch (err) { + console.error("ν’ˆλͺ© 일괄 등둝 μ‹€νŒ¨", err); + } + return false; + }, + [fetchRegisteredItems] + ); + + const unregisterItem = useCallback( + async (registeredId: string) => { + try { + const res = await apiClient.delete(`${API_BASE}/registered-items/${registeredId}`); + if (res.data?.success) { + if (selectedItemCode) { + const removedItem = items.find((i) => i.registered_id === registeredId); + if (removedItem) { + const removedCode = removedItem.item_code || removedItem[configRef.current.dataSource.itemCodeColumn]; + if (selectedItemCode === removedCode) { + setSelectedItemCode(null); + setSelectedItemName(null); + setSelectedVersionId(null); + setVersions([]); + setDetails([]); + } + } + } + await fetchRegisteredItems(); + return true; + } + } catch (err) { + console.error("등둝 ν’ˆλͺ© 제거 μ‹€νŒ¨", err); + } + return false; + }, + [selectedItemCode, items, fetchRegisteredItems] + ); + + // ──────────────────────────────────────── + // λΌμš°νŒ… 버전/곡정 κ΄€λ ¨ (κΈ°μ‘΄ 동일) + // ──────────────────────────────────────── const fetchVersions = useCallback( async (itemCode: string) => { try { @@ -94,7 +263,6 @@ export function useItemRouting(configPartial: Partial) { [configKey] ); - // 곡정 상세 λͺ©λ‘ 쑰회 (νŠΉμ • λ²„μ „μ˜ 곡정듀) - entity join 포함 const fetchDetails = useCallback( async (versionId: string) => { try { @@ -128,18 +296,14 @@ export function useItemRouting(configPartial: Partial) { [configKey] ); - // ν’ˆλͺ© 선택 const selectItem = useCallback( async (itemCode: string, itemName: string) => { setSelectedItemCode(itemCode); setSelectedItemName(itemName); setSelectedVersionId(null); setDetails([]); - const versionList = await fetchVersions(itemCode); - if (versionList.length > 0) { - // κΈ°λ³Έ 버전 μš°μ„ , μ—†μœΌλ©΄ 첫번째 버전 선택 const defaultVersion = versionList.find((v: RoutingVersionData) => v.is_default); const targetVersion = defaultVersion || (configRef.current.autoSelectFirstVersion ? versionList[0] : null); if (targetVersion) { @@ -151,7 +315,6 @@ export function useItemRouting(configPartial: Partial) { [fetchVersions, fetchDetails] ); - // 버전 선택 const selectVersion = useCallback( async (versionId: string) => { setSelectedVersionId(versionId); @@ -160,7 +323,6 @@ export function useItemRouting(configPartial: Partial) { [fetchDetails] ); - // λͺ¨λ‹¬μ—μ„œ 데이터 λ³€κ²½ ν›„ μƒˆλ‘œκ³ μΉ¨ const refreshVersions = useCallback(async () => { if (selectedItemCode) { const versionList = await fetchVersions(selectedItemCode); @@ -180,7 +342,6 @@ export function useItemRouting(configPartial: Partial) { } }, [selectedVersionId, fetchDetails]); - // 곡정 μ‚­μ œ const deleteDetail = useCallback( async (detailId: string) => { try { @@ -189,19 +350,13 @@ export function useItemRouting(configPartial: Partial) { `/table-management/tables/${ds.routingDetailTable}/delete`, { data: [{ id: detailId }] } ); - if (res.data?.success) { - await refreshDetails(); - return true; - } - } catch (err) { - console.error("곡정 μ‚­μ œ μ‹€νŒ¨", err); - } + if (res.data?.success) { await refreshDetails(); return true; } + } catch (err) { console.error("곡정 μ‚­μ œ μ‹€νŒ¨", err); } return false; }, [refreshDetails] ); - // 버전 μ‚­μ œ const deleteVersion = useCallback( async (versionId: string) => { try { @@ -211,22 +366,16 @@ export function useItemRouting(configPartial: Partial) { { data: [{ id: versionId }] } ); if (res.data?.success) { - if (selectedVersionId === versionId) { - setSelectedVersionId(null); - setDetails([]); - } + if (selectedVersionId === versionId) { setSelectedVersionId(null); setDetails([]); } await refreshVersions(); return true; } - } catch (err) { - console.error("버전 μ‚­μ œ μ‹€νŒ¨", err); - } + } catch (err) { console.error("버전 μ‚­μ œ μ‹€νŒ¨", err); } return false; }, [selectedVersionId, refreshVersions] ); - // κΈ°λ³Έ 버전 μ„€μ • const setDefaultVersion = useCallback( async (versionId: string) => { try { @@ -236,20 +385,15 @@ export function useItemRouting(configPartial: Partial) { routingFkColumn: ds.routingVersionFkColumn, }); if (res.data?.success) { - if (selectedItemCode) { - await fetchVersions(selectedItemCode); - } + if (selectedItemCode) await fetchVersions(selectedItemCode); return true; } - } catch (err) { - console.error("κΈ°λ³Έ 버전 μ„€μ • μ‹€νŒ¨", err); - } + } catch (err) { console.error("κΈ°λ³Έ 버전 μ„€μ • μ‹€νŒ¨", err); } return false; }, [selectedItemCode, fetchVersions] ); - // κΈ°λ³Έ 버전 ν•΄μ œ const unsetDefaultVersion = useCallback( async (versionId: string) => { try { @@ -258,14 +402,10 @@ export function useItemRouting(configPartial: Partial) { routingVersionTable: ds.routingVersionTable, }); if (res.data?.success) { - if (selectedItemCode) { - await fetchVersions(selectedItemCode); - } + if (selectedItemCode) await fetchVersions(selectedItemCode); return true; } - } catch (err) { - console.error("κΈ°λ³Έ 버전 ν•΄μ œ μ‹€νŒ¨", err); - } + } catch (err) { console.error("κΈ°λ³Έ 버전 ν•΄μ œ μ‹€νŒ¨", err); } return false; }, [selectedItemCode, fetchVersions] @@ -274,13 +414,20 @@ export function useItemRouting(configPartial: Partial) { return { config, items, + allItems, versions, details, loading, selectedItemCode, selectedItemName, selectedVersionId, + isRegisteredMode, fetchItems, + fetchRegisteredItems, + fetchAllItems, + registerItem, + registerItemsBatch, + unregisterItem, selectItem, selectVersion, refreshVersions, diff --git a/frontend/lib/registry/components/v2-item-routing/types.ts b/frontend/lib/registry/components/v2-item-routing/types.ts index 06b108da..08fe73c2 100644 --- a/frontend/lib/registry/components/v2-item-routing/types.ts +++ b/frontend/lib/registry/components/v2-item-routing/types.ts @@ -10,10 +10,10 @@ export interface ItemRoutingDataSource { itemNameColumn: string; itemCodeColumn: string; routingVersionTable: string; - routingVersionFkColumn: string; // item_routing_versionμ—μ„œ item_codeλ₯Ό κ°€λ¦¬ν‚€λŠ” FK + routingVersionFkColumn: string; routingVersionNameColumn: string; routingDetailTable: string; - routingDetailFkColumn: string; // item_routing_detailμ—μ„œ routing_version_idλ₯Ό κ°€λ¦¬ν‚€λŠ” FK + routingDetailFkColumn: string; processTable: string; processNameColumn: string; processCodeColumn: string; @@ -26,14 +26,24 @@ export interface ItemRoutingModals { processEditScreenId?: number; } -// 곡정 ν…Œμ΄λΈ” 컬럼 μ •μ˜ -export interface ProcessColumnDef { +// 컬럼 μ •μ˜ (곡정/ν’ˆλͺ© 곡용) +export interface ColumnDef { name: string; label: string; width?: number; align?: "left" | "center" | "right"; } +// 곡정 ν…Œμ΄λΈ” 컬럼 μ •μ˜ (κΈ°μ‘΄ ν˜Έν™˜) +export type ProcessColumnDef = ColumnDef; + +// ν’ˆλͺ© ν•„ν„° 쑰건 +export interface ItemFilterCondition { + column: string; + operator: "equals" | "contains" | "not_equals"; + value: string; +} + // 전체 Config export interface ItemRoutingConfig { dataSource: ItemRoutingDataSource; @@ -46,6 +56,14 @@ export interface ItemRoutingConfig { autoSelectFirstVersion?: boolean; versionAddButtonText?: string; processAddButtonText?: string; + itemListMode?: "all" | "registered"; + screenCode?: string; + /** 쒌츑 ν’ˆλͺ© λͺ©λ‘μ— ν‘œμ‹œν•  컬럼 */ + itemDisplayColumns?: ColumnDef[]; + /** ν’ˆλͺ© μΆ”κ°€ λͺ¨λ‹¬μ— ν‘œμ‹œν•  컬럼 */ + modalDisplayColumns?: ColumnDef[]; + /** ν’ˆλͺ© 쑰회 μ‹œ 사전 ν•„ν„° 쑰건 */ + itemFilterConditions?: ItemFilterCondition[]; } // μ»΄ν¬λ„ŒνŠΈ Props @@ -54,6 +72,7 @@ export interface ItemRoutingComponentProps { formData?: Record; isPreview?: boolean; tableName?: string; + screenId?: number | string; } // 데이터 λͺ¨λΈ