diff --git a/backend-node/package-lock.json b/backend-node/package-lock.json index f826a86a..d801ddbb 100644 --- a/backend-node/package-lock.json +++ b/backend-node/package-lock.json @@ -1043,6 +1043,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -2370,6 +2371,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -3463,6 +3465,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3699,6 +3702,7 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -3916,6 +3920,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4442,6 +4447,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -5652,6 +5658,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7414,6 +7421,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -8383,7 +8391,6 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", - "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -9272,6 +9279,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -10122,7 +10130,6 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -10931,6 +10938,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -11036,6 +11044,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/backend-node/src/controllers/entityJoinController.ts b/backend-node/src/controllers/entityJoinController.ts index fbb88750..4a541456 100644 --- a/backend-node/src/controllers/entityJoinController.ts +++ b/backend-node/src/controllers/entityJoinController.ts @@ -66,11 +66,23 @@ export class EntityJoinController { const userField = parsedAutoFilter.userField || "companyCode"; const userValue = ((req as any).user as any)[userField]; - if (userValue) { - searchConditions[filterColumn] = userValue; + // ๐Ÿ†• ํ”„๋ฆฌ๋ทฐ์šฉ ํšŒ์‚ฌ ์ฝ”๋“œ ์˜ค๋ฒ„๋ผ์ด๋“œ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ํ—ˆ์šฉ) + let finalCompanyCode = userValue; + if (parsedAutoFilter.companyCodeOverride && userValue === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ๋‹ค๋ฅธ ํšŒ์‚ฌ ์ฝ”๋“œ๋กœ ์˜ค๋ฒ„๋ผ์ด๋“œ ๊ฐ€๋Šฅ + finalCompanyCode = parsedAutoFilter.companyCodeOverride; + logger.info("๐Ÿ”“ ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ํšŒ์‚ฌ ์ฝ”๋“œ ์˜ค๋ฒ„๋ผ์ด๋“œ:", { + originalCompanyCode: userValue, + overrideCompanyCode: parsedAutoFilter.companyCodeOverride, + tableName, + }); + } + + if (finalCompanyCode) { + searchConditions[filterColumn] = finalCompanyCode; logger.info("๐Ÿ”’ Entity ์กฐ์ธ์— ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„ํ„ฐ ์ ์šฉ:", { filterColumn, - userValue, + finalCompanyCode, tableName, }); } diff --git a/backend-node/src/controllers/screenGroupController.ts b/backend-node/src/controllers/screenGroupController.ts index 518de7e8..52464ed4 100644 --- a/backend-node/src/controllers/screenGroupController.ts +++ b/backend-node/src/controllers/screenGroupController.ts @@ -1652,7 +1652,7 @@ export const getScreenSubTables = async (req: Request, res: Response) => { }); } }); - + rightPanelResult.rows.forEach((row: any) => { const screenId = row.screen_id; const mainTable = row.main_table; diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index 7c84898b..5bbec536 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -775,13 +775,25 @@ export async function getTableData( const userField = autoFilter?.userField || "companyCode"; const userValue = (req.user as any)[userField]; - if (userValue) { - enhancedSearch[filterColumn] = userValue; + // ๐Ÿ†• ํ”„๋ฆฌ๋ทฐ์šฉ ํšŒ์‚ฌ ์ฝ”๋“œ ์˜ค๋ฒ„๋ผ์ด๋“œ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ํ—ˆ์šฉ) + let finalCompanyCode = userValue; + if (autoFilter?.companyCodeOverride && userValue === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ๋‹ค๋ฅธ ํšŒ์‚ฌ ์ฝ”๋“œ๋กœ ์˜ค๋ฒ„๋ผ์ด๋“œ ๊ฐ€๋Šฅ + finalCompanyCode = autoFilter.companyCodeOverride; + logger.info("๐Ÿ”“ ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ํšŒ์‚ฌ ์ฝ”๋“œ ์˜ค๋ฒ„๋ผ์ด๋“œ:", { + originalCompanyCode: userValue, + overrideCompanyCode: autoFilter.companyCodeOverride, + tableName, + }); + } + + if (finalCompanyCode) { + enhancedSearch[filterColumn] = finalCompanyCode; logger.info("๐Ÿ” ํ˜„์žฌ ์‚ฌ์šฉ์ž ํ•„ํ„ฐ ์ ์šฉ:", { filterColumn, userField, - userValue, + userValue: finalCompanyCode, tableName, }); } else { diff --git a/docs/ํ™”๋ฉด๊ด€๊ณ„_์‹œ๊ฐํ™”_๊ฐœ์„ _๋ณด๊ณ ์„œ.md b/docs/ํ™”๋ฉด๊ด€๊ณ„_์‹œ๊ฐํ™”_๊ฐœ์„ _๋ณด๊ณ ์„œ.md index 83411d7f..27946afa 100644 --- a/docs/ํ™”๋ฉด๊ด€๊ณ„_์‹œ๊ฐํ™”_๊ฐœ์„ _๋ณด๊ณ ์„œ.md +++ b/docs/ํ™”๋ฉด๊ด€๊ณ„_์‹œ๊ฐํ™”_๊ฐœ์„ _๋ณด๊ณ ์„œ.md @@ -1682,6 +1682,61 @@ frontend/ --- +## ํ™”๋ฉด ์„ค์ • ๋ชจ๋‹ฌ ๊ฐœ์„  (2026-01-12) + +### ๊ฐœ์š” + +ํ™”๋ฉด ๋…ธ๋“œ ์šฐํด๋ฆญ ์‹œ ์—ด๋ฆฌ๋Š” ์„ค์ • ๋ชจ๋‹ฌ์„ ๋Œ€ํญ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค. + +### ์ฃผ์š” ๋ณ€๊ฒฝ ์‚ฌํ•ญ + +#### 1. ํ…Œ์ด๋ธ” ์ •๋ณด ์‹œ๊ฐํ™” ๊ฐœ์„  + +| ํ•ญ๋ชฉ | ๋ณ€๊ฒฝ ๋‚ด์šฉ | +|------|----------| +| ๋ฉ”์ธ ํ…Œ์ด๋ธ” | ์•„์ฝ”๋””์–ธ ํ˜•์‹์œผ๋กœ ๋ชจ๋“  ์ปฌ๋Ÿผ ํ‘œ์‹œ | +| ํ•„ํ„ฐ ํ…Œ์ด๋ธ” | ์•„์ฝ”๋””์–ธ ํ˜•์‹ + ํ•„ํ„ฐ/์กฐ์ธ ํ‚ค ์ƒ‰์ƒ ๊ตฌ๋ถ„ | +| ์‚ฌ์šฉ ์ค‘ ์ปฌ๋Ÿผ | ํŒŒ๋ž€์ƒ‰ ๋ฐฐ๊ฒฝ + "ํ•„๋“œ" ๋ฐฐ์ง€๋กœ ๊ฐ•์กฐ | + +#### 2. ํ™”๋ฉด ํ”„๋ฆฌ๋ทฐ ์ƒ์‹œ ํ‘œ์‹œ + +- ๋ชจ๋‹ฌ ๋ ˆ์ด์•„์›ƒ: ์ขŒ์ธก 40% (ํƒญ) / ์šฐ์ธก 60% (ํ”„๋ฆฌ๋ทฐ) +- ํƒญ ์ „ํ™˜ํ•ด๋„ ํ”„๋ฆฌ๋ทฐ ํ•ญ์ƒ ํ‘œ์‹œ + +#### 3. ์คŒ/๋“œ๋ž˜๊ทธ ๊ธฐ๋Šฅ (react-zoom-pan-pinch ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ) + +```bash +npm install react-zoom-pan-pinch +``` + +| ๊ธฐ๋Šฅ | ๋™์ž‘ | +|------|------| +| ํœ  ์Šคํฌ๋กค | ๋งˆ์šฐ์Šค ํฌ์ธํ„ฐ ๊ธฐ์ค€ ํ™•๋Œ€/์ถ•์†Œ (20%~300%) | +| ๋“œ๋ž˜๊ทธ | ํ™”๋ฉด ์ด๋™ | +| ํด๋ฆญ | iframe ๋‚ด๋ถ€ ๋ฒ„ํŠผ/๋ชฉ๋ก ์ƒํ˜ธ์ž‘์šฉ | + +#### 4. ํ”„๋ฆฌ๋ทฐ company_code ์ „๋‹ฌ ๋ฌธ์ œ ํ•ด๊ฒฐ + +| ๋ฌธ์ œ | ํ•ด๊ฒฐ | +|------|------| +| ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋กœ ๋‹ค๋ฅธ ํšŒ์‚ฌ ํ”„๋ฆฌ๋ทฐ ๋ถˆ๊ฐ€ | `companyCodeOverride` ํŒŒ๋ผ๋ฏธํ„ฐ ๋„์ž… | +| URL ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฌด์‹œ๋จ | ๋ฐฑ์—”๋“œ์—์„œ admin ์ „์šฉ ์˜ค๋ฒ„๋ผ์ด๋“œ ์ฒ˜๋ฆฌ | + +### ๊ด€๋ จ ํŒŒ์ผ + +| ํŒŒ์ผ | ๋ณ€๊ฒฝ ๋‚ด์šฉ | +|------|----------| +| `ScreenSettingModal.tsx` | ์ „์ฒด UI ๊ฐœ์„ , ์คŒ/๋“œ๋ž˜๊ทธ ๊ธฐ๋Šฅ | +| `entityJoin.ts` | `companyCodeOverride` ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ | +| `SplitPanelLayoutComponent.tsx` | `companyCode` prop ์ถ”๊ฐ€ | +| `entityJoinController.ts` | `companyCodeOverride` ์ฒ˜๋ฆฌ ๋กœ์ง | + +### ์ƒ์„ธ ๋ฌธ์„œ + +- [ํ™”๋ฉด์„ค์ •๋ชจ๋‹ฌ_๊ฐœ์„ _์™„๋ฃŒ_๋ณด๊ณ ์„œ.md](./ํ™”๋ฉด์„ค์ •๋ชจ๋‹ฌ_๊ฐœ์„ _์™„๋ฃŒ_๋ณด๊ณ ์„œ.md) + +--- + ## ๊ด€๋ จ ๋ฌธ์„œ - [๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ](.cursor/rules/multi-tenancy-guide.mdc) diff --git a/docs/ํ™”๋ฉด์„ค์ •๋ชจ๋‹ฌ_๊ฐœ์„ _์™„๋ฃŒ_๋ณด๊ณ ์„œ.md b/docs/ํ™”๋ฉด์„ค์ •๋ชจ๋‹ฌ_๊ฐœ์„ _์™„๋ฃŒ_๋ณด๊ณ ์„œ.md new file mode 100644 index 00000000..493c3f37 --- /dev/null +++ b/docs/ํ™”๋ฉด์„ค์ •๋ชจ๋‹ฌ_๊ฐœ์„ _์™„๋ฃŒ_๋ณด๊ณ ์„œ.md @@ -0,0 +1,535 @@ +# ํ™”๋ฉด ์„ค์ • ๋ชจ๋‹ฌ ๊ฐœ์„  ์™„๋ฃŒ ๋ณด๊ณ ์„œ + +## ๊ฐœ์š” +ํ™”๋ฉด ๊ด€๋ฆฌ์—์„œ ํ™”๋ฉด ๋…ธ๋“œ ์šฐํด๋ฆญ ์‹œ ์—ด๋ฆฌ๋Š” ์„ค์ • ๋ชจ๋‹ฌ์„ ๋Œ€ํญ ๊ฐœ์„ ํ•˜์—ฌ, ํ…Œ์ด๋ธ” ์ •๋ณด ์‹œ๊ฐํ™”, ํ•„๋“œ ๋งคํ•‘ ํ™•์ธ, ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ/์ถ”๊ฐ€/์ œ๊ฑฐ ๊ธฐ๋Šฅ, ์กฐ์ธ ์„ค์ • ๊ธฐ๋Šฅ, ์‹ค์‹œ๊ฐ„ ํ”„๋ฆฌ๋ทฐ ๊ธฐ๋Šฅ์„ ๊ฐ•ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค. + +## ์ฃผ์š” ๊ฐœ์„  ์‚ฌํ•ญ + +### 1. ํ™”๋ฉด ๊ฐœ์š” ํƒญ ํ†ตํ•ฉ ๊ฐœ์„  + +#### 1.1 ํ•„๋“œ ๋งคํ•‘ ํƒญ โ†’ ๊ฐœ์š” ํƒญ ํ†ตํ•ฉ +- ๊ธฐ์กด "ํ•„๋“œ ๋งคํ•‘" ํƒญ ์ œ๊ฑฐ +- ํ•„๋“œ ๋งคํ•‘ ์ •๋ณด๋ฅผ ๊ฐœ์š” ํƒญ์˜ ๋ฉ”์ธ/ํ•„ํ„ฐ ํ…Œ์ด๋ธ” ์•„์ฝ”๋””์–ธ์— ํ†ตํ•ฉ ํ‘œ์‹œ +- ๋” ์ง๊ด€์ ์ด๊ณ  ๊ฐ„๊ฒฐํ•œ UI ์ œ๊ณต + +#### 1.2 ๋ฉ”์ธ ํ…Œ์ด๋ธ” ์•„์ฝ”๋””์–ธ +- ๋ฉ”์ธ ํ…Œ์ด๋ธ”(์˜ˆ: `customer_mng`)์„ ์•„์ฝ”๋””์–ธ ํ˜•์‹์œผ๋กœ ํ‘œ์‹œ +- ํด๋ฆญ ์‹œ ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ ์ •๋ณด ํ‘œ์‹œ +- **1์—ด ๋ ˆ์ด์•„์›ƒ**: ์ปฌ๋Ÿผ ์ •๋ณด๋ฅผ ์„ธ๋กœ๋กœ ๋ฐฐ์น˜ +- ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ ์ค‘์ธ ์ปฌ๋Ÿผ์€ **ํŒŒ๋ž€์ƒ‰ ๋ฐฐ๊ฒฝ + "ํ•„๋“œ" ๋ฐฐ์ง€**๋กœ ๊ฐ•์กฐ +- **์ปฌ๋Ÿผ ์ •๋ ฌ**: + - ์‚ฌ์šฉ์ค‘์ธ ํ•„๋“œ๊ฐ€ ์ƒ๋‹จ์— ํ‘œ์‹œ + - ํ™”๋ฉด์— ํ‘œ์‹œ๋˜๋Š” ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ (y์ขŒํ‘œ ๊ธฐ์ค€) + - ๋ฏธ์‚ฌ์šฉ ์ปฌ๋Ÿผ์€ ํ•˜๋‹จ์— ํ‘œ์‹œ + +#### 1.3 ํ•„ํ„ฐ ํ…Œ์ด๋ธ” ์•„์ฝ”๋””์–ธ +- ํ•„ํ„ฐ ํ…Œ์ด๋ธ”(์˜ˆ: `customer_item_mapping`)์„ ์•„์ฝ”๋””์–ธ ํ˜•์‹์œผ๋กœ ํ‘œ์‹œ +- ํด๋ฆญ ์‹œ ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ ์ •๋ณด ํ‘œ์‹œ +- ์ปฌ๋Ÿผ๋ณ„ ์ƒ‰์ƒ ๊ตฌ๋ถ„: + - **ํŒŒ๋ž€์ƒ‰**: ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ ์ค‘์ธ ์ปฌ๋Ÿผ (ํ•„๋“œ) + - **๋ณด๋ผ์ƒ‰**: ํ•„ํ„ฐ ํ‚ค ์ปฌ๋Ÿผ (WHERE ์ ˆ์— ์‚ฌ์šฉ) + - **์ฃผํ™ฉ์ƒ‰**: ์กฐ์ธ ํ‚ค ์ปฌ๋Ÿผ (JOIN ์กฐ๊ฑด์— ์‚ฌ์šฉ) +- **๋‹ค์ค‘ ๋ฐฐ์ง€ ํ‘œ์‹œ**: ์ปฌ๋Ÿผ์ด ํ•„๋“œ์ด๋ฉด์„œ ์กฐ์ธ/ํ•„ํ„ฐ ํ‚ค์ธ ๊ฒฝ์šฐ ๋ฐฐ์ง€ ๋™์‹œ ํ‘œ์‹œ +- ํ•„ํ„ฐ ์—ฐ๊ฒฐ ์ •๋ณด ํ‘œ์‹œ (์˜ˆ: `โ†’ customer_mng`) + +#### 1.4 ์ปฌ๋Ÿผ ๋ ˆ์ด์•„์›ƒ ์ˆœ์„œ +- **์ˆœ์„œ**: `์ปฌ๋Ÿผ๋ช… | ๋ฐฐ์ง€ | ๋ฐ์ดํ„ฐํƒ€์ž…` +- ์˜ˆ: `๊ฑฐ๋ž˜์ฒ˜ ์ฝ”๋“œ` `[ํ•„๋“œ]` `character varying` +- ๋ฐ์ดํ„ฐํƒ€์ž…์€ ์˜ค๋ฅธ์ชฝ ์ •๋ ฌ + +#### 1.5 ํด๋ฆญ ์Šคํƒ€์ผ ๊ฐœ์„  +- **ํ…Œ๋‘๋ฆฌ ์ œ๊ฑฐ**: ring-2, ring-offset ๋“ฑ ์ œ๊ฑฐ +- **๊ฐ•์กฐ ์ƒ‰์ƒ ์—ฐํ•˜๊ฒŒ**: + - ์„ ํƒ๋จ: `bg-blue-100 border-blue-300` + - ๋ฏธ์„ ํƒ: `bg-blue-50 border-blue-200` +- ๋” ๋ถ€๋“œ๋Ÿฌ์šด ์‹œ๊ฐ์  ํ”ผ๋“œ๋ฐฑ + +#### 1.6 ํŒจ๋„ ๋†’์ด ๋™๊ธฐํ™” +- ์™ผ์ชฝ(์ปฌ๋Ÿผ ๋ชฉ๋ก)๊ณผ ์˜ค๋ฅธ์ชฝ(์„ค์ • ํŒจ๋„) ๋™์ผํ•œ `max-h-[350px]` ์ ์šฉ +- `overflow-y-auto`๋กœ ์Šคํฌ๋กค ์ฒ˜๋ฆฌ +- `items-stretch`๋กœ ์–‘์ชฝ ํŒจ๋„ ๋†’์ด ๋™๊ธฐํ™” + +### 2. ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ ๊ธฐ๋Šฅ + +#### 2.1 ์ธ๋ผ์ธ ์ปฌ๋Ÿผ ํŽธ์ง‘ +- ์‚ฌ์šฉ์ค‘์ธ ํ•„๋“œ(ํŒŒ๋ž€์ƒ‰ ๋ฐฐ๊ฒฝ)๋ฅผ ํด๋ฆญํ•˜๋ฉด ์šฐ์ธก์— "์ปฌ๋Ÿผ ์„ค์ •" ํŒจ๋„ ํ‘œ์‹œ +- ํŒจ๋„ ์ •๋ณด: + - **ํ™”๋ฉด ํ•„๋“œ**: ์ปฌ๋Ÿผ ํ•œ๊ธ€๋ช… ํ‘œ์‹œ (์˜ˆ: "๊ฑฐ๋ž˜์ฒ˜ ์ฝ”๋“œ") + - **ํ˜„์žฌ ์ปฌ๋Ÿผ**: ์˜๋ฌธ ์ปฌ๋Ÿผ๋ช… ํ‘œ์‹œ (์˜ˆ: `customer_code`) + - **์ปฌ๋Ÿผ ๋ณ€๊ฒฝ**: ๋“œ๋กญ๋‹ค์šด์œผ๋กœ ๋‹ค๋ฅธ ์ปฌ๋Ÿผ ์„ ํƒ +- ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์œผ๋กœ ์ปฌ๋Ÿผ ๋น ๋ฅด๊ฒŒ ์ฐพ๊ธฐ + +#### 2.2 ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜ +- ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ ํ›„ **ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ ์—†์ด** ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜ +- `onRefresh` ์ฝœ๋ฐฑ์œผ๋กœ ๋ฐ์ดํ„ฐ ๋ฆฌ๋กœ๋“œ + iframe ์ƒˆ๋กœ๊ณ ์นจ +- ๋” ๋น ๋ฅธ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ + +#### 2.3 ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ €์žฅ +- `screenApi.saveLayout()` ์‚ฌ์šฉํ•˜์—ฌ **ํ™”๋ฉด ๋””์ž์ด๋„ˆ์™€ ๋™์ผํ•œ ํ…Œ์ด๋ธ”์— ์ €์žฅ** +- ์ €์žฅ ์œ„์น˜: + - `componentConfig.leftPanel.columns` (๋ถ„ํ•  ํŒจ๋„) + - `componentConfig.rightPanel.columns` (๋ถ„ํ•  ํŒจ๋„) + - `usedColumns` ๋ฐฐ์—ด + - `bindField` ํ•„๋“œ + - `fieldMapping` ๋ฐฐ์—ด + +### 3. ํ•„๋“œ ์ถ”๊ฐ€/์ œ๊ฑฐ ๊ธฐ๋Šฅ (์‹ ๊ทœ) + +#### 3.1 ํ•„๋“œ ์ถ”๊ฐ€ +- ๋น„ํ•„๋“œ ์ปฌ๋Ÿผ(ํšŒ์ƒ‰/ํฐ์ƒ‰ ๋ฐฐ๊ฒฝ) ํด๋ฆญ +- "์ปฌ๋Ÿผ ์„ค์ •" ํŒจ๋„์— ์ปฌ๋Ÿผ ์ •๋ณด ํ‘œ์‹œ +- **"ํ•„๋“œ๋กœ ์ถ”๊ฐ€"** ๋ฒ„ํŠผ ํด๋ฆญ โ†’ ํ•ด๋‹น ์ปฌ๋Ÿผ์ด ํ™”๋ฉด ํ•„๋“œ๋กœ ์ถ”๊ฐ€๋จ +- ๋ฒ„ํŠผ ์Šคํƒ€์ผ: `text-blue-600 border-blue-300 hover:bg-blue-50` (ํŒŒ๋ž€์ƒ‰ ํ…Œ๋‘๋ฆฌ) + +#### 3.2 ํ•„๋“œ ์ œ๊ฑฐ +- ๊ธฐ์กด ํ•„๋“œ(ํŒŒ๋ž€์ƒ‰ ๋ฐฐ๊ฒฝ) ํด๋ฆญ +- "์ปฌ๋Ÿผ ์„ค์ •" ํŒจ๋„์— ํ•„๋“œ ์ •๋ณด ํ‘œ์‹œ +- **"ํ•„๋“œ์—์„œ ์ œ๊ฑฐ"** ๋ฒ„ํŠผ ํด๋ฆญ โ†’ ํ•ด๋‹น ํ•„๋“œ๊ฐ€ ํ™”๋ฉด์—์„œ ์ œ๊ฑฐ๋จ +- ๋ฒ„ํŠผ ์Šคํƒ€์ผ: `text-red-600 border-red-300 hover:bg-red-50` (๋นจ๊ฐ„์ƒ‰ ํ…Œ๋‘๋ฆฌ) + +#### 3.3 ์ €์žฅ ๋กœ์ง +```typescript +// ํ•„๋“œ ์ถ”๊ฐ€: ๋ฐฐ์—ด์— ์ƒˆ ์ปฌ๋Ÿผ ์ถ”๊ฐ€ +if (isAddingField) { + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + leftPanel: { + ...comp.componentConfig.leftPanel, + columns: [...leftColumns, { name: newColumn, columnName: newColumn }], + }, + }, + }; +} + +// ํ•„๋“œ ์ œ๊ฑฐ: ๋ฐฐ์—ด์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ ์ œ๊ฑฐ +if (isRemovingField) { + const filteredColumns = leftColumns.filter((_, i) => i !== columnIdx); + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + leftPanel: { + ...comp.componentConfig.leftPanel, + columns: filteredColumns, + }, + }, + }; +} +``` + +#### 3.4 ์ ์šฉ ๋ฒ”์œ„ +- ๋ฉ”์ธ ํ…Œ์ด๋ธ” ์•„์ฝ”๋””์–ธ: ํ•„๋“œ ์ถ”๊ฐ€/์ œ๊ฑฐ ๊ฐ€๋Šฅ +- ํ•„ํ„ฐ ํ…Œ์ด๋ธ” ์•„์ฝ”๋””์–ธ: ํ•„๋“œ ์ถ”๊ฐ€/์ œ๊ฑฐ ๊ฐ€๋Šฅ +- `usedColumns`, `componentConfig.usedColumns`, `componentConfig.columns`, `leftPanel.columns`, `rightPanel.columns` ๋ชจ๋‘ ์ง€์› + +### 4. ํ™”๋ฉด ํ”„๋ฆฌ๋ทฐ ์ƒ์‹œ ํ‘œ์‹œ + +#### 4.1 ๋ ˆ์ด์•„์›ƒ ๋ณ€๊ฒฝ +- ๊ธฐ์กด: ํƒญ์œผ๋กœ ํ”„๋ฆฌ๋ทฐ ์ „ํ™˜ +- ๊ฐœ์„ : **๋ชจ๋‹ฌ ์šฐ์ธก์— ํ”„๋ฆฌ๋ทฐ ์ƒ์‹œ ํ‘œ์‹œ** +- ๋ชจ๋‹ฌ ํฌ๊ธฐ ํ™•๋Œ€ (1600px ์ตœ๋Œ€ ๋„ˆ๋น„) +- ์ขŒ์ธก 40% (ํƒญ ์ฝ˜ํ…์ธ ) / ์šฐ์ธก 60% (ํ”„๋ฆฌ๋ทฐ) + +#### 4.2 ์คŒ/๋“œ๋ž˜๊ทธ/ํด๋ฆญ ๊ธฐ๋Šฅ (react-zoom-pan-pinch ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ) +- **ํœ  ์Šคํฌ๋กค**: ๋งˆ์šฐ์Šค ํฌ์ธํ„ฐ ์œ„์น˜ ๊ธฐ์ค€ ํ™•๋Œ€/์ถ•์†Œ (20% ~ 300%) +- **๋“œ๋ž˜๊ทธ**: ๋งˆ์šฐ์Šค ์™ผ์ชฝ ๋ฒ„ํŠผ์œผ๋กœ ํ™”๋ฉด ์ด๋™ (5px ์ด์ƒ ์ด๋™ ์‹œ) +- **ํด๋ฆญ**: iframe ๋‚ด๋ถ€ ์š”์†Œ ์ •์ƒ ํด๋ฆญ ๊ฐ€๋Šฅ + - ๋ฒ„ํŠผ, ์…€๋ ‰ํŠธ๋ฐ•์Šค, ์ฒดํฌ๋ฐ•์Šค, ํ…Œ์ด๋ธ” ํ–‰ ํด๋ฆญ + - ์ธํ’‹๋ฐ•์Šค/ํ…์ŠคํŠธ๋ฐ•์Šค ํฌ์ปค์Šค ๋ฐ ์ž…๋ ฅ + - X๋ฒ„ํŠผ(๋‹ซ๊ธฐ) ๋“ฑ SVG ์•„์ด์ฝ˜ ๋ฒ„ํŠผ ํด๋ฆญ + +#### 4.3 ํด๋ฆญ ์ขŒํ‘œ ๋ณด์ • ์‹œ์Šคํ…œ +- ์คŒ ์ƒํƒœ์—์„œ๋„ ์ •ํ™•ํ•œ ํด๋ฆญ ์œ„์น˜ ๊ณ„์‚ฐ +- `designWidth / rect.width` ๋น„์œจ๋กœ ์ขŒํ‘œ ๋ณ€ํ™˜ +- ์˜ค๋ฒ„๋ ˆ์ด ๋ฐฉ์‹์œผ๋กœ ๋“œ๋ž˜๊ทธ์™€ ํด๋ฆญ ๋ถ„๋ฆฌ ์ฒ˜๋ฆฌ + +### 5. ํ•„๋“œ+์กฐ์ธ ์ปฌ๋Ÿผ ์Šคํƒ€์ผ ๊ฐœ์„  + +#### 5.1 ๋‹ค์ค‘ ์—ญํ•  ์ปฌ๋Ÿผ ํ‘œ์‹œ +- ์ปฌ๋Ÿผ์ด **ํ•„๋“œ์ด๋ฉด์„œ ์กฐ์ธ ํ‚ค**์ธ ๊ฒฝ์šฐ: + - **ํŒŒ๋ž€์ƒ‰ ๋ฐฐ๊ฒฝ** (ํ•„๋“œ ๊ธฐ์ค€) + - **์™ผ์ชฝ์— ์ฃผํ™ฉ์ƒ‰ ์„ธ๋กœ ์„ ** (`border-l-4 border-l-orange-500`) + - ๋ฐฐ์ง€: `์กฐ์ธ` `ํ•„๋“œ` ๋™์‹œ ํ‘œ์‹œ +- ์ปฌ๋Ÿผ์ด **ํ•„๋“œ์ด๋ฉด์„œ ํ•„ํ„ฐ ํ‚ค**์ธ ๊ฒฝ์šฐ: + - **ํŒŒ๋ž€์ƒ‰ ๋ฐฐ๊ฒฝ** (ํ•„๋“œ ๊ธฐ์ค€) + - **์™ผ์ชฝ์— ๋ณด๋ผ์ƒ‰ ์„ธ๋กœ ์„ ** (`border-l-4 border-l-purple-400`) + - ๋ฐฐ์ง€: `ํ•„ํ„ฐ` `ํ•„๋“œ` ๋™์‹œ ํ‘œ์‹œ + +#### 5.2 ์กฐ์ธ ์ปฌ๋Ÿผ๋„ ํ•„๋“œ๋กœ ์ธ์‹ +- `filterTableColumnMappings` ์ƒ์„ฑ ์‹œ ์กฐ์ธ ์ปฌ๋Ÿผ(`ft.joinColumnRefs`)๋„ ํฌํ•จ +- ์กฐ์ธ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ๋ฅผ ํ™”๋ฉด์—์„œ ๋ณด์—ฌ์ฃผ๋ฏ€๋กœ ํ•„๋“œ๋กœ ๊ฐ„์ฃผ + +#### 5.3 ์ปฌ๋Ÿผ ์„ค์ • ํŒจ๋„ - ์กฐ์ธ ์ •๋ณด ํ‘œ์‹œ +- ์กฐ์ธ ํ‚ค ํด๋ฆญ ์‹œ ํŒจ๋„์— ์กฐ์ธ ์ •๋ณด ํ‘œ์‹œ: + - **๋Œ€์ƒ ํ…Œ์ด๋ธ”**: item_info (์‹ค์ œ ์ฐธ์กฐ ํ…Œ์ด๋ธ”) + - **์—ฐ๊ฒฐ ์ปฌ๋Ÿผ**: item_number (์ฐธ์กฐ ์ปฌ๋Ÿผ) + +### 6. ์กฐ์ธ ๊ด€๊ณ„ ์„ค์ •/์ˆ˜์ • ๊ธฐ๋Šฅ + +#### 6.1 ๊ธฐ๋Šฅ ์„ค๋ช… +- ์ปฌ๋Ÿผ ์„ค์ • ํŒจ๋„์—์„œ **์กฐ์ธ ๊ด€๊ณ„ ์ง์ ‘ ์ˆ˜์ •** ๊ฐ€๋Šฅ +- **๋ชจ๋“  ์ปฌ๋Ÿผ์—์„œ ์กฐ์ธ ์„ค์ • ๊ฐ€๋Šฅ** (๊ธฐ์กด ์กฐ์ธ ํ‚ค๊ฐ€ ์•„๋‹Œ ์ปฌ๋Ÿผ๋„ ํฌํ•จ) +- ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ(`column_labels` ํ…Œ์ด๋ธ”)์™€ ๋™์ผํ•œ ์ €์žฅ ์œ„์น˜ ์‚ฌ์šฉ + +#### 6.2 ์ €์žฅ ํ…Œ์ด๋ธ” +``` +column_labels ํ…Œ์ด๋ธ”: +โ”œโ”€โ”€ reference_table (์ฐธ์กฐ ํ…Œ์ด๋ธ”๋ช…) +โ”œโ”€โ”€ reference_column (์ฐธ์กฐ ์ปฌ๋Ÿผ - ๋ณดํ†ต PK) +โ””โ”€โ”€ display_column (ํ™”๋ฉด์— ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ) +``` + +#### 6.3 ๊ตฌํ˜„๋œ UI +1. **์ปฌ๋Ÿผ ํด๋ฆญ** โ†’ ์ปฌ๋Ÿผ ์„ค์ • ํŒจ๋„ ํ‘œ์‹œ +2. **"์กฐ์ธ" ์„น์…˜ ํ™•์ธ**: + - ์กฐ์ธ ์„ค์ • ์žˆ์Œ: "ํŽธ์ง‘" ๋ฒ„ํŠผ + - ์กฐ์ธ ์„ค์ • ์—†์Œ: "์ถ”๊ฐ€" ๋ฒ„ํŠผ +3. **๋“œ๋กญ๋‹ค์šด์œผ๋กœ ์„ค์ •** (๋ชจ๋‘ ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅ): + - ๋Œ€์ƒ ํ…Œ์ด๋ธ”: ์ „์ฒด ํ…Œ์ด๋ธ” ๋ชฉ๋ก์—์„œ ๊ฒ€์ƒ‰/์„ ํƒ + - ์—ฐ๊ฒฐ ์ปฌ๋Ÿผ (PK): ์„ ํƒํ•œ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ์ค‘ ๊ฒ€์ƒ‰/์„ ํƒ + - ํ‘œ์‹œ ์ปฌ๋Ÿผ: ํ™”๋ฉด์— ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ๊ฒ€์ƒ‰/์„ ํƒ +4. **์ €์žฅ ๋ฒ„ํŠผ** โ†’ `column_labels` ํ…Œ์ด๋ธ”์— ์ €์žฅ +5. **์ทจ์†Œ ๋ฒ„ํŠผ** โ†’ ํŽธ์ง‘ ์ทจ์†Œ + +#### 6.4 ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ๋“œ๋กญ๋‹ค์šด +- Popover + Command ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ +- ์‹ค์‹œ๊ฐ„ ํ…์ŠคํŠธ ๊ฒ€์ƒ‰ ์ง€์› +- ๋Œ€์ƒ ํ…Œ์ด๋ธ”, ์—ฐ๊ฒฐ ์ปฌ๋Ÿผ, ํ‘œ์‹œ ์ปฌ๋Ÿผ ๋ชจ๋‘ ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅ + +#### 6.5 API ์—ฐ๋™ +- **ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์กฐํšŒ**: `tableManagementApi.getTableList()` +- **์ปฌ๋Ÿผ ๋ชฉ๋ก ์กฐํšŒ**: `tableManagementApi.getColumnList(tableName)` +- **์ €์žฅ**: `tableManagementApi.updateColumnSettings(tableName, columnName, settings)` + +#### 6.6 ๋ฉ”์ธ ํ…Œ์ด๋ธ”์—๋„ ์กฐ์ธ ์„ค์ • ์ ์šฉ +- ๋ฉ”์ธ ํ…Œ์ด๋ธ” ์•„์ฝ”๋””์–ธ์—์„œ๋„ ์กฐ์ธ ์„ค์ • ๊ฐ€๋Šฅ +- ํ•„ํ„ฐ ํ…Œ์ด๋ธ”๊ณผ ๋™์ผํ•œ UI/๊ธฐ๋Šฅ ์ œ๊ณต + +#### 6.7 ์กฐ์ธ ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ˆ˜์ • +- ๊ธฐ์กด ์กฐ์ธ ํ‚ค ํด๋ฆญ ์‹œ `joinRef.refTable` ๊ฐ’์„ ์‚ฌ์šฉ +- ์˜ˆ: `ํ’ˆ๋ชฉ ID` โ†’ `item_info` (์‹ค์ œ ์ฐธ์กฐ ํ…Œ์ด๋ธ”) +- `mainTable` ๋Œ€์‹  `joinRef.refTable` ์‚ฌ์šฉ์œผ๋กœ ์ •ํ™•ํ•œ ํ…Œ์ด๋ธ” ํ‘œ์‹œ + +### 7. ๋ฐฐ์ง€ ์ˆœ์„œ ๋ฐ ์Šคํƒ€์ผ +- **๋ฐฐ์ง€ ์ˆœ์„œ**: `ํ•„ํ„ฐ` โ†’ `์กฐ์ธ` โ†’ `ํ•„๋“œ` (ํ•„๋“œ๊ฐ€ ๋งจ ๋’ค) +- **์กฐ์ธ ๋ฐฐ์ง€**: ์ฃผํ™ฉ์ƒ‰ ๋ฐฐ๊ฒฝ (`bg-orange-200 text-orange-700`) +- **ํ•„ํ„ฐ ๋ฐฐ์ง€**: ๋ณด๋ผ์ƒ‰ ๋ฐฐ๊ฒฝ (`bg-purple-200 text-purple-700`) +- **ํ•„๋“œ ๋ฐฐ์ง€**: ํŒŒ๋ž€์ƒ‰ ๋ฐฐ๊ฒฝ (`bg-blue-500 text-white`) + +### 8. ์ปดํฌ๋„ŒํŠธ ํ†ตํ•ฉ ๋ฆฌํŒฉํ† ๋ง + +#### 8.1 `TableColumnAccordion` ํ†ตํ•ฉ ์ปดํฌ๋„ŒํŠธ +- ๊ธฐ์กด `MainTableAccordion`๊ณผ `FilterTableAccordion`์„ ํ•˜๋‚˜์˜ ์ปดํฌ๋„ŒํŠธ๋กœ ํ†ตํ•ฉ +- `tableType` prop์œผ๋กœ "main" ๋˜๋Š” "filter" ๊ตฌ๋ถ„ +- ์ฝ”๋“œ ์ค‘๋ณต ์ œ๊ฑฐ ๋ฐ ์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ + +#### 8.2 Props ๊ตฌ์กฐ +```typescript +interface TableColumnAccordionProps { + tableName: string; + tableLabel?: string; + tableType: "main" | "filter"; + columnMappings?: ColumnMapping[]; + onColumnChange?: (fieldLabel: string, oldColumn: string, newColumn: string) => void; + onColumnReorder?: (newOrder: string[]) => void; + onJoinSettingSaved?: () => void; + // ํ•„ํ„ฐ ํ…Œ์ด๋ธ” ์ „์šฉ props + mainTable?: string; + filterKeyMapping?: FilterKeyMapping; + joinColumnRefs?: JoinColumnRef[]; +} +``` + +#### 8.3 ๋™์  ํ…Œ๋งˆ ์ ์šฉ +- ๋ฉ”์ธ ํ…Œ์ด๋ธ”: ํŒŒ๋ž€์ƒ‰ ํ…Œ๋งˆ (`blue`) +- ํ•„ํ„ฐ ํ…Œ์ด๋ธ”: ๋ณด๋ผ์ƒ‰ ํ…Œ๋งˆ (`purple`) +- `themeColor`, `themeIcon`, `themeBadge` ๋ณ€์ˆ˜๋กœ ๋™์  ์Šคํƒ€์ผ ์ ์šฉ + +### 9. ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ + +#### 9.1 ๊ธฐ๋Šฅ ์„ค๋ช… +- ์‚ฌ์šฉ ์ค‘์ธ ์ปฌ๋Ÿผ(ํ•„๋“œ)์„ ๋“œ๋ž˜๊ทธํ•˜์—ฌ ์ˆœ์„œ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ +- ๋“œ๋ž˜๊ทธ ์ค‘์—๋Š” ์‹œ๊ฐ์ ์œผ๋กœ๋งŒ ์ˆœ์„œ ๋ณ€๊ฒฝ, **๋“œ๋กญ ์‹œ์—๋งŒ ์ €์žฅ** +- ๋“œ๋ž˜๊ทธ ์ทจ์†Œ(์˜์—ญ ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ„ ๊ฒฝ์šฐ) ์‹œ ์›๋ž˜ ์ˆœ์„œ๋กœ ๋ณต์› + +#### 9.2 ๋“œ๋ž˜๊ทธ ์ƒํƒœ ๊ด€๋ฆฌ +```typescript +// ๋“œ๋ž˜๊ทธ ์ƒํƒœ +const [draggedIndex, setDraggedIndex] = useState(null); +const [localColumnOrder, setLocalColumnOrder] = useState(null); +``` + +#### 9.3 ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค๋Ÿฌ +```typescript +// ๋“œ๋ž˜๊ทธ ์‹œ์ž‘: ํ˜„์žฌ ์ˆœ์„œ๋ฅผ ๋กœ์ปฌ ์ƒํƒœ๋กœ ์ €์žฅ +const handleDragStart = (e: React.DragEvent, index: number) => { + setDraggedIndex(index); + const usedColumns = sortedColumns.filter(col => columnMappingMap.has(col.columnName.toLowerCase())); + setLocalColumnOrder(usedColumns.map(col => col.columnName)); +}; + +// ๋“œ๋ž˜๊ทธ ์ค‘: ๋กœ์ปฌ ์ˆœ์„œ๋งŒ ๋ณ€๊ฒฝ (์ €์žฅํ•˜์ง€ ์•Š์Œ) +const handleDragOver = (e: React.DragEvent, hoverIndex: number) => { + if (draggedIndex === null || draggedIndex === hoverIndex || !localColumnOrder) return; + const newOrder = [...localColumnOrder]; + const draggedItem = newOrder[draggedIndex]; + newOrder.splice(draggedIndex, 1); + newOrder.splice(hoverIndex, 0, draggedItem); + setDraggedIndex(hoverIndex); + setLocalColumnOrder(newOrder); +}; + +// ๋“œ๋กญ: ์ตœ์ข… ์ˆœ์„œ๋กœ ์ €์žฅ +const handleDrop = (e: React.DragEvent) => { + if (localColumnOrder && onColumnReorder) { + onColumnReorder(localColumnOrder); + } + setDraggedIndex(null); + setLocalColumnOrder(null); +}; + +// ๋“œ๋ž˜๊ทธ ์ทจ์†Œ +const handleDragEnd = () => { + setDraggedIndex(null); + setLocalColumnOrder(null); +}; +``` + +#### 9.4 ์‹œ๊ฐ์  ํ”ผ๋“œ๋ฐฑ +- ๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅํ•œ ์ปฌ๋Ÿผ: `cursor-grab active:cursor-grabbing` +- ๋“œ๋ž˜๊ทธ ์ค‘์ธ ์ปฌ๋Ÿผ: `opacity-50 scale-95` +- ๋“œ๋ž˜๊ทธ ์ค‘ ์‹ค์‹œ๊ฐ„ ์ˆœ์„œ ๋ณ€๊ฒฝ ํ‘œ์‹œ + +#### 9.5 ์ €์žฅ ๋กœ์ง (`handleColumnReorder`) +```typescript +const handleColumnReorder = async (tableType: "main" | "filter", newOrder: string[]) => { + const currentLayout = await screenApi.getLayout(screenId); + + const updatedComponents = currentLayout.components.map((comp: any) => { + // leftPanel.columns ์ˆœ์„œ ๋ณ€๊ฒฝ + if (comp.componentConfig?.leftPanel?.columns) { + const leftColumns = comp.componentConfig.leftPanel.columns; + const reorderedColumns = newOrder.map(colName => + leftColumns.find((col: any) => col.name?.toLowerCase() === colName.toLowerCase()) + ).filter(Boolean); + const remainingColumns = leftColumns.filter((col: any) => + !newOrder.some(n => n.toLowerCase() === col.name?.toLowerCase()) + ); + + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + leftPanel: { + ...comp.componentConfig.leftPanel, + columns: [...reorderedColumns, ...remainingColumns], + }, + }, + }; + } + return comp; + }); + + await screenApi.saveLayout(screenId, { ...currentLayout, components: updatedComponents }); + onRefresh?.(); +}; +``` + +#### 9.6 ์ง€์› ๋ฒ”์œ„ +- ๋ฉ”์ธ ํ…Œ์ด๋ธ”: `onColumnReorder={(newOrder) => handleColumnReorder("main", newOrder)}` +- ํ•„ํ„ฐ ํ…Œ์ด๋ธ”: `onColumnReorder={(newOrder) => handleColumnReorder("filter", newOrder)}` +- ์ง€์› ๋ฐฐ์—ด: + - `componentConfig.leftPanel.columns` + - `componentConfig.rightPanel.columns` + - `componentConfig.usedColumns` + - `componentConfig.columns` + +## ๊ธฐ์ˆ  ์Šคํƒ + +### ์‹ ๊ทœ ์˜์กด์„ฑ +```bash +npm install react-zoom-pan-pinch +``` + +### ์‚ฌ์šฉ๋œ ์ปดํฌ๋„ŒํŠธ +- `TransformWrapper`, `TransformComponent` - ์คŒ/๋“œ๋ž˜๊ทธ ๊ธฐ๋Šฅ +- `Accordion`, `AccordionContent`, `AccordionItem`, `AccordionTrigger` - ์•„์ฝ”๋””์–ธ UI +- `Popover`, `PopoverTrigger`, `PopoverContent` - ๋“œ๋กญ๋‹ค์šด ์ปจํ…Œ์ด๋„ˆ +- `Command`, `CommandInput`, `CommandList`, `CommandItem`, `CommandEmpty` - ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ์„ ํƒ UI +- `tableManagementApi.getColumnList()` - ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด ์กฐํšŒ +- `tableManagementApi.getTableList()` - ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์กฐํšŒ +- `tableManagementApi.updateColumnSettings()` - ์กฐ์ธ ์„ค์ • ์ €์žฅ +- `screenApi.saveLayout()` - ๋ ˆ์ด์•„์›ƒ ์ €์žฅ +- `screenApi.getLayout()` - ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ + +### ํ•ต์‹ฌ ๋กœ์ง + +#### ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ/์ถ”๊ฐ€/์ œ๊ฑฐ +```typescript +const handleColumnChange = async (fieldLabel: string, oldColumn: string, newColumn: string) => { + const isAddingField = fieldLabel === "__NEW_FIELD__"; + const isRemovingField = newColumn === "__REMOVE_FIELD__"; + + const currentLayout = await screenApi.getLayout(screenId); + + const updatedComponents = currentLayout.components.map((comp: any) => { + if (comp.componentConfig?.leftPanel?.columns) { + const leftColumns = comp.componentConfig.leftPanel.columns; + + // ํ•„๋“œ ์ถ”๊ฐ€ + if (isAddingField) { + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + leftPanel: { + ...comp.componentConfig.leftPanel, + columns: [...leftColumns, { name: newColumn, columnName: newColumn }], + }, + }, + }; + } + + // ํ•„๋“œ ์ œ๊ฑฐ + const columnIdx = leftColumns.findIndex((col: any) => ...); + if (columnIdx !== -1 && isRemovingField) { + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + leftPanel: { + ...comp.componentConfig.leftPanel, + columns: leftColumns.filter((_, i) => i !== columnIdx), + }, + }, + }; + } + + // ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ + if (columnIdx !== -1) { + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + leftPanel: { + ...comp.componentConfig.leftPanel, + columns: leftColumns.map((col, i) => + i === columnIdx ? { ...col, name: newColumn } : col + ), + }, + }, + }; + } + } + return comp; + }); + + await screenApi.saveLayout(screenId, { ...currentLayout, components: updatedComponents }); + onRefresh?.(); +}; +``` + +#### ์กฐ์ธ ์„ค์ • ํŽธ์ง‘๊ธฐ (JoinSettingEditor) +```tsx + +``` + +## ํŒŒ์ผ ๋ณ€๊ฒฝ ๋ชฉ๋ก + +| ํŒŒ์ผ | ๋ณ€๊ฒฝ ๋‚ด์šฉ | +|------|----------| +| `frontend/components/screen/ScreenSettingModal.tsx` | ์ „์ฒด UI ๊ฐœ์„ , ์คŒ/๋“œ๋ž˜๊ทธ ๊ธฐ๋Šฅ, ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ/์ถ”๊ฐ€/์ œ๊ฑฐ ๊ธฐ๋Šฅ, ์กฐ์ธ ์„ค์ • ๊ธฐ๋Šฅ, ํ•„๋“œ ๋งคํ•‘ ํ†ตํ•ฉ, ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜ | +| `frontend/components/screen/ScreenRelationFlow.tsx` | `filterKeyMapping`, `joinColumnRefs` ๋ฐ์ดํ„ฐ ์ „๋‹ฌ | +| `frontend/lib/api/entityJoin.ts` | `companyCodeOverride` ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ | +| `frontend/lib/api/screen.ts` | `saveLayout`, `getLayout` API ์‚ฌ์šฉ | +| `frontend/lib/api/tableManagement.ts` | `getTableList`, `getColumnList`, `updateColumnSettings` API | +| `frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx` | `companyCode` prop ์ถ”๊ฐ€ | +| `backend-node/src/controllers/entityJoinController.ts` | `companyCodeOverride` ์ฒ˜๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€ | + +## ์‚ฌ์šฉ ๋ฐฉ๋ฒ• + +### ํ™”๋ฉด ์„ค์ • ๋ชจ๋‹ฌ ์—ด๊ธฐ +1. ํ™”๋ฉด ๊ด€๋ฆฌ ํŽ˜์ด์ง€์—์„œ ํ™”๋ฉด ๊ทธ๋ฃน ์„ ํƒ +2. ํ™”๋ฉด ๋…ธ๋“œ ์šฐํด๋ฆญ โ†’ ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด ํ‘œ์‹œ +3. "ํ™”๋ฉด ์„ค์ •" ์„ ํƒ โ†’ ๋ชจ๋‹ฌ ์—ด๋ฆผ +4. ์ขŒ์ธก ํƒญ์—์„œ ์ •๋ณด ํ™•์ธ/์ˆ˜์ •, ์šฐ์ธก์—์„œ ์‹ค์‹œ๊ฐ„ ํ”„๋ฆฌ๋ทฐ + +### ํ”„๋ฆฌ๋ทฐ ์˜์—ญ ์กฐ์ž‘ +- **ํœ  ์Šคํฌ๋กค**: ํ™•๋Œ€/์ถ•์†Œ (5% ๋‹จ์œ„) +- **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ**: ํ™”๋ฉด ์ด๋™ (5px ์ด์ƒ ์›€์ง์—ฌ์•ผ ๋“œ๋ž˜๊ทธ๋กœ ์ธ์‹) +- **์งง์€ ํด๋ฆญ**: iframe ๋‚ด๋ถ€ ์š”์†Œ ํด๋ฆญ + +### ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ +1. ๋ฉ”์ธ/ํ•„ํ„ฐ ํ…Œ์ด๋ธ” ์•„์ฝ”๋””์–ธ ํŽผ์น˜๊ธฐ +2. ํŒŒ๋ž€์ƒ‰ ๋ฐฐ๊ฒฝ์˜ "ํ•„๋“œ" ์ปฌ๋Ÿผ ํด๋ฆญ +3. ์šฐ์ธก "์ปฌ๋Ÿผ ์„ค์ •" ํŒจ๋„ ํ™•์ธ +4. "์ปฌ๋Ÿผ ๋ณ€๊ฒฝ" ๋“œ๋กญ๋‹ค์šด์—์„œ ์ƒˆ ์ปฌ๋Ÿผ ์„ ํƒ +5. **์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜** (ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ ์—†์Œ) + +### ํ•„๋“œ ์ถ”๊ฐ€ +1. ๋ฉ”์ธ/ํ•„ํ„ฐ ํ…Œ์ด๋ธ” ์•„์ฝ”๋””์–ธ ํŽผ์น˜๊ธฐ +2. ํšŒ์ƒ‰/ํฐ์ƒ‰ ๋ฐฐ๊ฒฝ์˜ ๋น„ํ•„๋“œ ์ปฌ๋Ÿผ ํด๋ฆญ +3. ์šฐ์ธก ํŒจ๋„์—์„œ **"ํ•„๋“œ๋กœ ์ถ”๊ฐ€"** ๋ฒ„ํŠผ ํด๋ฆญ +4. ํ•ด๋‹น ์ปฌ๋Ÿผ์ด ํ™”๋ฉด ํ•„๋“œ๋กœ ์ถ”๊ฐ€๋จ + +### ํ•„๋“œ ์ œ๊ฑฐ +1. ๋ฉ”์ธ/ํ•„ํ„ฐ ํ…Œ์ด๋ธ” ์•„์ฝ”๋””์–ธ ํŽผ์น˜๊ธฐ +2. ํŒŒ๋ž€์ƒ‰ ๋ฐฐ๊ฒฝ์˜ ํ•„๋“œ ์ปฌ๋Ÿผ ํด๋ฆญ +3. ์šฐ์ธก ํŒจ๋„์—์„œ **"ํ•„๋“œ์—์„œ ์ œ๊ฑฐ"** ๋ฒ„ํŠผ ํด๋ฆญ +4. ํ•ด๋‹น ํ•„๋“œ๊ฐ€ ํ™”๋ฉด์—์„œ ์ œ๊ฑฐ๋จ + +### ์กฐ์ธ ์„ค์ • ์ถ”๊ฐ€/ํŽธ์ง‘ +1. ๋ฉ”์ธ/ํ•„ํ„ฐ ํ…Œ์ด๋ธ” ์•„์ฝ”๋””์–ธ ํŽผ์น˜๊ธฐ +2. ์•„๋ฌด ์ปฌ๋Ÿผ ํด๋ฆญ (์กฐ์ธ ํ‚ค๊ฐ€ ์•„๋‹ˆ์–ด๋„ ๋จ) +3. ์šฐ์ธก ํŒจ๋„์˜ "์กฐ์ธ" ์„น์…˜์—์„œ: + - ์กฐ์ธ ์—†์Œ: **"์ถ”๊ฐ€"** ๋ฒ„ํŠผ ํด๋ฆญ + - ์กฐ์ธ ์žˆ์Œ: **"ํŽธ์ง‘"** ๋ฒ„ํŠผ ํด๋ฆญ +4. ๋Œ€์ƒ ํ…Œ์ด๋ธ” ์„ ํƒ (๊ฒ€์ƒ‰ ๊ฐ€๋Šฅ) +5. ์—ฐ๊ฒฐ ์ปฌ๋Ÿผ (PK) ์„ ํƒ (๊ฒ€์ƒ‰ ๊ฐ€๋Šฅ) +6. ํ‘œ์‹œ ์ปฌ๋Ÿผ ์„ ํƒ (๊ฒ€์ƒ‰ ๊ฐ€๋Šฅ) +7. **"์ €์žฅ"** ๋ฒ„ํŠผ ํด๋ฆญ + +### ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ (๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ) +1. ๋ฉ”์ธ/ํ•„ํ„ฐ ํ…Œ์ด๋ธ” ์•„์ฝ”๋””์–ธ ํŽผ์น˜๊ธฐ +2. ํŒŒ๋ž€์ƒ‰ ๋ฐฐ๊ฒฝ์˜ "ํ•„๋“œ" ์ปฌ๋Ÿผ์„ ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ +3. ์›ํ•˜๋Š” ์œ„์น˜๋กœ ๋“œ๋ž˜๊ทธํ•˜์—ฌ ์ด๋™ (์‹ค์‹œ๊ฐ„์œผ๋กœ ์ˆœ์„œ ๋ณ€๊ฒฝ ํ‘œ์‹œ) +4. ๋งˆ์šฐ์Šค๋ฅผ ๋†“์œผ๋ฉด (๋“œ๋กญ) ์ˆœ์„œ๊ฐ€ ์ €์žฅ๋จ +5. ๋“œ๋ž˜๊ทธ ์ทจ์†Œํ•˜๋ ค๋ฉด ์ปฌ๋Ÿผ ์˜์—ญ ๋ฐ–์œผ๋กœ ๋“œ๋ž˜๊ทธ + +**์ฐธ๊ณ :** +- ์‚ฌ์šฉ ์ค‘์ธ ํ•„๋“œ๋งŒ ๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅ (ํŒŒ๋ž€์ƒ‰ ๋ฐฐ๊ฒฝ) +- ๋ฏธ์‚ฌ์šฉ ์ปฌ๋Ÿผ์€ ๋“œ๋ž˜๊ทธ ๋ถˆ๊ฐ€ +- ๋“œ๋ž˜๊ทธ ์ค‘์—๋Š” ์ €์žฅ๋˜์ง€ ์•Š๊ณ , ๋“œ๋กญ ์‹œ์—๋งŒ ์ €์žฅ๋จ + +--- + +## ์™„๋ฃŒ์ผ +2026-01-13 + +## ๋ณ€๊ฒฝ ์ด๋ ฅ +- 2026-01-12: ์ตœ์ดˆ ์ž‘์„ฑ (์คŒ/๋“œ๋ž˜๊ทธ/ํด๋ฆญ, company_code ์ „๋‹ฌ) +- 2026-01-12: ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ ๊ธฐ๋Šฅ ์ถ”๊ฐ€, ํ•„๋“œ ๋งคํ•‘ ํ†ตํ•ฉ, UI ๊ฐœ์„  (1์—ด ๋ ˆ์ด์•„์›ƒ, ๋ฐฐ์ง€ ๋ณ€๊ฒฝ) +- 2026-01-12: ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜ ๊ตฌํ˜„ (reload ์ œ๊ฑฐ), ๋ ˆ์ด์•„์›ƒ ์ˆœ์„œ ๋ณ€๊ฒฝ, ์Šคํƒ€์ผ ๊ฐœ์„  +- 2026-01-12: ํ•„๋“œ+์กฐ์ธ ์ปฌ๋Ÿผ ์Šคํƒ€์ผ ๊ฐœ์„  (ํŒŒ๋ž€๋ฐฐ๊ฒฝ + ์™ผ์ชฝ ์ฃผํ™ฉ์„ ), ์กฐ์ธ ์ •๋ณด ํŒจ๋„ ํ‘œ์‹œ +- 2026-01-12: ์กฐ์ธ ๊ด€๊ณ„ ์„ค์ •/์ˆ˜์ • ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์™„๋ฃŒ (column_labels ํ…Œ์ด๋ธ” ์ €์žฅ) +- 2026-01-13: ํ•„๋“œ ์ถ”๊ฐ€/์ œ๊ฑฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„ +- 2026-01-13: ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ์กฐ์ธ ์„ค์ • ๋“œ๋กญ๋‹ค์šด (Command ์ปดํฌ๋„ŒํŠธ) +- 2026-01-13: ๋ชจ๋“  ์ปฌ๋Ÿผ์—์„œ ์กฐ์ธ ์„ค์ • ๊ฐ€๋Šฅ (๋ฒ”์šฉ์„ฑ ํŒจ์น˜) +- 2026-01-13: ๋ฉ”์ธ ํ…Œ์ด๋ธ”์—๋„ ์กฐ์ธ ์„ค์ • ๊ธฐ๋Šฅ ์ถ”๊ฐ€ +- 2026-01-13: ์กฐ์ธ ๋ผ์ธ ์ƒ‰์ƒ ์ฃผํ™ฉ์ƒ‰์œผ๋กœ ๋ณ€๊ฒฝ (`border-l-orange-500`) +- 2026-01-13: ์กฐ์ธ ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ˆ˜์ • (`joinRef.refTable` ์‚ฌ์šฉ) +- 2026-01-13: ํŒจ๋„ ๋†’์ด ๋™๊ธฐํ™” (`max-h-[350px]`, `items-stretch`) +- 2026-01-13: `MainTableAccordion`๊ณผ `FilterTableAccordion`์„ `TableColumnAccordion`์œผ๋กœ ํ†ตํ•ฉ +- 2026-01-13: ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ ๊ธฐ๋Šฅ ๊ตฌํ˜„ +- 2026-01-13: ๋“œ๋ž˜๊ทธ ์ค‘์—๋Š” ๋กœ์ปฌ ์ƒํƒœ๋งŒ ๋ณ€๊ฒฝ, ๋“œ๋กญ ์‹œ์—๋งŒ ์ €์žฅํ•˜๋„๋ก ์ตœ์ ํ™” diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 99634357..c96f7483 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -35,6 +35,9 @@ function ScreenViewPage() { // URL ์ฟผ๋ฆฌ์—์„œ ํ”„๋ฆฌ๋ทฐ์šฉ company_code ๊ฐ€์ ธ์˜ค๊ธฐ const previewCompanyCode = searchParams.get("company_code"); + + // ํ”„๋ฆฌ๋ทฐ ๋ชจ๋“œ ๊ฐ์ง€ (iframe์—์„œ ๋กœ๋“œ๋  ๋•Œ) + const isPreviewMode = searchParams.get("preview") === "true"; // ๐Ÿ†• ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ์ •๋ณด const { user, userName, companyCode: authCompanyCode } = useAuth(); @@ -239,27 +242,40 @@ function ScreenViewPage() { const designWidth = layout?.screenResolution?.width || 1200; const designHeight = layout?.screenResolution?.height || 800; - // ์ปจํ…Œ์ด๋„ˆ์˜ ์‹ค์ œ ํฌ๊ธฐ - const containerWidth = containerRef.current.offsetWidth; - const containerHeight = containerRef.current.offsetHeight; + // ์ปจํ…Œ์ด๋„ˆ์˜ ์‹ค์ œ ํฌ๊ธฐ (ํ”„๋ฆฌ๋ทฐ ๋ชจ๋“œ์—์„œ๋Š” window ํฌ๊ธฐ ์‚ฌ์šฉ) + let containerWidth: number; + let containerHeight: number; + + if (isPreviewMode) { + // iframe์—์„œ๋Š” window ํฌ๊ธฐ๋ฅผ ์ง์ ‘ ์‚ฌ์šฉ + containerWidth = window.innerWidth; + containerHeight = window.innerHeight; + } else { + containerWidth = containerRef.current.offsetWidth; + containerHeight = containerRef.current.offsetHeight; + } - // ์—ฌ๋ฐฑ ์„ค์ •: ์ขŒ์šฐ 16px์”ฉ (์ด 32px), ์ƒ๋‹จ ํŒจ๋”ฉ 32px (pt-8) - const MARGIN_X = 32; - const availableWidth = containerWidth - MARGIN_X; - - // ๊ฐ€๋กœ ๊ธฐ์ค€ ์Šค์ผ€์ผ ๊ณ„์‚ฐ (์ขŒ์šฐ ์—ฌ๋ฐฑ 16px์”ฉ ๊ณ ์ •) - const newScale = availableWidth / designWidth; + let newScale: number; + + if (isPreviewMode) { + // ํ”„๋ฆฌ๋ทฐ ๋ชจ๋“œ: ๊ฐ€๋กœ/์„ธ๋กœ ๋ชจ๋‘ fitํ•˜๋„๋ก (์—ฌ๋ฐฑ ์—†์ด) + const scaleX = containerWidth / designWidth; + const scaleY = containerHeight / designHeight; + newScale = Math.min(scaleX, scaleY, 1); // ์ตœ๋Œ€ 1๋ฐฐ์œจ + } else { + // ์ผ๋ฐ˜ ๋ชจ๋“œ: ๊ฐ€๋กœ ๊ธฐ์ค€ ์Šค์ผ€์ผ (์ขŒ์šฐ ์—ฌ๋ฐฑ 16px์”ฉ ๊ณ ์ •) + const MARGIN_X = 32; + const availableWidth = containerWidth - MARGIN_X; + newScale = availableWidth / designWidth; + } // console.log("๐Ÿ“ ์Šค์ผ€์ผ ๊ณ„์‚ฐ:", { // containerWidth, // containerHeight, - // MARGIN_X, - // availableWidth, // designWidth, // designHeight, // finalScale: newScale, - // "์Šค์ผ€์ผ๋œ ํ™”๋ฉด ํฌ๊ธฐ": `${designWidth * newScale}px ร— ${designHeight * newScale}px`, - // "์‹ค์ œ ์ขŒ์šฐ ์—ฌ๋ฐฑ": `${(containerWidth - designWidth * newScale) / 2}px์”ฉ`, + // isPreviewMode, // }); setScale(newScale); @@ -278,7 +294,7 @@ function ScreenViewPage() { return () => { clearTimeout(timer); }; - }, [layout, isMobile]); + }, [layout, isMobile, isPreviewMode]); if (loading) { return ( @@ -316,7 +332,7 @@ function ScreenViewPage() { -
+
{/* ๋ ˆ์ด์•„์›ƒ ์ค€๋น„ ์ค‘ ๋กœ๋”ฉ ํ‘œ์‹œ */} {!layoutReady && (
diff --git a/frontend/components/screen/ScreenRelationFlow.tsx b/frontend/components/screen/ScreenRelationFlow.tsx index f3b14d56..d7f814d3 100644 --- a/frontend/components/screen/ScreenRelationFlow.tsx +++ b/frontend/components/screen/ScreenRelationFlow.tsx @@ -1107,10 +1107,37 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId ); const tableNodeData = tableNode?.data as TableNodeData | undefined; + // ํ•„ํ„ฐ ํ‚ค ๋งคํ•‘ ์ •๋ณด ์ถ”์ถœ (leftColumn โ†’ foreignKey) + let filterKeyMapping: { + mainTableColumn: string; + mainTableColumnLabel?: string; + filterTableColumn: string; + filterTableColumnLabel?: string; + } | undefined = undefined; + + if (subTableData?.leftColumn && subTableData?.foreignKey) { + // ๋ฉ”์ธ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ํ•œ๊ธ€๋ช… ์กฐํšŒ + const mainTable = subTablesDataMap[screenId]?.mainTable; + const mainTableCols = mainTable ? tableColumns[mainTable] : []; + const mainColInfo = mainTableCols?.find(c => c.columnName === subTableData.leftColumn); + + // ํ•„ํ„ฐ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ํ•œ๊ธ€๋ช… ์กฐํšŒ + const filterTableCols = tableColumns[tableName] || []; + const filterColInfo = filterTableCols?.find(c => c.columnName === subTableData.foreignKey); + + filterKeyMapping = { + mainTableColumn: subTableData.leftColumn, + mainTableColumnLabel: mainColInfo?.displayName, + filterTableColumn: subTableData.foreignKey, + filterTableColumnLabel: filterColInfo?.displayName, + }; + } + return { tableName, tableLabel: subTableData?.tableLabel || tableNodeData?.label || tableName, filterColumns: subTableData?.filterColumns || tableNodeData?.filterColumns || [], + filterKeyMapping, joinColumnRefs: subTableData?.joinColumnRefs || tableNodeData?.joinColumnRefs || [], }; }); diff --git a/frontend/components/screen/ScreenSettingModal.tsx b/frontend/components/screen/ScreenSettingModal.tsx index 6b6dea5f..79342ef6 100644 --- a/frontend/components/screen/ScreenSettingModal.tsx +++ b/frontend/components/screen/ScreenSettingModal.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useCallback, useMemo } from "react"; +import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { Dialog, DialogContent, @@ -45,6 +45,7 @@ import { Eye, Save, Plus, + Minus, Pencil, Trash2, RefreshCw, @@ -58,6 +59,13 @@ import { ChevronDown, ChevronRight, Filter, + RotateCcw, + X, + Zap, + MousePointer, + Globe, + Workflow, + Info, } from "lucide-react"; import { getDataFlows, @@ -68,7 +76,19 @@ import { getMultipleScreenLayoutSummary, LayoutItem, } from "@/lib/api/screenGroup"; -import { tableManagementApi, ColumnTypeInfo, TableInfo } from "@/lib/api/tableManagement"; +import { tableManagementApi, ColumnTypeInfo, TableInfo, ColumnSettings } from "@/lib/api/tableManagement"; +import { screenApi } from "@/lib/api/screen"; +import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"; +import { ExternalCallConfigAPI, ExternalCallConfig } from "@/lib/api/externalCallConfig"; +import { getFlowDefinitions } from "@/lib/api/flow"; +import { FlowDefinition } from "@/types/flow"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; // ============================================================ // ํƒ€์ž… ์ •์˜ @@ -78,6 +98,13 @@ interface FilterTableInfo { tableName: string; tableLabel?: string; filterColumns?: string[]; + // ํ•„ํ„ฐ ํ‚ค ๋งคํ•‘ ์ •๋ณด (๋ฉ”์ธ ํ…Œ์ด๋ธ”.์ปฌ๋Ÿผ โ†’ ํ•„ํ„ฐ ํ…Œ์ด๋ธ”.์ปฌ๋Ÿผ) + filterKeyMapping?: { + mainTableColumn: string; // ๋ฉ”์ธ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ (leftColumn) + mainTableColumnLabel?: string; + filterTableColumn: string; // ํ•„ํ„ฐ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ (foreignKey) + filterTableColumnLabel?: string; + }; joinColumnRefs?: Array<{ column: string; refTable: string; @@ -217,6 +244,7 @@ export function ScreenSettingModal({ const [loading, setLoading] = useState(false); const [dataFlows, setDataFlows] = useState([]); const [layoutItems, setLayoutItems] = useState([]); + const [iframeKey, setIframeKey] = useState(0); // iframe ์ƒˆ๋กœ๊ณ ์นจ์šฉ ํ‚ค // ๋ฐ์ดํ„ฐ ๋กœ๋“œ const loadData = useCallback(async () => { @@ -249,15 +277,15 @@ export function ScreenSettingModal({ } }, [isOpen, screenId, loadData]); - // ์ƒˆ๋กœ๊ณ ์นจ - const handleRefresh = () => { + // ์ƒˆ๋กœ๊ณ ์นจ (๋ฐ์ดํ„ฐ + iframe) + const handleRefresh = useCallback(() => { loadData(); - toast.success("์ƒˆ๋กœ๊ณ ์นจ ์™„๋ฃŒ"); - }; + setIframeKey(prev => prev + 1); // iframe ์ƒˆ๋กœ๊ณ ์นจ + }, [loadData]); return ( - + @@ -268,120 +296,254 @@ export function ScreenSettingModal({ - -
- - - - ํ™”๋ฉด ๊ฐœ์š” - - - - ํ•„๋“œ ๋งคํ•‘ - - - - ๋ฐ์ดํ„ฐ ํ๋ฆ„ - - - - ํ™”๋ฉด ํ”„๋ฆฌ๋ทฐ - - - +
+ + + + ๊ฐœ์š” + + + + ์ œ์–ด ๊ด€๋ฆฌ + + + + ๋ฐ์ดํ„ฐ ํ๋ฆ„ + + + +
+ + {/* ํƒญ 1: ํ™”๋ฉด ๊ฐœ์š” */} + + + + + {/* ํƒญ 2: ์ œ์–ด ๊ด€๋ฆฌ */} + + + + + {/* ํƒญ 3: ๋ฐ์ดํ„ฐ ํ๋ฆ„ */} + + + +
- {/* ํƒญ 1: ํ™”๋ฉด ๊ฐœ์š” */} - - - - - {/* ํƒญ 2: ํ•„๋“œ ๋งคํ•‘ */} - - - - - {/* ํƒญ 3: ๋ฐ์ดํ„ฐ ํ๋ฆ„ */} - - - - - {/* ํƒญ 4: ํ™”๋ฉด ํ”„๋ฆฌ๋ทฐ */} - - - -
+ {/* ์˜ค๋ฅธ์ชฝ: ํ™”๋ฉด ํ”„๋ฆฌ๋ทฐ (60%, ํ•ญ์ƒ ํ‘œ์‹œ) */} +
+ +
+
); } // ============================================================ -// ํ•„ํ„ฐ ํ…Œ์ด๋ธ” ์•„์ฝ”๋””์–ธ ์ปดํฌ๋„ŒํŠธ +// ํ†ตํ•ฉ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์•„์ฝ”๋””์–ธ ์ปดํฌ๋„ŒํŠธ // ============================================================ -interface FilterTableAccordionProps { - filterTable: FilterTableInfo; - mainTable?: string; +interface ColumnMapping { + columnName: string; + fieldLabel?: string; + order: number; // ํ™”๋ฉด ์ˆœ์„œ (y ์ขŒํ‘œ ๊ธฐ์ค€) } -function FilterTableAccordion({ filterTable: ft, mainTable }: FilterTableAccordionProps) { +interface JoinColumnRef { + column: string; + refTable: string; + refTableLabel?: string; + refColumn: string; + displayColumn?: string; +} + +interface FilterKeyMapping { + mainTableColumn: string; + mainTableColumnLabel?: string; + filterTableColumn: string; + filterTableColumnLabel?: string; +} + +interface TableColumnAccordionProps { + // ๊ณตํ†ต props + tableName: string; + tableLabel?: string; + tableType: "main" | "filter"; // ํ…Œ์ด๋ธ” ํƒ€์ž… + columnMappings?: ColumnMapping[]; + onColumnChange?: (fieldLabel: string, oldColumn: string, newColumn: string) => void; + onColumnReorder?: (newOrder: string[]) => void; // ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ + onJoinSettingSaved?: () => void; + + // ํ•„ํ„ฐ ํ…Œ์ด๋ธ” ์ „์šฉ props (optional) + mainTable?: string; // ๋ฉ”์ธ ํ…Œ์ด๋ธ”๋ช… (ํ•„ํ„ฐ ํ…Œ์ด๋ธ”์—์„œ ํ•„ํ„ฐ ์—ฐ๊ฒฐ ์ •๋ณด ํ‘œ์‹œ์šฉ) + filterKeyMapping?: FilterKeyMapping; + joinColumnRefs?: JoinColumnRef[]; +} + +function TableColumnAccordion({ + tableName, + tableLabel, + tableType, + columnMappings = [], + onColumnChange, + onColumnReorder, + onJoinSettingSaved, + mainTable, + filterKeyMapping, + joinColumnRefs = [], +}: TableColumnAccordionProps) { + // columnMappings๋ฅผ Map์œผ๋กœ ๋ณ€ํ™˜ (์ปฌ๋Ÿผ๋ช… โ†’ ๋งคํ•‘์ •๋ณด) + const columnMappingMap = useMemo(() => { + const map = new Map(); + columnMappings.forEach(m => map.set(m.columnName.toLowerCase(), m)); + return map; + }, [columnMappings]); + const [isOpen, setIsOpen] = useState(false); const [columns, setColumns] = useState([]); const [loadingColumns, setLoadingColumns] = useState(false); + + // ํŽธ์ง‘ ์ค‘์ธ ํ•„๋“œ + const [editingField, setEditingField] = useState(null); + + // ์กฐ์ธ ์„ค์ • ๊ด€๋ จ ์ƒํƒœ + const [allTables, setAllTables] = useState([]); + const [refTableColumns, setRefTableColumns] = useState([]); + const [loadingRefColumns, setLoadingRefColumns] = useState(false); + const [savingJoinSetting, setSavingJoinSetting] = useState(false); + + // ์กฐ์ธ ์„ค์ • ํŽธ์ง‘ ์ƒํƒœ + const [editingJoin, setEditingJoin] = useState<{ + columnName: string; + referenceTable: string; + referenceColumn: string; + displayColumn: string; + } | null>(null); + + // ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์ƒํƒœ + const [draggedIndex, setDraggedIndex] = useState(null); + const [localColumnOrder, setLocalColumnOrder] = useState(null); // ๋“œ๋ž˜๊ทธ ์ค‘ ๋กœ์ปฌ ์ˆœ์„œ + + // ์Šคํƒ€์ผ ์„ค์ • (ํ…Œ์ด๋ธ” ํƒ€์ž…๋ณ„) + const isMain = tableType === "main"; + const themeColor = isMain ? "blue" : "purple"; + const themeIcon = isMain ? Table2 : Filter; + const themeBadge = isMain ? "๋ฉ”์ธ" : "ํ•„ํ„ฐ"; + + // ํ•„ํ„ฐ ํ…Œ์ด๋ธ”์šฉ ํ”Œ๋ž˜๊ทธ + const hasJoinRefs = joinColumnRefs && joinColumnRefs.length > 0; + const hasFilterKey = !!filterKeyMapping; + + // ์ •๋ ฌ๋œ ์ปฌ๋Ÿผ ๋ชฉ๋ก + const sortedColumns = useMemo(() => { + if (columns.length === 0) return []; + + if (isMain) { + // ๋ฉ”์ธ: ์‚ฌ์šฉ ์ค‘ โ†’ ์•ˆ ์“ฐ๋Š” ์ปฌ๋Ÿผ + const used: (ColumnTypeInfo & { mapping: ColumnMapping })[] = []; + const unused: ColumnTypeInfo[] = []; + + columns.forEach(col => { + const mapping = columnMappingMap.get(col.columnName.toLowerCase()); + if (mapping) { + used.push({ ...col, mapping }); + } else { + unused.push(col); + } + }); + + used.sort((a, b) => a.mapping.order - b.mapping.order); + return [...used, ...unused]; + } else { + // ํ•„ํ„ฐ: ํ•„ํ„ฐํ‚ค โ†’ ์กฐ์ธํ‚ค โ†’ ํ•„๋“œ โ†’ ์•ˆ ์“ฐ๋Š” ์ปฌ๋Ÿผ + const filterKeys: ColumnTypeInfo[] = []; + const joinKeys: ColumnTypeInfo[] = []; + const fieldCols: (ColumnTypeInfo & { mapping: ColumnMapping })[] = []; + const unused: ColumnTypeInfo[] = []; + + columns.forEach(col => { + const colNameLower = col.columnName.toLowerCase(); + const isFilterKey = filterKeyMapping?.filterTableColumn?.toLowerCase() === colNameLower; + const isJoinKey = joinColumnRefs?.some(j => j.column.toLowerCase() === colNameLower); + const mapping = columnMappingMap.get(colNameLower); + + if (isFilterKey) { + filterKeys.push(col); + } else if (isJoinKey) { + joinKeys.push(col); + } else if (mapping) { + fieldCols.push({ ...col, mapping }); + } else { + unused.push(col); + } + }); + + fieldCols.sort((a, b) => a.mapping.order - b.mapping.order); + return [...filterKeys, ...joinKeys, ...fieldCols, ...unused]; + } + }, [columns, columnMappingMap, isMain, filterKeyMapping, joinColumnRefs]); - const hasJoinRefs = ft.joinColumnRefs && ft.joinColumnRefs.length > 0; - const hasFilterColumns = ft.filterColumns && ft.filterColumns.length > 0; - - // ์•„์ฝ”๋””์–ธ ์—ด๋ฆด ๋•Œ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ + // ์•„์ฝ”๋””์–ธ ์—ด๋ฆด ๋•Œ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ + ์ „์ฒด ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ const handleToggle = async () => { const newIsOpen = !isOpen; setIsOpen(newIsOpen); - // ์ฒ˜์Œ ์—ด๋ฆด ๋•Œ ์ปฌ๋Ÿผ ๋กœ๋“œ - if (newIsOpen && columns.length === 0 && ft.tableName) { + if (newIsOpen && columns.length === 0 && tableName) { setLoadingColumns(true); try { - const result = await tableManagementApi.getColumnList(ft.tableName); + const result = await tableManagementApi.getColumnList(tableName); if (result.success && result.data && result.data.columns) { setColumns(result.data.columns); } + + if (allTables.length === 0) { + const tablesResult = await tableManagementApi.getTableList(); + if (tablesResult.success && tablesResult.data) { + setAllTables(tablesResult.data); + } + } } catch (error) { console.error("ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ ์‹คํŒจ:", error); } finally { @@ -389,145 +551,734 @@ function FilterTableAccordion({ filterTable: ft, mainTable }: FilterTableAccordi } } }; + + // ์ฐธ์กฐ ํ…Œ์ด๋ธ” ์„ ํƒ ์‹œ ํ•ด๋‹น ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ๋กœ๋“œ + const loadRefTableColumns = useCallback(async (refTableName: string) => { + if (!refTableName) { + setRefTableColumns([]); + return; + } + + setLoadingRefColumns(true); + try { + const result = await tableManagementApi.getColumnList(refTableName); + if (result.success && result.data && result.data.columns) { + setRefTableColumns(result.data.columns); + } + } catch (error) { + console.error("์ฐธ์กฐ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ ์‹คํŒจ:", error); + } finally { + setLoadingRefColumns(false); + } + }, []); + + // ์กฐ์ธ ์„ค์ • ์ €์žฅ + const handleSaveJoinSetting = useCallback(async () => { + if (!editingJoin || !tableName) return; + + setSavingJoinSetting(true); + try { + const settings: ColumnSettings = { + columnLabel: columns.find(c => c.columnName === editingJoin.columnName)?.displayName || editingJoin.columnName, + webType: "entity", + detailSettings: JSON.stringify({}), + codeCategory: "", + codeValue: "", + referenceTable: editingJoin.referenceTable, + referenceColumn: editingJoin.referenceColumn, + displayColumn: editingJoin.displayColumn, + }; + + const result = await tableManagementApi.updateColumnSettings( + tableName, + editingJoin.columnName, + settings + ); + + if (result.success) { + toast.success("์กฐ์ธ ์„ค์ •์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + setEditingJoin(null); + onJoinSettingSaved?.(); + } else { + toast.error(result.message || "์กฐ์ธ ์„ค์ • ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + } catch (error) { + console.error("์กฐ์ธ ์„ค์ • ์ €์žฅ ์‹คํŒจ:", error); + toast.error("์กฐ์ธ ์„ค์ • ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + } finally { + setSavingJoinSetting(false); + } + }, [editingJoin, tableName, columns, onJoinSettingSaved]); + + // ์กฐ์ธ ์„ค์ • ํŽธ์ง‘ ์‹œ์ž‘ + const startEditingJoin = useCallback((columnName: string, currentRefTable?: string, currentRefColumn?: string, currentDisplayColumn?: string) => { + setEditingJoin({ + columnName, + referenceTable: currentRefTable || "", + referenceColumn: currentRefColumn || "", + displayColumn: currentDisplayColumn || "", + }); + + if (currentRefTable) { + loadRefTableColumns(currentRefTable); + } + }, [loadRefTableColumns]); + + // ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํ•ธ๋“ค๋Ÿฌ + const handleDragStart = useCallback((e: React.DragEvent, index: number) => { + setDraggedIndex(index); + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", String(index)); + + // ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ ์‹œ ํ˜„์žฌ ์ˆœ์„œ๋ฅผ ๋กœ์ปฌ ์ƒํƒœ๋กœ ์ €์žฅ + const usedColumns = sortedColumns.filter(col => { + const colNameLower = col.columnName.toLowerCase(); + return columnMappingMap.has(colNameLower); + }); + setLocalColumnOrder(usedColumns.map(col => col.columnName)); + }, [sortedColumns, columnMappingMap]); + + const handleDragOver = useCallback((e: React.DragEvent, hoverIndex: number) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + + if (draggedIndex === null || draggedIndex === hoverIndex || !localColumnOrder) return; + + // ์‚ฌ์šฉ ์ค‘์ธ ์ปฌ๋Ÿผ ์ˆ˜ ์ฒดํฌ + if (hoverIndex >= localColumnOrder.length || draggedIndex >= localColumnOrder.length) return; + + // ๋กœ์ปฌ ์ˆœ์„œ๋งŒ ๋ณ€๊ฒฝ (์ €์žฅํ•˜์ง€ ์•Š์Œ) + const newOrder = [...localColumnOrder]; + const draggedItem = newOrder[draggedIndex]; + newOrder.splice(draggedIndex, 1); + newOrder.splice(hoverIndex, 0, draggedItem); + + setDraggedIndex(hoverIndex); + setLocalColumnOrder(newOrder); + }, [draggedIndex, localColumnOrder]); + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + + // ๋“œ๋กญ ์‹œ ์ตœ์ข… ์ˆœ์„œ๋กœ ์ €์žฅ + if (localColumnOrder && onColumnReorder) { + onColumnReorder(localColumnOrder); + } + + setDraggedIndex(null); + setLocalColumnOrder(null); + }, [localColumnOrder, onColumnReorder]); + + const handleDragEnd = useCallback(() => { + // ๋“œ๋ž˜๊ทธ ์ทจ์†Œ ์‹œ (๋“œ๋กญ ์˜์—ญ ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ„ ๊ฒฝ์šฐ) + setDraggedIndex(null); + setLocalColumnOrder(null); + }, []); + + // ์ปฌ๋Ÿผ์˜ ํŠน์ˆ˜ ์ƒํƒœ ํ™•์ธ (ํ•„ํ„ฐ ํ…Œ์ด๋ธ”์šฉ) + const getColumnState = (colNameLower: string) => { + const isFilterKey = filterKeyMapping?.filterTableColumn?.toLowerCase() === colNameLower; + const joinRef = joinColumnRefs?.find(j => j.column.toLowerCase() === colNameLower); + const isJoinKey = !!joinRef; + const mapping = columnMappingMap.get(colNameLower); + const isUsed = !!mapping; + + return { isFilterKey, isJoinKey, joinRef, isUsed, mapping }; + }; + + const ThemeIcon = themeIcon; return ( -
- {/* ํ—ค๋” - ํด๋ฆญํ•˜๋ฉด ํŽผ์ณ์ง */} +
+ {/* ํ—ค๋” */} {/* ํŽผ์ณ์ง„ ๋‚ด์šฉ */} {isOpen && ( -
- {/* ํ•„ํ„ฐ ํ‚ค ์„ค๋ช… */} -
- {ft.tableLabel || ft.tableName}์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํ•„ํ„ฐ๋ง๋ฉ๋‹ˆ๋‹ค. -
+
+ {/* ํ•„ํ„ฐ ์—ฐ๊ฒฐ ์ •๋ณด (ํ•„ํ„ฐ ํ…Œ์ด๋ธ”๋งŒ) */} + {!isMain && filterKeyMapping && ( +
+ ํ•„ํ„ฐ + + {mainTable}.{filterKeyMapping.mainTableColumnLabel || filterKeyMapping.mainTableColumn} + + = + + {tableName}.{filterKeyMapping.filterTableColumnLabel || filterKeyMapping.filterTableColumn} + +
+ )} {/* ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด */}
-
- - ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ({loadingColumns ? "๋กœ๋”ฉ์ค‘..." : `${columns.length}๊ฐœ`}) -
- {loadingColumns ? ( -
- +
+
+ + ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ({loadingColumns ? "๋กœ๋”ฉ์ค‘..." : `${columns.length}๊ฐœ`})
- ) : columns.length > 0 ? ( -
- {columns.slice(0, 10).map((col, cIdx) => ( -
- {col.displayName || col.columnName} - - ({col.dataType}) - + {columnMappings.length > 0 && ( +
+
+
+ ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ ({columnMappings.length}๊ฐœ)
- ))} - {columns.length > 10 && ( -
- +{columns.length - 10}๊ฐœ ๋” -
- )} +
+ )} +
+ + {loadingColumns ? ( +
+ +
+ ) : sortedColumns.length > 0 ? ( +
+ {/* ์™ผ์ชฝ: ์ปฌ๋Ÿผ ๋ชฉ๋ก */} +
+ {(() => { + // ๋“œ๋ž˜๊ทธ ์ค‘์ผ ๋•Œ ๋กœ์ปฌ ์ˆœ์„œ ์ ์šฉ + let displayColumns = sortedColumns; + if (localColumnOrder && localColumnOrder.length > 0) { + // ์‚ฌ์šฉ ์ค‘์ธ ์ปฌ๋Ÿผ๋“ค์„ localColumnOrder์— ๋”ฐ๋ผ ์žฌ์ •๋ ฌ + const usedCols = sortedColumns.filter(col => columnMappingMap.has(col.columnName.toLowerCase())); + const unusedCols = sortedColumns.filter(col => !columnMappingMap.has(col.columnName.toLowerCase())); + + const reorderedUsed = localColumnOrder + .map(name => usedCols.find(col => col.columnName.toLowerCase() === name.toLowerCase())) + .filter(Boolean) as typeof usedCols; + + displayColumns = [...reorderedUsed, ...unusedCols]; + } + + return displayColumns.map((col, cIdx) => { + const colNameLower = col.columnName.toLowerCase(); + const { isFilterKey, isJoinKey, isUsed, mapping } = getColumnState(colNameLower); + const isSelected = editingField === (mapping?.fieldLabel || col.columnName); + const isDragging = draggedIndex === cIdx; + + // ๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ (์‚ฌ์šฉ ์ค‘์ธ ์ปฌ๋Ÿผ๋งŒ) + const canDrag = isUsed && !!onColumnReorder; + + // ์Šคํƒ€์ผ ๊ฒฐ์ • + let baseClass = ""; + let leftBorderClass = ""; + + if (isUsed) { + baseClass = isSelected + ? "bg-blue-100 border-blue-300" + : "bg-blue-50 border-blue-200 hover:bg-blue-100 hover:border-blue-300"; + if (isJoinKey) { + leftBorderClass = "border-l-4 border-l-orange-500"; + } else if (isFilterKey) { + leftBorderClass = "border-l-4 border-l-purple-400"; + } + } else if (isJoinKey) { + baseClass = isSelected + ? "bg-orange-100 border-orange-400" + : "bg-orange-50 border-orange-200 hover:bg-orange-100 hover:border-orange-300"; + } else if (isFilterKey) { + baseClass = isSelected + ? "bg-purple-100 border-purple-400" + : "bg-purple-50 border-purple-200 hover:bg-purple-100 hover:border-purple-300"; + } else { + baseClass = isSelected + ? "bg-gray-100 border-gray-400" + : "bg-gray-50 border-gray-200 hover:bg-gray-100"; + } + + return ( +
handleDragStart(e, cIdx) : undefined} + onDragOver={canDrag ? (e) => handleDragOver(e, cIdx) : undefined} + onDrop={canDrag ? handleDrop : undefined} + onDragEnd={canDrag ? handleDragEnd : undefined} + onClick={() => { + setEditingField(mapping?.fieldLabel || col.columnName); + setEditingJoin(null); + }} + className={`flex items-center justify-between gap-2 text-xs rounded px-2 py-1.5 border transition-all cursor-pointer ${baseClass} ${leftBorderClass} ${isDragging ? "opacity-50 scale-95" : ""} ${canDrag ? "cursor-grab active:cursor-grabbing" : ""}`} + > + + {col.displayName || col.columnName} + +
+ {isFilterKey && ( + ํ•„ํ„ฐ + )} + {isJoinKey && ( + ์กฐ์ธ + )} + {isUsed && ( + ํ•„๋“œ + )} + + {col.dataType?.split("(")[0]} + +
+
+ ); + }); + })()} +
+ + {/* ์˜ค๋ฅธ์ชฝ: ์ปฌ๋Ÿผ ์„ค์ • ํŒจ๋„ */} +
+ {editingField ? (() => { + const selectedMapping = columnMappings.find(m => m.fieldLabel === editingField); + const selectedColumn = selectedMapping + ? columns.find(c => c.columnName.toLowerCase() === selectedMapping.columnName?.toLowerCase()) + : columns.find(c => (c.displayName || c.columnName) === editingField || c.columnName === editingField); + const colNameLower = selectedColumn?.columnName?.toLowerCase() || editingField.toLowerCase(); + const { isFilterKey, isJoinKey, joinRef, isUsed } = getColumnState(colNameLower); + + // ์กฐ์ธ ์ •๋ณด - joinColumnRefs์—์„œ ๋จผ์ € ์ฐพ๊ณ , ์—†์œผ๋ฉด selectedColumn์—์„œ ๊ฐ€์ ธ์˜ด + const hasJoinSetting = isJoinKey || !!selectedColumn?.referenceTable; + + return ( +
+
์ปฌ๋Ÿผ ์„ค์ •
+ + {/* ํ™”๋ฉด ํ•„๋“œ ์ •๋ณด (ํ•„๋“œ์ธ ๊ฒฝ์šฐ๋งŒ) */} + {isUsed && ( + <> +
+ ํ™”๋ฉด ํ•„๋“œ +
+ {selectedColumn?.displayName || selectedMapping?.columnName || editingField} +
+
+ +
+ ํ˜„์žฌ ์ปฌ๋Ÿผ +
+ {selectedMapping?.columnName || "-"} +
+
+ +
+ ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ + + + + + + + + + ์—†์Œ + + {columns.map((c) => ( + { + if (onColumnChange && selectedMapping) { + onColumnChange(editingField, selectedMapping.columnName, c.columnName); + } + setEditingField(null); + }} + > + + {c.displayName || c.columnName} + + ))} + + + + + +
+ + {/* ํ•„๋“œ์—์„œ ์ œ๊ฑฐ */} + + + )} + + {/* ์ปฌ๋Ÿผ ๊ธฐ๋ณธ ์ •๋ณด (ํ•„๋“œ๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ) */} + {!isUsed && ( +
+
+
+ ์ปฌ๋Ÿผ๋ช… +
{selectedColumn?.columnName || editingField}
+
+
+ ๋ฐ์ดํ„ฐ ํƒ€์ž… +
{selectedColumn?.dataType || "-"}
+
+
+ + +
+ )} + + {/* ์กฐ์ธ ์„ค์ • */} +
+
+
+ + ์กฐ์ธ + + + {editingJoin && editingJoin.columnName === (selectedColumn?.columnName || editingField) ? "์—ฐ๊ฒฐ ํŽธ์ง‘" : (hasJoinSetting ? "์—ฐ๊ฒฐ ์ •๋ณด" : "์—ฐ๊ฒฐ ์„ค์ •")} + +
+ {editingJoin && editingJoin.columnName === (selectedColumn?.columnName || editingField) ? ( +
+ + +
+ ) : ( + + )} +
+ + {editingJoin && editingJoin.columnName === (selectedColumn?.columnName || editingField) ? ( + + ) : hasJoinSetting ? ( +
+
+ ๋Œ€์ƒ ํ…Œ์ด๋ธ”: + {isJoinKey && joinRef ? joinRef.refTable : selectedColumn?.referenceTable} +
+
+ ์—ฐ๊ฒฐ ์ปฌ๋Ÿผ: + {isJoinKey && joinRef ? joinRef.refColumn : selectedColumn?.referenceColumn} +
+
+ ) : ( +
์กฐ์ธ ์„ค์ •์ด ์—†์Šต๋‹ˆ๋‹ค.
+ )} +
+ + {/* ํ•„ํ„ฐ ์ •๋ณด (ํ•„ํ„ฐ ํ‚ค์ธ ๊ฒฝ์šฐ) - ํ•„ํ„ฐ ํ…Œ์ด๋ธ”์—์„œ๋งŒ */} + {!isMain && isFilterKey && filterKeyMapping && ( +
+
+ ํ•„ํ„ฐ + ํ•„ํ„ฐ๋ง ์ •๋ณด +
+
+
+ ๋Œ€์ƒ ํ…Œ์ด๋ธ”: + {mainTable} +
+
+ ์—ฐ๊ฒฐ ์ปฌ๋Ÿผ: + {filterKeyMapping.mainTableColumn} +
+
+
+ )} +
+ ); + })() : ( +
+ ํ•„๋“œ๋ฅผ ์„ ํƒํ•˜์„ธ์š” +
+ )} +
) : ( -
- ์ปฌ๋Ÿผ ์ •๋ณด ์—†์Œ -
+
์ปฌ๋Ÿผ ์ •๋ณด ์—†์Œ
)}
- - {/* ํ•„ํ„ฐ ์ปฌ๋Ÿผ ๋งคํ•‘ */} - {hasFilterColumns && ( -
-
- - ํ•„ํ„ฐ ํ‚ค ๋งคํ•‘ -
-
- {ft.filterColumns!.map((col, cIdx) => ( -
- - {mainTable}.{col} - - - - {ft.tableLabel || ft.tableName}.{col} - -
- ))} -
-
- )} - - {/* ์กฐ์ธ ๊ด€๊ณ„ */} - {hasJoinRefs && ( -
-
- - ์กฐ์ธ ๊ด€๊ณ„ ({ft.joinColumnRefs!.length}๊ฐœ) -
-
- {ft.joinColumnRefs!.map((join, jIdx) => ( -
- - {ft.tableLabel || ft.tableName}.{join.column} - - - - {join.refTableLabel || join.refTable}.{join.refColumn} - -
- ))} -
-
- )}
)}
); } +// ============================================================ +// ์กฐ์ธ ์„ค์ • ํŽธ์ง‘ ์ปดํฌ๋„ŒํŠธ (๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ Combobox ์‚ฌ์šฉ) +// ============================================================ + +interface JoinSettingEditorProps { + editingJoin: { + columnName: string; + referenceTable: string; + referenceColumn: string; + displayColumn: string; + }; + setEditingJoin: React.Dispatch>; + allTables: TableInfo[]; + refTableColumns: ColumnTypeInfo[]; + loadingRefColumns: boolean; + savingJoinSetting: boolean; + loadRefTableColumns: (tableName: string) => void; + handleSaveJoinSetting: () => void; +} + +function JoinSettingEditor({ + editingJoin, + setEditingJoin, + allTables, + refTableColumns, + loadingRefColumns, + savingJoinSetting, + loadRefTableColumns, + handleSaveJoinSetting, +}: JoinSettingEditorProps) { + const [tableSearchOpen, setTableSearchOpen] = useState(false); + const [refColSearchOpen, setRefColSearchOpen] = useState(false); + const [displayColSearchOpen, setDisplayColSearchOpen] = useState(false); + + const selectedTable = allTables.find(t => t.tableName === editingJoin.referenceTable); + const selectedRefCol = refTableColumns.find(c => c.columnName === editingJoin.referenceColumn); + const selectedDisplayCol = refTableColumns.find(c => c.columnName === editingJoin.displayColumn); + + return ( +
+ {/* ๋Œ€์ƒ ํ…Œ์ด๋ธ” ์„ ํƒ - ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅ Combobox */} +
+ ๋Œ€์ƒ ํ…Œ์ด๋ธ” + + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {allTables.map(t => ( + { + setEditingJoin({ ...editingJoin, referenceTable: t.tableName, referenceColumn: "", displayColumn: "" }); + loadRefTableColumns(t.tableName); + setTableSearchOpen(false); + }} + className="text-xs" + > + + {t.displayName || t.tableName} + + ))} + + + + + +
+ + {/* ์—ฐ๊ฒฐ ์ปฌ๋Ÿผ ์„ ํƒ - ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅ Combobox */} +
+ ์—ฐ๊ฒฐ ์ปฌ๋Ÿผ (PK) + + + + + + + + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {refTableColumns.map(c => ( + { + setEditingJoin({ ...editingJoin, referenceColumn: c.columnName }); + setRefColSearchOpen(false); + }} + className="text-xs" + > + + {c.displayName || c.columnName} + + ))} + + + + + +
+ + {/* ํ‘œ์‹œ ์ปฌ๋Ÿผ ์„ ํƒ - ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅ Combobox */} +
+ ํ‘œ์‹œ ์ปฌ๋Ÿผ + + + + + + + + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {refTableColumns.map(c => ( + { + setEditingJoin({ ...editingJoin, displayColumn: c.columnName }); + setDisplayColSearchOpen(false); + }} + className="text-xs" + > + + {c.displayName || c.columnName} + + ))} + + + + + +
+ +
+ ); +} + // ============================================================ // ํƒญ 1: ํ™”๋ฉด ๊ฐœ์š” // ============================================================ @@ -543,6 +1294,7 @@ interface OverviewTabProps { dataFlows: DataFlow[]; layoutItems: LayoutItem[]; // ์ปดํฌ๋„ŒํŠธ ์ปฌ๋Ÿผ ์ •๋ณด ์ถ”๊ฐ€ loading: boolean; + onRefresh?: () => void; // ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ ํ›„ ์ƒˆ๋กœ๊ณ ์นจ ์ฝœ๋ฐฑ } function OverviewTab({ @@ -556,7 +1308,574 @@ function OverviewTab({ dataFlows, layoutItems, loading, + onRefresh, }: OverviewTabProps) { + const [isSavingColumn, setIsSavingColumn] = useState(false); + + // ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ ์ €์žฅ ํ•จ์ˆ˜ - ํ™”๋ฉด ๋””์ž์ด๋„ˆ์™€ ๋™์ผํ•œ ๋ฐฉ์‹ + const handleColumnChange = useCallback(async (fieldLabel: string, oldColumn: string, newColumn: string) => { + console.log("[handleColumnChange] ์‹œ์ž‘", { screenId, fieldLabel, oldColumn, newColumn }); + + if (!screenId) { + toast.error("ํ™”๋ฉด ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); + return; + } + + // ํ•„๋“œ ์ถ”๊ฐ€/์ œ๊ฑฐ ์ฒ˜๋ฆฌ + const isAddingField = fieldLabel === "__NEW_FIELD__"; + const isRemovingField = newColumn === "__REMOVE_FIELD__"; + + setIsSavingColumn(true); + try { + // 1. ํ˜„์žฌ ๋ ˆ์ด์•„์›ƒ ๊ฐ€์ ธ์˜ค๊ธฐ + console.log("[handleColumnChange] ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ ์‹œ์ž‘", { screenId }); + const currentLayout = await screenApi.getLayout(screenId); + console.log("[handleColumnChange] ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ ์™„๋ฃŒ", { + hasLayout: !!currentLayout, + hasComponents: !!currentLayout?.components, + componentCount: currentLayout?.components?.length + }); + + if (!currentLayout?.components) { + toast.error("๋ ˆ์ด์•„์›ƒ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + console.error("[handleColumnChange] ๋ ˆ์ด์•„์›ƒ ์ •๋ณด ์—†์Œ", { currentLayout }); + return; + } + + // 2. ๋ ˆ์ด์•„์›ƒ์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ + let columnChanged = false; + + // ๋””๋ฒ„๊น…: ๊ฐ ์ปดํฌ๋„ŒํŠธ์˜ ๊ตฌ์กฐ ํ™•์ธ + console.log("[handleColumnChange] ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ ๋ถ„์„ ์‹œ์ž‘"); + currentLayout.components.forEach((comp: any, i: number) => { + console.log(`[handleColumnChange] ์ปดํฌ๋„ŒํŠธ ${i}:`, { + id: comp.id, + componentType: comp.componentType, + hasUsedColumns: !!comp.usedColumns, + usedColumns: comp.usedColumns, + hasComponentConfig: !!comp.componentConfig, + componentConfigKeys: comp.componentConfig ? Object.keys(comp.componentConfig) : [], + componentConfigColumns: comp.componentConfig?.columns, + componentConfigUsedColumns: comp.componentConfig?.usedColumns, + columnName: comp.columnName, + bindField: comp.bindField, + }); + }); + + const updatedComponents = currentLayout.components.map((comp: any) => { + // usedColumns ๋ฐฐ์—ด์ด ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ์—์„œ oldColumn์„ newColumn์œผ๋กœ ๊ต์ฒด + if (comp.usedColumns && Array.isArray(comp.usedColumns)) { + // ํ•„๋“œ ์ถ”๊ฐ€ + if (isAddingField) { + console.log("[handleColumnChange] usedColumns์— ํ•„๋“œ ์ถ”๊ฐ€", { compId: comp.id, newColumn }); + columnChanged = true; + return { + ...comp, + usedColumns: [...comp.usedColumns, newColumn], + }; + } + + const idx = comp.usedColumns.findIndex( + (col: string) => col.toLowerCase() === oldColumn.toLowerCase() + ); + if (idx !== -1) { + console.log("[handleColumnChange] usedColumns์—์„œ ์ฐพ์Œ", { compId: comp.id, idx, isRemovingField }); + columnChanged = true; + + // ํ•„๋“œ ์ œ๊ฑฐ + if (isRemovingField) { + return { + ...comp, + usedColumns: comp.usedColumns.filter((_: string, i: number) => i !== idx), + }; + } + + // ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ + return { + ...comp, + usedColumns: comp.usedColumns.map((col: string, i: number) => + i === idx ? newColumn : col + ), + }; + } + } + + // componentConfig ๋‚ด๋ถ€์˜ usedColumns๋„ ํ™•์ธ + if (comp.componentConfig?.usedColumns && Array.isArray(comp.componentConfig.usedColumns)) { + // ํ•„๋“œ ์ถ”๊ฐ€ + if (isAddingField && !columnChanged) { + console.log("[handleColumnChange] componentConfig.usedColumns์— ํ•„๋“œ ์ถ”๊ฐ€", { compId: comp.id, newColumn }); + columnChanged = true; + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + usedColumns: [...comp.componentConfig.usedColumns, newColumn], + }, + }; + } + + const idx = comp.componentConfig.usedColumns.findIndex( + (col: string) => col.toLowerCase() === oldColumn.toLowerCase() + ); + if (idx !== -1) { + console.log("[handleColumnChange] componentConfig.usedColumns์—์„œ ์ฐพ์Œ", { compId: comp.id, idx, isRemovingField }); + columnChanged = true; + + // ํ•„๋“œ ์ œ๊ฑฐ + if (isRemovingField) { + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + usedColumns: comp.componentConfig.usedColumns.filter((_: string, i: number) => i !== idx), + }, + }; + } + + // ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + usedColumns: comp.componentConfig.usedColumns.map((col: string, i: number) => + i === idx ? newColumn : col + ), + }, + }; + } + } + + // componentConfig.columns ๋ฐฐ์—ด๋„ ํ™•์ธ (์ปฌ๋Ÿผ ์„ค์ • ํ˜•ํƒœ) + if (comp.componentConfig?.columns && Array.isArray(comp.componentConfig.columns)) { + // ํ•„๋“œ ์ถ”๊ฐ€ + if (isAddingField && !columnChanged) { + console.log("[handleColumnChange] componentConfig.columns์— ํ•„๋“œ ์ถ”๊ฐ€", { compId: comp.id, newColumn }); + columnChanged = true; + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + columns: [...comp.componentConfig.columns, { field: newColumn, columnName: newColumn }], + }, + }; + } + + const columnIdx = comp.componentConfig.columns.findIndex( + (col: any) => { + const colName = typeof col === 'string' ? col : (col.field || col.columnName || col.name); + return colName?.toLowerCase() === oldColumn.toLowerCase(); + } + ); + if (columnIdx !== -1) { + console.log("[handleColumnChange] componentConfig.columns์—์„œ ์ฐพ์Œ", { compId: comp.id, columnIdx, isRemovingField }); + columnChanged = true; + + // ํ•„๋“œ ์ œ๊ฑฐ + if (isRemovingField) { + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + columns: comp.componentConfig.columns.filter((_: any, i: number) => i !== columnIdx), + }, + }; + } + + // ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ + const updatedColumns = comp.componentConfig.columns.map((col: any, i: number) => { + if (i !== columnIdx) return col; + if (typeof col === 'string') return newColumn; + return { ...col, field: newColumn, columnName: newColumn }; + }); + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + columns: updatedColumns, + }, + }; + } + } + + // columnName ํ•„๋“œ ์ฒดํฌ (์œ„์ ฏ ์ปดํฌ๋„ŒํŠธ) + if (comp.columnName?.toLowerCase() === oldColumn.toLowerCase()) { + console.log("[handleColumnChange] columnName์—์„œ ์ฐพ์Œ", { compId: comp.id }); + columnChanged = true; + return { + ...comp, + columnName: newColumn, + }; + } + + // bindField ํ•„๋“œ ์ฒดํฌ (๋ฐ”์ธ๋”ฉ ํ•„๋“œ) + if (comp.bindField?.toLowerCase() === oldColumn.toLowerCase()) { + console.log("[handleColumnChange] bindField์—์„œ ์ฐพ์Œ", { compId: comp.id }); + columnChanged = true; + return { + ...comp, + bindField: newColumn, + }; + } + + // split-panel-layout์˜ leftPanel.columns ๊ฒ€์‚ฌ + if (comp.componentConfig?.leftPanel?.columns && Array.isArray(comp.componentConfig.leftPanel.columns)) { + const leftColumns = comp.componentConfig.leftPanel.columns; + console.log("[handleColumnChange] leftPanel.columns ๊ฒ€์‚ฌ:", { + compId: comp.id, + leftColumnsCount: leftColumns.length, + leftColumnsContent: leftColumns.map((col: any) => typeof col === 'string' ? col : (col.name || col.columnName || col.field)), + searchingFor: isAddingField ? newColumn : oldColumn.toLowerCase(), + isAddingField, + isRemovingField, + }); + + // ํ•„๋“œ ์ถ”๊ฐ€: ๋ฐฐ์—ด์— ์ƒˆ ์ปฌ๋Ÿผ ์ถ”๊ฐ€ + if (isAddingField) { + console.log("[handleColumnChange] ํ•„๋“œ ์ถ”๊ฐ€", { compId: comp.id, newColumn }); + columnChanged = true; + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + leftPanel: { + ...comp.componentConfig.leftPanel, + columns: [...leftColumns, { name: newColumn, columnName: newColumn }], + }, + }, + }; + } + + const columnIdx = leftColumns.findIndex((col: any) => { + const colName = typeof col === 'string' ? col : (col.name || col.columnName || col.field); + return colName?.toLowerCase() === oldColumn.toLowerCase(); + }); + if (columnIdx !== -1) { + console.log("[handleColumnChange] leftPanel.columns์—์„œ ์ฐพ์Œ", { compId: comp.id, columnIdx, isRemovingField }); + columnChanged = true; + + // ํ•„๋“œ ์ œ๊ฑฐ: ๋ฐฐ์—ด์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ ์ œ๊ฑฐ + if (isRemovingField) { + const filteredColumns = leftColumns.filter((_: any, i: number) => i !== columnIdx); + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + leftPanel: { + ...comp.componentConfig.leftPanel, + columns: filteredColumns, + }, + }, + }; + } + + // ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ + const updatedLeftColumns = leftColumns.map((col: any, i: number) => { + if (i !== columnIdx) return col; + if (typeof col === 'string') return newColumn; + // ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ name/columnName ํ•„๋“œ ์—…๋ฐ์ดํŠธ + return { ...col, name: newColumn, columnName: newColumn }; + }); + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + leftPanel: { + ...comp.componentConfig.leftPanel, + columns: updatedLeftColumns, + }, + }, + }; + } + } + + // split-panel-layout์˜ rightPanel.columns ๊ฒ€์‚ฌ + if (comp.componentConfig?.rightPanel?.columns && Array.isArray(comp.componentConfig.rightPanel.columns)) { + const rightColumns = comp.componentConfig.rightPanel.columns; + + // ํ•„๋“œ ์ถ”๊ฐ€: ๋ฐฐ์—ด์— ์ƒˆ ์ปฌ๋Ÿผ ์ถ”๊ฐ€ + if (isAddingField && !columnChanged) { + console.log("[handleColumnChange] ํ•„๋“œ ์ถ”๊ฐ€ (rightPanel)", { compId: comp.id, newColumn }); + columnChanged = true; + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + rightPanel: { + ...comp.componentConfig.rightPanel, + columns: [...rightColumns, { name: newColumn, columnName: newColumn }], + }, + }, + }; + } + + const columnIdx = rightColumns.findIndex((col: any) => { + const colName = typeof col === 'string' ? col : (col.name || col.columnName || col.field); + return colName?.toLowerCase() === oldColumn.toLowerCase(); + }); + if (columnIdx !== -1) { + console.log("[handleColumnChange] rightPanel.columns์—์„œ ์ฐพ์Œ", { compId: comp.id, columnIdx, isRemovingField }); + columnChanged = true; + + // ํ•„๋“œ ์ œ๊ฑฐ + if (isRemovingField) { + const filteredColumns = rightColumns.filter((_: any, i: number) => i !== columnIdx); + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + rightPanel: { + ...comp.componentConfig.rightPanel, + columns: filteredColumns, + }, + }, + }; + } + + // ์ปฌ๋Ÿผ ๋ณ€๊ฒฝ + const updatedRightColumns = rightColumns.map((col: any, i: number) => { + if (i !== columnIdx) return col; + if (typeof col === 'string') return newColumn; + return { ...col, name: newColumn, columnName: newColumn }; + }); + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + rightPanel: { + ...comp.componentConfig.rightPanel, + columns: updatedRightColumns, + }, + }, + }; + } + } + + return comp; + }); + + if (!columnChanged) { + toast.warning("๋ณ€๊ฒฝํ•  ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + console.warn("[handleColumnChange] ๋ณ€๊ฒฝํ•  ์ปฌ๋Ÿผ ์—†์Œ", { oldColumn, newColumn }); + return; + } + + // 3. ์ €์žฅ + console.log("[handleColumnChange] ์ €์žฅ ์‹œ์ž‘", { + screenId, + componentCount: updatedComponents.length + }); + await screenApi.saveLayout(screenId, { + ...currentLayout, + components: updatedComponents, + }); + console.log("[handleColumnChange] ์ €์žฅ ์™„๋ฃŒ"); + + if (isAddingField) { + toast.success(`ํ•„๋“œ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค: ${newColumn}`); + } else if (isRemovingField) { + toast.success(`ํ•„๋“œ๊ฐ€ ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค: ${oldColumn}`); + } else { + toast.success(`์ปฌ๋Ÿผ์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค: ${oldColumn} โ†’ ${newColumn}`); + } + + // ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜์„ ์œ„ํ•ด ์ฝœ๋ฐฑ ํ˜ธ์ถœ + onRefresh?.(); + } catch (error) { + console.error("์ปฌ๋Ÿผ ๋ณ€๊ฒฝ ์ €์žฅ ์‹คํŒจ:", error); + toast.error("์ปฌ๋Ÿผ ๋ณ€๊ฒฝ ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + } finally { + setIsSavingColumn(false); + } + }, [screenId, onRefresh]); + + // ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ ์ €์žฅ ํ•จ์ˆ˜ + const handleColumnReorder = useCallback(async (tableType: "main" | "filter", newOrder: string[]) => { + console.log("[handleColumnReorder] ์‹œ์ž‘", { screenId, tableType, newOrder }); + + if (!screenId) { + console.warn("[handleColumnReorder] screenId ์—†์Œ"); + return; + } + + try { + // 1. ํ˜„์žฌ ๋ ˆ์ด์•„์›ƒ ๊ฐ€์ ธ์˜ค๊ธฐ + const currentLayout = await screenApi.getLayout(screenId); + + if (!currentLayout?.components) { + console.error("[handleColumnReorder] ๋ ˆ์ด์•„์›ƒ ์ •๋ณด ์—†์Œ"); + return; + } + + // 2. ๋ ˆ์ด์•„์›ƒ์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ๋“ค์˜ ์ˆœ์„œ ๋ณ€๊ฒฝ + let orderChanged = false; + + const updatedComponents = currentLayout.components.map((comp: any) => { + // split-panel-layout์˜ leftPanel.columns ์ˆœ์„œ ๋ณ€๊ฒฝ + if (comp.componentConfig?.leftPanel?.columns && Array.isArray(comp.componentConfig.leftPanel.columns)) { + const leftColumns = comp.componentConfig.leftPanel.columns as any[]; + + // newOrder์— ๋”ฐ๋ผ leftColumns ์žฌ์ •๋ ฌ + const reorderedColumns = newOrder.map(colName => { + return leftColumns.find((col: any) => { + const name = typeof col === 'string' ? col : (col.name || col.columnName || col.field); + return name?.toLowerCase() === colName.toLowerCase(); + }); + }).filter(Boolean); + + // ์›๋ž˜ ์—†๋˜ ์ปฌ๋Ÿผ๋“ค ์œ ์ง€ (newOrder์— ์—†๋Š” ์ปฌ๋Ÿผ๋“ค) + const remainingColumns = leftColumns.filter((col: any) => { + const name = typeof col === 'string' ? col : (col.name || col.columnName || col.field); + return !newOrder.some(n => n.toLowerCase() === name?.toLowerCase()); + }); + + if (reorderedColumns.length > 0) { + orderChanged = true; + console.log("[handleColumnReorder] leftPanel.columns ์ˆœ์„œ ๋ณ€๊ฒฝ", { + compId: comp.id, + before: leftColumns.map((c: any) => typeof c === 'string' ? c : (c.name || c.columnName)), + after: [...reorderedColumns, ...remainingColumns].map((c: any) => typeof c === 'string' ? c : (c.name || c.columnName)), + }); + + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + leftPanel: { + ...comp.componentConfig.leftPanel, + columns: [...reorderedColumns, ...remainingColumns], + }, + }, + }; + } + } + + // rightPanel.columns ์ˆœ์„œ ๋ณ€๊ฒฝ + if (comp.componentConfig?.rightPanel?.columns && Array.isArray(comp.componentConfig.rightPanel.columns)) { + const rightColumns = comp.componentConfig.rightPanel.columns as any[]; + + const reorderedColumns = newOrder.map(colName => { + return rightColumns.find((col: any) => { + const name = typeof col === 'string' ? col : (col.name || col.columnName || col.field); + return name?.toLowerCase() === colName.toLowerCase(); + }); + }).filter(Boolean); + + const remainingColumns = rightColumns.filter((col: any) => { + const name = typeof col === 'string' ? col : (col.name || col.columnName || col.field); + return !newOrder.some(n => n.toLowerCase() === name?.toLowerCase()); + }); + + if (reorderedColumns.length > 0) { + orderChanged = true; + console.log("[handleColumnReorder] rightPanel.columns ์ˆœ์„œ ๋ณ€๊ฒฝ", { + compId: comp.id, + before: rightColumns.map((c: any) => typeof c === 'string' ? c : (c.name || c.columnName)), + after: [...reorderedColumns, ...remainingColumns].map((c: any) => typeof c === 'string' ? c : (c.name || c.columnName)), + }); + + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + rightPanel: { + ...comp.componentConfig.rightPanel, + columns: [...reorderedColumns, ...remainingColumns], + }, + }, + }; + } + } + + // componentConfig.usedColumns ์ˆœ์„œ ๋ณ€๊ฒฝ + if (comp.componentConfig?.usedColumns && Array.isArray(comp.componentConfig.usedColumns)) { + const usedColumns = comp.componentConfig.usedColumns as string[]; + + const reorderedColumns = newOrder.filter(colName => + usedColumns.some(c => c.toLowerCase() === colName.toLowerCase()) + ); + + const remainingColumns = usedColumns.filter(c => + !newOrder.some(n => n.toLowerCase() === c.toLowerCase()) + ); + + if (reorderedColumns.length > 0) { + orderChanged = true; + console.log("[handleColumnReorder] usedColumns ์ˆœ์„œ ๋ณ€๊ฒฝ", { + compId: comp.id, + before: usedColumns, + after: [...reorderedColumns, ...remainingColumns], + }); + + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + usedColumns: [...reorderedColumns, ...remainingColumns], + }, + }; + } + } + + // componentConfig.columns ์ˆœ์„œ ๋ณ€๊ฒฝ + if (comp.componentConfig?.columns && Array.isArray(comp.componentConfig.columns)) { + const columns = comp.componentConfig.columns as any[]; + + const reorderedColumns = newOrder.map(colName => { + return columns.find((col: any) => { + const name = typeof col === 'string' ? col : (col.field || col.columnName || col.name); + return name?.toLowerCase() === colName.toLowerCase(); + }); + }).filter(Boolean); + + const remainingColumns = columns.filter((col: any) => { + const name = typeof col === 'string' ? col : (col.field || col.columnName || col.name); + return !newOrder.some(n => n.toLowerCase() === name?.toLowerCase()); + }); + + if (reorderedColumns.length > 0) { + orderChanged = true; + console.log("[handleColumnReorder] componentConfig.columns ์ˆœ์„œ ๋ณ€๊ฒฝ", { + compId: comp.id, + before: columns.map((c: any) => typeof c === 'string' ? c : (c.field || c.columnName)), + after: [...reorderedColumns, ...remainingColumns].map((c: any) => typeof c === 'string' ? c : (c.field || c.columnName)), + }); + + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + columns: [...reorderedColumns, ...remainingColumns], + }, + }; + } + } + + return comp; + }); + + if (!orderChanged) { + console.log("[handleColumnReorder] ์ˆœ์„œ ๋ณ€๊ฒฝ ์—†์Œ"); + return; + } + + // 3. ๋ ˆ์ด์•„์›ƒ ์ €์žฅ + console.log("[handleColumnReorder] ๋ ˆ์ด์•„์›ƒ ์ €์žฅ"); + await screenApi.saveLayout(screenId, { + ...currentLayout, + components: updatedComponents, + }); + + console.log("[handleColumnReorder] ์ˆœ์„œ ๋ณ€๊ฒฝ ์ €์žฅ ์™„๋ฃŒ"); + + // ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜์„ ์œ„ํ•ด ์ฝœ๋ฐฑ ํ˜ธ์ถœ + onRefresh?.(); + } catch (error) { + console.error("[handleColumnReorder] ์ˆœ์„œ ๋ณ€๊ฒฝ ์ €์žฅ ์‹คํŒจ:", error); + toast.error("์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + }, [screenId, onRefresh]); + // ํ†ต๊ณ„ ๊ณ„์‚ฐ (layoutItems์˜ ์ปฌ๋Ÿผ ์ˆ˜๋„ ํฌํ•จ) const stats = useMemo(() => { const totalJoins = filterTables.reduce( @@ -612,25 +1931,38 @@ function OverviewTab({
- {/* ๋ฉ”์ธ ํ…Œ์ด๋ธ” */} + {/* ๋ฉ”์ธ ํ…Œ์ด๋ธ” (์•„์ฝ”๋””์–ธ ํ˜•์‹) */}

๋ฉ”์ธ ํ…Œ์ด๋ธ”

{mainTable ? ( -
- -
-
{mainTableLabel || mainTable}
- {mainTableLabel && mainTable !== mainTableLabel && ( -
{mainTable}
- )} -
- - ๋ฉ”์ธ - -
+ a.y - b.y) // ํ™”๋ฉด ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ + .flatMap((item, idx) => + (item.usedColumns || []).map(col => ({ + columnName: col, + fieldLabel: col, // ์ปฌ๋Ÿผ๋ช… ์ž์ฒด๋ฅผ ์‹๋ณ„์ž๋กœ ์‚ฌ์šฉ (UI์—์„œ columnLabel ํ‘œ์‹œ) + order: idx * 100 + (item.usedColumns?.indexOf(col) || 0), // ์ˆœ์„œ ์œ ์ง€ + })) + ) + // ์ค‘๋ณต ์ œ๊ฑฐ (์ฒซ ๋ฒˆ์งธ ๋งคํ•‘๋งŒ ์œ ์ง€) + .filter((mapping, idx, arr) => + arr.findIndex(m => m.columnName.toLowerCase() === mapping.columnName.toLowerCase()) === idx + ) + } + onColumnChange={handleColumnChange} + onColumnReorder={(newOrder) => handleColumnReorder("main", newOrder)} + onJoinSettingSaved={onRefresh} + /> ) : (
๋ฉ”์ธ ํ…Œ์ด๋ธ”์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. @@ -646,13 +1978,49 @@ function OverviewTab({ {filterTables.length > 0 ? (
- {filterTables.map((ft, idx) => ( - - ))} + {filterTables.map((ft, idx) => { + // ์ด ํ•„ํ„ฐ ํ…Œ์ด๋ธ”์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์ปฌ๋Ÿผ ๋งคํ•‘ ์ •๋ณด ์ถ”์ถœ + // 1. layoutItems์˜ usedColumns์—์„œ ์ถ”์ถœ + const usedColumnMappings: ColumnMapping[] = layoutItems + .slice() + .sort((a, b) => a.y - b.y) + .flatMap((item, itemIdx) => + (item.usedColumns || []).map(col => ({ + columnName: col, + fieldLabel: col, + order: itemIdx * 100 + (item.usedColumns?.indexOf(col) || 0), + })) + ); + + // 2. ์กฐ์ธ ์ปฌ๋Ÿผ๋„ ํ•„๋“œ๋กœ ์ถ”๊ฐ€ (ํ™”๋ฉด์—์„œ ์กฐ์ธ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋ฏ€๋กœ) + const joinColumnMappings: ColumnMapping[] = (ft.joinColumnRefs || []).map((ref, refIdx) => ({ + columnName: ref.column, + fieldLabel: ref.column, + order: 1000 + refIdx, // ์กฐ์ธ ์ปฌ๋Ÿผ์€ ํ›„์ˆœ์œ„ + })); + + // 3. ํ•ฉ์น˜๊ณ  ์ค‘๋ณต ์ œ๊ฑฐ + const filterTableColumnMappings: ColumnMapping[] = [...usedColumnMappings, ...joinColumnMappings] + .filter((mapping, i, arr) => + arr.findIndex(m => m.columnName.toLowerCase() === mapping.columnName.toLowerCase()) === i + ); + + return ( + handleColumnReorder("filter", newOrder)} + onJoinSettingSaved={onRefresh} + /> + ); + })}
) : (
@@ -717,6 +2085,43 @@ function FieldMappingTab({ layoutItems, loading, }: FieldMappingTabProps) { + // ํŽธ์ง‘ ๋ชจ๋“œ ์ƒํƒœ + const [isEditMode, setIsEditMode] = useState(false); + // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋ชฉ๋ก (ํŽธ์ง‘์šฉ) + const [tableColumns, setTableColumns] = useState([]); + const [loadingTableColumns, setLoadingTableColumns] = useState(false); + // ํŽธ์ง‘ ์ค‘์ธ ์ปฌ๋Ÿผ ์ •๋ณด + const [editingColumn, setEditingColumn] = useState<{ + componentIdx: number; + columnIdx: number; + currentColumn: string; + } | null>(null); + const [editPopoverOpen, setEditPopoverOpen] = useState(false); + + // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ + const loadTableColumns = useCallback(async () => { + if (!mainTable || tableColumns.length > 0) return; + + setLoadingTableColumns(true); + try { + const result = await tableManagementApi.getColumnList(mainTable); + if (result.success && result.data?.columns) { + setTableColumns(result.data.columns); + } + } catch (error) { + console.error("ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ ์‹คํŒจ:", error); + } finally { + setLoadingTableColumns(false); + } + }, [mainTable, tableColumns.length]); + + // ํŽธ์ง‘ ๋ชจ๋“œ ์ง„์ž… ์‹œ ์ปฌ๋Ÿผ ๋กœ๋“œ + useEffect(() => { + if (isEditMode) { + loadTableColumns(); + } + }, [isEditMode, loadTableColumns]); + // ํ™”๋ฉด ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ปฌ๋Ÿผ ์ •๋ณด ์ถ”์ถœ const componentColumns = useMemo(() => { const result: Array<{ @@ -748,6 +2153,15 @@ function FieldMappingTab({ }); return allColumns.size; }, [componentColumns]); + + // ์ปฌ๋Ÿผ๋ช… โ†’ ํ‘œ์‹œ๋ช… ๋งคํ•‘ (ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์—์„œ ์ถ”์ถœ) + const columnDisplayMap = useMemo(() => { + const map: Record = {}; + tableColumns.forEach((tc) => { + map[tc.columnName] = tc.displayName || tc.columnName; + }); + return map; + }, [tableColumns]); // ์ปดํฌ๋„ŒํŠธ ํƒ€์ž…๋ณ„ ๊ทธ๋ฃนํ•‘ (๊ธฐ์กด fieldMappings์šฉ) const groupedMappings = useMemo(() => { @@ -782,12 +2196,34 @@ function FieldMappingTab({

ํ™”๋ฉด ์ปดํฌ๋„ŒํŠธ๋ณ„ ์ปฌ๋Ÿผ ์‚ฌ์šฉ ํ˜„ํ™ฉ

- ๊ฐ ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + {isEditMode + ? "์ปฌ๋Ÿผ์„ ํด๋ฆญํ•˜์—ฌ ๋งคํ•‘์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." + : "๊ฐ ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค."}

- - ์ด {totalColumns}๊ฐœ ์ปฌ๋Ÿผ - +
+ + ์ด {totalColumns}๊ฐœ ์ปฌ๋Ÿผ + + +
{componentColumns.length === 0 ? ( @@ -800,9 +2236,10 @@ function FieldMappingTab({ {componentColumns.map((comp, idx) => (
-
+ {/* ์ปดํฌ๋„ŒํŠธ ํ—ค๋” */} +
@@ -813,28 +2250,132 @@ function FieldMappingTab({
- {comp.columns.length}๊ฐœ ์ปฌ๋Ÿผ + {comp.columns.length}๊ฐœ ํ•„๋“œ
-
+ + {/* ํ•„๋“œ โ†’ ์ปฌ๋Ÿผ ๋งคํ•‘ ํ…Œ์ด๋ธ” */} +
+ {/* ํ…Œ์ด๋ธ” ํ—ค๋” */} +
+ ํ•„๋“œ๋ช… (ํ™”๋ฉด ํ‘œ์‹œ) + + ์ปฌ๋Ÿผ๋ช… (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค) +
+ + {/* ๋งคํ•‘ ํ–‰๋“ค */} {comp.columns.map((col, cIdx) => { const isJoinColumn = comp.joinColumns.includes(col); + const displayName = columnDisplayMap[col] || col; + const isEditing = editingColumn?.componentIdx === idx && editingColumn?.columnIdx === cIdx; + return ( - - {col} - {isJoinColumn && ( - + {/* ํ•„๋“œ๋ช… (ํ™”๋ฉด ํ‘œ์‹œ) */} +
+ + {displayName} + + {isJoinColumn && ( + + ์กฐ์ธ + + )} +
+ + {/* ํ™”์‚ดํ‘œ */} +
+ +
+ + {/* ์ปฌ๋Ÿผ๋ช… (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค) */} + {isEditMode ? ( + { + if (open) { + setEditingColumn({ componentIdx: idx, columnIdx: cIdx, currentColumn: col }); + } else { + setEditingColumn(null); + } + setEditPopoverOpen(open); + }} + > + + + + + + + + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + + {loadingTableColumns ? ( +
+ +
+ ) : ( + tableColumns.map((tableCol) => ( + { + toast.info(`์ปฌ๋Ÿผ ๋ณ€๊ฒฝ: ${col} โ†’ ${value}`, { + description: "์ €์žฅ ๊ธฐ๋Šฅ์€ ์•„์ง ๊ตฌํ˜„ ์ค‘์ž…๋‹ˆ๋‹ค." + }); + setEditPopoverOpen(false); + setEditingColumn(null); + }} + className="text-xs" + > +
+ + {tableCol.displayName || tableCol.columnName} + + {tableCol.displayName && tableCol.displayName !== tableCol.columnName && ( + + {tableCol.columnName} + + )} +
+ {tableCol.columnName === col && ( + + )} +
+ )) + )} +
+
+
+
+
+ ) : ( + {col} )} -
+
); })}
@@ -1210,6 +2751,627 @@ function DataFlowTab({ ); } +// ============================================================ +// ํƒญ: ์ œ์–ด ๊ด€๋ฆฌ +// ============================================================ + +interface ButtonControlInfo { + id: string; + label: string; + actionType: string; + targetTable?: string; + operations?: string[]; + confirmMessage?: string; + hasDataflowControl?: boolean; + dataflowControlMode?: string; + linkedExternalCall?: { + id: number; + name: string; + }; + linkedFlow?: { + id: number; + name: string; + }; +} + +interface ControlManagementTabProps { + screenId: number; + layoutItems: LayoutItem[]; + loading: boolean; + onRefresh: () => void; +} + +function ControlManagementTab({ + screenId, + layoutItems, + loading: parentLoading, + onRefresh, +}: ControlManagementTabProps) { + const [buttonControls, setButtonControls] = useState([]); + const [externalCalls, setExternalCalls] = useState([]); + const [flows, setFlows] = useState([]); + const [loading, setLoading] = useState(false); + const [expandedButton, setExpandedButton] = useState(null); + const [editingButton, setEditingButton] = useState(null); + const [editedValues, setEditedValues] = useState>({}); + + // ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์กฐํšŒ + const [tableList, setTableList] = useState([]); + + // ๋ฐ์ดํ„ฐ ๋กœ๋“œ + const loadData = useCallback(async () => { + setLoading(true); + try { + // 1. ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์—์„œ ๋ฒ„ํŠผ ์ •๋ณด ์ถ”์ถœ + const layoutResponse = await screenApi.getLayout(screenId); + console.log("[์ œ์–ด๊ด€๋ฆฌ] ๋ ˆ์ด์•„์›ƒ ์‘๋‹ต:", layoutResponse); + + if (layoutResponse?.components) { + const buttons: ButtonControlInfo[] = []; + + // ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฒ„ํŠผ ์ถ”์ถœ (๋‹ค์–‘ํ•œ ํ•„๋“œ ํ™•์ธ) + const extractButtons = (components: any[], depth = 0) => { + for (const comp of components) { + // ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ ํ•„ํ„ฐ๋ง (๋‹ค์–‘ํ•œ ์กฐ๊ฑด ํ™•์ธ) + const isButton = + comp.webType === "button" || + comp.componentType === "button" || + comp.type === "button" || + comp.componentKind?.includes("button") || + comp.widgetType === "button"; + + if (isButton) { + const config = comp.componentConfig || {}; + const webTypeConfig = comp.webTypeConfig || {}; + const action = config.action || {}; + + console.log("[์ œ์–ด๊ด€๋ฆฌ] ๋ฒ„ํŠผ ๋ฐœ๊ฒฌ:", comp); + + buttons.push({ + id: comp.id || comp.componentId || `btn-${buttons.length}`, + label: config.text || comp.label || comp.title || comp.name || "๋ฒ„ํŠผ", + actionType: typeof action === "string" ? action : (action.type || "custom"), + targetTable: config.tableName || webTypeConfig.tableName || comp.tableName, + operations: action.operations || [], + confirmMessage: action.confirmMessage || config.confirmMessage, + hasDataflowControl: webTypeConfig.enableDataflowControl, + dataflowControlMode: webTypeConfig.dataflowConfig?.controlMode, + linkedExternalCall: undefined, // TODO: ์—ฐ๊ฒฐ ์ •๋ณด ์กฐํšŒ + linkedFlow: webTypeConfig.dataflowConfig?.flowConfig ? { + id: webTypeConfig.dataflowConfig.flowConfig.flowId, + name: webTypeConfig.dataflowConfig.flowConfig.flowName, + } : undefined, + }); + } + + // ์ž์‹ ์ปดํฌ๋„ŒํŠธ ์ฒ˜๋ฆฌ (์—ฌ๋Ÿฌ ํ•„๋“œ ํ™•์ธ) + if (comp.children && Array.isArray(comp.children)) { + extractButtons(comp.children, depth + 1); + } + // componentConfig ๋‚ด ์ค‘์ฒฉ๋œ ์ปดํฌ๋„ŒํŠธ ํ™•์ธ + if (comp.componentConfig?.children && Array.isArray(comp.componentConfig.children)) { + extractButtons(comp.componentConfig.children, depth + 1); + } + // items ๋ฐฐ์—ด ํ™•์ธ (์ผ๋ถ€ ๋ ˆ์ด์•„์›ƒ์—์„œ ์‚ฌ์šฉ) + if (comp.items && Array.isArray(comp.items)) { + extractButtons(comp.items, depth + 1); + } + } + }; + + extractButtons(layoutResponse.components); + console.log("[์ œ์–ด๊ด€๋ฆฌ] ์ถ”์ถœ๋œ ๋ฒ„ํŠผ:", buttons); + setButtonControls(buttons); + } + + // 2. ์™ธ๋ถ€ ํ˜ธ์ถœ ๋ชฉ๋ก ์กฐํšŒ + const externalResponse = await ExternalCallConfigAPI.getConfigs({ is_active: "Y" }); + if (externalResponse.success && externalResponse.data) { + setExternalCalls(externalResponse.data); + } + + // 3. ํ”Œ๋กœ์šฐ ๋ชฉ๋ก ์กฐํšŒ + const flowResponse = await getFlowDefinitions({ isActive: true }); + if (flowResponse.success && flowResponse.data) { + setFlows(flowResponse.data); + } + + // 4. ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์กฐํšŒ + const tableResponse = await tableManagementApi.getTableList(); + if (tableResponse.success && tableResponse.data) { + setTableList(tableResponse.data); + } + } catch (error) { + console.error("์ œ์–ด ๊ด€๋ฆฌ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:", error); + toast.error("๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ"); + } finally { + setLoading(false); + } + }, [screenId]); + + useEffect(() => { + loadData(); + }, [loadData]); + + // ๋ฒ„ํŠผ ์„ค์ • ์ €์žฅ + const handleSaveButton = async (buttonId: string) => { + const values = editedValues[buttonId]; + if (!values) return; + + try { + // ๋ ˆ์ด์•„์›ƒ์—์„œ ํ•ด๋‹น ๋ฒ„ํŠผ ์ฐพ์•„์„œ ์—…๋ฐ์ดํŠธ + const layoutResponse = await screenApi.getLayout(screenId); + if (!layoutResponse?.components) { + toast.error("๋ ˆ์ด์•„์›ƒ์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"); + return; + } + + // ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ + const updateButton = (components: any[]): boolean => { + for (const comp of components) { + if ((comp.id === buttonId || comp.componentId === buttonId) && + (comp.webType === "button" || comp.componentKind?.includes("button"))) { + // componentConfig ์—…๋ฐ์ดํŠธ + if (!comp.componentConfig) comp.componentConfig = {}; + if (!comp.componentConfig.action) comp.componentConfig.action = {}; + + if (values.targetTable) { + comp.componentConfig.tableName = values.targetTable; + } + if (values.confirmMessage !== undefined) { + comp.componentConfig.action.confirmMessage = values.confirmMessage; + } + if (values.operations) { + comp.componentConfig.action.operations = values.operations; + } + + // webTypeConfig ์—…๋ฐ์ดํŠธ (ํ”Œ๋กœ์šฐ ์—ฐ๋™) + if (!comp.webTypeConfig) comp.webTypeConfig = {}; + if (values.linkedFlowId) { + comp.webTypeConfig.enableDataflowControl = true; + comp.webTypeConfig.dataflowConfig = { + controlMode: "flow", + flowConfig: { + flowId: values.linkedFlowId, + flowName: flows.find(f => f.id === values.linkedFlowId)?.name || "", + executionTiming: values.flowTiming || "after", + }, + }; + } else if (values.linkedFlowId === null) { + // ํ”Œ๋กœ์šฐ ์—ฐ๋™ ํ•ด์ œ + comp.webTypeConfig.enableDataflowControl = false; + delete comp.webTypeConfig.dataflowConfig; + } + + return true; + } + + if (comp.children && Array.isArray(comp.children)) { + if (updateButton(comp.children)) return true; + } + } + return false; + }; + + if (updateButton(layoutResponse.components)) { + // ๋ ˆ์ด์•„์›ƒ ์ €์žฅ + await screenApi.saveLayout(screenId, layoutResponse); + toast.success("๋ฒ„ํŠผ ์„ค์ •์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); + setEditingButton(null); + setEditedValues(prev => { + const next = { ...prev }; + delete next[buttonId]; + return next; + }); + loadData(); + onRefresh(); + } else { + toast.error("๋ฒ„ํŠผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"); + } + } catch (error) { + console.error("๋ฒ„ํŠผ ์„ค์ • ์ €์žฅ ์‹คํŒจ:", error); + toast.error("์ €์žฅ ์‹คํŒจ"); + } + }; + + // ์•ก์…˜ ํƒ€์ž… ๋ผ๋ฒจ + const getActionTypeLabel = (type: string) => { + const labels: Record = { + save: "์ €์žฅ", + delete: "์‚ญ์ œ", + refresh: "์ƒˆ๋กœ๊ณ ์นจ", + reset: "์ดˆ๊ธฐํ™”", + submit: "์ œ์ถœ", + cancel: "์ทจ์†Œ", + close: "๋‹ซ๊ธฐ", + navigate: "์ด๋™", + popup: "ํŒ์—…", + custom: "์ปค์Šคํ…€", + }; + return labels[type] || type; + }; + + // ์•ก์…˜ ํƒ€์ž… ์ƒ‰์ƒ + const getActionTypeColor = (type: string) => { + switch (type) { + case "save": + return "bg-green-100 text-green-700"; + case "delete": + return "bg-red-100 text-red-700"; + case "refresh": + return "bg-blue-100 text-blue-700"; + case "submit": + return "bg-purple-100 text-purple-700"; + default: + return "bg-gray-100 text-gray-700"; + } + }; + + if (loading || parentLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* ๋ฒ„ํŠผ ์•ก์…˜ ์„ค์ • */} +
+
+ + ๋ฒ„ํŠผ ์•ก์…˜ ์„ค์ • + + {buttonControls.length}๊ฐœ + +
+ +
+ {buttonControls.length === 0 ? ( +
+ +

๋ฒ„ํŠผ์ด ์—†์Šต๋‹ˆ๋‹ค

+

ํ™”๋ฉด ๋””์ž์ด๋„ˆ์—์„œ ๋ฒ„ํŠผ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”

+
+ ) : ( +
+ {buttonControls.map((btn) => ( +
+ {/* ๋ฒ„ํŠผ ํ—ค๋” */} +
setExpandedButton(expandedButton === btn.id ? null : btn.id)} + > + {expandedButton === btn.id ? ( + + ) : ( + + )} + [{btn.label}] + + {getActionTypeLabel(btn.actionType)} + + {btn.targetTable && ( + + โ†’ {btn.targetTable} + + )} + {btn.hasDataflowControl && ( + + + ์ œ์–ด ์—ฐ๋™ + + )} + +
+ + {/* ๋ฒ„ํŠผ ์ƒ์„ธ (ํ™•์žฅ ์‹œ) */} + {expandedButton === btn.id && ( +
+
+ {/* ๋Œ€์ƒ ํ…Œ์ด๋ธ” */} +
+ + {editingButton === btn.id ? ( + + ) : ( + + {btn.targetTable || ๋ฏธ์„ค์ •} + + )} +
+ + {/* ํ™•์ธ ๋ฉ”์‹œ์ง€ */} +
+ + {editingButton === btn.id ? ( + setEditedValues(prev => ({ + ...prev, + [btn.id]: { ...prev[btn.id], confirmMessage: e.target.value } + }))} + className="h-7 text-xs" + placeholder="์˜ˆ: ์ •๋ง ์ €์žฅํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + /> + ) : ( + + {btn.confirmMessage || ์—†์Œ} + + )} +
+ + {/* ํ”Œ๋กœ์šฐ ์—ฐ๋™ */} +
+ + {editingButton === btn.id ? ( + + ) : ( + + {btn.linkedFlow ? ( + + + {btn.linkedFlow.name} + + ) : ( + ์—†์Œ + )} + + )} +
+ + {/* ํŽธ์ง‘/์ €์žฅ ๋ฒ„ํŠผ */} +
+ {editingButton === btn.id ? ( + <> + + + + ) : ( + + )} +
+
+
+ )} +
+ ))} +
+ )} +
+
+ + {/* ์™ธ๋ถ€ ์—ฐ๋™ */} +
+
+ + ์™ธ๋ถ€ ์—ฐ๋™ + + {externalCalls.filter(e => e.is_active === "Y").length}๊ฐœ ํ™œ์„ฑ + +
+ +
+ {externalCalls.length === 0 ? ( +
+ +

์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ •์ด ์—†์Šต๋‹ˆ๋‹ค

+ +
+ ) : ( +
+ {externalCalls.slice(0, 5).map((call) => ( +
+ + {call.call_type} + + {call.config_name} + +
+ ))} + {externalCalls.length > 5 && ( +
+ +
+ )} +
+ )} +
+ +
+
+ + ๋ฒ„ํŠผ์— ์™ธ๋ถ€ ํ˜ธ์ถœ์„ ์—ฐ๊ฒฐํ•˜๋ ค๋ฉด ๋ฒ„ํŠผ ํŽธ์ง‘์—์„œ ์„ค์ •ํ•˜์„ธ์š” +
+
+
+ + {/* ํ”Œ๋กœ์šฐ ์—ฐ๋™ */} +
+
+ + ํ”Œ๋กœ์šฐ ์—ฐ๋™ + + {flows.length}๊ฐœ + +
+ +
+ {flows.length === 0 ? ( +
+ +

ํ”Œ๋กœ์šฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค

+ +
+ ) : ( +
+ {flows.slice(0, 5).map((flow) => ( +
+ + ํ”Œ๋กœ์šฐ + + {flow.name} + + {flow.tableName} + + +
+ ))} + {flows.length > 5 && ( +
+ +
+ )} +
+ )} +
+ +
+
+ + ๋ฒ„ํŠผ์— ํ”Œ๋กœ์šฐ๋ฅผ ์—ฐ๊ฒฐํ•˜๋ ค๋ฉด ๋ฒ„ํŠผ ํŽธ์ง‘์—์„œ ์„ค์ •ํ•˜์„ธ์š” +
+
+
+
+ ); +} + // ============================================================ // ํƒญ 4: ํ™”๋ฉด ํ”„๋ฆฌ๋ทฐ (iframe) // ============================================================ @@ -1218,11 +3380,52 @@ interface PreviewTabProps { screenId: number; screenName: string; companyCode?: string; + iframeKey?: number; // iframe ์ƒˆ๋กœ๊ณ ์นจ์šฉ ํ‚ค } -function PreviewTab({ screenId, screenName, companyCode }: PreviewTabProps) { +function PreviewTab({ screenId, screenName, companyCode, iframeKey = 0 }: PreviewTabProps) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const containerRef = useRef(null); + + // ํ™”๋ฉด ๋””์ž์ธ ํฌ๊ธฐ (๋ชจ๋‹ฌ ํ”„๋ฆฌ๋ทฐ์— ๋งž์ถ˜ ํฌ๊ธฐ) + const designWidth = 1200; + const designHeight = 750; + + // ์ปจํ…Œ์ด๋„ˆ์— ๋งž๋Š” ์ดˆ๊ธฐ ์Šค์ผ€์ผ ๊ณ„์‚ฐ + const [initialScale, setInitialScale] = useState(0.7); + + // ์ปจํ…Œ์ด๋„ˆ ํฌ๊ธฐ์— ๋งž์ถฐ ์ดˆ๊ธฐ ์Šค์ผ€์ผ ๊ณ„์‚ฐ + useEffect(() => { + const updateInitialScale = () => { + if (containerRef.current) { + const containerWidth = containerRef.current.offsetWidth; + const containerHeight = containerRef.current.offsetHeight; + + // ์—ฌ๋ฐฑ 5px์”ฉ๋งŒ ์ ์šฉํ•˜์—ฌ ๊ฝ‰ ์ฐจ๊ฒŒ + const scaleX = (containerWidth - 10) / designWidth; + const scaleY = (containerHeight - 10) / designHeight; + const newScale = Math.min(scaleX, scaleY); + + setInitialScale(newScale); + } + }; + + // ์ดˆ๊ธฐ ์ธก์ • (์•ฝ๊ฐ„์˜ ๋”œ๋ ˆ์ด) + const timer = setTimeout(updateInitialScale, 200); + + // ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ฐ์ง€ + const resizeObserver = new ResizeObserver(updateInitialScale); + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + return () => { + clearTimeout(timer); + resizeObserver.disconnect(); + }; + }, []); + // ํ™”๋ฉด URL ์ƒ์„ฑ (preview=true๋กœ ์‚ฌ์ด๋“œ๋ฐ” ์—†์ด ํ™”๋ฉด๋งŒ ํ‘œ์‹œ, company_code ์ „๋‹ฌ) const previewUrl = useMemo(() => { @@ -1253,19 +3456,17 @@ function PreviewTab({ screenId, screenName, companyCode }: PreviewTabProps) { }; return ( -
- {/* ์ƒ๋‹จ ํˆด๋ฐ” */} -
-
- - ํ™”๋ฉด ํ”„๋ฆฌ๋ทฐ - - Screen ID: {screenId} - +
+ {/* ์ƒ๋‹จ ํˆด๋ฐ” (์ตœ์†Œํ™”) */} +
+
+ + {screenName} + (ํœ : ํ™•๋Œ€/์ถ•์†Œ, ๋“œ๋ž˜๊ทธ: ์ด๋™)
-
+
-
- {/* iframe ์˜์—ญ */} -
+ {/* iframe ์˜์—ญ - Ctrl+ํœ ๋กœ ํ™•๋Œ€/์ถ•์†Œ, ๋‚ด๋ถ€ ๋ฒ„ํŠผ/๋ชฉ๋ก ํด๋ฆญ ๊ฐ€๋Šฅ */} +
{loading && (
@@ -1318,21 +3521,109 @@ function PreviewTab({ screenId, screenName, companyCode }: PreviewTabProps) {
) : ( -