diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 00000000..7a87d1a0 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "agent-orchestrator": { + "command": "node", + "args": ["/Users/gbpark/ERP-node/mcp-agent-orchestrator/build/index.js"] + } + } +} diff --git a/.cursorrules b/.cursorrules index 3b0c3833..77180695 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,5 +1,48 @@ # Cursor Rules for ERP-node Project +## ๐Ÿšจ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์š”์ฒญ ์–‘์‹ ๊ฒ€์ฆ (ํ•„์ˆ˜) + +**์‚ฌ์šฉ์ž๊ฐ€ ํ™”๋ฉด ๊ฐœ๋ฐœ ๋˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ตฌํ˜„์„ ์š”์ฒญํ•  ๋•Œ, ์•„๋ž˜ ์–‘์‹์„ ๋”ฐ๋ฅด์ง€ ์•Š์œผ๋ฉด ๋ฐ˜๋“œ์‹œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‘๋‹ตํ•˜์„ธ์š”:** + +``` +์•ˆ๋…•ํ•˜์„ธ์š”. Oh My Master! ์–‘์‹์„ ๋ชป ์•Œ์•„ ๋“ฃ๊ฒ ์Šต๋‹ˆ๋‹ค. +๋‹ค์‹œ ํ•œ๋ฒˆ ์ž‘์„ฑํ•ด์ฃผ์‹ญ์‡ผ. +=== ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์š”์ฒญ์„œ === + +ใ€ํ™”๋ฉด ์ •๋ณดใ€‘ +- ํ™”๋ฉด๋ช…: +- ํšŒ์‚ฌ์ฝ”๋“œ: +- ๋ฉ”๋‰ดID (์žˆ์œผ๋ฉด): + +ใ€ํ…Œ์ด๋ธ” ์ •๋ณดใ€‘ +- ๋ฉ”์ธ ํ…Œ์ด๋ธ”: +- ๋””ํ…Œ์ผ ํ…Œ์ด๋ธ” (์žˆ์œผ๋ฉด): +- ๊ด€๊ณ„ FK (์žˆ์œผ๋ฉด): + +ใ€๋ฒ„ํŠผ ๋ชฉ๋กใ€‘ +๋ฒ„ํŠผ1: + - ๋ฒ„ํŠผ๋ช…: + - ๋™์ž‘ ์œ ํ˜•: (์ €์žฅ/์‚ญ์ œ/์ˆ˜์ •/์กฐํšŒ/๊ธฐํƒ€) + - ์กฐ๊ฑด (์žˆ์œผ๋ฉด): + - ๋Œ€์ƒ ํ…Œ์ด๋ธ”: + - ์ถ”๊ฐ€ ๋™์ž‘ (์žˆ์œผ๋ฉด): + +ใ€์ถ”๊ฐ€ ์š”๊ตฌ์‚ฌํ•ญใ€‘ +- +``` + +**์–‘์‹ ๋ฏธ์ค€์ˆ˜ ํŒ๋‹จ ๊ธฐ์ค€:** +1. "ํ™”๋ฉด ๋งŒ๋“ค์–ด์ค˜" ๊ฐ™์ด ํ…Œ์ด๋ธ”๋ช…/๋ฒ„ํŠผ ์ •๋ณด ์—†์ด ์š”์ฒญ +2. "์ €์žฅํ•˜๋ฉด ์ €์žฅํ•ด์ค˜" ๊ฐ™์ด ๊ตฌ์ฒด์ ์ธ ํ…Œ์ด๋ธ”/๋กœ์ง ์„ค๋ช… ์—†์Œ +3. "์ด์ „์ด๋ž‘ ๋น„์Šทํ•˜๊ฒŒ" ๊ฐ™์ด ๋ชจํ˜ธํ•œ ์ฐธ์กฐ +4. ๋ฒ„ํŠผ๋ณ„ ์กฐ๊ฑด/๋™์ž‘์ด ๋ช…์‹œ๋˜์ง€ ์•Š์Œ + +**์–‘์‹ ๋ฏธ์ค€์ˆ˜ ์‹œ ์ ˆ๋Œ€ ์ž‘์—… ์ง„ํ–‰ํ•˜์ง€ ๋ง๊ณ , ์œ„ ์–‘์‹์„ ๋ณด์—ฌ์ฃผ๋ฉฐ ๋‹ค์‹œ ์ž‘์„ฑํ•˜๋ผ๊ณ  ์š”์ฒญํ•˜์„ธ์š”.** + +**์ƒ์„ธ ๊ฐ€์ด๋“œ**: [ํ™”๋ฉด๊ฐœ๋ฐœ_ํ‘œ์ค€_๊ฐ€์ด๋“œ.md](docs/screen-implementation-guide/ํ™”๋ฉด๊ฐœ๋ฐœ_ํ‘œ์ค€_๊ฐ€์ด๋“œ.md) + +--- + ## ๐Ÿšจ ์ตœ์šฐ์„  ๋ณด์•ˆ ๊ทœ์น™: ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ **๋ชจ๋“  ์ฝ”๋“œ ์ž‘์„ฑ/์ˆ˜์ • ์™„๋ฃŒ ํ›„ ๋ฐ˜๋“œ์‹œ ๋‹ค์Œ ํŒŒ์ผ์„ ํ™•์ธํ•˜์„ธ์š”:** diff --git a/backend-node/package-lock.json b/backend-node/package-lock.json index 7e1108c3..43b698d2 100644 --- a/backend-node/package-lock.json +++ b/backend-node/package-lock.json @@ -1044,6 +1044,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", @@ -2371,6 +2372,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", @@ -3474,6 +3476,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" } @@ -3710,6 +3713,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", @@ -3927,6 +3931,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4453,6 +4458,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -5663,6 +5669,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", @@ -7425,6 +7432,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -8394,7 +8402,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" }, @@ -9283,6 +9290,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", @@ -10133,7 +10141,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" } @@ -10942,6 +10949,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", @@ -11047,6 +11055,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/app.ts b/backend-node/src/app.ts index 771ab80d..1fbefea5 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -10,6 +10,43 @@ import { logger } from "./utils/logger"; import { errorHandler } from "./middleware/errorHandler"; import { refreshTokenIfNeeded } from "./middleware/authMiddleware"; +// ============================================ +// ํ”„๋กœ์„ธ์Šค ๋ ˆ๋ฒจ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ (์„œ๋ฒ„ ํฌ๋ž˜์‹œ ๋ฐฉ์ง€) +// ============================================ + +// ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ Promise ๊ฑฐ๋ถ€ ํ•ธ๋“ค๋Ÿฌ +process.on("unhandledRejection", (reason: Error | any, promise: Promise) => { + logger.error("โš ๏ธ Unhandled Promise Rejection:", { + reason: reason?.message || reason, + stack: reason?.stack, + }); + // ํ”„๋กœ์„ธ์Šค๋ฅผ ์ข…๋ฃŒํ•˜์ง€ ์•Š๊ณ  ๋กœ๊น…๋งŒ ์ˆ˜ํ–‰ + // ์‹ฌ๊ฐํ•œ ์—๋Ÿฌ์˜ ๊ฒฝ์šฐ graceful shutdown ๊ณ ๋ ค +}); + +// ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ +process.on("uncaughtException", (error: Error) => { + logger.error("๐Ÿ”ฅ Uncaught Exception:", { + message: error.message, + stack: error.stack, + }); + // ์˜ˆ์™ธ ๋ฐœ์ƒ ํ›„์—๋„ ์„œ๋ฒ„๋ฅผ ์œ ์ง€ํ•˜๋˜, ์ƒํƒœ๊ฐ€ ๋ถˆ์•ˆ์ •ํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ฃผ์˜ + // ์‹ฌ๊ฐํ•œ ์—๋Ÿฌ์˜ ๊ฒฝ์šฐ graceful shutdown ํ›„ ์žฌ์‹œ์ž‘ ๊ถŒ์žฅ +}); + +// SIGTERM ์‹œ๊ทธ๋„ ์ฒ˜๋ฆฌ (Docker/Kubernetes ํ™˜๊ฒฝ) +process.on("SIGTERM", () => { + logger.info("๐Ÿ“ด SIGTERM ์‹œ๊ทธ๋„ ์ˆ˜์‹ , graceful shutdown ์‹œ์ž‘..."); + // ์—ฌ๊ธฐ์„œ ์—ฐ๊ฒฐ ํ’€ ์ •๋ฆฌ ๋“ฑ cleanup ๋กœ์ง ์ถ”๊ฐ€ ๊ฐ€๋Šฅ + process.exit(0); +}); + +// SIGINT ์‹œ๊ทธ๋„ ์ฒ˜๋ฆฌ (Ctrl+C) +process.on("SIGINT", () => { + logger.info("๐Ÿ“ด SIGINT ์‹œ๊ทธ๋„ ์ˆ˜์‹ , graceful shutdown ์‹œ์ž‘..."); + process.exit(0); +}); + // ๋ผ์šฐํ„ฐ ์ž„ํฌํŠธ import authRoutes from "./routes/authRoutes"; import adminRoutes from "./routes/adminRoutes"; @@ -64,6 +101,7 @@ import yardLayoutRoutes from "./routes/yardLayoutRoutes"; // 3D ํ•„๋“œ import digitalTwinRoutes from "./routes/digitalTwinRoutes"; // ๋””์ง€ํ„ธ ํŠธ์œˆ (์•ผ๋“œ ๊ด€์ œ) import flowRoutes from "./routes/flowRoutes"; // ํ”Œ๋กœ์šฐ ๊ด€๋ฆฌ import flowExternalDbConnectionRoutes from "./routes/flowExternalDbConnectionRoutes"; // ํ”Œ๋กœ์šฐ ์ „์šฉ ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ +import scheduleRoutes from "./routes/scheduleRoutes"; // ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ import workHistoryRoutes from "./routes/workHistoryRoutes"; // ์ž‘์—… ์ด๋ ฅ ๊ด€๋ฆฌ import tableHistoryRoutes from "./routes/tableHistoryRoutes"; // ํ…Œ์ด๋ธ” ๋ณ€๊ฒฝ ์ด๋ ฅ ์กฐํšŒ import roleRoutes from "./routes/roleRoutes"; // ๊ถŒํ•œ ๊ทธ๋ฃน ๊ด€๋ฆฌ @@ -246,6 +284,7 @@ app.use("/api/yard-layouts", yardLayoutRoutes); // 3D ํ•„๋“œ app.use("/api/digital-twin", digitalTwinRoutes); // ๋””์ง€ํ„ธ ํŠธ์œˆ (์•ผ๋“œ ๊ด€์ œ) app.use("/api/flow-external-db", flowExternalDbConnectionRoutes); // ํ”Œ๋กœ์šฐ ์ „์šฉ ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ app.use("/api/flow", flowRoutes); // ํ”Œ๋กœ์šฐ ๊ด€๋ฆฌ (๋งˆ์ง€๋ง‰์— ๋“ฑ๋กํ•˜์—ฌ ๋‹ค๋ฅธ ๋ผ์šฐํŠธ์™€ ์ถฉ๋Œ ๋ฐฉ์ง€) +app.use("/api/schedule", scheduleRoutes); // ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ app.use("/api/work-history", workHistoryRoutes); // ์ž‘์—… ์ด๋ ฅ ๊ด€๋ฆฌ app.use("/api/table-history", tableHistoryRoutes); // ํ…Œ์ด๋ธ” ๋ณ€๊ฒฝ ์ด๋ ฅ ์กฐํšŒ app.use("/api/roles", roleRoutes); // ๊ถŒํ•œ ๊ทธ๋ฃน ๊ด€๋ฆฌ diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts index a530cf15..a89e50d1 100644 --- a/backend-node/src/controllers/adminController.ts +++ b/backend-node/src/controllers/adminController.ts @@ -1461,11 +1461,8 @@ async function cleanupMenuRelatedData(menuObjid: number): Promise { [menuObjid] ); - // 4. numbering_rules์—์„œ menu_objid๋ฅผ NULL๋กœ ์„ค์ • - await query( - `UPDATE numbering_rules SET menu_objid = NULL WHERE menu_objid = $1`, - [menuObjid] - ); + // 4. numbering_rules: ์ƒˆ ์Šคํ‚ค๋งˆ์—์„œ๋Š” ๋ฉ”๋‰ด์™€ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์Œ (์Šคํ‚ต) + // ์ƒˆ ์Šคํ‚ค๋งˆ: table_name + column_name + company_code ๊ธฐ๋ฐ˜ // 5. rel_menu_auth์—์„œ ๊ด€๋ จ ๊ถŒํ•œ ์‚ญ์ œ await query( diff --git a/backend-node/src/controllers/categoryTreeController.ts b/backend-node/src/controllers/categoryTreeController.ts index de6a8e2a..ec7ef92b 100644 --- a/backend-node/src/controllers/categoryTreeController.ts +++ b/backend-node/src/controllers/categoryTreeController.ts @@ -5,9 +5,13 @@ import { Router, Request, Response } from "express"; import { categoryTreeService, CreateCategoryValueInput, UpdateCategoryValueInput } from "../services/categoryTreeService"; import { logger } from "../utils/logger"; +import { authenticateToken } from "../middleware/authMiddleware"; const router = Router(); +// ๋ชจ๋“  ๋ผ์šฐํŠธ์— ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ์ ์šฉ +router.use(authenticateToken); + // ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ํƒ€์ž… interface AuthenticatedRequest extends Request { user?: { diff --git a/backend-node/src/controllers/fileController.ts b/backend-node/src/controllers/fileController.ts index d4e8d0cf..66418099 100644 --- a/backend-node/src/controllers/fileController.ts +++ b/backend-node/src/controllers/fileController.ts @@ -431,7 +431,7 @@ export const deleteFile = async ( // ํŒŒ์ผ ์ •๋ณด ์กฐํšŒ const fileRecord = await queryOne( `SELECT * FROM attach_file_info WHERE objid = $1`, - [parseInt(objid)] + [objid] ); if (!fileRecord) { @@ -460,7 +460,7 @@ export const deleteFile = async ( // ํŒŒ์ผ ์ƒํƒœ๋ฅผ DELETED๋กœ ๋ณ€๊ฒฝ (๋…ผ๋ฆฌ์  ์‚ญ์ œ) await query( "UPDATE attach_file_info SET status = $1 WHERE objid = $2", - ["DELETED", parseInt(objid)] + ["DELETED", objid] ); // ๐Ÿ†• ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ: ํ•ด๋‹น ํ–‰์˜ attachments ์ปฌ๋Ÿผ ์ž๋™ ์—…๋ฐ์ดํŠธ @@ -708,6 +708,40 @@ export const getComponentFiles = async ( ); } + // 3. ๋ ˆ์ฝ”๋“œ์˜ ์ปฌ๋Ÿผ ๊ฐ’์œผ๋กœ ํŒŒ์ผ ์ง์ ‘ ์กฐํšŒ (์ˆ˜์ • ๋ชจ๋‹ฌ์—์„œ ๊ธฐ์กด ํŒŒ์ผ ๋กœ๋“œ) + // target_objid ๋งค์นญ์ด ์•ˆ ๋  ๋•Œ, ํ…Œ์ด๋ธ” ๋ ˆ์ฝ”๋“œ์˜ ์ปฌ๋Ÿผ ๊ฐ’(ํŒŒ์ผ objid)์œผ๋กœ ์ง์ ‘ ์ฐพ๊ธฐ + if (dataFiles.length === 0 && templateFiles.length === 0 && tableName && recordId && columnName) { + try { + // ๋ ˆ์ฝ”๋“œ์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ ๊ฐ’ ์กฐํšŒ (ํŒŒ์ผ objid๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ์„ ์ˆ˜ ์žˆ์Œ) + const safeTable = String(tableName).replace(/[^a-zA-Z0-9_]/g, ""); + const safeColumn = String(columnName).replace(/[^a-zA-Z0-9_]/g, ""); + const recordResult = await query( + `SELECT "${safeColumn}" FROM "${safeTable}" WHERE id = $1 LIMIT 1`, + [recordId] + ); + + if (recordResult.length > 0 && recordResult[0][safeColumn]) { + const columnValue = String(recordResult[0][safeColumn]); + // ์ˆซ์ž๊ฐ’์ธ ๊ฒฝ์šฐ ํŒŒ์ผ objid๋กœ ๊ฐ„์ฃผํ•˜๊ณ  ์กฐํšŒ + if (/^\d+$/.test(columnValue)) { + console.log("๐Ÿ” [getComponentFiles] ๋ ˆ์ฝ”๋“œ ์ปฌ๋Ÿผ ๊ฐ’์œผ๋กœ ํŒŒ์ผ ์กฐํšŒ:", { table: safeTable, column: safeColumn, fileObjid: columnValue }); + const directFiles = await query( + `SELECT * FROM attach_file_info + WHERE objid = $1 AND status = $2 + ORDER BY regdate DESC`, + [columnValue, "ACTIVE"] + ); + if (directFiles.length > 0) { + console.log("โœ… [getComponentFiles] ๋ ˆ์ฝ”๋“œ ์ปฌ๋Ÿผ ๊ฐ’์œผ๋กœ ํŒŒ์ผ ์ฐพ์Œ:", directFiles.length, "๊ฑด"); + dataFiles = directFiles; + } + } + } + } catch (lookupError) { + console.warn("โš ๏ธ [getComponentFiles] ๋ ˆ์ฝ”๋“œ ์ปฌ๋Ÿผ ๊ฐ’ ์กฐํšŒ ์‹คํŒจ:", lookupError); + } + } + // ํŒŒ์ผ ์ •๋ณด ํฌ๋งทํŒ… ํ•จ์ˆ˜ const formatFileInfo = (file: any, isTemplate: boolean = false) => ({ objid: file.objid.toString(), @@ -782,7 +816,7 @@ export const previewFile = async ( const fileRecord = await queryOne( "SELECT * FROM attach_file_info WHERE objid = $1 LIMIT 1", - [parseInt(objid)] + [objid] ); if (!fileRecord || fileRecord.status !== "ACTIVE") { @@ -793,8 +827,9 @@ export const previewFile = async ( return; } - // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ํšŒ์‚ฌ ์ฝ”๋“œ ์ผ์น˜ ์—ฌ๋ถ€ ํ™•์ธ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ œ์™ธ) - if (companyCode !== "*" && fileRecord.company_code !== companyCode) { + // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ํšŒ์‚ฌ ์ฝ”๋“œ ์ผ์น˜ ์—ฌ๋ถ€ ํ™•์ธ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ๋ฐ ๊ณต๊ฐœ ์ ‘๊ทผ ์ œ์™ธ) + // ๊ณต๊ฐœ ์ ‘๊ทผ(req.user๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ)์€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ—ˆ์šฉ (์ด๋ฏธ์ง€ ํ‘œ์‹œ์šฉ) + if (companyCode && companyCode !== "*" && fileRecord.company_code !== companyCode) { console.warn("โš ๏ธ ๋‹ค๋ฅธ ํšŒ์‚ฌ ํŒŒ์ผ ์ ‘๊ทผ ์‹œ๋„:", { userId: req.user?.userId, userCompanyCode: companyCode, @@ -920,7 +955,7 @@ export const downloadFile = async ( const fileRecord = await queryOne( `SELECT * FROM attach_file_info WHERE objid = $1`, - [parseInt(objid)] + [objid] ); if (!fileRecord || fileRecord.status !== "ACTIVE") { @@ -1211,7 +1246,7 @@ export const setRepresentativeFile = async ( // ํŒŒ์ผ ์กด์žฌ ์—ฌ๋ถ€ ๋ฐ ๊ถŒํ•œ ํ™•์ธ const fileRecord = await queryOne( `SELECT * FROM attach_file_info WHERE objid = $1 AND status = $2`, - [parseInt(objid), "ACTIVE"] + [objid, "ACTIVE"] ); if (!fileRecord) { @@ -1236,7 +1271,7 @@ export const setRepresentativeFile = async ( `UPDATE attach_file_info SET is_representative = false WHERE target_objid = $1 AND objid != $2`, - [fileRecord.target_objid, parseInt(objid)] + [fileRecord.target_objid, objid] ); // ์„ ํƒํ•œ ํŒŒ์ผ์„ ๋Œ€ํ‘œ ํŒŒ์ผ๋กœ ์„ค์ • @@ -1244,7 +1279,7 @@ export const setRepresentativeFile = async ( `UPDATE attach_file_info SET is_representative = true WHERE objid = $1`, - [parseInt(objid)] + [objid] ); res.json({ @@ -1260,5 +1295,56 @@ export const setRepresentativeFile = async ( } }; +/** + * ํŒŒ์ผ ์ •๋ณด ์กฐํšŒ (๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋งŒ, ํŒŒ์ผ ๋‚ด์šฉ ์—†์Œ) + * ๊ณต๊ฐœ ์ ‘๊ทผ ํ—ˆ์šฉ + */ +export const getFileInfo = async (req: Request, res: Response) => { + try { + const { objid } = req.params; + + if (!objid) { + return res.status(400).json({ + success: false, + message: "ํŒŒ์ผ ID๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + }); + } + + // ํŒŒ์ผ ์ •๋ณด ์กฐํšŒ + const fileRecord = await queryOne( + `SELECT objid, real_file_name, file_size, file_ext, file_path, regdate, is_representative + FROM attach_file_info + WHERE objid = $1 AND status = 'ACTIVE'`, + [objid] + ); + + if (!fileRecord) { + return res.status(404).json({ + success: false, + message: "ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + }); + } + + res.json({ + success: true, + data: { + objid: fileRecord.objid.toString(), + realFileName: fileRecord.real_file_name, + fileSize: fileRecord.file_size, + fileExt: fileRecord.file_ext, + filePath: fileRecord.file_path, + regdate: fileRecord.regdate, + isRepresentative: fileRecord.is_representative, + }, + }); + } catch (error) { + console.error("ํŒŒ์ผ ์ •๋ณด ์กฐํšŒ ์˜ค๋ฅ˜:", error); + res.status(500).json({ + success: false, + message: "ํŒŒ์ผ ์ •๋ณด ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } +}; + // Multer ๋ฏธ๋“ค์›จ์–ด export export const uploadMiddleware = upload.array("files", 10); // ์ตœ๋Œ€ 10๊ฐœ ํŒŒ์ผ diff --git a/backend-node/src/controllers/numberingRuleController.ts b/backend-node/src/controllers/numberingRuleController.ts index f5cbc91a..d307b41a 100644 --- a/backend-node/src/controllers/numberingRuleController.ts +++ b/backend-node/src/controllers/numberingRuleController.ts @@ -3,392 +3,545 @@ */ import { Router, Response } from "express"; -import { authenticateToken, AuthenticatedRequest } from "../middleware/authMiddleware"; +import { + authenticateToken, + AuthenticatedRequest, +} from "../middleware/authMiddleware"; import { numberingRuleService } from "../services/numberingRuleService"; import { logger } from "../utils/logger"; const router = Router(); // ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ (์ „์ฒด) -router.get("/", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; +router.get( + "/", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; - try { - const rules = await numberingRuleService.getRuleList(companyCode); - return res.json({ success: true, data: rules }); - } catch (error: any) { - logger.error("๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, error: error.message }); + try { + const rules = await numberingRuleService.getRuleList(companyCode); + return res.json({ success: true, data: rules }); + } catch (error: any) { + logger.error("๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, error: error.message }); + } } -}); +); // ๋ฉ”๋‰ด๋ณ„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ทœ์น™ ์กฐํšŒ -router.get("/available/:menuObjid?", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; - const menuObjid = req.params.menuObjid ? parseInt(req.params.menuObjid) : undefined; +router.get( + "/available/:menuObjid?", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const menuObjid = req.params.menuObjid + ? parseInt(req.params.menuObjid) + : undefined; - logger.info("๋ฉ”๋‰ด๋ณ„ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์š”์ฒญ", { menuObjid, companyCode }); + logger.info("๋ฉ”๋‰ด๋ณ„ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์š”์ฒญ", { menuObjid, companyCode }); - try { - const rules = await numberingRuleService.getAvailableRulesForMenu(companyCode, menuObjid); - - logger.info("โœ… ๋ฉ”๋‰ด๋ณ„ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์„ฑ๊ณต (์ปจํŠธ๋กค๋Ÿฌ)", { - companyCode, - menuObjid, - rulesCount: rules.length - }); - - return res.json({ success: true, data: rules }); - } catch (error: any) { - logger.error("โŒ ๋ฉ”๋‰ด๋ณ„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ (์ปจํŠธ๋กค๋Ÿฌ)", { - error: error.message, - errorCode: error.code, - errorStack: error.stack, - companyCode, - menuObjid, - }); - return res.status(500).json({ success: false, error: error.message }); + try { + const rules = await numberingRuleService.getAvailableRulesForMenu( + companyCode, + menuObjid + ); + + logger.info("โœ… ๋ฉ”๋‰ด๋ณ„ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์„ฑ๊ณต (์ปจํŠธ๋กค๋Ÿฌ)", { + companyCode, + menuObjid, + rulesCount: rules.length, + }); + + return res.json({ success: true, data: rules }); + } catch (error: any) { + logger.error("โŒ ๋ฉ”๋‰ด๋ณ„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ (์ปจํŠธ๋กค๋Ÿฌ)", { + error: error.message, + errorCode: error.code, + errorStack: error.stack, + companyCode, + menuObjid, + }); + return res.status(500).json({ success: false, error: error.message }); + } } -}); +); // ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง - ๊ฐ„์†Œํ™”) -router.get("/available-for-screen", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; - const { tableName } = req.query; +router.get( + "/available-for-screen", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const { tableName } = req.query; - try { - // tableName ํ•„์ˆ˜ ๊ฒ€์ฆ - if (!tableName || typeof tableName !== "string") { - return res.status(400).json({ - success: false, - error: "tableName is required", - }); - } - - const rules = await numberingRuleService.getAvailableRulesForScreen( - companyCode, - tableName - ); - - logger.info("ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์„ฑ๊ณต", { - companyCode, - tableName, - count: rules.length, - }); - - return res.json({ success: true, data: rules }); - } catch (error: any) { - logger.error("ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ", { - error: error.message, - tableName, - }); - return res.status(500).json({ - success: false, - error: error.message, - }); - } -}); - -// ํŠน์ • ๊ทœ์น™ ์กฐํšŒ -router.get("/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; - const { ruleId } = req.params; - - try { - const rule = await numberingRuleService.getRuleById(ruleId, companyCode); - if (!rule) { - return res.status(404).json({ success: false, error: "๊ทœ์น™์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" }); - } - return res.json({ success: true, data: rule }); - } catch (error: any) { - logger.error("๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, error: error.message }); - } -}); - -// ๊ทœ์น™ ์ƒ์„ฑ -router.post("/", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; - const ruleConfig = req.body; - - logger.info("๐Ÿ” [POST /numbering-rules] ์ฑ„๋ฒˆ ๊ทœ์น™ ์ƒ์„ฑ ์š”์ฒญ:", { - companyCode, - userId, - ruleId: ruleConfig.ruleId, - ruleName: ruleConfig.ruleName, - scopeType: ruleConfig.scopeType, - menuObjid: ruleConfig.menuObjid, - tableName: ruleConfig.tableName, - partsCount: ruleConfig.parts?.length, - }); - - try { - if (!ruleConfig.ruleId || !ruleConfig.ruleName) { - return res.status(400).json({ success: false, error: "๊ทœ์น™ ID์™€ ๊ทœ์น™๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค" }); - } - - if (!Array.isArray(ruleConfig.parts) || ruleConfig.parts.length === 0) { - return res.status(400).json({ success: false, error: "์ตœ์†Œ 1๊ฐœ ์ด์ƒ์˜ ๊ทœ์น™ ํŒŒํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค" }); - } - - // ๐Ÿ†• scopeType์ด 'table'์ธ ๊ฒฝ์šฐ tableName ํ•„์ˆ˜ ์ฒดํฌ - if (ruleConfig.scopeType === "table") { - if (!ruleConfig.tableName || ruleConfig.tableName.trim() === "") { + try { + // tableName ํ•„์ˆ˜ ๊ฒ€์ฆ + if (!tableName || typeof tableName !== "string") { return res.status(400).json({ success: false, - error: "ํ…Œ์ด๋ธ” ๋ฒ”์œ„ ๊ทœ์น™์€ ํ…Œ์ด๋ธ”๋ช…(tableName)์ด ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค", + error: "tableName is required", }); } - } - const newRule = await numberingRuleService.createRule(ruleConfig, companyCode, userId); - - logger.info("โœ… [POST /numbering-rules] ์ฑ„๋ฒˆ ๊ทœ์น™ ์ƒ์„ฑ ์„ฑ๊ณต:", { - ruleId: newRule.ruleId, - menuObjid: newRule.menuObjid, - }); + const rules = await numberingRuleService.getAvailableRulesForScreen( + companyCode, + tableName + ); - return res.status(201).json({ success: true, data: newRule }); - } catch (error: any) { - if (error.code === "23505") { - return res.status(409).json({ success: false, error: "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๊ทœ์น™ ID์ž…๋‹ˆ๋‹ค" }); + logger.info("ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์„ฑ๊ณต", { + companyCode, + tableName, + count: rules.length, + }); + + return res.json({ success: true, data: rules }); + } catch (error: any) { + logger.error("ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ", { + error: error.message, + tableName, + }); + return res.status(500).json({ + success: false, + error: error.message, + }); } - logger.error("โŒ [POST /numbering-rules] ๊ทœ์น™ ์ƒ์„ฑ ์‹คํŒจ:", { - error: error.message, - stack: error.stack, - code: error.code, - }); - return res.status(500).json({ success: false, error: error.message }); } -}); +); + +// ํŠน์ • ๊ทœ์น™ ์กฐํšŒ +router.get( + "/:ruleId", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const { ruleId } = req.params; + + try { + const rule = await numberingRuleService.getRuleById(ruleId, companyCode); + if (!rule) { + return res + .status(404) + .json({ success: false, error: "๊ทœ์น™์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" }); + } + return res.json({ success: true, data: rule }); + } catch (error: any) { + logger.error("๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, error: error.message }); + } + } +); + +// ๊ทœ์น™ ์ƒ์„ฑ +router.post( + "/", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const ruleConfig = req.body; + + logger.info("๐Ÿ” [POST /numbering-rules] ์ฑ„๋ฒˆ ๊ทœ์น™ ์ƒ์„ฑ ์š”์ฒญ:", { + companyCode, + userId, + ruleId: ruleConfig.ruleId, + ruleName: ruleConfig.ruleName, + scopeType: ruleConfig.scopeType, + menuObjid: ruleConfig.menuObjid, + tableName: ruleConfig.tableName, + partsCount: ruleConfig.parts?.length, + }); + + try { + if (!ruleConfig.ruleId || !ruleConfig.ruleName) { + return res + .status(400) + .json({ success: false, error: "๊ทœ์น™ ID์™€ ๊ทœ์น™๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค" }); + } + + if (!Array.isArray(ruleConfig.parts) || ruleConfig.parts.length === 0) { + return res + .status(400) + .json({ + success: false, + error: "์ตœ์†Œ 1๊ฐœ ์ด์ƒ์˜ ๊ทœ์น™ ํŒŒํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค", + }); + } + + // ๐Ÿ†• scopeType์ด 'table'์ธ ๊ฒฝ์šฐ tableName ํ•„์ˆ˜ ์ฒดํฌ + if (ruleConfig.scopeType === "table") { + if (!ruleConfig.tableName || ruleConfig.tableName.trim() === "") { + return res.status(400).json({ + success: false, + error: "ํ…Œ์ด๋ธ” ๋ฒ”์œ„ ๊ทœ์น™์€ ํ…Œ์ด๋ธ”๋ช…(tableName)์ด ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค", + }); + } + } + + const newRule = await numberingRuleService.createRule( + ruleConfig, + companyCode, + userId + ); + + logger.info("โœ… [POST /numbering-rules] ์ฑ„๋ฒˆ ๊ทœ์น™ ์ƒ์„ฑ ์„ฑ๊ณต:", { + ruleId: newRule.ruleId, + menuObjid: newRule.menuObjid, + }); + + return res.status(201).json({ success: true, data: newRule }); + } catch (error: any) { + if (error.code === "23505") { + return res + .status(409) + .json({ success: false, error: "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๊ทœ์น™ ID์ž…๋‹ˆ๋‹ค" }); + } + logger.error("โŒ [POST /numbering-rules] ๊ทœ์น™ ์ƒ์„ฑ ์‹คํŒจ:", { + error: error.message, + stack: error.stack, + code: error.code, + }); + return res.status(500).json({ success: false, error: error.message }); + } + } +); // ๊ทœ์น™ ์ˆ˜์ • -router.put("/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; - const { ruleId } = req.params; - const updates = req.body; +router.put( + "/:ruleId", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const { ruleId } = req.params; + const updates = req.body; - logger.info("์ฑ„๋ฒˆ ๊ทœ์น™ ์ˆ˜์ • ์š”์ฒญ", { ruleId, companyCode, updates }); + logger.info("์ฑ„๋ฒˆ ๊ทœ์น™ ์ˆ˜์ • ์š”์ฒญ", { ruleId, companyCode, updates }); - try { - const updatedRule = await numberingRuleService.updateRule(ruleId, updates, companyCode); - logger.info("์ฑ„๋ฒˆ ๊ทœ์น™ ์ˆ˜์ • ์„ฑ๊ณต", { ruleId, companyCode }); - return res.json({ success: true, data: updatedRule }); - } catch (error: any) { - logger.error("์ฑ„๋ฒˆ ๊ทœ์น™ ์ˆ˜์ • ์‹คํŒจ", { - ruleId, - companyCode, - error: error.message, - stack: error.stack - }); - if (error.message.includes("์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜")) { - return res.status(404).json({ success: false, error: error.message }); + try { + const updatedRule = await numberingRuleService.updateRule( + ruleId, + updates, + companyCode + ); + logger.info("์ฑ„๋ฒˆ ๊ทœ์น™ ์ˆ˜์ • ์„ฑ๊ณต", { ruleId, companyCode }); + return res.json({ success: true, data: updatedRule }); + } catch (error: any) { + logger.error("์ฑ„๋ฒˆ ๊ทœ์น™ ์ˆ˜์ • ์‹คํŒจ", { + ruleId, + companyCode, + error: error.message, + stack: error.stack, + }); + if (error.message.includes("์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜")) { + return res.status(404).json({ success: false, error: error.message }); + } + return res.status(500).json({ success: false, error: error.message }); } - return res.status(500).json({ success: false, error: error.message }); } -}); +); // ๊ทœ์น™ ์‚ญ์ œ -router.delete("/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; - const { ruleId } = req.params; +router.delete( + "/:ruleId", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const { ruleId } = req.params; - try { - await numberingRuleService.deleteRule(ruleId, companyCode); - return res.json({ success: true, message: "๊ทœ์น™์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" }); - } catch (error: any) { - if (error.message.includes("์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜")) { - return res.status(404).json({ success: false, error: error.message }); + try { + await numberingRuleService.deleteRule(ruleId, companyCode); + return res.json({ success: true, message: "๊ทœ์น™์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" }); + } catch (error: any) { + if (error.message.includes("์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜")) { + return res.status(404).json({ success: false, error: error.message }); + } + logger.error("๊ทœ์น™ ์‚ญ์ œ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, error: error.message }); } - logger.error("๊ทœ์น™ ์‚ญ์ œ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, error: error.message }); } -}); +); // ์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ˆœ๋ฒˆ ์ฆ๊ฐ€ ์—†์Œ) -router.post("/:ruleId/preview", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; - const { ruleId } = req.params; - const { formData } = req.body; // ํผ ๋ฐ์ดํ„ฐ (์นดํ…Œ๊ณ ๋ฆฌ ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ์‹œ ์‚ฌ์šฉ) +router.post( + "/:ruleId/preview", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const { ruleId } = req.params; + const { formData } = req.body; // ํผ ๋ฐ์ดํ„ฐ (์นดํ…Œ๊ณ ๋ฆฌ ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ์‹œ ์‚ฌ์šฉ) - try { - const previewCode = await numberingRuleService.previewCode(ruleId, companyCode, formData); - return res.json({ success: true, data: { generatedCode: previewCode } }); - } catch (error: any) { - logger.error("์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, error: error.message }); + try { + const previewCode = await numberingRuleService.previewCode( + ruleId, + companyCode, + formData + ); + return res.json({ success: true, data: { generatedCode: previewCode } }); + } catch (error: any) { + logger.error("์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, error: error.message }); + } } -}); +); // ์ฝ”๋“œ ํ• ๋‹น (์ €์žฅ ์‹œ์ ์— ์‹ค์ œ ์ˆœ๋ฒˆ ์ฆ๊ฐ€) -router.post("/:ruleId/allocate", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; - const { ruleId } = req.params; - const { formData } = req.body; // ํผ ๋ฐ์ดํ„ฐ (๋‚ ์งœ ์ปฌ๋Ÿผ ๊ธฐ์ค€ ์ƒ์„ฑ ์‹œ ์‚ฌ์šฉ) +router.post( + "/:ruleId/allocate", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const { ruleId } = req.params; + const { formData, userInputCode } = req.body; // ํผ ๋ฐ์ดํ„ฐ + ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘ํ•œ ์ฝ”๋“œ - logger.info("์ฝ”๋“œ ํ• ๋‹น ์š”์ฒญ", { ruleId, companyCode, hasFormData: !!formData }); + logger.info("์ฝ”๋“œ ํ• ๋‹น ์š”์ฒญ", { + ruleId, + companyCode, + hasFormData: !!formData, + userInputCode, + }); - try { - const allocatedCode = await numberingRuleService.allocateCode(ruleId, companyCode, formData); - logger.info("์ฝ”๋“œ ํ• ๋‹น ์„ฑ๊ณต", { ruleId, allocatedCode }); - return res.json({ success: true, data: { generatedCode: allocatedCode } }); - } catch (error: any) { - logger.error("์ฝ”๋“œ ํ• ๋‹น ์‹คํŒจ", { ruleId, companyCode, error: error.message }); - return res.status(500).json({ success: false, error: error.message }); + try { + const allocatedCode = await numberingRuleService.allocateCode( + ruleId, + companyCode, + formData, + userInputCode + ); + logger.info("์ฝ”๋“œ ํ• ๋‹น ์„ฑ๊ณต", { ruleId, allocatedCode }); + return res.json({ + success: true, + data: { generatedCode: allocatedCode }, + }); + } catch (error: any) { + logger.error("์ฝ”๋“œ ํ• ๋‹น ์‹คํŒจ", { + ruleId, + companyCode, + error: error.message, + }); + return res.status(500).json({ success: false, error: error.message }); + } } -}); +); // ์ฝ”๋“œ ์ƒ์„ฑ (๊ธฐ์กด ํ˜ธํ™˜์„ฑ ์œ ์ง€, deprecated) -router.post("/:ruleId/generate", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; - const { ruleId } = req.params; +router.post( + "/:ruleId/generate", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const { ruleId } = req.params; - try { - const generatedCode = await numberingRuleService.generateCode(ruleId, companyCode); - return res.json({ success: true, data: { generatedCode } }); - } catch (error: any) { - logger.error("์ฝ”๋“œ ์ƒ์„ฑ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, error: error.message }); + try { + const generatedCode = await numberingRuleService.generateCode( + ruleId, + companyCode + ); + return res.json({ success: true, data: { generatedCode } }); + } catch (error: any) { + logger.error("์ฝ”๋“œ ์ƒ์„ฑ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, error: error.message }); + } } -}); +); // ์‹œํ€€์Šค ์ดˆ๊ธฐํ™” -router.post("/:ruleId/reset", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; - const { ruleId } = req.params; +router.post( + "/:ruleId/reset", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const { ruleId } = req.params; - try { - await numberingRuleService.resetSequence(ruleId, companyCode); - return res.json({ success: true, message: "์‹œํ€€์Šค๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค" }); - } catch (error: any) { - logger.error("์‹œํ€€์Šค ์ดˆ๊ธฐํ™” ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, error: error.message }); + try { + await numberingRuleService.resetSequence(ruleId, companyCode); + return res.json({ success: true, message: "์‹œํ€€์Šค๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค" }); + } catch (error: any) { + logger.error("์‹œํ€€์Šค ์ดˆ๊ธฐํ™” ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, error: error.message }); + } } -}); +); // ==================== ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ”์šฉ API ==================== // [ํ…Œ์ŠคํŠธ] ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ”์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ -router.get("/test/list/:menuObjid?", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; - const menuObjid = req.params.menuObjid ? parseInt(req.params.menuObjid) : undefined; +router.get( + "/test/list/:menuObjid?", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const menuObjid = req.params.menuObjid + ? parseInt(req.params.menuObjid) + : undefined; - logger.info("[ํ…Œ์ŠคํŠธ] ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ ์š”์ฒญ", { companyCode, menuObjid }); + logger.info("[ํ…Œ์ŠคํŠธ] ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ ์š”์ฒญ", { + companyCode, + menuObjid, + }); - try { - const rules = await numberingRuleService.getRulesFromTest(companyCode, menuObjid); - logger.info("[ํ…Œ์ŠคํŠธ] ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต", { companyCode, menuObjid, count: rules.length }); - return res.json({ success: true, data: rules }); - } catch (error: any) { - logger.error("[ํ…Œ์ŠคํŠธ] ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, error: error.message }); + try { + const rules = await numberingRuleService.getRulesFromTest( + companyCode, + menuObjid + ); + logger.info("[ํ…Œ์ŠคํŠธ] ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต", { + companyCode, + menuObjid, + count: rules.length, + }); + return res.json({ success: true, data: rules }); + } catch (error: any) { + logger.error("[ํ…Œ์ŠคํŠธ] ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", { + error: error.message, + }); + return res.status(500).json({ success: false, error: error.message }); + } } -}); +); // [ํ…Œ์ŠคํŠธ] ํ…Œ์ด๋ธ”+์ปฌ๋Ÿผ ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ -router.get("/test/by-column/:tableName/:columnName", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; - const { tableName, columnName } = req.params; +router.get( + "/test/by-column/:tableName/:columnName", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const { tableName, columnName } = req.params; - try { - const rule = await numberingRuleService.getNumberingRuleByColumn(companyCode, tableName, columnName); - return res.json({ success: true, data: rule }); - } catch (error: any) { - logger.error("ํ…Œ์ด๋ธ”+์ปฌ๋Ÿผ ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, error: error.message }); + try { + const rule = await numberingRuleService.getNumberingRuleByColumn( + companyCode, + tableName, + columnName + ); + return res.json({ success: true, data: rule }); + } catch (error: any) { + logger.error("ํ…Œ์ด๋ธ”+์ปฌ๋Ÿผ ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ", { + error: error.message, + }); + return res.status(500).json({ success: false, error: error.message }); + } } -}); +); // [ํ…Œ์ŠคํŠธ] ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ”์— ์ฑ„๋ฒˆ ๊ทœ์น™ ์ €์žฅ // ์ฑ„๋ฒˆ ๊ทœ์น™์€ ๋…๋ฆฝ์ ์œผ๋กœ ์ƒ์„ฑ ๊ฐ€๋Šฅ (๋‚˜์ค‘์— ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ์—์„œ ์ปฌ๋Ÿผ์— ์—ฐ๊ฒฐ) -router.post("/test/save", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; - const ruleConfig = req.body; +router.post( + "/test/save", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const ruleConfig = req.body; - logger.info("[ํ…Œ์ŠคํŠธ] ์ฑ„๋ฒˆ ๊ทœ์น™ ์ €์žฅ ์š”์ฒญ", { - ruleId: ruleConfig.ruleId, - ruleName: ruleConfig.ruleName, - tableName: ruleConfig.tableName || "(๋ฏธ์ง€์ •)", - columnName: ruleConfig.columnName || "(๋ฏธ์ง€์ •)", - }); + logger.info("[ํ…Œ์ŠคํŠธ] ์ฑ„๋ฒˆ ๊ทœ์น™ ์ €์žฅ ์š”์ฒญ", { + ruleId: ruleConfig.ruleId, + ruleName: ruleConfig.ruleName, + tableName: ruleConfig.tableName || "(๋ฏธ์ง€์ •)", + columnName: ruleConfig.columnName || "(๋ฏธ์ง€์ •)", + }); - try { - // ruleName๋งŒ ํ•„์ˆ˜, tableName/columnName์€ ์„ ํƒ (๋‚˜์ค‘์— ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ์—์„œ ์—ฐ๊ฒฐ) - if (!ruleConfig.ruleName) { - return res.status(400).json({ - success: false, - error: "ruleName is required" - }); + try { + // ruleName๋งŒ ํ•„์ˆ˜, tableName/columnName์€ ์„ ํƒ (๋‚˜์ค‘์— ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ์—์„œ ์—ฐ๊ฒฐ) + if (!ruleConfig.ruleName) { + return res.status(400).json({ + success: false, + error: "ruleName is required", + }); + } + + const savedRule = await numberingRuleService.saveRuleToTest( + ruleConfig, + companyCode, + userId + ); + return res.json({ success: true, data: savedRule }); + } catch (error: any) { + logger.error("[ํ…Œ์ŠคํŠธ] ์ฑ„๋ฒˆ ๊ทœ์น™ ์ €์žฅ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, error: error.message }); } - - const savedRule = await numberingRuleService.saveRuleToTest(ruleConfig, companyCode, userId); - return res.json({ success: true, data: savedRule }); - } catch (error: any) { - logger.error("[ํ…Œ์ŠคํŠธ] ์ฑ„๋ฒˆ ๊ทœ์น™ ์ €์žฅ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, error: error.message }); } -}); +); // [ํ…Œ์ŠคํŠธ] ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ”์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™ ์‚ญ์ œ -router.delete("/test/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; - const { ruleId } = req.params; +router.delete( + "/test/:ruleId", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const { ruleId } = req.params; - try { - await numberingRuleService.deleteRuleFromTest(ruleId, companyCode); - return res.json({ success: true, message: "ํ…Œ์ŠคํŠธ ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" }); - } catch (error: any) { - logger.error("[ํ…Œ์ŠคํŠธ] ์ฑ„๋ฒˆ ๊ทœ์น™ ์‚ญ์ œ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, error: error.message }); + try { + await numberingRuleService.deleteRuleFromTest(ruleId, companyCode); + return res.json({ + success: true, + message: "ํ…Œ์ŠคํŠธ ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค", + }); + } catch (error: any) { + logger.error("[ํ…Œ์ŠคํŠธ] ์ฑ„๋ฒˆ ๊ทœ์น™ ์‚ญ์ œ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, error: error.message }); + } } -}); +); // [ํ…Œ์ŠคํŠธ] ์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ) -router.post("/test/:ruleId/preview", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const companyCode = req.user!.companyCode; - const { ruleId } = req.params; - const { formData } = req.body; +router.post( + "/test/:ruleId/preview", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const companyCode = req.user!.companyCode; + const { ruleId } = req.params; + const { formData } = req.body; - try { - const previewCode = await numberingRuleService.previewCode(ruleId, companyCode, formData); - return res.json({ success: true, data: { generatedCode: previewCode } }); - } catch (error: any) { - logger.error("[ํ…Œ์ŠคํŠธ] ์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, error: error.message }); + try { + const previewCode = await numberingRuleService.previewCode( + ruleId, + companyCode, + formData + ); + return res.json({ success: true, data: { generatedCode: previewCode } }); + } catch (error: any) { + logger.error("[ํ…Œ์ŠคํŠธ] ์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, error: error.message }); + } } -}); +); // ==================== ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ API ==================== // ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ -router.post("/copy-for-company", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const userCompanyCode = req.user!.companyCode; - const { sourceCompanyCode, targetCompanyCode } = req.body; +router.post( + "/copy-for-company", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + const userCompanyCode = req.user!.companyCode; + const { sourceCompanyCode, targetCompanyCode } = req.body; - // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ - if (userCompanyCode !== "*") { - return res.status(403).json({ - success: false, - error: "์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค" - }); - } + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ + if (userCompanyCode !== "*") { + return res.status(403).json({ + success: false, + error: "์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค", + }); + } - if (!sourceCompanyCode || !targetCompanyCode) { - return res.status(400).json({ - success: false, - error: "sourceCompanyCode์™€ targetCompanyCode๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค" - }); - } + if (!sourceCompanyCode || !targetCompanyCode) { + return res.status(400).json({ + success: false, + error: "sourceCompanyCode์™€ targetCompanyCode๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค", + }); + } - try { - const result = await numberingRuleService.copyRulesForCompany(sourceCompanyCode, targetCompanyCode); - return res.json({ success: true, data: result }); - } catch (error: any) { - logger.error("ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, error: error.message }); + try { + const result = await numberingRuleService.copyRulesForCompany( + sourceCompanyCode, + targetCompanyCode + ); + return res.json({ success: true, data: result }); + } catch (error: any) { + logger.error("ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, error: error.message }); + } } -}); +); export default router; diff --git a/backend-node/src/controllers/scheduleController.ts b/backend-node/src/controllers/scheduleController.ts new file mode 100644 index 00000000..15012053 --- /dev/null +++ b/backend-node/src/controllers/scheduleController.ts @@ -0,0 +1,223 @@ +/** + * ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ ์ปจํŠธ๋กค๋Ÿฌ + * + * ์Šค์ผ€์ค„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ, ์ ์šฉ, ์กฐํšŒ API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + */ + +import { Request, Response } from "express"; +import { ScheduleService } from "../services/scheduleService"; + +export class ScheduleController { + private scheduleService: ScheduleService; + + constructor() { + this.scheduleService = new ScheduleService(); + } + + /** + * ์Šค์ผ€์ค„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ + * POST /api/schedule/preview + * + * ์„ ํƒํ•œ ์†Œ์Šค ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒ์„ฑ๋  ์Šค์ผ€์ค„์„ ๋ฏธ๋ฆฌ๋ณด๊ธฐํ•ฉ๋‹ˆ๋‹ค. + * ์‹ค์ œ ์ €์žฅ์€ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + */ + preview = async (req: Request, res: Response): Promise => { + try { + const { config, sourceData, period } = req.body; + const userId = (req as any).user?.userId || "system"; + const companyCode = (req as any).user?.companyCode || "*"; + + console.log("[ScheduleController] preview ํ˜ธ์ถœ:", { + scheduleType: config?.scheduleType, + sourceDataCount: sourceData?.length, + period, + userId, + companyCode, + }); + + // ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ + if (!config || !config.scheduleType) { + res.status(400).json({ + success: false, + message: "์Šค์ผ€์ค„ ์„ค์ •(config)์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + }); + return; + } + + if (!sourceData || sourceData.length === 0) { + res.status(400).json({ + success: false, + message: "์†Œ์Šค ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + }); + return; + } + + // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ƒ์„ฑ + const preview = await this.scheduleService.generatePreview( + config, + sourceData, + period, + companyCode + ); + + res.json({ + success: true, + preview, + }); + } catch (error: any) { + console.error("[ScheduleController] preview ์˜ค๋ฅ˜:", error); + res.status(500).json({ + success: false, + message: error.message || "์Šค์ผ€์ค„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } + }; + + /** + * ์Šค์ผ€์ค„ ์ ์šฉ + * POST /api/schedule/apply + * + * ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฒฐ๊ณผ๋ฅผ ์‹ค์ œ๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + */ + apply = async (req: Request, res: Response): Promise => { + try { + const { config, preview, options } = req.body; + const userId = (req as any).user?.userId || "system"; + const companyCode = (req as any).user?.companyCode || "*"; + + console.log("[ScheduleController] apply ํ˜ธ์ถœ:", { + scheduleType: config?.scheduleType, + createCount: preview?.summary?.createCount, + deleteCount: preview?.summary?.deleteCount, + options, + userId, + companyCode, + }); + + // ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ + if (!config || !preview) { + res.status(400).json({ + success: false, + message: "์„ค์ •(config)๊ณผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ(preview)๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + }); + return; + } + + // ์ ์šฉ + const applied = await this.scheduleService.applySchedules( + config, + preview, + options || { deleteExisting: true, updateMode: "replace" }, + companyCode, + userId + ); + + res.json({ + success: true, + applied, + message: `${applied.created}๊ฑด ์ƒ์„ฑ, ${applied.deleted}๊ฑด ์‚ญ์ œ, ${applied.updated}๊ฑด ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, + }); + } catch (error: any) { + console.error("[ScheduleController] apply ์˜ค๋ฅ˜:", error); + res.status(500).json({ + success: false, + message: error.message || "์Šค์ผ€์ค„ ์ ์šฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } + }; + + /** + * ์Šค์ผ€์ค„ ๋ชฉ๋ก ์กฐํšŒ + * GET /api/schedule/list + * + * ํƒ€์ž„๋ผ์ธ ํ‘œ์‹œ์šฉ ์Šค์ผ€์ค„ ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + list = async (req: Request, res: Response): Promise => { + try { + const { + scheduleType, + resourceType, + resourceId, + startDate, + endDate, + status, + } = req.query; + const companyCode = (req as any).user?.companyCode || "*"; + + console.log("[ScheduleController] list ํ˜ธ์ถœ:", { + scheduleType, + resourceType, + resourceId, + startDate, + endDate, + status, + companyCode, + }); + + const result = await this.scheduleService.getScheduleList({ + scheduleType: scheduleType as string, + resourceType: resourceType as string, + resourceId: resourceId as string, + startDate: startDate as string, + endDate: endDate as string, + status: status as string, + companyCode, + }); + + res.json({ + success: true, + data: result.data, + total: result.total, + }); + } catch (error: any) { + console.error("[ScheduleController] list ์˜ค๋ฅ˜:", error); + res.status(500).json({ + success: false, + message: error.message || "์Šค์ผ€์ค„ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } + }; + + /** + * ์Šค์ผ€์ค„ ์‚ญ์ œ + * DELETE /api/schedule/:scheduleId + */ + delete = async (req: Request, res: Response): Promise => { + try { + const { scheduleId } = req.params; + const userId = (req as any).user?.userId || "system"; + const companyCode = (req as any).user?.companyCode || "*"; + + console.log("[ScheduleController] delete ํ˜ธ์ถœ:", { + scheduleId, + userId, + companyCode, + }); + + const result = await this.scheduleService.deleteSchedule( + parseInt(scheduleId, 10), + companyCode, + userId + ); + + if (!result.success) { + res.status(404).json({ + success: false, + message: result.message || "์Šค์ผ€์ค„์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + }); + return; + } + + res.json({ + success: true, + message: "์Šค์ผ€์ค„์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + }); + } catch (error: any) { + console.error("[ScheduleController] delete ์˜ค๋ฅ˜:", error); + res.status(500).json({ + success: false, + message: error.message || "์Šค์ผ€์ค„ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } + }; +} diff --git a/backend-node/src/controllers/screenGroupController.ts b/backend-node/src/controllers/screenGroupController.ts index d963aea6..88230f48 100644 --- a/backend-node/src/controllers/screenGroupController.ts +++ b/backend-node/src/controllers/screenGroupController.ts @@ -46,11 +46,13 @@ export const getScreenGroups = async (req: AuthenticatedRequest, res: Response) const countResult = await pool.query(countQuery, params); const total = parseInt(countResult.rows[0].total); - // ๋ฐ์ดํ„ฐ ์กฐํšŒ (screens ๋ฐฐ์—ด ํฌํ•จ) + // ๋ฐ์ดํ„ฐ ์กฐํšŒ (screens ๋ฐฐ์—ด ํฌํ•จ) - ์‚ญ์ œ๋œ ํ™”๋ฉด(is_active = 'D') ์ œ์™ธ const dataQuery = ` SELECT sg.*, - (SELECT COUNT(*) FROM screen_group_screens sgs WHERE sgs.group_id = sg.id) as screen_count, + (SELECT COUNT(*) FROM screen_group_screens sgs + LEFT JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id + WHERE sgs.group_id = sg.id AND sd.is_active != 'D') as screen_count, (SELECT json_agg( json_build_object( 'id', sgs.id, @@ -64,6 +66,7 @@ export const getScreenGroups = async (req: AuthenticatedRequest, res: Response) ) FROM screen_group_screens sgs LEFT JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id WHERE sgs.group_id = sg.id + AND sd.is_active != 'D' ) as screens FROM screen_groups sg ${whereClause} @@ -111,6 +114,7 @@ export const getScreenGroup = async (req: AuthenticatedRequest, res: Response) = ) FROM screen_group_screens sgs LEFT JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id WHERE sgs.group_id = sg.id + AND sd.is_active != 'D' ) as screens FROM screen_groups sg WHERE sg.id = $1 @@ -308,39 +312,108 @@ export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response await client.query('BEGIN'); - // 1. ์‚ญ์ œํ•  ๊ทธ๋ฃน๊ณผ ํ•˜์œ„ ๊ทธ๋ฃน ID ์ˆ˜์ง‘ (CASCADE ์‚ญ์ œ ๋Œ€์ƒ) + // 0. ์‚ญ์ œํ•  ๊ทธ๋ฃน์˜ company_code ํ™•์ธ + const targetGroupResult = await client.query( + `SELECT company_code FROM screen_groups WHERE id = $1`, + [id] + ); + if (targetGroupResult.rows.length === 0) { + await client.query('ROLLBACK'); + return res.status(404).json({ success: false, message: "ํ™”๋ฉด ๊ทธ๋ฃน์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); + } + const targetCompanyCode = targetGroupResult.rows[0].company_code; + + // ๊ถŒํ•œ ์ฒดํฌ: ์ตœ๊ณ ๊ด€๋ฆฌ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ ์ž์‹ ์˜ ํšŒ์‚ฌ ๊ทธ๋ฃน๋งŒ ์‚ญ์ œ ๊ฐ€๋Šฅ + if (companyCode !== "*" && targetCompanyCode !== companyCode) { + await client.query('ROLLBACK'); + return res.status(403).json({ success: false, message: "๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค." }); + } + + // 1. ์‚ญ์ œํ•  ๊ทธ๋ฃน๊ณผ ํ•˜์œ„ ๊ทธ๋ฃน ID ์ˆ˜์ง‘ (๊ฐ™์€ ํšŒ์‚ฌ๋งŒ - CASCADE ์‚ญ์ œ ๋Œ€์ƒ) const childGroupsResult = await client.query(` WITH RECURSIVE child_groups AS ( - SELECT id FROM screen_groups WHERE id = $1 + SELECT id, company_code FROM screen_groups WHERE id = $1 AND company_code = $2 UNION ALL - SELECT sg.id FROM screen_groups sg - JOIN child_groups cg ON sg.parent_group_id = cg.id + SELECT sg.id, sg.company_code FROM screen_groups sg + JOIN child_groups cg ON sg.parent_group_id = cg.id AND sg.company_code = cg.company_code ) SELECT id FROM child_groups - `, [id]); + `, [id, targetCompanyCode]); const groupIdsToDelete = childGroupsResult.rows.map((r: any) => r.id); - // 2. menu_info์—์„œ ์‚ญ์ œ๋  screen_group ์ฐธ์กฐ๋ฅผ NULL๋กœ ์ •๋ฆฌ + logger.info("ํ™”๋ฉด ๊ทธ๋ฃน ์‚ญ์ œ ๋Œ€์ƒ", { + companyCode, + targetCompanyCode, + groupId: id, + childGroupIds: groupIdsToDelete + }); + + // 2. ์‚ญ์ œ๋  ๊ทธ๋ฃน์— ์—ฐ๊ฒฐ๋œ ๋ฉ”๋‰ด ์ •๋ฆฌ if (groupIdsToDelete.length > 0) { - await client.query(` - UPDATE menu_info - SET screen_group_id = NULL + // 2-1. ์‚ญ์ œํ•  ๋ฉ”๋‰ด objid ์ˆ˜์ง‘ + const menusToDelete = await client.query(` + SELECT objid FROM menu_info WHERE screen_group_id = ANY($1::int[]) - `, [groupIdsToDelete]); + AND company_code = $2 + `, [groupIdsToDelete, targetCompanyCode]); + const menuObjids = menusToDelete.rows.map((r: any) => r.objid); + + if (menuObjids.length > 0) { + // 2-2. screen_menu_assignments์—์„œ ํ•ด๋‹น ๋ฉ”๋‰ด ๊ด€๋ จ ๋ฐ์ดํ„ฐ ์‚ญ์ œ + await client.query(` + DELETE FROM screen_menu_assignments + WHERE menu_objid = ANY($1::bigint[]) + AND company_code = $2 + `, [menuObjids, targetCompanyCode]); + + // 2-3. menu_info์—์„œ ํ•ด๋‹น ๋ฉ”๋‰ด ์‚ญ์ œ + await client.query(` + DELETE FROM menu_info + WHERE screen_group_id = ANY($1::int[]) + AND company_code = $2 + `, [groupIdsToDelete, targetCompanyCode]); + + logger.info("๊ทธ๋ฃน ์‚ญ์ œ ์‹œ ์—ฐ๊ฒฐ๋œ ๋ฉ”๋‰ด ์‚ญ์ œ", { + groupIds: groupIdsToDelete, + deletedMenuCount: menuObjids.length, + companyCode: targetCompanyCode + }); + } + + // 2-4. ํ•ด๋‹น ํšŒ์‚ฌ์˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ์‚ญ์ œ (์ตœ์ƒ์œ„ ๊ทธ๋ฃน ์‚ญ์ œ ์‹œ) + // ์‚ญ์ œ๋˜๋Š” ๊ทธ๋ฃน์ด ์ตœ์ƒ์œ„์ธ์ง€ ํ™•์ธ + const isRootGroup = await client.query( + `SELECT 1 FROM screen_groups WHERE id = $1 AND parent_group_id IS NULL`, + [id] + ); + + if (isRootGroup.rows.length > 0) { + // ์ตœ์ƒ์œ„ ๊ทธ๋ฃน ์‚ญ์ œ ์‹œ ํ•ด๋‹น ํšŒ์‚ฌ์˜ ์ฑ„๋ฒˆ ๊ทœ์น™๋„ ์‚ญ์ œ + // ๋จผ์ € ํŒŒํŠธ ์‚ญ์ œ + await client.query( + `DELETE FROM numbering_rule_parts + WHERE rule_id IN (SELECT rule_id FROM numbering_rules WHERE company_code = $1)`, + [targetCompanyCode] + ); + // ๊ทœ์น™ ์‚ญ์ œ + const deletedRules = await client.query( + `DELETE FROM numbering_rules WHERE company_code = $1 RETURNING rule_id`, + [targetCompanyCode] + ); + if (deletedRules.rowCount && deletedRules.rowCount > 0) { + logger.info("๊ทธ๋ฃน ์‚ญ์ œ ์‹œ ์ฑ„๋ฒˆ ๊ทœ์น™ ์‚ญ์ œ", { + companyCode: targetCompanyCode, + deletedCount: deletedRules.rowCount + }); + } + } } - // 3. screen_groups ์‚ญ์ œ - let query = `DELETE FROM screen_groups WHERE id = $1`; - const params: any[] = [id]; - - if (companyCode !== "*") { - query += ` AND company_code = $2`; - params.push(companyCode); - } - - query += " RETURNING id"; - - const result = await client.query(query, params); + // 3. screen_groups ์‚ญ์ œ (ํ•ด๋‹น ๊ทธ๋ฃน๋งŒ - ํ•˜์œ„ ๊ทธ๋ฃน์€ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ˆœ์ฐจ ์‚ญ์ œ) + const result = await client.query( + `DELETE FROM screen_groups WHERE id = $1 AND company_code = $2 RETURNING id`, + [id, targetCompanyCode] + ); if (result.rows.length === 0) { await client.query('ROLLBACK'); @@ -349,7 +422,7 @@ export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response await client.query('COMMIT'); - logger.info("ํ™”๋ฉด ๊ทธ๋ฃน ์‚ญ์ œ", { companyCode, groupId: id, cleanedRefs: groupIdsToDelete.length }); + logger.info("ํ™”๋ฉด ๊ทธ๋ฃน ์‚ญ์ œ ์™„๋ฃŒ", { companyCode, targetCompanyCode, groupId: id, cleanedRefs: groupIdsToDelete.length }); res.json({ success: true, message: "ํ™”๋ฉด ๊ทธ๋ฃน์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." }); } catch (error: any) { @@ -1668,7 +1741,9 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons }); // 4. rightPanel.relation ํŒŒ์‹ฑ (split-panel-layout ๋“ฑ์—์„œ ์‚ฌ์šฉ) + // screen_layouts (v1)์™€ screen_layouts_v2 ๋ชจ๋‘ ์กฐํšŒ const rightPanelQuery = ` + -- V1: screen_layouts์—์„œ ์กฐํšŒ SELECT sd.screen_id, sd.screen_name, @@ -1681,6 +1756,23 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons JOIN screen_layouts sl ON sd.screen_id = sl.screen_id WHERE sd.screen_id = ANY($1) AND sl.properties->'componentConfig'->'rightPanel'->'relation' IS NOT NULL + + UNION ALL + + -- V2: screen_layouts_v2์—์„œ ์กฐํšŒ (v2-split-panel-layout ์ปดํฌ๋„ŒํŠธ) + SELECT + sd.screen_id, + sd.screen_name, + sd.table_name as main_table, + comp->'overrides'->>'type' as component_type, + comp->'overrides'->'rightPanel'->'relation' as right_panel_relation, + comp->'overrides'->'rightPanel'->>'tableName' as right_panel_table, + comp->'overrides'->'rightPanel'->'columns' as right_panel_columns + FROM screen_definitions sd + JOIN screen_layouts_v2 slv2 ON sd.screen_id = slv2.screen_id, + jsonb_array_elements(slv2.layout_data->'components') as comp + WHERE sd.screen_id = ANY($1) + AND comp->'overrides'->'rightPanel'->'relation' IS NOT NULL `; const rightPanelResult = await pool.query(rightPanelQuery, [screenIds]); @@ -2049,9 +2141,56 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons })) }); + // ============================================================ + // 6. ์ „์—ญ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์ˆ˜์ง‘ (์šฐ์„ ์ˆœ์œ„ ์ ์šฉ์šฉ) + // ============================================================ + // ๋ฉ”์ธ ํ…Œ์ด๋ธ” ์กฐ๊ฑด: + // 1. screen_definitions.table_name (์ปดํฌ๋„ŒํŠธ ์ง์ ‘ ์—ฐ๊ฒฐ) + // 2. v2-split-panel-layout์˜ rightPanel.tableName (WHERE ์กฐ๊ฑด ๋Œ€์ƒ) + // + // ์ด ๋ชฉ๋ก์— ์žˆ์œผ๋ฉด ์„œ๋ธŒ ํ…Œ์ด๋ธ”๋กœ ๋ถ„๋ฅ˜๋˜์ง€ ์•Š์Œ (์šฐ์„ ์ˆœ์œ„: ๋ฉ”์ธ > ์„œ๋ธŒ) + const globalMainTablesQuery = ` + -- 1. ๋ชจ๋“  ํ™”๋ฉด์˜ ๋ฉ”์ธ ํ…Œ์ด๋ธ” (screen_definitions.table_name) + SELECT DISTINCT table_name as main_table + FROM screen_definitions + WHERE screen_id = ANY($1) + AND table_name IS NOT NULL + + UNION + + -- 2. v2-split-panel-layout์˜ rightPanel.tableName (WHERE ์กฐ๊ฑด ๋Œ€์ƒ) + -- ํ˜„์žฌ ๊ทธ๋ฃน์˜ ํ™”๋ฉด๋“ค์—์„œ ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ๋กœ ์—ฐ๊ฒฐ๋œ ํ…Œ์ด๋ธ” + SELECT DISTINCT comp->'overrides'->'rightPanel'->>'tableName' as main_table + FROM screen_definitions sd + JOIN screen_layouts_v2 slv2 ON sd.screen_id = slv2.screen_id, + jsonb_array_elements(slv2.layout_data->'components') as comp + WHERE sd.screen_id = ANY($1) + AND comp->'overrides'->'rightPanel'->>'tableName' IS NOT NULL + + UNION + + -- 3. v1 screen_layouts์˜ rightPanel.tableName (WHERE ์กฐ๊ฑด ๋Œ€์ƒ) + SELECT DISTINCT sl.properties->'componentConfig'->'rightPanel'->>'tableName' as main_table + FROM screen_definitions sd + JOIN screen_layouts sl ON sd.screen_id = sl.screen_id + WHERE sd.screen_id = ANY($1) + AND sl.properties->'componentConfig'->'rightPanel'->>'tableName' IS NOT NULL + `; + + const globalMainTablesResult = await pool.query(globalMainTablesQuery, [screenIds]); + const globalMainTables = globalMainTablesResult.rows + .map((r: any) => r.main_table) + .filter((t: string) => t != null && t !== ''); + + logger.info("์ „์—ญ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์ˆ˜์ง‘ ์™„๋ฃŒ", { + count: globalMainTables.length, + tables: globalMainTables + }); + res.json({ success: true, data: screenSubTables, + globalMainTables: globalMainTables, // ๋ฉ”์ธ ํ…Œ์ด๋ธ”๋กœ ๋ถ„๋ฅ˜๋˜์–ด์•ผ ํ•˜๋Š” ํ…Œ์ด๋ธ” ๋ชฉ๋ก }); } catch (error: any) { logger.error("ํ™”๋ฉด ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์ •๋ณด ์กฐํšŒ ์‹คํŒจ:", error); diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index e38e2cc5..a494ae3d 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -557,7 +557,16 @@ export async function updateColumnInputType( ): Promise { try { const { tableName, columnName } = req.params; - const { inputType, detailSettings } = req.body; + let { inputType, detailSettings } = req.body; + + // ๐Ÿ”ฅ "direct" ๋˜๋Š” "auto"๋Š” ํ”„๋ก ํŠธ์—”๋“œ์˜ ์ž…๋ ฅ ๋ฐฉ์‹ ๊ตฌ๋ถ„๊ฐ’์ด๋ฏ€๋กœ + // DB์˜ input_type(์›นํƒ€์ž…)์œผ๋กœ ์ €์žฅํ•˜๋ฉด ์•ˆ ๋จ - "text"๋กœ ๋ณ€ํ™˜ + if (inputType === "direct" || inputType === "auto") { + logger.warn( + `์ž˜๋ชป๋œ inputType ๊ฐ’ ๊ฐ์ง€: ${inputType} โ†’ 'text'๋กœ ๋ณ€ํ™˜ (${tableName}.${columnName})` + ); + inputType = "text"; + } // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) let companyCode = req.user?.companyCode; @@ -662,14 +671,14 @@ export async function getTableRecord( logger.info(`ํ•„ํ„ฐ: ${filterColumn} = ${filterValue}`); logger.info(`ํ‘œ์‹œ ์ปฌ๋Ÿผ: ${displayColumn}`); - if (!tableName || !filterColumn || !filterValue || !displayColumn) { + if (!tableName || !filterColumn || !filterValue) { const response: ApiResponse = { success: false, message: "ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", error: { code: "MISSING_PARAMETERS", details: - "tableName, filterColumn, filterValue, displayColumn์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + "tableName, filterColumn, filterValue๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. displayColumn์€ ์„ ํƒ์ ์ž…๋‹ˆ๋‹ค.", }, }; res.status(400).json(response); @@ -701,9 +710,12 @@ export async function getTableRecord( } const record = result.data[0]; - const displayValue = record[displayColumn]; + // displayColumn์ด "*"์ด๊ฑฐ๋‚˜ ์—†์œผ๋ฉด ์ „์ฒด ๋ ˆ์ฝ”๋“œ ๋ฐ˜ํ™˜ + const displayValue = displayColumn && displayColumn !== "*" + ? record[displayColumn] + : record; - logger.info(`๋ ˆ์ฝ”๋“œ ์กฐํšŒ ์™„๋ฃŒ: ${displayColumn} = ${displayValue}`); + logger.info(`๋ ˆ์ฝ”๋“œ ์กฐํšŒ ์™„๋ฃŒ: ${displayColumn || "*"} = ${typeof displayValue === 'object' ? '[์ „์ฒด ๋ ˆ์ฝ”๋“œ]' : displayValue}`); const response: ApiResponse<{ value: any; record: any }> = { success: true, @@ -1357,8 +1369,17 @@ export async function updateColumnWebType( `๋ ˆ๊ฑฐ์‹œ API ์‚ฌ์šฉ: updateColumnWebType โ†’ updateColumnInputType ์‚ฌ์šฉ ๊ถŒ์žฅ` ); - // webType์„ inputType์œผ๋กœ ๋ณ€ํ™˜ - const convertedInputType = inputType || webType || "text"; + // ๐Ÿ”ฅ inputType์ด "direct" ๋˜๋Š” "auto"์ด๋ฉด ๋ฌด์‹œํ•˜๊ณ  webType ์‚ฌ์šฉ + // "direct"/"auto"๋Š” ํ”„๋ก ํŠธ์—”๋“œ์˜ ์ž…๋ ฅ ๋ฐฉ์‹(์ง์ ‘์ž…๋ ฅ/์ž๋™์ž…๋ ฅ) ๊ตฌ๋ถ„๊ฐ’์ด์ง€ + // DB์— ์ €์žฅํ•  ์›น ํƒ€์ž…(text, number, date ๋“ฑ)์ด ์•„๋‹˜ + let convertedInputType = webType || "text"; + if (inputType && inputType !== "direct" && inputType !== "auto") { + convertedInputType = inputType; + } + + logger.info( + `์›นํƒ€์ž… ๋ณ€ํ™˜: webType=${webType}, inputType=${inputType} โ†’ ${convertedInputType}` + ); // ์ƒˆ๋กœ์šด ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ req.body = { inputType: convertedInputType, detailSettings }; @@ -2323,6 +2344,8 @@ export async function getTableEntityRelations( * * table_type_columns์—์„œ reference_table์ด ํ˜„์žฌ ํ…Œ์ด๋ธ”์ธ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ฐพ์•„์„œ * ํ•ด๋‹น ํ…Œ์ด๋ธ”๊ณผ FK ์ปฌ๋Ÿผ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * ์šฐ์„ ์ˆœ์œ„: ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ company_code > ๊ณตํ†ต('*') */ export async function getReferencedByTables( req: AuthenticatedRequest, @@ -2330,9 +2353,11 @@ export async function getReferencedByTables( ): Promise { try { const { tableName } = req.params; + // ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ํšŒ์‚ฌ ์ฝ”๋“œ (์—†์œผ๋ฉด '*' ์‚ฌ์šฉ) + const userCompanyCode = req.user?.companyCode || "*"; logger.info( - `=== ํ…Œ์ด๋ธ” ์ฐธ์กฐ ๊ด€๊ณ„ ์กฐํšŒ ์‹œ์ž‘: ${tableName} ์„ ์ฐธ์กฐํ•˜๋Š” ํ…Œ์ด๋ธ” ===` + `=== ํ…Œ์ด๋ธ” ์ฐธ์กฐ ๊ด€๊ณ„ ์กฐํšŒ ์‹œ์ž‘: ${tableName} ์„ ์ฐธ์กฐํ•˜๋Š” ํ…Œ์ด๋ธ” (ํšŒ์‚ฌ์ฝ”๋“œ: ${userCompanyCode}) ===` ); if (!tableName) { @@ -2350,23 +2375,41 @@ export async function getReferencedByTables( // table_type_columns์—์„œ reference_table์ด ํ˜„์žฌ ํ…Œ์ด๋ธ”์ธ ๋ ˆ์ฝ”๋“œ ์กฐํšŒ // input_type์ด 'entity'์ธ ๊ฒƒ๋งŒ ์กฐํšŒ (์‹ค์ œ FK ๊ด€๊ณ„) + // ์šฐ์„ ์ˆœ์œ„: ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ company_code > ๊ณตํ†ต('*') + // ROW_NUMBER๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฐ™์€ ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ ์กฐํ•ฉ์—์„œ ํšŒ์‚ฌ์ฝ”๋“œ ์šฐ์„ ์ˆœ์œ„๋กœ ํ•˜๋‚˜๋งŒ ์„ ํƒ const sqlQuery = ` + WITH ranked AS ( + SELECT + ttc.table_name, + ttc.column_name, + ttc.column_label, + ttc.reference_table, + ttc.reference_column, + ttc.display_column, + ttc.company_code, + ROW_NUMBER() OVER ( + PARTITION BY ttc.table_name, ttc.column_name + ORDER BY CASE WHEN ttc.company_code = $2 THEN 1 ELSE 2 END + ) as rn + FROM table_type_columns ttc + WHERE ttc.reference_table = $1 + AND ttc.input_type = 'entity' + AND ttc.company_code IN ($2, '*') + ) SELECT DISTINCT - ttc.table_name, - ttc.column_name, - ttc.column_label, - ttc.reference_table, - ttc.reference_column, - ttc.display_column, - ttc.table_name as table_label - FROM table_type_columns ttc - WHERE ttc.reference_table = $1 - AND ttc.input_type = 'entity' - AND ttc.company_code = '*' - ORDER BY ttc.table_name, ttc.column_name + table_name, + column_name, + column_label, + reference_table, + reference_column, + display_column, + table_name as table_label + FROM ranked + WHERE rn = 1 + ORDER BY table_name, column_name `; - const result = await query(sqlQuery, [tableName]); + const result = await query(sqlQuery, [tableName, userCompanyCode]); const referencedByTables = result.map((row: any) => ({ tableName: row.table_name, @@ -2379,7 +2422,7 @@ export async function getReferencedByTables( })); logger.info( - `ํ…Œ์ด๋ธ” ์ฐธ์กฐ ๊ด€๊ณ„ ์กฐํšŒ ์™„๋ฃŒ: ${referencedByTables.length}๊ฐœ ๋ฐœ๊ฒฌ` + `ํ…Œ์ด๋ธ” ์ฐธ์กฐ ๊ด€๊ณ„ ์กฐํšŒ ์™„๋ฃŒ: ${referencedByTables.length}๊ฐœ ๋ฐœ๊ฒฌ (ํšŒ์‚ฌ์ฝ”๋“œ: ${userCompanyCode})` ); const response: ApiResponse = { diff --git a/backend-node/src/database/db.ts b/backend-node/src/database/db.ts index ae775525..4c249ac3 100644 --- a/backend-node/src/database/db.ts +++ b/backend-node/src/database/db.ts @@ -81,8 +81,26 @@ export const initializePool = (): Pool => { pool.on("error", (err, client) => { console.error("โŒ PostgreSQL ์—ฐ๊ฒฐ ํ’€ ์—๋Ÿฌ:", err); + // ์—ฐ๊ฒฐ ํ’€ ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์ž๋™ ์žฌ์—ฐ๊ฒฐ ์‹œ๋„ + // Pool์€ ์ž๋™์œผ๋กœ ์—ฐ๊ฒฐ์„ ์žฌ์ƒ์„ฑํ•˜๋ฏ€๋กœ ๋ณ„๋„ ์ฒ˜๋ฆฌ ๋ถˆํ•„์š” + // ๋‹ค๋งŒ, ์—ฐ์† ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์•Œ๋ฆผ์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Œ }); + // ์—ฐ๊ฒฐ ํ’€ ์ƒํƒœ ์ฒดํฌ (5๋ถ„๋งˆ๋‹ค) + setInterval(() => { + if (pool) { + const status = { + totalCount: pool.totalCount, + idleCount: pool.idleCount, + waitingCount: pool.waitingCount, + }; + // ๋Œ€๊ธฐ ์ค‘์ธ ์—ฐ๊ฒฐ์ด ๋งŽ์œผ๋ฉด ๊ฒฝ๊ณ  + if (status.waitingCount > 5) { + console.warn("โš ๏ธ PostgreSQL ์—ฐ๊ฒฐ ํ’€ ๋Œ€๊ธฐ์—ด ์ฆ๊ฐ€:", status); + } + } + }, 5 * 60 * 1000); + console.log( `๐Ÿš€ PostgreSQL ์—ฐ๊ฒฐ ํ’€ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}` ); diff --git a/backend-node/src/routes/commonCodeRoutes.ts b/backend-node/src/routes/commonCodeRoutes.ts index 3885d12a..d1205e51 100644 --- a/backend-node/src/routes/commonCodeRoutes.ts +++ b/backend-node/src/routes/commonCodeRoutes.ts @@ -73,20 +73,4 @@ router.get("/categories/:categoryCode/options", (req, res) => commonCodeController.getCodeOptions(req, res) ); -// ๊ณ„์ธต ๊ตฌ์กฐ ์ฝ”๋“œ ์กฐํšŒ (ํŠธ๋ฆฌ ํ˜•ํƒœ) -router.get("/categories/:categoryCode/hierarchy", (req, res) => - commonCodeController.getCodesHierarchy(req, res) -); - -// ์ž์‹ ์ฝ”๋“œ ์กฐํšŒ (์—ฐ์‡„ ์„ ํƒ์šฉ) -router.get("/categories/:categoryCode/children", (req, res) => - commonCodeController.getChildCodes(req, res) -); - -// ์นดํ…Œ๊ณ ๋ฆฌ โ†’ ๊ณตํ†ต์ฝ”๋“œ ํ˜ธํ™˜ API (๋ ˆ๊ฑฐ์‹œ ์ง€์›) -// ๊ธฐ์กด ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž…์ด ๊ณตํ†ต์ฝ”๋“œ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋œ ํ›„์—๋„ ๋™์ž‘ -router.get("/category-options/:tableName/:columnName", (req, res) => - commonCodeController.getCategoryOptionsAsCode(req, res) -); - export default router; diff --git a/backend-node/src/routes/fileRoutes.ts b/backend-node/src/routes/fileRoutes.ts index 64f02d14..562a0b7f 100644 --- a/backend-node/src/routes/fileRoutes.ts +++ b/backend-node/src/routes/fileRoutes.ts @@ -11,6 +11,7 @@ import { generateTempToken, getFileByToken, setRepresentativeFile, + getFileInfo, } from "../controllers/fileController"; import { authenticateToken } from "../middleware/authMiddleware"; @@ -24,6 +25,20 @@ const router = Router(); */ router.get("/public/:token", getFileByToken); +/** + * @route GET /api/files/preview/:objid + * @desc ํŒŒ์ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ด๋ฏธ์ง€ ๋“ฑ) - ๊ณต๊ฐœ ์ ‘๊ทผ ํ—ˆ์šฉ + * @access Public + */ +router.get("/preview/:objid", previewFile); + +/** + * @route GET /api/files/info/:objid + * @desc ํŒŒ์ผ ์ •๋ณด ์กฐํšŒ (๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋งŒ, ํŒŒ์ผ ๋‚ด์šฉ ์—†์Œ) - ๊ณต๊ฐœ ์ ‘๊ทผ ํ—ˆ์šฉ + * @access Public + */ +router.get("/info/:objid", getFileInfo); + // ๋ชจ๋“  ํŒŒ์ผ API๋Š” ์ธ์ฆ ํ•„์š” router.use(authenticateToken); @@ -64,12 +79,7 @@ router.get("/linked/:tableName/:recordId", getLinkedFiles); */ router.delete("/:objid", deleteFile); -/** - * @route GET /api/files/preview/:objid - * @desc ํŒŒ์ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ด๋ฏธ์ง€ ๋“ฑ) - * @access Private - */ -router.get("/preview/:objid", previewFile); +// preview ๋ผ์šฐํŠธ๋Š” ์ƒ๋‹จ ๊ณต๊ฐœ ์ ‘๊ทผ ๊ตฌ์—ญ์œผ๋กœ ์ด๋™๋จ /** * @route GET /api/files/download/:objid diff --git a/backend-node/src/routes/scheduleRoutes.ts b/backend-node/src/routes/scheduleRoutes.ts new file mode 100644 index 00000000..98dbc771 --- /dev/null +++ b/backend-node/src/routes/scheduleRoutes.ts @@ -0,0 +1,33 @@ +/** + * ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ ๋ผ์šฐํ„ฐ + */ + +import { Router } from "express"; +import { ScheduleController } from "../controllers/scheduleController"; +import { authenticateToken } from "../middleware/authMiddleware"; + +const router = Router(); +const scheduleController = new ScheduleController(); + +// ๋ชจ๋“  ์Šค์ผ€์ค„ ๋ผ์šฐํŠธ์— ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ์ ์šฉ +router.use(authenticateToken); + +// ==================== ์Šค์ผ€์ค„ ์ƒ์„ฑ ==================== + +// ์Šค์ผ€์ค„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ +router.post("/preview", scheduleController.preview); + +// ์Šค์ผ€์ค„ ์ ์šฉ +router.post("/apply", scheduleController.apply); + +// ==================== ์Šค์ผ€์ค„ ์กฐํšŒ ==================== + +// ์Šค์ผ€์ค„ ๋ชฉ๋ก ์กฐํšŒ +router.get("/list", scheduleController.list); + +// ==================== ์Šค์ผ€์ค„ ์‚ญ์ œ ==================== + +// ์Šค์ผ€์ค„ ์‚ญ์ œ +router.delete("/:scheduleId", scheduleController.delete); + +export default router; diff --git a/backend-node/src/services/categoryTreeService.ts b/backend-node/src/services/categoryTreeService.ts index 985e671f..1550a780 100644 --- a/backend-node/src/services/categoryTreeService.ts +++ b/backend-node/src/services/categoryTreeService.ts @@ -43,6 +43,7 @@ export interface CreateCategoryValueInput { icon?: string; isActive?: boolean; isDefault?: boolean; + targetCompanyCode?: string; // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ํŠน์ • ํšŒ์‚ฌ๋ฅผ ์„ ํƒํ•  ๋•Œ ์‚ฌ์šฉ } // ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ˆ˜์ • ์ž…๋ ฅ @@ -89,7 +90,7 @@ class CategoryTreeService { updated_at AS "updatedAt", created_by AS "createdBy", updated_by AS "updatedBy" - FROM category_values_test + FROM category_values WHERE (company_code = $1 OR company_code = '*') AND table_name = $2 AND column_name = $3 @@ -142,7 +143,7 @@ class CategoryTreeService { company_code AS "companyCode", created_at AS "createdAt", updated_at AS "updatedAt" - FROM category_values_test + FROM category_values WHERE (company_code = $1 OR company_code = '*') AND table_name = $2 AND column_name = $3 @@ -184,7 +185,7 @@ class CategoryTreeService { company_code AS "companyCode", created_at AS "createdAt", updated_at AS "updatedAt" - FROM category_values_test + FROM category_values WHERE (company_code = $1 OR company_code = '*') AND value_id = $2 `; @@ -221,7 +222,7 @@ class CategoryTreeService { } const query = ` - INSERT INTO category_values_test ( + INSERT INTO category_values ( table_name, column_name, value_code, value_label, value_order, parent_value_id, depth, path, description, color, icon, is_active, is_default, company_code, created_by, updated_by @@ -334,7 +335,7 @@ class CategoryTreeService { } const query = ` - UPDATE category_values_test + UPDATE category_values SET value_code = COALESCE($3, value_code), value_label = COALESCE($4, value_label), @@ -415,11 +416,11 @@ class CategoryTreeService { // ์žฌ๊ท€ CTE๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ๋“  ํ•˜์œ„ ์นดํ…Œ๊ณ ๋ฆฌ ์ˆ˜์ง‘ const query = ` WITH RECURSIVE category_tree AS ( - SELECT value_id FROM category_values_test + SELECT value_id FROM category_values WHERE parent_value_id = $1 AND (company_code = $2 OR company_code = '*') UNION ALL SELECT cv.value_id - FROM category_values_test cv + FROM category_values cv INNER JOIN category_tree ct ON cv.parent_value_id = ct.value_id WHERE cv.company_code = $2 OR cv.company_code = '*' ) @@ -452,7 +453,7 @@ class CategoryTreeService { for (const id of reversedIds) { await pool.query( - `DELETE FROM category_values_test WHERE (company_code = $1 OR company_code = '*') AND value_id = $2`, + `DELETE FROM category_values WHERE (company_code = $1 OR company_code = '*') AND value_id = $2`, [companyCode, id] ); } @@ -479,7 +480,7 @@ class CategoryTreeService { const query = ` SELECT value_id, value_label - FROM category_values_test + FROM category_values WHERE (company_code = $1 OR company_code = '*') AND parent_value_id = $2 `; @@ -488,7 +489,7 @@ class CategoryTreeService { for (const child of result.rows) { const newPath = `${parentPath}/${child.value_label}`; - await pool.query(`UPDATE category_values_test SET path = $1, updated_at = NOW() WHERE value_id = $2`, [ + await pool.query(`UPDATE category_values SET path = $1, updated_at = NOW() WHERE value_id = $2`, [ newPath, child.value_id, ]); @@ -550,7 +551,7 @@ class CategoryTreeService { /** * ์ „์ฒด ์นดํ…Œ๊ณ ๋ฆฌ ํ‚ค ๋ชฉ๋ก ์กฐํšŒ (๋ชจ๋“  ํ…Œ์ด๋ธ”.์ปฌ๋Ÿผ ์กฐํ•ฉ) - * category_values_test ํ…Œ์ด๋ธ”์—์„œ ๊ณ ์œ ํ•œ table_name, column_name ์กฐํ•ฉ์„ ์กฐํšŒ + * category_values ํ…Œ์ด๋ธ”์—์„œ ๊ณ ์œ ํ•œ table_name, column_name ์กฐํ•ฉ์„ ์กฐํšŒ * ๋ผ๋ฒจ ์ •๋ณด๋„ ํ•จ๊ป˜ ๋ฐ˜ํ™˜ */ async getAllCategoryKeys(companyCode: string): Promise<{ tableName: string; columnName: string; tableLabel: string; columnLabel: string }[]> { @@ -564,7 +565,7 @@ class CategoryTreeService { cv.column_name AS "columnName", COALESCE(tl.table_label, cv.table_name) AS "tableLabel", COALESCE(ttc.column_label, cv.column_name) AS "columnLabel" - FROM category_values_test cv + FROM category_values cv LEFT JOIN table_labels tl ON tl.table_name = cv.table_name LEFT JOIN table_type_columns ttc ON ttc.table_name = cv.table_name AND ttc.column_name = cv.column_name AND ttc.company_code = '*' WHERE cv.company_code = $1 OR cv.company_code = '*' diff --git a/backend-node/src/services/menuCopyService.ts b/backend-node/src/services/menuCopyService.ts index 1980a82c..e91124af 100644 --- a/backend-node/src/services/menuCopyService.ts +++ b/backend-node/src/services/menuCopyService.ts @@ -851,47 +851,10 @@ export class MenuCopyService { ]); logger.info(` โœ… ๋ฉ”๋‰ด ๊ถŒํ•œ ์‚ญ์ œ ์™„๋ฃŒ`); - // 5-4. ์ฑ„๋ฒˆ ๊ทœ์น™ ์ฒ˜๋ฆฌ (์ฒดํฌ ์ œ์•ฝ์กฐ๊ฑด ๊ณ ๋ ค) - // scope_type = 'menu'์ธ ์ฑ„๋ฒˆ ๊ทœ์น™: ๋ฉ”๋‰ด ์ „์šฉ์ด๋ฏ€๋กœ ์‚ญ์ œ (ํŒŒํŠธ ํฌํ•จ) - // check_menu_scope_requires_menu_objid ์ œ์•ฝ: scope_type='menu'์ด๋ฉด menu_objid NOT NULL ํ•„์ˆ˜ - const menuScopedRulesResult = await client.query( - `SELECT rule_id FROM numbering_rules - WHERE menu_objid = ANY($1) AND company_code = $2 AND scope_type = 'menu'`, - [existingMenuIds, targetCompanyCode] - ); - if (menuScopedRulesResult.rows.length > 0) { - const menuScopedRuleIds = menuScopedRulesResult.rows.map( - (r) => r.rule_id - ); - // ์ฑ„๋ฒˆ ๊ทœ์น™ ํŒŒํŠธ ๋จผ์ € ์‚ญ์ œ - await client.query( - `DELETE FROM numbering_rule_parts WHERE rule_id = ANY($1)`, - [menuScopedRuleIds] - ); - // ์ฑ„๋ฒˆ ๊ทœ์น™ ์‚ญ์ œ - await client.query( - `DELETE FROM numbering_rules WHERE rule_id = ANY($1)`, - [menuScopedRuleIds] - ); - logger.info( - ` โœ… ๋ฉ”๋‰ด ์ „์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์‚ญ์ œ: ${menuScopedRuleIds.length}๊ฐœ` - ); - } - - // scope_type != 'menu'์ธ ์ฑ„๋ฒˆ ๊ทœ์น™: menu_objid๋งŒ NULL๋กœ ์„ค์ • (๊ทœ์น™ ๋ณด์กด) - const updatedNumberingRules = await client.query( - `UPDATE numbering_rules - SET menu_objid = NULL - WHERE menu_objid = ANY($1) AND company_code = $2 - AND (scope_type IS NULL OR scope_type != 'menu') - RETURNING rule_id`, - [existingMenuIds, targetCompanyCode] - ); - if (updatedNumberingRules.rowCount && updatedNumberingRules.rowCount > 0) { - logger.info( - ` โœ… ํ…Œ์ด๋ธ” ์Šค์ฝ”ํ”„ ์ฑ„๋ฒˆ ๊ทœ์น™ ์—ฐ๊ฒฐ ํ•ด์ œ: ${updatedNumberingRules.rowCount}๊ฐœ (๋ฐ์ดํ„ฐ ๋ณด์กด๋จ)` - ); - } + // 5-4. ์ฑ„๋ฒˆ ๊ทœ์น™ ์ฒ˜๋ฆฌ (์ƒˆ ์Šคํ‚ค๋งˆ์—์„œ๋Š” menu_objid ์—†์Œ - ์Šคํ‚ต) + // ์ƒˆ numbering_rules ์Šคํ‚ค๋งˆ: table_name + column_name + company_code ๊ธฐ๋ฐ˜ + // ๋ฉ”๋‰ด์™€ ์ง์ ‘ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๋ฉ”๋‰ด ์‚ญ์ œ ์‹œ ์ฒ˜๋ฆฌ ๋ถˆํ•„์š” + logger.info(` โญ๏ธ ์ฑ„๋ฒˆ ๊ทœ์น™: ์ƒˆ ์Šคํ‚ค๋งˆ์—์„œ๋Š” ๋ฉ”๋‰ด์™€ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์Œ (์Šคํ‚ต)`); // 5-5. ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์‚ญ์ œ (menu_objid๊ฐ€ NOT NULL์ด๋ฏ€๋กœ NULL ์„ค์ • ๋ถˆ๊ฐ€) // ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘์€ ๋ฉ”๋‰ด์™€ ๊ฐ•ํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ํ•จ๊ป˜ ์‚ญ์ œ @@ -961,6 +924,16 @@ export class MenuCopyService { const menus = await this.collectMenuTree(sourceMenuObjid, client); const sourceCompanyCode = menus[0].company_code!; + // ๊ฐ™์€ ํšŒ์‚ฌ๋กœ ๋ณต์ œํ•˜๋Š” ๊ฒฝ์šฐ ๊ฒฝ๊ณ  (์ž๊ธฐ ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ ์†์ƒ ์œ„ํ—˜) + if (sourceCompanyCode === targetCompanyCode) { + logger.warn( + `โš ๏ธ ๊ฐ™์€ ํšŒ์‚ฌ๋กœ ๋ฉ”๋‰ด ๋ณต์ œ ์‹œ๋„: ${sourceCompanyCode} โ†’ ${targetCompanyCode}` + ); + warnings.push( + "๊ฐ™์€ ํšŒ์‚ฌ๋กœ ๋ณต์ œํ•˜๋ฉด ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ(์นดํ…Œ๊ณ ๋ฆฌ, ์ฑ„๋ฒˆ ๋“ฑ)๊ฐ€ ๋ณต์ œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค." + ); + } + const screenIds = await this.collectScreens( menus.map((m) => m.objid), sourceCompanyCode, @@ -1116,6 +1089,10 @@ export class MenuCopyService { client ); + // === 6.5๋‹จ๊ณ„: ๋ฉ”๋‰ด URL ์—…๋ฐ์ดํŠธ (ํ™”๋ฉด ID ์žฌ๋งคํ•‘) === + logger.info("\n๐Ÿ”„ [6.5๋‹จ๊ณ„] ๋ฉ”๋‰ด URL ํ™”๋ฉด ID ์žฌ๋งคํ•‘"); + await this.updateMenuUrls(menuIdMap, screenIdMap, client); + // === 7๋‹จ๊ณ„: ํ…Œ์ด๋ธ” ํƒ€์ž… ์„ค์ • ๋ณต์‚ฌ === if (additionalCopyOptions?.copyTableTypeColumns) { logger.info("\n๐Ÿ“ฆ [7๋‹จ๊ณ„] ํ…Œ์ด๋ธ” ํƒ€์ž… ์„ค์ • ๋ณต์‚ฌ"); @@ -1556,22 +1533,22 @@ export class MenuCopyService { // === ๊ธฐ์กด ๋ณต์‚ฌ๋ณธ์ด ์žˆ๋Š” ๊ฒฝ์šฐ: ์—…๋ฐ์ดํŠธ === const existingScreenId = existingCopy.screen_id; - // ์›๋ณธ ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ - const sourceLayoutsResult = await client.query( - `SELECT * FROM screen_layouts WHERE screen_id = $1 ORDER BY display_order`, + // ์›๋ณธ V2 ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ + const sourceLayoutV2Result = await client.query<{ layout_data: any }>( + `SELECT layout_data FROM screen_layouts_v2 WHERE screen_id = $1`, [originalScreenId] ); - // ๋Œ€์ƒ ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ - const targetLayoutsResult = await client.query( - `SELECT * FROM screen_layouts WHERE screen_id = $1 ORDER BY display_order`, + // ๋Œ€์ƒ V2 ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ + const targetLayoutV2Result = await client.query<{ layout_data: any }>( + `SELECT layout_data FROM screen_layouts_v2 WHERE screen_id = $1`, [existingScreenId] ); - // ๋ณ€๊ฒฝ ์—ฌ๋ถ€ ํ™•์ธ (๋ ˆ์ด์•„์›ƒ ๊ฐœ์ˆ˜ ๋˜๋Š” ๋‚ด์šฉ ๋น„๊ต) - const hasChanges = this.hasLayoutChanges( - sourceLayoutsResult.rows, - targetLayoutsResult.rows + // ๋ณ€๊ฒฝ ์—ฌ๋ถ€ ํ™•์ธ (V2 ๋ ˆ์ด์•„์›ƒ ๋น„๊ต) + const hasChanges = this.hasLayoutChangesV2( + sourceLayoutV2Result.rows[0]?.layout_data, + targetLayoutV2Result.rows[0]?.layout_data ); if (hasChanges) { @@ -1673,9 +1650,9 @@ export class MenuCopyService { } } - // === 2๋‹จ๊ณ„: screen_layouts ์ฒ˜๋ฆฌ (์ด์ œ screenIdMap์ด ์™„์„ฑ๋จ) === + // === 2๋‹จ๊ณ„: screen_layouts_v2 ์ฒ˜๋ฆฌ (์ด์ œ screenIdMap์ด ์™„์„ฑ๋จ) === logger.info( - `\n๐Ÿ“ ๋ ˆ์ด์•„์›ƒ ์ฒ˜๋ฆฌ ์‹œ์ž‘ (screenIdMap ์™„์„ฑ: ${screenIdMap.size}๊ฐœ)` + `\n๐Ÿ“ V2 ๋ ˆ์ด์•„์›ƒ ์ฒ˜๋ฆฌ ์‹œ์ž‘ (screenIdMap ์™„์„ฑ: ${screenIdMap.size}๊ฐœ)` ); for (const { @@ -1685,91 +1662,51 @@ export class MenuCopyService { isUpdate, } of screenDefsToProcess) { try { - // ์›๋ณธ ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ - const layoutsResult = await client.query( - `SELECT * FROM screen_layouts WHERE screen_id = $1 ORDER BY display_order`, + // ์›๋ณธ V2 ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ + const layoutV2Result = await client.query<{ layout_data: any }>( + `SELECT layout_data FROM screen_layouts_v2 WHERE screen_id = $1`, [originalScreenId] ); - if (isUpdate) { - // ์—…๋ฐ์ดํŠธ: ๊ธฐ์กด ๋ ˆ์ด์•„์›ƒ ์‚ญ์ œ ํ›„ ์ƒˆ๋กœ ์‚ฝ์ž… - await client.query( - `DELETE FROM screen_layouts WHERE screen_id = $1`, - [targetScreenId] + const layoutData = layoutV2Result.rows[0]?.layout_data; + const components = layoutData?.components || []; + + if (layoutData && components.length > 0) { + // component_id ๋งคํ•‘ ์ƒ์„ฑ (์›๋ณธ โ†’ ์ƒˆ ID) + const componentIdMap = new Map(); + const timestamp = Date.now(); + components.forEach((comp: any, idx: number) => { + const newComponentId = `comp_${timestamp}_${idx}_${Math.random().toString(36).substr(2, 5)}`; + componentIdMap.set(comp.id, newComponentId); + }); + + // V2 ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ ๋ณต์‚ฌ ๋ฐ ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ + const updatedLayoutData = this.updateReferencesInLayoutDataV2( + layoutData, + componentIdMap, + screenIdMap, + flowIdMap, + numberingRuleIdMap, + menuIdMap ); - logger.info(` โ†ณ ๊ธฐ์กด ๋ ˆ์ด์•„์›ƒ ์‚ญ์ œ (์—…๋ฐ์ดํŠธ ์ค€๋น„)`); - } - // component_id ๋งคํ•‘ ์ƒ์„ฑ (์›๋ณธ โ†’ ์ƒˆ ID) - const componentIdMap = new Map(); - const timestamp = Date.now(); - layoutsResult.rows.forEach((layout, idx) => { - const newComponentId = `comp_${timestamp}_${idx}_${Math.random().toString(36).substr(2, 5)}`; - componentIdMap.set(layout.component_id, newComponentId); - }); - - // ๋ ˆ์ด์•„์›ƒ ๋ฐฐ์น˜ ์‚ฝ์ž… ์ค€๋น„ - if (layoutsResult.rows.length > 0) { - const layoutValues: string[] = []; - const layoutParams: any[] = []; - let paramIdx = 1; - - for (const layout of layoutsResult.rows) { - const newComponentId = componentIdMap.get(layout.component_id)!; - - const newParentId = layout.parent_id - ? componentIdMap.get(layout.parent_id) || layout.parent_id - : null; - const newZoneId = layout.zone_id - ? componentIdMap.get(layout.zone_id) || layout.zone_id - : null; - - const updatedProperties = this.updateReferencesInProperties( - layout.properties, - screenIdMap, - flowIdMap, - numberingRuleIdMap, - menuIdMap - ); - - layoutValues.push( - `($${paramIdx}, $${paramIdx + 1}, $${paramIdx + 2}, $${paramIdx + 3}, $${paramIdx + 4}, $${paramIdx + 5}, $${paramIdx + 6}, $${paramIdx + 7}, $${paramIdx + 8}, $${paramIdx + 9}, $${paramIdx + 10}, $${paramIdx + 11}, $${paramIdx + 12}, $${paramIdx + 13})` - ); - layoutParams.push( - targetScreenId, - layout.component_type, - newComponentId, - newParentId, - layout.position_x, - layout.position_y, - layout.width, - layout.height, - updatedProperties, - layout.display_order, - layout.layout_type, - layout.layout_config, - layout.zones_config, - newZoneId - ); - paramIdx += 14; - } - - // ๋ฐฐ์น˜ INSERT + // V2 ๋ ˆ์ด์•„์›ƒ ์ €์žฅ (UPSERT) await client.query( - `INSERT INTO screen_layouts ( - screen_id, component_type, component_id, parent_id, - position_x, position_y, width, height, properties, - display_order, layout_type, layout_config, zones_config, zone_id - ) VALUES ${layoutValues.join(", ")}`, - layoutParams + `INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data, created_at, updated_at) + VALUES ($1, $2, $3, NOW(), NOW()) + ON CONFLICT (screen_id, company_code) + DO UPDATE SET layout_data = $3, updated_at = NOW()`, + [targetScreenId, targetCompanyCode, JSON.stringify(updatedLayoutData)] ); - } - const action = isUpdate ? "์—…๋ฐ์ดํŠธ" : "๋ณต์‚ฌ"; - logger.info(` โ†ณ ๋ ˆ์ด์•„์›ƒ ${action}: ${layoutsResult.rows.length}๊ฐœ`); + const action = isUpdate ? "์—…๋ฐ์ดํŠธ" : "๋ณต์‚ฌ"; + logger.info(` โ†ณ V2 ๋ ˆ์ด์•„์›ƒ ${action}: ${components.length}๊ฐœ ์ปดํฌ๋„ŒํŠธ`); + } else { + logger.info(` โ†ณ V2 ๋ ˆ์ด์•„์›ƒ ์—†์Œ (์Šคํ‚ต): screen_id=${originalScreenId}`); + } } catch (error: any) { logger.error( - `โŒ ๋ ˆ์ด์•„์›ƒ ์ฒ˜๋ฆฌ ์‹คํŒจ: screen_id=${originalScreenId}`, + `โŒ V2 ๋ ˆ์ด์•„์›ƒ ์ฒ˜๋ฆฌ ์‹คํŒจ: screen_id=${originalScreenId}`, error ); throw error; @@ -1835,6 +1772,83 @@ export class MenuCopyService { return false; } + /** + * V2 ๋ ˆ์ด์•„์›ƒ ๋ณ€๊ฒฝ ์—ฌ๋ถ€ ํ™•์ธ (screen_layouts_v2์šฉ) + */ + private hasLayoutChangesV2( + sourceLayoutData: any, + targetLayoutData: any + ): boolean { + // 1. ๋‘˜ ๋‹ค ์—†์œผ๋ฉด ๋ณ€๊ฒฝ ์—†์Œ + if (!sourceLayoutData && !targetLayoutData) return false; + + // 2. ํ•˜๋‚˜๋งŒ ์žˆ์œผ๋ฉด ๋ณ€๊ฒฝ๋จ + if (!sourceLayoutData || !targetLayoutData) return true; + + // 3. components ๋ฐฐ์—ด ๋น„๊ต + const sourceComps = sourceLayoutData.components || []; + const targetComps = targetLayoutData.components || []; + + if (sourceComps.length !== targetComps.length) return true; + + // 4. ๊ฐ ์ปดํฌ๋„ŒํŠธ ๋น„๊ต (url, position, size, overrides) + for (let i = 0; i < sourceComps.length; i++) { + const s = sourceComps[i]; + const t = targetComps[i]; + + if (s.url !== t.url) return true; + if (JSON.stringify(s.position) !== JSON.stringify(t.position)) return true; + if (JSON.stringify(s.size) !== JSON.stringify(t.size)) return true; + if (JSON.stringify(s.overrides) !== JSON.stringify(t.overrides)) return true; + } + + return false; + } + + /** + * V2 ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ์˜ ์ฐธ์กฐ ID๋“ค์„ ์—…๋ฐ์ดํŠธ (componentId, flowId, ruleId, screenId, menuId) + */ + private updateReferencesInLayoutDataV2( + layoutData: any, + componentIdMap: Map, + screenIdMap: Map, + flowIdMap: Map, + numberingRuleIdMap?: Map, + menuIdMap?: Map + ): any { + if (!layoutData?.components) return layoutData; + + const updatedComponents = layoutData.components.map((comp: any) => { + // 1. componentId ๋งคํ•‘ + const newId = componentIdMap.get(comp.id) || comp.id; + + // 2. overrides ๋ณต์‚ฌ ๋ฐ ์žฌ๊ท€์  ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ + let overrides = JSON.parse(JSON.stringify(comp.overrides || {})); + + // ์žฌ๊ท€์ ์œผ๋กœ ๋ชจ๋“  ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ + this.recursiveUpdateReferences( + overrides, + screenIdMap, + flowIdMap, + "", + numberingRuleIdMap, + menuIdMap + ); + + return { + ...comp, + id: newId, + overrides, + }; + }); + + return { + ...layoutData, + components: updatedComponents, + updatedAt: new Date().toISOString(), + }; + } + /** * ๋ฉ”๋‰ด ์œ„์ƒ ์ •๋ ฌ (๋ถ€๋ชจ ๋จผ์ €) */ @@ -2231,6 +2245,68 @@ export class MenuCopyService { } } + /** + * ๋ฉ”๋‰ด URL ์—…๋ฐ์ดํŠธ (ํ™”๋ฉด ID ์žฌ๋งคํ•‘) + * menu_url์— ํฌํ•จ๋œ /screens/{screenId} ํ˜•์‹์˜ ํ™”๋ฉด ID๋ฅผ ๋ณต์ œ๋œ ํ™”๋ฉด ID๋กœ ๊ต์ฒด + */ + private async updateMenuUrls( + menuIdMap: Map, + screenIdMap: Map, + client: PoolClient + ): Promise { + if (menuIdMap.size === 0 || screenIdMap.size === 0) { + logger.info("๐Ÿ“ญ ๋ฉ”๋‰ด URL ์—…๋ฐ์ดํŠธ ๋Œ€์ƒ ์—†์Œ"); + return; + } + + const newMenuObjids = Array.from(menuIdMap.values()); + + // ๋ณต์ œ๋œ ๋ฉ”๋‰ด ์ค‘ menu_url์ด ์žˆ๋Š” ๊ฒƒ ์กฐํšŒ + const menusWithUrl = await client.query<{ + objid: number; + menu_url: string; + }>( + `SELECT objid, menu_url FROM menu_info + WHERE objid = ANY($1) AND menu_url IS NOT NULL AND menu_url != ''`, + [newMenuObjids] + ); + + if (menusWithUrl.rows.length === 0) { + logger.info("๐Ÿ“ญ menu_url ์—…๋ฐ์ดํŠธ ๋Œ€์ƒ ์—†์Œ"); + return; + } + + let updatedCount = 0; + const screenIdPattern = /\/screens\/(\d+)/; + + for (const menu of menusWithUrl.rows) { + const match = menu.menu_url.match(screenIdPattern); + if (!match) continue; + + const originalScreenId = parseInt(match[1], 10); + const newScreenId = screenIdMap.get(originalScreenId); + + if (newScreenId && newScreenId !== originalScreenId) { + const newMenuUrl = menu.menu_url.replace( + `/screens/${originalScreenId}`, + `/screens/${newScreenId}` + ); + + await client.query( + `UPDATE menu_info SET menu_url = $1 WHERE objid = $2`, + [newMenuUrl, menu.objid] + ); + + logger.info( + ` ๐Ÿ”— ๋ฉ”๋‰ด URL ์—…๋ฐ์ดํŠธ: ${menu.menu_url} โ†’ ${newMenuUrl}` + ); + updatedCount++; + } + } + + logger.info(`โœ… ๋ฉ”๋‰ด URL ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${updatedCount}๊ฐœ`); + } + /** * ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ + ์ฝ”๋“œ ๋ณต์‚ฌ (์ตœ์ ํ™”: ๋ฐฐ์น˜ ์กฐํšŒ/์‚ฝ์ž…) */ @@ -2477,8 +2553,9 @@ export class MenuCopyService { } /** - * ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ณต์‚ฌ (์ตœ์ ํ™”: ๋ฐฐ์น˜ ์กฐํšŒ/์‚ฝ์ž…) - * ํ™”๋ฉด ๋ณต์‚ฌ ์ „์— ํ˜ธ์ถœ๋˜์–ด numberingRuleId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ์— ์‚ฌ์šฉ๋จ + * ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ณต์‚ฌ (์ƒˆ ์Šคํ‚ค๋งˆ: table_name + column_name ๊ธฐ๋ฐ˜) + * ํ”„๋ก ํŠธ์—”๋“œ์—์„œ /numbering-rules/copy-for-company API๋ฅผ ๋ณ„๋„ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ + * ์ด ํ•จ์ˆ˜๋Š” ruleIdMap ์ƒ์„ฑ๋งŒ ๋‹ด๋‹น (์‹ค์ œ ๋ณต์ œ๋Š” numberingRuleService์—์„œ ์ฒ˜๋ฆฌ) */ private async copyNumberingRulesWithMap( menuObjids: number[], @@ -2487,222 +2564,47 @@ export class MenuCopyService { userId: string, client: PoolClient ): Promise<{ copiedCount: number; ruleIdMap: Map }> { - let copiedCount = 0; const ruleIdMap = new Map(); - if (menuObjids.length === 0) { - return { copiedCount, ruleIdMap }; - } - - // === ์ตœ์ ํ™”: ๋ฐฐ์น˜ ์กฐํšŒ === - // 1. ๋ชจ๋“  ์›๋ณธ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ•œ ๋ฒˆ์— ์กฐํšŒ - const allRulesResult = await client.query( - `SELECT * FROM numbering_rules WHERE menu_objid = ANY($1)`, - [menuObjids] + // ์ƒˆ ์Šคํ‚ค๋งˆ์—์„œ๋Š” ์ฑ„๋ฒˆ๊ทœ์น™์ด ๋ฉ”๋‰ด์™€ ์ง์ ‘ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์Œ + // ํ”„๋ก ํŠธ์—”๋“œ์—์„œ /numbering-rules/copy-for-company API๋ฅผ ๋ณ„๋„ ํ˜ธ์ถœ + // ์—ฌ๊ธฐ์„œ๋Š” ๊ธฐ์กด ๊ทœ์น™ ID๋ฅผ ๊ทธ๋Œ€๋กœ ๋งคํ•‘ (ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์˜ numberingRuleId ์ฐธ์กฐ์šฉ) + + // ์›๋ณธ ํšŒ์‚ฌ์˜ ์ฑ„๋ฒˆ๊ทœ์น™ ์กฐํšŒ (company_code ๊ธฐ๋ฐ˜) + const sourceRulesResult = await client.query( + `SELECT rule_id, rule_name FROM numbering_rules WHERE company_code = $1`, + [menuObjids.length > 0 ? (await client.query( + `SELECT company_code FROM menu_info WHERE objid = $1`, + [menuObjids[0]] + )).rows[0]?.company_code : null] ); - if (allRulesResult.rows.length === 0) { - logger.info(` ๐Ÿ“ญ ๋ณต์‚ฌํ•  ์ฑ„๋ฒˆ ๊ทœ์น™ ์—†์Œ`); - return { copiedCount, ruleIdMap }; - } - - // 2. ๋Œ€์ƒ ํšŒ์‚ฌ์— ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ชจ๋“  ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (์›๋ณธ ID + ์ƒˆ๋กœ ์ƒ์„ฑ๋  ID ๋ชจ๋‘ ์ฒดํฌ ํ•„์š”) - const existingRulesResult = await client.query( - `SELECT rule_id FROM numbering_rules WHERE company_code = $1`, + // ๋Œ€์ƒ ํšŒ์‚ฌ์˜ ์ฑ„๋ฒˆ๊ทœ์น™ ์กฐํšŒ (์ด๋ฆ„ ๊ธฐ์ค€ ๋งคํ•‘) + const targetRulesResult = await client.query( + `SELECT rule_id, rule_name FROM numbering_rules WHERE company_code = $1`, [targetCompanyCode] ); - const existingRuleIds = new Set( - existingRulesResult.rows.map((r) => r.rule_id) + + const targetRulesByName = new Map( + targetRulesResult.rows.map((r: any) => [r.rule_name, r.rule_id]) ); - // 3. ๋ณต์‚ฌํ•  ๊ทœ์น™๊ณผ ์Šคํ‚ตํ•  ๊ทœ์น™ ๋ถ„๋ฅ˜ - const rulesToCopy: any[] = []; - const originalToNewRuleMap: Array<{ original: string; new: string }> = []; - - // ๊ธฐ์กด ๊ทœ์น™ ์ค‘ menu_objid ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•œ ๊ทœ์น™๋“ค - const rulesToUpdate: Array<{ ruleId: string; newMenuObjid: number }> = []; - - for (const rule of allRulesResult.rows) { - // ์ƒˆ rule_id ๊ณ„์‚ฐ: ํšŒ์‚ฌ์ฝ”๋“œ ์ ‘๋‘์‚ฌ ์ œ๊ฑฐ ํ›„ ๋Œ€์ƒ ํšŒ์‚ฌ์ฝ”๋“œ ์ถ”๊ฐ€ - // ์˜ˆ: COMPANY_10_rule-123 -> rule-123 -> COMPANY_16_rule-123 - // ์˜ˆ: rule-123 -> rule-123 -> COMPANY_16_rule-123 - // ์˜ˆ: WACE_ํ’ˆ๋ชฉ์ฝ”๋“œ -> ํ’ˆ๋ชฉ์ฝ”๋“œ -> COMPANY_16_ํ’ˆ๋ชฉ์ฝ”๋“œ - let baseName = rule.rule_id; - - // ํšŒ์‚ฌ์ฝ”๋“œ ์ ‘๋‘์‚ฌ ํŒจํ„ด๋“ค์„ ์ˆœ์„œ๋Œ€๋กœ ์ œ๊ฑฐ ์‹œ๋„ - // 1. COMPANY_์ˆซ์ž_ ํŒจํ„ด (์˜ˆ: COMPANY_10_) - // 2. ์ผ๋ฐ˜ ์ ‘๋‘์‚ฌ_ ํŒจํ„ด (์˜ˆ: WACE_) - if (baseName.match(/^COMPANY_\d+_/)) { - baseName = baseName.replace(/^COMPANY_\d+_/, ""); - } else if (baseName.includes("_")) { - baseName = baseName.replace(/^[^_]+_/, ""); - } - - const newRuleId = `${targetCompanyCode}_${baseName}`; - - if (existingRuleIds.has(rule.rule_id)) { - // ์›๋ณธ ID๊ฐ€ ์ด๋ฏธ ์กด์žฌ (๋™์ผํ•œ ID๋กœ ๋งคํ•‘) - ruleIdMap.set(rule.rule_id, rule.rule_id); - - const newMenuObjid = menuIdMap.get(rule.menu_objid); - if (newMenuObjid) { - rulesToUpdate.push({ ruleId: rule.rule_id, newMenuObjid }); - } - logger.info(` โ™ป๏ธ ์ฑ„๋ฒˆ๊ทœ์น™ ์ด๋ฏธ ์กด์žฌ (์›๋ณธ ID): ${rule.rule_id}`); - } else if (existingRuleIds.has(newRuleId)) { - // ์ƒˆ๋กœ ์ƒ์„ฑ๋  ID๊ฐ€ ์ด๋ฏธ ์กด์žฌ (๊ธฐ์กด ๊ทœ์น™์œผ๋กœ ๋งคํ•‘) - ruleIdMap.set(rule.rule_id, newRuleId); - - const newMenuObjid = menuIdMap.get(rule.menu_objid); - if (newMenuObjid) { - rulesToUpdate.push({ ruleId: newRuleId, newMenuObjid }); - } - logger.info( - ` โ™ป๏ธ ์ฑ„๋ฒˆ๊ทœ์น™ ์ด๋ฏธ ์กด์žฌ (๋Œ€์ƒ ID): ${rule.rule_id} -> ${newRuleId}` - ); - } else { - // ์ƒˆ๋กœ ๋ณต์‚ฌ ํ•„์š” - ruleIdMap.set(rule.rule_id, newRuleId); - originalToNewRuleMap.push({ original: rule.rule_id, new: newRuleId }); - rulesToCopy.push({ ...rule, newRuleId }); - logger.info(` ๐Ÿ“‹ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์‚ฌ ์˜ˆ์ •: ${rule.rule_id} -> ${newRuleId}`); + // ์ด๋ฆ„ ๊ธฐ์ค€์œผ๋กœ ๋งคํ•‘ ์ƒ์„ฑ + for (const sourceRule of sourceRulesResult.rows) { + const targetRuleId = targetRulesByName.get(sourceRule.rule_name); + if (targetRuleId) { + ruleIdMap.set(sourceRule.rule_id, targetRuleId); + logger.info(` ๐Ÿ”— ์ฑ„๋ฒˆ๊ทœ์น™ ๋งคํ•‘: ${sourceRule.rule_id} -> ${targetRuleId}`); } } - // 4. ๋ฐฐ์น˜ INSERT๋กœ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ณต์‚ฌ - // menu ์Šค์ฝ”ํ”„์ธ๋ฐ menu_objid ๋งคํ•‘์ด ์—†๋Š” ๊ทœ์น™์€ ์ œ์™ธ (์—ฐ๊ฒฐ ์—†์ด ๋ณต์ œํ•˜์ง€ ์•Š์Œ) - const validRulesToCopy = rulesToCopy.filter((r) => { - if (r.scope_type === "menu") { - const newMenuObjid = menuIdMap.get(r.menu_objid); - if (newMenuObjid === undefined) { - logger.info(` โญ๏ธ ์ฑ„๋ฒˆ๊ทœ์น™ "${r.rule_name}" ๊ฑด๋„ˆ๋œ€: ๋ฉ”๋‰ด ์—ฐ๊ฒฐ ์—†์Œ (์›๋ณธ menu_objid: ${r.menu_objid})`); - // ruleIdMap์—์„œ๋„ ์ œ๊ฑฐ - ruleIdMap.delete(r.rule_id); - return false; // ๋ณต์ œ ๋Œ€์ƒ์—์„œ ์ œ์™ธ - } - } - return true; - }); - - if (validRulesToCopy.length > 0) { - const ruleValues = validRulesToCopy - .map( - (_, i) => - `($${i * 13 + 1}, $${i * 13 + 2}, $${i * 13 + 3}, $${i * 13 + 4}, $${i * 13 + 5}, $${i * 13 + 6}, $${i * 13 + 7}, $${i * 13 + 8}, $${i * 13 + 9}, NOW(), $${i * 13 + 10}, $${i * 13 + 11}, $${i * 13 + 12}, $${i * 13 + 13})` - ) - .join(", "); - - const ruleParams = validRulesToCopy.flatMap((r) => { - const newMenuObjid = menuIdMap.get(r.menu_objid); - // menu ์Šค์ฝ”ํ”„์ธ ๊ฒฝ์šฐ ๋ฐ˜๋“œ์‹œ menu_objid๊ฐ€ ์žˆ์Œ (์œ„์—์„œ ํ•„ํ„ฐ๋ง๋จ) - const finalMenuObjid = newMenuObjid !== undefined ? newMenuObjid : null; - // scope_type์€ ์›๋ณธ ์œ ์ง€ (menu ์Šค์ฝ”ํ”„๋Š” ๋ฐ˜๋“œ์‹œ menu_objid๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ) - const finalScopeType = r.scope_type; - - return [ - r.newRuleId, - r.rule_name, - r.description, - r.separator, - r.reset_period, - 0, - r.table_name, - r.column_name, - targetCompanyCode, - userId, - finalMenuObjid, - finalScopeType, - null, - ]; - }); - - await client.query( - `INSERT INTO numbering_rules ( - rule_id, rule_name, description, separator, reset_period, - current_sequence, table_name, column_name, company_code, - created_at, created_by, menu_objid, scope_type, last_generated_date - ) VALUES ${ruleValues}`, - ruleParams - ); - - copiedCount = validRulesToCopy.length; - logger.info(` โœ… ์ฑ„๋ฒˆ ๊ทœ์น™ ${copiedCount}๊ฐœ ๋ณต์‚ฌ (${rulesToCopy.length - validRulesToCopy.length}๊ฐœ ๊ฑด๋„ˆ๋œ€)`); - } - - // 4-1. ๊ธฐ์กด ์ฑ„๋ฒˆ ๊ทœ์น™์˜ menu_objid ์—…๋ฐ์ดํŠธ (์ƒˆ ๋ฉ”๋‰ด์™€ ์—ฐ๊ฒฐ) - ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ - if (rulesToUpdate.length > 0) { - // CASE WHEN์„ ์‚ฌ์šฉํ•œ ๋ฐฐ์น˜ ์—…๋ฐ์ดํŠธ - // menu_objid๋Š” numeric ํƒ€์ž…์ด๋ฏ€๋กœ ::numeric ์บ์ŠคํŒ… ํ•„์š” - const caseWhen = rulesToUpdate - .map( - (_, i) => `WHEN rule_id = $${i * 2 + 1} THEN $${i * 2 + 2}::numeric` - ) - .join(" "); - const ruleIdsForUpdate = rulesToUpdate.map((r) => r.ruleId); - const params = rulesToUpdate.flatMap((r) => [r.ruleId, r.newMenuObjid]); - - await client.query( - `UPDATE numbering_rules - SET menu_objid = CASE ${caseWhen} END, updated_at = NOW() - WHERE rule_id = ANY($${params.length + 1}) AND company_code = $${params.length + 2}`, - [...params, ruleIdsForUpdate, targetCompanyCode] - ); - logger.info( - ` โœ… ๊ธฐ์กด ์ฑ„๋ฒˆ ๊ทœ์น™ ${rulesToUpdate.length}๊ฐœ ๋ฉ”๋‰ด ์—ฐ๊ฒฐ ๊ฐฑ์‹ ` - ); - } - - // 5. ๋ชจ๋“  ์›๋ณธ ํŒŒํŠธ ํ•œ ๋ฒˆ์— ์กฐํšŒ (์ƒˆ๋กœ ๋ณต์‚ฌํ•œ ๊ทœ์น™๋งŒ ๋Œ€์ƒ) - if (rulesToCopy.length > 0) { - const originalRuleIds = rulesToCopy.map((r) => r.rule_id); - const allPartsResult = await client.query( - `SELECT * FROM numbering_rule_parts - WHERE rule_id = ANY($1) ORDER BY rule_id, part_order`, - [originalRuleIds] - ); - - // 6. ๋ฐฐ์น˜ INSERT๋กœ ์ฑ„๋ฒˆ ๊ทœ์น™ ํŒŒํŠธ ๋ณต์‚ฌ - if (allPartsResult.rows.length > 0) { - // ์›๋ณธ rule_id -> ์ƒˆ rule_id ๋งคํ•‘ - const ruleMapping = new Map( - originalToNewRuleMap.map((m) => [m.original, m.new]) - ); - - const partValues = allPartsResult.rows - .map( - (_, i) => - `($${i * 7 + 1}, $${i * 7 + 2}, $${i * 7 + 3}, $${i * 7 + 4}, $${i * 7 + 5}, $${i * 7 + 6}, $${i * 7 + 7}, NOW())` - ) - .join(", "); - - const partParams = allPartsResult.rows.flatMap((p) => [ - ruleMapping.get(p.rule_id), - p.part_order, - p.part_type, - p.generation_method, - p.auto_config, - p.manual_config, - targetCompanyCode, - ]); - - await client.query( - `INSERT INTO numbering_rule_parts ( - rule_id, part_order, part_type, generation_method, - auto_config, manual_config, company_code, created_at - ) VALUES ${partValues}`, - partParams - ); - - logger.info(` โœ… ์ฑ„๋ฒˆ ๊ทœ์น™ ํŒŒํŠธ ${allPartsResult.rows.length}๊ฐœ ๋ณต์‚ฌ`); - } - } - - logger.info( - `โœ… ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ณต์‚ฌ ์™„๋ฃŒ: ${copiedCount}๊ฐœ, ๋งคํ•‘: ${ruleIdMap.size}๊ฐœ` - ); - return { copiedCount, ruleIdMap }; + logger.info(` ๐Ÿ“‹ ์ฑ„๋ฒˆ๊ทœ์น™ ๋งคํ•‘ ์™„๋ฃŒ: ${ruleIdMap.size}๊ฐœ`); + + // ์‹ค์ œ ๋ณต์ œ๋Š” ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ณ„๋„ API ํ˜ธ์ถœ๋กœ ์ฒ˜๋ฆฌ๋จ + return { copiedCount: 0, ruleIdMap }; } + /** * ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ + ๊ฐ’ ๋ณต์‚ฌ (์ตœ์ ํ™”: ๋ฐฐ์น˜ ์กฐํšŒ) * diff --git a/backend-node/src/services/nodeFlowExecutionService.ts b/backend-node/src/services/nodeFlowExecutionService.ts index eadddf9f..9bc59d97 100644 --- a/backend-node/src/services/nodeFlowExecutionService.ts +++ b/backend-node/src/services/nodeFlowExecutionService.ts @@ -102,6 +102,80 @@ export interface NodeExecutionSummary { error?: string; } +// ===== ํ—ฌํผ ํ•จ์ˆ˜ ===== + +/** + * ๐Ÿ”ง ์œ ํšจํ•œ ๊ฐ’์ธ์ง€ ์ฒดํฌ (์ค‘๊ด„ํ˜ธ, ๋”ฐ์˜ดํ‘œ, ๋ฐฑ์Šฌ๋ž˜์‹œ ์—†์–ด์•ผ ํ•จ) + * ์ˆซ์ž๋„ ์œ ํšจํ•œ ๊ฐ’์œผ๋กœ ์ฒ˜๋ฆฌ + */ +function isValidDBValue(v: any): boolean { + // ์ˆซ์ž๋ฉด ์œ ํšจ (๋‚˜์ค‘์— ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜๋จ) + if (typeof v === "number" && !isNaN(v)) return true; + + // ๋ฌธ์ž์—ด์ด ์•„๋‹ˆ๋ฉด ๋ฌดํšจ + if (typeof v !== "string") return false; + if (!v || v.trim() === "") return false; + if (v.includes("{") || v.includes("}") || v.includes('"') || v.includes("\\")) return false; + return true; +} + +/** + * ๐Ÿ”ง ๊ฐ’์„ DB ์ €์žฅ์šฉ์œผ๋กœ ์ •๊ทœํ™” (PostgreSQL ๋ฐฐ์—ด ํ˜•์‹ ์ €์žฅ ๋ฐฉ์ง€) + * - JavaScript ๋ฐฐ์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด (์œ ํšจํ•œ ๊ฐ’๋งŒ) + * - PostgreSQL ๋ฐฐ์—ด ํ˜•์‹ ๋ฌธ์ž์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด (์œ ํšจํ•œ ๊ฐ’๋งŒ) + * - ์ค‘์ฒฉ๋œ ์ž˜๋ชป๋œ ํ˜•์‹ โ†’ null + */ +function normalizeValueForDB(value: any): any { + // 1. ๋ฐฐ์—ด์ด๋ฉด ์œ ํšจํ•œ ๊ฐ’๋งŒ ํ•„ํ„ฐ๋ง ํ›„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + if (Array.isArray(value)) { + // ์ˆซ์ž๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  ์œ ํšจํ•œ ๊ฐ’๋งŒ ํ•„ํ„ฐ๋ง + const validValues = value + .map(v => typeof v === "number" ? String(v) : v) + .filter(isValidDBValue) + .map(v => typeof v === "number" ? String(v) : v); // ์ตœ์ข… ๋ฌธ์ž์—ด ๋ณ€ํ™˜ + if (validValues.length === 0) { + console.warn(`โš ๏ธ [normalizeValueForDB] ๋ฐฐ์—ด์— ์œ ํšจํ•œ ๊ฐ’ ์—†์Œ:`, value); + return null; + } + const normalized = validValues.join(","); + console.log(`๐Ÿ”ง [normalizeValueForDB] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด:`, { original: value.length, valid: validValues.length, normalized }); + return normalized; + } + + // 2. ๋ฌธ์ž์—ด์ธ๋ฐ ์ž˜๋ชป๋œ ํ˜•์‹์ด๋ฉด ์ •๋ฆฌ + if (typeof value === "string" && value) { + // ์ž˜๋ชป๋œ ํ˜•์‹ ๊ฐ์ง€ + if (value.includes("{") || value.includes("}") || value.includes('\\"') || value.includes("\\\\")) { + console.warn(`โš ๏ธ [normalizeValueForDB] ์ž˜๋ชป๋œ ๋ฌธ์ž์—ด ํ˜•์‹:`, value.substring(0, 80)); + + // ์ •๊ทœํ‘œํ˜„์‹์œผ๋กœ ์œ ํšจํ•œ ์ฝ”๋“œ๋งŒ ์ถ”์ถœ + const codePattern = /\b(CAT_[A-Z0-9_]+|[A-Z]{2,}_[A-Z0-9_]+)\b/g; + const matches = value.match(codePattern); + + if (matches && matches.length > 0) { + const uniqueValues = [...new Set(matches)]; + const normalized = uniqueValues.join(","); + console.log(`๐Ÿ”ง [normalizeValueForDB] ์ฝ”๋“œ ์ถ”์ถœ:`, { count: uniqueValues.length, normalized }); + return normalized; + } + + console.warn(`โš ๏ธ [normalizeValueForDB] ์œ ํšจํ•œ ์ฝ”๋“œ ์—†์Œ, null ๋ฐ˜ํ™˜`); + return null; + } + + // ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด์ด๋ฉด ๊ฐ ๊ฐ’ ๊ฒ€์ฆ + if (value.includes(",")) { + const parts = value.split(",").map(v => v.trim()).filter(isValidDBValue); + if (parts.length === 0) { + return null; + } + return parts.join(","); + } + } + + return value; +} + // ===== ๋ฉ”์ธ ์‹คํ–‰ ์„œ๋น„์Šค ===== export class NodeFlowExecutionService { @@ -845,6 +919,9 @@ export class NodeFlowExecutionService { logger.info( `๐Ÿ“Š ์ปจํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ: ${context.dataSourceType}, ${context.sourceData.length}๊ฑด` ); + // ๐Ÿ” ๋””๋ฒ„๊น…: sourceData ๋‚ด์šฉ ์ถœ๋ ฅ + logger.info(`๐Ÿ“Š [ํ…Œ์ด๋ธ”์†Œ์Šค] sourceData ํ•„๋“œ: ${JSON.stringify(Object.keys(context.sourceData[0]))}`); + logger.info(`๐Ÿ“Š [ํ…Œ์ด๋ธ”์†Œ์Šค] sourceData.sabun: ${context.sourceData[0]?.sabun}`); return context.sourceData; } @@ -1016,10 +1093,12 @@ export class NodeFlowExecutionService { ); } - values.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + const normalizedValue = normalizeValueForDB(value); + values.push(normalizedValue); // ๐Ÿ”ฅ ์‚ฝ์ž…๋œ ๊ฐ’์„ ๋ฐ์ดํ„ฐ์— ๋ฐ˜์˜ - insertedData[mapping.targetField] = value; + insertedData[mapping.targetField] = normalizedValue; } // ๐Ÿ†• writer์™€ company_code ์ž๋™ ์ถ”๊ฐ€ (ํ•„๋“œ ๋งคํ•‘์— ์—†๋Š” ๊ฒฝ์šฐ) @@ -1152,9 +1231,11 @@ export class NodeFlowExecutionService { mapping.staticValue !== undefined ? mapping.staticValue : data[mapping.sourceField]; - values.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + const normalizedValue = normalizeValueForDB(value); + values.push(normalizedValue); // ๐Ÿ”ฅ ์‚ฝ์ž…๋œ ๋ฐ์ดํ„ฐ ๊ฐ์ฒด์— ๋งคํ•‘๋œ ๊ฐ’ ์ ์šฉ - insertedData[mapping.targetField] = value; + insertedData[mapping.targetField] = normalizedValue; }); // ์™ธ๋ถ€ DB๋ณ„ SQL ๋ฌธ๋ฒ• ์ฐจ์ด ์ฒ˜๋ฆฌ @@ -1490,7 +1571,8 @@ export class NodeFlowExecutionService { if (mapping.targetField) { setClauses.push(`${mapping.targetField} = $${paramIndex}`); - values.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + values.push(normalizeValueForDB(value)); paramIndex++; } }); @@ -1553,11 +1635,13 @@ export class NodeFlowExecutionService { // targetField๊ฐ€ ๋น„์–ด์žˆ์ง€ ์•Š์€ ๊ฒฝ์šฐ๋งŒ ์ถ”๊ฐ€ if (mapping.targetField) { setClauses.push(`${mapping.targetField} = $${paramIndex}`); - values.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + const normalizedValue = normalizeValueForDB(value); + values.push(normalizedValue); paramIndex++; // ๐Ÿ”ฅ ์—…๋ฐ์ดํŠธ๋œ ๊ฐ’์„ ๋ฐ์ดํ„ฐ์— ๋ฐ˜์˜ - updatedData[mapping.targetField] = value; + updatedData[mapping.targetField] = normalizedValue; } else { console.log( `โš ๏ธ targetField๊ฐ€ ๋น„์–ด์žˆ์–ด ์Šคํ‚ต: ${mapping.sourceField}` @@ -1682,10 +1766,12 @@ export class NodeFlowExecutionService { setClauses.push(`${mapping.targetField} = $${paramIndex}`); } - values.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + const normalizedValue = normalizeValueForDB(value); + values.push(normalizedValue); paramIndex++; // ๐Ÿ”ฅ ์—…๋ฐ์ดํŠธ๋œ ๋ฐ์ดํ„ฐ ๊ฐ์ฒด์— ๋งคํ•‘๋œ ๊ฐ’ ์ ์šฉ - updatedData[mapping.targetField] = value; + updatedData[mapping.targetField] = normalizedValue; }); // WHERE ์กฐ๊ฑด ์ƒ์„ฑ @@ -2314,7 +2400,8 @@ export class NodeFlowExecutionService { ? mapping.staticValue : data[mapping.sourceField]; setClauses.push(`${mapping.targetField} = $${paramIndex}`); - updateValues.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + updateValues.push(normalizeValueForDB(value)); paramIndex++; } }); @@ -2365,7 +2452,8 @@ export class NodeFlowExecutionService { ? mapping.staticValue : data[mapping.sourceField]; columns.push(mapping.targetField); - values.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + values.push(normalizeValueForDB(value)); }); // ๐Ÿ†• writer์™€ company_code ์ž๋™ ์ถ”๊ฐ€ (ํ•„๋“œ ๋งคํ•‘์— ์—†๋Š” ๊ฒฝ์šฐ) @@ -2546,7 +2634,8 @@ export class NodeFlowExecutionService { setClauses.push(`${mapping.targetField} = $${paramIndex}`); } - updateValues.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + updateValues.push(normalizeValueForDB(value)); paramIndex++; } }); @@ -2584,7 +2673,8 @@ export class NodeFlowExecutionService { ? mapping.staticValue : data[mapping.sourceField]; columns.push(mapping.targetField); - values.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + values.push(normalizeValueForDB(value)); }); let insertSql: string; diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index 83e9b705..4f5bf1e9 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -47,11 +47,11 @@ class NumberingRuleService { logger.info("์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ ์‹œ์ž‘", { companyCode }); const pool = getPool(); - + // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ company_code="*" ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Œ let query: string; let params: any[]; - + if (companyCode === "*") { // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๊ฐ€๋Šฅ query = ` @@ -65,8 +65,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -88,8 +88,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -107,7 +107,7 @@ class NumberingRuleService { for (const rule of result.rows) { let partsQuery: string; let partsParams: any[]; - + if (companyCode === "*") { // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ํŒŒํŠธ ์กฐํšŒ partsQuery = ` @@ -156,7 +156,7 @@ class NumberingRuleService { /** * ํ˜„์žฌ ๋ฉ”๋‰ด์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) - * + * * ๋ฉ”๋‰ด ์Šค์ฝ”ํ”„ ๊ทœ์น™: * - menuObjid๊ฐ€ ์ œ๊ณต๋˜๋ฉด ํ˜•์ œ ๋ฉ”๋‰ด์˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ํฌํ•จ * - ์šฐ์„ ์ˆœ์œ„: menu (ํ˜•์ œ ๋ฉ”๋‰ด) > table > global @@ -166,7 +166,7 @@ class NumberingRuleService { menuObjid?: number ): Promise { let menuAndChildObjids: number[] = []; // catch ๋ธ”๋ก์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•จ์ˆ˜ ์ตœ์ƒ๋‹จ์— ์„ ์–ธ - + try { logger.info("๋ฉ”๋‰ด๋ณ„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹œ์ž‘ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„)", { companyCode, @@ -178,14 +178,17 @@ class NumberingRuleService { // 1. ์„ ํƒํ•œ ๋ฉ”๋‰ด์™€ ํ•˜์œ„ ๋ฉ”๋‰ด OBJID ์กฐํšŒ (ํ˜•์ œ ๋ฉ”๋‰ด ์ œ์™ธ) if (menuObjid) { menuAndChildObjids = await getMenuAndChildObjids(menuObjid); - logger.info("์„ ํƒํ•œ ๋ฉ”๋‰ด ๋ฐ ํ•˜์œ„ ๋ฉ”๋‰ด OBJID ๋ชฉ๋ก", { menuObjid, menuAndChildObjids }); + logger.info("์„ ํƒํ•œ ๋ฉ”๋‰ด ๋ฐ ํ•˜์œ„ ๋ฉ”๋‰ด OBJID ๋ชฉ๋ก", { + menuObjid, + menuAndChildObjids, + }); } // menuObjid๊ฐ€ ์—†์œผ๋ฉด global ๊ทœ์น™๋งŒ ๋ฐ˜ํ™˜ if (!menuObjid || menuAndChildObjids.length === 0) { let query: string; let params: any[]; - + if (companyCode === "*") { // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  global ๊ทœ์น™ ์กฐํšŒ query = ` @@ -199,13 +202,13 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" FROM numbering_rules - WHERE scope_type = 'global' + WHERE 1=1 ORDER BY created_at DESC `; params = []; @@ -222,14 +225,13 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" FROM numbering_rules - WHERE company_code = $1 AND scope_type = 'global' - ORDER BY created_at DESC + WHERE company_code = $1 ORDER BY created_at DESC `; params = [companyCode]; } @@ -240,7 +242,7 @@ class NumberingRuleService { for (const rule of result.rows) { let partsQuery: string; let partsParams: any[]; - + if (companyCode === "*") { partsQuery = ` SELECT @@ -282,9 +284,9 @@ class NumberingRuleService { // ์šฐ์„ ์ˆœ์œ„: menu (ํ˜•์ œ ๋ฉ”๋‰ด) > table > global let query: string; let params: any[]; - + if (companyCode === "*") { - // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ๊ทœ์น™ ์กฐํšŒ (์„ ํƒํ•œ ๋ฉ”๋‰ด + ํ•˜์œ„ ๋ฉ”๋‰ด) + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ๊ทœ์น™ ์กฐํšŒ query = ` SELECT rule_id AS "ruleId", @@ -296,28 +298,18 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" FROM numbering_rules - WHERE - scope_type = 'global' - OR (scope_type = 'menu' AND menu_objid = ANY($1)) - OR (scope_type = 'table' AND menu_objid = ANY($1)) - ORDER BY - CASE - WHEN scope_type = 'menu' OR (scope_type = 'table' AND menu_objid = ANY($1)) THEN 1 - WHEN scope_type = 'table' THEN 2 - WHEN scope_type = 'global' THEN 3 - END, - created_at DESC + ORDER BY created_at DESC `; - params = [menuAndChildObjids]; - logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ฉ”๋‰ด ๋ฐ ํ•˜์œ„ ๋ฉ”๋‰ด ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ", { menuAndChildObjids }); + params = []; + logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ์ „์ฒด ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ"); } else { - // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ๊ทœ์น™๋งŒ ์กฐํšŒ (์„ ํƒํ•œ ๋ฉ”๋‰ด + ํ•˜์œ„ ๋ฉ”๋‰ด) + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ๊ทœ์น™๋งŒ ์กฐํšŒ query = ` SELECT rule_id AS "ruleId", @@ -329,33 +321,22 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" FROM numbering_rules WHERE company_code = $1 - AND ( - scope_type = 'global' - OR (scope_type = 'menu' AND menu_objid = ANY($2)) - OR (scope_type = 'table' AND menu_objid = ANY($2)) - ) - ORDER BY - CASE - WHEN scope_type = 'menu' OR (scope_type = 'table' AND menu_objid = ANY($2)) THEN 1 - WHEN scope_type = 'table' THEN 2 - WHEN scope_type = 'global' THEN 3 - END, - created_at DESC + ORDER BY created_at DESC `; - params = [companyCode, menuAndChildObjids]; - logger.info("ํšŒ์‚ฌ๋ณ„: ๋ฉ”๋‰ด ๋ฐ ํ•˜์œ„ ๋ฉ”๋‰ด ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ", { companyCode, menuAndChildObjids }); + params = [companyCode]; + logger.info("ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ", { companyCode }); } logger.info("๐Ÿ” ์ฑ„๋ฒˆ ๊ทœ์น™ ์ฟผ๋ฆฌ ์‹คํ–‰", { queryPreview: query.substring(0, 200), - paramsTypes: params.map(p => typeof p), + paramsTypes: params.map((p) => typeof p), paramsValues: params, }); @@ -368,7 +349,7 @@ class NumberingRuleService { try { let partsQuery: string; let partsParams: any[]; - + if (companyCode === "*") { partsQuery = ` SELECT @@ -401,7 +382,7 @@ class NumberingRuleService { const partsResult = await pool.query(partsQuery, partsParams); rule.parts = partsResult.rows; - + logger.info("โœ… ๊ทœ์น™ ํŒŒํŠธ ์กฐํšŒ ์„ฑ๊ณต", { ruleId: rule.ruleId, ruleName: rule.ruleName, @@ -475,8 +456,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -500,8 +481,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -559,11 +540,11 @@ class NumberingRuleService { companyCode: string ): Promise { const pool = getPool(); - + // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ company_code="*" ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Œ let query: string; let params: any[]; - + if (companyCode === "*") { // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ๊ทœ์น™ ์กฐํšŒ ๊ฐ€๋Šฅ query = ` @@ -577,8 +558,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -599,8 +580,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -620,7 +601,7 @@ class NumberingRuleService { // ํŒŒํŠธ ์ •๋ณด ์กฐํšŒ let partsQuery: string; let partsParams: any[]; - + if (companyCode === "*") { partsQuery = ` SELECT @@ -676,7 +657,7 @@ class NumberingRuleService { INSERT INTO numbering_rules ( rule_id, rule_name, description, separator, reset_period, current_sequence, table_name, column_name, company_code, - menu_objid, scope_type, created_by + category_column, category_value_id, created_by ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING rule_id AS "ruleId", @@ -688,8 +669,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -705,8 +686,8 @@ class NumberingRuleService { config.tableName || null, config.columnName || null, companyCode, - config.menuObjid || null, - config.scopeType || "global", + config.categoryColumn || null, + config.categoryValueId || null, userId, ]); @@ -778,8 +759,8 @@ class NumberingRuleService { reset_period = COALESCE($4, reset_period), table_name = COALESCE($5, table_name), column_name = COALESCE($6, column_name), - menu_objid = COALESCE($7, menu_objid), - scope_type = COALESCE($8, scope_type), + category_column = COALESCE($7, category_column), + category_value_id = COALESCE($8, category_value_id), updated_at = NOW() WHERE rule_id = $9 AND company_code = $10 RETURNING @@ -792,8 +773,8 @@ class NumberingRuleService { table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", + category_column AS "categoryColumn", + category_value_id AS "categoryValueId", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" @@ -806,8 +787,8 @@ class NumberingRuleService { updates.resetPeriod, updates.tableName, updates.columnName, - updates.menuObjid, - updates.scopeType, + updates.categoryColumn, + updates.categoryValueId, ruleId, companyCode, ]); @@ -858,12 +839,12 @@ class NumberingRuleService { return { ...ruleResult.rows[0], parts }; } catch (error: any) { await client.query("ROLLBACK"); - logger.error("์ฑ„๋ฒˆ ๊ทœ์น™ ์ˆ˜์ • ์‹คํŒจ", { + logger.error("์ฑ„๋ฒˆ ๊ทœ์น™ ์ˆ˜์ • ์‹คํŒจ", { ruleId, companyCode, error: error.message, stack: error.stack, - updates + updates, }); throw error; } finally { @@ -897,7 +878,7 @@ class NumberingRuleService { * @param formData ํผ ๋ฐ์ดํ„ฐ (์นดํ…Œ๊ณ ๋ฆฌ ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ์‹œ ์‚ฌ์šฉ) */ async previewCode( - ruleId: string, + ruleId: string, companyCode: string, formData?: Record ): Promise { @@ -908,8 +889,9 @@ class NumberingRuleService { .sort((a: any, b: any) => a.order - b.order) .map((part: any) => { if (part.generationMethod === "manual") { - // ์ˆ˜๋™ ์ž…๋ ฅ - ํ”Œ๋ ˆ์ด์Šคํ™€๋” ํ‘œ์‹œ (์‹ค์ œ ๊ฐ’์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅ) - return part.manualConfig?.placeholder || "____"; + // ์ˆ˜๋™ ์ž…๋ ฅ - ํ•ญ์ƒ ____ ๋งˆ์ปค ์‚ฌ์šฉ (ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ํŽธ์ง‘ ๊ฐ€๋Šฅํ•˜๊ฒŒ ์ฒ˜๋ฆฌ) + // placeholder ํ…์ŠคํŠธ๋Š” ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ณ„๋„๋กœ ํ‘œ์‹œ + return "____"; } const autoConfig = part.autoConfig || {}; @@ -932,21 +914,26 @@ class NumberingRuleService { case "date": { // ๋‚ ์งœ (๋‹ค์–‘ํ•œ ๋‚ ์งœ ํ˜•์‹) const dateFormat = autoConfig.dateFormat || "YYYYMMDD"; - + // ์ปฌ๋Ÿผ ๊ธฐ์ค€ ์ƒ์„ฑ์ธ ๊ฒฝ์šฐ ํผ ๋ฐ์ดํ„ฐ์—์„œ ๋‚ ์งœ ์ถ”์ถœ - if (autoConfig.useColumnValue && autoConfig.sourceColumnName && formData) { + if ( + autoConfig.useColumnValue && + autoConfig.sourceColumnName && + formData + ) { const columnValue = formData[autoConfig.sourceColumnName]; if (columnValue) { - const dateValue = columnValue instanceof Date - ? columnValue - : new Date(columnValue); - + const dateValue = + columnValue instanceof Date + ? columnValue + : new Date(columnValue); + if (!isNaN(dateValue.getTime())) { return this.formatDate(dateValue, dateFormat); } } } - + return this.formatDate(new Date(), dateFormat); } @@ -959,63 +946,68 @@ class NumberingRuleService { // ์นดํ…Œ๊ณ ๋ฆฌ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ์ƒ์„ฑ const categoryKey = autoConfig.categoryKey; // ์˜ˆ: "item_info.material" const categoryMappings = autoConfig.categoryMappings || []; - + if (!categoryKey || !formData) { - logger.warn("์นดํ…Œ๊ณ ๋ฆฌ ํ‚ค ๋˜๋Š” ํผ ๋ฐ์ดํ„ฐ ์—†์Œ", { categoryKey, hasFormData: !!formData }); + logger.warn("์นดํ…Œ๊ณ ๋ฆฌ ํ‚ค ๋˜๋Š” ํผ ๋ฐ์ดํ„ฐ ์—†์Œ", { + categoryKey, + hasFormData: !!formData, + }); return ""; } - + // categoryKey์—์„œ ์ปฌ๋Ÿผ๋ช… ์ถ”์ถœ (์˜ˆ: "item_info.material" -> "material") - const columnName = categoryKey.includes(".") - ? categoryKey.split(".")[1] + const columnName = categoryKey.includes(".") + ? categoryKey.split(".")[1] : categoryKey; - + // ํผ ๋ฐ์ดํ„ฐ์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ์˜ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ const selectedValue = formData[columnName]; - - logger.info("์นดํ…Œ๊ณ ๋ฆฌ ํŒŒํŠธ ์ฒ˜๋ฆฌ", { - categoryKey, - columnName, + + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ํŒŒํŠธ ์ฒ˜๋ฆฌ", { + categoryKey, + columnName, selectedValue, formDataKeys: Object.keys(formData), - mappingsCount: categoryMappings.length + mappingsCount: categoryMappings.length, }); - + if (!selectedValue) { - logger.warn("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์ด ์„ ํƒ๋˜์ง€ ์•Š์Œ", { columnName, formDataKeys: Object.keys(formData) }); + logger.warn("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์ด ์„ ํƒ๋˜์ง€ ์•Š์Œ", { + columnName, + formDataKeys: Object.keys(formData), + }); return ""; } - + // ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘์—์„œ ํ•ด๋‹น ๊ฐ’์— ๋Œ€ํ•œ ํ˜•์‹ ์ฐพ๊ธฐ // selectedValue๋Š” valueCode์ผ ์ˆ˜ ์žˆ์Œ (V2Select์—์„œ valueCode๋ฅผ value๋กœ ์‚ฌ์šฉ) const selectedValueStr = String(selectedValue); - const mapping = categoryMappings.find( - (m: any) => { - // ID๋กœ ๋งค์นญ - if (m.categoryValueId?.toString() === selectedValueStr) return true; - // ๋ผ๋ฒจ๋กœ ๋งค์นญ - if (m.categoryValueLabel === selectedValueStr) return true; - // valueCode๋กœ ๋งค์นญ (๋ผ๋ฒจ๊ณผ ๋™์ผํ•  ์ˆ˜ ์žˆ์Œ) - if (m.categoryValueLabel === selectedValueStr) return true; - return false; - } - ); - + const mapping = categoryMappings.find((m: any) => { + // ID๋กœ ๋งค์นญ + if (m.categoryValueId?.toString() === selectedValueStr) + return true; + // ๋ผ๋ฒจ๋กœ ๋งค์นญ + if (m.categoryValueLabel === selectedValueStr) return true; + // valueCode๋กœ ๋งค์นญ (๋ผ๋ฒจ๊ณผ ๋™์ผํ•  ์ˆ˜ ์žˆ์Œ) + if (m.categoryValueLabel === selectedValueStr) return true; + return false; + }); + if (mapping) { - logger.info("์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์ ์šฉ", { - selectedValue, + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์ ์šฉ", { + selectedValue, format: mapping.format, - categoryValueLabel: mapping.categoryValueLabel + categoryValueLabel: mapping.categoryValueLabel, }); return mapping.format || ""; } - - logger.warn("์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ", { - selectedValue, - availableMappings: categoryMappings.map((m: any) => ({ - id: m.categoryValueId, - label: m.categoryValueLabel - })) + + logger.warn("์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ", { + selectedValue, + availableMappings: categoryMappings.map((m: any) => ({ + id: m.categoryValueId, + label: m.categoryValueLabel, + })), }); return ""; } @@ -1027,7 +1019,12 @@ class NumberingRuleService { }); const previewCode = parts.join(rule.separator || ""); - logger.info("์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ƒ์„ฑ", { ruleId, previewCode, companyCode, hasFormData: !!formData }); + logger.info("์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ƒ์„ฑ", { + ruleId, + previewCode, + companyCode, + hasFormData: !!formData, + }); return previewCode; } @@ -1036,11 +1033,13 @@ class NumberingRuleService { * @param ruleId ์ฑ„๋ฒˆ ๊ทœ์น™ ID * @param companyCode ํšŒ์‚ฌ ์ฝ”๋“œ * @param formData ํผ ๋ฐ์ดํ„ฐ (๋‚ ์งœ ์ปฌ๋Ÿผ ๊ธฐ์ค€ ์ƒ์„ฑ ์‹œ ์‚ฌ์šฉ) + * @param userInputCode ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘ํ•œ ์ตœ์ข… ์ฝ”๋“œ (์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ถ”์ถœ์šฉ) */ async allocateCode( - ruleId: string, - companyCode: string, - formData?: Record + ruleId: string, + companyCode: string, + formData?: Record, + userInputCode?: string ): Promise { const pool = getPool(); const client = await pool.connect(); @@ -1051,11 +1050,117 @@ class NumberingRuleService { const rule = await this.getRuleById(ruleId, companyCode); if (!rule) throw new Error("๊ทœ์น™์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"); + // ์ˆ˜๋™ ์ž…๋ ฅ ํŒŒํŠธ๊ฐ€ ์žˆ๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ฝ”๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ถ”์ถœ + const manualParts = rule.parts.filter( + (p: any) => p.generationMethod === "manual" + ); + let extractedManualValues: string[] = []; + + if (manualParts.length > 0 && userInputCode) { + // ํ”„๋ฆฌ๋ทฐ ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•ด์„œ ____ ์œ„์น˜ ํŒŒ์•… + // ๐Ÿ”ง category ํŒŒํŠธ๋„ ์ฒ˜๋ฆฌํ•˜์—ฌ ์˜ฌ๋ฐ”๋ฅธ ํ…œํ”Œ๋ฆฟ ์ƒ์„ฑ + const previewParts = rule.parts + .sort((a: any, b: any) => a.order - b.order) + .map((part: any) => { + if (part.generationMethod === "manual") { + return "____"; + } + const autoConfig = part.autoConfig || {}; + switch (part.partType) { + case "sequence": { + const length = autoConfig.sequenceLength || 3; + return "X".repeat(length); // ์ˆœ๋ฒˆ ์ž๋ฆฌ ํ‘œ์‹œ + } + case "text": + return autoConfig.textValue || ""; + case "date": + return "DATEPART"; // ๋‚ ์งœ ์ž๋ฆฌ ํ‘œ์‹œ + case "category": { + // ์นดํ…Œ๊ณ ๋ฆฌ ํŒŒํŠธ: formData์—์„œ ์‹ค์ œ ๊ฐ’์„ ๊ฐ€์ ธ์™€์„œ ๋งคํ•‘๋œ ํ˜•์‹ ์‚ฌ์šฉ + const categoryKey = autoConfig.categoryKey; + const categoryMappings = autoConfig.categoryMappings || []; + + if (!categoryKey || !formData) { + return "CATEGORY"; // ํด๋ฐฑ + } + + const columnName = categoryKey.includes(".") + ? categoryKey.split(".")[1] + : categoryKey; + const selectedValue = formData[columnName]; + + if (!selectedValue) { + return "CATEGORY"; // ํด๋ฐฑ + } + + const selectedValueStr = String(selectedValue); + const mapping = categoryMappings.find((m: any) => { + if (m.categoryValueId?.toString() === selectedValueStr) + return true; + if (m.categoryValueLabel === selectedValueStr) return true; + return false; + }); + + return mapping?.format || "CATEGORY"; + } + default: + return ""; + } + }); + + const separator = rule.separator || ""; + const previewTemplate = previewParts.join(separator); + + // ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์ฝ”๋“œ์—์„œ ์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ถ”์ถœ + // ์˜ˆ: ํ…œํ”Œ๋ฆฟ "R-____-XXX", ์‚ฌ์šฉ์ž์ž…๋ ฅ "R-MYVALUE-012" โ†’ "MYVALUE" ์ถ”์ถœ + const templateParts = previewTemplate.split("____"); + if (templateParts.length > 1) { + let remainingCode = userInputCode; + for (let i = 0; i < templateParts.length - 1; i++) { + const prefix = templateParts[i]; + const suffix = templateParts[i + 1]; + + // prefix ์ดํ›„ ๋ถ€๋ถ„ ์ถ”์ถœ + if (prefix && remainingCode.startsWith(prefix)) { + remainingCode = remainingCode.slice(prefix.length); + } + + // suffix ์ด์ „๊นŒ์ง€๊ฐ€ ์ˆ˜๋™ ์ž…๋ ฅ ๊ฐ’ + if (suffix) { + // suffix์—์„œ ์ˆœ๋ฒˆ(XXX)์ด๋‚˜ ๋‚ ์งœ ๋ถ€๋ถ„์„ ์ œ์™ธํ•œ ์‹ค์ œ ๊ตฌ๋ถ„์ž ์ฐพ๊ธฐ + const suffixStart = suffix.replace(/X+|DATEPART/g, ""); + const manualEndIndex = suffixStart + ? remainingCode.indexOf(suffixStart) + : remainingCode.length; + if (manualEndIndex > 0) { + extractedManualValues.push( + remainingCode.slice(0, manualEndIndex) + ); + remainingCode = remainingCode.slice(manualEndIndex); + } + } else { + extractedManualValues.push(remainingCode); + } + } + } + + logger.info( + `์ˆ˜๋™ ์ž…๋ ฅ ๊ฐ’ ์ถ”์ถœ: userInputCode=${userInputCode}, previewTemplate=${previewTemplate}, extractedManualValues=${JSON.stringify(extractedManualValues)}` + ); + } + + let manualPartIndex = 0; const parts = rule.parts .sort((a: any, b: any) => a.order - b.order) .map((part: any) => { if (part.generationMethod === "manual") { - return part.manualConfig?.value || ""; + // ์ถ”์ถœ๋œ ์ˆ˜๋™ ์ž…๋ ฅ ๊ฐ’ ์‚ฌ์šฉ, ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ + const manualValue = + extractedManualValues[manualPartIndex] || + part.manualConfig?.value || + ""; + manualPartIndex++; + return manualValue; } const autoConfig = part.autoConfig || {}; @@ -1078,16 +1183,21 @@ class NumberingRuleService { case "date": { // ๋‚ ์งœ (๋‹ค์–‘ํ•œ ๋‚ ์งœ ํ˜•์‹) const dateFormat = autoConfig.dateFormat || "YYYYMMDD"; - + // ์ปฌ๋Ÿผ ๊ธฐ์ค€ ์ƒ์„ฑ์ธ ๊ฒฝ์šฐ ํผ ๋ฐ์ดํ„ฐ์—์„œ ๋‚ ์งœ ์ถ”์ถœ - if (autoConfig.useColumnValue && autoConfig.sourceColumnName && formData) { + if ( + autoConfig.useColumnValue && + autoConfig.sourceColumnName && + formData + ) { const columnValue = formData[autoConfig.sourceColumnName]; if (columnValue) { // ๋‚ ์งœ ๋ฌธ์ž์—ด ๋˜๋Š” Date ๊ฐ์ฒด๋ฅผ Date๋กœ ๋ณ€ํ™˜ - const dateValue = columnValue instanceof Date - ? columnValue - : new Date(columnValue); - + const dateValue = + columnValue instanceof Date + ? columnValue + : new Date(columnValue); + if (!isNaN(dateValue.getTime())) { logger.info("์ปฌ๋Ÿผ ๊ธฐ์ค€ ๋‚ ์งœ ์ƒ์„ฑ", { sourceColumn: autoConfig.sourceColumnName, @@ -1108,7 +1218,7 @@ class NumberingRuleService { }); } } - + // ๊ธฐ๋ณธ: ํ˜„์žฌ ๋‚ ์งœ ์‚ฌ์šฉ return this.formatDate(new Date(), dateFormat); } @@ -1118,6 +1228,73 @@ class NumberingRuleService { return autoConfig.textValue || "TEXT"; } + case "category": { + // ์นดํ…Œ๊ณ ๋ฆฌ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ์ƒ์„ฑ (allocateCode์šฉ) + const categoryKey = autoConfig.categoryKey; // ์˜ˆ: "item_info.material" + const categoryMappings = autoConfig.categoryMappings || []; + + if (!categoryKey || !formData) { + logger.warn("allocateCode: ์นดํ…Œ๊ณ ๋ฆฌ ํ‚ค ๋˜๋Š” ํผ ๋ฐ์ดํ„ฐ ์—†์Œ", { + categoryKey, + hasFormData: !!formData, + }); + return ""; + } + + // categoryKey์—์„œ ์ปฌ๋Ÿผ๋ช… ์ถ”์ถœ (์˜ˆ: "item_info.material" -> "material") + const columnName = categoryKey.includes(".") + ? categoryKey.split(".")[1] + : categoryKey; + + // ํผ ๋ฐ์ดํ„ฐ์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ์˜ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ + const selectedValue = formData[columnName]; + + logger.info("allocateCode: ์นดํ…Œ๊ณ ๋ฆฌ ํŒŒํŠธ ์ฒ˜๋ฆฌ", { + categoryKey, + columnName, + selectedValue, + formDataKeys: Object.keys(formData), + mappingsCount: categoryMappings.length, + }); + + if (!selectedValue) { + logger.warn("allocateCode: ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์ด ์„ ํƒ๋˜์ง€ ์•Š์Œ", { + columnName, + formDataKeys: Object.keys(formData), + }); + return ""; + } + + // ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘์—์„œ ํ•ด๋‹น ๊ฐ’์— ๋Œ€ํ•œ ํ˜•์‹ ์ฐพ๊ธฐ + const selectedValueStr = String(selectedValue); + const mapping = categoryMappings.find((m: any) => { + // ID๋กœ ๋งค์นญ + if (m.categoryValueId?.toString() === selectedValueStr) + return true; + // ๋ผ๋ฒจ๋กœ ๋งค์นญ + if (m.categoryValueLabel === selectedValueStr) return true; + return false; + }); + + if (mapping) { + logger.info("allocateCode: ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์ ์šฉ", { + selectedValue, + format: mapping.format, + categoryValueLabel: mapping.categoryValueLabel, + }); + return mapping.format || ""; + } + + logger.warn("allocateCode: ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ", { + selectedValue, + availableMappings: categoryMappings.map((m: any) => ({ + id: m.categoryValueId, + label: m.categoryValueLabel, + })), + }); + return ""; + } + default: logger.warn("์•Œ ์ˆ˜ ์—†๋Š” ํŒŒํŠธ ํƒ€์ž…", { partType: part.partType }); return ""; @@ -1198,21 +1375,24 @@ class NumberingRuleService { /** * [ํ…Œ์ŠคํŠธ] ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ”์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ - * numbering_rules_test ํ…Œ์ด๋ธ” ์‚ฌ์šฉ + * numbering_rules ํ…Œ์ด๋ธ” ์‚ฌ์šฉ */ async getRulesFromTest( companyCode: string, menuObjid?: number ): Promise { try { - logger.info("[ํ…Œ์ŠคํŠธ] ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ ์‹œ์ž‘", { companyCode, menuObjid }); + logger.info("[ํ…Œ์ŠคํŠธ] ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ ์‹œ์ž‘", { + companyCode, + menuObjid, + }); const pool = getPool(); - + // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž vs ์ผ๋ฐ˜ ํšŒ์‚ฌ let query: string; let params: any[]; - + if (companyCode === "*") { // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ๊ทœ์น™ ์กฐํšŒ query = ` @@ -1231,7 +1411,7 @@ class NumberingRuleService { created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" - FROM numbering_rules_test + FROM numbering_rules ORDER BY created_at DESC `; params = []; @@ -1253,7 +1433,7 @@ class NumberingRuleService { created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" - FROM numbering_rules_test + FROM numbering_rules WHERE company_code = $1 ORDER BY created_at DESC `; @@ -1272,7 +1452,7 @@ class NumberingRuleService { generation_method AS "generationMethod", auto_config AS "autoConfig", manual_config AS "manualConfig" - FROM numbering_rule_parts_test + FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2 ORDER BY part_order `; @@ -1300,8 +1480,8 @@ class NumberingRuleService { } /** - * [ํ…Œ์ŠคํŠธ] ํ…Œ์ด๋ธ”๋ช… + ์ปฌ๋Ÿผ๋ช… ๊ธฐ๋ฐ˜์œผ๋กœ ์ฑ„๋ฒˆ๊ทœ์น™ ์กฐํšŒ (menu_objid ์—†์ด) - * numbering_rules_test ํ…Œ์ด๋ธ” ์‚ฌ์šฉ + * ํ…Œ์ด๋ธ”๋ช… + ์ปฌ๋Ÿผ๋ช… ๊ธฐ๋ฐ˜์œผ๋กœ ์ฑ„๋ฒˆ๊ทœ์น™ ์กฐํšŒ + * numbering_rules ํ…Œ์ด๋ธ” ์‚ฌ์šฉ */ async getNumberingRuleByColumn( companyCode: string, @@ -1333,8 +1513,8 @@ class NumberingRuleService { r.created_at AS "createdAt", r.updated_at AS "updatedAt", r.created_by AS "createdBy" - FROM numbering_rules_test r - LEFT JOIN category_values_test cv ON r.category_value_id = cv.value_id + FROM numbering_rules r + LEFT JOIN category_values cv ON r.category_value_id = cv.value_id WHERE r.company_code = $1 AND r.table_name = $2 AND r.column_name = $3 @@ -1365,11 +1545,14 @@ class NumberingRuleService { generation_method AS "generationMethod", auto_config AS "autoConfig", manual_config AS "manualConfig" - FROM numbering_rule_parts_test + FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2 ORDER BY part_order `; - const partsResult = await pool.query(partsQuery, [rule.ruleId, companyCode]); + const partsResult = await pool.query(partsQuery, [ + rule.ruleId, + companyCode, + ]); rule.parts = partsResult.rows; logger.info("ํ…Œ์ด๋ธ”+์ปฌ๋Ÿผ ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์„ฑ๊ณต (ํ…Œ์ŠคํŠธ)", { @@ -1391,7 +1574,7 @@ class NumberingRuleService { /** * [ํ…Œ์ŠคํŠธ] ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ”์— ์ฑ„๋ฒˆ๊ทœ์น™ ์ €์žฅ - * numbering_rules_test ํ…Œ์ด๋ธ” ์‚ฌ์šฉ + * numbering_rules ํ…Œ์ด๋ธ” ์‚ฌ์šฉ */ async saveRuleToTest( config: NumberingRuleConfig, @@ -1414,15 +1597,18 @@ class NumberingRuleService { // ๊ธฐ์กด ๊ทœ์น™ ํ™•์ธ const existingQuery = ` - SELECT rule_id FROM numbering_rules_test + SELECT rule_id FROM numbering_rules WHERE rule_id = $1 AND company_code = $2 `; - const existingResult = await client.query(existingQuery, [config.ruleId, companyCode]); + const existingResult = await client.query(existingQuery, [ + config.ruleId, + companyCode, + ]); if (existingResult.rows.length > 0) { // ์—…๋ฐ์ดํŠธ const updateQuery = ` - UPDATE numbering_rules_test SET + UPDATE numbering_rules SET rule_name = $1, description = $2, separator = $3, @@ -1449,13 +1635,13 @@ class NumberingRuleService { // ๊ธฐ์กด ํŒŒํŠธ ์‚ญ์ œ await client.query( - "DELETE FROM numbering_rule_parts_test WHERE rule_id = $1 AND company_code = $2", + "DELETE FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2", [config.ruleId, companyCode] ); } else { // ์‹ ๊ทœ ๋“ฑ๋ก const insertQuery = ` - INSERT INTO numbering_rules_test ( + INSERT INTO numbering_rules ( rule_id, rule_name, description, separator, reset_period, current_sequence, table_name, column_name, company_code, category_column, category_value_id, @@ -1482,7 +1668,7 @@ class NumberingRuleService { if (config.parts && config.parts.length > 0) { for (const part of config.parts) { const partInsertQuery = ` - INSERT INTO numbering_rule_parts_test ( + INSERT INTO numbering_rule_parts ( rule_id, part_order, part_type, generation_method, auto_config, manual_config, company_code, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) @@ -1523,7 +1709,7 @@ class NumberingRuleService { /** * [ํ…Œ์ŠคํŠธ] ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ”์—์„œ ์ฑ„๋ฒˆ๊ทœ์น™ ์‚ญ์ œ - * numbering_rules_test ํ…Œ์ด๋ธ” ์‚ฌ์šฉ + * numbering_rules ํ…Œ์ด๋ธ” ์‚ฌ์šฉ */ async deleteRuleFromTest(ruleId: string, companyCode: string): Promise { const pool = getPool(); @@ -1532,17 +1718,20 @@ class NumberingRuleService { try { await client.query("BEGIN"); - logger.info("ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ”์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™ ์‚ญ์ œ ์‹œ์ž‘", { ruleId, companyCode }); + logger.info("ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ”์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™ ์‚ญ์ œ ์‹œ์ž‘", { + ruleId, + companyCode, + }); // ํŒŒํŠธ ๋จผ์ € ์‚ญ์ œ await client.query( - "DELETE FROM numbering_rule_parts_test WHERE rule_id = $1 AND company_code = $2", + "DELETE FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2", [ruleId, companyCode] ); // ๊ทœ์น™ ์‚ญ์ œ const result = await client.query( - "DELETE FROM numbering_rules_test WHERE rule_id = $1 AND company_code = $2", + "DELETE FROM numbering_rules WHERE rule_id = $1 AND company_code = $2", [ruleId, companyCode] ); @@ -1608,8 +1797,8 @@ class NumberingRuleService { r.created_at AS "createdAt", r.updated_at AS "updatedAt", r.created_by AS "createdBy" - FROM numbering_rules_test r - LEFT JOIN category_values_test cv ON r.category_value_id = cv.value_id + FROM numbering_rules r + LEFT JOIN category_values cv ON r.category_value_id = cv.value_id WHERE r.company_code = $1 AND r.table_name = $2 AND r.column_name = $3 @@ -1636,11 +1825,14 @@ class NumberingRuleService { generation_method AS "generationMethod", auto_config AS "autoConfig", manual_config AS "manualConfig" - FROM numbering_rule_parts_test + FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2 ORDER BY part_order `; - const partsResult = await pool.query(partsQuery, [rule.ruleId, companyCode]); + const partsResult = await pool.query(partsQuery, [ + rule.ruleId, + companyCode, + ]); rule.parts = partsResult.rows; logger.info("์นดํ…Œ๊ณ ๋ฆฌ ์กฐ๊ฑด ๋งค์นญ ์ฑ„๋ฒˆ ๊ทœ์น™ ์ฐพ์Œ", { @@ -1668,14 +1860,18 @@ class NumberingRuleService { r.created_at AS "createdAt", r.updated_at AS "updatedAt", r.created_by AS "createdBy" - FROM numbering_rules_test r + FROM numbering_rules r WHERE r.company_code = $1 AND r.table_name = $2 AND r.column_name = $3 AND r.category_value_id IS NULL LIMIT 1 `; - const defaultResult = await pool.query(defaultQuery, [companyCode, tableName, columnName]); + const defaultResult = await pool.query(defaultQuery, [ + companyCode, + tableName, + columnName, + ]); if (defaultResult.rows.length > 0) { const rule = defaultResult.rows[0]; @@ -1688,11 +1884,14 @@ class NumberingRuleService { generation_method AS "generationMethod", auto_config AS "autoConfig", manual_config AS "manualConfig" - FROM numbering_rule_parts_test + FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2 ORDER BY part_order `; - const partsResult = await pool.query(partsQuery, [rule.ruleId, companyCode]); + const partsResult = await pool.query(partsQuery, [ + rule.ruleId, + companyCode, + ]); rule.parts = partsResult.rows; logger.info("๊ธฐ๋ณธ ์ฑ„๋ฒˆ ๊ทœ์น™ ์ฐพ์Œ (์นดํ…Œ๊ณ ๋ฆฌ ์กฐ๊ฑด ์—†์Œ)", { @@ -1745,15 +1944,19 @@ class NumberingRuleService { r.created_at AS "createdAt", r.updated_at AS "updatedAt", r.created_by AS "createdBy" - FROM numbering_rules_test r - LEFT JOIN category_values_test cv ON r.category_value_id = cv.value_id + FROM numbering_rules r + LEFT JOIN category_values cv ON r.category_value_id = cv.value_id WHERE r.company_code = $1 AND r.table_name = $2 AND r.column_name = $3 ORDER BY r.category_value_id NULLS FIRST, r.created_at `; - const result = await pool.query(query, [companyCode, tableName, columnName]); - + const result = await pool.query(query, [ + companyCode, + tableName, + columnName, + ]); + // ๊ฐ ๊ทœ์น™์˜ ํŒŒํŠธ ์ •๋ณด ์กฐํšŒ for (const rule of result.rows) { const partsQuery = ` @@ -1764,11 +1967,14 @@ class NumberingRuleService { generation_method AS "generationMethod", auto_config AS "autoConfig", manual_config AS "manualConfig" - FROM numbering_rule_parts_test + FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2 ORDER BY part_order `; - const partsResult = await pool.query(partsQuery, [rule.ruleId, companyCode]); + const partsResult = await pool.query(partsQuery, [ + rule.ruleId, + companyCode, + ]); rule.parts = partsResult.rows; } @@ -1782,34 +1988,60 @@ class NumberingRuleService { } /** - * ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ (๋ฉ”๋‰ด ๋™๊ธฐํ™” ์™„๋ฃŒ ํ›„ ํ˜ธ์ถœ) - * ๋ฉ”๋‰ด ์ด๋ฆ„์„ ๊ธฐ์ค€์œผ๋กœ ์ฑ„๋ฒˆ๊ทœ์น™์„ ๋Œ€์ƒ ํšŒ์‚ฌ์˜ ๋ฉ”๋‰ด์— ์—ฐ๊ฒฐ + * ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ (ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜) + * numbering_rules, numbering_rule_parts ํ…Œ์ด๋ธ” ์‚ฌ์šฉ * ๋ณต์ œ ํ›„ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์˜ numberingRuleId ์ฐธ์กฐ๋„ ์—…๋ฐ์ดํŠธ */ async copyRulesForCompany( sourceCompanyCode: string, targetCompanyCode: string - ): Promise<{ copiedCount: number; skippedCount: number; details: string[]; ruleIdMap: Record }> { + ): Promise<{ + copiedCount: number; + skippedCount: number; + details: string[]; + ruleIdMap: Record; + }> { const pool = getPool(); const client = await pool.connect(); - - const result = { copiedCount: 0, skippedCount: 0, details: [] as string[], ruleIdMap: {} as Record }; + + const result = { + copiedCount: 0, + skippedCount: 0, + details: [] as string[], + ruleIdMap: {} as Record, + }; try { await client.query("BEGIN"); - // 1. ์›๋ณธ ํšŒ์‚ฌ์˜ ์ฑ„๋ฒˆ๊ทœ์น™ ์กฐํšŒ (menu + table ์Šค์ฝ”ํ”„ ๋ชจ๋‘) + // 0. ๋Œ€์ƒ ํšŒ์‚ฌ์˜ ๊ธฐ์กด ์ฑ„๋ฒˆ๊ทœ์น™ ์‚ญ์ œ (๊นจ๋—ํ•˜๊ฒŒ ๋ณต์ œํ•˜๊ธฐ ์œ„ํ•ด) + // ๋จผ์ € ํŒŒํŠธ ์‚ญ์ œ + await client.query( + `DELETE FROM numbering_rule_parts + WHERE rule_id IN (SELECT rule_id FROM numbering_rules WHERE company_code = $1)`, + [targetCompanyCode] + ); + // ๊ทœ์น™ ์‚ญ์ œ + const deleteResult = await client.query( + `DELETE FROM numbering_rules WHERE company_code = $1 RETURNING rule_id`, + [targetCompanyCode] + ); + if (deleteResult.rowCount && deleteResult.rowCount > 0) { + logger.info("๊ธฐ์กด ์ฑ„๋ฒˆ๊ทœ์น™ ์‚ญ์ œ", { + targetCompanyCode, + deletedCount: deleteResult.rowCount, + }); + } + + // 1. ์›๋ณธ ํšŒ์‚ฌ์˜ ์ฑ„๋ฒˆ๊ทœ์น™ ์กฐํšŒ - numbering_rules ์‚ฌ์šฉ const sourceRulesResult = await client.query( - `SELECT nr.*, mi.menu_name_kor as source_menu_name - FROM numbering_rules nr - LEFT JOIN menu_info mi ON nr.menu_objid = mi.objid - WHERE nr.company_code = $1 AND nr.scope_type IN ('menu', 'table')`, + `SELECT * FROM numbering_rules WHERE company_code = $1`, [sourceCompanyCode] ); - logger.info("์›๋ณธ ์ฑ„๋ฒˆ๊ทœ์น™ ์กฐํšŒ", { - sourceCompanyCode, - count: sourceRulesResult.rowCount + logger.info("์›๋ณธ ์ฑ„๋ฒˆ๊ทœ์น™ ์กฐํšŒ", { + sourceCompanyCode, + count: sourceRulesResult.rowCount, }); // 2. ๊ฐ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ @@ -1817,7 +2049,7 @@ class NumberingRuleService { // ์ƒˆ rule_id ์ƒ์„ฑ const newRuleId = `rule-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - // ์ด๋ฏธ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ (์ด๋ฆ„ ๊ธฐ๋ฐ˜) + // ์ด๋ฏธ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ (์ด๋ฆ„ ๊ธฐ๋ฐ˜) - numbering_rules ์‚ฌ์šฉ const existsCheck = await client.query( `SELECT rule_id FROM numbering_rules WHERE company_code = $1 AND rule_name = $2`, @@ -1832,32 +2064,12 @@ class NumberingRuleService { continue; } - let targetMenuObjid = null; - - // menu ์Šค์ฝ”ํ”„์ธ ๊ฒฝ์šฐ ๋Œ€์ƒ ๋ฉ”๋‰ด ์ฐพ๊ธฐ - if (rule.scope_type === 'menu' && rule.source_menu_name) { - const targetMenuResult = await client.query( - `SELECT objid FROM menu_info - WHERE company_code = $1 AND menu_name_kor = $2 - LIMIT 1`, - [targetCompanyCode, rule.source_menu_name] - ); - - if (targetMenuResult.rows.length === 0) { - result.skippedCount++; - result.details.push(`๊ฑด๋„ˆ๋œ€ (๋ฉ”๋‰ด ์—†์Œ): ${rule.rule_name} - ๋ฉ”๋‰ด: ${rule.source_menu_name}`); - continue; - } - - targetMenuObjid = targetMenuResult.rows[0].objid; - } - - // ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ + // ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ - numbering_rules ์‚ฌ์šฉ await client.query( `INSERT INTO numbering_rules ( rule_id, rule_name, description, separator, reset_period, current_sequence, table_name, column_name, company_code, - created_at, updated_at, created_by, scope_type, menu_objid + created_at, updated_at, created_by, category_column, category_value_id ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW(), $10, $11, $12)`, [ newRuleId, @@ -1870,12 +2082,12 @@ class NumberingRuleService { rule.column_name, targetCompanyCode, rule.created_by, - rule.scope_type, - targetMenuObjid, + rule.category_column, + rule.category_value_id, ] ); - // ์ฑ„๋ฒˆ๊ทœ์น™ ํŒŒํŠธ ๋ณต์ œ + // ์ฑ„๋ฒˆ๊ทœ์น™ ํŒŒํŠธ ๋ณต์ œ - numbering_rule_parts ์‚ฌ์šฉ const partsResult = await client.query( `SELECT * FROM numbering_rule_parts WHERE rule_id = $1 ORDER BY part_order`, [rule.rule_id] @@ -1902,20 +2114,19 @@ class NumberingRuleService { // ๋งคํ•‘ ์ถ”๊ฐ€ result.ruleIdMap[rule.rule_id] = newRuleId; result.copiedCount++; - result.details.push(`๋ณต์ œ ์™„๋ฃŒ: ${rule.rule_name} (${rule.scope_type})`); - logger.info("์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์™„๋ฃŒ", { - ruleName: rule.rule_name, + result.details.push(`๋ณต์ œ ์™„๋ฃŒ: ${rule.rule_name}`); + logger.info("์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์™„๋ฃŒ", { + ruleName: rule.rule_name, oldRuleId: rule.rule_id, - newRuleId, - targetMenuObjid + newRuleId, }); } // 3. ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์˜ numberingRuleId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ if (Object.keys(result.ruleIdMap).length > 0) { - logger.info("ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ numberingRuleId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘", { + logger.info("ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ numberingRuleId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘", { targetCompanyCode, - mappingCount: Object.keys(result.ruleIdMap).length + mappingCount: Object.keys(result.ruleIdMap).length, }); // ๋Œ€์ƒ ํšŒ์‚ฌ์˜ ๋ชจ๋“  ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ @@ -1935,9 +2146,13 @@ class NumberingRuleService { let updated = false; // ๊ฐ ๋งคํ•‘์— ๋Œ€ํ•ด ์น˜ํ™˜ - for (const [oldRuleId, newRuleId] of Object.entries(result.ruleIdMap)) { + for (const [oldRuleId, newRuleId] of Object.entries( + result.ruleIdMap + )) { if (propsStr.includes(`"${oldRuleId}"`)) { - propsStr = propsStr.split(`"${oldRuleId}"`).join(`"${newRuleId}"`); + propsStr = propsStr + .split(`"${oldRuleId}"`) + .join(`"${newRuleId}"`); updated = true; } } @@ -1951,27 +2166,33 @@ class NumberingRuleService { } } - logger.info("ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ numberingRuleId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ", { + logger.info("ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ numberingRuleId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ", { targetCompanyCode, - updatedLayouts + updatedLayouts, }); - result.details.push(`ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ${updatedLayouts}๊ฐœ์˜ ์ฑ„๋ฒˆ๊ทœ์น™ ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ`); + result.details.push( + `ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ${updatedLayouts}๊ฐœ์˜ ์ฑ„๋ฒˆ๊ทœ์น™ ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ` + ); } await client.query("COMMIT"); - - logger.info("ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์™„๋ฃŒ", { - sourceCompanyCode, - targetCompanyCode, + + logger.info("ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์™„๋ฃŒ", { + sourceCompanyCode, + targetCompanyCode, copiedCount: result.copiedCount, skippedCount: result.skippedCount, - ruleIdMapCount: Object.keys(result.ruleIdMap).length + ruleIdMapCount: Object.keys(result.ruleIdMap).length, }); return result; } catch (error) { await client.query("ROLLBACK"); - logger.error("ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์‹คํŒจ", { error, sourceCompanyCode, targetCompanyCode }); + logger.error("ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์‹คํŒจ", { + error, + sourceCompanyCode, + targetCompanyCode, + }); throw error; } finally { client.release(); diff --git a/backend-node/src/services/scheduleService.ts b/backend-node/src/services/scheduleService.ts new file mode 100644 index 00000000..0ce378d5 --- /dev/null +++ b/backend-node/src/services/scheduleService.ts @@ -0,0 +1,520 @@ +/** + * ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค + * + * ์Šค์ผ€์ค„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ƒ์„ฑ, ์ ์šฉ, ์กฐํšŒ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + */ + +import { pool } from "../database/db"; + +// ============================================================================ +// ํƒ€์ž… ์ •์˜ +// ============================================================================ + +export interface ScheduleGenerationConfig { + scheduleType: "PRODUCTION" | "MAINTENANCE" | "SHIPPING" | "WORK_ASSIGN"; + source: { + tableName: string; + groupByField: string; + quantityField: string; + dueDateField?: string; + }; + resource: { + type: string; + idField: string; + nameField: string; + }; + rules: { + leadTimeDays?: number; + dailyCapacity?: number; + workingDays?: number[]; + considerStock?: boolean; + stockTableName?: string; + stockQtyField?: string; + safetyStockField?: string; + }; + target: { + tableName: string; + }; +} + +export interface SchedulePreview { + toCreate: any[]; + toDelete: any[]; + toUpdate: any[]; + summary: { + createCount: number; + deleteCount: number; + updateCount: number; + totalQty: number; + }; +} + +export interface ApplyOptions { + deleteExisting: boolean; + updateMode: "replace" | "merge"; +} + +export interface ApplyResult { + created: number; + deleted: number; + updated: number; +} + +export interface ScheduleListQuery { + scheduleType?: string; + resourceType?: string; + resourceId?: string; + startDate?: string; + endDate?: string; + status?: string; + companyCode: string; +} + +// ============================================================================ +// ์„œ๋น„์Šค ํด๋ž˜์Šค +// ============================================================================ + +export class ScheduleService { + /** + * ์Šค์ผ€์ค„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ƒ์„ฑ + */ + async generatePreview( + config: ScheduleGenerationConfig, + sourceData: any[], + period: { start: string; end: string } | undefined, + companyCode: string + ): Promise { + console.log("[ScheduleService] generatePreview ์‹œ์ž‘:", { + scheduleType: config.scheduleType, + sourceDataCount: sourceData.length, + period, + companyCode, + }); + + // ๊ธฐ๋ณธ ๊ธฐ๊ฐ„ ์„ค์ • (ํ˜„์žฌ ์›”) + const now = new Date(); + const defaultPeriod = { + start: new Date(now.getFullYear(), now.getMonth(), 1) + .toISOString() + .split("T")[0], + end: new Date(now.getFullYear(), now.getMonth() + 1, 0) + .toISOString() + .split("T")[0], + }; + const effectivePeriod = period || defaultPeriod; + + // 1. ์†Œ์Šค ๋ฐ์ดํ„ฐ๋ฅผ ๋ฆฌ์†Œ์Šค๋ณ„๋กœ ๊ทธ๋ฃนํ™” + const groupedData = this.groupByResource(sourceData, config); + + // 2. ๊ฐ ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•ด ์Šค์ผ€์ค„ ์ƒ์„ฑ + const toCreate: any[] = []; + let totalQty = 0; + + for (const [resourceId, items] of Object.entries(groupedData)) { + const schedules = this.generateSchedulesForResource( + resourceId, + items as any[], + config, + effectivePeriod, + companyCode + ); + toCreate.push(...schedules); + totalQty += schedules.reduce((sum, s) => sum + (s.plan_qty || 0), 0); + } + + // 3. ๊ธฐ์กด ์Šค์ผ€์ค„ ์กฐํšŒ (์‚ญ์ œ ๋Œ€์ƒ) + // ๊ทธ๋ฃน ํ‚ค์—์„œ ๋ฆฌ์†Œ์Šค ID๋งŒ ์ถ”์ถœ ("๋ฆฌ์†Œ์ŠคID|๋‚ ์งœ" ํ˜•์‹์—์„œ "๋ฆฌ์†Œ์ŠคID"๋งŒ) + const resourceIds = [ + ...new Set(Object.keys(groupedData).map((key) => key.split("|")[0])), + ]; + const toDelete = await this.getExistingSchedules( + config.scheduleType, + resourceIds, + effectivePeriod, + companyCode + ); + + // 4. ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฒฐ๊ณผ ์ƒ์„ฑ + const preview: SchedulePreview = { + toCreate, + toDelete, + toUpdate: [], // ํ˜„์žฌ๋Š” Replace ๋ชจ๋“œ๋งŒ ์ง€์› + summary: { + createCount: toCreate.length, + deleteCount: toDelete.length, + updateCount: 0, + totalQty, + }, + }; + + console.log("[ScheduleService] generatePreview ์™„๋ฃŒ:", preview.summary); + + return preview; + } + + /** + * ์Šค์ผ€์ค„ ์ ์šฉ + */ + async applySchedules( + config: ScheduleGenerationConfig, + preview: SchedulePreview, + options: ApplyOptions, + companyCode: string, + userId: string + ): Promise { + console.log("[ScheduleService] applySchedules ์‹œ์ž‘:", { + createCount: preview.summary.createCount, + deleteCount: preview.summary.deleteCount, + options, + companyCode, + userId, + }); + + const client = await pool.connect(); + const result: ApplyResult = { created: 0, deleted: 0, updated: 0 }; + + try { + await client.query("BEGIN"); + + // 1. ๊ธฐ์กด ์Šค์ผ€์ค„ ์‚ญ์ œ + if (options.deleteExisting && preview.toDelete.length > 0) { + const deleteIds = preview.toDelete.map((s) => s.schedule_id); + await client.query( + `DELETE FROM schedule_mng + WHERE schedule_id = ANY($1) AND company_code = $2`, + [deleteIds, companyCode] + ); + result.deleted = deleteIds.length; + console.log("[ScheduleService] ์Šค์ผ€์ค„ ์‚ญ์ œ ์™„๋ฃŒ:", result.deleted); + } + + // 2. ์ƒˆ ์Šค์ผ€์ค„ ์ƒ์„ฑ + for (const schedule of preview.toCreate) { + await client.query( + `INSERT INTO schedule_mng ( + company_code, schedule_type, schedule_name, + resource_type, resource_id, resource_name, + start_date, end_date, due_date, + plan_qty, unit, status, priority, + source_table, source_id, source_group_key, + auto_generated, generated_at, generated_by, + metadata, created_by, updated_by + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, + $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22 + )`, + [ + companyCode, + schedule.schedule_type, + schedule.schedule_name, + schedule.resource_type, + schedule.resource_id, + schedule.resource_name, + schedule.start_date, + schedule.end_date, + schedule.due_date || null, + schedule.plan_qty, + schedule.unit || null, + schedule.status || "PLANNED", + schedule.priority || null, + schedule.source_table || null, + schedule.source_id || null, + schedule.source_group_key || null, + true, + new Date(), + userId, + schedule.metadata ? JSON.stringify(schedule.metadata) : null, + userId, + userId, + ] + ); + result.created++; + } + + await client.query("COMMIT"); + console.log("[ScheduleService] applySchedules ์™„๋ฃŒ:", result); + + return result; + } catch (error) { + await client.query("ROLLBACK"); + console.error("[ScheduleService] applySchedules ์˜ค๋ฅ˜:", error); + throw error; + } finally { + client.release(); + } + } + + /** + * ์Šค์ผ€์ค„ ๋ชฉ๋ก ์กฐํšŒ + */ + async getScheduleList( + query: ScheduleListQuery + ): Promise<{ data: any[]; total: number }> { + const conditions: string[] = []; + const params: any[] = []; + let paramIndex = 1; + + // company_code ํ•„ํ„ฐ + if (query.companyCode !== "*") { + conditions.push(`company_code = $${paramIndex++}`); + params.push(query.companyCode); + } + + // scheduleType ํ•„ํ„ฐ + if (query.scheduleType) { + conditions.push(`schedule_type = $${paramIndex++}`); + params.push(query.scheduleType); + } + + // resourceType ํ•„ํ„ฐ + if (query.resourceType) { + conditions.push(`resource_type = $${paramIndex++}`); + params.push(query.resourceType); + } + + // resourceId ํ•„ํ„ฐ + if (query.resourceId) { + conditions.push(`resource_id = $${paramIndex++}`); + params.push(query.resourceId); + } + + // ๊ธฐ๊ฐ„ ํ•„ํ„ฐ + if (query.startDate) { + conditions.push(`end_date >= $${paramIndex++}`); + params.push(query.startDate); + } + if (query.endDate) { + conditions.push(`start_date <= $${paramIndex++}`); + params.push(query.endDate); + } + + // status ํ•„ํ„ฐ + if (query.status) { + conditions.push(`status = $${paramIndex++}`); + params.push(query.status); + } + + const whereClause = + conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + + const result = await pool.query( + `SELECT * FROM schedule_mng + ${whereClause} + ORDER BY start_date, resource_id`, + params + ); + + return { + data: result.rows, + total: result.rows.length, + }; + } + + /** + * ์Šค์ผ€์ค„ ์‚ญ์ œ + */ + async deleteSchedule( + scheduleId: number, + companyCode: string, + userId: string + ): Promise<{ success: boolean; message?: string }> { + const result = await pool.query( + `DELETE FROM schedule_mng + WHERE schedule_id = $1 AND (company_code = $2 OR $2 = '*') + RETURNING schedule_id`, + [scheduleId, companyCode] + ); + + if (result.rowCount === 0) { + return { + success: false, + message: "์Šค์ผ€์ค„์„ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.", + }; + } + + // ์ด๋ ฅ ๊ธฐ๋ก + await pool.query( + `INSERT INTO schedule_history (company_code, schedule_id, action, changed_by) + VALUES ($1, $2, 'DELETE', $3)`, + [companyCode, scheduleId, userId] + ); + + return { success: true }; + } + + // ============================================================================ + // ํ—ฌํผ ๋ฉ”์„œ๋“œ + // ============================================================================ + + /** + * ์†Œ์Šค ๋ฐ์ดํ„ฐ๋ฅผ ๋ฆฌ์†Œ์Šค๋ณ„๋กœ ๊ทธ๋ฃนํ™” + * - ๊ธฐ์ค€์ผ(dueDateField)์ด ์„ค์ •๋œ ๊ฒฝ์šฐ: ๋ฆฌ์†Œ์Šค + ๊ธฐ์ค€์ผ ์กฐํ•ฉ์œผ๋กœ ๊ทธ๋ฃนํ™” + * - ๊ธฐ์ค€์ผ์ด ์—†๋Š” ๊ฒฝ์šฐ: ๋ฆฌ์†Œ์Šค๋ณ„๋กœ๋งŒ ๊ทธ๋ฃนํ™” + */ + private groupByResource( + sourceData: any[], + config: ScheduleGenerationConfig + ): Record { + const grouped: Record = {}; + const dueDateField = config.source.dueDateField; + + for (const item of sourceData) { + const resourceId = item[config.resource.idField]; + if (!resourceId) continue; + + // ๊ทธ๋ฃน ํ‚ค ์ƒ์„ฑ: ๊ธฐ์ค€์ผ์ด ์žˆ์œผ๋ฉด "๋ฆฌ์†Œ์ŠคID|๊ธฐ์ค€์ผ", ์—†์œผ๋ฉด "๋ฆฌ์†Œ์ŠคID" + let groupKey = resourceId; + if (dueDateField && item[dueDateField]) { + // ๋‚ ์งœ๋ฅผ YYYY-MM-DD ํ˜•์‹์œผ๋กœ ์ •๊ทœํ™” + const dueDate = new Date(item[dueDateField]) + .toISOString() + .split("T")[0]; + groupKey = `${resourceId}|${dueDate}`; + } + + if (!grouped[groupKey]) { + grouped[groupKey] = []; + } + grouped[groupKey].push(item); + } + + console.log("[ScheduleService] ๊ทธ๋ฃนํ™” ๊ฒฐ๊ณผ:", { + groupCount: Object.keys(grouped).length, + groups: Object.keys(grouped), + dueDateField, + }); + + return grouped; + } + + /** + * ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ์Šค์ผ€์ค„ ์ƒ์„ฑ + * - groupKey ํ˜•์‹: "๋ฆฌ์†Œ์ŠคID" ๋˜๋Š” "๋ฆฌ์†Œ์ŠคID|๊ธฐ์ค€์ผ(YYYY-MM-DD)" + */ + private generateSchedulesForResource( + groupKey: string, + items: any[], + config: ScheduleGenerationConfig, + period: { start: string; end: string }, + companyCode: string + ): any[] { + const schedules: any[] = []; + + // ๊ทธ๋ฃน ํ‚ค์—์„œ ๋ฆฌ์†Œ์ŠคID์™€ ๊ธฐ์ค€์ผ ๋ถ„๋ฆฌ + const [resourceId, groupDueDate] = groupKey.split("|"); + const resourceName = items[0]?.[config.resource.nameField] || resourceId; + + // ์ด ์ˆ˜๋Ÿ‰ ๊ณ„์‚ฐ + const totalQty = items.reduce((sum, item) => { + return sum + (parseFloat(item[config.source.quantityField]) || 0); + }, 0); + + if (totalQty <= 0) return schedules; + + // ์Šค์ผ€์ค„ ๊ทœ์น™ ์ ์šฉ + const { + leadTimeDays = 3, + dailyCapacity = totalQty, + workingDays = [1, 2, 3, 4, 5], + } = config.rules; + + // ๊ธฐ์ค€์ผ(๋‚ฉ๊ธฐ์ผ/๋งˆ๊ฐ์ผ) ๊ฒฐ์ • + let dueDate: Date; + if (groupDueDate) { + // ๊ทธ๋ฃน ํ‚ค์— ๊ธฐ์ค€์ผ์ด ํฌํ•จ๋œ ๊ฒฝ์šฐ + dueDate = new Date(groupDueDate); + } else if (config.source.dueDateField) { + // ์•„์ดํ…œ์—์„œ ๊ธฐ์ค€์ผ ์ฐพ๊ธฐ (๊ฐ€์žฅ ๋น ๋ฅธ ๋‚ ์งœ) + let earliestDate: Date | null = null; + for (const item of items) { + const itemDueDate = item[config.source.dueDateField]; + if (itemDueDate) { + const date = new Date(itemDueDate); + if (!earliestDate || date < earliestDate) { + earliestDate = date; + } + } + } + dueDate = earliestDate || new Date(period.end); + } else { + // ๊ธฐ์ค€์ผ์ด ์—†์œผ๋ฉด ๊ธฐ๊ฐ„ ์ข…๋ฃŒ์ผ ์‚ฌ์šฉ + dueDate = new Date(period.end); + } + + // ์ข…๋ฃŒ์ผ = ๊ธฐ์ค€์ผ (๋‚ฉ๊ธฐ์ผ์— ๋งž์ถฐ ์™„๋ฃŒ) + const endDate = new Date(dueDate); + + // ์‹œ์ž‘์ผ ๊ณ„์‚ฐ (์ข…๋ฃŒ์ผ์—์„œ ๋ฆฌ๋“œํƒ€์ž„๋งŒํผ ์—ญ์‚ฐ) + const startDate = new Date(endDate); + startDate.setDate(startDate.getDate() - leadTimeDays); + + // ์Šค์ผ€์ค„๋ช… ์ƒ์„ฑ (๊ธฐ์ค€์ผ ํฌํ•จ) + const dueDateStr = dueDate.toISOString().split("T")[0]; + const scheduleName = groupDueDate + ? `${resourceName} (${dueDateStr})` + : `${resourceName} - ${config.scheduleType}`; + + // ์Šค์ผ€์ค„ ์ƒ์„ฑ + schedules.push({ + schedule_type: config.scheduleType, + schedule_name: scheduleName, + resource_type: config.resource.type, + resource_id: resourceId, + resource_name: resourceName, + start_date: startDate.toISOString(), + end_date: endDate.toISOString(), + due_date: dueDate.toISOString(), + plan_qty: totalQty, + status: "PLANNED", + source_table: config.source.tableName, + source_id: items + .map((i) => i.id || i.order_no || i.sales_order_no) + .join(","), + source_group_key: resourceId, + metadata: { + sourceCount: items.length, + dailyCapacity, + leadTimeDays, + workingDays, + groupDueDate: groupDueDate || null, + }, + }); + + console.log("[ScheduleService] ์Šค์ผ€์ค„ ์ƒ์„ฑ:", { + groupKey, + resourceId, + resourceName, + dueDate: dueDateStr, + totalQty, + startDate: startDate.toISOString().split("T")[0], + endDate: endDate.toISOString().split("T")[0], + }); + + return schedules; + } + + /** + * ๊ธฐ์กด ์Šค์ผ€์ค„ ์กฐํšŒ (์‚ญ์ œ ๋Œ€์ƒ) + */ + private async getExistingSchedules( + scheduleType: string, + resourceIds: string[], + period: { start: string; end: string }, + companyCode: string + ): Promise { + if (resourceIds.length === 0) return []; + + const result = await pool.query( + `SELECT * FROM schedule_mng + WHERE schedule_type = $1 + AND resource_id = ANY($2) + AND end_date >= $3 + AND start_date <= $4 + AND (company_code = $5 OR $5 = '*') + AND auto_generated = true`, + [scheduleType, resourceIds, period.start, period.end, companyCode] + ); + + return result.rows; + } +} diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 394c0b95..77c82a91 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -635,7 +635,76 @@ export class ScreenManagementService { // ํŠธ๋žœ์žญ์…˜์œผ๋กœ ํ™”๋ฉด ์‚ญ์ œ์™€ ๋ฉ”๋‰ด ํ• ๋‹น ์ •๋ฆฌ๋ฅผ ํ•จ๊ป˜ ์ฒ˜๋ฆฌ (Raw Query) await transaction(async (client) => { - // ์†Œํ”„ํŠธ ์‚ญ์ œ (ํœด์ง€ํ†ต์œผ๋กœ ์ด๋™) + // 1. ํ™”๋ฉด์—์„œ ์‚ฌ์šฉํ•˜๋Š” flowId ์ˆ˜์ง‘ (V2 ๋ ˆ์ด์•„์›ƒ) + const layoutResult = await client.query<{ layout_data: any }>( + `SELECT layout_data FROM screen_layouts_v2 + WHERE screen_id = $1 AND (company_code = $2 OR company_code = '*') + ORDER BY CASE WHEN company_code = $2 THEN 0 ELSE 1 END + LIMIT 1`, + [screenId, userCompanyCode], + ); + + const layoutData = layoutResult.rows[0]?.layout_data; + const flowIds = this.collectFlowIdsFromLayoutData(layoutData); + + // 2. ๊ฐ flowId๊ฐ€ ๋‹ค๋ฅธ ํ™”๋ฉด์—์„œ๋„ ์‚ฌ์šฉ๋˜๋Š”์ง€ ์ฒดํฌ ํ›„ ์‚ญ์ œ + if (flowIds.size > 0) { + for (const flowId of flowIds) { + // ๋‹ค๋ฅธ ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ ์ค‘์ธ์ง€ ํ™•์ธ (๊ฐ™์€ ํšŒ์‚ฌ ๋‚ด, ์‚ญ์ œ๋˜์ง€ ์•Š์€ ํ™”๋ฉด ๊ธฐ์ค€) + const companyFilterForCheck = userCompanyCode === "*" ? "" : " AND sd.company_code = $3"; + const checkParams = userCompanyCode === "*" + ? [screenId, flowId] + : [screenId, flowId, userCompanyCode]; + + const otherUsageResult = await client.query<{ count: string }>( + `SELECT COUNT(*) as count FROM screen_layouts_v2 slv + JOIN screen_definitions sd ON slv.screen_id = sd.screen_id + WHERE slv.screen_id != $1 + AND sd.is_active != 'D' + ${companyFilterForCheck} + AND ( + slv.layout_data::text LIKE '%"flowId":' || $2 || '%' + OR slv.layout_data::text LIKE '%"flowId":"' || $2 || '"%' + )`, + checkParams, + ); + + const otherUsageCount = parseInt(otherUsageResult.rows[0]?.count || "0"); + + // ๋‹ค๋ฅธ ํ™”๋ฉด์—์„œ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์—๋งŒ ํ”Œ๋กœ์šฐ ์‚ญ์ œ + if (otherUsageCount === 0) { + // ํ•ด๋‹น ํšŒ์‚ฌ์˜ ํ”Œ๋กœ์šฐ๋งŒ ์‚ญ์ œ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ) + const companyFilter = userCompanyCode === "*" ? "" : " AND company_code = $2"; + const flowParams = userCompanyCode === "*" ? [flowId] : [flowId, userCompanyCode]; + + // 1. flow_definition ๊ด€๋ จ ๋ฐ์ดํ„ฐ ๋จผ์ € ์‚ญ์ œ (์™ธ๋ž˜ํ‚ค ์ˆœ์„œ) + await client.query( + `DELETE FROM flow_step_connection WHERE flow_definition_id = $1`, + [flowId], + ); + await client.query( + `DELETE FROM flow_step WHERE flow_definition_id = $1`, + [flowId], + ); + await client.query( + `DELETE FROM flow_definition WHERE id = $1${companyFilter}`, + flowParams, + ); + + // 2. node_flows ํ…Œ์ด๋ธ”์—์„œ๋„ ์‚ญ์ œ (์ œ์–ดํ”Œ๋กœ์šฐ) + await client.query( + `DELETE FROM node_flows WHERE flow_id = $1${companyFilter}`, + flowParams, + ); + + logger.info("ํ™”๋ฉด ์‚ญ์ œ ์‹œ ํ”Œ๋กœ์šฐ ์‚ญ์ œ (flow_definition + node_flows)", { screenId, flowId, companyCode: userCompanyCode }); + } else { + logger.debug("ํ”Œ๋กœ์šฐ๊ฐ€ ๋‹ค๋ฅธ ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ ์ค‘ - ์‚ญ์ œ ์Šคํ‚ต", { screenId, flowId, otherUsageCount }); + } + } + } + + // 3. ์†Œํ”„ํŠธ ์‚ญ์ œ (ํœด์ง€ํ†ต์œผ๋กœ ์ด๋™) await client.query( `UPDATE screen_definitions SET is_active = 'D', @@ -655,13 +724,21 @@ export class ScreenManagementService { ], ); - // ๋ฉ”๋‰ด ํ• ๋‹น๋„ ๋น„ํ™œ์„ฑํ™” + // 4. ๋ฉ”๋‰ด ํ• ๋‹น๋„ ๋น„ํ™œ์„ฑํ™” await client.query( `UPDATE screen_menu_assignments SET is_active = 'N' WHERE screen_id = $1 AND is_active = 'Y'`, [screenId], ); + + // 5. ํ™”๋ฉด ๊ทธ๋ฃน ์—ฐ๊ฒฐ ์‚ญ์ œ (screen_group_screens) + await client.query( + `DELETE FROM screen_group_screens WHERE screen_id = $1`, + [screenId], + ); + + logger.info("ํ™”๋ฉด ์‚ญ์ œ ์‹œ ๊ทธ๋ฃน ์—ฐ๊ฒฐ ํ•ด์ œ", { screenId }); }); } @@ -1665,18 +1742,28 @@ export class ScreenManagementService { console.log(`V2 ๋ ˆ์ด์•„์›ƒ ๋ฐœ๊ฒฌ, V2 ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜`); const layoutData = v2Layout.layout_data; + // URL์—์„œ ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ์ถ”์ถœํ•˜๋Š” ํ—ฌํผ ํ•จ์ˆ˜ + const getTypeFromUrl = (url: string | undefined): string => { + if (!url) return "component"; + const parts = url.split("/"); + return parts[parts.length - 1] || "component"; + }; + // V2 ํ˜•์‹์˜ components๋ฅผ LayoutData ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ - const components = (layoutData.components || []).map((comp: any) => ({ - id: comp.id, - type: comp.overrides?.type || "component", - position: comp.position || { x: 0, y: 0, z: 1 }, - size: comp.size || { width: 200, height: 100 }, - componentUrl: comp.url, - componentType: comp.overrides?.type, - componentConfig: comp.overrides || {}, - displayOrder: comp.displayOrder || 0, - ...comp.overrides, - })); + const components = (layoutData.components || []).map((comp: any) => { + const componentType = getTypeFromUrl(comp.url); + return { + id: comp.id, + type: componentType, + position: comp.position || { x: 0, y: 0, z: 1 }, + size: comp.size || { width: 200, height: 100 }, + componentUrl: comp.url, + componentType: componentType, + componentConfig: comp.overrides || {}, + displayOrder: comp.displayOrder || 0, + ...comp.overrides, + }; + }); // screenResolution์ด ์—†์œผ๋ฉด ์ปดํฌ๋„ŒํŠธ ์œ„์น˜ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž๋™ ๊ณ„์‚ฐ let screenResolution = layoutData.screenResolution; @@ -2936,7 +3023,7 @@ export class ScreenManagementService { * - current_sequence๋Š” 0์œผ๋กœ ์ดˆ๊ธฐํ™” */ /** - * ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ณต์ œ (numbering_rules_test ํ…Œ์ด๋ธ” ์‚ฌ์šฉ) + * ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ณต์ œ (numbering_rules ํ…Œ์ด๋ธ” ์‚ฌ์šฉ) * - menu_objid ์˜์กด์„ฑ ์ œ๊ฑฐ๋จ * - table_name + column_name + company_code ๊ธฐ๋ฐ˜ */ @@ -2954,10 +3041,10 @@ export class ScreenManagementService { console.log(`๐Ÿ”„ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ณต์‚ฌ ์‹œ์ž‘: ${ruleIds.size}๊ฐœ ๊ทœ์น™`); - // 1. ์›๋ณธ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (numbering_rules_test ํ…Œ์ด๋ธ”) + // 1. ์›๋ณธ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (numbering_rules ํ…Œ์ด๋ธ”) const ruleIdArray = Array.from(ruleIds); const sourceRulesResult = await client.query( - `SELECT * FROM numbering_rules_test WHERE rule_id = ANY($1)`, + `SELECT * FROM numbering_rules WHERE rule_id = ANY($1)`, [ruleIdArray], ); @@ -2970,7 +3057,7 @@ export class ScreenManagementService { // 2. ๋Œ€์ƒ ํšŒ์‚ฌ์˜ ๊ธฐ์กด ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (์ด๋ฆ„ ๊ธฐ์ค€) const existingRulesResult = await client.query( - `SELECT rule_id, rule_name FROM numbering_rules_test WHERE company_code = $1`, + `SELECT rule_id, rule_name FROM numbering_rules WHERE company_code = $1`, [targetCompanyCode], ); const existingRulesByName = new Map( @@ -2991,9 +3078,9 @@ export class ScreenManagementService { // ์ƒˆ๋กœ ๋ณต์‚ฌ - ์ƒˆ rule_id ์ƒ์„ฑ const newRuleId = `rule-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - // numbering_rules_test ๋ณต์‚ฌ (current_sequence = 0์œผ๋กœ ์ดˆ๊ธฐํ™”) + // numbering_rules ๋ณต์‚ฌ (current_sequence = 0์œผ๋กœ ์ดˆ๊ธฐํ™”) await client.query( - `INSERT INTO numbering_rules_test ( + `INSERT INTO numbering_rules ( rule_id, rule_name, description, separator, reset_period, current_sequence, table_name, column_name, company_code, created_at, updated_at, created_by, last_generated_date, @@ -3018,15 +3105,15 @@ export class ScreenManagementService { ], ); - // numbering_rule_parts_test ๋ณต์‚ฌ + // numbering_rule_parts ๋ณต์‚ฌ const partsResult = await client.query( - `SELECT * FROM numbering_rule_parts_test WHERE rule_id = $1 ORDER BY part_order`, + `SELECT * FROM numbering_rule_parts WHERE rule_id = $1 ORDER BY part_order`, [rule.rule_id], ); for (const part of partsResult.rows) { await client.query( - `INSERT INTO numbering_rule_parts_test ( + `INSERT INTO numbering_rule_parts ( rule_id, part_order, part_type, generation_method, auto_config, manual_config, company_code, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, @@ -3471,6 +3558,371 @@ export class ScreenManagementService { return flowIds; } + /** + * V2 ๋ ˆ์ด์•„์›ƒ์—์„œ flowId ์ˆ˜์ง‘ (screen_layouts_v2์šฉ) + * - overrides.flowId (flow-widget) + * - overrides.webTypeConfig.dataflowConfig.flowConfig.flowId (๋ฒ„ํŠผ) + * - overrides.webTypeConfig.dataflowConfig.flowControls[].flowId + * - overrides.action.excelAfterUploadFlows[].flowId + */ + private collectFlowIdsFromLayoutData(layoutData: any): Set { + const flowIds = new Set(); + if (!layoutData?.components) return flowIds; + + for (const comp of layoutData.components) { + const overrides = comp.overrides || {}; + + // 1. overrides.flowId (flow-widget ๋“ฑ) + if (overrides.flowId && !isNaN(parseInt(overrides.flowId))) { + flowIds.add(parseInt(overrides.flowId)); + } + + // 2. webTypeConfig.dataflowConfig.flowConfig.flowId (๋ฒ„ํŠผ) + const flowConfigId = overrides?.webTypeConfig?.dataflowConfig?.flowConfig?.flowId; + if (flowConfigId && !isNaN(parseInt(flowConfigId))) { + flowIds.add(parseInt(flowConfigId)); + } + + // 3. webTypeConfig.dataflowConfig.selectedDiagramId + const diagramId = overrides?.webTypeConfig?.dataflowConfig?.selectedDiagramId; + if (diagramId && !isNaN(parseInt(diagramId))) { + flowIds.add(parseInt(diagramId)); + } + + // 4. webTypeConfig.dataflowConfig.flowControls[].flowId + const flowControls = overrides?.webTypeConfig?.dataflowConfig?.flowControls; + if (Array.isArray(flowControls)) { + for (const control of flowControls) { + if (control?.flowId && !isNaN(parseInt(control.flowId))) { + flowIds.add(parseInt(control.flowId)); + } + } + } + + // 5. action.excelAfterUploadFlows[].flowId + const excelFlows = overrides?.action?.excelAfterUploadFlows; + if (Array.isArray(excelFlows)) { + for (const flow of excelFlows) { + if (flow?.flowId && !isNaN(parseInt(flow.flowId))) { + flowIds.add(parseInt(flow.flowId)); + } + } + } + } + + return flowIds; + } + + /** + * V2 ๋ ˆ์ด์•„์›ƒ์—์„œ numberingRuleId ์ˆ˜์ง‘ (screen_layouts_v2์šฉ) + * - overrides.autoGeneration.options.numberingRuleId + * - overrides.sections[].fields[].numberingRule.ruleId + * - overrides.action.excelNumberingRuleId + * - overrides.action.numberingRuleId + */ + private collectNumberingRuleIdsFromLayoutData(layoutData: any): Set { + const ruleIds = new Set(); + if (!layoutData?.components) return ruleIds; + + for (const comp of layoutData.components) { + const overrides = comp.overrides || {}; + + // 1. autoGeneration.options.numberingRuleId + const autoGenRuleId = overrides?.autoGeneration?.options?.numberingRuleId; + if (autoGenRuleId && typeof autoGenRuleId === "string" && autoGenRuleId.startsWith("rule-")) { + ruleIds.add(autoGenRuleId); + } + + // 2. sections[].fields[].numberingRule.ruleId + const sections = overrides?.sections; + if (Array.isArray(sections)) { + for (const section of sections) { + const fields = section?.fields; + if (Array.isArray(fields)) { + for (const field of fields) { + const ruleId = field?.numberingRule?.ruleId; + if (ruleId && typeof ruleId === "string" && ruleId.startsWith("rule-")) { + ruleIds.add(ruleId); + } + } + } + // optionalFieldGroups ๋‚ด๋ถ€ + const optGroups = section?.optionalFieldGroups; + if (Array.isArray(optGroups)) { + for (const optGroup of optGroups) { + const optFields = optGroup?.fields; + if (Array.isArray(optFields)) { + for (const field of optFields) { + const ruleId = field?.numberingRule?.ruleId; + if (ruleId && typeof ruleId === "string" && ruleId.startsWith("rule-")) { + ruleIds.add(ruleId); + } + } + } + } + } + } + } + + // 3. action.excelNumberingRuleId + const excelRuleId = overrides?.action?.excelNumberingRuleId; + if (excelRuleId && typeof excelRuleId === "string" && excelRuleId.startsWith("rule-")) { + ruleIds.add(excelRuleId); + } + + // 4. action.numberingRuleId + const actionRuleId = overrides?.action?.numberingRuleId; + if (actionRuleId && typeof actionRuleId === "string" && actionRuleId.startsWith("rule-")) { + ruleIds.add(actionRuleId); + } + } + + return ruleIds; + } + + /** + * V2 ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ์˜ ์ฐธ์กฐ ID๋“ค์„ ์—…๋ฐ์ดํŠธ + * - componentId, flowId, numberingRuleId, screenId ๋งคํ•‘ ์ ์šฉ + */ + private updateReferencesInLayoutData( + layoutData: any, + mappings: { + componentIdMap: Map; + flowIdMap?: Map; + ruleIdMap?: Map; + screenIdMap?: Map; + }, + ): any { + if (!layoutData?.components) return layoutData; + + const updatedComponents = layoutData.components.map((comp: any) => { + // 1. componentId ๋งคํ•‘ + const newId = mappings.componentIdMap.get(comp.id) || comp.id; + + // 2. overrides ๋ณต์‚ฌ ๋ฐ ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ + let overrides = JSON.parse(JSON.stringify(comp.overrides || {})); + + // flowId ๋งคํ•‘ + if (mappings.flowIdMap && mappings.flowIdMap.size > 0) { + overrides = this.updateFlowIdsInOverrides(overrides, mappings.flowIdMap); + } + + // numberingRuleId ๋งคํ•‘ + if (mappings.ruleIdMap && mappings.ruleIdMap.size > 0) { + overrides = this.updateNumberingRuleIdsInOverrides(overrides, mappings.ruleIdMap); + } + + // screenId ๋งคํ•‘ (ํƒญ, ๋ฒ„ํŠผ ๋“ฑ) + if (mappings.screenIdMap && mappings.screenIdMap.size > 0) { + overrides = this.updateScreenIdsInOverrides(overrides, mappings.screenIdMap); + } + + return { + ...comp, + id: newId, + overrides, + }; + }); + + return { + ...layoutData, + components: updatedComponents, + updatedAt: new Date().toISOString(), + }; + } + + /** + * V2 overrides ๋‚ด์˜ flowId ์—…๋ฐ์ดํŠธ + */ + private updateFlowIdsInOverrides( + overrides: any, + flowIdMap: Map, + ): any { + if (!overrides || flowIdMap.size === 0) return overrides; + + // 1. overrides.flowId (flow-widget) + if (overrides.flowId) { + const oldId = parseInt(overrides.flowId); + const newId = flowIdMap.get(oldId); + if (newId) { + overrides.flowId = newId; + console.log(` ๐Ÿ”— flowId: ${oldId} โ†’ ${newId}`); + } + } + + // 2. webTypeConfig.dataflowConfig.flowConfig.flowId + if (overrides?.webTypeConfig?.dataflowConfig?.flowConfig?.flowId) { + const oldId = parseInt(overrides.webTypeConfig.dataflowConfig.flowConfig.flowId); + const newId = flowIdMap.get(oldId); + if (newId) { + overrides.webTypeConfig.dataflowConfig.flowConfig.flowId = newId; + console.log(` ๐Ÿ”— flowConfig.flowId: ${oldId} โ†’ ${newId}`); + } + } + + // 3. webTypeConfig.dataflowConfig.selectedDiagramId + if (overrides?.webTypeConfig?.dataflowConfig?.selectedDiagramId) { + const oldId = parseInt(overrides.webTypeConfig.dataflowConfig.selectedDiagramId); + const newId = flowIdMap.get(oldId); + if (newId) { + overrides.webTypeConfig.dataflowConfig.selectedDiagramId = newId; + console.log(` ๐Ÿ”— selectedDiagramId: ${oldId} โ†’ ${newId}`); + } + } + + // 4. webTypeConfig.dataflowConfig.flowControls[] + if (Array.isArray(overrides?.webTypeConfig?.dataflowConfig?.flowControls)) { + for (const control of overrides.webTypeConfig.dataflowConfig.flowControls) { + if (control?.flowId) { + const oldId = parseInt(control.flowId); + const newId = flowIdMap.get(oldId); + if (newId) { + control.flowId = newId; + console.log(` ๐Ÿ”— flowControls.flowId: ${oldId} โ†’ ${newId}`); + } + } + } + } + + // 5. action.excelAfterUploadFlows[] + if (Array.isArray(overrides?.action?.excelAfterUploadFlows)) { + for (const flow of overrides.action.excelAfterUploadFlows) { + if (flow?.flowId) { + const oldId = parseInt(flow.flowId); + const newId = flowIdMap.get(oldId); + if (newId) { + flow.flowId = newId; + console.log(` ๐Ÿ”— excelAfterUploadFlows.flowId: ${oldId} โ†’ ${newId}`); + } + } + } + } + + return overrides; + } + + /** + * V2 overrides ๋‚ด์˜ numberingRuleId ์—…๋ฐ์ดํŠธ + */ + private updateNumberingRuleIdsInOverrides( + overrides: any, + ruleIdMap: Map, + ): any { + if (!overrides || ruleIdMap.size === 0) return overrides; + + // 1. autoGeneration.options.numberingRuleId + if (overrides?.autoGeneration?.options?.numberingRuleId) { + const oldId = overrides.autoGeneration.options.numberingRuleId; + const newId = ruleIdMap.get(oldId); + if (newId) { + overrides.autoGeneration.options.numberingRuleId = newId; + console.log(` ๐Ÿ”— autoGeneration.numberingRuleId: ${oldId} โ†’ ${newId}`); + } + } + + // 2. sections[].fields[].numberingRule.ruleId + if (Array.isArray(overrides?.sections)) { + for (const section of overrides.sections) { + if (Array.isArray(section?.fields)) { + for (const field of section.fields) { + if (field?.numberingRule?.ruleId) { + const oldId = field.numberingRule.ruleId; + const newId = ruleIdMap.get(oldId); + if (newId) { + field.numberingRule.ruleId = newId; + console.log(` ๐Ÿ”— field.numberingRule.ruleId: ${oldId} โ†’ ${newId}`); + } + } + } + } + if (Array.isArray(section?.optionalFieldGroups)) { + for (const optGroup of section.optionalFieldGroups) { + if (Array.isArray(optGroup?.fields)) { + for (const field of optGroup.fields) { + if (field?.numberingRule?.ruleId) { + const oldId = field.numberingRule.ruleId; + const newId = ruleIdMap.get(oldId); + if (newId) { + field.numberingRule.ruleId = newId; + console.log(` ๐Ÿ”— optField.numberingRule.ruleId: ${oldId} โ†’ ${newId}`); + } + } + } + } + } + } + } + } + + // 3. action.excelNumberingRuleId + if (overrides?.action?.excelNumberingRuleId) { + const oldId = overrides.action.excelNumberingRuleId; + const newId = ruleIdMap.get(oldId); + if (newId) { + overrides.action.excelNumberingRuleId = newId; + console.log(` ๐Ÿ”— excelNumberingRuleId: ${oldId} โ†’ ${newId}`); + } + } + + // 4. action.numberingRuleId + if (overrides?.action?.numberingRuleId) { + const oldId = overrides.action.numberingRuleId; + const newId = ruleIdMap.get(oldId); + if (newId) { + overrides.action.numberingRuleId = newId; + console.log(` ๐Ÿ”— action.numberingRuleId: ${oldId} โ†’ ${newId}`); + } + } + + return overrides; + } + + /** + * V2 overrides ๋‚ด์˜ screenId ์—…๋ฐ์ดํŠธ (ํƒญ, ๋ฒ„ํŠผ ๋“ฑ) + */ + private updateScreenIdsInOverrides( + overrides: any, + screenIdMap: Map, + ): any { + if (!overrides || screenIdMap.size === 0) return overrides; + + // 1. tabs[].screenId (ํƒญ ์œ„์ ฏ) + if (Array.isArray(overrides?.tabs)) { + for (const tab of overrides.tabs) { + if (tab?.screenId) { + const oldId = parseInt(tab.screenId); + const newId = screenIdMap.get(oldId); + if (newId) { + tab.screenId = newId; + console.log(` ๐Ÿ”— tab.screenId: ${oldId} โ†’ ${newId}`); + } + } + } + } + + // 2. action.targetScreenId (๋ฒ„ํŠผ) + if (overrides?.action?.targetScreenId) { + const oldId = parseInt(overrides.action.targetScreenId); + const newId = screenIdMap.get(oldId); + if (newId) { + overrides.action.targetScreenId = newId; + console.log(` ๐Ÿ”— action.targetScreenId: ${oldId} โ†’ ${newId}`); + } + } + + // 3. action.modalScreenId + if (overrides?.action?.modalScreenId) { + const oldId = parseInt(overrides.action.modalScreenId); + const newId = screenIdMap.get(oldId); + if (newId) { + overrides.action.modalScreenId = newId; + console.log(` ๐Ÿ”— action.modalScreenId: ${oldId} โ†’ ${newId}`); + } + } + + return overrides; + } + /** * ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ๋ณต์‚ฌ ๋ฐ ID ๋งคํ•‘ ๋ฐ˜ํ™˜ * - ์›๋ณธ ํšŒ์‚ฌ์˜ ํ”Œ๋กœ์šฐ๋ฅผ ๋Œ€์ƒ ํšŒ์‚ฌ๋กœ ๋ณต์‚ฌ @@ -3709,24 +4161,34 @@ export class ScreenManagementService { const newScreen = newScreenResult.rows[0]; - // 4. ์›๋ณธ ํ™”๋ฉด์˜ ๋ ˆ์ด์•„์›ƒ ์ •๋ณด ์กฐํšŒ - const sourceLayoutsResult = await client.query( - `SELECT * FROM screen_layouts - WHERE screen_id = $1 - ORDER BY display_order ASC NULLS LAST`, - [sourceScreenId], + // 4. ์›๋ณธ ํ™”๋ฉด์˜ V2 ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ + let sourceLayoutV2Result = await client.query<{ layout_data: any }>( + `SELECT layout_data FROM screen_layouts_v2 + WHERE screen_id = $1 AND company_code = $2`, + [sourceScreenId, sourceScreen.company_code], ); - const sourceLayouts = sourceLayoutsResult.rows; + // ์—†์œผ๋ฉด ๊ณตํ†ต(*) ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ + let layoutData = sourceLayoutV2Result.rows[0]?.layout_data; + if (!layoutData && sourceScreen.company_code !== "*") { + const fallbackResult = await client.query<{ layout_data: any }>( + `SELECT layout_data FROM screen_layouts_v2 + WHERE screen_id = $1 AND company_code = '*'`, + [sourceScreenId], + ); + layoutData = fallbackResult.rows[0]?.layout_data; + } + + const components = layoutData?.components || []; // 5. ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ๋ณต์‚ฌ (ํšŒ์‚ฌ๊ฐ€ ๋‹ค๋ฅธ ๊ฒฝ์šฐ) let flowIdMap = new Map(); if ( - sourceLayouts.length > 0 && + components.length > 0 && sourceScreen.company_code !== targetCompanyCode ) { - // ๋ ˆ์ด์•„์›ƒ์—์„œ ์‚ฌ์šฉํ•˜๋Š” flowId ์ˆ˜์ง‘ - const flowIds = this.collectFlowIdsFromLayouts(sourceLayouts); + // V2 ๋ ˆ์ด์•„์›ƒ์—์„œ flowId ์ˆ˜์ง‘ + const flowIds = this.collectFlowIdsFromLayoutData(layoutData); if (flowIds.size > 0) { console.log(`๐Ÿ” ํ™”๋ฉด ๋ณต์‚ฌ - flowId ์ˆ˜์ง‘: ${flowIds.size}๊ฐœ`); @@ -3744,11 +4206,11 @@ export class ScreenManagementService { // 5.1. ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ณต์‚ฌ (ํšŒ์‚ฌ๊ฐ€ ๋‹ค๋ฅธ ๊ฒฝ์šฐ) let ruleIdMap = new Map(); if ( - sourceLayouts.length > 0 && + components.length > 0 && sourceScreen.company_code !== targetCompanyCode ) { - // ๋ ˆ์ด์•„์›ƒ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ฑ„๋ฒˆ ๊ทœ์น™ ID ์ˆ˜์ง‘ - const ruleIds = this.collectNumberingRuleIdsFromLayouts(sourceLayouts); + // V2 ๋ ˆ์ด์•„์›ƒ์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™ ID ์ˆ˜์ง‘ + const ruleIds = this.collectNumberingRuleIdsFromLayoutData(layoutData); if (ruleIds.size > 0) { console.log(`๐Ÿ” ํ™”๋ฉด ๋ณต์‚ฌ - ์ฑ„๋ฒˆ ๊ทœ์น™ ID ์ˆ˜์ง‘: ${ruleIds.size}๊ฐœ`); @@ -3763,81 +4225,43 @@ export class ScreenManagementService { } } - // 6. ๋ ˆ์ด์•„์›ƒ์ด ์žˆ๋‹ค๋ฉด ๋ณต์‚ฌ - if (sourceLayouts.length > 0) { + // 6. V2 ๋ ˆ์ด์•„์›ƒ์ด ์žˆ๋‹ค๋ฉด ๋ณต์‚ฌ + if (layoutData && components.length > 0) { try { - // ID ๋งคํ•‘ ๋งต ์ƒ์„ฑ - const idMapping: { [oldId: string]: string } = {}; - - // ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ ID ๋ฏธ๋ฆฌ ์ƒ์„ฑ - sourceLayouts.forEach((layout: any) => { - idMapping[layout.component_id] = generateId(); - }); - - // ๊ฐ ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ ๋ณต์‚ฌ - for (const sourceLayout of sourceLayouts) { - const newComponentId = idMapping[sourceLayout.component_id]; - const newParentId = sourceLayout.parent_id - ? idMapping[sourceLayout.parent_id] - : null; - - // properties ํŒŒ์‹ฑ - let properties = sourceLayout.properties; - if (typeof properties === "string") { - try { - properties = JSON.parse(properties); - } catch (e) { - // ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ - } - } - - // flowId ๋งคํ•‘ ์ ์šฉ (ํšŒ์‚ฌ๊ฐ€ ๋‹ค๋ฅธ ๊ฒฝ์šฐ) - if (flowIdMap.size > 0) { - properties = this.updateFlowIdsInProperties( - properties, - flowIdMap, - ); - } - - // ์ฑ„๋ฒˆ ๊ทœ์น™ ID ๋งคํ•‘ ์ ์šฉ (ํšŒ์‚ฌ๊ฐ€ ๋‹ค๋ฅธ ๊ฒฝ์šฐ) - if (ruleIdMap.size > 0) { - properties = this.updateNumberingRuleIdsInProperties( - properties, - ruleIdMap, - ); - } - - // ํƒญ ์ปดํฌ๋„ŒํŠธ์˜ screenId๋Š” ๊ฐœ๋ณ„ ๋ณต์ œ ์‹œ์ ์— ์—…๋ฐ์ดํŠธํ•˜์ง€ ์•Š์Œ - // ๋ชจ๋“  ํ™”๋ฉด ๋ณต์ œ ์™„๋ฃŒ ํ›„ updateTabScreenReferences์—์„œ screenIdMap ๊ธฐ๋ฐ˜์œผ๋กœ ์ผ๊ด„ ์—…๋ฐ์ดํŠธ - - await client.query( - `INSERT INTO screen_layouts ( - screen_id, component_type, component_id, parent_id, - position_x, position_y, width, height, properties, - display_order, created_date - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, - [ - newScreen.screen_id, - sourceLayout.component_type, - newComponentId, - newParentId, - Math.round(sourceLayout.position_x), // ์ •์ˆ˜๋กœ ๋ฐ˜์˜ฌ๋ฆผ - Math.round(sourceLayout.position_y), // ์ •์ˆ˜๋กœ ๋ฐ˜์˜ฌ๋ฆผ - Math.round(sourceLayout.width), // ์ •์ˆ˜๋กœ ๋ฐ˜์˜ฌ๋ฆผ - Math.round(sourceLayout.height), // ์ •์ˆ˜๋กœ ๋ฐ˜์˜ฌ๋ฆผ - JSON.stringify(properties), - sourceLayout.display_order, - new Date(), - ], - ); + // componentId ๋งคํ•‘ ์ƒ์„ฑ + const componentIdMap = new Map(); + for (const comp of components) { + componentIdMap.set(comp.id, generateId()); } + + // V2 ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ ๋ณต์‚ฌ ๋ฐ ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ + const updatedLayoutData = this.updateReferencesInLayoutData( + layoutData, + { + componentIdMap, + flowIdMap: flowIdMap.size > 0 ? flowIdMap : undefined, + ruleIdMap: ruleIdMap.size > 0 ? ruleIdMap : undefined, + // screenIdMap์€ ๋ชจ๋“  ํ™”๋ฉด ๋ณต์ œ ์™„๋ฃŒ ํ›„ updateTabScreenReferences์—์„œ ์ผ๊ด„ ์ฒ˜๋ฆฌ + }, + ); + + // V2 ๋ ˆ์ด์•„์›ƒ ์ €์žฅ (UPSERT) + await client.query( + `INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data, created_at, updated_at) + VALUES ($1, $2, $3, NOW(), NOW()) + ON CONFLICT (screen_id, company_code) + DO UPDATE SET layout_data = $3, updated_at = NOW()`, + [newScreen.screen_id, targetCompanyCode, JSON.stringify(updatedLayoutData)], + ); + + console.log(` โœ… V2 ๋ ˆ์ด์•„์›ƒ ๋ณต์‚ฌ ์™„๋ฃŒ: ${components.length}๊ฐœ ์ปดํฌ๋„ŒํŠธ`); } catch (error) { - console.error("๋ ˆ์ด์•„์›ƒ ๋ณต์‚ฌ ์ค‘ ์˜ค๋ฅ˜:", error); + console.error("V2 ๋ ˆ์ด์•„์›ƒ ๋ณต์‚ฌ ์ค‘ ์˜ค๋ฅ˜:", error); // ๋ ˆ์ด์•„์›ƒ ๋ณต์‚ฌ ์‹คํŒจํ•ด๋„ ํ™”๋ฉด ์ƒ์„ฑ์€ ์œ ์ง€ } } - // 6. ์ƒ์„ฑ๋œ ํ™”๋ฉด ์ •๋ณด ๋ฐ˜ํ™˜ + // 7. ์ƒ์„ฑ๋œ ํ™”๋ฉด ์ •๋ณด ๋ฐ˜ํ™˜ return { screenId: newScreen.screen_id, screenCode: newScreen.screen_code, @@ -4195,7 +4619,8 @@ export class ScreenManagementService { ); if (menuInfo.rows.length > 0) { - const isAdminMenu = menuInfo.rows[0].menu_type === "1"; + // menu_type: "0" = ๊ด€๋ฆฌ์ž ๋ฉ”๋‰ด, "1" = ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด + const isAdminMenu = menuInfo.rows[0].menu_type === "0"; const newMenuUrl = isAdminMenu ? `/screens/${newScreenId}?mode=admin` : `/screens/${newScreenId}`; @@ -4248,6 +4673,15 @@ export class ScreenManagementService { details: [] as string[], }; + // ๊ฐ™์€ ํšŒ์‚ฌ๋กœ ๋ณต์ œํ•˜๋Š” ๊ฒฝ์šฐ ์Šคํ‚ต (์ž๊ธฐ ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ ์‚ญ์ œ ๋ฐฉ์ง€) + if (sourceCompanyCode === targetCompanyCode) { + logger.warn( + `โš ๏ธ ๊ฐ™์€ ํšŒ์‚ฌ๋กœ ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ๋ณต์ œ ์‹œ๋„ - ์Šคํ‚ต: ${sourceCompanyCode}`, + ); + result.details.push("๊ฐ™์€ ํšŒ์‚ฌ๋กœ๋Š” ๋ณต์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + return result; + } + return transaction(async (client) => { logger.info( `๐Ÿ“ฆ ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ๋ณต์ œ: ${sourceCompanyCode} โ†’ ${targetCompanyCode}`, @@ -4351,7 +4785,7 @@ export class ScreenManagementService { } /** - * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ณต์ œ (category_values_test ํ…Œ์ด๋ธ” ์‚ฌ์šฉ) + * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ณต์ œ (category_values ํ…Œ์ด๋ธ” ์‚ฌ์šฉ) * - menu_objid ์˜์กด์„ฑ ์ œ๊ฑฐ๋จ * - table_name + column_name + company_code ๊ธฐ๋ฐ˜ */ @@ -4369,20 +4803,29 @@ export class ScreenManagementService { details: [] as string[], }; + // ๊ฐ™์€ ํšŒ์‚ฌ๋กœ ๋ณต์ œํ•˜๋Š” ๊ฒฝ์šฐ ์Šคํ‚ต (์ž๊ธฐ ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ ์‚ญ์ œ ๋ฐฉ์ง€) + if (sourceCompanyCode === targetCompanyCode) { + logger.warn( + `โš ๏ธ ๊ฐ™์€ ํšŒ์‚ฌ๋กœ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ณต์ œ ์‹œ๋„ - ์Šคํ‚ต: ${sourceCompanyCode}`, + ); + result.details.push("๊ฐ™์€ ํšŒ์‚ฌ๋กœ๋Š” ๋ณต์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + return result; + } + return transaction(async (client) => { logger.info( `๐Ÿ“ฆ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ณต์ œ: ${sourceCompanyCode} โ†’ ${targetCompanyCode}`, ); - // 1. ๊ธฐ์กด ๋Œ€์ƒ ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ์‚ญ์ œ + // 1. ๊ธฐ์กด ๋Œ€์ƒ ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ์‚ญ์ œ (๋‹ค๋ฅธ ํšŒ์‚ฌ๋กœ ๋ณต์ œ ์‹œ์—๋งŒ) await client.query( - `DELETE FROM category_values_test WHERE company_code = $1`, + `DELETE FROM category_values WHERE company_code = $1`, [targetCompanyCode], ); - // 2. category_values_test ๋ณต์ œ + // 2. category_values ๋ณต์ œ const values = await client.query( - `SELECT * FROM category_values_test WHERE company_code = $1`, + `SELECT * FROM category_values WHERE company_code = $1`, [sourceCompanyCode], ); @@ -4391,7 +4834,7 @@ export class ScreenManagementService { for (const v of values.rows) { const insertResult = await client.query( - `INSERT INTO category_values_test + `INSERT INTO category_values (table_name, column_name, value_code, value_label, value_order, parent_value_id, depth, path, description, color, icon, is_active, is_default, company_code, created_by) @@ -4426,7 +4869,7 @@ export class ScreenManagementService { const newValueId = valueIdMap.get(v.value_id); if (newParentId && newValueId) { await client.query( - `UPDATE category_values_test SET parent_value_id = $1 WHERE value_id = $2`, + `UPDATE category_values SET parent_value_id = $1 WHERE value_id = $2`, [newParentId, newValueId], ); } @@ -4451,6 +4894,15 @@ export class ScreenManagementService { details: [] as string[], }; + // ๊ฐ™์€ ํšŒ์‚ฌ๋กœ ๋ณต์ œํ•˜๋Š” ๊ฒฝ์šฐ ์Šคํ‚ต (์ž๊ธฐ ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ ์‚ญ์ œ ๋ฐฉ์ง€) + if (sourceCompanyCode === targetCompanyCode) { + logger.warn( + `โš ๏ธ ๊ฐ™์€ ํšŒ์‚ฌ๋กœ ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ ์‹œ๋„ - ์Šคํ‚ต: ${sourceCompanyCode}`, + ); + result.details.push("๊ฐ™์€ ํšŒ์‚ฌ๋กœ๋Š” ๋ณต์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + return result; + } + return transaction(async (client) => { logger.info( `๐Ÿ“ฆ ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ: ${sourceCompanyCode} โ†’ ${targetCompanyCode}`, @@ -4514,6 +4966,15 @@ export class ScreenManagementService { details: [] as string[], }; + // ๊ฐ™์€ ํšŒ์‚ฌ๋กœ ๋ณต์ œํ•˜๋Š” ๊ฒฝ์šฐ ์Šคํ‚ต (์ž๊ธฐ ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ ์‚ญ์ œ ๋ฐฉ์ง€) + if (sourceCompanyCode === targetCompanyCode) { + logger.warn( + `โš ๏ธ ๊ฐ™์€ ํšŒ์‚ฌ๋กœ ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ ์‹œ๋„ - ์Šคํ‚ต: ${sourceCompanyCode}`, + ); + result.details.push("๊ฐ™์€ ํšŒ์‚ฌ๋กœ๋Š” ๋ณต์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + return result; + } + return transaction(async (client) => { logger.info( `๐Ÿ“ฆ ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ: ${sourceCompanyCode} โ†’ ${targetCompanyCode}`, diff --git a/backend-node/src/services/tableCategoryValueService.ts b/backend-node/src/services/tableCategoryValueService.ts index c4149147..2eb35f64 100644 --- a/backend-node/src/services/tableCategoryValueService.ts +++ b/backend-node/src/services/tableCategoryValueService.ts @@ -212,22 +212,22 @@ class TableCategoryValueService { updated_at AS "updatedAt", created_by AS "createdBy", updated_by AS "updatedBy" - FROM category_values_test + FROM category_values WHERE table_name = $1 AND column_name = $2 `; - // category_values_test ํ…Œ์ด๋ธ” ์‚ฌ์šฉ (menu_objid ์—†์Œ) + // category_values ํ…Œ์ด๋ธ” ์‚ฌ์šฉ (menu_objid ์—†์Œ) if (companyCode === "*") { // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ๊ฐ’ ์กฐํšŒ query = baseSelect; params = [tableName, columnName]; - logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์ฒด ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ (category_values_test)"); + logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์ฒด ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ (category_values)"); } else { // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ํšŒ์‚ฌ ๋˜๋Š” ๊ณตํ†ต(*) ์นดํ…Œ๊ณ ๋ฆฌ ์กฐํšŒ query = baseSelect + ` AND (company_code = $3 OR company_code = '*')`; params = [tableName, columnName, companyCode]; - logger.info("ํšŒ์‚ฌ๋ณ„ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ (category_values_test)", { companyCode }); + logger.info("ํšŒ์‚ฌ๋ณ„ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ (category_values)", { companyCode }); } if (!includeInactive) { diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index c8196235..db5f32ed 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -289,29 +289,48 @@ export class TableManagementService { companyCode, }); - const mappings = await query( - `SELECT - logical_column_name as "columnName", - menu_objid as "menuObjid" - FROM category_column_mapping - WHERE table_name = $1 - AND company_code = $2`, - [tableName, companyCode] - ); + try { + // menu_objid ์ปฌ๋Ÿผ์ด ์žˆ๋Š”์ง€ ๋จผ์ € ํ™•์ธ + const columnCheck = await query( + `SELECT column_name FROM information_schema.columns + WHERE table_name = 'category_column_mapping' AND column_name = 'menu_objid'` + ); - logger.info("โœ… getColumnList: ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์กฐํšŒ ์™„๋ฃŒ", { - tableName, - companyCode, - mappingCount: mappings.length, - mappings: mappings, - }); + if (columnCheck.length > 0) { + // menu_objid ์ปฌ๋Ÿผ์ด ์žˆ๋Š” ๊ฒฝ์šฐ + const mappings = await query( + `SELECT + logical_column_name as "columnName", + menu_objid as "menuObjid" + FROM category_column_mapping + WHERE table_name = $1 + AND company_code = $2`, + [tableName, companyCode] + ); - mappings.forEach((m: any) => { - if (!categoryMappings.has(m.columnName)) { - categoryMappings.set(m.columnName, []); + logger.info("โœ… getColumnList: ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์กฐํšŒ ์™„๋ฃŒ", { + tableName, + companyCode, + mappingCount: mappings.length, + }); + + mappings.forEach((m: any) => { + if (!categoryMappings.has(m.columnName)) { + categoryMappings.set(m.columnName, []); + } + categoryMappings.get(m.columnName)!.push(Number(m.menuObjid)); + }); + } else { + // menu_objid ์ปฌ๋Ÿผ์ด ์—†๋Š” ๊ฒฝ์šฐ - ๋งคํ•‘ ์—†์ด ์ง„ํ–‰ + logger.info( + "โš ๏ธ getColumnList: menu_objid ์ปฌ๋Ÿผ์ด ์—†์Œ, ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์Šคํ‚ต" + ); } - categoryMappings.get(m.columnName)!.push(Number(m.menuObjid)); - }); + } catch (mappingError: any) { + logger.warn("โš ๏ธ getColumnList: ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์กฐํšŒ ์‹คํŒจ, ์Šคํ‚ต", { + error: mappingError.message, + }); + } logger.info("โœ… getColumnList: categoryMappings Map ์ƒ์„ฑ ์™„๋ฃŒ", { size: categoryMappings.size, @@ -456,13 +475,25 @@ export class TableManagementService { `์ปฌ๋Ÿผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}.${columnName}, company: ${companyCode}` ); + // ๐Ÿ”ฅ "direct" ๋˜๋Š” "auto"๋Š” ํ”„๋ก ํŠธ์—”๋“œ์˜ ์ž…๋ ฅ ๋ฐฉ์‹ ๊ตฌ๋ถ„๊ฐ’์ด๋ฏ€๋กœ + // DB์˜ input_type(์›นํƒ€์ž…)์œผ๋กœ ์ €์žฅํ•˜๋ฉด ์•ˆ ๋จ - "text"๋กœ ๋ณ€ํ™˜ + if (settings.inputType === "direct" || settings.inputType === "auto") { + logger.warn( + `์ž˜๋ชป๋œ inputType ๊ฐ’ ๊ฐ์ง€: ${settings.inputType} โ†’ 'text'๋กœ ๋ณ€ํ™˜ (${tableName}.${columnName})` + ); + settings.inputType = "text"; + } + // ํ…Œ์ด๋ธ”์ด table_labels์— ์—†์œผ๋ฉด ์ž๋™ ์ถ”๊ฐ€ await this.insertTableIfNotExists(tableName); // table_type_columns์— ๋ชจ๋“  ์„ค์ • ์ €์žฅ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ง€์›) // detailSettings๊ฐ€ ๋ฌธ์ž์—ด์ด๋ฉด ๊ทธ๋Œ€๋กœ, ๊ฐ์ฒด๋ฉด JSON.stringify let detailSettingsStr = settings.detailSettings; - if (typeof settings.detailSettings === "object" && settings.detailSettings !== null) { + if ( + typeof settings.detailSettings === "object" && + settings.detailSettings !== null + ) { detailSettingsStr = JSON.stringify(settings.detailSettings); } @@ -708,12 +739,23 @@ export class TableManagementService { inputType?: string ): Promise { try { + // ๐Ÿ”ฅ 'direct'๋‚˜ 'auto'๋Š” ํ”„๋ก ํŠธ์—”๋“œ์˜ ์ž…๋ ฅ ๋ฐฉ์‹ ๊ตฌ๋ถ„๊ฐ’์ด๋ฏ€๋กœ + // DB์˜ input_type(์›นํƒ€์ž…)์œผ๋กœ ์ €์žฅํ•˜๋ฉด ์•ˆ ๋จ - 'text'๋กœ ๋ณ€ํ™˜ + let finalWebType = webType; + if (webType === "direct" || webType === "auto") { + logger.warn( + `์ž˜๋ชป๋œ webType ๊ฐ’ ๊ฐ์ง€: ${webType} โ†’ 'text'๋กœ ๋ณ€ํ™˜ (${tableName}.${columnName})` + ); + finalWebType = "text"; + } + logger.info( - `์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ์‹œ์ž‘: ${tableName}.${columnName} = ${webType}` + `์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ์‹œ์ž‘: ${tableName}.${columnName} = ${finalWebType}` ); // ์›น ํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ์ƒ์„ธ ์„ค์ • ์ƒ์„ฑ - const defaultDetailSettings = this.generateDefaultDetailSettings(webType); + const defaultDetailSettings = + this.generateDefaultDetailSettings(finalWebType); // ์‚ฌ์šฉ์ž ์ •์˜ ์„ค์ •๊ณผ ๊ธฐ๋ณธ ์„ค์ • ๋ณ‘ํ•ฉ const finalDetailSettings = { @@ -732,10 +774,15 @@ export class TableManagementService { input_type = EXCLUDED.input_type, detail_settings = EXCLUDED.detail_settings, updated_date = NOW()`, - [tableName, columnName, webType, JSON.stringify(finalDetailSettings)] + [ + tableName, + columnName, + finalWebType, + JSON.stringify(finalDetailSettings), + ] ); logger.info( - `์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ์™„๋ฃŒ: ${tableName}.${columnName} = ${webType}` + `์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ์™„๋ฃŒ: ${tableName}.${columnName} = ${finalWebType}` ); } catch (error) { logger.error( @@ -760,13 +807,23 @@ export class TableManagementService { detailSettings?: Record ): Promise { try { + // ๐Ÿ”ฅ 'direct'๋‚˜ 'auto'๋Š” ํ”„๋ก ํŠธ์—”๋“œ์˜ ์ž…๋ ฅ ๋ฐฉ์‹ ๊ตฌ๋ถ„๊ฐ’์ด๋ฏ€๋กœ + // DB์˜ input_type(์›นํƒ€์ž…)์œผ๋กœ ์ €์žฅํ•˜๋ฉด ์•ˆ ๋จ - 'text'๋กœ ๋ณ€ํ™˜ + let finalInputType = inputType; + if (inputType === "direct" || inputType === "auto") { + logger.warn( + `์ž˜๋ชป๋œ input_type ๊ฐ’ ๊ฐ์ง€: ${inputType} โ†’ 'text'๋กœ ๋ณ€ํ™˜ (${tableName}.${columnName})` + ); + finalInputType = "text"; + } + logger.info( - `์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ์‹œ์ž‘: ${tableName}.${columnName} = ${inputType}, company: ${companyCode}` + `์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ์‹œ์ž‘: ${tableName}.${columnName} = ${finalInputType}, company: ${companyCode}` ); // ์ž…๋ ฅ ํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ์ƒ์„ธ ์„ค์ • ์ƒ์„ฑ const defaultDetailSettings = - this.generateDefaultInputTypeSettings(inputType); + this.generateDefaultInputTypeSettings(finalInputType); // ์‚ฌ์šฉ์ž ์ •์˜ ์„ค์ •๊ณผ ๊ธฐ๋ณธ ์„ค์ • ๋ณ‘ํ•ฉ const finalDetailSettings = { @@ -788,7 +845,7 @@ export class TableManagementService { [ tableName, columnName, - inputType, + finalInputType, JSON.stringify(finalDetailSettings), companyCode, ] @@ -798,7 +855,7 @@ export class TableManagementService { await this.syncScreenLayoutsInputType( tableName, columnName, - inputType, + finalInputType, companyCode ); @@ -1415,6 +1472,44 @@ export class TableManagementService { }); } + // ๐Ÿ”ง ํŒŒ์ดํ”„๋กœ ๊ตฌ๋ถ„๋œ ๋ฌธ์ž์—ด ์ฒ˜๋ฆฌ (๊ฐ์ฒด์—์„œ ์ถ”์ถœํ•œ actualValue๋„ ์ฒ˜๋ฆฌ) + if (typeof actualValue === "string" && actualValue.includes("|")) { + const columnInfo = await this.getColumnWebTypeInfo( + tableName, + columnName + ); + + // ๋‚ ์งœ ํƒ€์ž…์ด๋ฉด ๋‚ ์งœ ๋ฒ”์œ„๋กœ ์ฒ˜๋ฆฌ + if ( + columnInfo && + (columnInfo.webType === "date" || columnInfo.webType === "datetime") + ) { + return this.buildDateRangeCondition( + columnName, + actualValue, + paramIndex + ); + } + + // ๊ทธ ์™ธ ํƒ€์ž…์ด๋ฉด ๋‹ค์ค‘์„ ํƒ(IN ์กฐ๊ฑด)์œผ๋กœ ์ฒ˜๋ฆฌ + const multiValues = actualValue + .split("|") + .filter((v: string) => v.trim() !== ""); + if (multiValues.length > 0) { + const placeholders = multiValues + .map((_: string, idx: number) => `$${paramIndex + idx}`) + .join(", "); + logger.info( + `๐Ÿ” ๋‹ค์ค‘์„ ํƒ ํ•„ํ„ฐ ์ ์šฉ (๊ฐ์ฒด์—์„œ ์ถ”์ถœ): ${columnName} IN (${multiValues.join(", ")})` + ); + return { + whereClause: `${columnName}::text IN (${placeholders})`, + values: multiValues, + paramCount: multiValues.length, + }; + } + } + // "__ALL__" ๊ฐ’์ด๊ฑฐ๋‚˜ ๋นˆ ๊ฐ’์ด๋ฉด ํ•„ํ„ฐ ์กฐ๊ฑด์„ ์ ์šฉํ•˜์ง€ ์•Š์Œ if ( actualValue === "__ALL__" || @@ -2171,6 +2266,9 @@ export class TableManagementService { ? `WHERE ${whereConditions.join(" AND ")}` : ""; + // ์•ˆ์ „ํ•œ ํ…Œ์ด๋ธ”๋ช… ๊ฒ€์ฆ + const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, ""); + // ORDER BY ์กฐ๊ฑด ๊ตฌ์„ฑ let orderClause = ""; if (sortBy) { @@ -2178,11 +2276,17 @@ export class TableManagementService { const safeSortOrder = sortOrder.toLowerCase() === "desc" ? "DESC" : "ASC"; orderClause = `ORDER BY ${safeSortBy} ${safeSortOrder}`; + } else { + // sortBy๊ฐ€ ์—†์œผ๋ฉด created_date ์ปฌ๋Ÿผ์ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ๊ธฐ๋ณธ ์ •๋ ฌ ์ ์šฉ + const hasCreatedDate = await query( + `SELECT 1 FROM information_schema.columns WHERE table_name = $1 AND column_name = 'created_date' LIMIT 1`, + [safeTableName] + ); + if (hasCreatedDate.length > 0) { + orderClause = `ORDER BY main.created_date DESC`; + } } - // ์•ˆ์ „ํ•œ ํ…Œ์ด๋ธ”๋ช… ๊ฒ€์ฆ - const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, ""); - // ์ „์ฒด ๊ฐœ์ˆ˜ ์กฐํšŒ (main ๋ณ„์นญ ์ถ”๊ฐ€ - buildWhereClause๊ฐ€ main. ์ ‘๋‘์‚ฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ํ•„์š”) const countQuery = `SELECT COUNT(*) as count FROM ${safeTableName} main ${whereClause}`; const countResult = await query(countQuery, searchValues); @@ -3090,9 +3194,13 @@ export class TableManagementService { } // ORDER BY ์ ˆ ๊ตฌ์„ฑ + // sortBy๊ฐ€ ์—†์œผ๋ฉด created_date ์ปฌ๋Ÿผ์ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ๊ธฐ๋ณธ ์ •๋ ฌ ์ ์šฉ + const hasCreatedDateColumn = selectColumns.includes("created_date"); const orderBy = options.sortBy - ? `main.${options.sortBy} ${options.sortOrder === "desc" ? "DESC" : "ASC"}` - : ""; + ? `main."${options.sortBy}" ${options.sortOrder === "desc" ? "DESC" : "ASC"}` + : hasCreatedDateColumn + ? `main."created_date" DESC` + : ""; // ํŽ˜์ด์ง• ๊ณ„์‚ฐ const offset = (options.page - 1) * options.size; @@ -3302,14 +3410,17 @@ export class TableManagementService { const entitySearchColumns: string[] = []; // Entity ์กฐ์ธ ์ฟผ๋ฆฌ ์ƒ์„ฑํ•˜์—ฌ ๋ณ„์นญ ๋งคํ•‘ ์–ป๊ธฐ + const hasCreatedDateForSearch = selectColumns.includes("created_date"); const joinQueryResult = entityJoinService.buildJoinQuery( tableName, joinConfigs, selectColumns, "", // WHERE ์ ˆ์€ ๋‚˜์ค‘์— ์ถ”๊ฐ€ options.sortBy - ? `main.${options.sortBy} ${options.sortOrder || "ASC"}` - : undefined, + ? `main."${options.sortBy}" ${options.sortOrder || "ASC"}` + : hasCreatedDateForSearch + ? `main."created_date" DESC` + : undefined, options.size, (options.page - 1) * options.size ); @@ -3323,14 +3434,16 @@ export class TableManagementService { if (options.search) { for (const [key, value] of Object.entries(options.search)) { - // ๊ฒ€์ƒ‰๊ฐ’ ์ถ”์ถœ (๊ฐ์ฒด ํ˜•ํƒœ์ผ ์ˆ˜ ์žˆ์Œ) + // ๊ฒ€์ƒ‰๊ฐ’ ๋ฐ operator ์ถ”์ถœ (๊ฐ์ฒด ํ˜•ํƒœ์ผ ์ˆ˜ ์žˆ์Œ) let searchValue = value; + let operator = "contains"; // ๊ธฐ๋ณธ๊ฐ’: ๋ถ€๋ถ„ ์ผ์น˜ if ( typeof value === "object" && value !== null && "value" in value ) { searchValue = value.value; + operator = (value as any).operator || "contains"; } // ๋นˆ ๊ฐ’์ด๋ฉด ์Šคํ‚ต @@ -3382,15 +3495,49 @@ export class TableManagementService { // ๊ธฐ๋ณธ Entity ์กฐ์ธ ์ปฌ๋Ÿผ์ธ ๊ฒฝ์šฐ: ์กฐ์ธ๋œ ํ…Œ์ด๋ธ”์˜ ํ‘œ์‹œ ์ปฌ๋Ÿผ์—์„œ ๊ฒ€์ƒ‰ const aliasKey = `${joinConfig.referenceTable}:${joinConfig.sourceColumn}`; const alias = aliasMap.get(aliasKey); - whereConditions.push( - `${alias}.${joinConfig.displayColumn} ILIKE '%${safeValue}%'` - ); - entitySearchColumns.push( - `${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})` - ); - logger.info( - `๐ŸŽฏ Entity ์กฐ์ธ ๊ฒ€์ƒ‰: ${key} โ†’ ${joinConfig.referenceTable}.${joinConfig.displayColumn} LIKE '%${safeValue}%' (๋ณ„์นญ: ${alias})` - ); + + // ๐Ÿ”ง ํŒŒ์ดํ”„๋กœ ๊ตฌ๋ถ„๋œ ๋‹ค์ค‘ ์„ ํƒ๊ฐ’ ์ฒ˜๋ฆฌ + if (safeValue.includes("|")) { + const multiValues = safeValue + .split("|") + .filter((v: string) => v.trim() !== ""); + if (multiValues.length > 0) { + const inClause = multiValues + .map((v: string) => `'${v}'`) + .join(", "); + whereConditions.push( + `${alias}.${joinConfig.displayColumn}::text IN (${inClause})` + ); + entitySearchColumns.push( + `${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})` + ); + logger.info( + `๐ŸŽฏ Entity ์กฐ์ธ ๋‹ค์ค‘์„ ํƒ ๊ฒ€์ƒ‰: ${key} โ†’ ${joinConfig.referenceTable}.${joinConfig.displayColumn} IN (${multiValues.join(", ")}) (๋ณ„์นญ: ${alias})` + ); + } + } else if (operator === "equals") { + // ๐Ÿ”ง equals ์—ฐ์‚ฐ์ž: ์ •ํ™•ํžˆ ์ผ์น˜ + whereConditions.push( + `${alias}.${joinConfig.displayColumn}::text = '${safeValue}'` + ); + entitySearchColumns.push( + `${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})` + ); + logger.info( + `๐ŸŽฏ Entity ์กฐ์ธ ์ •ํ™•ํžˆ ์ผ์น˜ ๊ฒ€์ƒ‰: ${key} โ†’ ${joinConfig.referenceTable}.${joinConfig.displayColumn} = '${safeValue}' (๋ณ„์นญ: ${alias})` + ); + } else { + // ๊ธฐ๋ณธ: ๋ถ€๋ถ„ ์ผ์น˜ (ILIKE) + whereConditions.push( + `${alias}.${joinConfig.displayColumn} ILIKE '%${safeValue}%'` + ); + entitySearchColumns.push( + `${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})` + ); + logger.info( + `๐ŸŽฏ Entity ์กฐ์ธ ๊ฒ€์ƒ‰: ${key} โ†’ ${joinConfig.referenceTable}.${joinConfig.displayColumn} LIKE '%${safeValue}%' (๋ณ„์นญ: ${alias})` + ); + } } else if (key === "writer_dept_code") { // writer_dept_code: user_info.dept_code์—์„œ ๊ฒ€์ƒ‰ const userAliasKey = Array.from(aliasMap.keys()).find((k) => @@ -3427,18 +3574,44 @@ export class TableManagementService { } } else { // ์ผ๋ฐ˜ ์ปฌ๋Ÿผ์ธ ๊ฒฝ์šฐ: ๋ฉ”์ธ ํ…Œ์ด๋ธ”์—์„œ ๊ฒ€์ƒ‰ - whereConditions.push(`main.${key} ILIKE '%${safeValue}%'`); - logger.info( - `๐Ÿ” ์ผ๋ฐ˜ ์ปฌ๋Ÿผ ๊ฒ€์ƒ‰: ${key} โ†’ main.${key} LIKE '%${safeValue}%'` - ); + // ๐Ÿ”ง ํŒŒ์ดํ”„๋กœ ๊ตฌ๋ถ„๋œ ๋‹ค์ค‘ ์„ ํƒ๊ฐ’ ์ฒ˜๋ฆฌ + if (safeValue.includes("|")) { + const multiValues = safeValue + .split("|") + .filter((v: string) => v.trim() !== ""); + if (multiValues.length > 0) { + const inClause = multiValues + .map((v: string) => `'${v}'`) + .join(", "); + whereConditions.push(`main.${key}::text IN (${inClause})`); + logger.info( + `๐Ÿ” ๋‹ค์ค‘์„ ํƒ ์ปฌ๋Ÿผ ๊ฒ€์ƒ‰: ${key} โ†’ main.${key} IN (${multiValues.join(", ")})` + ); + } + } else if (operator === "equals") { + // ๐Ÿ”ง equals ์—ฐ์‚ฐ์ž: ์ •ํ™•ํžˆ ์ผ์น˜ + whereConditions.push(`main.${key}::text = '${safeValue}'`); + logger.info( + `๐Ÿ” ์ •ํ™•ํžˆ ์ผ์น˜ ๊ฒ€์ƒ‰: ${key} โ†’ main.${key} = '${safeValue}'` + ); + } else { + // ๊ธฐ๋ณธ: ๋ถ€๋ถ„ ์ผ์น˜ (ILIKE) + whereConditions.push(`main.${key} ILIKE '%${safeValue}%'`); + logger.info( + `๐Ÿ” ์ผ๋ฐ˜ ์ปฌ๋Ÿผ ๊ฒ€์ƒ‰: ${key} โ†’ main.${key} LIKE '%${safeValue}%'` + ); + } } } } const whereClause = whereConditions.join(" AND "); + const hasCreatedDateForOrder = selectColumns.includes("created_date"); const orderBy = options.sortBy - ? `main.${options.sortBy} ${options.sortOrder === "desc" ? "DESC" : "ASC"}` - : ""; + ? `main."${options.sortBy}" ${options.sortOrder === "desc" ? "DESC" : "ASC"}` + : hasCreatedDateForOrder + ? `main."created_date" DESC` + : ""; // ํŽ˜์ด์ง• ๊ณ„์‚ฐ const offset = (options.page - 1) * options.size; @@ -3715,6 +3888,7 @@ export class TableManagementService { columnName: string; displayName: string; dataType: string; + inputType?: string; }> > { return await entityJoinService.getReferenceTableColumns(tableName); @@ -4163,31 +4337,46 @@ export class TableManagementService { if (mappingTableExists) { logger.info("์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์กฐํšŒ ์‹œ์ž‘", { tableName, companyCode }); - const mappings = await query( - `SELECT DISTINCT ON (logical_column_name, menu_objid) - logical_column_name as "columnName", - menu_objid as "menuObjid" - FROM category_column_mapping - WHERE table_name = $1 - AND company_code IN ($2, '*') - ORDER BY logical_column_name, menu_objid, - CASE WHEN company_code = $2 THEN 0 ELSE 1 END`, - [tableName, companyCode] - ); + try { + // menu_objid ์ปฌ๋Ÿผ์ด ์žˆ๋Š”์ง€ ๋จผ์ € ํ™•์ธ + const columnCheck = await query( + `SELECT column_name FROM information_schema.columns + WHERE table_name = 'category_column_mapping' AND column_name = 'menu_objid'` + ); - logger.info("์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์กฐํšŒ ์™„๋ฃŒ", { - tableName, - companyCode, - mappingCount: mappings.length, - mappings: mappings, - }); + if (columnCheck.length > 0) { + const mappings = await query( + `SELECT DISTINCT ON (logical_column_name, menu_objid) + logical_column_name as "columnName", + menu_objid as "menuObjid" + FROM category_column_mapping + WHERE table_name = $1 + AND company_code IN ($2, '*') + ORDER BY logical_column_name, menu_objid, + CASE WHEN company_code = $2 THEN 0 ELSE 1 END`, + [tableName, companyCode] + ); - mappings.forEach((m: any) => { - if (!categoryMappings.has(m.columnName)) { - categoryMappings.set(m.columnName, []); + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์กฐํšŒ ์™„๋ฃŒ", { + tableName, + companyCode, + mappingCount: mappings.length, + }); + + mappings.forEach((m: any) => { + if (!categoryMappings.has(m.columnName)) { + categoryMappings.set(m.columnName, []); + } + categoryMappings.get(m.columnName)!.push(Number(m.menuObjid)); + }); + } else { + logger.info("โš ๏ธ menu_objid ์ปฌ๋Ÿผ์ด ์—†์Œ, ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์Šคํ‚ต"); } - categoryMappings.get(m.columnName)!.push(Number(m.menuObjid)); - }); + } catch (mappingError: any) { + logger.warn("โš ๏ธ ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์กฐํšŒ ์‹คํŒจ, ์Šคํ‚ต", { + error: mappingError.message, + }); + } logger.info("categoryMappings Map ์ƒ์„ฑ ์™„๋ฃŒ", { size: categoryMappings.size, diff --git a/db/migrations/RUN_078_MIGRATION.md b/db/migrations/RUN_078_MIGRATION.md new file mode 100644 index 00000000..05669d0c --- /dev/null +++ b/db/migrations/RUN_078_MIGRATION.md @@ -0,0 +1,83 @@ +# 078 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ๊ฐ€์ด๋“œ + +## ์‹คํ–‰ํ•  ํŒŒ์ผ (์ˆœ์„œ๋Œ€๋กœ) + +1. **078_create_production_plan_tables.sql** - ํ…Œ์ด๋ธ” ์ƒ์„ฑ +2. **078b_insert_production_plan_sample_data.sql** - ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ +3. **078c_insert_production_plan_screen.sql** - ํ™”๋ฉด ์ •์˜ ๋ฐ ๋ ˆ์ด์•„์›ƒ + +## ์‹คํ–‰ ๋ฐฉ๋ฒ• + +### ๋ฐฉ๋ฒ• 1: psql ๋ช…๋ น์–ด (ํ„ฐ๋ฏธ๋„) + +```bash +# ํ…Œ์ด๋ธ” ์ƒ์„ฑ +psql -h localhost -U postgres -d wace -f db/migrations/078_create_production_plan_tables.sql + +# ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ž…๋ ฅ +psql -h localhost -U postgres -d wace -f db/migrations/078b_insert_production_plan_sample_data.sql +``` + +### ๋ฐฉ๋ฒ• 2: DBeaver / pgAdmin์—์„œ ์‹คํ–‰ + +1. DB ์—ฐ๊ฒฐ ํ›„ SQL ์—๋””ํ„ฐ ์—ด๊ธฐ +2. `078_create_production_plan_tables.sql` ๋‚ด์šฉ ๋ณต์‚ฌ & ์‹คํ–‰ +3. `078b_insert_production_plan_sample_data.sql` ๋‚ด์šฉ ๋ณต์‚ฌ & ์‹คํ–‰ + +### ๋ฐฉ๋ฒ• 3: Docker ํ™˜๊ฒฝ + +```bash +# Docker ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์—์„œ ์‹คํ–‰ +docker exec -i psql -U postgres -d wace < db/migrations/078_create_production_plan_tables.sql +docker exec -i psql -U postgres -d wace < db/migrations/078b_insert_production_plan_sample_data.sql +``` + +## ์ƒ์„ฑ๋˜๋Š” ํ…Œ์ด๋ธ” + +| ํ…Œ์ด๋ธ”๋ช… | ์„ค๋ช… | +|---------|------| +| `equipment_info` | ์„ค๋น„ ์ •๋ณด ๋งˆ์Šคํ„ฐ | +| `production_plan_mng` | ์ƒ์‚ฐ๊ณ„ํš ๊ด€๋ฆฌ | +| `production_plan_order_rel` | ์ƒ์‚ฐ๊ณ„ํš-์ˆ˜์ฃผ ์—ฐ๊ฒฐ | + +## ์ƒ์„ฑ๋˜๋Š” ํ™”๋ฉด + +| ํ™”๋ฉด | ์„ค๋ช… | +|------|------| +| ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ (๋ฉ”์ธ) | ์ƒ์‚ฐ๊ณ„ํš ๋ชฉ๋ก ์กฐํšŒ/๋“ฑ๋ก/์ˆ˜์ •/์‚ญ์ œ | +| ์ƒ์‚ฐ๊ณ„ํš ๋“ฑ๋ก/์ˆ˜์ • (๋ชจ๋‹ฌ) | ์ƒ์‚ฐ๊ณ„ํš ์ƒ์„ธ ์ž…๋ ฅ ํผ | + +## ํ™•์ธ ์ฟผ๋ฆฌ + +```sql +-- ํ…Œ์ด๋ธ” ์ƒ์„ฑ ํ™•์ธ +SELECT table_name FROM information_schema.tables +WHERE table_schema = 'public' +AND table_name IN ('equipment_info', 'production_plan_mng', 'production_plan_order_rel'); + +-- ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ํ™•์ธ +SELECT * FROM equipment_info; +SELECT * FROM production_plan_mng; + +-- ํ™”๋ฉด ์ƒ์„ฑ ํ™•์ธ +SELECT id, screen_name, screen_code, table_name +FROM screen_definitions +WHERE screen_code LIKE '%PP%'; + +-- ๋ ˆ์ด์•„์›ƒ ํ™•์ธ +SELECT sl.id, sd.screen_name, sl.layout_name +FROM screen_layouts_v2 sl +JOIN screen_definitions sd ON sl.screen_id = sd.id +WHERE sd.screen_code LIKE '%PP%'; +``` + +## ๋ฉ”๋‰ด ์—ฐ๊ฒฐ (์ˆ˜๋™ ์ž‘์—… ํ•„์š”) + +ํ™”๋ฉด ์ƒ์„ฑ ํ›„, ๋ฉ”๋‰ด์— ์—ฐ๊ฒฐํ•˜๋ ค๋ฉด `menu_info` ํ…Œ์ด๋ธ”์—์„œ ํ•ด๋‹น ๋ฉ”๋‰ด์˜ `screen_id`๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์„ธ์š”: + +```sql +-- ์˜ˆ์‹œ: ์ƒ์‚ฐ๊ด€๋ฆฌ > ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ ๋ฉ”๋‰ด์— ์—ฐ๊ฒฐ +UPDATE menu_info +SET screen_id = (SELECT id FROM screen_definitions WHERE screen_code = 'TOPSEAL_PP_MAIN') +WHERE menu_name = '์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ' AND company_code = 'TOPSEAL'; +``` diff --git a/db/migrations/so_main_layout.json b/db/migrations/so_main_layout.json new file mode 100644 index 00000000..f00aac5a --- /dev/null +++ b/db/migrations/so_main_layout.json @@ -0,0 +1,179 @@ +{ + "version": "2.0", + "components": [ + { + "id": "comp_search", + "url": "@/lib/registry/components/v2-table-search-widget", + "size": { "width": 1920, "height": 80 }, + "position": { "x": 0, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-table-search-widget", + "label": "Search Filter", + "webTypeConfig": {} + }, + "displayOrder": 0 + }, + { + "id": "comp_table", + "url": "@/lib/registry/components/v2-table-list", + "size": { "width": 1920, "height": 800 }, + "position": { "x": 0, "y": 150, "z": 1 }, + "overrides": { + "type": "v2-table-list", + "label": "Sales Order List", + "filter": { "enabled": true, "filters": [] }, + "height": "auto", + "actions": { "actions": [], "bulkActions": false, "showActions": false }, + "columns": [ + { "align": "left", "order": 0, "format": "text", "visible": true, "sortable": true, "columnName": "order_no", "searchable": true, "displayName": "Order No" }, + { "align": "left", "order": 1, "format": "text", "visible": true, "sortable": true, "columnName": "partner_id", "searchable": true, "displayName": "Customer" }, + { "align": "left", "order": 2, "format": "text", "visible": true, "sortable": true, "columnName": "part_code", "searchable": true, "displayName": "Part Code" }, + { "align": "left", "order": 3, "format": "text", "visible": true, "sortable": true, "columnName": "part_name", "searchable": true, "displayName": "Part Name" }, + { "align": "left", "order": 4, "format": "text", "visible": true, "sortable": true, "columnName": "spec", "searchable": true, "displayName": "Spec" }, + { "align": "left", "order": 5, "format": "text", "visible": true, "sortable": true, "columnName": "material", "searchable": true, "displayName": "Material" }, + { "align": "right", "order": 6, "format": "number", "visible": true, "sortable": true, "columnName": "order_qty", "searchable": false, "displayName": "Order Qty" }, + { "align": "right", "order": 7, "format": "number", "visible": true, "sortable": true, "columnName": "ship_qty", "searchable": false, "displayName": "Ship Qty" }, + { "align": "right", "order": 8, "format": "number", "visible": true, "sortable": true, "columnName": "balance_qty", "searchable": false, "displayName": "Balance" }, + { "align": "right", "order": 9, "format": "number", "visible": true, "sortable": true, "columnName": "inventory_qty", "searchable": false, "displayName": "Stock" }, + { "align": "right", "order": 10, "format": "number", "visible": true, "sortable": true, "columnName": "plan_ship_qty", "searchable": false, "displayName": "Plan Ship Qty" }, + { "align": "right", "order": 11, "format": "number", "visible": true, "sortable": true, "columnName": "unit_price", "searchable": false, "displayName": "Unit Price" }, + { "align": "right", "order": 12, "format": "number", "visible": true, "sortable": true, "columnName": "total_amount", "searchable": false, "displayName": "Amount" }, + { "align": "left", "order": 13, "format": "text", "visible": true, "sortable": true, "columnName": "delivery_partner_id", "searchable": true, "displayName": "Delivery Partner" }, + { "align": "left", "order": 14, "format": "text", "visible": true, "sortable": true, "columnName": "delivery_address", "searchable": true, "displayName": "Delivery Address" }, + { "align": "center", "order": 15, "format": "text", "visible": true, "sortable": true, "columnName": "shipping_method", "searchable": true, "displayName": "Shipping Method" }, + { "align": "center", "order": 16, "format": "date", "visible": true, "sortable": true, "columnName": "due_date", "searchable": false, "displayName": "Due Date" }, + { "align": "center", "order": 17, "format": "date", "visible": true, "sortable": true, "columnName": "order_date", "searchable": false, "displayName": "Order Date" }, + { "align": "center", "order": 18, "format": "text", "visible": true, "sortable": true, "columnName": "status", "searchable": true, "displayName": "Status" }, + { "align": "left", "order": 19, "format": "text", "visible": true, "sortable": true, "columnName": "manager_name", "searchable": true, "displayName": "Manager" }, + { "align": "left", "order": 20, "format": "text", "visible": true, "sortable": true, "columnName": "memo", "searchable": true, "displayName": "Memo" } + ], + "autoLoad": true, + "checkbox": { "enabled": true, "multiple": true, "position": "left", "selectAll": true }, + "pagination": { "enabled": true, "pageSize": 20, "showPageInfo": true, "pageSizeOptions": [10, 20, 50, 100], "showSizeSelector": true }, + "showFooter": true, + "showHeader": true, + "tableStyle": { "theme": "default", "rowHeight": "normal", "borderStyle": "light", "headerStyle": "default", "hoverEffect": true, "alternateRows": true }, + "displayMode": "table", + "stickyHeader": false, + "selectedTable": "sales_order_mng", + "webTypeConfig": {}, + "horizontalScroll": { "enabled": true, "maxColumnWidth": 300, "minColumnWidth": 80, "maxVisibleColumns": 10 } + }, + "displayOrder": 1 + }, + { + "id": "comp_btn_upload", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 1610, "y": 30, "z": 1 }, + "overrides": { + "text": "Excel Upload", + "type": "v2-button-primary", + "label": "Excel Upload Button", + "action": { "type": "excel_upload" }, + "variant": "secondary", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 2 + }, + { + "id": "comp_btn_download", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 110, "height": 40 }, + "position": { "x": 1720, "y": 30, "z": 1 }, + "overrides": { + "text": "Excel Download", + "type": "v2-button-primary", + "label": "Excel Download Button", + "action": { "type": "excel_download" }, + "variant": "secondary", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 3 + }, + { + "id": "comp_btn_register", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 1500, "y": 100, "z": 1 }, + "overrides": { + "text": "New Order", + "type": "v2-button-primary", + "label": "New Order Button", + "action": { + "type": "modal", + "modalSize": "lg", + "modalTitle": "New Sales Order", + "targetScreenId": 3732, + "successMessage": "Saved successfully.", + "errorMessage": "Error saving." + }, + "variant": "success", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 4 + }, + { + "id": "comp_btn_edit", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1610, "y": 100, "z": 1 }, + "overrides": { + "text": "Edit", + "type": "v2-button-primary", + "label": "Edit Button", + "action": { + "type": "edit", + "modalSize": "lg", + "modalTitle": "Edit Sales Order", + "targetScreenId": 3732, + "successMessage": "Updated successfully.", + "errorMessage": "Error updating." + }, + "variant": "secondary", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 5 + }, + { + "id": "comp_btn_delete", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1700, "y": 100, "z": 1 }, + "overrides": { + "text": "Delete", + "type": "v2-button-primary", + "label": "Delete Button", + "action": { + "type": "delete", + "successMessage": "Deleted successfully.", + "errorMessage": "Error deleting." + }, + "variant": "danger", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 6 + }, + { + "id": "comp_btn_shipment", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 1790, "y": 100, "z": 1 }, + "overrides": { + "text": "Shipment Plan", + "type": "v2-button-primary", + "label": "Shipment Plan Button", + "action": { "type": "custom" }, + "variant": "secondary", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 7 + } + ] +} diff --git a/db/migrations/so_main_layout_kr.json b/db/migrations/so_main_layout_kr.json new file mode 100644 index 00000000..16361a23 --- /dev/null +++ b/db/migrations/so_main_layout_kr.json @@ -0,0 +1,179 @@ +{ + "version": "2.0", + "components": [ + { + "id": "comp_search", + "url": "@/lib/registry/components/v2-table-search-widget", + "size": { "width": 1920, "height": 80 }, + "position": { "x": 0, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-table-search-widget", + "label": "๊ฒ€์ƒ‰ ํ•„ํ„ฐ", + "webTypeConfig": {} + }, + "displayOrder": 0 + }, + { + "id": "comp_table", + "url": "@/lib/registry/components/v2-table-list", + "size": { "width": 1920, "height": 800 }, + "position": { "x": 0, "y": 150, "z": 1 }, + "overrides": { + "type": "v2-table-list", + "label": "์ˆ˜์ฃผ ๋ชฉ๋ก", + "filter": { "enabled": true, "filters": [] }, + "height": "auto", + "actions": { "actions": [], "bulkActions": false, "showActions": false }, + "columns": [ + { "align": "left", "order": 0, "format": "text", "visible": true, "sortable": true, "columnName": "order_no", "searchable": true, "displayName": "์ˆ˜์ฃผ๋ฒˆํ˜ธ" }, + { "align": "left", "order": 1, "format": "text", "visible": true, "sortable": true, "columnName": "partner_id", "searchable": true, "displayName": "๊ฑฐ๋ž˜์ฒ˜" }, + { "align": "left", "order": 2, "format": "text", "visible": true, "sortable": true, "columnName": "part_code", "searchable": true, "displayName": "ํ’ˆ๋ชฉ์ฝ”๋“œ" }, + { "align": "left", "order": 3, "format": "text", "visible": true, "sortable": true, "columnName": "part_name", "searchable": true, "displayName": "ํ’ˆ๋ช…" }, + { "align": "left", "order": 4, "format": "text", "visible": true, "sortable": true, "columnName": "spec", "searchable": true, "displayName": "๊ทœ๊ฒฉ" }, + { "align": "left", "order": 5, "format": "text", "visible": true, "sortable": true, "columnName": "material", "searchable": true, "displayName": "์žฌ์งˆ" }, + { "align": "right", "order": 6, "format": "number", "visible": true, "sortable": true, "columnName": "order_qty", "searchable": false, "displayName": "์ˆ˜์ฃผ์ˆ˜๋Ÿ‰" }, + { "align": "right", "order": 7, "format": "number", "visible": true, "sortable": true, "columnName": "ship_qty", "searchable": false, "displayName": "์ถœํ•˜์ˆ˜๋Ÿ‰" }, + { "align": "right", "order": 8, "format": "number", "visible": true, "sortable": true, "columnName": "balance_qty", "searchable": false, "displayName": "์ž”๋Ÿ‰" }, + { "align": "right", "order": 9, "format": "number", "visible": true, "sortable": true, "columnName": "inventory_qty", "searchable": false, "displayName": "ํ˜„์žฌ๊ณ " }, + { "align": "right", "order": 10, "format": "number", "visible": true, "sortable": true, "columnName": "plan_ship_qty", "searchable": false, "displayName": "์ถœํ•˜๊ณ„ํš๋Ÿ‰" }, + { "align": "right", "order": 11, "format": "number", "visible": true, "sortable": true, "columnName": "unit_price", "searchable": false, "displayName": "๋‹จ๊ฐ€" }, + { "align": "right", "order": 12, "format": "number", "visible": true, "sortable": true, "columnName": "total_amount", "searchable": false, "displayName": "๊ธˆ์•ก" }, + { "align": "left", "order": 13, "format": "text", "visible": true, "sortable": true, "columnName": "delivery_partner_id", "searchable": true, "displayName": "๋‚ฉํ’ˆ์ฒ˜" }, + { "align": "left", "order": 14, "format": "text", "visible": true, "sortable": true, "columnName": "delivery_address", "searchable": true, "displayName": "๋‚ฉํ’ˆ์žฅ์†Œ" }, + { "align": "center", "order": 15, "format": "text", "visible": true, "sortable": true, "columnName": "shipping_method", "searchable": true, "displayName": "๋ฐฐ์†ก๋ฐฉ๋ฒ•" }, + { "align": "center", "order": 16, "format": "date", "visible": true, "sortable": true, "columnName": "due_date", "searchable": false, "displayName": "๋‚ฉ๊ธฐ์ผ" }, + { "align": "center", "order": 17, "format": "date", "visible": true, "sortable": true, "columnName": "order_date", "searchable": false, "displayName": "์ˆ˜์ฃผ์ผ" }, + { "align": "center", "order": 18, "format": "text", "visible": true, "sortable": true, "columnName": "status", "searchable": true, "displayName": "์ƒํƒœ" }, + { "align": "left", "order": 19, "format": "text", "visible": true, "sortable": true, "columnName": "manager_name", "searchable": true, "displayName": "๋‹ด๋‹น์ž" }, + { "align": "left", "order": 20, "format": "text", "visible": true, "sortable": true, "columnName": "memo", "searchable": true, "displayName": "๋ฉ”๋ชจ" } + ], + "autoLoad": true, + "checkbox": { "enabled": true, "multiple": true, "position": "left", "selectAll": true }, + "pagination": { "enabled": true, "pageSize": 20, "showPageInfo": true, "pageSizeOptions": [10, 20, 50, 100], "showSizeSelector": true }, + "showFooter": true, + "showHeader": true, + "tableStyle": { "theme": "default", "rowHeight": "normal", "borderStyle": "light", "headerStyle": "default", "hoverEffect": true, "alternateRows": true }, + "displayMode": "table", + "stickyHeader": false, + "selectedTable": "sales_order_mng", + "webTypeConfig": {}, + "horizontalScroll": { "enabled": true, "maxColumnWidth": 300, "minColumnWidth": 80, "maxVisibleColumns": 10 } + }, + "displayOrder": 1 + }, + { + "id": "comp_btn_upload", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 1610, "y": 30, "z": 1 }, + "overrides": { + "text": "์—‘์…€ ์—…๋กœ๋“œ", + "type": "v2-button-primary", + "label": "์—‘์…€ ์—…๋กœ๋“œ ๋ฒ„ํŠผ", + "action": { "type": "excel_upload" }, + "variant": "secondary", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 2 + }, + { + "id": "comp_btn_download", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 110, "height": 40 }, + "position": { "x": 1720, "y": 30, "z": 1 }, + "overrides": { + "text": "์—‘์…€ ๋‹ค์šด๋กœ๋“œ", + "type": "v2-button-primary", + "label": "์—‘์…€ ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ", + "action": { "type": "excel_download" }, + "variant": "secondary", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 3 + }, + { + "id": "comp_btn_register", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 1500, "y": 100, "z": 1 }, + "overrides": { + "text": "์ˆ˜์ฃผ ๋“ฑ๋ก", + "type": "v2-button-primary", + "label": "์ˆ˜์ฃผ ๋“ฑ๋ก ๋ฒ„ํŠผ", + "action": { + "type": "modal", + "modalSize": "lg", + "modalTitle": "์ˆ˜์ฃผ ๋“ฑ๋ก", + "targetScreenId": 3732, + "successMessage": "์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + "errorMessage": "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." + }, + "variant": "success", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 4 + }, + { + "id": "comp_btn_edit", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1610, "y": 100, "z": 1 }, + "overrides": { + "text": "์ˆ˜์ •", + "type": "v2-button-primary", + "label": "์ˆ˜์ • ๋ฒ„ํŠผ", + "action": { + "type": "edit", + "modalSize": "lg", + "modalTitle": "์ˆ˜์ฃผ ์ˆ˜์ •", + "targetScreenId": 3732, + "successMessage": "์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + "errorMessage": "์ˆ˜์ • ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." + }, + "variant": "secondary", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 5 + }, + { + "id": "comp_btn_delete", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1700, "y": 100, "z": 1 }, + "overrides": { + "text": "์‚ญ์ œ", + "type": "v2-button-primary", + "label": "์‚ญ์ œ ๋ฒ„ํŠผ", + "action": { + "type": "delete", + "successMessage": "์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + "errorMessage": "์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." + }, + "variant": "danger", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 6 + }, + { + "id": "comp_btn_shipment", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 1790, "y": 100, "z": 1 }, + "overrides": { + "text": "์ถœํ•˜๊ณ„ํš", + "type": "v2-button-primary", + "label": "์ถœํ•˜๊ณ„ํš ๋ฒ„ํŠผ", + "action": { "type": "custom" }, + "variant": "secondary", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 7 + } + ] +} diff --git a/db/migrations/so_modal_layout.json b/db/migrations/so_modal_layout.json new file mode 100644 index 00000000..4245c052 --- /dev/null +++ b/db/migrations/so_modal_layout.json @@ -0,0 +1,254 @@ +{ + "version": "2.0", + "components": [ + { + "id": "comp_order_no", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "Order No", + "fieldName": "order_no", + "placeholder": "Enter order number", + "required": true + }, + "displayOrder": 0 + }, + { + "id": "comp_order_date", + "url": "@/lib/registry/components/v2-date", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-date", + "label": "Order Date", + "fieldName": "order_date", + "required": true + }, + "displayOrder": 1 + }, + { + "id": "comp_partner_id", + "url": "@/lib/registry/components/v2-select", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 100, "z": 1 }, + "overrides": { + "type": "v2-select", + "label": "Customer", + "fieldName": "partner_id", + "required": true, + "config": { + "mode": "dropdown", + "source": "table", + "sourceTable": "customer_mng", + "valueField": "id", + "labelField": "name" + } + }, + "displayOrder": 2 + }, + { + "id": "comp_part_code", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 100, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "Part Code", + "fieldName": "part_code", + "placeholder": "Enter part code", + "required": true + }, + "displayOrder": 3 + }, + { + "id": "comp_part_name", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 180, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "Part Name", + "fieldName": "part_name", + "placeholder": "Enter part name" + }, + "displayOrder": 4 + }, + { + "id": "comp_spec", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 180, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "Spec", + "fieldName": "spec", + "placeholder": "Enter spec" + }, + "displayOrder": 5 + }, + { + "id": "comp_material", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 260, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "Material", + "fieldName": "material", + "placeholder": "Enter material" + }, + "displayOrder": 6 + }, + { + "id": "comp_order_qty", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 260, "z": 1 }, + "overrides": { + "type": "v2-input", + "inputType": "number", + "label": "Order Qty", + "fieldName": "order_qty", + "placeholder": "Enter order quantity", + "required": true + }, + "displayOrder": 7 + }, + { + "id": "comp_unit_price", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 340, "z": 1 }, + "overrides": { + "type": "v2-input", + "inputType": "number", + "label": "Unit Price", + "fieldName": "unit_price", + "placeholder": "Enter unit price", + "required": true + }, + "displayOrder": 8 + }, + { + "id": "comp_due_date", + "url": "@/lib/registry/components/v2-date", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 340, "z": 1 }, + "overrides": { + "type": "v2-date", + "label": "Due Date", + "fieldName": "due_date" + }, + "displayOrder": 9 + }, + { + "id": "comp_status", + "url": "@/lib/registry/components/v2-select", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 420, "z": 1 }, + "overrides": { + "type": "v2-select", + "label": "Status", + "fieldName": "status", + "required": true, + "config": { + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "์ˆ˜์ฃผ", "label": "์ˆ˜์ฃผ" }, + { "value": "์ง„ํ–‰์ค‘", "label": "์ง„ํ–‰์ค‘" }, + { "value": "์™„๋ฃŒ", "label": "์™„๋ฃŒ" }, + { "value": "์ทจ์†Œ", "label": "์ทจ์†Œ" } + ] + } + }, + "displayOrder": 10 + }, + { + "id": "comp_shipping_method", + "url": "@/lib/registry/components/v2-select", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 420, "z": 1 }, + "overrides": { + "type": "v2-select", + "label": "Shipping Method", + "fieldName": "shipping_method", + "config": { + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "ํƒ๋ฐฐ", "label": "ํƒ๋ฐฐ" }, + { "value": "ํ™”๋ฌผ", "label": "ํ™”๋ฌผ" }, + { "value": "์ง์†ก", "label": "์ง์†ก" }, + { "value": "ํ€ต์„œ๋น„์Šค", "label": "ํ€ต์„œ๋น„์Šค" }, + { "value": "ํ•ด์ƒ์šด์†ก", "label": "ํ•ด์ƒ์šด์†ก" } + ] + } + }, + "displayOrder": 11 + }, + { + "id": "comp_delivery_address", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 620, "height": 60 }, + "position": { "x": 20, "y": 500, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "Delivery Address", + "fieldName": "delivery_address", + "placeholder": "Enter delivery address" + }, + "displayOrder": 12 + }, + { + "id": "comp_manager_name", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 580, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "Manager", + "fieldName": "manager_name", + "placeholder": "Enter manager name" + }, + "displayOrder": 13 + }, + { + "id": "comp_memo", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 620, "height": 80 }, + "position": { "x": 20, "y": 660, "z": 1 }, + "overrides": { + "type": "v2-input", + "inputType": "textarea", + "label": "Memo", + "fieldName": "memo", + "placeholder": "Enter memo" + }, + "displayOrder": 14 + }, + { + "id": "comp_btn_save", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 540, "y": 760, "z": 1 }, + "overrides": { + "text": "Save", + "type": "v2-button-primary", + "label": "Save Button", + "action": { + "type": "save", + "closeModalAfterSave": true, + "refreshParentTable": true, + "successMessage": "Saved successfully.", + "errorMessage": "Error saving." + }, + "variant": "primary", + "actionType": "button" + }, + "displayOrder": 15 + } + ] +} diff --git a/db/migrations/so_modal_layout_kr.json b/db/migrations/so_modal_layout_kr.json new file mode 100644 index 00000000..f07e3ae6 --- /dev/null +++ b/db/migrations/so_modal_layout_kr.json @@ -0,0 +1,254 @@ +{ + "version": "2.0", + "components": [ + { + "id": "comp_order_no", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "์ˆ˜์ฃผ๋ฒˆํ˜ธ", + "fieldName": "order_no", + "placeholder": "์ˆ˜์ฃผ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”", + "required": true + }, + "displayOrder": 0 + }, + { + "id": "comp_order_date", + "url": "@/lib/registry/components/v2-date", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-date", + "label": "์ˆ˜์ฃผ์ผ", + "fieldName": "order_date", + "required": true + }, + "displayOrder": 1 + }, + { + "id": "comp_partner_id", + "url": "@/lib/registry/components/v2-select", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 100, "z": 1 }, + "overrides": { + "type": "v2-select", + "label": "๊ฑฐ๋ž˜์ฒ˜", + "fieldName": "partner_id", + "required": true, + "config": { + "mode": "dropdown", + "source": "table", + "sourceTable": "customer_mng", + "valueField": "id", + "labelField": "name" + } + }, + "displayOrder": 2 + }, + { + "id": "comp_part_code", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 100, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "ํ’ˆ๋ชฉ์ฝ”๋“œ", + "fieldName": "part_code", + "placeholder": "ํ’ˆ๋ชฉ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”", + "required": true + }, + "displayOrder": 3 + }, + { + "id": "comp_part_name", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 180, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "ํ’ˆ๋ช…", + "fieldName": "part_name", + "placeholder": "ํ’ˆ๋ช…์„ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 4 + }, + { + "id": "comp_spec", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 180, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "๊ทœ๊ฒฉ", + "fieldName": "spec", + "placeholder": "๊ทœ๊ฒฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 5 + }, + { + "id": "comp_material", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 260, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "์žฌ์งˆ", + "fieldName": "material", + "placeholder": "์žฌ์งˆ์„ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 6 + }, + { + "id": "comp_order_qty", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 260, "z": 1 }, + "overrides": { + "type": "v2-input", + "inputType": "number", + "label": "์ˆ˜์ฃผ์ˆ˜๋Ÿ‰", + "fieldName": "order_qty", + "placeholder": "์ˆ˜์ฃผ์ˆ˜๋Ÿ‰์„ ์ž…๋ ฅํ•˜์„ธ์š”", + "required": true + }, + "displayOrder": 7 + }, + { + "id": "comp_unit_price", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 340, "z": 1 }, + "overrides": { + "type": "v2-input", + "inputType": "number", + "label": "๋‹จ๊ฐ€", + "fieldName": "unit_price", + "placeholder": "๋‹จ๊ฐ€๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”", + "required": true + }, + "displayOrder": 8 + }, + { + "id": "comp_due_date", + "url": "@/lib/registry/components/v2-date", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 340, "z": 1 }, + "overrides": { + "type": "v2-date", + "label": "๋‚ฉ๊ธฐ์ผ", + "fieldName": "due_date" + }, + "displayOrder": 9 + }, + { + "id": "comp_status", + "url": "@/lib/registry/components/v2-select", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 420, "z": 1 }, + "overrides": { + "type": "v2-select", + "label": "์ƒํƒœ", + "fieldName": "status", + "required": true, + "config": { + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "์ˆ˜์ฃผ", "label": "์ˆ˜์ฃผ" }, + { "value": "์ง„ํ–‰์ค‘", "label": "์ง„ํ–‰์ค‘" }, + { "value": "์™„๋ฃŒ", "label": "์™„๋ฃŒ" }, + { "value": "์ทจ์†Œ", "label": "์ทจ์†Œ" } + ] + } + }, + "displayOrder": 10 + }, + { + "id": "comp_shipping_method", + "url": "@/lib/registry/components/v2-select", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 420, "z": 1 }, + "overrides": { + "type": "v2-select", + "label": "๋ฐฐ์†ก๋ฐฉ๋ฒ•", + "fieldName": "shipping_method", + "config": { + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "ํƒ๋ฐฐ", "label": "ํƒ๋ฐฐ" }, + { "value": "ํ™”๋ฌผ", "label": "ํ™”๋ฌผ" }, + { "value": "์ง์†ก", "label": "์ง์†ก" }, + { "value": "ํ€ต์„œ๋น„์Šค", "label": "ํ€ต์„œ๋น„์Šค" }, + { "value": "ํ•ด์ƒ์šด์†ก", "label": "ํ•ด์ƒ์šด์†ก" } + ] + } + }, + "displayOrder": 11 + }, + { + "id": "comp_delivery_address", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 620, "height": 60 }, + "position": { "x": 20, "y": 500, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "๋‚ฉํ’ˆ์žฅ์†Œ", + "fieldName": "delivery_address", + "placeholder": "๋‚ฉํ’ˆ์žฅ์†Œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 12 + }, + { + "id": "comp_manager_name", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 580, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "๋‹ด๋‹น์ž", + "fieldName": "manager_name", + "placeholder": "๋‹ด๋‹น์ž๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 13 + }, + { + "id": "comp_memo", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 620, "height": 80 }, + "position": { "x": 20, "y": 660, "z": 1 }, + "overrides": { + "type": "v2-input", + "inputType": "textarea", + "label": "๋ฉ”๋ชจ", + "fieldName": "memo", + "placeholder": "๋ฉ”๋ชจ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 14 + }, + { + "id": "comp_btn_save", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 540, "y": 760, "z": 1 }, + "overrides": { + "text": "์ €์žฅ", + "type": "v2-button-primary", + "label": "์ €์žฅ ๋ฒ„ํŠผ", + "action": { + "type": "save", + "closeModalAfterSave": true, + "refreshParentTable": true, + "successMessage": "์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + "errorMessage": "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." + }, + "variant": "primary", + "actionType": "button" + }, + "displayOrder": 15 + } + ] +} diff --git a/docker-compose.frontend.win.yml b/docker-compose.frontend.win.yml index db9722d8..79589463 100644 --- a/docker-compose.frontend.win.yml +++ b/docker-compose.frontend.win.yml @@ -5,13 +5,20 @@ services: frontend: build: context: ./frontend - dockerfile: Dockerfile.dev + dockerfile: ../docker/dev/frontend.Dockerfile container_name: pms-frontend-win ports: - "9771:3000" environment: - NEXT_PUBLIC_API_URL=http://localhost:8080/api - WATCHPACK_POLLING=true + - NODE_OPTIONS=--max-old-space-size=4096 + deploy: + resources: + limits: + memory: 6G + reservations: + memory: 2G volumes: - ./frontend:/app - /app/node_modules diff --git a/docker/dev/docker-compose.frontend.mac.yml b/docker/dev/docker-compose.frontend.mac.yml index e02a1287..6428d481 100644 --- a/docker/dev/docker-compose.frontend.mac.yml +++ b/docker/dev/docker-compose.frontend.mac.yml @@ -9,6 +9,8 @@ services: - "9771:3000" environment: - NEXT_PUBLIC_API_URL=http://localhost:8080/api + - NODE_OPTIONS=--max-old-space-size=8192 + - NEXT_TELEMETRY_DISABLED=1 volumes: - ../../frontend:/app - /app/node_modules diff --git a/docs/DDD1542/FLOW_BASED_RESPONSIVE_DESIGN.md b/docs/DDD1542/FLOW_BASED_RESPONSIVE_DESIGN.md new file mode 100644 index 00000000..f885debb --- /dev/null +++ b/docs/DDD1542/FLOW_BASED_RESPONSIVE_DESIGN.md @@ -0,0 +1,729 @@ +# Flow ๊ธฐ๋ฐ˜ ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ ์„ค๊ณ„์„œ + +> ์ž‘์„ฑ์ผ: 2026-01-30 +> ๋ชฉํ‘œ: ์ง„์ •ํ•œ ๋ฐ˜์‘ํ˜• ๊ตฌํ˜„ (PC/ํƒœ๋ธ”๋ฆฟ/๋ชจ๋ฐ”์ผ ์ „์ฒด ๋Œ€์‘) + +--- + +## 1. ํ•ต์‹ฌ ๊ฒฐ๋ก  + +### 1.1 ํ˜„์žฌ ๋ฐฉ์‹ vs ๋ฐ˜์‘ํ˜• ํ‘œ์ค€ + +| ํ•ญ๋ชฉ | ํ˜„์žฌ ์‹œ์Šคํ…œ | ์›น ํ‘œ์ค€ (2025) | +|------|-------------|----------------| +| ๋ฐฐ์น˜ ๋ฐฉ์‹ | `position: absolute` | **Flexbox / CSS Grid** | +| ์ขŒํ‘œ | ํ”ฝ์…€ ๊ณ ์ • (x, y) | **Flow ๊ธฐ๋ฐ˜ (์ˆœ์„œ)** | +| ํ™”๋ฉด ์ถ•์†Œ ์‹œ | ๊ทธ๋Œ€๋กœ (์ž˜๋ฆผ) | **์ž๋™ ์žฌ๋ฐฐ์น˜** | +| ์šฉ๋„ | ํˆดํŒ, ์˜ค๋ฒ„๋ ˆ์ด | **์ „์ฒด ๋ ˆ์ด์•„์›ƒ** | + +> **๊ฒฐ๋ก **: `position: absolute`๋Š” ์ „์ฒด ๋ ˆ์ด์•„์›ƒ์— ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ ๋จ (์›น ํ‘œ์ค€) + +### 1.2 ๊ตฌํ˜„ ๋ฐฉํ–ฅ + +``` +์ ˆ๋Œ€ ์ขŒํ‘œ (x, y ํ”ฝ์…€) + โ†“ ๋ณ€ํ™˜ +Flow ๊ธฐ๋ฐ˜ ๋ฐฐ์น˜ (Flexbox + Grid) + โ†“ ๊ฒฐ๊ณผ +ํ™”๋ฉด ํฌ๊ธฐ์— ๋”ฐ๋ผ ์ž๋™ ์žฌ๋ฐฐ์น˜ +``` + +--- + +## 2. ์‹ค์ œ ํ™”๋ฉด ๋ฐ์ดํ„ฐ ๋ถ„์„ + +### 2.1 ๋ถ„์„ ๋Œ€์ƒ + +``` +์ด ๋ ˆ์ด์•„์›ƒ: 1,250๊ฐœ +์ด ์ปดํฌ๋„ŒํŠธ: 5,236๊ฐœ +๋ถ„์„ ์ƒ˜ํ”Œ: 6๊ฐœ ํ™”๋ฉด (23, 20, 18, 16, 18, 5๊ฐœ ์ปดํฌ๋„ŒํŠธ) +``` + +### 2.2 ํ™”๋ฉด 68 (์ˆ˜์ฃผ ๋ชฉ๋ก) - ๊ฐ€๋กœ ๋ฐฐ์น˜ ํŒจํ„ด + +``` +y=88: [๋ถ„๋ฆฌ] [์ €์žฅ] [์ˆ˜์ •] [์‚ญ์ œ] โ† ๊ฐ™์€ ํ–‰์— ๋ฒ„ํŠผ 4๊ฐœ + x=1277 x=1436 x=1594 x=1753 + +y=128: [โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ํ…Œ์ด๋ธ” โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€] + x=8, width=1904 +``` + +**๋ณ€ํ™˜ ํ›„**: +```html +
+ + + + +
+
+ + +``` + +**๋ฐ˜์‘ํ˜• ๋™์ž‘**: +``` +1920px: [๋ถ„๋ฆฌ] [์ €์žฅ] [์ˆ˜์ •] [์‚ญ์ œ] โ† ๊ฐ€๋กœ ๋ฐฐ์น˜ +1280px: [๋ถ„๋ฆฌ] [์ €์žฅ] [์ˆ˜์ •] [์‚ญ์ œ] โ† ๊ฐ€๋กœ ๋ฐฐ์น˜ (๊ณต๊ฐ„ ์ถฉ๋ถ„) + 768px: [๋ถ„๋ฆฌ] [์ €์žฅ] โ† ์ค„๋ฐ”๊ฟˆ ๋ฐœ์ƒ + [์ˆ˜์ •] [์‚ญ์ œ] + 375px: [๋ถ„๋ฆฌ] โ† ์„ธ๋กœ ๋ฐฐ์น˜ + [์ €์žฅ] + [์ˆ˜์ •] + [์‚ญ์ œ] +``` + +### 2.3 ํ™”๋ฉด 119 (์žฅ์น˜ ๊ด€๋ฆฌ) - 2์—ด ํผ ํŒจํ„ด + +``` +y=80: [์žฅ์น˜ ์ฝ”๋“œ ] [์‹œ๋ฆฌ์–ผ๋„˜๋ฒ„ ] + x=136, w=256 x=408, w=256 + +y=160: [์ œ์กฐ์‚ฌ ] + x=136, w=528 + +y=240: [ํ’ˆ๋ฒˆ ] [๋ชจ๋ธ๋ช… ] + x=136, w=256 x=408, w=256 + +y=320: [๊ตฌ๋งค์ผ ] [์ƒํƒœ ] +y=400: [๊ณต๊ธ‰์‚ฌ ] [๊ตฌ๋งค ๊ฐ€๊ฒฉ ] +y=480: [๊ณ„์•ฝ ๋ฒˆํ˜ธ ] [๊ณต๊ธ‰์‚ฌ ์ „ํ™” ] +... (2์—ด ๋ฐ˜๋ณต) + +y=840: [์ €์žฅ] + x=544 +``` + +**๋ณ€ํ™˜ ํ›„**: +```html +
+ + +
+
+ +
+
+ + + + +
+ + + +
+ + + +
+
+ + + +
+ +
+``` + +**๋ฐ˜์‘ํ˜• ๋™์ž‘**: +``` +1920px: [์ž…๋ ฅ๋ฐฉ์‹] [ํŒ๋งค์œ ํ˜•] [๋‹จ๊ฐ€๋ฐฉ์‹] [๋‹จ๊ฐ€์ˆ˜์ •] โ† 4์—ด +1280px: [์ž…๋ ฅ๋ฐฉ์‹] [ํŒ๋งค์œ ํ˜•] [๋‹จ๊ฐ€๋ฐฉ์‹] โ† 3์—ด + [๋‹จ๊ฐ€์ˆ˜์ •] + 768px: [์ž…๋ ฅ๋ฐฉ์‹] [ํŒ๋งค์œ ํ˜•] โ† 2์—ด + [๋‹จ๊ฐ€๋ฐฉ์‹] [๋‹จ๊ฐ€์ˆ˜์ •] + 375px: [์ž…๋ ฅ๋ฐฉ์‹] โ† 1์—ด + [ํŒ๋งค์œ ํ˜•] + [๋‹จ๊ฐ€๋ฐฉ์‹] + [๋‹จ๊ฐ€์ˆ˜์ •] +``` + +--- + +## 3. ๋ณ€ํ™˜ ๊ทœ์น™ + +### 3.1 Row ๊ทธ๋ฃนํ™” ์•Œ๊ณ ๋ฆฌ์ฆ˜ + +```typescript +const ROW_THRESHOLD = 40; // px + +function groupByRows(components: Component[]): Row[] { + // 1. y ์ขŒํ‘œ๋กœ ์ •๋ ฌ + const sorted = [...components].sort((a, b) => a.position.y - b.position.y); + + const rows: Row[] = []; + let currentRow: Component[] = []; + let currentY = -Infinity; + + for (const comp of sorted) { + if (comp.position.y - currentY > ROW_THRESHOLD) { + // ์ƒˆ๋กœ์šด Row ์‹œ์ž‘ + if (currentRow.length > 0) { + rows.push({ + y: currentY, + components: currentRow.sort((a, b) => a.position.x - b.position.x) + }); + } + currentRow = [comp]; + currentY = comp.position.y; + } else { + // ๊ฐ™์€ Row์— ์ถ”๊ฐ€ + currentRow.push(comp); + } + } + + // ๋งˆ์ง€๋ง‰ Row ์ถ”๊ฐ€ + if (currentRow.length > 0) { + rows.push({ + y: currentY, + components: currentRow.sort((a, b) => a.position.x - b.position.x) + }); + } + + return rows; +} +``` + +### 3.2 ํ™”๋ฉด 68 ์ ์šฉ ์˜ˆ์‹œ + +**์ž…๋ ฅ**: +```json +[ + { "id": "comp_1899", "position": { "x": 1277, "y": 88 }, "text": "๋ถ„๋ฆฌ" }, + { "id": "comp_1898", "position": { "x": 1436, "y": 88 }, "text": "์ €์žฅ" }, + { "id": "comp_1897", "position": { "x": 1594, "y": 88 }, "text": "์ˆ˜์ •" }, + { "id": "comp_1896", "position": { "x": 1753, "y": 88 }, "text": "์‚ญ์ œ" }, + { "id": "comp_1895", "position": { "x": 8, "y": 128 }, "type": "table" } +] +``` + +**๋ณ€ํ™˜ ๊ฒฐ๊ณผ**: +```json +{ + "rows": [ + { + "y": 88, + "justify": "end", + "components": ["comp_1899", "comp_1898", "comp_1897", "comp_1896"] + }, + { + "y": 128, + "justify": "start", + "components": ["comp_1895"] + } + ] +} +``` + +### 3.3 ์ •๋ ฌ ๋ฐฉํ–ฅ ๊ฒฐ์ • + +```typescript +function determineJustify(row: Row, screenWidth: number): string { + const firstX = row.components[0].position.x; + const lastComp = row.components[row.components.length - 1]; + const lastEnd = lastComp.position.x + lastComp.size.width; + + // ์™ผ์ชฝ ์—ฌ๋ฐฑ vs ์˜ค๋ฅธ์ชฝ ์—ฌ๋ฐฑ ๋น„๊ต + const leftMargin = firstX; + const rightMargin = screenWidth - lastEnd; + + if (leftMargin > rightMargin * 2) { + return "end"; // ์˜ค๋ฅธ์ชฝ ์ •๋ ฌ + } else if (rightMargin > leftMargin * 2) { + return "start"; // ์™ผ์ชฝ ์ •๋ ฌ + } else { + return "center"; // ์ค‘์•™ ์ •๋ ฌ + } +} + +// ํ™”๋ฉด 68 ๋ฒ„ํŠผ ๊ทธ๋ฃน: +// leftMargin = 1277, rightMargin = 1920 - 1912 = 8 +// โ†’ "end" (์˜ค๋ฅธ์ชฝ ์ •๋ ฌ) +``` + +--- + +## 4. ๋ Œ๋”๋ง ๊ตฌํ˜„ + +### 4.1 ์ƒˆ๋กœ์šด FlowLayout ์ปดํฌ๋„ŒํŠธ + +```tsx +// frontend/lib/registry/layouts/flow/FlowLayout.tsx + +interface FlowLayoutProps { + layout: LayoutData; + renderer: DynamicComponentRenderer; +} + +export function FlowLayout({ layout, renderer }: FlowLayoutProps) { + // 1. Row ๊ทธ๋ฃนํ™” + const rows = useMemo(() => { + return groupByRows(layout.components); + }, [layout.components]); + + return ( +
+ {rows.map((row, index) => ( + + ))} +
+ ); +} + +function FlowRow({ row, renderer }: { row: Row; renderer: any }) { + const justify = determineJustify(row, 1920); + + const justifyClass = { + start: "justify-start", + center: "justify-center", + end: "justify-end", + }[justify]; + + return ( +
+ {row.components.map((comp) => ( +
+ {renderer.renderChild(comp)} +
+ ))} +
+ ); +} +``` + +### 4.2 ๊ธฐ์กด ์ฝ”๋“œ ์ˆ˜์ • ์œ„์น˜ + +**ํ˜„์žฌ (RealtimePreviewDynamic.tsx ๋ผ์ธ 524-536)**: +```tsx +const baseStyle = { + left: `${adjustedPositionX}px`, // โŒ ์ ˆ๋Œ€ ์ขŒํ‘œ + top: `${position.y}px`, // โŒ ์ ˆ๋Œ€ ์ขŒํ‘œ + position: "absolute", // โŒ ์ ˆ๋Œ€ ์œ„์น˜ +}; +``` + +**๋ณ€๊ฒฝ ํ›„**: +```tsx +// FlowLayout ์‚ฌ์šฉ ์‹œ position ๊ด€๋ จ ์Šคํƒ€์ผ ์ œ๊ฑฐ +const baseStyle = isFlowMode ? { + // position, left, top ์—†์Œ + minWidth: size.width, + height: size.height, +} : { + left: `${adjustedPositionX}px`, + top: `${position.y}px`, + position: "absolute", +}; +``` + +--- + +## 5. ๊ฐ€์ƒ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + +### 5.1 ์‹œ๋‚˜๋ฆฌ์˜ค 1: ํ™”๋ฉด 68 (๋ฒ„ํŠผ 4๊ฐœ + ํ…Œ์ด๋ธ”) + +**๋ Œ๋”๋ง ๊ฒฐ๊ณผ (1920px)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋ถ„๋ฆฌ] [์ €์žฅ] [์ˆ˜์ •] [์‚ญ์ œ] โ”‚ +โ”‚ flex-wrap, justify-end โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ…Œ์ด๋ธ” (w-full) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ: ๋ฒ„ํŠผ ์˜ค๋ฅธ์ชฝ ์ •๋ ฌ, ํ…Œ์ด๋ธ” ์ „์ฒด ๋„ˆ๋น„ +``` + +**๋ Œ๋”๋ง ๊ฒฐ๊ณผ (1280px)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋ถ„๋ฆฌ] [์ €์žฅ] [์ˆ˜์ •] [์‚ญ์ œ] โ”‚ +โ”‚ flex-wrap, justify-end โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ…Œ์ด๋ธ” (w-full) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ: ๋ฒ„ํŠผ ํฌ๊ธฐ ์œ ์ง€, ํ…Œ์ด๋ธ” ๋„ˆ๋น„ ์กฐ์ • +``` + +**๋ Œ๋”๋ง ๊ฒฐ๊ณผ (768px)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋ถ„๋ฆฌ] [์ €์žฅ] โ”‚ +โ”‚ [์ˆ˜์ •] [์‚ญ์ œ] โ”‚ โ† ์ž๋™ ์ค„๋ฐ”๊ฟˆ! +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ…Œ์ด๋ธ” (w-full) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ: ๋ฒ„ํŠผ ์ค„๋ฐ”๊ฟˆ, ํ…Œ์ด๋ธ” ๋„ˆ๋น„ ์กฐ์ • +``` + +**๋ Œ๋”๋ง ๊ฒฐ๊ณผ (375px)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋ถ„๋ฆฌ] โ”‚ +โ”‚ [์ €์žฅ] โ”‚ +โ”‚ [์ˆ˜์ •] โ”‚ +โ”‚ [์‚ญ์ œ] โ”‚ โ† ์„ธ๋กœ ๋ฐฐ์น˜ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ…Œ์ด๋ธ” โ”‚ โ”‚ (๊ฐ€๋กœ ์Šคํฌ๋กค) +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ: ๋ฒ„ํŠผ ์„ธ๋กœ ๋ฐฐ์น˜, ํ…Œ์ด๋ธ” ๊ฐ€๋กœ ์Šคํฌ๋กค +``` + +### 5.2 ์‹œ๋‚˜๋ฆฌ์˜ค 2: ํ™”๋ฉด 119 (2์—ด ํผ) + +**๋ Œ๋”๋ง ๊ฒฐ๊ณผ (1920px)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [์žฅ์น˜ ์ฝ”๋“œ ] [์‹œ๋ฆฌ์–ผ๋„˜๋ฒ„ ] โ”‚ +โ”‚ grid-cols-2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [์ œ์กฐ์‚ฌ ] โ”‚ +โ”‚ col-span-2 (์ „์ฒด ๋„ˆ๋น„) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [ํ’ˆ๋ฒˆ ] [๋ชจ๋ธ๋ช…โ–ผ ] โ”‚ +โ”‚ ... โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ: 2์—ด ๊ทธ๋ฆฌ๋“œ +``` + +**๋ Œ๋”๋ง ๊ฒฐ๊ณผ (768px)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [์žฅ์น˜ ์ฝ”๋“œ ] โ”‚ +โ”‚ [์‹œ๋ฆฌ์–ผ๋„˜๋ฒ„ ] โ”‚ โ† 1์—ด๋กœ ๋ณ€๊ฒฝ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [์ œ์กฐ์‚ฌ ] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [ํ’ˆ๋ฒˆ ] โ”‚ +โ”‚ [๋ชจ๋ธ๋ช…โ–ผ ] โ”‚ +โ”‚ ... โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ: 1์—ด ๊ทธ๋ฆฌ๋“œ +``` + +### 5.3 ์‹œ๋‚˜๋ฆฌ์˜ค 3: ๋ถ„ํ•  ํŒจ๋„ + +**ํ˜„์žฌ SplitPanelLayout ๋™์ž‘**: +``` +์ขŒ์ธก 60% | ์šฐ์ธก 40% โ† ์ด๋ฏธ ํผ์„ผํŠธ ๊ธฐ๋ฐ˜ +``` + +**๋ณ€๊ฒฝ ํ›„ (768px ์ดํ•˜)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ์ขŒ์ธก 100% โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ์šฐ์ธก 100% โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โ† ์„ธ๋กœ ๋ฐฐ์น˜๋กœ ์ „ํ™˜ +``` + +**๊ตฌํ˜„**: +```tsx +// SplitPanelLayoutComponent.tsx +const isMobile = useMediaQuery("(max-width: 768px)"); + +return ( +
+
+ {/* ์ขŒ์ธก ํŒจ๋„ */} +
+
+ {/* ์šฐ์ธก ํŒจ๋„ */} +
+
+); +``` + +--- + +## 6. ์—ฃ์ง€ ์ผ€์ด์Šค ๊ฒ€์ฆ + +### 6.1 ๊ฒน์น˜๋Š” ์ปดํฌ๋„ŒํŠธ + +**ํ˜„์žฌ ๋ฐ์ดํ„ฐ (ํ™”๋ฉด 74)**: +```json +{ "id": "comp_2606", "position": { "x": 161, "y": 400 } }, // ๋ถ„ํ•  ํŒจ๋„ +{ "id": "comp_fkk75q08", "position": { "x": 161, "y": 400 } } // ๋ผ๋””์˜ค ๋ฒ„ํŠผ +``` + +**๋ฌธ์ œ**: ๊ฐ™์€ ์œ„์น˜์— ๋‘ ์ปดํฌ๋„ŒํŠธ โ†’ z-index๋กœ ๊ฒน์ณ์„œ ํ‘œ์‹œ + +**ํ•ด๊ฒฐ**: +- z-index๊ฐ€ ๋†’์€ ์ปดํฌ๋„ŒํŠธ ์šฐ์„  +- ๋˜๋Š” parent-child ๊ด€๊ณ„๋ฉด ์ค‘์ฒฉ ์ฒ˜๋ฆฌ + +```typescript +function resolveOverlaps(row: Row): Row { + // z-index๋กœ ์ •๋ ฌํ•˜์—ฌ ๋†’์€ ๊ฒƒ๋งŒ ํ‘œ์‹œ + // ๋˜๋Š” parentId ํ™•์ธํ•˜์—ฌ ์ค‘์ฒฉ ์ฒ˜๋ฆฌ +} +``` + +### 6.2 ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ + +**ํ˜„์žฌ ๋ฐ์ดํ„ฐ (ํ™”๋ฉด 4103)**: +```json +{ + "id": "section-customer-info", + "conditionalConfig": { + "field": "input_method", + "value": "customer_first", + "action": "show" + } +} +``` + +**๋™์ž‘**: ์กฐ๊ฑด์— ๋”ฐ๋ผ show/hide +**Flow ๋ ˆ์ด์•„์›ƒ์—์„œ**: ์ˆจ๊ฒจ์ง€๋ฉด ๊ณต๊ฐ„๋„ ์‚ฌ๋ผ์ง (flex ์ž๋™ ์กฐ์ •) + +โœ… ๋ฌธ์ œ์—†์Œ + +### 6.3 ํ…Œ์ด๋ธ” + ๋ฒ„ํŠผ ์กฐํ•ฉ + +**ํŒจํ„ด**: +``` +[๋ฒ„ํŠผ ๊ทธ๋ฃน] โ† flex-wrap, justify-end +[ํ…Œ์ด๋ธ”] โ† w-full +``` + +**ํ…Œ์ด๋ธ” ๊ฐ€๋กœ ์Šคํฌ๋กค**: +- ํ…Œ์ด๋ธ” ๋‚ด๋ถ€๋Š” ๊ฐ€๋กœ ์Šคํฌ๋กค ์ง€์› +- ์™ธ๋ถ€ ์ปจํ…Œ์ด๋„ˆ๋Š” w-full + +โœ… ๋ฌธ์ œ์—†์Œ + +### 6.4 ์„น์…˜ ์นด๋“œ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ + +**ํ˜„์žฌ**: ์„น์…˜ ์นด๋“œ์™€ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ณ„๋„๋กœ ์ €์žฅ๋จ + +**๋ณ€ํ™˜ ์‹œ**: +1. ์„น์…˜ ์นด๋“œ์˜ y ๋ฒ”์œ„ ํŒŒ์•… +2. ํ•ด๋‹น y ๋ฒ”์œ„ ๋‚ด ์ปดํฌ๋„ŒํŠธ๋“ค์„ ์„น์…˜ ์ž์‹์œผ๋กœ ๊ทธ๋ฃนํ™” +3. ์„น์…˜ ๋‚ด๋ถ€์—์„œ ๋‹ค์‹œ Row ๊ทธ๋ฃนํ™” + +```typescript +function groupWithinSection( + section: Component, + allComponents: Component[] +): Component[] { + const sectionTop = section.position.y; + const sectionBottom = section.position.y + section.size.height; + + return allComponents.filter(comp => { + return comp.id !== section.id && + comp.position.y >= sectionTop && + comp.position.y < sectionBottom; + }); +} +``` + +--- + +## 7. ํ˜ธํ™˜์„ฑ ๊ฒ€์ฆ + +### 7.1 ๊ธฐ์กด ๊ธฐ๋Šฅ ํ˜ธํ™˜ + +| ๊ธฐ๋Šฅ | ํ˜ธํ™˜ ์—ฌ๋ถ€ | ์„ค๋ช… | +|------|----------|------| +| ๋””์ž์ธ ๋ชจ๋“œ | โš ๏ธ ์ˆ˜์ • ํ•„์š” | ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ๋กœ์ง ์ˆ˜์ • | +| ๋ฏธ๋ฆฌ๋ณด๊ธฐ | โœ… ํ˜ธํ™˜ | Flow ๋ ˆ์ด์•„์›ƒ์œผ๋กœ ๋ Œ๋”๋ง | +| ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ | โœ… ํ˜ธํ™˜ | flex๋กœ ์ž๋™ ์กฐ์ • | +| ๋ถ„ํ•  ํŒจ๋„ | โš ๏ธ ์ˆ˜์ • ํ•„์š” | ๋ฐ˜์‘ํ˜• ์ „ํ™˜ ๋กœ์ง ์ถ”๊ฐ€ | +| ํ…Œ์ด๋ธ” | โœ… ํ˜ธํ™˜ | w-full ์ ์šฉ | +| ๋ชจ๋‹ฌ | โœ… ํ˜ธํ™˜ | ๋ชจ๋‹ฌ ๋‚ด๋ถ€๋„ Flow ์ ์šฉ | + +### 7.2 ๋””์ž์ธ ๋ชจ๋“œ ์ˆ˜์ • + +**ํ˜„์žฌ**: ๋“œ๋ž˜๊ทธํ•˜๋ฉด x, y ํ”ฝ์…€ ์ €์žฅ +**๋ณ€๊ฒฝ ํ›„**: ๋“œ๋ž˜๊ทธํ•˜๋ฉด x, y ํ”ฝ์…€ ์ €์žฅ (๋™์ผ) โ†’ ๋ Œ๋”๋ง ์‹œ ๋ณ€ํ™˜ + +``` +์ €์žฅ: ํ”ฝ์…€ ์ขŒํ‘œ (๊ธฐ์กด ์œ ์ง€) +๋ Œ๋”๋ง: Flow ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€ํ™˜ +``` + +**์žฅ์ **: DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ถˆํ•„์š” + +--- + +## 8. ๊ตฌํ˜„ ๊ณ„ํš + +### Phase 1: ํ•ต์‹ฌ ๋ณ€ํ™˜ ๋กœ์ง (1์ผ) + +1. `groupByRows()` ํ•จ์ˆ˜ ๊ตฌํ˜„ +2. `determineJustify()` ํ•จ์ˆ˜ ๊ตฌํ˜„ +3. `FlowLayout` ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ + +### Phase 2: ๋ Œ๋”๋ง ์ ์šฉ (1์ผ) + +1. `DynamicComponentRenderer`์— Flow ๋ชจ๋“œ ์ถ”๊ฐ€ +2. `RealtimePreviewDynamic` ์ˆ˜์ • +3. ๊ธฐ์กด absolute ์Šคํƒ€์ผ ์กฐ๊ฑด๋ถ€ ์ ์šฉ + +### Phase 3: ํŠน์ˆ˜ ์ผ€์ด์Šค ์ฒ˜๋ฆฌ (1์ผ) + +1. ์„น์…˜ ์นด๋“œ ๋‚ด๋ถ€ ๊ทธ๋ฃนํ™” +2. ๊ฒน์น˜๋Š” ์ปดํฌ๋„ŒํŠธ ์ฒ˜๋ฆฌ +3. ๋ถ„ํ•  ํŒจ๋„ ๋ฐ˜์‘ํ˜• ์ „ํ™˜ + +### Phase 4: ํ…Œ์ŠคํŠธ (1์ผ) + +1. ํ™”๋ฉด 68 (๋ฒ„ํŠผ + ํ…Œ์ด๋ธ”) ํ…Œ์ŠคํŠธ +2. ํ™”๋ฉด 119 (2์—ด ํผ) ํ…Œ์ŠคํŠธ +3. ํ™”๋ฉด 4103 (๋ณต์žกํ•œ ํผ) ํ…Œ์ŠคํŠธ +4. PC 1920px โ†’ 1280px ํ…Œ์ŠคํŠธ +5. ํƒœ๋ธ”๋ฆฟ 768px ํ…Œ์ŠคํŠธ +6. ๋ชจ๋ฐ”์ผ 375px ํ…Œ์ŠคํŠธ + +--- + +## 9. ์˜ˆ์ƒ ์ด์Šˆ + +### 9.1 ๋””์ž์ด๋„ˆ ์˜๋„ ์†์‹ค + +**๋ฌธ์ œ**: ๋””์ž์ด๋„ˆ๊ฐ€ ์˜๋„์ ์œผ๋กœ ๋ฐฐ์น˜ํ•œ ์œ„์น˜๊ฐ€ ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ์Œ + +**ํ•ด๊ฒฐ**: +- ๊ธฐ๋ณธ Flow ๋ ˆ์ด์•„์›ƒ ์ ์šฉ +- ํ•„์š”์‹œ `flexOrder` ์†์„ฑ์œผ๋กœ ์ˆœ์„œ ์กฐ์ • ๊ฐ€๋Šฅ +- ๋˜๋Š” `fixedPosition: true` ์˜ต์…˜์œผ๋กœ ์ ˆ๋Œ€ ์ขŒํ‘œ ์œ ์ง€ + +### 9.2 ๋ณต์žกํ•œ ๋ ˆ์ด์•„์›ƒ + +**๋ฌธ์ œ**: ์ผ๋ถ€ ํ™”๋ฉด์€ ์ž์œ  ๋ฐฐ์น˜๊ฐ€ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Œ + +**ํ•ด๊ฒฐ**: +- ํ™”๋ฉด๋ณ„ `layoutMode` ์„ค์ • + - `"flow"`: Flow ๊ธฐ๋ฐ˜ (๊ธฐ๋ณธ๊ฐ’) + - `"absolute"`: ๊ธฐ์กด ์ ˆ๋Œ€ ์ขŒํ‘œ + +### 9.3 ์„ฑ๋Šฅ + +**๋ฌธ์ œ**: ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค Row ๊ทธ๋ฃนํ™” ๊ณ„์‚ฐ + +**ํ•ด๊ฒฐ**: +- `useMemo`๋กœ ์บ์‹ฑ +- ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก ๋ณ€๊ฒฝ ์‹œ์—๋งŒ ์žฌ๊ณ„์‚ฐ + +--- + +## 10. ์ตœ์ข… ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### ๊ตฌํ˜„ ์ „ + +- [ ] ํ˜„์žฌ ๋™์ž‘ํ•˜๋Š” ํ™”๋ฉด ์Šคํฌ๋ฆฐ์ƒท (๋น„๊ต์šฉ) +- [ ] ํ…Œ์ŠคํŠธ ํ™”๋ฉด ๋ชฉ๋ก ํ™•์ • (68, 119, 4103) + +### ๊ตฌํ˜„ ์ค‘ + +- [ ] `groupByRows()` ๊ตฌํ˜„ +- [ ] `determineJustify()` ๊ตฌํ˜„ +- [ ] `FlowLayout` ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ +- [ ] `DynamicComponentRenderer` ์ˆ˜์ • +- [ ] `RealtimePreviewDynamic` ์ˆ˜์ • + +### ํ…Œ์ŠคํŠธ + +- [ ] 1920px ํ…Œ์ŠคํŠธ +- [ ] 1280px ํ…Œ์ŠคํŠธ +- [ ] 768px ํ…Œ์ŠคํŠธ +- [ ] 375px ํ…Œ์ŠคํŠธ +- [ ] ๋””์ž์ธ ๋ชจ๋“œ ํ…Œ์ŠคํŠธ +- [ ] ๋ถ„ํ•  ํŒจ๋„ ํ…Œ์ŠคํŠธ +- [ ] ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ํ…Œ์ŠคํŠธ + +--- + +## 11. ๊ฒฐ๋ก  + +### 11.1 ๊ตฌํ˜„ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ + +**โœ… ๊ฐ€๋Šฅ** + +- ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์œ ์ง€ (DB ๋ณ€๊ฒฝ ์—†์Œ) +- ๋ Œ๋”๋ง ๋ ˆ๋ฒจ์—์„œ๋งŒ ๋ณ€ํ™˜ +- ๋ชจ๋“  ํ™”๋ฉด ํŒจํ„ด ๋ถ„์„ ์™„๋ฃŒ +- ์—ฃ์ง€ ์ผ€์ด์Šค ํ•ด๊ฒฐ์ฑ… ํ™•๋ณด + +### 11.2 ํ•ต์‹ฌ ๋ณ€๊ฒฝ ์‚ฌํ•ญ + +``` +Before: position: absolute + left/top ํ”ฝ์…€ +After: Flexbox + flex-wrap + justify-* +``` + +### 11.3 ์˜ˆ์ƒ ํšจ๊ณผ + +| ํ™”๋ฉด ํฌ๊ธฐ | Before | After | +|-----------|--------|-------| +| 1920px | ์ •์ƒ | ์ •์ƒ | +| 1280px | ๋ฒ„ํŠผ ์ž˜๋ฆผ | **์ž๋™ ์กฐ์ •** | +| 768px | ๋ ˆ์ด์•„์›ƒ ๊นจ์ง | **์ž๋™ ์žฌ๋ฐฐ์น˜** | +| 375px | ์‚ฌ์šฉ ๋ถˆ๊ฐ€ | **์ž๋™ ์„ธ๋กœ ๋ฐฐ์น˜** | diff --git a/docs/DDD1542/MULTI_SELECT_ARRAY_SERIALIZATION_FIX.md b/docs/DDD1542/MULTI_SELECT_ARRAY_SERIALIZATION_FIX.md new file mode 100644 index 00000000..3d5ba77c --- /dev/null +++ b/docs/DDD1542/MULTI_SELECT_ARRAY_SERIALIZATION_FIX.md @@ -0,0 +1,253 @@ +# ๋‹ค์ค‘ ์„ ํƒ(Multi-Select) ๋ฐฐ์—ด ์ง๋ ฌํ™” ๋ฌธ์ œ ํ•ด๊ฒฐ ๋ณด๊ณ ์„œ + +## ๋ฌธ์ œ ์š”์•ฝ + +**์ฆ์ƒ**: ๋‹ค์ค‘ ์„ ํƒ ์ปดํฌ๋„ŒํŠธ(TagboxSelect, ์ฒดํฌ๋ฐ•์Šค ๋“ฑ)๋กœ ์„ ํƒํ•œ ๊ฐ’์ด DB์— ์ €์žฅ๋  ๋•Œ ์†์ƒ๋˜๊ฑฐ๋‚˜ `null`๋กœ ์ €์žฅ๋จ + +**์˜ํ–ฅ๋ฐ›๋Š” ๊ธฐ๋Šฅ**: +- ํ’ˆ๋ชฉ์ •๋ณด์˜ `division` (๊ตฌ๋ถ„) ํ•„๋“œ +- ๋ชจ๋“  ๋‹ค์ค‘ ์„ ํƒ ์นดํ…Œ๊ณ ๋ฆฌ ํ•„๋“œ + +**์†์ƒ๋œ ๋ฐ์ดํ„ฐ ์˜ˆ์‹œ**: +``` +{"{\"{\\\"CAT_ML7SR2T9_IM7H\\\",\\\"CAT_ML8ZFQFU_EE5Z\\\"}\"}",...} +``` + +**์ •์ƒ ๋ฐ์ดํ„ฐ ์˜ˆ์‹œ**: +``` +CAT_ML7SR2T9_IM7H,CAT_ML8ZFQFU_EE5Z,CAT_ML8ZFVEL_1TOR +``` + +--- + +## ๋ฌธ์ œ ์›์ธ ๋ถ„์„ + +### 1. PostgreSQL์˜ ๋ฐฐ์—ด ์ž๋™ ๋ณ€ํ™˜ + +Node.js์˜ `node-pg` ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” JavaScript ๋ฐฐ์—ด์„ PostgreSQL ๋ฐฐ์—ด ๋ฆฌํ„ฐ๋Ÿด(`{...}`)๋กœ ์ž๋™ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + +```javascript +// JavaScript +["CAT_1", "CAT_2", "CAT_3"] + +// PostgreSQL๋กœ ์ž๋™ ๋ณ€ํ™˜๋จ +{"CAT_1","CAT_2","CAT_3"} +``` + +ํ•˜์ง€๋งŒ ์šฐ๋ฆฌ ์‹œ์Šคํ…œ์€ ์ปค์Šคํ…€ ํ…Œ์ด๋ธ”์—์„œ **์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด**์„ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค: +``` +CAT_1,CAT_2,CAT_3 +``` + +### 2. ์—ฌ๋Ÿฌ ์ €์žฅ ๊ฒฝ๋กœ์˜ ์กด์žฌ + +์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•œ ๊ฒฐ๊ณผ, ์ €์žฅ ๋กœ์ง์ด ์—ฌ๋Ÿฌ ๊ฒฝ๋กœ๋กœ ๋‚˜๋‰˜์–ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค: + +| ๊ฒฝ๋กœ | ํŒŒ์ผ | ์„ค๋ช… | +|------|------|------| +| 1 | `buttonActions.ts` | ๊ธฐ๋ณธ ์ €์žฅ ๋กœ์ง (INSERT/UPDATE) | +| 2 | `EditModal.tsx` | ๋ชจ๋‹ฌ ๋‚ด ์ง์ ‘ ์ €์žฅ (CREATE/UPDATE) | +| 3 | `nodeFlowExecutionService.ts` | ๋ฐฑ์—”๋“œ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์ €์žฅ | + +### 3. ์™œ ์ดˆ๊ธฐ ์ˆ˜์ •์ด ์‹คํŒจํ–ˆ๋Š”๊ฐ€? + +#### ์‹œ๋„ 1: `buttonActions.ts`์— ๋ฐฐ์—ด ๋ณ€ํ™˜ ์ถ”๊ฐ€ +```typescript +// buttonActions.ts (๋ผ์ธ 1002-1025) +if (isUpdate) { + for (const key of Object.keys(formData)) { + if (Array.isArray(value)) { + formData[key] = value.join(","); + } + } +} +``` + +**์‹คํŒจ ์ด์œ **: `EditModal`์ด `onSave` ์ฝœ๋ฐฑ์„ ์ œ๊ณตํ•˜๋ฉด, `buttonActions.ts`๋Š” ์ด ์ฝœ๋ฐฑ์„ ๋ฐ”๋กœ ํ˜ธ์ถœํ•˜๊ณ  ๋‚ด๋ถ€ ์ €์žฅ ๋กœ์ง์„ ๊ฑด๋„ˆ๋œ€ + +```typescript +// buttonActions.ts (๋ผ์ธ 545-552) +if (onSave) { + await onSave(); // ๋ฐ”๋กœ ์—ฌ๊ธฐ์„œ EditModal.handleSave()๊ฐ€ ํ˜ธ์ถœ๋จ + return true; // ์•„๋ž˜ ๋ฐฐ์—ด ๋ณ€ํ™˜ ๋กœ์ง์— ๋„๋‹ฌํ•˜์ง€ ์•Š์Œ! +} +``` + +#### ์‹œ๋„ 2: `nodeFlowExecutionService.ts`์— `normalizeValueForDB` ์ถ”๊ฐ€ + +**๋ถ€๋ถ„ ์„ฑ๊ณต**: INSERT์—์„œ๋Š” ๋™์ž‘ํ–ˆ์œผ๋‚˜, EditModal์˜ UPDATE ๊ฒฝ๋กœ๋Š” ์—ฌ์ „ํžˆ ๋ฌธ์ œ + +--- + +## ์ตœ์ข… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• + +### ํ•ต์‹ฌ ์ˆ˜์ •: `EditModal.tsx`์— ์ง์ ‘ ๋ฐฐ์—ด ๋ณ€ํ™˜ ์ถ”๊ฐ€ + +EditModal์ด ์ง์ ‘ `dynamicFormApi.updateFormDataPartial`์„ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ, **์ €์žฅ ์ง์ „**์— ๋ฐฐ์—ด์„ ๋ณ€ํ™˜ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. + +#### ์ˆ˜์ • ์œ„์น˜ 1: UPDATE ๊ฒฝ๋กœ (๋ผ์ธ 957-1002) + +```typescript +// EditModal.tsx - UPDATE ๋ชจ๋“œ +Object.keys(formData).forEach((key) => { + if (formData[key] !== originalData[key]) { + let value = formData[key]; + + if (Array.isArray(value)) { + // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ + const isRepeaterData = value.length > 0 && + typeof value[0] === "object" && + ("_targetTable" in value[0] || "_isNewItem" in value[0]); + + if (!isRepeaterData) { + // ๐Ÿ”ง ์†์ƒ๋œ ๊ฐ’ ํ•„ํ„ฐ๋ง + const isValidValue = (v: any): boolean => { + if (typeof v === "number") return true; + if (typeof v !== "string") return false; + if (v.includes("{") || v.includes("}") || v.includes('"') || v.includes("\\")) + return false; + return true; + }; + + // ์œ ํšจํ•œ ๊ฐ’๋งŒ ์‰ผํ‘œ๋กœ ์—ฐ๊ฒฐ + const validValues = value.filter(isValidValue); + value = validValues.join(","); + } + } + + changedData[key] = value; + } +}); +``` + +#### ์ˆ˜์ • ์œ„์น˜ 2: CREATE ๊ฒฝ๋กœ (๋ผ์ธ 855-875) + +```typescript +// EditModal.tsx - CREATE ๋ชจ๋“œ +Object.entries(dataToSave).forEach(([key, value]) => { + if (!Array.isArray(value)) { + masterDataToSave[key] = value; + } else { + const isRepeaterData = /* ๋ฆฌํ”ผํ„ฐ ์ฒดํฌ */; + + if (isRepeaterData) { + // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ๋Š” ์ œ์™ธ (๋ณ„๋„ ์ €์žฅ) + } else { + // ๋‹ค์ค‘ ์„ ํƒ ๋ฐฐ์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด + const validValues = value.filter(isValidValue); + masterDataToSave[key] = validValues.join(","); + } + } +}); +``` + +#### ์ˆ˜์ • ์œ„์น˜ 3: ๊ทธ๋ฃน UPDATE ๊ฒฝ๋กœ (๋ผ์ธ 630-650) + +๊ทธ๋ฃน ํ’ˆ๋ชฉ ์ˆ˜์ • ์‹œ์—๋„ ๋™์ผํ•œ ๋กœ์ง ์ ์šฉ + +--- + +## ์†์ƒ๋œ ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง + +๊ธฐ์กด์— ์†์ƒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐฐ์—ด์— ํฌํ•จ๋  ์ˆ˜ ์žˆ์–ด์„œ, ๋ณ€ํ™˜ ์ „ ํ•„ํ„ฐ๋ง์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค: + +```typescript +const isValidValue = (v: any): boolean => { + // ์ˆซ์ž๋Š” ์œ ํšจ + if (typeof v === "number" && !isNaN(v)) return true; + // ๋ฌธ์ž์—ด์ด ์•„๋‹ˆ๋ฉด ๋ฌดํšจ + if (typeof v !== "string") return false; + // ๋นˆ ๊ฐ’ ๋ฌดํšจ + if (!v || v.trim() === "") return false; + // PostgreSQL ๋ฐฐ์—ด ํ˜•์‹ ๊ฐ์ง€ โ†’ ๋ฌดํšจ + if (v.includes("{") || v.includes("}") || v.includes('"') || v.includes("\\")) + return false; + return true; +}; +``` + +**ํ•„ํ„ฐ๋ง ์˜ˆ์‹œ**: +``` +์ž…๋ ฅ ๋ฐฐ์—ด: ['{"CAT_1","CAT_2"}', 'CAT_ML7SR2T9_IM7H', 'CAT_ML8ZFQFU_EE5Z'] + โ†‘ ์†์ƒ๋จ (ํ•„ํ„ฐ๋ง) โ†‘ ์œ ํšจ โ†‘ ์œ ํšจ + +์ถœ๋ ฅ: 'CAT_ML7SR2T9_IM7H,CAT_ML8ZFQFU_EE5Z' +``` + +--- + +## ์ˆ˜์ •๋œ ํŒŒ์ผ ๋ชฉ๋ก + +| ํŒŒ์ผ | ์ˆ˜์ • ๋‚ด์šฉ | +|------|-----------| +| `frontend/components/screen/EditModal.tsx` | CREATE/UPDATE/๊ทธ๋ฃนUPDATE ๊ฒฝ๋กœ์— ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜ + ์†์ƒ๊ฐ’ ํ•„ํ„ฐ๋ง | +| `frontend/lib/utils/buttonActions.ts` | INSERT ๊ฒฝ๋กœ์— ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜ (์ด๋ฏธ ์ˆ˜์ •๋จ) | +| `frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx` | handleChange์—์„œ ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜ | +| `backend-node/src/services/nodeFlowExecutionService.ts` | normalizeValueForDB ํ—ฌํผ ์ถ”๊ฐ€ | + +--- + +## ๊ตํ›ˆ ๋ฐ ํ–ฅํ›„ ์ฃผ์˜์‚ฌํ•ญ + +### 1. ์ €์žฅ ๊ฒฝ๋กœ ํŒŒ์•…์˜ ์ค‘์š”์„ฑ + +ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ €์žฅ ๋กœ์ง์ด ์—ฌ๋Ÿฌ ๊ฒฝ๋กœ๋กœ ๋ถ„๊ธฐ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, **๋ชจ๋“  ๊ฒฝ๋กœ๋ฅผ ์ถ”์ **ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + +``` +์‚ฌ์šฉ์ž ์ €์žฅ ๋ฒ„ํŠผ ํด๋ฆญ + โ†“ +ButtonPrimaryComponent + โ†“ +buttonActions.handleSave() + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ onSave ์ฝœ๋ฐฑ์ด ์žˆ์œผ๋ฉด? โ”‚ +โ”‚ โ†’ EditModal.handleSave() ์ง์ ‘ ํ˜ธ์ถœโ”‚ โ† ์ด ๊ฒฝ๋กœ๋ฅผ ๋†“์นจ! +โ”‚ onSave ์ฝœ๋ฐฑ์ด ์—†์œผ๋ฉด? โ”‚ +โ”‚ โ†’ buttonActions ๋‚ด๋ถ€ ์ €์žฅ ๋กœ์ง โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2. ๋กœ๊ทธ ๊ธฐ๋ฐ˜ ๋””๋ฒ„๊น… + +๋กœ๊ทธ๊ฐ€ ์–ด๋””๊นŒ์ง€ ์ฐํžˆ๊ณ  ์–ด๋””์„œ ์•ˆ ์ฐํžˆ๋Š”์ง€๋ฅผ ํ†ตํ•ด ์ฝ”๋“œ ๊ฒฝ๋กœ๋ฅผ ์ถ”์ : + +``` +[์˜ˆ์ƒํ•œ ๋กœ๊ทธ] +buttonActions.ts:512 ๐Ÿ” [handleSave] ์ง„์ž… +buttonActions.ts:1021 ๐Ÿ”ง ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜ โ† ์ด๊ฒŒ ์•ˆ ๋‚˜์˜ด! + +[์‹ค์ œ ๋กœ๊ทธ] +buttonActions.ts:512 ๐Ÿ” [handleSave] ์ง„์ž… +dynamicForm.ts:140 ๐Ÿ”„ ํผ ๋ฐ์ดํ„ฐ ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ โ† ๋ฐ”๋กœ ์—ฌ๊ธฐ๋กœ ์ ํ”„! +``` + +### 3. ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ vs ๋‹ค์ค‘ ์„ ํƒ ๊ตฌ๋ถ„ + +๋ฐฐ์—ด์ด๋ผ๊ณ  ๋ชจ๋‘ ์‰ผํ‘œ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค: + +| ํƒ€์ž… | ์˜ˆ์‹œ | ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ• | +|------|------|-----------| +| ๋‹ค์ค‘ ์„ ํƒ | `["CAT_1", "CAT_2"]` | ์‰ผํ‘œ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ | +| ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ | `[{id: 1, _targetTable: "..."}]` | ๋ณ„๋„ ํ…Œ์ด๋ธ”์— ์ €์žฅ, ๋งˆ์Šคํ„ฐ์—์„œ ์ œ์™ธ | + +--- + +## ํ™•์ธ๋œ ์ •์ƒ ๋™์ž‘ + +``` +EditModal.tsx:1002 ๐Ÿ”ง [EditModal UPDATE] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜: division + {original: 3, valid: 3, converted: 'CAT_ML7SR2T9_IM7H,CAT_ML8ZFQFU_EE5Z,CAT_ML8ZFVEL_1TOR'} + +dynamicForm.ts:153 โœ… ํผ ๋ฐ์ดํ„ฐ ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต +``` + +--- + +## ์ž‘์„ฑ์ผ + +2026-02-05 + +## ์ž‘์„ฑ์ž + +AI Assistant (Claude) diff --git a/docs/DDD1542/PC_RESPONSIVE_IMPLEMENTATION_PLAN.md b/docs/DDD1542/PC_RESPONSIVE_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..3d6ec12d --- /dev/null +++ b/docs/DDD1542/PC_RESPONSIVE_IMPLEMENTATION_PLAN.md @@ -0,0 +1,688 @@ +# PC ๋ฐ˜์‘ํ˜• ๊ตฌํ˜„ ๊ณ„ํš์„œ + +> ์ž‘์„ฑ์ผ: 2026-01-30 +> ๋ชฉํ‘œ: PC ํ™˜๊ฒฝ (1280px ~ 1920px)์—์„œ ์™„๋ฒฝํ•œ ๋ฐ˜์‘ํ˜• ๊ตฌํ˜„ + +--- + +## 1. ๋ชฉํ‘œ ์ •์˜ + +### 1.1 ๋ฒ”์œ„ + +| ํ™˜๊ฒฝ | ํ™”๋ฉด ํฌ๊ธฐ | ์šฐ์„ ์ˆœ์œ„ | +|------|-----------|----------| +| **PC (๋Œ€ํ˜• ๋ชจ๋‹ˆํ„ฐ)** | 1920px | ๊ธฐ์ค€ | +| **PC (๋…ธํŠธ๋ถ)** | 1280px ~ 1440px | **1์ˆœ์œ„** | +| ํƒœ๋ธ”๋ฆฟ | 768px ~ 1024px | 2์ˆœ์œ„ (์ถ”ํ›„) | +| ๋ชจ๋ฐ”์ผ | < 768px | 3์ˆœ์œ„ (์ถ”ํ›„) | + +### 1.2 ๋ชฉํ‘œ ๋™์ž‘ + +``` +1920px ํ™”๋ฉด์—์„œ ๋””์ž์ธ + โ†“ +1280px ํ™”๋ฉด์œผ๋กœ ์ถ•์†Œ + โ†“ +์ปดํฌ๋„ŒํŠธ๋“ค์ด ๋น„์œจ์— ๋งž๊ฒŒ ์žฌ๋ฐฐ์น˜ (์œ„์น˜, ํฌ๊ธฐ ๋ชจ๋‘) + โ†“ +๋ ˆ์ด์•„์›ƒ ๊นจ์ง€์ง€ ์•Š์Œ +``` + +### 1.3 ์„ฑ๊ณต ๊ธฐ์ค€ + +- [ ] 1920px์—์„œ ๋””์ž์ธํ•œ ํ™”๋ฉด์ด 1280px์—์„œ ์ •์ƒ ํ‘œ์‹œ +- [ ] ๋ฒ„ํŠผ์ด ํ™”๋ฉด ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€์ง€ ์•Š์Œ +- [ ] ํ…Œ์ด๋ธ”์ด ํ™”๋ฉด ๋„ˆ๋น„์— ๋งž๊ฒŒ ์กฐ์ •๋จ +- [ ] ๋ถ„ํ•  ํŒจ๋„์ด ๋น„์œจ ์œ ์ง€ํ•˜๋ฉฐ ์ถ•์†Œ๋จ + +--- + +## 2. ํ˜„์žฌ ์‹œ์Šคํ…œ ๋ถ„์„ + +### 2.1 ๋ Œ๋”๋ง ํ๋ฆ„ (ํ˜„์žฌ) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 1. API ํ˜ธ์ถœ โ”‚ +โ”‚ screenApi.getLayoutV2(screenId) โ”‚ +โ”‚ โ†’ screen_layouts_v2.layout_data (JSONB) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 2. ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ โ”‚ +โ”‚ convertV2ToLegacy(v2Response) โ”‚ +โ”‚ โ†’ components ๋ฐฐ์—ด (position, size ํฌํ•จ) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 3. ์Šค์ผ€์ผ ๊ณ„์‚ฐ (page.tsx ๋ผ์ธ 395-460) โ”‚ +โ”‚ const designWidth = layout.screenResolution.width || 1200โ”‚ +โ”‚ const newScale = containerWidth / designWidth โ”‚ +โ”‚ โ†’ ์ „์ฒด ํ™”๋ฉด์„ scale()๋กœ ์ถ•์†Œ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 4. ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง (RealtimePreviewDynamic.tsx ๋ผ์ธ 524-536) โ”‚ +โ”‚ left: `${position.x}px` โ† ํ”ฝ์…€ ๊ณ ์ • โ”‚ +โ”‚ top: `${position.y}px` โ† ํ”ฝ์…€ ๊ณ ์ • โ”‚ +โ”‚ position: absolute โ† ์ ˆ๋Œ€ ์œ„์น˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2.2 ํ˜„์žฌ ๋ฐฉ์‹์˜ ๋ฌธ์ œ์  + +**ํ˜„์žฌ**: `transform: scale()` ๋ฐฉ์‹ +```tsx +// page.tsx ๋ผ์ธ 515-520 +
+``` + +| ๋ฌธ์ œ | ์„ค๋ช… | +|------|------| +| **์ถ•์†Œ๋งŒ ๋จ** | ๋ ˆ์ด์•„์›ƒ ์žฌ๋ฐฐ์น˜ ์—†์Œ | +| **ํฐํŠธ ์ž‘์•„์ง** | ์ „์ฒด scale๋กœ ํฐํŠธ๋„ ์ถ•์†Œ | +| **ํด๋ฆญ ์˜์—ญ ์˜ค์ฐจ** | scale ์ ์šฉ ์‹œ ํด๋ฆญ ์œ„์น˜ ๊ณ„์‚ฐ ์˜ค๋ฅ˜ ๊ฐ€๋Šฅ | +| **์ง„์ •ํ•œ ๋ฐ˜์‘ํ˜• ์•„๋‹˜** | ๋น„์œจ๋งŒ ์œ ์ง€, ๋ ˆ์ด์•„์›ƒ ์ตœ์ ํ™” ์—†์Œ | + +### 2.3 position.x, position.y ์‚ฌ์šฉ ์œ„์น˜ + +| ํŒŒ์ผ | ๋ผ์ธ | ์šฉ๋„ | +|------|------|------| +| `RealtimePreviewDynamic.tsx` | 524-526 | ์ปดํฌ๋„ŒํŠธ ์œ„์น˜ ์Šคํƒ€์ผ | +| `AutoRegisteringComponentRenderer.ts` | 42-43 | ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ | +| `page.tsx` | 744-745 | ์ž์‹ ์ปดํฌ๋„ŒํŠธ ์ƒ๋Œ€ ์œ„์น˜ | +| `ScreenDesigner.tsx` | 2890-2894 | ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์œ„์น˜ | +| `ScreenModal.tsx` | 620-621 | ๋ชจ๋‹ฌ ๋‚ด ์˜คํ”„์…‹ ์กฐ์ • | + +--- + +## 3. ๊ตฌํ˜„ ๋ฐฉ์‹: ํผ์„ผํŠธ ๊ธฐ๋ฐ˜ ๋ฐฐ์น˜ + +### 3.1 ํ•ต์‹ฌ ์•„์ด๋””์–ด + +``` +ํ”ฝ์…€ ์ขŒํ‘œ (1920px ๊ธฐ์ค€) + โ†“ +ํผ์„ผํŠธ๋กœ ๋ณ€ํ™˜ + โ†“ +ํ™”๋ฉด ํฌ๊ธฐ์— ๊ด€๊ณ„์—†์ด ๋น„์œจ ์œ ์ง€ +``` + +**์˜ˆ์‹œ**: +``` +๋ฒ„ํŠผ ์œ„์น˜: x=1753px (1920px ๊ธฐ์ค€) + โ†“ +ํผ์„ผํŠธ: 1753 / 1920 = 91.3% + โ†“ +1280px ํ™”๋ฉด: 1280 * 0.913 = 1168px + โ†“ +๋ฒ„ํŠผ์ด ํ™”๋ฉด ์•ˆ์— ์ •์ƒ ํ‘œ์‹œ +``` + +### 3.2 ๋ณ€ํ™˜ ๊ณต์‹ + +```typescript +// ํ”ฝ์…€ โ†’ ํผ์„ผํŠธ ๋ณ€ํ™˜ +const DESIGN_WIDTH = 1920; + +function toPercent(pixelX: number): string { + return `${(pixelX / DESIGN_WIDTH) * 100}%`; +} + +// ์‚ฌ์šฉ +left: toPercent(position.x) // "91.3%" +width: toPercent(size.width) // "8.2%" +``` + +### 3.3 Y์ถ• ์ฒ˜๋ฆฌ + +Y์ถ•์€ ๋‘ ๊ฐ€์ง€ ์˜ต์…˜: + +**์˜ต์…˜ A: Y์ถ•๋„ ํผ์„ผํŠธ (๊ถŒ์žฅ)** +```typescript +const DESIGN_HEIGHT = 1080; +top: `${(position.y / DESIGN_HEIGHT) * 100}%` +``` + +**์˜ต์…˜ B: Y์ถ•์€ ํ”ฝ์…€ ์œ ์ง€** +```typescript +top: `${position.y}px` // ์„ธ๋กœ๋Š” ์Šคํฌ๋กค๋กœ ํ•ด๊ฒฐ +``` + +**๊ฒฐ์ •: ์˜ต์…˜ B (Y์ถ• ํ”ฝ์…€ ์œ ์ง€)** +- ์ด์œ : ์„ธ๋กœ ์Šคํฌ๋กค์€ ์ž์—ฐ์Šค๋Ÿฌ์›€ +- ๊ฐ€๋กœ๋งŒ ๋ฐ˜์‘ํ˜•์ด๋ฉด PC ํ™˜๊ฒฝ์—์„œ ์ถฉ๋ถ„ + +--- + +## 4. ๊ตฌํ˜„ ์ƒ์„ธ + +### 4.1 ์ˆ˜์ • ํŒŒ์ผ ๋ชฉ๋ก + +| ํŒŒ์ผ | ์ˆ˜์ • ๋‚ด์šฉ | +|------|-----------| +| `RealtimePreviewDynamic.tsx` | left, width๋ฅผ ํผ์„ผํŠธ๋กœ ๋ณ€๊ฒฝ | +| `AutoRegisteringComponentRenderer.ts` | left, width๋ฅผ ํผ์„ผํŠธ๋กœ ๋ณ€๊ฒฝ | +| `page.tsx` | scale ์ œ๊ฑฐ, ์ปจํ…Œ์ด๋„ˆ width: 100% | + +### 4.2 RealtimePreviewDynamic.tsx ์ˆ˜์ • + +**ํ˜„์žฌ (๋ผ์ธ 524-530)**: +```tsx +const baseStyle = { + left: `${adjustedPositionX}px`, + top: `${position.y}px`, + width: displayWidth, + height: displayHeight, + zIndex: component.type === "layout" ? 1 : position.z || 2, +}; +``` + +**๋ณ€๊ฒฝ ํ›„**: +```tsx +const DESIGN_WIDTH = 1920; + +const baseStyle = { + left: `${(adjustedPositionX / DESIGN_WIDTH) * 100}%`, // ํผ์„ผํŠธ + top: `${position.y}px`, // Y์ถ•์€ ํ”ฝ์…€ ์œ ์ง€ + width: `${(parseFloat(displayWidth) / DESIGN_WIDTH) * 100}%`, // ํผ์„ผํŠธ + height: displayHeight, // ๋†’์ด๋Š” ํ”ฝ์…€ ์œ ์ง€ + zIndex: component.type === "layout" ? 1 : position.z || 2, +}; +``` + +### 4.3 AutoRegisteringComponentRenderer.ts ์ˆ˜์ • + +**ํ˜„์žฌ (๋ผ์ธ 40-48)**: +```tsx +const baseStyle: React.CSSProperties = { + position: "absolute", + left: `${component.position?.x || 0}px`, + top: `${component.position?.y || 0}px`, + width: `${component.size?.width || 200}px`, + height: `${component.size?.height || 36}px`, + zIndex: component.position?.z || 1, +}; +``` + +**๋ณ€๊ฒฝ ํ›„**: +```tsx +const DESIGN_WIDTH = 1920; + +const baseStyle: React.CSSProperties = { + position: "absolute", + left: `${((component.position?.x || 0) / DESIGN_WIDTH) * 100}%`, // ํผ์„ผํŠธ + top: `${component.position?.y || 0}px`, // Y์ถ•์€ ํ”ฝ์…€ ์œ ์ง€ + width: `${((component.size?.width || 200) / DESIGN_WIDTH) * 100}%`, // ํผ์„ผํŠธ + height: `${component.size?.height || 36}px`, // ๋†’์ด๋Š” ํ”ฝ์…€ ์œ ์ง€ + zIndex: component.position?.z || 1, +}; +``` + +### 4.4 page.tsx ์ˆ˜์ • + +**ํ˜„์žฌ (๋ผ์ธ 515-528)**: +```tsx +
+``` + +**๋ณ€๊ฒฝ ํ›„**: +```tsx +
+``` + +### 4.5 ๊ณตํ†ต ์ƒ์ˆ˜ ํŒŒ์ผ ์ƒ์„ฑ + +```typescript +// frontend/lib/constants/responsive.ts + +export const RESPONSIVE_CONFIG = { + DESIGN_WIDTH: 1920, + DESIGN_HEIGHT: 1080, + MIN_WIDTH: 1280, + MAX_WIDTH: 1920, +} as const; + +export function toPercentX(pixelX: number): string { + return `${(pixelX / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`; +} + +export function toPercentWidth(pixelWidth: number): string { + return `${(pixelWidth / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`; +} +``` + +--- + +## 5. ๊ฐ€์ƒ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + +### 5.1 ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹œ๋‚˜๋ฆฌ์˜ค + +**ํ…Œ์ŠคํŠธ ํ™”๋ฉด**: screen_id = 68 (์ˆ˜์ฃผ ๋ชฉ๋ก) +```json +{ + "components": [ + { + "id": "comp_1895", + "url": "v2-table-list", + "position": { "x": 8, "y": 128 }, + "size": { "width": 1904, "height": 600 } + }, + { + "id": "comp_1896", + "url": "v2-button-primary", + "position": { "x": 1753, "y": 88 }, + "size": { "width": 158, "height": 40 } + }, + { + "id": "comp_1897", + "url": "v2-button-primary", + "position": { "x": 1594, "y": 88 }, + "size": { "width": 158, "height": 40 } + }, + { + "id": "comp_1898", + "url": "v2-button-primary", + "position": { "x": 1436, "y": 88 }, + "size": { "width": 158, "height": 40 } + } + ] +} +``` + +### 5.2 ํ˜„์žฌ ๋ฐฉ์‹ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + +**1920px ํ™”๋ฉด**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋ถ„๋ฆฌ] [์ €์žฅ] [์ˆ˜์ •] [์‚ญ์ œ] โ”‚ +โ”‚ 1277 1436 1594 1753 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ x=8 x=1904 โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ…Œ์ด๋ธ” (width: 1904px) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ ํ‘œ์‹œ +``` + +**1280px ํ™”๋ฉด (ํ˜„์žฌ scale ๋ฐฉ์‹)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ scale(0.67) ์ ์šฉ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [๋ถ„๋ฆฌ][์ €][์ˆ˜][์‚ญ] โ”‚ โ”‚ โ† ์ „์ฒด ์ถ•์†Œ, ํฐํŠธ ์ž‘์•„์ง +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ํ…Œ์ด๋ธ” (์ถ•์†Œ๋จ) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ (์—ฌ๋ฐฑ ๋ฐœ์ƒ) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โš ๏ธ ์ž‘๋™ํ•˜์ง€๋งŒ ํฐํŠธ/์—ฌ๋ฐฑ ๋ฌธ์ œ +``` + +### 5.3 ํผ์„ผํŠธ ๋ฐฉ์‹ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + +**๋ณ€ํ™˜ ๊ณ„์‚ฐ**: +``` +ํ…Œ์ด๋ธ”: + x: 8px โ†’ 8/1920 = 0.42% + width: 1904px โ†’ 1904/1920 = 99.17% + +์‚ญ์ œ ๋ฒ„ํŠผ: + x: 1753px โ†’ 1753/1920 = 91.30% + width: 158px โ†’ 158/1920 = 8.23% + +์ˆ˜์ • ๋ฒ„ํŠผ: + x: 1594px โ†’ 1594/1920 = 83.02% + width: 158px โ†’ 158/1920 = 8.23% + +์ €์žฅ ๋ฒ„ํŠผ: + x: 1436px โ†’ 1436/1920 = 74.79% + width: 158px โ†’ 158/1920 = 8.23% + +๋ถ„๋ฆฌ ๋ฒ„ํŠผ: + x: 1277px โ†’ 1277/1920 = 66.51% + width: 158px โ†’ 158/1920 = 8.23% +``` + +**1920px ํ™”๋ฉด**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋ถ„๋ฆฌ] [์ €์žฅ] [์ˆ˜์ •] [์‚ญ์ œ] โ”‚ +โ”‚ 66.5% 74.8% 83.0% 91.3% โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ 0.42% 99.6% โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ…Œ์ด๋ธ” (width: 99.17%) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ ํ‘œ์‹œ (1920px์™€ ๋™์ผ) +``` + +**1280px ํ™”๋ฉด (ํผ์„ผํŠธ ๋ฐฉ์‹)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋ถ„๋ฆฌ][์ €์žฅ][์ˆ˜์ •][์‚ญ์ œ] โ”‚ +โ”‚ 66.5% 74.8% 83.0% 91.3% โ”‚ +โ”‚ = 851 957 1063 1169 โ”‚ โ† ํ™”๋ฉด ์•ˆ์— ํ‘œ์‹œ! +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ 0.42% 99.6% โ”‚ +โ”‚ = 5px = 1275 โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ…Œ์ด๋ธ” (width: 99.17%) โ”‚ โ”‚ โ† ํ™”๋ฉด ๋„ˆ๋น„์— ๋งž๊ฒŒ ์กฐ์ • +โ”‚ โ”‚ = 1280 * 0.9917 = 1269px โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ๋น„์œจ ์œ ์ง€, ํ™”๋ฉด ์•ˆ์— ํ‘œ์‹œ, ํฐํŠธ ํฌ๊ธฐ ์œ ์ง€ +``` + +### 5.4 ๋ฒ„ํŠผ ๊ฐ„๊ฒฉ ๊ฒ€์ฆ + +**1920px**: +``` +๋ถ„๋ฆฌ: 1277px, ๋„ˆ๋น„ 158px โ†’ ๋: 1435px +์ €์žฅ: 1436px (๊ฐ„๊ฒฉ: 1px) +์ˆ˜์ •: 1594px (๊ฐ„๊ฒฉ: 1px) +์‚ญ์ œ: 1753px (๊ฐ„๊ฒฉ: 1px) +``` + +**1280px (ํผ์„ผํŠธ ๋ณ€ํ™˜ ํ›„)**: +``` +๋ถ„๋ฆฌ: 1280 * 0.665 = 851px, ๋„ˆ๋น„ 1280 * 0.082 = 105px โ†’ ๋: 956px +์ €์žฅ: 1280 * 0.748 = 957px (๊ฐ„๊ฒฉ: 1px) โœ… +์ˆ˜์ •: 1280 * 0.830 = 1063px (๊ฐ„๊ฒฉ: 1px) โœ… +์‚ญ์ œ: 1280 * 0.913 = 1169px (๊ฐ„๊ฒฉ: 1px) โœ… +``` + +**๊ฒฐ๋ก **: ๋ฒ„ํŠผ ๊ฐ„๊ฒฉ ๋น„์œจ๋„ ์œ ์ง€๋จ + +--- + +## 6. ์—ฃ์ง€ ์ผ€์ด์Šค ๊ฒ€์ฆ + +### 6.1 ๋ถ„ํ•  ํŒจ๋„ (SplitPanelLayout) + +**ํ˜„์žฌ ๋™์ž‘**: +- ์ขŒ์ธก ํŒจ๋„: 60% ๋„ˆ๋น„ +- ์šฐ์ธก ํŒจ๋„: 40% ๋„ˆ๋น„ +- **์ด๋ฏธ ํผ์„ผํŠธ ๊ธฐ๋ฐ˜!** + +**์‹œ๋ฎฌ๋ ˆ์ด์…˜**: +``` +1920px: ์ขŒ์ธก 1152px, ์šฐ์ธก 768px +1280px: ์ขŒ์ธก 768px, ์šฐ์ธก 512px +โœ… ์ž๋™์œผ๋กœ ๋น„์œจ ์œ ์ง€๋จ +``` + +**๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ**: +- ๋ฌธ์ œ: ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ”ฝ์…€ ๊ณ ์ •์ด๋ฉด ๊นจ์ง +- ํ•ด๊ฒฐ: ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€๋„ ํผ์„ผํŠธ ์ ์šฉ ํ•„์š” + +### 6.2 ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ (TableList) + +**ํ˜„์žฌ**: +- ํ…Œ์ด๋ธ” ์ž์ฒด๋Š” ์ปจํ…Œ์ด๋„ˆ ๋„ˆ๋น„ 100% ์‚ฌ์šฉ +- ์ปฌ๋Ÿผ ๋„ˆ๋น„๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ ์กฐ์ • + +**์‹œ๋ฎฌ๋ ˆ์ด์…˜**: +``` +1920px: ํ…Œ์ด๋ธ” ์ปจํ…Œ์ด๋„ˆ width: 99.17% = 1904px +1280px: ํ…Œ์ด๋ธ” ์ปจํ…Œ์ด๋„ˆ width: 99.17% = 1269px +โœ… ํ…Œ์ด๋ธ”์ด ์ž๋™์œผ๋กœ ์กฐ์ •๋จ +``` + +### 6.3 ์ž์‹ ์ปดํฌ๋„ŒํŠธ ์ƒ๋Œ€ ์œ„์น˜ + +**ํ˜„์žฌ ์ฝ”๋“œ (page.tsx ๋ผ์ธ 744-745)**: +```typescript +const relativeChildComponent = { + position: { + x: child.position.x - component.position.x, + y: child.position.y - component.position.y, + }, +}; +``` + +**๋ฌธ์ œ**: ์ƒ๋Œ€ ์ขŒํ‘œ๋„ ํ”ฝ์…€ ๊ธฐ๋ฐ˜ + +**ํ•ด๊ฒฐ**: ๋ถ€๋ชจ ๊ธฐ์ค€ ํผ์„ผํŠธ๋กœ ๋ณ€ํ™˜ +```typescript +const relativeChildComponent = { + position: { + // ๋ถ€๋ชจ ๋„ˆ๋น„ ๊ธฐ์ค€ ํผ์„ผํŠธ + xPercent: ((child.position.x - component.position.x) / component.size.width) * 100, + y: child.position.y - component.position.y, + }, +}; +``` + +### 6.4 ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ (๋””์ž์ธ ๋ชจ๋“œ) + +**ScreenDesigner.tsx**: +- ๋“œ๋กญ ์œ„์น˜๋Š” ์—ฌ์ „ํžˆ ํ”ฝ์…€๋กœ ์ €์žฅ +- ๋ Œ๋”๋ง ์‹œ์—๋งŒ ํผ์„ผํŠธ๋กœ ๋ณ€ํ™˜ +- **์ €์žฅ ๋ฐฉ์‹ ๋ณ€๊ฒฝ ์—†์Œ!** + +**์‹œ๋ฎฌ๋ ˆ์ด์…˜**: +``` +1. ๋””์ž์ด๋„ˆ๊ฐ€ 1920px ํ™”๋ฉด์—์„œ ๋ฒ„ํŠผ ๋“œ๋กญ +2. position: { x: 1753, y: 88 } ์ €์žฅ (ํ”ฝ์…€) +3. ๋ Œ๋”๋ง ์‹œ 91.3%๋กœ ๋ณ€ํ™˜ +4. 1280px ํ™”๋ฉด์—์„œ๋„ ์ •์ƒ ํ‘œ์‹œ +โœ… ๋””์ž์ธ ๋ชจ๋“œ ํ˜ธํ™˜ +``` + +### 6.5 ๋ชจ๋‹ฌ ๋‚ด ํ™”๋ฉด + +**ScreenModal.tsx (๋ผ์ธ 620-621)**: +```typescript +x: parseFloat(component.position?.x?.toString() || "0") - offsetX, +y: parseFloat(component.position?.y?.toString() || "0") - offsetY, +``` + +**๋ฌธ์ œ**: ์˜คํ”„์…‹ ๊ณ„์‚ฐ์ด ํ”ฝ์…€ ๊ธฐ๋ฐ˜ + +**ํ•ด๊ฒฐ**: ๋ชจ๋‹ฌ ์ปจํ…Œ์ด๋„ˆ๋„ ํผ์„ผํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ +```typescript +// ๋ชจ๋‹ฌ ์ปจํ…Œ์ด๋„ˆ ๋„ˆ๋น„ ๊ธฐ์ค€์œผ๋กœ ํผ์„ผํŠธ ๊ณ„์‚ฐ +const modalWidth = containerRef.current?.clientWidth || DESIGN_WIDTH; +const xPercent = ((position.x - offsetX) / DESIGN_WIDTH) * 100; +``` + +--- + +## 7. ์ž ์žฌ์  ๋ฌธ์ œ ๋ฐ ํ•ด๊ฒฐ์ฑ… + +### 7.1 ์ตœ์†Œ ๋„ˆ๋น„ ๋ฌธ์ œ + +**๋ฌธ์ œ**: ๋ฒ„ํŠผ์ด ๋„ˆ๋ฌด ์ž‘์•„์งˆ ์ˆ˜ ์žˆ์Œ +``` +158px ๋ฒ„ํŠผ โ†’ 1280px ํ™”๋ฉด์—์„œ 105px +โ†’ ํ…์ŠคํŠธ๊ฐ€ ์ž˜๋ฆด ์ˆ˜ ์žˆ์Œ +``` + +**ํ•ด๊ฒฐ**: min-width ์„ค์ • +```css +min-width: 80px; +``` + +### 7.2 ๊ฒน์นจ ๋ฌธ์ œ + +**๋ฌธ์ œ**: ํ™”๋ฉด์ด ์ž‘์•„์ง€๋ฉด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ฒน์น  ์ˆ˜ ์žˆ์Œ + +**์‹œ๋ฎฌ๋ ˆ์ด์…˜**: +``` +1920px: ๋ฒ„ํŠผ 4๊ฐœ๊ฐ€ ๊ฐ„๊ฒฉ 1px๋กœ ๋ฐฐ์น˜ +1280px: ๋ฒ„ํŠผ 4๊ฐœ๊ฐ€ ๊ฐ„๊ฒฉ 1px๋กœ ๋ฐฐ์น˜ (๋น„์œจ ์œ ์ง€) +โœ… ๊ฒน์น˜์ง€ ์•Š์Œ (๊ฐ„๊ฒฉ๋„ ๋น„์œจ๋กœ ์ถ•์†Œ) +``` + +### 7.3 ํฐํŠธ ํฌ๊ธฐ + +**ํ˜„์žฌ**: ํฐํŠธ๋Š” px ๊ณ ์ • +**๋ณ€๊ฒฝ ํ›„**: ํฐํŠธ ํฌ๊ธฐ ์œ ์ง€ (scale์ด ์•„๋‹ˆ๋ฏ€๋กœ) + +**๊ฒฐ๊ณผ**: ํฐํŠธ ํฌ๊ธฐ๋Š” ๊ทธ๋Œ€๋กœ, ๋ ˆ์ด์•„์›ƒ๋งŒ ๋น„์œจ ์กฐ์ • +โœ… ๊ฐ€๋…์„ฑ ์œ ์ง€ + +### 7.4 height ์ฒ˜๋ฆฌ + +**๊ฒฐ์ •**: height๋Š” ํ”ฝ์…€ ์œ ์ง€ +- ์ด์œ : ์„ธ๋กœ ์Šคํฌ๋กค์€ ์ž์—ฐ์Šค๋Ÿฌ์›€ +- ์„ธ๋กœ ๋ฐ˜์‘ํ˜•์€ ๋ถˆํ•„์š” (PC ํ™˜๊ฒฝ) + +--- + +## 8. ํ˜ธํ™˜์„ฑ ๊ฒ€์ฆ + +### 8.1 ๊ธฐ์กด ํ™”๋ฉด ํ˜ธํ™˜ + +| ํ•ญ๋ชฉ | ํ˜ธํ™˜ ์—ฌ๋ถ€ | ์ด์œ  | +|------|----------|------| +| ์ผ๋ฐ˜ ๋ฒ„ํŠผ | โœ… | ํผ์„ผํŠธ๋กœ ๋ณ€ํ™˜, ์œ„์น˜ ์œ ์ง€ | +| ํ…Œ์ด๋ธ” | โœ… | ์ปจํ…Œ์ด๋„ˆ ๋น„์œจ ์œ ์ง€ | +| ๋ถ„ํ•  ํŒจ๋„ | โœ… | ์ด๋ฏธ ํผ์„ผํŠธ ๊ธฐ๋ฐ˜ | +| ํƒญ ๋ ˆ์ด์•„์›ƒ | โœ… | ์ปจํ…Œ์ด๋„ˆ ๋น„์œจ ์œ ์ง€ | +| ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ | โœ… | ๋‚ด๋ถ€๋Š” ๊ธฐ์กด ๋ฐฉ์‹ | +| ์ธํ’‹ ํ•„๋“œ | โœ… | ์ปจํ…Œ์ด๋„ˆ ๋น„์œจ ์œ ์ง€ | + +### 8.2 ๋””์ž์ธ ๋ชจ๋“œ ํ˜ธํ™˜ + +| ํ•ญ๋ชฉ | ํ˜ธํ™˜ ์—ฌ๋ถ€ | ์ด์œ  | +|------|----------|------| +| ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ | โœ… | ์ €์žฅ์€ ํ”ฝ์…€, ๋ Œ๋”๋ง๋งŒ ํผ์„ผํŠธ | +| ๋ฆฌ์‚ฌ์ด์ฆˆ | โœ… | ์ €์žฅ์€ ํ”ฝ์…€, ๋ Œ๋”๋ง๋งŒ ํผ์„ผํŠธ | +| ๊ทธ๋ฆฌ๋“œ ์Šค๋ƒ… | โœ… | ์Šค๋ƒ…์€ ํ”ฝ์…€ ๊ธฐ์ค€ ์œ ์ง€ | +| ๋ฏธ๋ฆฌ๋ณด๊ธฐ | โœ… | ๋ Œ๋”๋ง ๋™์ผ ๋ฐฉ์‹ | + +### 8.3 API ํ˜ธํ™˜ + +| ํ•ญ๋ชฉ | ํ˜ธํ™˜ ์—ฌ๋ถ€ | ์ด์œ  | +|------|----------|------| +| DB ์ €์žฅ | โœ… | ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์—†์Œ (ํ”ฝ์…€ ์ €์žฅ) | +| API ์‘๋‹ต | โœ… | ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์—†์Œ | +| V2 ๋ณ€ํ™˜ | โœ… | ๋ณ€ํ™˜ ๋กœ์ง ๋ณ€๊ฒฝ ์—†์Œ | + +--- + +## 9. ๊ตฌํ˜„ ์ˆœ์„œ + +### Phase 1: ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ ์ƒ์„ฑ (30๋ถ„) + +```typescript +// frontend/lib/constants/responsive.ts +export const RESPONSIVE_CONFIG = { + DESIGN_WIDTH: 1920, +} as const; + +export function toPercentX(pixelX: number): string { + return `${(pixelX / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`; +} + +export function toPercentWidth(pixelWidth: number): string { + return `${(pixelWidth / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`; +} +``` + +### Phase 2: RealtimePreviewDynamic.tsx ์ˆ˜์ • (1์‹œ๊ฐ„) + +1. import ์ถ”๊ฐ€ +2. baseStyle์˜ left, width๋ฅผ ํผ์„ผํŠธ๋กœ ๋ณ€๊ฒฝ +3. ๋ถ„ํ•  ํŒจ๋„ ์œ„ ๋ฒ„ํŠผ ์กฐ์ • ๋กœ์ง๋„ ํผ์„ผํŠธ ์ ์šฉ + +### Phase 3: AutoRegisteringComponentRenderer.ts ์ˆ˜์ • (30๋ถ„) + +1. import ์ถ”๊ฐ€ +2. getComponentStyle()์˜ left, width๋ฅผ ํผ์„ผํŠธ๋กœ ๋ณ€๊ฒฝ + +### Phase 4: page.tsx ์ˆ˜์ • (1์‹œ๊ฐ„) + +1. scale ๋กœ์ง ์ œ๊ฑฐ ๋˜๋Š” ์ˆ˜์ • +2. ์ปจํ…Œ์ด๋„ˆ width: 100%๋กœ ๋ณ€๊ฒฝ +3. ์ž์‹ ์ปดํฌ๋„ŒํŠธ ์ƒ๋Œ€ ์œ„์น˜ ๊ณ„์‚ฐ ์ˆ˜์ • + +### Phase 5: ํ…Œ์ŠคํŠธ (1์‹œ๊ฐ„) + +1. 1920px ํ™”๋ฉด์—์„œ ๊ธฐ์กด ํ™”๋ฉด ์ •์ƒ ๋™์ž‘ ํ™•์ธ +2. 1280px ํ™”๋ฉด์œผ๋กœ ์ถ•์†Œ ํ…Œ์ŠคํŠธ +3. ๋ถ„ํ•  ํŒจ๋„ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +4. ๋””์ž์ธ ๋ชจ๋“œ ํ…Œ์ŠคํŠธ + +--- + +## 10. ์ตœ์ข… ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### ๊ตฌํ˜„ ์ „ + +- [ ] ํ˜„์žฌ ๋™์ž‘ํ•˜๋Š” ํ™”๋ฉด ์Šคํฌ๋ฆฐ์ƒท ์บก์ฒ˜ (๋น„๊ต์šฉ) +- [ ] ํ…Œ์ŠคํŠธ ํ™”๋ฉด ๋ชฉ๋ก ์„ ์ • + +### ๊ตฌํ˜„ ์ค‘ + +- [ ] responsive.ts ์ƒ์„ฑ +- [ ] RealtimePreviewDynamic.tsx ์ˆ˜์ • +- [ ] AutoRegisteringComponentRenderer.ts ์ˆ˜์ • +- [ ] page.tsx ์ˆ˜์ • + +### ๊ตฌํ˜„ ํ›„ + +- [ ] 1920px ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] 1440px ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] 1280px ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] ๋ถ„ํ•  ํŒจ๋„ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] ๋””์ž์ธ ๋ชจ๋“œ ํ…Œ์ŠคํŠธ +- [ ] ๋ชจ๋‹ฌ ๋‚ด ํ™”๋ฉด ํ…Œ์ŠคํŠธ + +--- + +## 11. ์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„ + +| ์ž‘์—… | ์‹œ๊ฐ„ | +|------|------| +| ์œ ํ‹ธ๋ฆฌํ‹ฐ ์ƒ์„ฑ | 30๋ถ„ | +| RealtimePreviewDynamic.tsx | 1์‹œ๊ฐ„ | +| AutoRegisteringComponentRenderer.ts | 30๋ถ„ | +| page.tsx | 1์‹œ๊ฐ„ | +| ํ…Œ์ŠคํŠธ | 1์‹œ๊ฐ„ | +| **ํ•ฉ๊ณ„** | **4์‹œ๊ฐ„** | + +--- + +## 12. ๊ฒฐ๋ก  + +**ํผ์„ผํŠธ ๊ธฐ๋ฐ˜ ๋ฐฐ์น˜**๊ฐ€ PC ๋ฐ˜์‘ํ˜•์˜ ๊ฐ€์žฅ ํ™•์‹คํ•œ ํ•ด๊ฒฐ์ฑ…์ž…๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | scale ๋ฐฉ์‹ | ํผ์„ผํŠธ ๋ฐฉ์‹ | +|------|-----------|------------| +| ํฐํŠธ ํฌ๊ธฐ | ์ถ•์†Œ๋จ | **์œ ์ง€** | +| ๋ ˆ์ด์•„์›ƒ ๋น„์œจ | ์œ ์ง€ | **์œ ์ง€** | +| ํด๋ฆญ ์˜์—ญ | ์˜ค์ฐจ ๊ฐ€๋Šฅ | **์ •ํ™•** | +| ๊ตฌํ˜„ ๋ณต์žก๋„ | ๋‚ฎ์Œ | **์ค‘๊ฐ„** | +| ์ง„์ •ํ•œ ๋ฐ˜์‘ํ˜• | โŒ | **โœ…** | + +**DB ๋ณ€๊ฒฝ ์—†์ด, ๋ Œ๋”๋ง ๋กœ์ง๋งŒ ์ˆ˜์ •**ํ•˜์—ฌ ์™„๋ฒฝํ•œ PC ๋ฐ˜์‘ํ˜•์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. diff --git a/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md b/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md new file mode 100644 index 00000000..411fdd1f --- /dev/null +++ b/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md @@ -0,0 +1,832 @@ +# ๋ฐ˜์‘ํ˜• ๊ทธ๋ฆฌ๋“œ ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ + +> ์ตœ์ข… ์—…๋ฐ์ดํŠธ: 2026-01-30 + +--- + +## 1. ๊ฐœ์š” + +### 1.1 ํ˜„์žฌ ๋ฌธ์ œ + +**์ปดํฌ๋„ŒํŠธ ์œ„์น˜/ํฌ๊ธฐ๊ฐ€ ํ”ฝ์…€ ๋‹จ์œ„๋กœ ๊ณ ์ •๋˜์–ด ๋ฐ˜์‘ํ˜• ๋ฏธ์ง€์›** + +```json +// ํ˜„์žฌ DB ์ €์žฅ ๋ฐฉ์‹ (screen_layouts_v2.layout_data) +{ + "position": { "x": 1753, "y": 88 }, + "size": { "width": 158, "height": 40 } +} +``` + +| ํ™”๋ฉด ํฌ๊ธฐ | ๊ฒฐ๊ณผ | +|-----------|------| +| 1920px (๋””์ž์ธ ๊ธฐ์ค€) | ์ •์ƒ | +| 1280px (๋…ธํŠธ๋ถ) | ์˜ค๋ฅธ์ชฝ ๋ฒ„ํŠผ ์ž˜๋ฆผ | +| 768px (ํƒœ๋ธ”๋ฆฟ) | ๋ ˆ์ด์•„์›ƒ ์™„์ „ํžˆ ๊นจ์ง | +| 375px (๋ชจ๋ฐ”์ผ) | ์‚ฌ์šฉ ๋ถˆ๊ฐ€ | + +### 1.2 ๋ชฉํ‘œ + +| ๋ชฉํ‘œ | ์„ค๋ช… | +|------|------| +| PC ๋Œ€์‘ | 1280px ~ 1920px | +| ํƒœ๋ธ”๋ฆฟ ๋Œ€์‘ | 768px ~ 1024px | +| ๋ชจ๋ฐ”์ผ ๋Œ€์‘ | 320px ~ 767px | + +### 1.3 ํ•ด๊ฒฐ ๋ฐฉํ–ฅ + +``` +ํ˜„์žฌ: ํ”ฝ์…€ ์ขŒํ‘œ โ†’ position: absolute โ†’ ๊ณ ์ • ๋ ˆ์ด์•„์›ƒ +๋ณ€๊ฒฝ: ๊ทธ๋ฆฌ๋“œ ์…€ ๋ฒˆํ˜ธ โ†’ CSS Grid + ResizeObserver โ†’ ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ +``` + +--- + +## 2. ํ˜„์žฌ ์‹œ์Šคํ…œ ๋ถ„์„ + +### 2.1 ๋ฐ์ดํ„ฐ ํ˜„ํ™ฉ + +``` +์ด ๋ ˆ์ด์•„์›ƒ: 1,250๊ฐœ +์ด ์ปดํฌ๋„ŒํŠธ: 5,236๊ฐœ +ํšŒ์‚ฌ ์ˆ˜: 14๊ฐœ +ํ…Œ์ด๋ธ” ํฌ๊ธฐ: ์•ฝ 3MB +``` + +### 2.2 ์ปดํฌ๋„ŒํŠธ ํƒ€์ž…๋ณ„ ๋ถ„ํฌ + +| ์ปดํฌ๋„ŒํŠธ | ์ˆ˜๋Ÿ‰ | shadcn ์‚ฌ์šฉ | +|----------|------|-------------| +| v2-input | 1,914 | โœ… `@/components/ui/input` | +| v2-button-primary | 1,549 | โœ… `@/components/ui/button` | +| v2-table-search-widget | 355 | โœ… shadcn ๊ธฐ๋ฐ˜ | +| v2-select | 327 | โœ… `@/components/ui/select` | +| v2-table-list | 285 | โœ… `@/components/ui/table` | +| v2-media | 181 | โœ… shadcn ๊ธฐ๋ฐ˜ | +| v2-date | 132 | โœ… `@/components/ui/calendar` | +| **v2-split-panel-layout** | **131** | โœ… shadcn ๊ธฐ๋ฐ˜ (**๋ฐ˜์‘ํ˜• ํ•„์š”**) | +| v2-tabs-widget | 75 | โœ… shadcn ๊ธฐ๋ฐ˜ | +| ๊ธฐํƒ€ | 287 | โœ… shadcn ๊ธฐ๋ฐ˜ | +| **ํ•ฉ๊ณ„** | **5,236** | **์ „๋ถ€ shadcn** | + +### 2.3 ํ˜„์žฌ ๋ Œ๋”๋ง ๋ฐฉ์‹ + +```tsx +// frontend/lib/registry/layouts/flexbox/FlexboxLayout.tsx (๋ผ์ธ 234-248) +{components.map((child) => ( +
+ {renderer.renderChild(child)} +
+))} +``` + +### 2.4 ํ•ต์‹ฌ ๋ฐœ๊ฒฌ + +``` +โœ… ์ด๋ฏธ ์žˆ๋Š” ๊ฒƒ: +- 12์ปฌ๋Ÿผ ๊ทธ๋ฆฌ๋“œ ์„ค์ • (gridSettings.columns: 12) +- ๊ทธ๋ฆฌ๋“œ ์Šค๋ƒ… ๊ธฐ๋Šฅ (snapToGrid: true) +- shadcn/ui ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ (์ „์ฒด) + +โŒ ์—†๋Š” ๊ฒƒ: +- ๊ทธ๋ฆฌ๋“œ ์…€ ๋ฒˆํ˜ธ ์ €์žฅ (ํ˜„์žฌ ํ”ฝ์…€ ์ €์žฅ) +- ๋ฐ˜์‘ํ˜• ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ ์„ค์ • +- CSS Grid ๊ธฐ๋ฐ˜ ๋ Œ๋”๋ง +- ๋ถ„ํ•  ํŒจ๋„ ๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ +``` + +### 2.5 ๋ ˆ์ด์•„์›ƒ ์‹œ์Šคํ…œ ๊ตฌ์กฐ + +ํ˜„์žฌ ์‹œ์Šคํ…œ์—๋Š” ๋‘ ๊ฐ€์ง€ ๋ ˆ๋ฒจ์˜ ๋ ˆ์ด์•„์›ƒ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค: + +#### 2.5.1 ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ (screen_layouts_v2) + +ํ™”๋ฉด ์ „์ฒด์˜ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜๋ฅผ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. + +```json +// DB ๊ตฌ์กฐ +{ + "version": "2.0", + "components": [ + { "id": "comp_1", "position": { "x": 100, "y": 50 }, ... }, + { "id": "comp_2", "position": { "x": 500, "y": 50 }, ... }, + { "id": "GridLayout_1", "position": { "x": 100, "y": 200 }, ... } + ] +} +``` + +**ํ˜„์žฌ**: absolute ํฌ์ง€์…˜์œผ๋กœ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ โ†’ **๋ฐ˜์‘ํ˜• ๋ถˆ๊ฐ€** + +#### 2.5.2 ์ปดํฌ๋„ŒํŠธ ๋ ˆ์ด์•„์›ƒ (GridLayout, FlexboxLayout ๋“ฑ) + +๊ฐœ๋ณ„ ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์˜ zone ๋ฐฐ์น˜๋ฅผ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. + +| ์ปดํฌ๋„ŒํŠธ | ์œ„์น˜ | ๋‚ด๋ถ€ ๊ตฌ์กฐ | CSS Grid ์‚ฌ์šฉ | +|----------|------|-----------|---------------| +| `GridLayout` | `layouts/grid/` | zones ๋ฐฐ์—ด | โœ… ์ด๋ฏธ ์‚ฌ์šฉ | +| `FlexboxLayout` | `layouts/flexbox/` | zones ๋ฐฐ์—ด | โŒ absolute | +| `SplitLayout` | `layouts/split/` | left/right | โŒ flex | +| `TabsLayout` | `layouts/` | tabs ๋ฐฐ์—ด | โŒ ํƒญ ๊ตฌ์กฐ | +| `CardLayout` | `layouts/card-layout/` | zones ๋ฐฐ์—ด | โŒ flex | +| `AccordionLayout` | `layouts/accordion/` | items ๋ฐฐ์—ด | โŒ ์•„์ฝ”๋””์–ธ | + +#### 2.5.3 ๊ตฌ์กฐ ๋‹ค์ด์–ด๊ทธ๋žจ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ screen_layouts_v2 (ํ™”๋ฉด ์ „์ฒด) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ˜„์žฌ: absolute ํฌ์ง€์…˜ โ†’ ๋ฐ˜์‘ํ˜• ๋ถˆ๊ฐ€ โ”‚ โ”‚ +โ”‚ โ”‚ ๋ณ€๊ฒฝ: ResponsiveGridLayout (CSS Grid) โ†’ ๋ฐ˜์‘ํ˜• ๊ฐ€๋Šฅ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-button โ”‚ โ”‚ v2-input โ”‚ โ”‚ GridLayout (์ปดํฌ๋„ŒํŠธ) โ”‚ โ”‚ +โ”‚ โ”‚ (shadcn) โ”‚ โ”‚ (shadcn) โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ zone1 โ”‚ zone2 โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ (์ด๋ฏธ โ”‚ (์ด๋ฏธ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ CSS Gridโ”‚ CSS Grid) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2.6 ๊ธฐ์กด ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ ํ˜ธํ™˜์„ฑ + +#### 2.6.1 GridLayout (๊ธฐ์กด ์ปค์Šคํ…€ ๊ทธ๋ฆฌ๋“œ) + +```tsx +// frontend/lib/registry/layouts/grid/GridLayout.tsx +// ์ด๋ฏธ CSS Grid๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Œ! + +const gridStyle: React.CSSProperties = { + display: "grid", + gridTemplateRows: `repeat(${gridConfig.rows}, 1fr)`, + gridTemplateColumns: `repeat(${gridConfig.columns}, 1fr)`, + gap: `${gridConfig.gap || 16}px`, +}; +``` + +**ํ˜ธํ™˜์„ฑ**: โœ… **์™„์ „ ํ˜ธํ™˜** +- GridLayout์€ ํ™”๋ฉด ๋‚ด ํ•˜๋‚˜์˜ ์ปดํฌ๋„ŒํŠธ๋กœ ์ทจ๊ธ‰๋จ +- ResponsiveGridLayout์ด GridLayout์˜ **์œ„์น˜๋งŒ** ๊ด€๋ฆฌ +- GridLayout ๋‚ด๋ถ€๋Š” ๊ธฐ์กด ๋ฐฉ์‹ ๊ทธ๋Œ€๋กœ ๋™์ž‘ + +#### 2.6.2 FlexboxLayout + +```tsx +// frontend/lib/registry/layouts/flexbox/FlexboxLayout.tsx +// zone ๋‚ด๋ถ€์—์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ absolute๋กœ ๋ฐฐ์น˜ + +{zoneChildren.map((child) => ( +
+ {renderer.renderChild(child)} +
+))} +``` + +**ํ˜ธํ™˜์„ฑ**: โœ… **ํ˜ธํ™˜** (๋‚ด๋ถ€๋Š” ๊ธฐ์กด ๋ฐฉ์‹ ์œ ์ง€) +- FlexboxLayout ์ปดํฌ๋„ŒํŠธ ์ž์ฒด์˜ ์œ„์น˜๋Š” ResponsiveGridLayout์ด ๊ด€๋ฆฌ +- ๋‚ด๋ถ€ zone์˜ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜๋Š” ๊ธฐ์กด absolute ๋ฐฉ์‹ ์œ ์ง€ + +#### 2.6.3 SplitPanelLayout (๋ถ„ํ•  ํŒจ๋„) + +**ํ˜ธํ™˜์„ฑ**: โš ๏ธ **๋ณ„๋„ ์ˆ˜์ • ํ•„์š”** +- ์™ธ๋ถ€ ์œ„์น˜: ResponsiveGridLayout์ด ๊ด€๋ฆฌ โœ… +- ๋‚ด๋ถ€ ๋ฐ˜์‘ํ˜•: ๋ณ„๋„ ์ˆ˜์ • ํ•„์š” (๋ชจ๋ฐ”์ผ์—์„œ ์ƒํ•˜ ๋ถ„ํ• ) + +#### 2.6.4 ํ˜ธํ™˜์„ฑ ์š”์•ฝ + +| ์ปดํฌ๋„ŒํŠธ | ์™ธ๋ถ€ ๋ฐฐ์น˜ | ๋‚ด๋ถ€ ๋™์ž‘ | ์ถ”๊ฐ€ ์ˆ˜์ • | +|----------|----------|----------|-----------| +| **v2-button, v2-input ๋“ฑ** | โœ… ๋ฐ˜์‘ํ˜• | โœ… shadcn ๊ทธ๋Œ€๋กœ | โŒ ๋ถˆํ•„์š” | +| **GridLayout** | โœ… ๋ฐ˜์‘ํ˜• | โœ… CSS Grid ๊ทธ๋Œ€๋กœ | โŒ ๋ถˆํ•„์š” | +| **FlexboxLayout** | โœ… ๋ฐ˜์‘ํ˜• | โš ๏ธ absolute ์œ ์ง€ | โŒ ๋ถˆํ•„์š” | +| **SplitPanelLayout** | โœ… ๋ฐ˜์‘ํ˜• | โŒ ์ขŒ์šฐ ๊ณ ์ • | โš ๏ธ ๋‚ด๋ถ€ ๋ฐ˜์‘ํ˜• ์ถ”๊ฐ€ | +| **TabsLayout** | โœ… ๋ฐ˜์‘ํ˜• | โœ… ํƒญ ๊ทธ๋Œ€๋กœ | โŒ ๋ถˆํ•„์š” | + +### 2.7 ๋™์ž‘ ๋ฐฉ์‹ ๋น„๊ต + +#### ๋ณ€๊ฒฝ ์ „ + +``` +ํ™”๋ฉด ๋กœ๋“œ + โ†“ +screen_layouts_v2์—์„œ components ์กฐํšŒ + โ†“ +๊ฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ position.x, position.y๋กœ absolute ๋ฐฐ์น˜ + โ†“ +GridLayout ์ปดํฌ๋„ŒํŠธ๋„ absolute๋กœ ๋ฐฐ์น˜๋จ + โ†“ +GridLayout ๋‚ด๋ถ€๋Š” CSS Grid๋กœ zone ๋ฐฐ์น˜ + โ†“ +๊ฒฐ๊ณผ: ํ™”๋ฉด ํฌ๊ธฐ ๋ณ€ํ•ด๋„ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ ์œ„์น˜ ๊ณ ์ • +``` + +#### ๋ณ€๊ฒฝ ํ›„ + +``` +ํ™”๋ฉด ๋กœ๋“œ + โ†“ +screen_layouts_v2์—์„œ components ์กฐํšŒ + โ†“ +layoutMode === "grid" ํ™•์ธ + โ†“ +ResponsiveGridLayout์œผ๋กœ ๋ Œ๋”๋ง (CSS Grid) + โ†“ +๊ฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ grid.col, grid.colSpan์œผ๋กœ ๋ฐฐ์น˜ + โ†“ +ํ™”๋ฉด ํฌ๊ธฐ ๊ฐ์ง€ (ResizeObserver) + โ†“ +breakpoint์— ๋”ฐ๋ผ responsive.sm/md/lg ์ ์šฉ + โ†“ +GridLayout ์ปดํฌ๋„ŒํŠธ๋„ ๋ฐ˜์‘ํ˜•์œผ๋กœ ๋ฐฐ์น˜๋จ + โ†“ +GridLayout ๋‚ด๋ถ€๋Š” ๊ธฐ์กด CSS Grid๋กœ zone ๋ฐฐ์น˜ (๋ณ€๊ฒฝ ์—†์Œ) + โ†“ +๊ฒฐ๊ณผ: ํ™”๋ฉด ํฌ๊ธฐ์— ๋”ฐ๋ผ ์ปดํฌ๋„ŒํŠธ ์žฌ๋ฐฐ์น˜ +``` + +--- + +## 3. ๊ธฐ์ˆ  ๊ฒฐ์ • + +### 3.1 ์™œ Tailwind ๋™์  ํด๋ž˜์Šค๊ฐ€ ์•„๋‹Œ CSS Grid + Inline Style์ธ๊ฐ€? + +**Tailwind ๋™์  ํด๋ž˜์Šค์˜ ํ•œ๊ณ„**: +```tsx +// โŒ ์ด๊ฑด ์•ˆ ๋จ - Tailwind๊ฐ€ ๋นŒ๋“œ ํƒ€์ž„์— ์ธ์‹ ๋ชปํ•จ +className={`col-start-${col} md:col-start-${mdCol}`} + +// โœ… ์ด๊ฒƒ๋งŒ ๋จ - ์ •์  ํด๋ž˜์Šค +className="col-start-1 md:col-start-3" +``` + +Tailwind๋Š” **๋นŒ๋“œ ํƒ€์ž„**์— ํด๋ž˜์Šค๋ฅผ ์Šค์บ”ํ•˜๋ฏ€๋กœ, ๋Ÿฐํƒ€์ž„์— ๋™์ ์œผ๋กœ ์ƒ์„ฑ๋˜๋Š” ํด๋ž˜์Šค๋Š” ์ธ์‹ํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. + +**ํ•ด๊ฒฐ์ฑ…: CSS Grid + Inline Style + ResizeObserver**: +```tsx +// โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ• +
+
+ {component} +
+
+``` + +### 3.2 ์—ญํ•  ๋ถ„๋‹ด + +| ์˜์—ญ | ๊ธฐ์ˆ  | ์„ค๋ช… | +|------|------|------| +| **UI ์ปดํฌ๋„ŒํŠธ** | shadcn/ui | ๋ฒ„ํŠผ, ์ธํ’‹, ํ…Œ์ด๋ธ” ๋“ฑ (์ด๋ฏธ ์ ์šฉ๋จ) | +| **๋ ˆ์ด์•„์›ƒ ๋ฐฐ์น˜** | CSS Grid + Inline Style | ์ปดํฌ๋„ŒํŠธ ์œ„์น˜, ํฌ๊ธฐ, ๋ฐ˜์‘ํ˜• | +| **๋ฐ˜์‘ํ˜• ๊ฐ์ง€** | ResizeObserver | ํ™”๋ฉด ํฌ๊ธฐ ๊ฐ์ง€ ๋ฐ ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ ๋ณ€๊ฒฝ | + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ResponsiveGridLayout (CSS Grid) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ shadcn โ”‚ โ”‚ shadcn โ”‚ โ”‚ shadcn โ”‚ โ”‚ +โ”‚ โ”‚ Button โ”‚ โ”‚ Input โ”‚ โ”‚ Select โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ shadcn Table โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 4. ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ๋ณ€๊ฒฝ + +### 4.1 ํ˜„์žฌ ๊ตฌ์กฐ (V2) + +```json +{ + "version": "2.0", + "components": [{ + "id": "comp_xxx", + "url": "@/lib/registry/components/v2-button-primary", + "position": { "x": 1753, "y": 88, "z": 1 }, + "size": { "width": 158, "height": 40 }, + "overrides": { ... } + }] +} +``` + +### 4.2 ๋ณ€๊ฒฝ ํ›„ ๊ตฌ์กฐ (V2 + ๊ทธ๋ฆฌ๋“œ) + +```json +{ + "version": "2.0", + "layoutMode": "grid", + "components": [{ + "id": "comp_xxx", + "url": "@/lib/registry/components/v2-button-primary", + "position": { "x": 1753, "y": 88, "z": 1 }, + "size": { "width": 158, "height": 40 }, + "grid": { + "col": 11, + "row": 2, + "colSpan": 1, + "rowSpan": 1 + }, + "responsive": { + "sm": { "col": 1, "colSpan": 12 }, + "md": { "col": 7, "colSpan": 6 }, + "lg": { "col": 11, "colSpan": 1 } + }, + "overrides": { ... } + }], + "gridSettings": { + "columns": 12, + "rowHeight": 80, + "gap": 16 + } +} +``` + +### 4.3 ํ•„๋“œ ์„ค๋ช… + +| ํ•„๋“œ | ํƒ€์ž… | ์„ค๋ช… | +|------|------|------| +| `layoutMode` | string | "grid" (๋ฐ˜์‘ํ˜• ๊ทธ๋ฆฌ๋“œ ์‚ฌ์šฉ) | +| `grid.col` | number | ์‹œ์ž‘ ์ปฌ๋Ÿผ (1-12) | +| `grid.row` | number | ์‹œ์ž‘ ํ–‰ (1๋ถ€ํ„ฐ) | +| `grid.colSpan` | number | ์ฐจ์ง€ํ•˜๋Š” ์ปฌ๋Ÿผ ์ˆ˜ | +| `grid.rowSpan` | number | ์ฐจ์ง€ํ•˜๋Š” ํ–‰ ์ˆ˜ | +| `responsive.sm` | object | ๋ชจ๋ฐ”์ผ (< 768px) ์„ค์ • | +| `responsive.md` | object | ํƒœ๋ธ”๋ฆฟ (768px ~ 1024px) ์„ค์ • | +| `responsive.lg` | object | ๋ฐ์Šคํฌํ†ฑ (> 1024px) ์„ค์ • | + +### 4.4 ํ˜ธํ™˜์„ฑ + +- `position`, `size` ํ•„๋“œ๋Š” ์œ ์ง€ (๋””์ž์ธ ๋ชจ๋“œ + ํด๋ฐฑ์šฉ) +- `layoutMode`๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ์กด ๋ฐฉ์‹(absolute) ์‚ฌ์šฉ +- ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ›„์—๋„ ๊ธฐ์กด ํ™”๋ฉด ์ •์ƒ ๋™์ž‘ + +--- + +## 5. ๊ตฌํ˜„ ์ƒ์„ธ + +### 5.1 ๊ทธ๋ฆฌ๋“œ ๋ณ€ํ™˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ + +```typescript +// frontend/lib/utils/gridConverter.ts + +const DESIGN_WIDTH = 1920; +const COLUMNS = 12; +const COLUMN_WIDTH = DESIGN_WIDTH / COLUMNS; // 160px +const ROW_HEIGHT = 80; + +/** + * ํ”ฝ์…€ ์ขŒํ‘œ๋ฅผ ๊ทธ๋ฆฌ๋“œ ์…€ ๋ฒˆํ˜ธ๋กœ ๋ณ€ํ™˜ + */ +export function pixelToGrid( + position: { x: number; y: number }, + size: { width: number; height: number } +): GridPosition { + return { + col: Math.max(1, Math.min(12, Math.round(position.x / COLUMN_WIDTH) + 1)), + row: Math.max(1, Math.round(position.y / ROW_HEIGHT) + 1), + colSpan: Math.max(1, Math.round(size.width / COLUMN_WIDTH)), + rowSpan: Math.max(1, Math.round(size.height / ROW_HEIGHT)), + }; +} + +/** + * ๊ธฐ๋ณธ ๋ฐ˜์‘ํ˜• ์„ค์ • ์ƒ์„ฑ + */ +export function getDefaultResponsive(grid: GridPosition): ResponsiveConfig { + return { + sm: { col: 1, colSpan: 12 }, // ๋ชจ๋ฐ”์ผ: ์ „์ฒด ๋„ˆ๋น„ + md: { + col: Math.max(1, Math.round(grid.col / 2)), + colSpan: Math.min(grid.colSpan * 2, 12) + }, // ํƒœ๋ธ”๋ฆฟ: 2๋ฐฐ ํ™•์žฅ + lg: { col: grid.col, colSpan: grid.colSpan }, // ๋ฐ์Šคํฌํ†ฑ: ์›๋ณธ + }; +} +``` + +### 5.2 ๋ฐ˜์‘ํ˜• ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ + +```tsx +// frontend/lib/registry/layouts/responsive-grid/ResponsiveGridLayout.tsx + +import React, { useRef, useState, useEffect } from "react"; + +type Breakpoint = "sm" | "md" | "lg"; + +interface ResponsiveGridLayoutProps { + layout: LayoutData; + isDesignMode: boolean; + renderer: ComponentRenderer; +} + +export function ResponsiveGridLayout({ + layout, + isDesignMode, + renderer, +}: ResponsiveGridLayoutProps) { + const containerRef = useRef(null); + const [breakpoint, setBreakpoint] = useState("lg"); + + // ํ™”๋ฉด ํฌ๊ธฐ ๊ฐ์ง€ + useEffect(() => { + if (!containerRef.current) return; + + const observer = new ResizeObserver((entries) => { + const width = entries[0].contentRect.width; + if (width < 768) setBreakpoint("sm"); + else if (width < 1024) setBreakpoint("md"); + else setBreakpoint("lg"); + }); + + observer.observe(containerRef.current); + return () => observer.disconnect(); + }, []); + + const gridSettings = layout.gridSettings || { columns: 12, rowHeight: 80, gap: 16 }; + + return ( +
+ {layout.components + .sort((a, b) => (a.grid?.row || 0) - (b.grid?.row || 0)) + .map((component) => { + // ๋ฐ˜์‘ํ˜• ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ + const gridConfig = component.responsive?.[breakpoint] || component.grid; + const { col, colSpan } = gridConfig; + const rowSpan = component.grid?.rowSpan || 1; + + return ( +
+ {renderer.renderChild(component)} +
+ ); + })} +
+ ); +} +``` + +### 5.3 ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ ํ›… + +```typescript +// frontend/lib/registry/layouts/responsive-grid/useBreakpoint.ts + +import { useState, useEffect, RefObject } from "react"; + +type Breakpoint = "sm" | "md" | "lg"; + +export function useBreakpoint(containerRef: RefObject): Breakpoint { + const [breakpoint, setBreakpoint] = useState("lg"); + + useEffect(() => { + if (!containerRef.current) return; + + const observer = new ResizeObserver((entries) => { + const width = entries[0].contentRect.width; + if (width < 768) setBreakpoint("sm"); + else if (width < 1024) setBreakpoint("md"); + else setBreakpoint("lg"); + }); + + observer.observe(containerRef.current); + return () => observer.disconnect(); + }, [containerRef]); + + return breakpoint; +} +``` + +### 5.4 ๋ถ„ํ•  ํŒจ๋„ ๋ฐ˜์‘ํ˜• ์ˆ˜์ • + +```tsx +// frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx + +// ์ถ”๊ฐ€ํ•  ์ฝ”๋“œ +const containerRef = useRef(null); +const [isMobile, setIsMobile] = useState(false); + +useEffect(() => { + if (!containerRef.current) return; + + const observer = new ResizeObserver((entries) => { + const width = entries[0].contentRect.width; + setIsMobile(width < 768); + }); + + observer.observe(containerRef.current); + return () => observer.disconnect(); +}, []); + +// ๋ Œ๋”๋ง ๋ถ€๋ถ„ ์ˆ˜์ • +return ( +
+
+ {/* ์ขŒ์ธก/์ƒ๋‹จ ํŒจ๋„ */} +
+
+ {/* ์šฐ์ธก/ํ•˜๋‹จ ํŒจ๋„ */} +
+
+); +``` + +--- + +## 6. ๋ Œ๋”๋ง ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ + +```typescript +// frontend/lib/registry/DynamicComponentRenderer.tsx + +function renderLayout(layout: LayoutData) { + // layoutMode์— ๋”ฐ๋ผ ๋ถ„๊ธฐ + if (layout.layoutMode === "grid") { + return ; + } + + // ๊ธฐ์กด ๋ฐฉ์‹ (ํด๋ฐฑ) + return ; +} +``` + +--- + +## 7. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +### 7.1 ๋ฐฑ์—… + +```sql +-- ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „ ๋ฐฑ์—… +CREATE TABLE screen_layouts_v2_backup_20260130 AS +SELECT * FROM screen_layouts_v2; +``` + +### 7.2 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ + +```sql +-- grid, responsive ํ•„๋“œ ์ถ”๊ฐ€ +UPDATE screen_layouts_v2 +SET layout_data = ( + SELECT jsonb_set( + jsonb_set( + layout_data, + '{layoutMode}', + '"grid"' + ), + '{components}', + ( + SELECT jsonb_agg( + comp || jsonb_build_object( + 'grid', jsonb_build_object( + 'col', GREATEST(1, LEAST(12, ROUND((comp->'position'->>'x')::NUMERIC / 160) + 1)), + 'row', GREATEST(1, ROUND((comp->'position'->>'y')::NUMERIC / 80) + 1), + 'colSpan', GREATEST(1, ROUND((comp->'size'->>'width')::NUMERIC / 160)), + 'rowSpan', GREATEST(1, ROUND((comp->'size'->>'height')::NUMERIC / 80)) + ), + 'responsive', jsonb_build_object( + 'sm', jsonb_build_object('col', 1, 'colSpan', 12), + 'md', jsonb_build_object( + 'col', GREATEST(1, ROUND((ROUND((comp->'position'->>'x')::NUMERIC / 160) + 1) / 2.0)), + 'colSpan', LEAST(ROUND((comp->'size'->>'width')::NUMERIC / 160) * 2, 12) + ), + 'lg', jsonb_build_object( + 'col', GREATEST(1, LEAST(12, ROUND((comp->'position'->>'x')::NUMERIC / 160) + 1)), + 'colSpan', GREATEST(1, ROUND((comp->'size'->>'width')::NUMERIC / 160)) + ) + ) + ) + ) + FROM jsonb_array_elements(layout_data->'components') as comp + ) + ) +); +``` + +### 7.3 ๋กค๋ฐฑ + +```sql +-- ๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ๋กค๋ฐฑ +DROP TABLE screen_layouts_v2; +ALTER TABLE screen_layouts_v2_backup_20260130 RENAME TO screen_layouts_v2; +``` + +--- + +## 8. ๋™์ž‘ ํ๋ฆ„ + +### 8.1 ๋ฐ์Šคํฌํ†ฑ (> 1024px) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 1 2 3 4 5 6 7 8 9 10 โ”‚ 11 12 โ”‚ โ”‚ +โ”‚ โ”‚ [๋ฒ„ํŠผ] โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ํ…Œ์ด๋ธ” (12์ปฌ๋Ÿผ) โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 8.2 ํƒœ๋ธ”๋ฆฟ (768px ~ 1024px) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 1 2 3 4 5 6 โ”‚ 7 8 9 10 11 12 โ”‚ +โ”‚ โ”‚ [๋ฒ„ํŠผ] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ํ…Œ์ด๋ธ” (12์ปฌ๋Ÿผ) โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 8.3 ๋ชจ๋ฐ”์ผ (< 768px) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋ฒ„ํŠผ] โ”‚ โ† 12์ปฌ๋Ÿผ (์ „์ฒด ๋„ˆ๋น„) +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ํ…Œ์ด๋ธ” (์Šคํฌ๋กค) โ”‚ โ† 12์ปฌ๋Ÿผ (์ „์ฒด ๋„ˆ๋น„) +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 8.4 ๋ถ„ํ•  ํŒจ๋„ (๋ฐ˜์‘ํ˜•) + +**๋ฐ์Šคํฌํ†ฑ**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ์ขŒ์ธก ํŒจ๋„ (60%) โ”‚ ์šฐ์ธก ํŒจ๋„ (40%) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**๋ชจ๋ฐ”์ผ**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ์ƒ๋‹จ ํŒจ๋„ (์ด์ „ ์ขŒ์ธก) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ•˜๋‹จ ํŒจ๋„ (์ด์ „ ์šฐ์ธก) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 9. ์ˆ˜์ • ํŒŒ์ผ ๋ชฉ๋ก + +### 9.1 ์ƒˆ๋กœ ์ƒ์„ฑ + +| ํŒŒ์ผ | ์„ค๋ช… | +|------|------| +| `lib/utils/gridConverter.ts` | ํ”ฝ์…€ โ†’ ๊ทธ๋ฆฌ๋“œ ๋ณ€ํ™˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ | +| `lib/registry/layouts/responsive-grid/ResponsiveGridLayout.tsx` | CSS Grid ๋ ˆ์ด์•„์›ƒ | +| `lib/registry/layouts/responsive-grid/useBreakpoint.ts` | ResizeObserver ํ›… | +| `lib/registry/layouts/responsive-grid/index.ts` | ๋ชจ๋“ˆ export | + +### 9.2 ์ˆ˜์ • + +| ํŒŒ์ผ | ์ˆ˜์ • ๋‚ด์šฉ | +|------|-----------| +| `lib/registry/DynamicComponentRenderer.tsx` | layoutMode ๋ถ„๊ธฐ ์ถ”๊ฐ€ | +| `components/screen/ScreenDesigner.tsx` | ์ €์žฅ ์‹œ grid/responsive ์ƒ์„ฑ | +| `v2-split-panel-layout/SplitPanelLayoutComponent.tsx` | ๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ ์ถ”๊ฐ€ | + +### 9.3 ์ˆ˜์ • ์—†์Œ + +| ํŒŒ์ผ | ์ด์œ  | +|------|------| +| `v2-input/*` | ๋ ˆ์ด์•„์›ƒ๊ณผ ๋ฌด๊ด€ (shadcn ๊ทธ๋Œ€๋กœ) | +| `v2-button-primary/*` | ๋ ˆ์ด์•„์›ƒ๊ณผ ๋ฌด๊ด€ (shadcn ๊ทธ๋Œ€๋กœ) | +| `v2-table-list/*` | ๋ ˆ์ด์•„์›ƒ๊ณผ ๋ฌด๊ด€ (shadcn ๊ทธ๋Œ€๋กœ) | +| `v2-select/*` | ๋ ˆ์ด์•„์›ƒ๊ณผ ๋ฌด๊ด€ (shadcn ๊ทธ๋Œ€๋กœ) | +| **...๋ชจ๋“  v2 ์ปดํฌ๋„ŒํŠธ** | **์ˆ˜์ • ๋ถˆํ•„์š”** | + +--- + +## 10. ์ž‘์—… ์ผ์ • + +| Phase | ์ž‘์—… | ํŒŒ์ผ | ์‹œ๊ฐ„ | +|-------|------|------|------| +| **1** | ๊ทธ๋ฆฌ๋“œ ๋ณ€ํ™˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ | `gridConverter.ts` | 2์‹œ๊ฐ„ | +| **1** | ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ ํ›… | `useBreakpoint.ts` | 1์‹œ๊ฐ„ | +| **2** | ResponsiveGridLayout | `ResponsiveGridLayout.tsx` | 4์‹œ๊ฐ„ | +| **2** | ๋ Œ๋”๋ง ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ | `DynamicComponentRenderer.tsx` | 1์‹œ๊ฐ„ | +| **3** | ์ €์žฅ ๋กœ์ง ์ˆ˜์ • | `ScreenDesigner.tsx` | 2์‹œ๊ฐ„ | +| **3** | ๋ถ„ํ•  ํŒจ๋„ ๋ฐ˜์‘ํ˜• | `SplitPanelLayoutComponent.tsx` | 3์‹œ๊ฐ„ | +| **4** | ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ | SQL | 2์‹œ๊ฐ„ | +| **4** | ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ | - | 1์‹œ๊ฐ„ | +| **5** | ํ…Œ์ŠคํŠธ ๋ฐ ๋ฒ„๊ทธ ์ˆ˜์ • | - | 4์‹œ๊ฐ„ | +| | **ํ•ฉ๊ณ„** | | **์•ฝ 2.5์ผ** | + +--- + +## 11. ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### ๊ฐœ๋ฐœ ์ „ + +- [ ] screen_layouts_v2 ๋ฐฑ์—… ์™„๋ฃŒ +- [ ] ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„ + +### Phase 1: ์œ ํ‹ธ๋ฆฌํ‹ฐ + +- [ ] `gridConverter.ts` ์ƒ์„ฑ +- [ ] `useBreakpoint.ts` ์ƒ์„ฑ +- [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ + +### Phase 2: ๋ ˆ์ด์•„์›ƒ + +- [ ] `ResponsiveGridLayout.tsx` ์ƒ์„ฑ +- [ ] `DynamicComponentRenderer.tsx` ๋ถ„๊ธฐ ์ถ”๊ฐ€ +- [ ] ๊ธฐ์กด ํ™”๋ฉด ์ •์ƒ ๋™์ž‘ ํ™•์ธ + +### Phase 3: ์ €์žฅ/์ˆ˜์ • + +- [ ] `ScreenDesigner.tsx` ์ €์žฅ ๋กœ์ง ์ˆ˜์ • +- [ ] `SplitPanelLayoutComponent.tsx` ๋ฐ˜์‘ํ˜• ์ถ”๊ฐ€ +- [ ] ๋””์ž์ธ ๋ชจ๋“œ ํ…Œ์ŠคํŠธ + +### Phase 4: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +- [ ] ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ ํ…Œ์ŠคํŠธ (๊ฐœ๋ฐœ DB) +- [ ] ์šด์˜ DB ๋ฐฑ์—… +- [ ] ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ +- [ ] ๊ฒ€์ฆ + +### Phase 5: ํ…Œ์ŠคํŠธ + +- [ ] PC (1920px, 1280px) ํ…Œ์ŠคํŠธ +- [ ] ํƒœ๋ธ”๋ฆฟ (768px, 1024px) ํ…Œ์ŠคํŠธ +- [ ] ๋ชจ๋ฐ”์ผ (375px, 414px) ํ…Œ์ŠคํŠธ +- [ ] ๋ถ„ํ•  ํŒจ๋„ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] GridLayout ์ปดํฌ๋„ŒํŠธ ํฌํ•จ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] FlexboxLayout ์ปดํฌ๋„ŒํŠธ ํฌํ•จ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] TabsLayout ์ปดํฌ๋„ŒํŠธ ํฌํ•จ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] ์ค‘์ฒฉ ๋ ˆ์ด์•„์›ƒ (GridLayout ์•ˆ์— ์ปดํฌ๋„ŒํŠธ) ํ…Œ์ŠคํŠธ + +--- + +## 12. ๋ฆฌ์Šคํฌ ๋ฐ ๋Œ€์‘ + +| ๋ฆฌ์Šคํฌ | ์˜ํ–ฅ | ๋Œ€์‘ | +|--------|------|------| +| ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํŒจ | ๋†’์Œ | ๋ฐฑ์—… ํ…Œ์ด๋ธ”์—์„œ ์ฆ‰์‹œ ๋กค๋ฐฑ | +| ๊ธฐ์กด ํ™”๋ฉด ๊นจ์ง | ์ค‘๊ฐ„ | `layoutMode` ์—†์œผ๋ฉด ๊ธฐ์กด ๋ฐฉ์‹ ์‚ฌ์šฉ (ํด๋ฐฑ) | +| ๋””์ž์ธ ๋ชจ๋“œ ํ˜ผ๋ž€ | ๋‚ฎ์Œ | position/size ํ•„๋“œ ์œ ์ง€ | +| GridLayout ๋‚ด๋ถ€ ๊นจ์ง | ๋‚ฎ์Œ | ๋‚ด๋ถ€๋Š” ๊ธฐ์กด ๋ฐฉ์‹ ์œ ์ง€, ์™ธ๋ถ€ ๋ฐฐ์น˜๋งŒ ๋ณ€๊ฒฝ | +| ์ค‘์ฒฉ ๋ ˆ์ด์•„์›ƒ ๋ฌธ์ œ | ๋‚ฎ์Œ | ๊ฐ ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ | + +--- + +## 13. ์ฐธ๊ณ  + +- [COMPONENT_LAYOUT_V2_ARCHITECTURE.md](./COMPONENT_LAYOUT_V2_ARCHITECTURE.md) - V2 ์•„ํ‚คํ…์ฒ˜ +- [CSS Grid Layout - MDN](https://developer.mozilla.org/ko/docs/Web/CSS/CSS_Grid_Layout) +- [ResizeObserver - MDN](https://developer.mozilla.org/ko/docs/Web/API/ResizeObserver) +- [shadcn/ui](https://ui.shadcn.com/) - ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ diff --git a/docs/DDD1542/V2_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_ํ•™์Šต๋…ธํŠธ_DDD1542.md b/docs/DDD1542/V2_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_ํ•™์Šต๋…ธํŠธ_DDD1542.md new file mode 100644 index 00000000..801fb213 --- /dev/null +++ b/docs/DDD1542/V2_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_ํ•™์Šต๋…ธํŠธ_DDD1542.md @@ -0,0 +1,399 @@ +# V2 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•™์Šต๋…ธํŠธ (DDD1542 ์ „์šฉ) + +> **๋ชฉ์ **: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ž‘์—… ์ „ ์™„๋ฒฝํ•œ ์ดํ•ด๋ฅผ ์œ„ํ•œ ๊ฐœ์ธ ํ•™์Šต๋…ธํŠธ +> **์ž‘์„ฑ์ผ**: 2026-02-03 +> **์ ˆ๋Œ€ ๊ทœ์น™**: ๋ชจ๋ฅด๋ฉด ๋ฌผ์–ด๋ณด๊ธฐ, ์ถ”์ธก ๊ธˆ์ง€ + +--- + +## 1. ๊ฐ€์žฅ ์ค‘์š”ํ•œ ํ•ต์‹ฌ (์ด์ „ ์‹ ํ•˜๊ฐ€ ์‹คํŒจํ•œ ์ด์œ ) + +### 1.1 "component" vs "v2-input" ์ฐจ์ด + +``` +[์ž˜๋ชป๋œ ์ƒํƒœ] [์˜ฌ๋ฐ”๋ฅธ ์ƒํƒœ] +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ component โ”‚ โ”‚ v2-input โ”‚ +โ”‚ ์—…์ฒด์ฝ”๋“œ โ”‚ โ”‚ ์—…์ฒด์ฝ”๋“œ โ”‚ +โ”‚ "์ž๋™ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค" โ”‚ โ”‚ "์ž๋™ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค" โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†‘ โ†‘ + ํ…Œ์ด๋ธ”-์ปฌ๋Ÿผ ์—ฐ๊ฒฐ ์—†์Œ table_name + column_name ์—ฐ๊ฒฐ๋จ +``` + +**ํ•ต์‹ฌ**: ์ปฌ๋Ÿผ์„ ์™ผ์ชฝ ํŒจ๋„์—์„œ **๋“œ๋ž˜๊ทธ**ํ•ด์•ผ ์˜ฌ๋ฐ”๋ฅธ ์—ฐ๊ฒฐ์ด ์ƒ์„ฑ๋จ + +### 1.2 ์˜ฌ๋ฐ”๋ฅธ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ ๋ฐฉ๋ฒ• + +``` +[์™ผ์ชฝ ํŒจ๋„: ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋ชฉ๋ก] +์šด์†ก์—…์ฒด (8๊ฐœ) +โ”œโ”€โ”€ ์—…์ฒด์ฝ”๋“œ [numbering] โ”€๋“œ๋ž˜๊ทธโ†’ ํ™”๋ฉด ์บ”๋ฒ„์Šค โ†’ v2-numbering-rule (๋˜๋Š” v2-input) +โ”œโ”€โ”€ ์—…์ฒด๋ช… [text] โ”€๋“œ๋ž˜๊ทธโ†’ ํ™”๋ฉด ์บ”๋ฒ„์Šค โ†’ v2-input +โ”œโ”€โ”€ ์œ ํ˜• [category] โ”€๋“œ๋ž˜๊ทธโ†’ ํ™”๋ฉด ์บ”๋ฒ„์Šค โ†’ v2-select +โ”œโ”€โ”€ ์—ฐ๋ฝ์ฒ˜ [text] โ”€๋“œ๋ž˜๊ทธโ†’ ํ™”๋ฉด ์บ”๋ฒ„์Šค โ†’ v2-input +โ””โ”€โ”€ ... +``` + +### 1.3 input_type โ†’ V2 ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +| table_type_columns.input_type | V2 ์ปดํฌ๋„ŒํŠธ | ์—ฐ๋™ ํ…Œ์ด๋ธ” | +|-------------------------------|-------------|-------------| +| text | v2-input | - | +| number | v2-input (type=number) | - | +| date | v2-date | - | +| category | v2-select | category_values | +| numbering | v2-numbering-rule ๋˜๋Š” v2-input | numbering_rules | +| entity | v2-entity-search | ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ | + +--- + +## 2. V1 vs V2 ๊ตฌ์กฐ ์ฐจ์ด + +### 2.1 ํ…Œ์ด๋ธ” ๊ตฌ์กฐ + +``` +V1 (๋ณธ์„œ๋ฒ„: screen_layouts) V2 (๊ฐœ๋ฐœ์„œ๋ฒ„: screen_layouts_v2) +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +- ์ปดํฌ๋„ŒํŠธ๋ณ„ 1๊ฐœ ๋ ˆ์ฝ”๋“œ - ํ™”๋ฉด๋‹น 1๊ฐœ ๋ ˆ์ฝ”๋“œ +- properties JSONB - layout_data JSONB +- component_type VARCHAR - url (์ปดํฌ๋„ŒํŠธ ๊ฒฝ๋กœ) +- menu_objid ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ/์นดํ…Œ๊ณ ๋ฆฌ - table_name + column_name ๊ธฐ๋ฐ˜ +``` + +### 2.2 V2 layout_data ๊ตฌ์กฐ + +```json +{ + "version": "2.0", + "components": [ + { + "id": "comp_xxx", + "url": "@/lib/registry/components/v2-table-list", + "position": { "x": 0, "y": 0 }, + "size": { "width": 100, "height": 50 }, + "displayOrder": 0, + "overrides": { + "tableName": "inspection_standard", + "columns": ["id", "name", "status"] + } + } + ], + "updatedAt": "2026-02-03T12:00:00Z" +} +``` + +### 2.3 ์ปดํฌ๋„ŒํŠธ URL ๋งคํ•‘ + +```typescript +const V1_TO_V2_URL_MAPPING = { + 'table-list': '@/lib/registry/components/v2-table-list', + 'button-primary': '@/lib/registry/components/v2-button-primary', + 'text-input': '@/lib/registry/components/v2-input', + 'select-basic': '@/lib/registry/components/v2-select', + 'date-input': '@/lib/registry/components/v2-date', + 'entity-search-input': '@/lib/registry/components/v2-entity-search', + 'category-manager': '@/lib/registry/components/v2-category-manager', + 'numbering-rule': '@/lib/registry/components/v2-numbering-rule', + 'tabs-widget': '@/lib/registry/components/v2-tabs-widget', + 'split-panel-layout': '@/lib/registry/components/v2-split-panel-layout', +}; +``` + +--- + +## 3. ๋ฐ์ดํ„ฐ ํƒ€์ž… ๊ด€๋ฆฌ (V2) + +### 3.1 ํ•ต์‹ฌ ํ…Œ์ด๋ธ” ๊ด€๊ณ„ + +``` +table_type_columns (์ปฌ๋Ÿผ ํƒ€์ž… ์ •์˜) +โ”œโ”€โ”€ input_type = 'category' โ†’ category_values (table_name + column_name) +โ”œโ”€โ”€ input_type = 'numbering' โ†’ numbering_rules (detail_settings.numberingRuleId) +โ”œโ”€โ”€ input_type = 'entity' โ†’ ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ +โ””โ”€โ”€ input_type = 'text', 'number', 'date', etc. +``` + +### 3.2 category_values ์กฐํšŒ ์ฟผ๋ฆฌ + +```sql +-- ํŠน์ • ํ…Œ์ด๋ธ”.์ปฌ๋Ÿผ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ +SELECT value_id, value_code, value_label, parent_value_id, depth +FROM category_values +WHERE table_name = 'ํ…Œ์ด๋ธ”๋ช…' + AND column_name = '์ปฌ๋Ÿผ๋ช…' + AND company_code = 'COMPANY_7' +ORDER BY value_order; +``` + +### 3.3 numbering_rules ์—ฐ๊ฒฐ ๋ฐฉ์‹ + +```json +// table_type_columns.detail_settings +{ + "numberingRuleId": "rule-xxx" +} + +// numbering_rules์—์„œ ํ•ด๋‹น rule ์กฐํšŒ +SELECT * FROM numbering_rules WHERE rule_id = 'rule-xxx'; +``` + +--- + +## 4. V2 ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก (23๊ฐœ) + +### 4.1 ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| v2-input | ์ž…๋ ฅ | ํ…์ŠคํŠธ, ์ˆซ์ž, ๋น„๋ฐ€๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ | +| v2-select | ์„ ํƒ | ๋“œ๋กญ๋‹ค์šด, ๋ผ๋””์˜ค, ์ฒดํฌ๋ฐ•์Šค | +| v2-date | ๋‚ ์งœ | ๋‚ ์งœ, ์‹œ๊ฐ„, ๋‚ ์งœ๋ฒ”์œ„ | + +### 4.2 ํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| v2-text-display | ํ…์ŠคํŠธ ํ‘œ์‹œ | ๋ผ๋ฒจ, ์ œ๋ชฉ | +| v2-card-display | ์นด๋“œ ๋””์Šคํ”Œ๋ ˆ์ด | ์นด๋“œ ํ˜•ํƒœ ๋ฐ์ดํ„ฐ | +| v2-aggregation-widget | ์ง‘๊ณ„ ์œ„์ ฏ | ํ•ฉ๊ณ„, ํ‰๊ท , ๊ฐœ์ˆ˜ | + +### 4.3 ํ…Œ์ด๋ธ”/๋ฐ์ดํ„ฐ ์ปดํฌ๋„ŒํŠธ + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| v2-table-list | ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | ๋ฐ์ดํ„ฐ ๊ทธ๋ฆฌ๋“œ | +| v2-table-search-widget | ๊ฒ€์ƒ‰ ํ•„ํ„ฐ | ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰ | +| v2-pivot-grid | ํ”ผ๋ฒ— ๊ทธ๋ฆฌ๋“œ | ๋‹ค์ฐจ์› ๋ถ„์„ | +| v2-table-grouped | ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” | ๊ทธ๋ฃน๋ณ„ ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ | + +### 4.4 ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| v2-split-panel-layout | ๋ถ„ํ•  ํŒจ๋„ | ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ | +| v2-tabs-widget | ํƒญ ์œ„์ ฏ | ํƒญ ์ „ํ™˜ | +| v2-section-card | ์„น์…˜ ์นด๋“œ | ์ œ๋ชฉ+ํ…Œ๋‘๋ฆฌ ๊ทธ๋ฃน | +| v2-section-paper | ์„น์…˜ ํŽ˜์ดํผ | ๋ฐฐ๊ฒฝ์ƒ‰ ๊ทธ๋ฃน | +| v2-divider-line | ๊ตฌ๋ถ„์„  | ์˜์—ญ ๊ตฌ๋ถ„ | +| v2-repeat-container | ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ | ๋ฐ์ดํ„ฐ ๋ฐ˜๋ณต | +| v2-unified-repeater | ํ†ตํ•ฉ ๋ฆฌํ”ผํ„ฐ | ์ธ๋ผ์ธ/๋ชจ๋‹ฌ/๋ฒ„ํŠผ | + +### 4.5 ์•ก์…˜/ํŠน์ˆ˜ ์ปดํฌ๋„ŒํŠธ + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| v2-button-primary | ๊ธฐ๋ณธ ๋ฒ„ํŠผ | ์ €์žฅ, ์‚ญ์ œ ๋“ฑ | +| v2-numbering-rule | ์ฑ„๋ฒˆ ๊ทœ์น™ | ์ž๋™ ์ฝ”๋“œ ์ƒ์„ฑ | +| v2-category-manager | ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ์ž | ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ | +| v2-location-swap-selector | ์œ„์น˜ ๊ตํ™˜ | ์œ„์น˜ ์„ ํƒ | +| v2-rack-structure | ๋ž™ ๊ตฌ์กฐ | ์ฐฝ๊ณ  ๋ž™ ์‹œ๊ฐํ™” | + +--- + +## 5. ํ™”๋ฉด ํŒจํ„ด (5๊ฐ€์ง€) + +### 5.1 ํŒจํ„ด A: ๊ธฐ๋ณธ ๋งˆ์Šคํ„ฐ ํ™”๋ฉด + +``` +์‚ฌ์šฉ ์กฐ๊ฑด: ๋‹จ์ผ ํ…Œ์ด๋ธ” CRUD + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-table-search-widget โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ v2-table-list โ”‚ +โ”‚ [์‹ ๊ทœ] [์‚ญ์ œ] v2-button-primary โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 5.2 ํŒจํ„ด B: ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ํ™”๋ฉด + +``` +์‚ฌ์šฉ ์กฐ๊ฑด: ๋งˆ์Šคํ„ฐ ์„ ํƒ โ†’ ๋””ํ…Œ์ผ ํ‘œ์‹œ + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๋งˆ์Šคํ„ฐ ๋ฆฌ์ŠคํŠธ โ”‚ ๋””ํ…Œ์ผ ๋ฆฌ์ŠคํŠธ โ”‚ +โ”‚ v2-table-list โ”‚ v2-table-list โ”‚ +โ”‚ โ”‚ (relation: foreignKey) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + v2-split-panel-layout +``` + +**ํ•„์ˆ˜ ์„ค์ •:** +```json +{ + "leftPanel": { "tableName": "master_table" }, + "rightPanel": { + "tableName": "detail_table", + "relation": { "type": "detail", "foreignKey": "master_id" } + }, + "splitRatio": 30 +} +``` + +### 5.3 ํŒจํ„ด C: ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ + ํƒญ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๋งˆ์Šคํ„ฐ ๋ฆฌ์ŠคํŠธ โ”‚ v2-tabs-widget โ”‚ +โ”‚ v2-table-list โ”‚ โ”œโ”€ ํƒญ1: v2-table-list โ”‚ +โ”‚ โ”‚ โ”œโ”€ ํƒญ2: v2-table-list โ”‚ +โ”‚ โ”‚ โ””โ”€ ํƒญ3: ํผ ์ปดํฌ๋„ŒํŠธ๋“ค โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + v2-split-panel-layout +``` + +--- + +## 6. ๋ชจ๋‹ฌ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๋ณ€๊ฒฝ + +### 6.1 V1 (๋ณธ์„œ๋ฒ„) + +``` +ํ™”๋ฉด A (screen_id: 142) - ๊ฒ€์‚ฌ์žฅ๋น„๊ด€๋ฆฌ + โ””โ”€โ”€ ๋ฒ„ํŠผ ํด๋ฆญ โ†’ ํ™”๋ฉด B (screen_id: 143) - ๊ฒ€์‚ฌ์žฅ๋น„ ๋“ฑ๋ก๋ชจ๋‹ฌ (๋ณ„๋„ screen_id) +``` + +### 6.2 V2 (๊ฐœ๋ฐœ์„œ๋ฒ„) + +``` +ํ™”๋ฉด A (screen_id: 142) - ๊ฒ€์‚ฌ์žฅ๋น„๊ด€๋ฆฌ + โ””โ”€โ”€ layout_data.components[] ๋‚ด์— v2-dialog-form ๋˜๋Š” overlay ํฌํ•จ +``` + +**ํ•ต์‹ฌ**: V2์—์„œ๋Š” ๋ชจ๋‹ฌ์„ ๋ณ„๋„ ํ™”๋ฉด์ด ์•„๋‹Œ, ๋ถ€๋ชจ ํ™”๋ฉด์˜ ์ปดํฌ๋„ŒํŠธ๋กœ ํ†ตํ•ฉ + +--- + +## 7. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ˆ์ฐจ (Step by Step) + +### Step 1: ์‚ฌ์ „ ๋ถ„์„ + +```sql +-- ๋ณธ์„œ๋ฒ„ ํ™”๋ฉด ๋ชฉ๋ก ํ™•์ธ +SELECT sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name, + COUNT(sl.layout_id) as component_count +FROM screen_definitions sd +LEFT JOIN screen_layouts sl ON sd.screen_id = sl.screen_id +WHERE sd.screen_code LIKE 'COMPANY_7_%' +GROUP BY sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name; + +-- ๊ฐœ๋ฐœ์„œ๋ฒ„ V2 ํ˜„ํ™ฉ ํ™•์ธ +SELECT sd.screen_id, sd.screen_code, sd.screen_name, + sv2.layout_data IS NOT NULL as has_v2_layout +FROM screen_definitions sd +LEFT JOIN screen_layouts_v2 sv2 ON sd.screen_id = sv2.screen_id +WHERE sd.company_code = 'COMPANY_7'; +``` + +### Step 2: table_type_columns ํ™•์ธ + +```sql +-- ํ•ด๋‹น ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ํƒ€์ž… ํ™•์ธ +SELECT column_name, column_label, input_type, detail_settings +FROM table_type_columns +WHERE table_name = '๋Œ€์ƒํ…Œ์ด๋ธ”๋ช…' + AND company_code = 'COMPANY_7'; +``` + +### Step 3: V2 layout_data ์ƒ์„ฑ + +```json +{ + "version": "2.0", + "components": [ + { + "id": "์ƒ์„ฑ๋œID", + "url": "@/lib/registry/components/v2-์ปดํฌ๋„ŒํŠธํƒ€์ž…", + "position": { "x": 0, "y": 0 }, + "size": { "width": 100, "height": 50 }, + "displayOrder": 0, + "overrides": { + "tableName": "ํ…Œ์ด๋ธ”๋ช…", + "fieldName": "์ปฌ๋Ÿผ๋ช…" + } + } + ], + "migratedFrom": "V1", + "migratedAt": "2026-02-03T00:00:00Z" +} +``` + +### Step 4: screen_layouts_v2 INSERT + +```sql +INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data) +VALUES ($1, $2, $3::jsonb) +ON CONFLICT (screen_id, company_code) +DO UPDATE SET layout_data = $3::jsonb, updated_at = NOW(); +``` + +### Step 5: ๊ฒ€์ฆ + +- [ ] ํ™”๋ฉด ๋ Œ๋”๋ง ํ™•์ธ (component๊ฐ€ ์•„๋‹Œ v2-xxx๋กœ ํ‘œ์‹œ๋˜๋Š”์ง€) +- [ ] ์ปดํฌ๋„ŒํŠธ๋ณ„ ํ…Œ์ด๋ธ”-์ปฌ๋Ÿผ ์—ฐ๊ฒฐ ํ™•์ธ +- [ ] ์นดํ…Œ๊ณ ๋ฆฌ ๋“œ๋กญ๋‹ค์šด ๋™์ž‘ ํ™•์ธ +- [ ] ์ฑ„๋ฒˆ ๊ทœ์น™ ๋™์ž‘ ํ™•์ธ +- [ ] ์ €์žฅ/์ˆ˜์ •/์‚ญ์ œ ํ…Œ์ŠคํŠธ + +--- + +## 8. ํ’ˆ์งˆ๊ด€๋ฆฌ ๋ฉ”๋‰ด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ˜„ํ™ฉ + +| ๋ณธ์„œ๋ฒ„ ์ฝ”๋“œ | ํ™”๋ฉด๋ช… | ํ…Œ์ด๋ธ” | ์ƒํƒœ | ๋น„๊ณ  | +|-------------|--------|--------|------|------| +| COMPANY_7_126 | ๊ฒ€์‚ฌ์ •๋ณด ๊ด€๋ฆฌ | inspection_standard | โœ… V2 ์กด์žฌ | ์ปดํฌ๋„ŒํŠธ ๊ฒ€์ฆ ํ•„์š” | +| COMPANY_7_127 | ํ’ˆ๋ชฉ์˜ต์…˜ ์„ค์ • | - | โœ… V2 ์กด์žฌ | v2-category-manager | +| COMPANY_7_138 | ์นดํ…Œ๊ณ ๋ฆฌ ์„ค์ • | inspection_standard | โŒ ๋ˆ„๋ฝ | table_name ๊ธฐ๋ฐ˜ | +| COMPANY_7_139 | ์ฝ”๋“œ ์„ค์ • | inspection_standard | โŒ ๋ˆ„๋ฝ | table_name ๊ธฐ๋ฐ˜ | +| COMPANY_7_142 | ๊ฒ€์‚ฌ์žฅ๋น„ ๊ด€๋ฆฌ | inspection_equipment_mng | โŒ ๋ˆ„๋ฝ | ๋ชจ๋‹ฌ ํ†ตํ•ฉ | +| COMPANY_7_143 | ๊ฒ€์‚ฌ์žฅ๋น„ ๋“ฑ๋ก๋ชจ๋‹ฌ | inspection_equipment_mng | โŒ ๋ˆ„๋ฝ | โ†’ 142 ํ†ตํ•ฉ | +| COMPANY_7_144 | ๋ถˆ๋Ÿ‰๊ธฐ์ค€ ์ •๋ณด | defect_standard_mng | โŒ ๋ˆ„๋ฝ | ๋ชจ๋‹ฌ ํ†ตํ•ฉ | +| COMPANY_7_145 | ๋ถˆ๋Ÿ‰๊ธฐ์ค€ ๋“ฑ๋ก๋ชจ๋‹ฌ | defect_standard_mng | โŒ ๋ˆ„๋ฝ | โ†’ 144 ํ†ตํ•ฉ | + +--- + +## 9. ๊ด€๋ จ ์ฝ”๋“œ ํŒŒ์ผ ๊ฒฝ๋กœ + +| ํ•ญ๋ชฉ | ๊ฒฝ๋กœ | +|------|------| +| V2 ์ปดํฌ๋„ŒํŠธ ํด๋” | `frontend/lib/registry/components/v2-xxx/` | +| ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก | `frontend/lib/registry/components/index.ts` | +| ์นดํ…Œ๊ณ ๋ฆฌ ์„œ๋น„์Šค | `backend-node/src/services/categoryTreeService.ts` | +| ์ฑ„๋ฒˆ ์„œ๋น„์Šค | `backend-node/src/services/numberingRuleService.ts` | +| ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ API | `frontend/lib/api/entityJoin.ts` | +| ํผ ํ˜ธํ™˜์„ฑ ํ›… | `frontend/hooks/useFormCompatibility.ts` | + +--- + +## 10. ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง ๊ฒƒ + +1. โŒ **ํ…Œ์ด๋ธ”-์ปฌ๋Ÿผ ์—ฐ๊ฒฐ ์—†์ด ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜** โ†’ "component"๋กœ ํ‘œ์‹œ๋จ +2. โŒ **menu_objid ๊ธฐ๋ฐ˜ ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ ์‚ฌ์šฉ** โ†’ V2๋Š” table_name + column_name ๊ธฐ๋ฐ˜ +3. โŒ **๋ชจ๋‹ฌ์„ ๋ณ„๋„ screen_id๋กœ ์ƒ์„ฑ** โ†’ V2๋Š” ๋ถ€๋ชจ ํ™”๋ฉด์— ํ†ตํ•ฉ +4. โŒ **V1 ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ์‚ฌ์šฉ** โ†’ ๋ฐ˜๋“œ์‹œ v2- ์ ‘๋‘์‚ฌ ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ +5. โŒ **company_code ํ•„ํ„ฐ๋ง ๋ˆ„๋ฝ** โ†’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„์ˆ˜ + +--- + +## 11. ๋ชจ๋ฅด๋ฉด ํ™•์ธํ•  ๊ณณ + +1. **์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ**: `docs/V2_์ปดํฌ๋„ŒํŠธ_๋ถ„์„_๊ฐ€์ด๋“œ.md` +2. **ํ™”๋ฉด ๊ฐœ๋ฐœ ํ‘œ์ค€**: `docs/screen-implementation-guide/ํ™”๋ฉด๊ฐœ๋ฐœ_ํ‘œ์ค€_๊ฐ€์ด๋“œ.md` +3. **๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ˆ์ฐจ**: `docs/DDD1542/๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์ƒ์„ธ๊ฐ€์ด๋“œ.md` +4. **ํƒ‘์‹ค ๋””์ž์ธ ๋ช…์„ธ**: `/Users/gbpark/Downloads/ํ™”๋ฉด๊ฐœ๋ฐœ 8/` +5. **์‹ค์ œ ์ฝ”๋“œ**: ์œ„ ๊ฒฝ๋กœ์˜ ์†Œ์Šค ํŒŒ์ผ๋“ค + +--- + +## 12. ์™•์˜ ํ›ˆ๊ณ„ + +> **"ํ•ญ์ƒ ์• ๋งคํ•œ ๊ฑฐ๋Š” mdํŒŒ์ผ ๋ณด๊ฑฐ๋‚˜ ๋ฌผ์–ด๋ณผ ๊ฒƒ. ์ฝ”๋“œ์—๋Š” ์ „๋ถ€ ์ •๋‹ต์ด ์žˆ์Œ. ๋งŒ์•ฝ ๋ชจ๋ฅธ๋‹ค๋ฉด ๋„ˆ ์ž˜๋ชป. ์‹ค์ˆ˜ํ•ด๋„ ๋„ˆ ์ž˜๋ชป."** + +--- + +## ๋ณ€๊ฒฝ ์ด๋ ฅ + +| ๋‚ ์งœ | ์ž‘์„ฑ์ž | ๋‚ด์šฉ | +|------|--------|------| +| 2026-02-03 | DDD1542 | ์ดˆ์•ˆ ์ž‘์„ฑ (๋ฌธ์„œ 4๊ฐœ ์ •๋… ํ›„) | diff --git a/docs/DDD1542/๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_๊ฐ€์ด๋“œ.md b/docs/DDD1542/๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_๊ฐ€์ด๋“œ.md new file mode 100644 index 00000000..b7a0e353 --- /dev/null +++ b/docs/DDD1542/๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_๊ฐ€์ด๋“œ.md @@ -0,0 +1,453 @@ +# ๋ณธ์„œ๋ฒ„ โ†’ ๊ฐœ๋ฐœ์„œ๋ฒ„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ (๊ณต์šฉ) + +> **์ด ๋ฌธ์„œ๋Š” ๋‹ค์Œ AI ์—์ด์ „ํŠธ๊ฐ€ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ž‘์—…์„ ์ด์–ด๋ฐ›์„ ๋•Œ ์ฐธ๊ณ ํ•˜๋Š” ํ•ต์‹ฌ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค.** + +--- + +## ๋น ๋ฅธ ์‹œ์ž‘ + +### ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ฐฉํ–ฅ (์ ˆ๋Œ€ ์žŠ์ง€ ๋ง ๊ฒƒ) + +``` +๋ณธ์„œ๋ฒ„ (Production) โ†’ ๊ฐœ๋ฐœ์„œ๋ฒ„ (Development) +211.115.91.141:11134 39.117.244.52:11132 +screen_layouts (V1) screen_layouts_v2 (V2) +``` + +**๋ฐ˜๋Œ€๋กœ ํ•˜๋ฉด ์•ˆ ๋จ!** ๊ฐœ๋ฐœ์„œ๋ฒ„ ์™„์„ฑ ํ›„ โ†’ ๋ณธ์„œ๋ฒ„๋กœ ๋ฐฐํฌ ์˜ˆ์ • + +### DB ์ ‘์† ์ •๋ณด + +```bash +# ๋ณธ์„œ๋ฒ„ (Production) +docker exec pms-backend-mac node -e ' +const { Pool } = require("pg"); +const pool = new Pool({ + connectionString: "postgresql://postgres:vexplor0909!!@211.115.91.141:11134/plm?sslmode=disable", + ssl: false +}); +// ์ฟผ๋ฆฌ ์‹คํ–‰ +' + +# ๊ฐœ๋ฐœ์„œ๋ฒ„ (Development) +docker exec pms-backend-mac node -e ' +const { Pool } = require("pg"); +const pool = new Pool({ + connectionString: "postgresql://postgres:ph0909!!@39.117.244.52:11132/plm?sslmode=disable", + ssl: false +}); +// ์ฟผ๋ฆฌ ์‹คํ–‰ +' +``` + +--- + +## ์ ˆ๋Œ€ ์ฃผ์˜: ์ปดํฌ๋„ŒํŠธ-์ปฌ๋Ÿผ ์—ฐ๊ฒฐ (์ด์ „ ์‹คํŒจ ์›์ธ) + +### "component" vs "v2-input" ๊ตฌ๋ถ„ + +``` +โŒ ์ž˜๋ชป๋œ ์ƒํƒœ โœ… ์˜ฌ๋ฐ”๋ฅธ ์ƒํƒœ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ component โ”‚ โ”‚ v2-input โ”‚ +โ”‚ ์—…์ฒด์ฝ”๋“œ โ”‚ โ”‚ ์—…์ฒด์ฝ”๋“œ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†‘ โ†‘ + overrides.type ์—†์Œ overrides.type = "v2-input" +``` + +**ํ•ต์‹ฌ ์›์ธ**: ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ทธ๋ƒฅ ๋ฐฐ์น˜ํ•˜๋ฉด "component"๋กœ ํ‘œ์‹œ๋จ. ๋ฐ˜๋“œ์‹œ ์™ผ์ชฝ ํŒจ๋„์—์„œ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์„ **๋“œ๋ž˜๊ทธ**ํ•ด์•ผ ์˜ฌ๋ฐ”๋ฅธ v2-xxx ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ƒ์„ฑ๋จ. + +### ๐Ÿ”ฅ ํ•ต์‹ฌ ๋ฐœ๊ฒฌ: overrides.type ํ•„์ˆ˜ (2026-02-04 ๋ฐœ๊ฒฌ) + +**"component"๋กœ ํ‘œ์‹œ๋˜๋Š” ๊ทผ๋ณธ ์›์ธ:** + +| ํ•ญ๋ชฉ | ๋“œ๋ž˜๊ทธ๋กœ ๋ฐฐ์น˜ | ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (์ž˜๋ชป๋œ) | +|------|---------------|----------------------| +| `overrides.type` | **"v2-input"** โœ… | **์—†์Œ** โŒ | +| `overrides.webType` | "text" ๋“ฑ | ์—†์Œ | +| `overrides.tableName` | "carrier_mng" ๋“ฑ | ์—†์Œ | + +**ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ์ปดํฌ๋„ŒํŠธ ํƒ€์ž…์„ ์ธ์‹ํ•˜๋Š” ๋ฐฉ๋ฒ•:** +1. `overrides.type` ํ™•์ธ โ†’ ์žˆ์œผ๋ฉด ํ•ด๋‹น ๊ฐ’ ์‚ฌ์šฉ (์˜ˆ: "v2-input") +2. ์—†์œผ๋ฉด โ†’ ๊ธฐ๋ณธ๊ฐ’ "component"๋กœ ํด๋ฐฑ + +**๊ฒฐ๋ก **: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ `overrides.type` ํ•„๋“œ๋ฅผ ๋ฐ˜๋“œ์‹œ ์„ค์ •ํ•ด์•ผ ํ•จ! + +### input_type โ†’ V2 ์ปดํฌ๋„ŒํŠธ ์ž๋™ ๋งคํ•‘ + +| table_type_columns.input_type | ๋“œ๋ž˜๊ทธ ์‹œ ์ƒ์„ฑ๋˜๋Š” V2 ์ปดํฌ๋„ŒํŠธ | +|-------------------------------|-------------------------------| +| text | v2-input | +| number | v2-input (type=number) | +| date | v2-date | +| category | v2-select (category_values ์—ฐ๋™) | +| numbering | v2-numbering-rule ๋˜๋Š” v2-input | +| entity | v2-entity-search | + +**์ ˆ๋Œ€ ๊ทœ์น™**: ์ปดํฌ๋„ŒํŠธ๊ฐ€ "component"๋กœ ํ‘œ์‹œ๋˜๋ฉด ์—ฐ๊ฒฐ ์‹คํŒจ ์ƒํƒœ. ๋ฐ˜๋“œ์‹œ "v2-xxx"๋กœ ํ‘œ์‹œ๋˜์–ด์•ผ ํ•จ. + +--- + +## ํ•ต์‹ฌ ๊ฐœ๋… + +### V1 vs V2 ๊ตฌ์กฐ ์ฐจ์ด + +| ๊ตฌ๋ถ„ | V1 (๋ณธ์„œ๋ฒ„) | V2 (๊ฐœ๋ฐœ์„œ๋ฒ„) | +|------|-------------|---------------| +| ํ…Œ์ด๋ธ” | screen_layouts | screen_layouts_v2 | +| ๋ ˆ์ฝ”๋“œ | ์ปดํฌ๋„ŒํŠธ๋ณ„ 1๊ฐœ | ํ™”๋ฉด๋‹น 1๊ฐœ | +| ์„ค์ • ์ €์žฅ | properties JSONB | layout_data.components[].overrides | +| ์ฑ„๋ฒˆ/์นดํ…Œ๊ณ ๋ฆฌ | menu_objid ๊ธฐ๋ฐ˜ | table_name + column_name ๊ธฐ๋ฐ˜ | +| ์ปดํฌ๋„ŒํŠธ ์ฐธ์กฐ | component_type ๋ฌธ์ž์—ด | url ๊ฒฝ๋กœ (@/lib/registry/...) | + +### ๋ฐ์ดํ„ฐ ํƒ€์ž… ๊ด€๋ฆฌ (V2) + +``` +table_type_columns (input_type) +โ”œโ”€โ”€ 'category' โ†’ category_values ํ…Œ์ด๋ธ” +โ”œโ”€โ”€ 'numbering' โ†’ numbering_rules ํ…Œ์ด๋ธ” (detail_settings.numberingRuleId) +โ”œโ”€โ”€ 'entity' โ†’ ์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰ +โ””โ”€โ”€ 'text', 'number', 'date', etc. +``` + +### ์ปดํฌ๋„ŒํŠธ URL ๋งคํ•‘ + +```typescript +const V1_TO_V2_MAPPING = { + 'table-list': '@/lib/registry/components/v2-table-list', + 'button-primary': '@/lib/registry/components/v2-button-primary', + 'text-input': '@/lib/registry/components/v2-text-input', + 'select-basic': '@/lib/registry/components/v2-select', + 'date-input': '@/lib/registry/components/v2-date-input', + 'entity-search-input': '@/lib/registry/components/v2-entity-search', + 'category-manager': '@/lib/registry/components/v2-category-manager', + 'numbering-rule': '@/lib/registry/components/v2-numbering-rule', + 'tabs-widget': '@/lib/registry/components/v2-tabs-widget', + 'textarea-basic': '@/lib/registry/components/v2-textarea', +}; +``` + +### ๋ชจ๋‹ฌ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๋ณ€๊ฒฝ + +- **V1**: ๋ณ„๋„ ํ™”๋ฉด(screen_id)์œผ๋กœ ๋ชจ๋‹ฌ ๊ด€๋ฆฌ +- **V2**: ๋ถ€๋ชจ ํ™”๋ฉด์— overlay/dialog ์ปดํฌ๋„ŒํŠธ๋กœ ํ†ตํ•ฉ + +--- + +## ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋Œ€์ƒ ๋ฉ”๋‰ด ํ˜„ํ™ฉ + +### ํ’ˆ์งˆ๊ด€๋ฆฌ (์šฐ์„ ์ˆœ์œ„ 1) + +| ๋ณธ์„œ๋ฒ„ ์ฝ”๋“œ | ํ™”๋ฉด๋ช… | ์ƒํƒœ | ๋น„๊ณ  | +|-------------|--------|------|------| +| COMPANY_7_126 | ๊ฒ€์‚ฌ์ •๋ณด ๊ด€๋ฆฌ | โœ… V2 ์กด์žฌ | ์ปดํฌ๋„ŒํŠธ ๊ฒ€์ฆ ํ•„์š” | +| COMPANY_7_127 | ํ’ˆ๋ชฉ์˜ต์…˜ ์„ค์ • | โœ… V2 ์กด์žฌ | v2-category-manager ์‚ฌ์šฉ์ค‘ | +| COMPANY_7_138 | ์นดํ…Œ๊ณ ๋ฆฌ ์„ค์ • | โŒ ๋ˆ„๋ฝ | table_name ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ | +| COMPANY_7_139 | ์ฝ”๋“œ ์„ค์ • | โŒ ๋ˆ„๋ฝ | table_name ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ | +| COMPANY_7_142 | ๊ฒ€์‚ฌ์žฅ๋น„ ๊ด€๋ฆฌ | โŒ ๋ˆ„๋ฝ | ๋ชจ๋‹ฌ ํ†ตํ•ฉ ํ•„์š” | +| COMPANY_7_143 | ๊ฒ€์‚ฌ์žฅ๋น„ ๋“ฑ๋ก๋ชจ๋‹ฌ | โŒ ๋ˆ„๋ฝ | โ†’ 142์— ํ†ตํ•ฉ | +| COMPANY_7_144 | ๋ถˆ๋Ÿ‰๊ธฐ์ค€ ์ •๋ณด | โŒ ๋ˆ„๋ฝ | ๋ชจ๋‹ฌ ํ†ตํ•ฉ ํ•„์š” | +| COMPANY_7_145 | ๋ถˆ๋Ÿ‰๊ธฐ์ค€ ๋“ฑ๋ก๋ชจ๋‹ฌ | โŒ ๋ˆ„๋ฝ | โ†’ 144์— ํ†ตํ•ฉ | + +### ๋‹ค์Œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋Œ€์ƒ (๋ฏธ์ •) + +- [ ] ๋ฌผ๋ฅ˜๊ด€๋ฆฌ +- [ ] ์ƒ์‚ฐ๊ด€๋ฆฌ +- [ ] ์˜์—…๊ด€๋ฆฌ +- [ ] ๊ธฐํƒ€ ๋ฉ”๋‰ด๋“ค + +--- + +## ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ž‘์—… ์ ˆ์ฐจ + +### Step 1: ๋ถ„์„ + +```sql +-- ๋ณธ์„œ๋ฒ„ ํŠน์ • ๋ฉ”๋‰ด ํ™”๋ฉด ๋ชฉ๋ก ์กฐํšŒ +SELECT + sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name, + COUNT(sl.layout_id) as component_count +FROM screen_definitions sd +LEFT JOIN screen_layouts sl ON sd.screen_id = sl.screen_id +WHERE sd.screen_name LIKE '%[๋ฉ”๋‰ด๋ช…]%' + AND sd.company_code = 'COMPANY_7' +GROUP BY sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name; + +-- ๊ฐœ๋ฐœ์„œ๋ฒ„ V2 ํ˜„ํ™ฉ ํ™•์ธ +SELECT + sd.screen_id, sd.screen_code, sd.screen_name, + sv2.layout_id IS NOT NULL as has_v2 +FROM screen_definitions sd +LEFT JOIN screen_layouts_v2 sv2 ON sd.screen_id = sv2.screen_id +WHERE sd.company_code = 'COMPANY_7'; +``` + +### Step 2: screen_definitions ๋™๊ธฐํ™” + +๋ณธ์„œ๋ฒ„์—๋งŒ ์žˆ๋Š” ํ™”๋ฉด์„ ๊ฐœ๋ฐœ์„œ๋ฒ„์— ์ถ”๊ฐ€ + +### Step 3: V1 โ†’ V2 ๋ ˆ์ด์•„์›ƒ ๋ณ€ํ™˜ + +```typescript +// layout_data ๊ตฌ์กฐ +{ + "version": "2.0", + "components": [ + { + "id": "comp_xxx", + "url": "@/lib/registry/components/v2-table-list", + "position": { "x": 0, "y": 0 }, + "size": { "width": 100, "height": 50 }, + "displayOrder": 0, + "overrides": { + "tableName": "ํ…Œ์ด๋ธ”๋ช…", + "columns": ["์ปฌ๋Ÿผ1", "์ปฌ๋Ÿผ2"] + } + } + ] +} +``` + +### Step 4: ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ์ดํ„ฐ ํ™•์ธ/์ƒ์„ฑ + +```sql +-- ํ…Œ์ด๋ธ”์˜ category ์ปฌ๋Ÿผ ํ™•์ธ +SELECT column_name, column_label +FROM table_type_columns +WHERE table_name = '[ํ…Œ์ด๋ธ”๋ช…]' + AND input_type = 'category'; + +-- category_values ๋ฐ์ดํ„ฐ ํ™•์ธ +SELECT value_id, value_code, value_label +FROM category_values +WHERE table_name = '[ํ…Œ์ด๋ธ”๋ช…]' + AND column_name = '[์ปฌ๋Ÿผ๋ช…]' + AND company_code = 'COMPANY_7'; +``` + +### Step 5: ์ฑ„๋ฒˆ ๊ทœ์น™ ํ™•์ธ/์ƒ์„ฑ + +```sql +-- numbering ์ปฌ๋Ÿผ ํ™•์ธ +SELECT column_name, column_label, detail_settings +FROM table_type_columns +WHERE table_name = '[ํ…Œ์ด๋ธ”๋ช…]' + AND input_type = 'numbering'; + +-- numbering_rules ๋ฐ์ดํ„ฐ ํ™•์ธ +SELECT rule_id, rule_name, table_name, column_name +FROM numbering_rules +WHERE company_code = 'COMPANY_7'; +``` + +### Step 6: ๊ฒ€์ฆ + +- [ ] ํ™”๋ฉด ๋ Œ๋”๋ง ํ™•์ธ +- [ ] ์ปดํฌ๋„ŒํŠธ ๋™์ž‘ ํ™•์ธ +- [ ] ์ €์žฅ/์ˆ˜์ •/์‚ญ์ œ ํ…Œ์ŠคํŠธ +- [ ] ์นดํ…Œ๊ณ ๋ฆฌ ๋“œ๋กญ๋‹ค์šด ๋™์ž‘ +- [ ] ์ฑ„๋ฒˆ ๊ทœ์น™ ๋™์ž‘ + +--- + +## ํ•ต์‹ฌ ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ + +### screen_layouts_v2 + +```sql +CREATE TABLE screen_layouts_v2 ( + layout_id SERIAL PRIMARY KEY, + screen_id INTEGER NOT NULL, + company_code VARCHAR(20) NOT NULL, + layout_data JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(screen_id, company_code) +); +``` + +### category_values + +```sql +-- ํ•ต์‹ฌ ์ปฌ๋Ÿผ +value_id, table_name, column_name, value_code, value_label, +parent_value_id, depth, path, company_code +``` + +### numbering_rules + numbering_rule_parts + +```sql +-- numbering_rules ํ•ต์‹ฌ ์ปฌ๋Ÿผ +rule_id, rule_name, table_name, column_name, separator, +reset_period, current_sequence, company_code + +-- numbering_rule_parts ํ•ต์‹ฌ ์ปฌ๋Ÿผ +rule_id, part_order, part_type, generation_method, +auto_config, manual_config, company_code +``` + +### table_type_columns + +```sql +-- ํ•ต์‹ฌ ์ปฌ๋Ÿผ +table_name, column_name, input_type, column_label, +detail_settings, company_code +``` + +--- + +## ์ฐธ๊ณ  ๋ฌธ์„œ + +### ํ•„์ˆ˜ ์ฝ๊ธฐ + +1. **[๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์ƒ์„ธ๊ฐ€์ด๋“œ.md](./๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์ƒ์„ธ๊ฐ€์ด๋“œ.md)** - ์ƒ์„ธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ˆ์ฐจ +2. **[ํ™”๋ฉด๊ฐœ๋ฐœ_ํ‘œ์ค€_๊ฐ€์ด๋“œ.md](../screen-implementation-guide/ํ™”๋ฉด๊ฐœ๋ฐœ_ํ‘œ์ค€_๊ฐ€์ด๋“œ.md)** - V2 ํ™”๋ฉด ๊ฐœ๋ฐœ ํ‘œ์ค€ +3. **[SCREEN_DEVELOPMENT_STANDARD.md](../screen-implementation-guide/SCREEN_DEVELOPMENT_STANDARD.md)** - ์˜๋ฌธ ํ‘œ์ค€ ๊ฐ€์ด๋“œ + +### ์ฝ”๋“œ ์ฐธ์กฐ + +| ํŒŒ์ผ | ์„ค๋ช… | +|------|------| +| `backend-node/src/services/categoryTreeService.ts` | ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ ์„œ๋น„์Šค | +| `backend-node/src/services/numberingRuleService.ts` | ์ฑ„๋ฒˆ ๊ทœ์น™ ์„œ๋น„์Šค | +| `frontend/lib/registry/components/v2-category-manager/` | V2 ์นดํ…Œ๊ณ ๋ฆฌ ์ปดํฌ๋„ŒํŠธ | +| `frontend/lib/registry/components/v2-numbering-rule/` | V2 ์ฑ„๋ฒˆ ์ปดํฌ๋„ŒํŠธ | + +### ๊ด€๋ จ ๋ฌธ์„œ + +- `docs/V2_์ปดํฌ๋„ŒํŠธ_๋ถ„์„_๊ฐ€์ด๋“œ.md` +- `docs/V2_์ปดํฌ๋„ŒํŠธ_์—ฐ๋™_๊ฐ€์ด๋“œ.md` +- `docs/DDD1542/COMPONENT_LAYOUT_V2_ARCHITECTURE.md` +- `docs/DDD1542/COMPONENT_MIGRATION_PLAN.md` + +--- + +## ์ฃผ์˜์‚ฌํ•ญ + +### ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง ๊ฒƒ + +1. **๊ฐœ๋ฐœ์„œ๋ฒ„ โ†’ ๋ณธ์„œ๋ฒ„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜** (๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ) +2. **๋ณธ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ์ง์ ‘ ์ˆ˜์ •** (SELECT๋งŒ ํ—ˆ์šฉ) +3. **company_code ๋ˆ„๋ฝ** (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„์ˆ˜) +4. **ํ…Œ์ด๋ธ”-์ปฌ๋Ÿผ ์—ฐ๊ฒฐ ์—†์ด ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜** ("component"๋กœ ํ‘œ์‹œ๋˜๋ฉด ์‹คํŒจ) +5. **menu_objid ๊ธฐ๋ฐ˜ ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ ์‚ฌ์šฉ** (V2๋Š” table_name + column_name ๊ธฐ๋ฐ˜) + +### ๋ฐ˜๋“œ์‹œ ํ•  ๊ฒƒ + +1. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „ **๊ฐœ๋ฐœ์„œ๋ฒ„ ๋ฐฑ์—…** +2. ์ปดํฌ๋„ŒํŠธ ๋ณ€ํ™˜ ์‹œ **V2 ์ปดํฌ๋„ŒํŠธ๋งŒ ์‚ฌ์šฉ** (v2- prefix) +3. ๋ชจ๋‹ฌ ํ™”๋ฉด์€ **๋ถ€๋ชจ ํ™”๋ฉด์— ํ†ตํ•ฉ** +4. ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ์€ **table_name + column_name ๊ธฐ๋ฐ˜** +5. ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ ํ›„ **"v2-xxx"๋กœ ํ‘œ์‹œ๋˜๋Š”์ง€ ๋ฐ˜๋“œ์‹œ ํ™•์ธ** + +### ์‹คํŒจ ์‚ฌ๋ก€ (์ด์ „ ์ž‘์—…์ž) + +**๋ฌผ๋ฅ˜์ •๋ณด๊ด€๋ฆฌ โ†’ ์šด์†ก์—…์ฒด ๊ด€๋ฆฌ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํŒจ** + +- **์›์ธ**: ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ๋ฐฐ์น˜ํ•˜์—ฌ "component"๋กœ ์ƒ์„ฑ๋จ +- **์ฆ์ƒ**: ํ™”๋ฉด์— "component" ๋ผ๋ฒจ ํ‘œ์‹œ, ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ์‹คํŒจ +- **ํ•ด๊ฒฐ**: ์™ผ์ชฝ ํŒจ๋„์—์„œ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์„ ๋“œ๋ž˜๊ทธํ•˜์—ฌ "v2-input" ๋“ฑ์œผ๋กœ ์ƒ์„ฑ + +--- + +## ๐Ÿ”ง ์ผ๊ด„ ์ˆ˜์ • SQL (overrides.type ๋ˆ„๋ฝ ๋ฌธ์ œ) + +### ๋ฌธ์ œ ์ง„๋‹จ ์ฟผ๋ฆฌ + +```sql +-- overrides.type์ด ์—†๋Š” ์ปดํฌ๋„ŒํŠธ ์ˆ˜ ํ™•์ธ +SELECT + COUNT(DISTINCT sv2.screen_id) as affected_screens, + COUNT(*) as affected_components +FROM screen_layouts_v2 sv2, + jsonb_array_elements(sv2.layout_data->'components') as comp +WHERE (comp->>'url' LIKE '%/v2-input' + OR comp->>'url' LIKE '%/v2-select' + OR comp->>'url' LIKE '%/v2-date') + AND NOT (comp->'overrides' ? 'type'); +``` + +### ์ผ๊ด„ ์ˆ˜์ • ์ฟผ๋ฆฌ (๊ฐœ๋ฐœ์„œ๋ฒ„์—์„œ๋งŒ!) + +```sql +UPDATE screen_layouts_v2 +SET layout_data = jsonb_set( + layout_data, + '{components}', + ( + SELECT jsonb_agg( + CASE + WHEN comp->>'url' LIKE '%/v2-input' AND NOT (comp->'overrides' ? 'type') + THEN jsonb_set(comp, '{overrides,type}', '"v2-input"') + WHEN comp->>'url' LIKE '%/v2-select' AND NOT (comp->'overrides' ? 'type') + THEN jsonb_set(comp, '{overrides,type}', '"v2-select"') + WHEN comp->>'url' LIKE '%/v2-date' AND NOT (comp->'overrides' ? 'type') + THEN jsonb_set(comp, '{overrides,type}', '"v2-date"') + WHEN comp->>'url' LIKE '%/v2-textarea' AND NOT (comp->'overrides' ? 'type') + THEN jsonb_set(comp, '{overrides,type}', '"v2-textarea"') + ELSE comp + END + ) + FROM jsonb_array_elements(layout_data->'components') comp + ) +), +updated_at = NOW() +WHERE EXISTS ( + SELECT 1 FROM jsonb_array_elements(layout_data->'components') c + WHERE (c->>'url' LIKE '%/v2-input' OR c->>'url' LIKE '%/v2-select' + OR c->>'url' LIKE '%/v2-date' OR c->>'url' LIKE '%/v2-textarea') + AND NOT (c->'overrides' ? 'type') +); +``` + +### 2026-02-04 ์ผ๊ด„ ์ˆ˜์ • ์‹คํ–‰ ๊ฒฐ๊ณผ + +| ํ•ญ๋ชฉ | ์ˆ˜๋Ÿ‰ | +|------|------| +| ์ˆ˜์ •๋œ ํ™”๋ฉด | 397๊ฐœ | +| ์ˆ˜์ •๋œ ์ปดํฌ๋„ŒํŠธ | 2,455๊ฐœ | +| v2-input | 1,983๊ฐœ | +| v2-select | 336๊ฐœ | +| v2-date | 136๊ฐœ | + +--- + +## ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ง„ํ–‰ ๋กœ๊ทธ + +| ๋‚ ์งœ | ๋ฉ”๋‰ด | ๋‹ด๋‹น | ์ƒํƒœ | ๋น„๊ณ  | +|------|------|------|------|------| +| 2026-02-03 | ํ’ˆ์งˆ๊ด€๋ฆฌ | DDD1542 | ๋ถ„์„ ์™„๋ฃŒ | ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋Œ€๊ธฐ | +| 2026-02-03 | ๋ฌผ๋ฅ˜๊ด€๋ฆฌ (์šด์†ก์—…์ฒด) | ์ด์ „ ์‹ ํ•˜ | โŒ ์‹คํŒจ | component ์—ฐ๊ฒฐ ์˜ค๋ฅ˜ | +| 2026-02-03 | ๋ฌธ์„œ ํ•™์Šต | DDD1542 | โœ… ์™„๋ฃŒ | ํ•ต์‹ฌ 4๊ฐœ ๋ฌธ์„œ ์ •๋…, ํ•™์Šต๋…ธํŠธ ์ž‘์„ฑ | +| **2026-02-04** | **overrides.type ์›์ธ ๋ถ„์„** | **AI** | **โœ… ์™„๋ฃŒ** | **ํ•ต์‹ฌ ์›์ธ ๋ฐœ๊ฒฌ: overrides.type ๋ˆ„๋ฝ** | +| **2026-02-04** | **์ „์ฒด ์ž…๋ ฅํผ ์ผ๊ด„ ์ˆ˜์ •** | **AI** | **โœ… ์™„๋ฃŒ** | **397๊ฐœ ํ™”๋ฉด, 2,455๊ฐœ ์ปดํฌ๋„ŒํŠธ ์ˆ˜์ •** | +| | ๋ฌผ๋ฅ˜๊ด€๋ฆฌ | - | ๋ฏธ์‹œ์ž‘ | | +| | ์ƒ์‚ฐ๊ด€๋ฆฌ | - | ๋ฏธ์‹œ์ž‘ | | +| | ์˜์—…๊ด€๋ฆฌ | - | ๋ฏธ์‹œ์ž‘ | | + +--- + +## ๋‹ค์Œ ์ž‘์—… ์š”์ฒญ ์˜ˆ์‹œ + +๋‹ค์Œ AI์—๊ฒŒ ์š”์ฒญํ•  ๋•Œ ์ด๋ ‡๊ฒŒ ๋งํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค: + +``` +"๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_๊ฐ€์ด๋“œ.md ์ฝ๊ณ  ํ’ˆ์งˆ๊ด€๋ฆฌ ๋ฉ”๋‰ด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ง„ํ–‰ํ•ด์ค˜" + +"๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_๊ฐ€์ด๋“œ.md ์ฐธ๊ณ ํ•ด์„œ ๋ฌผ๋ฅ˜๊ด€๋ฆฌ ๋ฉ”๋‰ด ๋ถ„์„ํ•ด์ค˜" + +"๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์ƒ์„ธ๊ฐ€์ด๋“œ.md ๋ณด๊ณ  COMPANY_7_142 ํ™”๋ฉด V2๋กœ ๋ณ€ํ™˜ํ•ด์ค˜" +``` + +--- + +## ๋ณ€๊ฒฝ ์ด๋ ฅ + +| ๋‚ ์งœ | ์ž‘์„ฑ์ž | ๋‚ด์šฉ | +|------|--------|------| +| 2026-02-03 | DDD1542 | ์ดˆ์•ˆ ์ž‘์„ฑ | +| 2026-02-03 | DDD1542 | ์ปดํฌ๋„ŒํŠธ-์ปฌ๋Ÿผ ์—ฐ๊ฒฐ ์ฃผ์˜์‚ฌํ•ญ ์ถ”๊ฐ€ (์ด์ „ ์‹คํŒจ ์›์ธ) | +| 2026-02-03 | DDD1542 | ๊ฐœ์ธ ํ•™์Šต๋…ธํŠธ ์ž‘์„ฑ (V2_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_ํ•™์Šต๋…ธํŠธ_DDD1542.md) | +| **2026-02-04** | **AI** | **ํ•ต์‹ฌ ์›์ธ ๋ฐœ๊ฒฌ: overrides.type ํ•„๋“œ ๋ˆ„๋ฝ ๋ฌธ์ œ** | +| **2026-02-04** | **AI** | **์ผ๊ด„ ์ˆ˜์ • SQL ์ถ”๊ฐ€ ๋ฐ 397๊ฐœ ํ™”๋ฉด ์ˆ˜์ • ์™„๋ฃŒ** | diff --git a/docs/DDD1542/๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์ƒ์„ธ๊ฐ€์ด๋“œ.md b/docs/DDD1542/๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์ƒ์„ธ๊ฐ€์ด๋“œ.md new file mode 100644 index 00000000..42ce37f1 --- /dev/null +++ b/docs/DDD1542/๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์ƒ์„ธ๊ฐ€์ด๋“œ.md @@ -0,0 +1,553 @@ +# ๋ณธ์„œ๋ฒ„ โ†’ ๊ฐœ๋ฐœ์„œ๋ฒ„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ + +## ๊ฐœ์š” + +๋ณธ ๋ฌธ์„œ๋Š” **๋ณธ์„œ๋ฒ„(Production)**์˜ `screen_layouts` (V1) ๋ฐ์ดํ„ฐ๋ฅผ **๊ฐœ๋ฐœ์„œ๋ฒ„(Development)**์˜ `screen_layouts_v2` ์‹œ์Šคํ…œ์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜๋Š” ์ ˆ์ฐจ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + +### ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ฐฉํ–ฅ +``` +๋ณธ์„œ๋ฒ„ (Production) ๊ฐœ๋ฐœ์„œ๋ฒ„ (Development) +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ screen_layouts (V1) โ”‚ โ†’ โ”‚ screen_layouts_v2 โ”‚ +โ”‚ - ์ปดํฌ๋„ŒํŠธ๋ณ„ ๋ ˆ์ฝ”๋“œ โ”‚ โ”‚ - ํ™”๋ฉด๋‹น 1๊ฐœ ๋ ˆ์ฝ”๋“œ โ”‚ +โ”‚ - properties JSONB โ”‚ โ”‚ - layout_data JSONB โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### ์ตœ์ข… ๋ชฉํ‘œ +๊ฐœ๋ฐœ์„œ๋ฒ„์—์„œ ์™„์„ฑ ํ›„ **๊ฐœ๋ฐœ์„œ๋ฒ„ โ†’ ๋ณธ์„œ๋ฒ„**๋กœ ๋ฐฐํฌ + +--- + +## 1. V1 vs V2 ๊ตฌ์กฐ ์ฐจ์ด + +### 1.1 screen_layouts (V1) - ๋ณธ์„œ๋ฒ„ + +```sql +-- ์ปดํฌ๋„ŒํŠธ๋ณ„ 1๊ฐœ ๋ ˆ์ฝ”๋“œ +CREATE TABLE screen_layouts ( + layout_id SERIAL PRIMARY KEY, + screen_id INTEGER, + component_type VARCHAR(50), + component_id VARCHAR(100), + properties JSONB, -- ๋ชจ๋“  ์„ค์ •๊ฐ’ ํฌํ•จ + ... +); +``` + +**ํŠน์ง•:** +- ํ™”๋ฉด๋‹น N๊ฐœ ๋ ˆ์ฝ”๋“œ (์ปดํฌ๋„ŒํŠธ ์ˆ˜๋งŒํผ) +- `properties`์— ๋ชจ๋“  ์„ค์ • ์ €์žฅ (defaults + overrides ๊ตฌ๋ถ„ ์—†์Œ) +- `menu_objid` ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ/์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ + +### 1.2 screen_layouts_v2 - ๊ฐœ๋ฐœ์„œ๋ฒ„ + +```sql +-- ํ™”๋ฉด๋‹น 1๊ฐœ ๋ ˆ์ฝ”๋“œ +CREATE TABLE screen_layouts_v2 ( + layout_id SERIAL PRIMARY KEY, + screen_id INTEGER NOT NULL, + company_code VARCHAR(20) NOT NULL, + layout_data JSONB NOT NULL DEFAULT '{}'::jsonb, + UNIQUE(screen_id, company_code) +); +``` + +**layout_data ๊ตฌ์กฐ:** +```json +{ + "version": "2.0", + "components": [ + { + "id": "comp_xxx", + "url": "@/lib/registry/components/v2-table-list", + "position": { "x": 0, "y": 0 }, + "size": { "width": 100, "height": 50 }, + "displayOrder": 0, + "overrides": { + "tableName": "inspection_standard", + "columns": ["id", "name"] + } + } + ], + "updatedAt": "2026-02-03T12:00:00Z" +} +``` + +**ํŠน์ง•:** +- ํ™”๋ฉด๋‹น 1๊ฐœ ๋ ˆ์ฝ”๋“œ +- `url` + `overrides` ๋ฐฉ์‹ (Zod ์Šคํ‚ค๋งˆ defaults์™€ ๋ณ‘ํ•ฉ) +- `table_name + column_name` ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ/์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ (์ „์—ญ) + +--- + +## 2. ๋ฐ์ดํ„ฐ ํƒ€์ž… ๊ด€๋ฆฌ ๊ตฌ์กฐ (V2) + +### 2.1 ํ•ต์‹ฌ ํ…Œ์ด๋ธ” ๊ด€๊ณ„ + +``` +table_type_columns (์ปฌ๋Ÿผ ํƒ€์ž… ์ •์˜) +โ”œโ”€โ”€ input_type = 'category' โ†’ category_values +โ”œโ”€โ”€ input_type = 'numbering' โ†’ numbering_rules +โ””โ”€โ”€ input_type = 'text', 'date', 'number', etc. +``` + +### 2.2 table_type_columns + +๊ฐ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ๋ณ„ ์ž…๋ ฅ ํƒ€์ž…์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + +```sql +SELECT table_name, column_name, input_type, column_label +FROM table_type_columns +WHERE input_type IN ('category', 'numbering'); +``` + +**์ฃผ์š” input_type:** +| input_type | ์„ค๋ช… | ์—ฐ๊ฒฐ ํ…Œ์ด๋ธ” | +|------------|------|-------------| +| text | ํ…์ŠคํŠธ ์ž…๋ ฅ | - | +| number | ์ˆซ์ž ์ž…๋ ฅ | - | +| date | ๋‚ ์งœ ์ž…๋ ฅ | - | +| category | ์นดํ…Œ๊ณ ๋ฆฌ ๋“œ๋กญ๋‹ค์šด | category_values | +| numbering | ์ž๋™ ์ฑ„๋ฒˆ | numbering_rules | +| entity | ์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰ | - | + +### 2.3 category_values (์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ) + +```sql +-- ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ +SELECT value_id, table_name, column_name, value_code, value_label, + parent_value_id, depth, company_code +FROM category_values +WHERE table_name = 'inspection_standard' + AND column_name = 'inspection_method' + AND company_code = 'COMPANY_7'; +``` + +**V1 vs V2 ์ฐจ์ด:** +| ๊ตฌ๋ถ„ | V1 | V2 | +|------|----|----| +| ํ‚ค | menu_objid | table_name + column_name | +| ๋ฒ”์œ„ | ํ™”๋ฉด๋ณ„ | ์ „์—ญ (ํ…Œ์ด๋ธ”.์ปฌ๋Ÿผ๋ณ„) | +| ๊ณ„์ธต | ๋‹จ์ผ | 3๋‹จ๊ณ„ (๋Œ€/์ค‘/์†Œ๋ถ„๋ฅ˜) | + +### 2.4 numbering_rules (์ฑ„๋ฒˆ ๊ทœ์น™) + +```sql +-- ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ +SELECT rule_id, rule_name, table_name, column_name, separator, + reset_period, current_sequence, company_code +FROM numbering_rules +WHERE company_code = 'COMPANY_7'; +``` + +**์—ฐ๊ฒฐ ๋ฐฉ์‹:** +``` +table_type_columns.detail_settings = '{"numberingRuleId": "rule-xxx"}' + โ†“ + numbering_rules.rule_id = "rule-xxx" +``` + +--- + +## 3. ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +### 3.1 ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +| V1 (๋ณธ์„œ๋ฒ„) | V2 (๊ฐœ๋ฐœ์„œ๋ฒ„) | ๋น„๊ณ  | +|-------------|---------------|------| +| table-list | v2-table-list | ํ…Œ์ด๋ธ” ๋ชฉ๋ก | +| button-primary | v2-button-primary | ๋ฒ„ํŠผ | +| text-input | v2-text-input | ํ…์ŠคํŠธ ์ž…๋ ฅ | +| select-basic | v2-select | ๋“œ๋กญ๋‹ค์šด | +| date-input | v2-date-input | ๋‚ ์งœ ์ž…๋ ฅ | +| entity-search-input | v2-entity-search | ์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰ | +| tabs-widget | v2-tabs-widget | ํƒญ | + +### 3.2 ํŠน์ˆ˜ ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +| V1 (๋ณธ์„œ๋ฒ„) | V2 (๊ฐœ๋ฐœ์„œ๋ฒ„) | ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ฐฉ์‹ | +|-------------|---------------|-------------------| +| category-manager | v2-category-manager | table_name ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ | +| numbering-rule | v2-numbering-rule | table_name ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ | +| ๋ชจ๋‹ฌ ํ™”๋ฉด | overlay ํ†ตํ•ฉ | ๋ถ€๋ชจ ํ™”๋ฉด์— ํ†ตํ•ฉ | + +### 3.3 ๋ชจ๋‹ฌ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๋ณ€๊ฒฝ + +**V1 (๋ณธ์„œ๋ฒ„):** +``` +ํ™”๋ฉด A (screen_id: 142) - ๊ฒ€์‚ฌ์žฅ๋น„๊ด€๋ฆฌ + โ””โ”€โ”€ ๋ฒ„ํŠผ ํด๋ฆญ โ†’ ํ™”๋ฉด B (screen_id: 143) - ๊ฒ€์‚ฌ์žฅ๋น„ ๋“ฑ๋ก๋ชจ๋‹ฌ +``` + +**V2 (๊ฐœ๋ฐœ์„œ๋ฒ„):** +``` +ํ™”๋ฉด A (screen_id: 142) - ๊ฒ€์‚ฌ์žฅ๋น„๊ด€๋ฆฌ + โ””โ”€โ”€ v2-dialog-form ์ปดํฌ๋„ŒํŠธ๋กœ ๋ชจ๋‹ฌ ํ†ตํ•ฉ +``` + +--- + +## 4. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ˆ์ฐจ + +### 4.1 ์‚ฌ์ „ ๋ถ„์„ + +```sql +-- 1. ๋ณธ์„œ๋ฒ„ ํ™”๋ฉด ๋ชฉ๋ก ํ™•์ธ +SELECT sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name, + COUNT(sl.layout_id) as component_count +FROM screen_definitions sd +LEFT JOIN screen_layouts sl ON sd.screen_id = sl.screen_id +WHERE sd.screen_code LIKE 'COMPANY_7_%' + AND sd.screen_name LIKE '%ํ’ˆ์งˆ%' +GROUP BY sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name; + +-- 2. ๊ฐœ๋ฐœ์„œ๋ฒ„ V2 ํ™”๋ฉด ํ˜„ํ™ฉ ํ™•์ธ +SELECT sd.screen_id, sd.screen_code, sd.screen_name, + sv2.layout_data IS NOT NULL as has_v2_layout +FROM screen_definitions sd +LEFT JOIN screen_layouts_v2 sv2 ON sd.screen_id = sv2.screen_id +WHERE sd.company_code = 'COMPANY_7'; +``` + +### 4.2 Step 1: screen_definitions ๋™๊ธฐํ™” + +```sql +-- ๋ณธ์„œ๋ฒ„์—๋งŒ ์žˆ๋Š” ํ™”๋ฉด์„ ๊ฐœ๋ฐœ์„œ๋ฒ„์— ์ถ”๊ฐ€ +INSERT INTO screen_definitions (screen_code, screen_name, table_name, company_code, ...) +SELECT screen_code, screen_name, table_name, company_code, ... +FROM [๋ณธ์„œ๋ฒ„].screen_definitions +WHERE screen_code NOT IN (SELECT screen_code FROM screen_definitions); +``` + +### 4.3 Step 2: V1 โ†’ V2 ๋ ˆ์ด์•„์›ƒ ๋ณ€ํ™˜ + +```typescript +// ๋ณ€ํ™˜ ๋กœ์ง (pseudo-code) +async function convertV1toV2(screenId: number, companyCode: string) { + // 1. V1 ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ + const v1Layouts = await getV1Layouts(screenId); + + // 2. V2 ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ + const v2Layout = { + version: "2.0", + components: v1Layouts.map(v1 => ({ + id: v1.component_id, + url: mapComponentUrl(v1.component_type), + position: { x: v1.position_x, y: v1.position_y }, + size: { width: v1.width, height: v1.height }, + displayOrder: v1.display_order, + overrides: extractOverrides(v1.properties) + })), + updatedAt: new Date().toISOString() + }; + + // 3. V2 ํ…Œ์ด๋ธ”์— ์ €์žฅ + await saveV2Layout(screenId, companyCode, v2Layout); +} + +function mapComponentUrl(v1Type: string): string { + const mapping = { + 'table-list': '@/lib/registry/components/v2-table-list', + 'button-primary': '@/lib/registry/components/v2-button-primary', + 'category-manager': '@/lib/registry/components/v2-category-manager', + 'numbering-rule': '@/lib/registry/components/v2-numbering-rule', + // ... ๊ธฐํƒ€ ๋งคํ•‘ + }; + return mapping[v1Type] || `@/lib/registry/components/v2-${v1Type}`; +} +``` + +### 4.4 Step 3: ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +```sql +-- ๋ณธ์„œ๋ฒ„ ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ์ดํ„ฐ โ†’ ๊ฐœ๋ฐœ์„œ๋ฒ„ category_values +INSERT INTO category_values ( + table_name, column_name, value_code, value_label, + value_order, parent_value_id, depth, company_code +) +SELECT + -- V1 ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ table_name + column_name ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€ํ™˜ + 'inspection_standard' as table_name, + 'inspection_method' as column_name, + value_code, + value_label, + sort_order, + NULL as parent_value_id, + 1 as depth, + 'COMPANY_7' as company_code +FROM [๋ณธ์„œ๋ฒ„_์นดํ…Œ๊ณ ๋ฆฌ_๋ฐ์ดํ„ฐ]; +``` + +### 4.5 Step 4: ์ฑ„๋ฒˆ ๊ทœ์น™ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +```sql +-- ๋ณธ์„œ๋ฒ„ ์ฑ„๋ฒˆ ๊ทœ์น™ โ†’ ๊ฐœ๋ฐœ์„œ๋ฒ„ numbering_rules +INSERT INTO numbering_rules ( + rule_id, rule_name, table_name, column_name, + separator, reset_period, current_sequence, company_code +) +SELECT + rule_id, + rule_name, + 'inspection_standard' as table_name, + 'inspection_code' as column_name, + separator, + reset_period, + 0 as current_sequence, -- ์‹œํ€€์Šค ์ดˆ๊ธฐํ™” + 'COMPANY_7' as company_code +FROM [๋ณธ์„œ๋ฒ„_์ฑ„๋ฒˆ_๊ทœ์น™]; +``` + +### 4.6 Step 5: table_type_columns ์„ค์ • + +```sql +-- ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ ์„ค์ • +UPDATE table_type_columns +SET input_type = 'category' +WHERE table_name = 'inspection_standard' + AND column_name = 'inspection_method' + AND company_code = 'COMPANY_7'; + +-- ์ฑ„๋ฒˆ ์ปฌ๋Ÿผ ์„ค์ • +UPDATE table_type_columns +SET + input_type = 'numbering', + detail_settings = '{"numberingRuleId": "rule-xxx"}' +WHERE table_name = 'inspection_standard' + AND column_name = 'inspection_code' + AND company_code = 'COMPANY_7'; +``` + +--- + +## 5. ํ’ˆ์งˆ๊ด€๋ฆฌ ๋ฉ”๋‰ด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ˜„ํ™ฉ + +### 5.1 ํ™”๋ฉด ๋งคํ•‘ ํ˜„ํ™ฉ + +| ๋ณธ์„œ๋ฒ„ ์ฝ”๋“œ | ํ™”๋ฉด๋ช… | ํ…Œ์ด๋ธ” | ๊ฐœ๋ฐœ์„œ๋ฒ„ ์ƒํƒœ | ๋น„๊ณ  | +|-------------|--------|--------|---------------|------| +| COMPANY_7_126 | ๊ฒ€์‚ฌ์ •๋ณด ๊ด€๋ฆฌ | inspection_standard | โœ… V2 ์กด์žฌ | ์ปดํฌ๋„ŒํŠธ ์ˆ˜ ํ™•์ธ ํ•„์š” | +| COMPANY_7_127 | ํ’ˆ๋ชฉ์˜ต์…˜ ์„ค์ • | - | โœ… V2 ์กด์žฌ | v2-category-manager ์‚ฌ์šฉ์ค‘ | +| COMPANY_7_138 | ์นดํ…Œ๊ณ ๋ฆฌ ์„ค์ • | inspection_standard | โŒ ๋ˆ„๋ฝ | V2: table_name ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ | +| COMPANY_7_139 | ์ฝ”๋“œ ์„ค์ • | inspection_standard | โŒ ๋ˆ„๋ฝ | V2: table_name ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ | +| COMPANY_7_142 | ๊ฒ€์‚ฌ์žฅ๋น„ ๊ด€๋ฆฌ | inspection_equipment_mng | โŒ ๋ˆ„๋ฝ | ๋ชจ๋‹ฌ ํ†ตํ•ฉ ํ•„์š” | +| COMPANY_7_143 | ๊ฒ€์‚ฌ์žฅ๋น„ ๋“ฑ๋ก๋ชจ๋‹ฌ | inspection_equipment_mng | โŒ ๋ˆ„๋ฝ | COMPANY_7_142์— ํ†ตํ•ฉ | +| COMPANY_7_144 | ๋ถˆ๋Ÿ‰๊ธฐ์ค€ ์ •๋ณด | defect_standard_mng | โŒ ๋ˆ„๋ฝ | ๋ชจ๋‹ฌ ํ†ตํ•ฉ ํ•„์š” | +| COMPANY_7_145 | ๋ถˆ๋Ÿ‰๊ธฐ์ค€ ๋“ฑ๋ก๋ชจ๋‹ฌ | defect_standard_mng | โŒ ๋ˆ„๋ฝ | COMPANY_7_144์— ํ†ตํ•ฉ | + +### 5.2 ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ ์ปฌ๋Ÿผ ํ˜„ํ™ฉ + +**inspection_standard:** +| ์ปฌ๋Ÿผ | input_type | ๋ผ๋ฒจ | +|------|------------|------| +| inspection_method | category | ๊ฒ€์‚ฌ๋ฐฉ๋ฒ• | +| unit | category | ๋‹จ์œ„ | +| apply_type | category | ์ ์šฉ๊ตฌ๋ถ„ | +| inspection_type | category | ์œ ํ˜• | + +**inspection_equipment_mng:** +| ์ปฌ๋Ÿผ | input_type | ๋ผ๋ฒจ | +|------|------------|------| +| equipment_type | category | ์žฅ๋น„์œ ํ˜• | +| installation_location | category | ์„ค์น˜์žฅ์†Œ | +| equipment_status | category | ์žฅ๋น„์ƒํƒœ | + +**defect_standard_mng:** +| ์ปฌ๋Ÿผ | input_type | ๋ผ๋ฒจ | +|------|------------|------| +| defect_type | category | ๋ถˆ๋Ÿ‰์œ ํ˜• | +| severity | category | ์‹ฌ๊ฐ๋„ | +| inspection_type | category | ๊ฒ€์‚ฌ์œ ํ˜• | + +--- + +## 6. ์ž๋™ํ™” ์Šคํฌ๋ฆฝํŠธ + +### 6.1 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ + +```typescript +// backend-node/src/scripts/migrateV1toV2.ts +import { getPool } from "../database/db"; + +interface MigrationResult { + screenCode: string; + success: boolean; + message: string; + componentCount?: number; +} + +async function migrateScreenToV2( + screenCode: string, + companyCode: string +): Promise { + const pool = getPool(); + + try { + // 1. V1 ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ (๋ณธ์„œ๋ฒ„์—์„œ) + const v1Result = await pool.query(` + SELECT sl.*, sd.table_name, sd.screen_name + FROM screen_layouts sl + JOIN screen_definitions sd ON sl.screen_id = sd.screen_id + WHERE sd.screen_code = $1 + ORDER BY sl.display_order + `, [screenCode]); + + if (v1Result.rows.length === 0) { + return { screenCode, success: false, message: "V1 ๋ ˆ์ด์•„์›ƒ ์—†์Œ" }; + } + + // 2. V2 ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ + const components = v1Result.rows + .filter(row => row.component_type !== '_metadata') + .map(row => ({ + id: row.component_id || `comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + url: mapComponentUrl(row.component_type), + position: { x: row.position_x || 0, y: row.position_y || 0 }, + size: { width: row.width || 100, height: row.height || 50 }, + displayOrder: row.display_order || 0, + overrides: extractOverrides(row.properties, row.component_type) + })); + + const layoutData = { + version: "2.0", + components, + migratedFrom: "V1", + migratedAt: new Date().toISOString() + }; + + // 3. ๊ฐœ๋ฐœ์„œ๋ฒ„ V2 ํ…Œ์ด๋ธ”์— ์ €์žฅ + const screenId = v1Result.rows[0].screen_id; + + await pool.query(` + INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data) + VALUES ($1, $2, $3) + ON CONFLICT (screen_id, company_code) + DO UPDATE SET layout_data = $3, updated_at = NOW() + `, [screenId, companyCode, JSON.stringify(layoutData)]); + + return { + screenCode, + success: true, + message: "๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ", + componentCount: components.length + }; + } catch (error: any) { + return { screenCode, success: false, message: error.message }; + } +} + +function mapComponentUrl(v1Type: string): string { + const mapping: Record = { + 'table-list': '@/lib/registry/components/v2-table-list', + 'button-primary': '@/lib/registry/components/v2-button-primary', + 'text-input': '@/lib/registry/components/v2-text-input', + 'select-basic': '@/lib/registry/components/v2-select', + 'date-input': '@/lib/registry/components/v2-date-input', + 'entity-search-input': '@/lib/registry/components/v2-entity-search', + 'category-manager': '@/lib/registry/components/v2-category-manager', + 'numbering-rule': '@/lib/registry/components/v2-numbering-rule', + 'tabs-widget': '@/lib/registry/components/v2-tabs-widget', + 'textarea-basic': '@/lib/registry/components/v2-textarea', + }; + return mapping[v1Type] || `@/lib/registry/components/v2-${v1Type}`; +} + +function extractOverrides(properties: any, componentType: string): Record { + if (!properties) return {}; + + // V2 Zod ์Šคํ‚ค๋งˆ defaults์™€ ๋น„๊ตํ•˜์—ฌ ๋‹ค๋ฅธ ๊ฐ’๋งŒ ์ถ”์ถœ + // (์‹ค์ œ ๊ตฌํ˜„ ์‹œ ๊ฐ ์ปดํฌ๋„ŒํŠธ์˜ defaultConfig์™€ ๋น„๊ต) + const overrides: Record = {}; + + // ํ•„์ˆ˜ ์„ค์ •๋งŒ ์ถ”์ถœ + if (properties.tableName) overrides.tableName = properties.tableName; + if (properties.columns) overrides.columns = properties.columns; + if (properties.label) overrides.label = properties.label; + if (properties.onClick) overrides.onClick = properties.onClick; + + return overrides; +} +``` + +--- + +## 7. ๊ฒ€์ฆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### 7.1 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „ + +- [ ] ๋ณธ์„œ๋ฒ„ ํ™”๋ฉด ๋ชฉ๋ก ํ™•์ธ +- [ ] ๊ฐœ๋ฐœ์„œ๋ฒ„ ๊ธฐ์กด V2 ๋ฐ์ดํ„ฐ ๋ฐฑ์—… +- [ ] ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ ํ…Œ์ด๋ธ” ๊ฒ€ํ†  +- [ ] ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ ๋ฐ์ดํ„ฐ ๋ถ„์„ + +### 7.2 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ›„ + +- [ ] screen_definitions ๋™๊ธฐํ™” ํ™•์ธ +- [ ] screen_layouts_v2 ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ํ™•์ธ +- [ ] ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ํ…Œ์ŠคํŠธ +- [ ] ์นดํ…Œ๊ณ ๋ฆฌ ๋“œ๋กญ๋‹ค์šด ๋™์ž‘ ํ™•์ธ +- [ ] ์ฑ„๋ฒˆ ๊ทœ์น™ ๋™์ž‘ ํ™•์ธ +- [ ] ์ €์žฅ/์ˆ˜์ •/์‚ญ์ œ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ + +### 7.3 ๋ชจ๋‹ฌ ํ†ตํ•ฉ ํ™•์ธ + +- [ ] ๊ธฐ์กด ๋ชจ๋‹ฌ ํ™”๋ฉด โ†’ overlay ํ†ตํ•ฉ ์™„๋ฃŒ +- [ ] ๋ถ€๋ชจ-์ž์‹ ๋ฐ์ดํ„ฐ ์—ฐ๋™ ํ™•์ธ +- [ ] ๋ชจ๋‹ฌ ์—ด๊ธฐ/๋‹ซ๊ธฐ ๋™์ž‘ ํ™•์ธ + +--- + +## 8. ๋กค๋ฐฑ ๊ณ„ํš + +๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํŒจ ์‹œ ๋กค๋ฐฑ ์ ˆ์ฐจ: + +```sql +-- 1. V2 ๋ ˆ์ด์•„์›ƒ ๋กค๋ฐฑ +DELETE FROM screen_layouts_v2 +WHERE screen_id IN ( + SELECT screen_id FROM screen_definitions + WHERE screen_code LIKE 'COMPANY_7_%' +); + +-- 2. ์ถ”๊ฐ€๋œ screen_definitions ๋กค๋ฐฑ +DELETE FROM screen_definitions +WHERE screen_code IN ('์‹ ๊ทœ_์ถ”๊ฐ€๋œ_์ฝ”๋“œ๋“ค') + AND company_code = 'COMPANY_7'; + +-- 3. category_values ๋กค๋ฐฑ +DELETE FROM category_values +WHERE company_code = 'COMPANY_7' + AND created_at > '[๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์‹œ์ž‘_์‹œ๊ฐ„]'; + +-- 4. numbering_rules ๋กค๋ฐฑ +DELETE FROM numbering_rules +WHERE company_code = 'COMPANY_7' + AND created_at > '[๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์‹œ์ž‘_์‹œ๊ฐ„]'; +``` + +--- + +## 9. ์ฐธ๊ณ  ์ž๋ฃŒ + +### ๊ด€๋ จ ์ฝ”๋“œ ํŒŒ์ผ + +- **V2 Category Manager**: `frontend/lib/registry/components/v2-category-manager/` +- **V2 Numbering Rule**: `frontend/lib/registry/components/v2-numbering-rule/` +- **Category Service**: `backend-node/src/services/categoryTreeService.ts` +- **Numbering Service**: `backend-node/src/services/numberingRuleService.ts` + +### ๊ด€๋ จ ๋ฌธ์„œ + +- [V2 ์ปดํฌ๋„ŒํŠธ ๋ถ„์„ ๊ฐ€์ด๋“œ](../V2_์ปดํฌ๋„ŒํŠธ_๋ถ„์„_๊ฐ€์ด๋“œ.md) +- [V2 ์ปดํฌ๋„ŒํŠธ ์—ฐ๋™ ๊ฐ€์ด๋“œ](../V2_์ปดํฌ๋„ŒํŠธ_์—ฐ๋™_๊ฐ€์ด๋“œ.md) +- [ํ™”๋ฉด ๊ฐœ๋ฐœ ํ‘œ์ค€ ๊ฐ€์ด๋“œ](../screen-implementation-guide/SCREEN_DEVELOPMENT_STANDARD.md) +- [์ปดํฌ๋„ŒํŠธ ๋ ˆ์ด์•„์›ƒ V2 ์•„ํ‚คํ…์ฒ˜](./COMPONENT_LAYOUT_V2_ARCHITECTURE.md) + +--- + +## ๋ณ€๊ฒฝ ์ด๋ ฅ + +| ๋‚ ์งœ | ์ž‘์„ฑ์ž | ๋‚ด์šฉ | +|------|--------|------| +| 2026-02-03 | DDD1542 | ์ดˆ์•ˆ ์ž‘์„ฑ | diff --git a/docs/DDD1542/ํ™”๋ฉด๊ด€๊ณ„_์‹œ๊ฐํ™”_๊ฐœ์„ _๋ณด๊ณ ์„œ.md b/docs/DDD1542/ํ™”๋ฉด๊ด€๊ณ„_์‹œ๊ฐํ™”_๊ฐœ์„ _๋ณด๊ณ ์„œ.md index 27946afa..aea92243 100644 --- a/docs/DDD1542/ํ™”๋ฉด๊ด€๊ณ„_์‹œ๊ฐํ™”_๊ฐœ์„ _๋ณด๊ณ ์„œ.md +++ b/docs/DDD1542/ํ™”๋ฉด๊ด€๊ณ„_์‹œ๊ฐํ™”_๊ฐœ์„ _๋ณด๊ณ ์„œ.md @@ -23,7 +23,8 @@ | ํ…Œ์ด๋ธ”๋ช… | ์šฉ๋„ | ์ฃผ์š” ์ปฌ๋Ÿผ | |----------|------|----------| | `screen_definitions` | ํ™”๋ฉด ์ •์˜ ์ •๋ณด | `screen_id`, `screen_name`, `table_name`, `company_code` | -| `screen_layouts` | ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ/์ปดํฌ๋„ŒํŠธ ์ •๋ณด | `screen_id`, `properties` (JSONB - componentConfig ํฌํ•จ) | +| `screen_layouts` | ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ/์ปดํฌ๋„ŒํŠธ ์ •๋ณด (Legacy) | `screen_id`, `properties` (JSONB - componentConfig ํฌํ•จ) | +| `screen_layouts_v2` | ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ/์ปดํฌ๋„ŒํŠธ ์ •๋ณด (V2) | `screen_id`, `layout_data` (JSONB - components ๋ฐฐ์—ด) | | `screen_groups` | ํ™”๋ฉด ๊ทธ๋ฃน ์ •๋ณด | `group_id`, `group_code`, `group_name`, `parent_group_id` | | `screen_group_mappings` | ํ™”๋ฉด-๊ทธ๋ฃน ๋งคํ•‘ | `group_id`, `screen_id`, `display_order` | @@ -86,9 +87,17 @@ screen_groups (๊ทธ๋ฃน) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€ screen_definitions (ํ™”๋ฉด) โ”‚ โ”‚ - โ”‚ โ””โ”€โ”€โ”€ screen_layouts (๋ ˆ์ด์•„์›ƒ/์ปดํฌ๋„ŒํŠธ) + โ”‚ โ”œโ”€โ”€โ”€ screen_layouts (Legacy) + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ””โ”€โ”€โ”€ properties.componentConfig + โ”‚ โ”‚ โ”œโ”€โ”€ fieldMappings + โ”‚ โ”‚ โ”œโ”€โ”€ parentDataMapping + โ”‚ โ”‚ โ”œโ”€โ”€ columns.mapping + โ”‚ โ”‚ โ””โ”€โ”€ rightPanel.relation + โ”‚ โ”‚ + โ”‚ โ””โ”€โ”€โ”€ screen_layouts_v2 (V2) โ† ํ˜„์žฌ ํ‘œ์ค€ โ”‚ โ”‚ - โ”‚ โ””โ”€โ”€โ”€ properties.componentConfig + โ”‚ โ””โ”€โ”€โ”€ layout_data.components[].overrides โ”‚ โ”œโ”€โ”€ fieldMappings โ”‚ โ”œโ”€โ”€ parentDataMapping โ”‚ โ”œโ”€โ”€ columns.mapping @@ -1120,9 +1129,12 @@ screenSubTables[screenId].subTables.push({ 21. [x] ํ•„ํ„ฐ ์—ฐ๊ฒฐ์„  ํฌ์ปค์‹ฑ ์ œ์–ด (ํ•ด๋‹น ํ™”๋ฉด ํฌ์ปค์‹ฑ ์‹œ์—๋งŒ ํ‘œ์‹œ) 22. [x] ์ €์žฅ ํ…Œ์ด๋ธ” ์ œ์™ธ ์กฐ๊ฑด ์ถ”๊ฐ€ (table-list + ์ฒดํฌ๋ฐ•์Šค + openModalWithData) 23. [x] ์ฒซ ์ง„์ž… ์‹œ ํฌ์ปค์‹ฑ ์—†์ด ์‹œ์ž‘ (ํŠธ๋ฆฌ์—์„œ ํ™”๋ฉด ํด๋ฆญ ์‹œ ๊ทธ๋ฃน๋งŒ ์ง„์ž…) -24. [ ] **์„  ๊ต์ฐจ์  ์ด์งˆ๊ฐ ํ•ด๊ฒฐ** (๊ณ„ํš ์ค‘) -22. [ ] ๋ฒ”๋ก€ UI ์ถ”๊ฐ€ (์„ ํƒ์‚ฌํ•ญ) -23. [ ] ์—ฃ์ง€ ๋ผ๋ฒจ์— ๊ด€๊ณ„ ์œ ํ˜• ํ‘œ์‹œ (์„ ํƒ์‚ฌํ•ญ) +24. [x] **screen_layouts_v2 ์ง€์› ์ถ”๊ฐ€** (rightPanel.relation V2 UNION ์ฟผ๋ฆฌ) โœ… 2026-01-30 +25. [x] **ํ…Œ์ด๋ธ” ๋ถ„๋ฅ˜ ์šฐ์„ ์ˆœ์œ„ ์‹œ์Šคํ…œ** (๋ฉ”์ธ > ์„œ๋ธŒ ์šฐ์„ ์ˆœ์œ„ ์ ์šฉ) โœ… 2026-01-30 +26. [x] **globalMainTables API ์ถ”๊ฐ€** (WHERE ์กฐ๊ฑด ๋Œ€์ƒ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋ฐ˜ํ™˜) โœ… 2026-01-30 +27. [ ] **์„  ๊ต์ฐจ์  ์ด์งˆ๊ฐ ํ•ด๊ฒฐ** (๊ณ„ํš ์ค‘) +28. [ ] ๋ฒ”๋ก€ UI ์ถ”๊ฐ€ (์„ ํƒ์‚ฌํ•ญ) +29. [ ] ์—ฃ์ง€ ๋ผ๋ฒจ์— ๊ด€๊ณ„ ์œ ํ˜• ํ‘œ์‹œ (์„ ํƒ์‚ฌํ•ญ) --- @@ -1682,6 +1694,149 @@ frontend/ --- +## ํ…Œ์ด๋ธ” ๋ถ„๋ฅ˜ ์šฐ์„ ์ˆœ์œ„ ์‹œ์Šคํ…œ (2026-01-30) + +### ๋ฐฐ๊ฒฝ + +๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ๊ด€๊ณ„์˜ ๋””ํ…Œ์ผ ํ…Œ์ด๋ธ”(์˜ˆ: `user_dept`)์ด ๋‹ค๋ฅธ ๊ณณ์—์„œ autocomplete ์ฐธ์กฐ๋กœ๋„ ์‚ฌ์šฉ๋˜๋Š” ๊ฒฝ์šฐ, +์„œ๋ธŒ ํ…Œ์ด๋ธ” ์˜์—ญ์— ์ž˜๋ชป ๋ฐฐ์น˜๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. + +### ๋ฌธ์ œ ์ƒํ™ฉ + +``` +[user_info] - ํ™”๋ฉด 139์˜ ๋””ํ…Œ์ผ โ†’ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ์˜์—ญ (O) +[user_dept] - ํ™”๋ฉด 162์˜ ๋””ํ…Œ์ผ์ด์ง€๋งŒ autocomplete ์ฐธ์กฐ๋„ ์žˆ์Œ โ†’ ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์˜์—ญ (X) +``` + +**์›์ธ**: ํ…Œ์ด๋ธ” ๋ถ„๋ฅ˜ ์‹œ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ์—†์–ด์„œ ๋จผ์ € ๋ฐœ๊ฒฌ๋œ ๊ด€๊ณ„ ํƒ€์ž…์œผ๋กœ ๋ถ„๋ฅ˜๋จ + +### ํ•ด๊ฒฐ์ฑ…: ์šฐ์„ ์ˆœ์œ„ ๊ธฐ๋ฐ˜ ํ…Œ์ด๋ธ” ๋ถ„๋ฅ˜ + +#### ๋ถ„๋ฅ˜ ๊ทœ์น™ + +| ์šฐ์„ ์ˆœ์œ„ | ๋ถ„๋ฅ˜ | ์กฐ๊ฑด | ๋น„๊ณ  | +|----------|------|------|------| +| **1์ˆœ์œ„** | ๋ฉ”์ธ ํ…Œ์ด๋ธ” | `screen_definitions.table_name` | ์ปดํฌ๋„ŒํŠธ ์ง์ ‘ ์—ฐ๊ฒฐ | +| **1์ˆœ์œ„** | ๋ฉ”์ธ ํ…Œ์ด๋ธ” | `v2-split-panel-layout.rightPanel.tableName` | WHERE ์กฐ๊ฑด ๋Œ€์ƒ | +| **2์ˆœ์œ„** | ์„œ๋ธŒ ํ…Œ์ด๋ธ” | ์กฐ์ธ์œผ๋กœ๋งŒ ์—ฐ๊ฒฐ๋œ ํ…Œ์ด๋ธ” | autocomplete ๋“ฑ ์ฐธ์กฐ | + +#### ํ•ต์‹ฌ ๊ทœ์น™ + +> **๋ฉ”์ธ ์กฐ๊ฑด์— ํ•ด๋‹นํ•˜๋ฉด, ์„œ๋ธŒ ์กฐ๊ฑด์ด ์žˆ์–ด๋„ ๋ฌด์กฐ๊ฑด ๋ฉ”์ธ์œผ๋กœ ๋ถ„๋ฅ˜** + +### ๋ฐฑ์—”๋“œ ๋ณ€๊ฒฝ (`screenGroupController.ts`) + +#### 1. screen_layouts_v2 ์ง€์› ์ถ”๊ฐ€ + +`rightPanelQuery`์— V2 ํ…Œ์ด๋ธ” UNION ์ถ”๊ฐ€: + +```sql +-- V1: screen_layouts์—์„œ ์กฐํšŒ +SELECT ... +FROM screen_definitions sd +JOIN screen_layouts sl ON sd.screen_id = sl.screen_id +WHERE sl.properties->'componentConfig'->'rightPanel'->'relation' IS NOT NULL + +UNION ALL + +-- V2: screen_layouts_v2์—์„œ ์กฐํšŒ (v2-split-panel-layout ์ปดํฌ๋„ŒํŠธ) +SELECT + sd.screen_id, + comp->'overrides'->>'type' as component_type, + comp->'overrides'->'rightPanel'->'relation' as right_panel_relation, + comp->'overrides'->'rightPanel'->>'tableName' as right_panel_table, + ... +FROM screen_definitions sd +JOIN screen_layouts_v2 slv2 ON sd.screen_id = slv2.screen_id, +jsonb_array_elements(slv2.layout_data->'components') as comp +WHERE comp->'overrides'->'rightPanel'->'relation' IS NOT NULL +``` + +#### 2. globalMainTables API ์ถ”๊ฐ€ + +`getScreenSubTables` ์‘๋‹ต์— ์ „์—ญ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์ถ”๊ฐ€: + +```sql +-- ๋ชจ๋“  ํ™”๋ฉด์˜ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ์ˆ˜์ง‘ +SELECT DISTINCT table_name as main_table FROM screen_definitions WHERE screen_id = ANY($1) +UNION +SELECT DISTINCT comp->'overrides'->'rightPanel'->>'tableName' as main_table +FROM screen_layouts_v2 ... +``` + +**์‘๋‹ต ๊ตฌ์กฐ:** +```typescript +res.json({ + success: true, + data: screenSubTables, + globalMainTables: globalMainTables, // ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์ถ”๊ฐ€ +}); +``` + +### ํ”„๋ก ํŠธ์—”๋“œ ๋ณ€๊ฒฝ (`ScreenRelationFlow.tsx`) + +#### 1. globalMainTables ์ƒํƒœ ์ถ”๊ฐ€ + +```typescript +const [globalMainTables, setGlobalMainTables] = useState>(new Set()); +``` + +#### 2. ์šฐ์„ ์ˆœ์œ„ ๊ธฐ๋ฐ˜ ํ…Œ์ด๋ธ” ๋ถ„๋ฅ˜ + +```typescript +// 1. globalMainTables๋ฅผ mainTableSet์— ๋จผ์ € ์ถ”๊ฐ€ (์šฐ์„ ์ˆœ์œ„ ์ ์šฉ) +globalMainTables.forEach((tableName) => { + if (!mainTableSet.has(tableName)) { + mainTableSet.add(tableName); + filterTableSet.add(tableName); // ๋ณด๋ผ์ƒ‰ ํ…Œ๋‘๋ฆฌ + } +}); + +// 2. ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์ˆ˜์ง‘ (mainTableSet์— ์—†๋Š” ๊ฒƒ๋งŒ) +screenSubData.subTables.forEach((subTable) => { + if (mainTableSet.has(subTable.tableName)) { + return; // ๋ฉ”์ธ ํ…Œ์ด๋ธ”์€ ์„œ๋ธŒ์—์„œ ์ œ์™ธ + } + subTableSet.add(subTable.tableName); +}); +``` + +### ์‹œ๊ฐ์  ๊ฒฐ๊ณผ + +#### ๋ณ€๊ฒฝ ์ „ + +``` +[ํ™”๋ฉด ๋…ธ๋“œ๋“ค] + โ”‚ + โ–ผ +[๋ฉ”์ธ ํ…Œ์ด๋ธ”: dept_info, user_info] โ† user_dept ์—†์Œ + โ”‚ + โ–ผ +[์„œ๋ธŒ ํ…Œ์ด๋ธ”: user_dept, customer_mng] โ† user_dept๊ฐ€ ์ž˜๋ชป ๋ฐฐ์น˜๋จ +``` + +#### ๋ณ€๊ฒฝ ํ›„ + +``` +[ํ™”๋ฉด ๋…ธ๋“œ๋“ค] + โ”‚ + โ–ผ +[๋ฉ”์ธ ํ…Œ์ด๋ธ”: dept_info, user_info, user_dept] โ† user_dept ๋ณด๋ผ์ƒ‰ ํ…Œ๋‘๋ฆฌ + โ”‚ + โ–ผ +[์„œ๋ธŒ ํ…Œ์ด๋ธ”: customer_mng] โ† ์กฐ์ธ ์ฐธ์กฐ์šฉ ํ…Œ์ด๋ธ”๋งŒ +``` + +### ๊ด€๋ จ ํŒŒ์ผ + +| ํŒŒ์ผ | ๋ณ€๊ฒฝ ๋‚ด์šฉ | +|------|----------| +| `backend-node/src/controllers/screenGroupController.ts` | screen_layouts_v2 UNION ์ถ”๊ฐ€, globalMainTables ๋ฐ˜ํ™˜ | +| `frontend/components/screen/ScreenRelationFlow.tsx` | globalMainTables ์ƒํƒœ, ์šฐ์„ ์ˆœ์œ„ ๋ถ„๋ฅ˜ ๋กœ์ง | +| `frontend/components/screen/ScreenNode.tsx` | isFilterTable prop ๋ฐ ๋ณด๋ผ์ƒ‰ ํ…Œ๋‘๋ฆฌ ์Šคํƒ€์ผ | + +--- + ## ํ™”๋ฉด ์„ค์ • ๋ชจ๋‹ฌ ๊ฐœ์„  (2026-01-12) ### ๊ฐœ์š” @@ -1742,4 +1897,6 @@ npm install react-zoom-pan-pinch - [๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ](.cursor/rules/multi-tenancy-guide.mdc) - [API ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ ๊ทœ์น™](.cursor/rules/api-client-usage.mdc) - [๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ์Šคํƒ€์ผ ๊ฐ€์ด๋“œ](.cursor/rules/admin-page-style-guide.mdc) +- [ํ™”๋ฉด ๋ณต์ œ V2 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš์„œ](../SCREEN_COPY_V2_MIGRATION_PLAN.md) - screen_layouts_v2 ๋ณต์ œ ๋กœ์ง +- [V2 ์ปดํฌ๋„ŒํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ถ„์„](../V2_COMPONENT_MIGRATION_ANALYSIS.md) - V2 ์•„ํ‚คํ…์ฒ˜ diff --git a/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md b/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md new file mode 100644 index 00000000..c60f1dfb --- /dev/null +++ b/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md @@ -0,0 +1,525 @@ +# ํ™”๋ฉด ๋ณต์ œ ๋กœ์ง V2 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš์„œ + +> ์ž‘์„ฑ์ผ: 2026-01-28 + +## 1. ํ˜„ํ™ฉ ๋ถ„์„ + +### 1.1 ํ˜„์žฌ ๋ณต์ œ ๋ฐฉ์‹ (Legacy) + +``` +ํ…Œ์ด๋ธ”: screen_layouts (๋‹ค์ค‘ ๋ ˆ์ฝ”๋“œ) +๋ฐฉ์‹: ํ™”๋ฉด๋‹น N๊ฐœ ๋ ˆ์ฝ”๋“œ (์ปดํฌ๋„ŒํŠธ ์ˆ˜๋งŒํผ) +์ €์žฅ: properties์— ์ „์ฒด ์„ค์ • "๋ฐ•์ œ" +``` + +**๋ฐ์ดํ„ฐ ๊ตฌ์กฐ:** +```sql +-- ํ™”๋ฉด๋‹น ์—ฌ๋Ÿฌ ๋ ˆ์ฝ”๋“œ +SELECT * FROM screen_layouts WHERE screen_id = 123; +-- layout_id | screen_id | component_type | component_id | properties (์ „์ฒด ์„ค์ •) +-- 1 | 123 | table-list | comp_001 | {"tableName": "user", "columns": [...], ...} +-- 2 | 123 | button | comp_002 | {"label": "์ €์žฅ", "variant": "default", ...} +``` + +### 1.2 V2 ๋ฐฉ์‹ + +``` +ํ…Œ์ด๋ธ”: screen_layouts_v2 (1๊ฐœ ๋ ˆ์ฝ”๋“œ) +๋ฐฉ์‹: ํ™”๋ฉด๋‹น 1๊ฐœ ๋ ˆ์ฝ”๋“œ (JSONB) +์ €์žฅ: url + overrides (์ฐจ์ด๊ฐ’๋งŒ) +``` + +**๋ฐ์ดํ„ฐ ๊ตฌ์กฐ:** +```sql +-- ํ™”๋ฉด๋‹น 1๊ฐœ ๋ ˆ์ฝ”๋“œ +SELECT layout_data FROM screen_layouts_v2 WHERE screen_id = 123; +-- { +-- "version": "2.0", +-- "components": [ +-- { "id": "comp_001", "url": "@/lib/registry/components/table-list", "overrides": {...} }, +-- { "id": "comp_002", "url": "@/lib/registry/components/button-primary", "overrides": {...} } +-- ] +-- } +``` + +--- + +## 2. ํ˜„์žฌ ๋ณต์ œ ๋กœ์ง ๋ถ„์„ + +### 2.1 ๋ณต์ œ ์ง„์ž…์  (2๊ณณ) + +| ๊ฒฝ๋กœ | ํŒŒ์ผ | ํ•จ์ˆ˜ | ์šฉ๋„ | +|-----|------|------|-----| +| ๋‹จ์ผ ํ™”๋ฉด ๋ณต์ œ | `screenManagementService.ts` | `copyScreen()` | ํ™”๋ฉด ๊ด€๋ฆฌ์—์„œ ๊ฐœ๋ณ„ ํ™”๋ฉด ๋ณต์ œ | +| ๋ฉ”๋‰ด ์ผ๊ด„ ๋ณต์ œ | `menuCopyService.ts` | `copyScreens()` | ๋ฉ”๋‰ด ๋ณต์ œ ์‹œ ์—ฐ๊ฒฐ๋œ ํ™”๋ฉด๋“ค ๋ณต์ œ | + +### 2.2 screenManagementService.copyScreen() ํ๋ฆ„ + +``` +1. screen_definitions ์กฐํšŒ (์›๋ณธ) +2. screen_definitions INSERT (๋Œ€์ƒ) +3. screen_layouts ์กฐํšŒ (์›๋ณธ) โ† Legacy +4. flowId ์ˆ˜์ง‘ ๋ฐ ๋ณต์ œ (ํšŒ์‚ฌ ๊ฐ„ ๋ณต์ œ ์‹œ) +5. numberingRuleId ์ˆ˜์ง‘ ๋ฐ ๋ณต์ œ (ํšŒ์‚ฌ ๊ฐ„ ๋ณต์ œ ์‹œ) +6. componentId ์žฌ์ƒ์„ฑ (idMapping) +7. properties ๋‚ด ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ (flowId, ruleId) +8. screen_layouts INSERT (๋Œ€์ƒ) โ† Legacy +``` + +**V2 ์ฒ˜๋ฆฌ: โŒ ์—†์Œ** + +### 2.3 menuCopyService.copyScreens() ํ๋ฆ„ + +``` +1๋‹จ๊ณ„: screen_definitions ์ฒ˜๋ฆฌ + - ๊ธฐ์กด ๋ณต์‚ฌ๋ณธ ์กด์žฌ ์‹œ: ์—…๋ฐ์ดํŠธ + - ์—†์œผ๋ฉด: ์‹ ๊ทœ ์ƒ์„ฑ + - screenIdMap ์ƒ์„ฑ + +2๋‹จ๊ณ„: screen_layouts ์ฒ˜๋ฆฌ + - ์›๋ณธ ์กฐํšŒ + - componentIdMap ์ƒ์„ฑ + - properties ๋‚ด ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ (screenId, flowId, ruleId, menuId) + - ๋ฐฐ์น˜ INSERT +``` + +**V2 ์ฒ˜๋ฆฌ: โŒ ์—†์Œ** + +### 2.4 ๋ณต์ œ ์‹œ ์ฒ˜๋ฆฌ๋˜๋Š” ์ฐธ์กฐ ID๋“ค + +| ์ฐธ์กฐ ID | ์„ค๋ช… | ๋งคํ•‘ ๋ฐฉ์‹ | +|--------|-----|----------| +| `componentId` | ์ปดํฌ๋„ŒํŠธ ๊ณ ์œ  ID | ์ƒˆ๋กœ ์ƒ์„ฑ (`comp_xxx`) | +| `parentId` | ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ ID | componentIdMap์œผ๋กœ ๋งคํ•‘ | +| `flowId` | ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ID | flowIdMap์œผ๋กœ ๋งคํ•‘ (ํšŒ์‚ฌ ๊ฐ„ ๋ณต์ œ ์‹œ) | +| `numberingRuleId` | ์ฑ„๋ฒˆ ๊ทœ์น™ ID | ruleIdMap์œผ๋กœ ๋งคํ•‘ (ํšŒ์‚ฌ ๊ฐ„ ๋ณต์ œ ์‹œ) | +| `screenId` (ํƒญ) | ํƒญ์—์„œ ์ฐธ์กฐํ•˜๋Š” ํ™”๋ฉด ID | screenIdMap์œผ๋กœ ๋งคํ•‘ | +| `menuObjid` | ๋ฉ”๋‰ด ID | menuIdMap์œผ๋กœ ๋งคํ•‘ | + +--- + +## 3. V2 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ ๋ณ€๊ฒฝ ํ•„์š” ์‚ฌํ•ญ + +### 3.1 ํ•ต์‹ฌ ๋ณ€๊ฒฝ์  + +| ํ•ญ๋ชฉ | Legacy | V2 | +|-----|--------|-----| +| ์ฝ๊ธฐ ํ…Œ์ด๋ธ” | `screen_layouts` | `screen_layouts_v2` | +| ์“ฐ๊ธฐ ํ…Œ์ด๋ธ” | `screen_layouts` | `screen_layouts_v2` | +| ๋ฐ์ดํ„ฐ ํ˜•ํƒœ | N๊ฐœ ๋ ˆ์ฝ”๋“œ | 1๊ฐœ JSONB | +| ID ๋งคํ•‘ ์œ„์น˜ | ๊ฐ ๋ ˆ์ฝ”๋“œ์˜ ์ปฌ๋Ÿผ | JSONB ๋‚ด๋ถ€ ์ˆœํšŒ | +| ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ | `properties` JSON | `overrides` JSON | + +### 3.2 ์ˆ˜์ •ํ•ด์•ผ ํ•  ํ•จ์ˆ˜๋“ค + +#### screenManagementService.ts + +| ํ•จ์ˆ˜ | ๋ณ€๊ฒฝ ๋‚ด์šฉ | +|-----|----------| +| `copyScreen()` | screen_layouts_v2 ๋ณต์ œ ๋กœ์ง ์ถ”๊ฐ€ | +| `collectFlowIdsFromLayouts()` | V2 JSONB ๊ตฌ์กฐ์—์„œ flowId ์ˆ˜์ง‘ | +| `collectNumberingRuleIdsFromLayouts()` | V2 JSONB ๊ตฌ์กฐ์—์„œ ruleId ์ˆ˜์ง‘ | +| `updateFlowIdsInProperties()` | V2 overrides ๋‚ด flowId ์—…๋ฐ์ดํŠธ | +| `updateNumberingRuleIdsInProperties()` | V2 overrides ๋‚ด ruleId ์—…๋ฐ์ดํŠธ | + +#### menuCopyService.ts + +| ํ•จ์ˆ˜ | ๋ณ€๊ฒฝ ๋‚ด์šฉ | +|-----|----------| +| `copyScreens()` | screen_layouts_v2 ๋ณต์ œ ๋กœ์ง ์ถ”๊ฐ€ | +| `hasLayoutChanges()` | V2 JSONB ๋น„๊ต ๋กœ์ง | +| `updateReferencesInProperties()` | V2 overrides ๋‚ด ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ | + +### 3.3 ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•  ํ•จ์ˆ˜๋“ค + +```typescript +// V2 ๋ ˆ์ด์•„์›ƒ ๋ณต์ œ (๊ณตํ†ต) +async copyLayoutV2( + sourceScreenId: number, + targetScreenId: number, + targetCompanyCode: string, + mappings: { + componentIdMap: Map; + flowIdMap: Map; + ruleIdMap: Map; + screenIdMap: Map; + menuIdMap?: Map; + }, + client: PoolClient +): Promise + +// V2 JSONB์—์„œ ์ฐธ์กฐ ID ์ˆ˜์ง‘ +collectReferencesFromLayoutV2(layoutData: any): { + flowIds: Set; + ruleIds: Set; + screenIds: Set; +} + +// V2 JSONB ๋‚ด ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ +updateReferencesInLayoutV2( + layoutData: any, + mappings: { ... } +): any +``` + +--- + +## 4. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „๋žต + +### 4.1 ์ „๋žต: V2 ์™„์ „ ์ „ํ™˜ + +``` +๊ฒฐ์ •: V2๋งŒ ๋ณต์ œ (Legacy ๋ณต์ œ ์ œ๊ฑฐ) +์ด์œ : ๊น”๋”ํ•œ ์ฝ”๋“œ, ์œ ์ง€๋ณด์ˆ˜ ์šฉ์ด, V2 ์•„ํ‚คํ…์ฒ˜ ์ผ๊ด€์„ฑ +์ „์ œ: ๊ธฐ์กด ํ™”๋ฉด๋“ค์€ ์ด๋ฏธ screen_layouts_v2๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ (1,347๊ฐœ 100%) +``` + +### 4.2 ๋‹จ๊ณ„๋ณ„ ๊ณ„ํš + +#### Phase 1: V2 ๋ณต์ œ ๋กœ์ง ๊ตฌํ˜„ ๋ฐ ์ „ํ™˜ + +``` +๋ชฉํ‘œ: Legacy ๋ณต์ œ๋ฅผ V2 ๋ณต์ œ๋กœ ์™„์ „ ๊ต์ฒด +์˜ํ–ฅ: ๋ณต์ œ ์‹œ screen_layouts_v2 ํ…Œ์ด๋ธ”๋งŒ ์‚ฌ์šฉ + +์ž‘์—…: +1. copyLayoutV2() ๊ณตํ†ต ํ•จ์ˆ˜ ๊ตฌํ˜„ +2. screenManagementService.copyScreen() - Legacy โ†’ V2 ๊ต์ฒด +3. menuCopyService.copyScreens() - Legacy โ†’ V2 ๊ต์ฒด +4. ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ฆ +``` + +#### Phase 2: Legacy ์ฝ”๋“œ ์ •๋ฆฌ + +``` +๋ชฉํ‘œ: ๋ถˆํ•„์š”ํ•œ Legacy ๋ณต์ œ ์ฝ”๋“œ ์ œ๊ฑฐ +์˜ํ–ฅ: ์ฝ”๋“œ ๊ฐ„์†Œํ™” + +์ž‘์—…: +1. screen_layouts ๊ด€๋ จ ๋ณต์ œ ์ฝ”๋“œ ์ œ๊ฑฐ +2. ๊ด€๋ จ ํ—ฌํผ ํ•จ์ˆ˜ ์ •๋ฆฌ (collectFlowIdsFromLayouts ๋“ฑ) +3. ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋ฐ ์ •๋ฆฌ +``` + +#### Phase 3: Legacy ํ…Œ์ด๋ธ” ์ •๋ฆฌ (์„ ํƒ, ์ถ”ํ›„) + +``` +๋ชฉํ‘œ: ๋ถˆํ•„์š”ํ•œ ํ…Œ์ด๋ธ” ์ œ๊ฑฐ +์˜ํ–ฅ: ๋ฐ์ดํ„ฐ ์ •๋ฆฌ + +์ž‘์—…: +1. screen_layouts ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋ฐฑ์—… +2. screen_layouts ํ…Œ์ด๋ธ” ์‚ญ์ œ (๋˜๋Š” ๋ณด๊ด€) +3. ๊ด€๋ จ ์ฝ”๋“œ ์ •๋ฆฌ +``` + +--- + +## 5. ์ƒ์„ธ ๊ตฌํ˜„ ๊ณ„ํš + +### 5.1 Phase 1 ์ž‘์—… ๋ชฉ๋ก + +| # | ์ž‘์—… | ํŒŒ์ผ | ์˜ˆ์ƒ ๊ณต์ˆ˜ | +|---|-----|------|---------| +| 1 | `copyLayoutV2()` ๊ณตํ†ต ํ•จ์ˆ˜ ๊ตฌํ˜„ | screenManagementService.ts | 2์‹œ๊ฐ„ | +| 2 | `collectReferencesFromLayoutV2()` ๊ตฌํ˜„ | screenManagementService.ts | 1์‹œ๊ฐ„ | +| 3 | `updateReferencesInLayoutV2()` ๊ตฌํ˜„ | screenManagementService.ts | 2์‹œ๊ฐ„ | +| 4 | `copyScreen()` - Legacy ์ œ๊ฑฐ, V2๋กœ ๊ต์ฒด | screenManagementService.ts | 2์‹œ๊ฐ„ | +| 5 | `copyScreens()` - Legacy ์ œ๊ฑฐ, V2๋กœ ๊ต์ฒด | menuCopyService.ts | 3์‹œ๊ฐ„ | +| 6 | ๋‹จ์œ„ ํ…Œ์ŠคํŠธ | - | 2์‹œ๊ฐ„ | +| 7 | ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ | - | 2์‹œ๊ฐ„ | + +**์ด ์˜ˆ์ƒ ๊ณต์ˆ˜: 14์‹œ๊ฐ„ (์•ฝ 2์ผ)** + +### 5.2 ์ฃผ์š” ๋ณ€๊ฒฝ ํฌ์ธํŠธ + +#### copyScreen() ๋ณ€๊ฒฝ ์ „ํ›„ + +**Before (Legacy):** +```typescript +// 4. ์›๋ณธ ํ™”๋ฉด์˜ ๋ ˆ์ด์•„์›ƒ ์ •๋ณด ์กฐํšŒ +const sourceLayoutsResult = await client.query( + `SELECT * FROM screen_layouts WHERE screen_id = $1`, + [sourceScreenId] +); +// ... N๊ฐœ ๋ ˆ์ฝ”๋“œ ์ˆœํšŒํ•˜๋ฉฐ INSERT +``` + +**After (V2):** +```typescript +// 4. ์›๋ณธ V2 ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ +const sourceLayoutV2 = await client.query( + `SELECT layout_data FROM screen_layouts_v2 + WHERE screen_id = $1 AND company_code = $2`, + [sourceScreenId, sourceCompanyCode] +); +// ... JSONB ๋ณ€ํ™˜ ํ›„ 1๊ฐœ ๋ ˆ์ฝ”๋“œ INSERT +``` + +#### copyScreens() ๋ณ€๊ฒฝ ์ „ํ›„ + +**Before (Legacy):** +```typescript +// ๋ ˆ์ด์•„์›ƒ ๋ฐฐ์น˜ INSERT +await client.query( + `INSERT INTO screen_layouts (...) VALUES ${layoutValues.join(", ")}`, + layoutParams +); +``` + +**After (V2):** +```typescript +// V2 ๋ ˆ์ด์•„์›ƒ UPSERT +await this.copyLayoutV2( + originalScreenId, targetScreenId, sourceCompanyCode, targetCompanyCode, + { componentIdMap, flowIdMap, ruleIdMap, screenIdMap, menuIdMap }, + client +); +``` + +### 5.2 copyLayoutV2() ๊ตฌํ˜„ ๋ฐฉ์•ˆ + +```typescript +private async copyLayoutV2( + sourceScreenId: number, + targetScreenId: number, + sourceCompanyCode: string, + targetCompanyCode: string, + mappings: { + componentIdMap: Map; + flowIdMap?: Map; + ruleIdMap?: Map; + screenIdMap?: Map; + menuIdMap?: Map; + }, + client: PoolClient +): Promise { + // 1. ์›๋ณธ V2 ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ + const sourceResult = await client.query( + `SELECT layout_data FROM screen_layouts_v2 + WHERE screen_id = $1 AND company_code = $2`, + [sourceScreenId, sourceCompanyCode] + ); + + if (sourceResult.rows.length === 0) { + // V2 ๋ ˆ์ด์•„์›ƒ ์—†์œผ๋ฉด ์Šคํ‚ต (Legacy๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ) + return; + } + + const layoutData = sourceResult.rows[0].layout_data; + + // 2. components ๋ฐฐ์—ด ์ˆœํšŒํ•˜๋ฉฐ ID ๋งคํ•‘ + const updatedComponents = layoutData.components.map((comp: any) => { + const newId = mappings.componentIdMap.get(comp.id) || comp.id; + + // overrides ๋‚ด ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ + let updatedOverrides = { ...comp.overrides }; + + // flowId ๋งคํ•‘ + if (mappings.flowIdMap && updatedOverrides.flowId) { + const newFlowId = mappings.flowIdMap.get(updatedOverrides.flowId); + if (newFlowId) updatedOverrides.flowId = newFlowId; + } + + // numberingRuleId ๋งคํ•‘ + if (mappings.ruleIdMap && updatedOverrides.numberingRuleId) { + const newRuleId = mappings.ruleIdMap.get(updatedOverrides.numberingRuleId); + if (newRuleId) updatedOverrides.numberingRuleId = newRuleId; + } + + // screenId ๋งคํ•‘ (ํƒญ ์ปดํฌ๋„ŒํŠธ ๋“ฑ) + if (mappings.screenIdMap && updatedOverrides.screenId) { + const newScreenId = mappings.screenIdMap.get(updatedOverrides.screenId); + if (newScreenId) updatedOverrides.screenId = newScreenId; + } + + // tabs ๋ฐฐ์—ด ๋‚ด screenId ๋งคํ•‘ + if (mappings.screenIdMap && Array.isArray(updatedOverrides.tabs)) { + updatedOverrides.tabs = updatedOverrides.tabs.map((tab: any) => ({ + ...tab, + screenId: mappings.screenIdMap.get(tab.screenId) || tab.screenId + })); + } + + return { + ...comp, + id: newId, + overrides: updatedOverrides + }; + }); + + const newLayoutData = { + ...layoutData, + components: updatedComponents, + updatedAt: new Date().toISOString() + }; + + // 3. ๋Œ€์ƒ V2 ๋ ˆ์ด์•„์›ƒ ์ €์žฅ (UPSERT) + await client.query( + `INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data, created_at, updated_at) + VALUES ($1, $2, $3, NOW(), NOW()) + ON CONFLICT (screen_id, company_code) + DO UPDATE SET layout_data = $3, updated_at = NOW()`, + [targetScreenId, targetCompanyCode, JSON.stringify(newLayoutData)] + ); +} +``` + +--- + +## 6. ํ…Œ์ŠคํŠธ ๊ณ„ํš + +### 6.1 ๋‹จ์œ„ ํ…Œ์ŠคํŠธ + +| ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค | ์„ค๋ช… | +|-------------|------| +| V2 ๋ ˆ์ด์•„์›ƒ ๋ณต์ œ - ๊ธฐ๋ณธ | ๋‹จ์ˆœ ์ปดํฌ๋„ŒํŠธ ๋ณต์ œ | +| V2 ๋ ˆ์ด์•„์›ƒ ๋ณต์ œ - flowId ๋งคํ•‘ | ํšŒ์‚ฌ ๊ฐ„ ๋ณต์ œ ์‹œ flowId ๋ณ€๊ฒฝ ํ™•์ธ | +| V2 ๋ ˆ์ด์•„์›ƒ ๋ณต์ œ - ruleId ๋งคํ•‘ | ํšŒ์‚ฌ ๊ฐ„ ๋ณต์ œ ์‹œ ruleId ๋ณ€๊ฒฝ ํ™•์ธ | +| V2 ๋ ˆ์ด์•„์›ƒ ๋ณต์ œ - ํƒญ screenId ๋งคํ•‘ | ํƒญ ์ปดํฌ๋„ŒํŠธ์˜ screenId ๋ณ€๊ฒฝ ํ™•์ธ | +| V2 ๋ ˆ์ด์•„์›ƒ ์—†๋Š” ๊ฒฝ์šฐ | Legacy๋งŒ ์žˆ๋Š” ํ™”๋ฉด ๋ณต์ œ ์‹œ ์Šคํ‚ต ํ™•์ธ | + +### 6.2 ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ + +| ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค | ์„ค๋ช… | +|-------------|------| +| ๋‹จ์ผ ํ™”๋ฉด ๋ณต์ œ (๊ฐ™์€ ํšŒ์‚ฌ) | copyScreen() - ๋™์ผ ํšŒ์‚ฌ ๋‚ด ๋ณต์ œ | +| ๋‹จ์ผ ํ™”๋ฉด ๋ณต์ œ (๋‹ค๋ฅธ ํšŒ์‚ฌ) | copyScreen() - ํšŒ์‚ฌ ๊ฐ„ ๋ณต์ œ | +| ๋ฉ”๋‰ด ์ผ๊ด„ ๋ณต์ œ | copyScreens() - ์—ฌ๋Ÿฌ ํ™”๋ฉด ๋™์‹œ ๋ณต์ œ | +| ๋ชจ๋‹ฌ ํฌํ•จ ๋ณต์ œ | copyScreenWithModals() - ๋ฉ”์ธ + ๋ชจ๋‹ฌ ๋ณต์ œ | + +### 6.3 ๊ฒ€์ฆ ํ•ญ๋ชฉ + +``` +๋ณต์ œ ํ›„ ํ™•์ธ: +- [ ] screen_layouts_v2์— ๋ ˆ์ฝ”๋“œ ์ƒ์„ฑ๋จ +- [ ] componentId๊ฐ€ ์ƒˆ๋กœ ์ƒ์„ฑ๋จ +- [ ] flowId๊ฐ€ ์ •ํ™•ํžˆ ๋งคํ•‘๋จ +- [ ] numberingRuleId๊ฐ€ ์ •ํ™•ํžˆ ๋งคํ•‘๋จ +- [ ] ํƒญ ์ปดํฌ๋„ŒํŠธ์˜ screenId๊ฐ€ ์ •ํ™•ํžˆ ๋งคํ•‘๋จ +- [ ] screen_layouts(Legacy)๋Š” ๋ณต์ œ๋˜์ง€ ์•Š์Œ +- [ ] ๋ณต์ œ๋œ ํ™”๋ฉด์ด ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ •์ƒ ๋กœ๋“œ๋จ +- [ ] ๋ณต์ œ๋œ ํ™”๋ฉด ํŽธ์ง‘/์ €์žฅ ์ •์ƒ ๋™์ž‘ +``` + +--- + +## 7. ์˜ํ–ฅ ๋ถ„์„ + +### 7.1 ์˜ํ–ฅ ๋ฐ›๋Š” ๊ธฐ๋Šฅ + +| ๊ธฐ๋Šฅ | ์˜ํ–ฅ | ๋น„๊ณ  | +|-----|-----|-----| +| ํ™”๋ฉด ๊ด€๋ฆฌ - ํ™”๋ฉด ๋ณต์ œ | ์ง์ ‘ ์˜ํ–ฅ | copyScreen() | +| ํ™”๋ฉด ๊ด€๋ฆฌ - ๊ทธ๋ฃน ๋ณต์ œ | ์ง์ ‘ ์˜ํ–ฅ | copyScreenWithModals() | +| ๋ฉ”๋‰ด ๋ณต์ œ | ์ง์ ‘ ์˜ํ–ฅ | menuCopyService.copyScreens() | +| ํ™”๋ฉด ๋””์ž์ด๋„ˆ | ๊ฐ„์ ‘ ์˜ํ–ฅ | ๋ณต์ œ๋œ ํ™”๋ฉด ๋กœ๋“œ ์‹œ V2 ์‚ฌ์šฉ | + +### 7.2 ๋กค๋ฐฑ ๊ณ„ํš + +``` +V2 ์ „ํ™˜ ๋กค๋ฐฑ (ํ•„์š”์‹œ): +1. Git์—์„œ ์ด์ „ ๋ฒ„์ „ ๋ณต์› (copyScreen, copyScreens) +2. Legacy ๋ณต์ œ ์ฝ”๋“œ ๋ณต์› +3. ํ…Œ์ŠคํŠธ ํ›„ ๋ฐฐํฌ + +์ฃผ์˜์‚ฌํ•ญ: +- V2๋กœ ๋ณต์ œ๋œ ํ™”๋ฉด๋“ค์€ screen_layouts_v2์—๋งŒ ๋ฐ์ดํ„ฐ ์กด์žฌ +- ๋กค๋ฐฑ ์‹œ ํ•ด๋‹น ํ™”๋ฉด๋“ค์€ screen_layouts์— ๋ฐ์ดํ„ฐ ์—†์Œ +- ํ•„์š”์‹œ V2 โ†’ Legacy ์—ญ๋ณ€ํ™˜ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ +``` + +--- + +## 8. ๊ด€๋ จ ํŒŒ์ผ + +### 8.1 ์ˆ˜์ • ๋Œ€์ƒ + +| ํŒŒ์ผ | ๋ณ€๊ฒฝ ๋‚ด์šฉ | +|-----|----------| +| `backend-node/src/services/screenManagementService.ts` | copyLayoutV2(), copyScreen() ์ˆ˜์ • | +| `backend-node/src/services/menuCopyService.ts` | copyScreens() ์ˆ˜์ • | + +### 8.2 ์ฐธ๊ณ  ํŒŒ์ผ + +| ํŒŒ์ผ | ์„ค๋ช… | +|-----|-----| +| `docs/COMPONENT_LAYOUT_V2_ARCHITECTURE.md` | V2 ์•„ํ‚คํ…์ฒ˜ ๋ฌธ์„œ | +| `frontend/lib/api/screen.ts` | getLayoutV2, saveLayoutV2 | +| `frontend/lib/utils/layoutV2Converter.ts` | V2 ๋ณ€ํ™˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ | + +--- + +## 9. ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### 9.1 ๊ฐœ๋ฐœ ์ „ + +- [ ] V2 ์•„ํ‚คํ…์ฒ˜ ๋ฌธ์„œ ์ˆ™์ง€ +- [ ] ํ˜„์žฌ ๋ณต์ œ ๋กœ์ง ์ฝ”๋“œ ๋ฆฌ๋ทฐ +- [ ] ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„ (V2 ๋ ˆ์ด์•„์›ƒ์ด ์žˆ๋Š” ํ™”๋ฉด) + +### 9.2 Phase 1 ์™„๋ฃŒ ์กฐ๊ฑด + +- [x] copyLayoutV2() ํ•จ์ˆ˜ ๊ตฌํ˜„ โœ… 2026-01-28 +- [x] collectReferencesFromLayoutV2() ํ•จ์ˆ˜ ๊ตฌํ˜„ โœ… 2026-01-28 +- [x] updateReferencesInLayoutV2() ํ•จ์ˆ˜ ๊ตฌํ˜„ โœ… 2026-01-28 +- [x] copyScreen() - Legacy ์ œ๊ฑฐ, V2๋กœ ๊ต์ฒด โœ… 2026-01-28 +- [x] copyScreens() - Legacy ์ œ๊ฑฐ, V2๋กœ ๊ต์ฒด โœ… 2026-01-28 +- [x] hasLayoutChangesV2() ํ•จ์ˆ˜ ์ถ”๊ฐ€ โœ… 2026-01-28 +- [x] updateTabScreenReferences() V2 ์ง€์› ์ถ”๊ฐ€ โœ… 2026-01-28 +- [x] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ โœ… 2026-01-30 +- [x] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ โœ… 2026-01-30 +- [x] V2 ์ „์šฉ ๋ณต์ œ ๋™์ž‘ ํ™•์ธ โœ… 2026-01-30 + +### 9.3 Phase 2 ์™„๋ฃŒ ์กฐ๊ฑด + +- [ ] Legacy ๊ด€๋ จ ํ—ฌํผ ํ•จ์ˆ˜ ์ •๋ฆฌ +- [ ] ๋ถˆํ•„์š”ํ•œ ์ฝ”๋“œ ์ œ๊ฑฐ +- [ ] ์ฝ”๋“œ ๋ฆฌ๋ทฐ ์™„๋ฃŒ +- [ ] ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ + +--- + +## 10. ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๊ฒ€์ฆ ๊ฒฐ๊ณผ + +### 10.1 ๊ฒ€์ฆ๋œ ์‹œ๋‚˜๋ฆฌ์˜ค + +| ์‹œ๋‚˜๋ฆฌ์˜ค | ๊ฒฐ๊ณผ | ๋น„๊ณ  | +|---------|------|------| +| ๊ฐ™์€ ํšŒ์‚ฌ ๋‚ด ๋ณต์ œ | โœ… ์ •์ƒ | componentId๋งŒ ์ƒˆ๋กœ ์ƒ์„ฑ | +| ํšŒ์‚ฌ ๊ฐ„ ๋ณต์ œ (flowId ๋งคํ•‘) | โœ… ์ •์ƒ | flowIdMap ์ ์šฉ๋จ | +| ํšŒ์‚ฌ ๊ฐ„ ๋ณต์ œ (ruleId ๋งคํ•‘) | โœ… ์ •์ƒ | ruleIdMap ์ ์šฉ๋จ | +| ํƒญ ์ปดํฌ๋„ŒํŠธ screenId ๋งคํ•‘ | โœ… ์ •์ƒ | updateTabScreenReferences V2 ์ง€์› ์ถ”๊ฐ€ | +| V2 ๋ ˆ์ด์•„์›ƒ ์—†๋Š” ํ™”๋ฉด | โœ… ์ •์ƒ | ์Šคํ‚ต ์ฒ˜๋ฆฌ | + +### 10.2 ๋ฐœ๊ฒฌ ๋ฐ ์ˆ˜์ •๋œ ๋ฌธ์ œ + +| ๋ฌธ์ œ | ํ•ด๊ฒฐ | +|-----|------| +| updateTabScreenReferences๊ฐ€ V2 ๋ฏธ์ง€์› | V2 ์ฒ˜๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€ ์™„๋ฃŒ | + +### 10.3 Zod ํ™œ์šฉ ๊ฐ€๋Šฅ์„ฑ + +ํ”„๋ก ํŠธ์—”๋“œ์— ์ด๋ฏธ ํ›Œ๋ฅญํ•œ Zod ์œ ํ‹ธ๋ฆฌํ‹ฐ ์กด์žฌ: +- `deepMerge()` - ๊นŠ์€ ๋ณ‘ํ•ฉ +- `extractCustomConfig()` - ์ฐจ์ด๊ฐ’ ์ถ”์ถœ +- `loadComponentV2()` / `saveComponentV2()` - V2 ๋กœ๋“œ/์ €์žฅ + +ํ–ฅํ›„ ๋ฐฑ์—”๋“œ์—๋„ Zod ์ถ”๊ฐ€ ์‹œ: +- ํƒ€์ž… ์•ˆ์ „์„ฑ ํ–ฅ์ƒ +- ํ”„๋ก ํŠธ/๋ฐฑ์—”๋“œ ์Šคํ‚ค๋งˆ ๊ณต์œ  ๊ฐ€๋Šฅ +- ๋ฒ”์šฉ ์ฐธ์กฐ ํƒ์ƒ‰ ๋กœ์ง์œผ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ ์ œ๊ฑฐ ๊ฐ€๋Šฅ + +--- + +## 11. ๋ณ€๊ฒฝ ์ด๋ ฅ + +| ๋‚ ์งœ | ๋ณ€๊ฒฝ ๋‚ด์šฉ | ์ž‘์„ฑ์ž | +|-----|----------|-------| +| 2026-01-28 | ์ดˆ์•ˆ ์ž‘์„ฑ | Claude | +| 2026-01-28 | V2 ์™„์ „ ์ „ํ™˜ ์ „๋žต์œผ๋กœ ๋ณ€๊ฒฝ (๋ณ‘ํ–‰ ์šด์˜ โ†’ V2 ์ „์šฉ) | Claude | +| 2026-01-28 | Phase 1 ๊ตฌํ˜„ ์™„๋ฃŒ - V2 ๋ณต์ œ ํ•จ์ˆ˜๋“ค ๊ตฌํ˜„ ๋ฐ Legacy ๊ต์ฒด | Claude | +| 2026-01-28 | ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๊ฒ€์ฆ - updateTabScreenReferences V2 ์ง€์› ์ถ”๊ฐ€ | Claude | +| 2026-01-28 | V2 ๊ฒฝ๋กœ ์ง€์› ์ถ”๊ฐ€ - action/sections ์ง์ ‘ ๊ฒฝ๋กœ (componentConfig ์—†์ด) | Claude | +| 2026-01-30 | **์‹ค์ œ ์ฝ”๋“œ ๊ตฌํ˜„ ์™„๋ฃŒ** - copyScreen(), copyScreens() V2 ์ „ํ™˜ | Claude | +| 2026-01-30 | **Phase 1 ํ…Œ์ŠคํŠธ ์™„๋ฃŒ** - ๋‹จ์œ„/ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ ํ™•์ธ | Claude | \ No newline at end of file diff --git a/docs/V2_COMPONENT_MIGRATION_ANALYSIS.md b/docs/V2_COMPONENT_MIGRATION_ANALYSIS.md new file mode 100644 index 00000000..591e243b --- /dev/null +++ b/docs/V2_COMPONENT_MIGRATION_ANALYSIS.md @@ -0,0 +1,356 @@ +# V2 ์ปดํฌ๋„ŒํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ถ„์„ ๋ณด๊ณ ์„œ + +> ์ž‘์„ฑ์ผ: 2026-01-27 +> ๋ชฉ์ : ๋ฏธ๊ตฌํ˜„ V1 ์ปดํฌ๋„ŒํŠธ๋“ค์˜ V2 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€๋Šฅ์„ฑ ๋ถ„์„ + +--- + +## 1. ํ˜„ํ™ฉ ์š”์•ฝ + +| ๊ตฌ๋ถ„ | ๊ฐœ์ˆ˜ | ๋น„์œจ | +|------|------|------| +| V1 ์ด ์ปดํฌ๋„ŒํŠธ | 7,170๊ฐœ | 100% | +| V2 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ | 5,212๊ฐœ | 72.7% | +| **๋ฏธ๊ตฌํ˜„ (๋ถ„์„ ๋Œ€์ƒ)** | **~520๊ฐœ** | **7.3%** | + +--- + +## 2. ๋ฏธ๊ตฌํ˜„ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ธ ๋ถ„์„ + +### 2.1 โœ… ํ†ตํ•ฉ ๊ฐ€๋Šฅ (๊ธฐ์กด V2 ์ปดํฌ๋„ŒํŠธ๋กœ ๋Œ€์ฒด) + +#### 2.1.1 `unified-list` (97๊ฐœ) โ†’ `v2-table-list` + +**๋ถ„์„ ๊ฒฐ๊ณผ**: โœ… **ํ†ตํ•ฉ ๊ฐ€๋Šฅ** + +| ํ•ญ๋ชฉ | unified-list | v2-table-list | +|------|-------------|---------------| +| ํ…Œ์ด๋ธ” ๋ทฐ | โœ… | โœ… | +| ์นด๋“œ ๋ทฐ | โœ… | โŒ (์ถ”๊ฐ€ ํ•„์š”) | +| ๊ฒ€์ƒ‰ | โœ… | โœ… | +| ํŽ˜์ด์ง€๋„ค์ด์…˜ | โœ… | โœ… | +| ํŽธ์ง‘ ๊ฐ€๋Šฅ | โœ… | โœ… | + +**๊ฒฐ๋ก **: `v2-table-list`์— `cardView` ๋ชจ๋“œ๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ํ†ตํ•ฉ ๊ฐ€๋Šฅ. ๋˜๋Š” DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์œผ๋กœ `v2-table-list`๋กœ ๋ณ€ํ™˜. + +**์ž‘์—…๋Ÿ‰**: ์ค‘๊ฐ„ (v2-table-list ํ™•์žฅ ๋˜๋Š” DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜) + +--- + +#### 2.1.2 `autocomplete-search-input` (50๊ฐœ) โ†’ `v2-select` + +**๋ถ„์„ ๊ฒฐ๊ณผ**: โœ… **ํ†ตํ•ฉ ๊ฐ€๋Šฅ** + +| ํ•ญ๋ชฉ | autocomplete-search-input | v2-select | +|------|--------------------------|-----------| +| ์ž๋™์™„์„ฑ ๋“œ๋กญ๋‹ค์šด | โœ… | โœ… (mode: autocomplete) | +| ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๊ฒ€์ƒ‰ | โœ… | โœ… (dataSource ์„ค์ •) | +| ํ‘œ์‹œ/๊ฐ’ ํ•„๋“œ ๋ถ„๋ฆฌ | โœ… | โœ… | + +**๊ฒฐ๋ก **: `v2-select`์˜ `mode: "autocomplete"` ๋˜๋Š” `mode: "combobox"`๋กœ ๋Œ€์ฒด ๊ฐ€๋Šฅ. + +**์ž‘์—…๋Ÿ‰**: ๋‚ฎ์Œ (DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋งŒ) + +--- + +#### 2.1.3 `repeater-field-group` (24๊ฐœ) โ†’ `v2-repeater` + +**๋ถ„์„ ๊ฒฐ๊ณผ**: โœ… **ํ†ตํ•ฉ ๊ฐ€๋Šฅ** + +`v2-repeater`๊ฐ€ ์ด๋ฏธ ๋‹ค์Œ์„ ์ง€์›: +- ์ธ๋ผ์ธ ํ…Œ์ด๋ธ” ๋ชจ๋“œ +- ๋ชจ๋‹ฌ ์„ ํƒ ๋ชจ๋“œ +- ๋ฒ„ํŠผ ๋ชจ๋“œ + +**๊ฒฐ๋ก **: `v2-repeater`์˜ `renderMode: "inline"`์œผ๋กœ ๋Œ€์ฒด. + +**์ž‘์—…๋Ÿ‰**: ๋‚ฎ์Œ (DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋งŒ) + +--- + +#### 2.1.4 `simple-repeater-table` (1๊ฐœ) โ†’ `v2-repeater` + +**๋ถ„์„ ๊ฒฐ๊ณผ**: โœ… **ํ†ตํ•ฉ ๊ฐ€๋Šฅ** + +**๊ฒฐ๋ก **: `v2-repeater`๋กœ ๋Œ€์ฒด. + +**์ž‘์—…๋Ÿ‰**: ๋งค์šฐ ๋‚ฎ์Œ + +--- + +### 2.2 โš ๏ธ Renderer ์ถ”๊ฐ€๋งŒ ํ•„์š” (์ฝ”๋“œ ๊ตฌ์กฐ ์žˆ์Œ) + +#### 2.2.1 `split-panel-layout2` (8๊ฐœ) + +**๋ถ„์„ ๊ฒฐ๊ณผ**: โš ๏ธ **Renderer ์ถ”๊ฐ€ ํ•„์š”** + +- V1 Renderer: `SplitPanelLayout2Renderer.tsx` โœ… ์กด์žฌ +- V2 Renderer: โŒ ์—†์Œ +- Component: `SplitPanelLayout2Component.tsx` โœ… ์กด์žฌ + +**๊ฒฐ๋ก **: V2 ํ˜•์‹์œผ๋กœ DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋งŒ ํ•˜๋ฉด ๋จ (๊ธฐ์กด Renderer๊ฐ€ `split-panel-layout2` ID๋กœ ๋“ฑ๋ก๋จ). + +**์ž‘์—…๋Ÿ‰**: ๋งค์šฐ ๋‚ฎ์Œ (DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋งŒ) + +--- + +#### 2.2.2 `repeat-screen-modal` (7๊ฐœ) + +**๋ถ„์„ ๊ฒฐ๊ณผ**: โš ๏ธ **Renderer ์ถ”๊ฐ€ ํ•„์š”** + +- V1 Renderer: `RepeatScreenModalRenderer.tsx` โœ… ์กด์žฌ +- ์ •์˜: `hidden: true` (v2-repeat-screen-modal ์‚ฌ์šฉ์œผ๋กœ ํŒจ๋„์—์„œ ์ˆจ๊น€) + +**๊ฒฐ๋ก **: ๊ธฐ์กด Renderer ์‚ฌ์šฉ ๊ฐ€๋Šฅ, DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋งŒ. + +**์ž‘์—…๋Ÿ‰**: ๋งค์šฐ ๋‚ฎ์Œ + +--- + +#### 2.2.3 `related-data-buttons` (5๊ฐœ) + +**๋ถ„์„ ๊ฒฐ๊ณผ**: โš ๏ธ **Renderer ์ถ”๊ฐ€ ํ•„์š”** + +- V1 Renderer: `RelatedDataButtonsRenderer.tsx` โœ… ์กด์žฌ +- Component: `RelatedDataButtonsComponent.tsx` โœ… ์กด์žฌ + +**๊ฒฐ๋ก **: ๊ธฐ์กด Renderer ์‚ฌ์šฉ ๊ฐ€๋Šฅ, DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋งŒ. + +**์ž‘์—…๋Ÿ‰**: ๋งค์šฐ ๋‚ฎ์Œ + +--- + +### 2.3 โŒ ๋ณ„๋„ V2 ๊ฐœ๋ฐœ ํ•„์š” (๋ณต์žกํ•œ ๊ตฌ์กฐ) + +#### 2.3.1 `entity-search-input` (99๊ฐœ) + +**๋ถ„์„ ๊ฒฐ๊ณผ**: โŒ **๋ณ„๋„ ๊ฐœ๋ฐœ ํ•„์š”** + +**ํŠน์ง•**: +```typescript +// ๋ชจ๋‹ฌ ๊ธฐ๋ฐ˜ ์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰ +- ํ…Œ์ด๋ธ” ์„ ํƒ (tableName) +- ๊ฒ€์ƒ‰ ํ•„๋“œ ์„ค์ • (searchFields) +- ๋ชจ๋‹ฌ ํŒ์—… (modalTitle, modalColumns) +- ๊ฐ’/ํ‘œ์‹œ ํ•„๋“œ ๋ถ„๋ฆฌ (valueField, displayField) +- ์ถ”๊ฐ€ ์ •๋ณด ํ‘œ์‹œ (additionalFields) +``` + +**๋ณต์žก๋„ ์š”์ธ**: +1. ๋ชจ๋‹ฌ ๊ฒ€์ƒ‰ UI๊ฐ€ ํ•„์š” +2. ๋‹ค์–‘ํ•œ ํ…Œ์ด๋ธ” ์—ฐ๋™ +3. ์ถ”๊ฐ€ ํ•„๋“œ ์—ฐ๊ณ„ ๋กœ์ง + +**๊ถŒ์žฅ ๋ฐฉ์•ˆ**: +- `v2-entity-search` ์ƒˆ๋กœ ๊ฐœ๋ฐœ +- ๋˜๋Š” `v2-select`์— `mode: "entity"` ์ถ”๊ฐ€ + +**์ž‘์—…๋Ÿ‰**: ๋†’์Œ (1-2์ผ) + +--- + +#### 2.3.2 `modal-repeater-table` (68๊ฐœ) + +**๋ถ„์„ ๊ฒฐ๊ณผ**: โŒ **๋ณ„๋„ ๊ฐœ๋ฐœ ํ•„์š”** + +**ํŠน์ง•**: +```typescript +// ๋ชจ๋‹ฌ์—์„œ ํ•ญ๋ชฉ ๊ฒ€์ƒ‰ + ๋™์  ํ…Œ์ด๋ธ” +- ์†Œ์Šค ํ…Œ์ด๋ธ” (sourceTable, sourceColumns) +- ๋ชจ๋‹ฌ ๊ฒ€์ƒ‰ (modalTitle, modalButtonText, multiSelect) +- ๋™์  ์ปฌ๋Ÿผ ์ถ”๊ฐ€ (columns) +- ๊ณ„์‚ฐ ๊ทœ์น™ (calculationRules) +- ๊ณ ์œ  ํ•„๋“œ (uniqueField) +``` + +**๋ณต์žก๋„ ์š”์ธ**: +1. ๋ชจ๋‹ฌ ๊ฒ€์ƒ‰ + ์„ ํƒ +2. ๋™์  ํ…Œ์ด๋ธ” ํ–‰ ์ถ”๊ฐ€/์‚ญ์ œ +3. ๊ณ„์‚ฐ ๊ทœ์น™ (๋‹จ๊ฐ€ ร— ์ˆ˜๋Ÿ‰ = ๊ธˆ์•ก) +4. ์ค‘๋ณต ๋ฐฉ์ง€ ๋กœ์ง + +**๊ถŒ์žฅ ๋ฐฉ์•ˆ**: +- `v2-repeater`์˜ `modal` ๋ชจ๋“œ ํ™•์žฅ +- `ItemSelectionModal` + `RepeaterTable` ์žฌ์‚ฌ์šฉ + +**์ž‘์—…๋Ÿ‰**: ์ค‘๊ฐ„ (v2-repeater๊ฐ€ ์ด๋ฏธ ๊ธฐ๋ฐ˜ ์ œ๊ณต) + +--- + +#### 2.3.3 `selected-items-detail-input` (83๊ฐœ) + +**๋ถ„์„ ๊ฒฐ๊ณผ**: โŒ **๋ณ„๋„ ๊ฐœ๋ฐœ ํ•„์š”** + +**ํŠน์ง•**: +```typescript +// ์„ ํƒ๋œ ํ•ญ๋ชฉ๋“ค์˜ ์ƒ์„ธ ์ž…๋ ฅ +- ๋ฐ์ดํ„ฐ ์†Œ์Šค (dataSourceId) +- ํ‘œ์‹œ ์ปฌ๋Ÿผ (displayColumns) +- ์ถ”๊ฐ€ ์ž…๋ ฅ ํ•„๋“œ (additionalFields) +- ํƒ€๊ฒŸ ํ…Œ์ด๋ธ” (targetTable) +- ๋ ˆ์ด์•„์›ƒ (grid/table) +``` + +**๋ณต์žก๋„ ์š”์ธ**: +1. ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์‹  +2. ๋™์  ํ•„๋“œ ์ƒ์„ฑ +3. ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ €์žฅ + +**๊ถŒ์žฅ ๋ฐฉ์•ˆ**: +- `v2-selected-items-detail` ์ƒˆ๋กœ ๊ฐœ๋ฐœ +- ๋˜๋Š” `v2-repeater`์— `mode: "detail-input"` ์ถ”๊ฐ€ + +**์ž‘์—…๋Ÿ‰**: ์ค‘๊ฐ„~๋†’์Œ + +--- + +#### 2.3.4 `conditional-container` (53๊ฐœ) + +**๋ถ„์„ ๊ฒฐ๊ณผ**: โŒ **๋ณ„๋„ ๊ฐœ๋ฐœ ํ•„์š”** + +**ํŠน์ง•**: +```typescript +// ์กฐ๊ฑด๋ถ€ UI ๋ถ„๊ธฐ +- ์ œ์–ด ํ•„๋“œ (controlField, controlLabel) +- ์กฐ๊ฑด๋ณ„ ์„น์…˜ (sections: [{condition, label, screenId}]) +- ๊ธฐ๋ณธ๊ฐ’ (defaultValue) +``` + +**๋ณต์žก๋„ ์š”์ธ**: +1. ์…€๋ ‰ํŠธ๋ฐ•์Šค ๊ฐ’์— ๋”ฐ๋ฅธ ๋™์  UI ๋ณ€๊ฒฝ +2. ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ (screenId) +3. ์ƒํƒœ ๊ด€๋ฆฌ ๋ณต์žก + +**๊ถŒ์žฅ ๋ฐฉ์•ˆ**: +- `v2-conditional-container` ์ƒˆ๋กœ ๊ฐœ๋ฐœ +- ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง + ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ ๋กœ์ง + +**์ž‘์—…๋Ÿ‰**: ๋†’์Œ + +--- + +#### 2.3.5 `universal-form-modal` (26๊ฐœ) + +**๋ถ„์„ ๊ฒฐ๊ณผ**: โŒ **๋ณ„๋„ ๊ฐœ๋ฐœ ํ•„์š”** + +**ํŠน์ง•**: +```typescript +// ๋ฒ”์šฉ ํผ ๋ชจ๋‹ฌ +- ์„น์…˜ ๊ธฐ๋ฐ˜ ๋ ˆ์ด์•„์›ƒ +- ๋ฐ˜๋ณต ์„น์…˜ +- ์ฑ„๋ฒˆ๊ทœ์น™ ์—ฐ๋™ +- ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ €์žฅ +``` + +**๋ณต์žก๋„ ์š”์ธ**: +1. ๋™์  ์„น์…˜ ๊ตฌ์„ฑ +2. ์ฑ„๋ฒˆ๊ทœ์น™ ์—ฐ๋™ +3. ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ €์žฅ +4. ๋ฐ˜๋ณต ํ•„๋“œ ๊ทธ๋ฃน + +**๊ถŒ์žฅ ๋ฐฉ์•ˆ**: +- `v2-universal-form` ์ƒˆ๋กœ ๊ฐœ๋ฐœ +- ๋˜๋Š” ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ ์œ ์ง€ (ํŠน์ˆ˜ ๋ชฉ์ ) + +**์ž‘์—…๋Ÿ‰**: ๋งค์šฐ ๋†’์Œ (3์ผ ์ด์ƒ) + +--- + +### 2.4 ๐ŸŸข V1 ์œ ์ง€ ๊ถŒ์žฅ (ํŠน์ˆ˜ ๋ชฉ์ ) + +| ์ปดํฌ๋„ŒํŠธ | ๊ฐœ์ˆ˜ | ์ด์œ  | +|----------|------|------| +| `tax-invoice-list` | 1 | ์„ธ๊ธˆ๊ณ„์‚ฐ์„œ ์ „์šฉ, ์žฌ์‚ฌ์šฉ ๋‚ฎ์Œ | +| `mail-recipient-selector` | 1 | ๋ฉ”์ผ ์ „์šฉ, ์žฌ์‚ฌ์šฉ ๋‚ฎ์Œ | +| `unified-select` | 5 | โ†’ v2-select๋กœ ์ด๋ฏธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ | +| `unified-date` | 2 | โ†’ v2-date๋กœ ์ด๋ฏธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ | +| `unified-repeater` | 2 | โ†’ v2-repeater๋กœ ์ด๋ฏธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ | + +--- + +## 3. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์šฐ์„ ์ˆœ์œ„ ๊ถŒ์žฅ + +### 3.1 ์ฆ‰์‹œ ์ฒ˜๋ฆฌ (1์ผ ์ด๋‚ด) + +| ์ˆœ์œ„ | ์ปดํฌ๋„ŒํŠธ | ๊ฐœ์ˆ˜ | ์ž‘์—… | +|------|----------|------|------| +| 1 | `split-panel-layout2` | 8 | DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋งŒ | +| 2 | `repeat-screen-modal` | 7 | DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋งŒ | +| 3 | `related-data-buttons` | 5 | DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋งŒ | +| 4 | `autocomplete-search-input` | 50 | โ†’ v2-select ๋ณ€ํ™˜ | +| 5 | `repeater-field-group` | 24 | โ†’ v2-repeater ๋ณ€ํ™˜ | + +**์ด: 94๊ฐœ ์ปดํฌ๋„ŒํŠธ** + +--- + +### 3.2 ๋‹จ๊ธฐ ์ฒ˜๋ฆฌ (1์ฃผ ์ด๋‚ด) + +| ์ˆœ์œ„ | ์ปดํฌ๋„ŒํŠธ | ๊ฐœ์ˆ˜ | ์ž‘์—… | +|------|----------|------|------| +| 1 | `unified-list` | 97 | โ†’ v2-table-list ํ™•์žฅ ๋˜๋Š” ๋ณ€ํ™˜ | +| 2 | `modal-repeater-table` | 68 | v2-repeater modal ๋ชจ๋“œ ํ™•์žฅ | + +**์ด: 165๊ฐœ ์ปดํฌ๋„ŒํŠธ** + +--- + +### 3.3 ์ค‘๊ธฐ ์ฒ˜๋ฆฌ (2์ฃผ ์ด์ƒ) + +| ์ˆœ์œ„ | ์ปดํฌ๋„ŒํŠธ | ๊ฐœ์ˆ˜ | ์ž‘์—… | +|------|----------|------|------| +| 1 | `entity-search-input` | 99 | v2-entity-search ์‹ ๊ทœ ๊ฐœ๋ฐœ | +| 2 | `selected-items-detail-input` | 83 | v2-selected-items-detail ๊ฐœ๋ฐœ | +| 3 | `conditional-container` | 53 | v2-conditional-container ๊ฐœ๋ฐœ | +| 4 | `universal-form-modal` | 26 | v2-universal-form ๊ฐœ๋ฐœ | + +**์ด: 261๊ฐœ ์ปดํฌ๋„ŒํŠธ** + +--- + +## 4. ๊ถŒ์žฅ ์•„ํ‚คํ…์ฒ˜ + +### 4.1 V2 ์ปดํฌ๋„ŒํŠธ ํ†ตํ•ฉ ๊ณ„ํš + +``` +v2-input โ† text-input, number-input, textarea, unified-input โœ… ์™„๋ฃŒ +v2-select โ† select-basic, checkbox, radio, autocomplete โš ๏ธ ์ง„ํ–‰์ค‘ +v2-date โ† date-input, unified-date โœ… ์™„๋ฃŒ +v2-media โ† file-upload, image-widget โœ… ์™„๋ฃŒ +v2-table-list โ† table-list, unified-list โš ๏ธ ํ™•์žฅ ํ•„์š” +v2-repeater โ† repeater-field-group, modal-repeater-table, + simple-repeater-table, related-data-buttons โš ๏ธ ์ง„ํ–‰์ค‘ +v2-entity-search โ† entity-search-input (์‹ ๊ทœ ๊ฐœ๋ฐœ ํ•„์š”) +v2-conditional โ† conditional-container (์‹ ๊ทœ ๊ฐœ๋ฐœ ํ•„์š”) +``` + +--- + +## 5. ๊ฒฐ๋ก  + +### ์ฆ‰์‹œ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ (Renderer/DB๋งŒ) +- `split-panel-layout2`, `repeat-screen-modal`, `related-data-buttons`: **20๊ฐœ** +- `autocomplete-search-input` โ†’ `v2-select`: **50๊ฐœ** +- `repeater-field-group` โ†’ `v2-repeater`: **24๊ฐœ** + +### ํ†ตํ•ฉ ๊ฒ€ํ†  ํ•„์š” +- `unified-list` โ†’ `v2-table-list` ํ™•์žฅ: **97๊ฐœ** +- `modal-repeater-table` โ†’ `v2-repeater` ํ™•์žฅ: **68๊ฐœ** + +### ์‹ ๊ทœ ๊ฐœ๋ฐœ ํ•„์š” +- `entity-search-input`: **99๊ฐœ** (๋ณต์žก๋„ ๋†’์Œ) +- `selected-items-detail-input`: **83๊ฐœ** +- `conditional-container`: **53๊ฐœ** +- `universal-form-modal`: **26๊ฐœ** + +### ์œ ์ง€ +- ํŠน์ˆ˜ ๋ชฉ์  ์ปดํฌ๋„ŒํŠธ: **3๊ฐœ** (tax-invoice-list, mail-recipient-selector) + +--- + +## 6. ๋‹ค์Œ ๋‹จ๊ณ„ + +1. **์ฆ‰์‹œ**: `split-panel-layout2`, `repeat-screen-modal`, `related-data-buttons` DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ +2. **์ด๋ฒˆ ์ฃผ**: `autocomplete-search-input` โ†’ `v2-select`, `repeater-field-group` โ†’ `v2-repeater` ๋ณ€ํ™˜ +3. **๋‹ค์Œ ์ฃผ**: `unified-list`, `modal-repeater-table` ํ†ตํ•ฉ ์„ค๊ณ„ +4. **์ดํ›„**: `entity-search-input`, `conditional-container` ์‹ ๊ทœ ๊ฐœ๋ฐœ ๊ณ„ํš ์ˆ˜๋ฆฝ diff --git a/docs/V2_์ปดํฌ๋„ŒํŠธ_๋ถ„์„_๊ฐ€์ด๋“œ.md b/docs/V2_์ปดํฌ๋„ŒํŠธ_๋ถ„์„_๊ฐ€์ด๋“œ.md new file mode 100644 index 00000000..451cf602 --- /dev/null +++ b/docs/V2_์ปดํฌ๋„ŒํŠธ_๋ถ„์„_๊ฐ€์ด๋“œ.md @@ -0,0 +1,1077 @@ +# V2 ์ปดํฌ๋„ŒํŠธ ๋ถ„์„ ๊ฐ€์ด๋“œ + +## ๊ฐœ์š” + +V2 ์ปดํฌ๋„ŒํŠธ๋Š” **ํ™”๋ฉด๊ด€๋ฆฌ ์‹œ์Šคํ…œ ์ „์šฉ**์œผ๋กœ ๊ฐœ๋ฐœ๋œ ์ปดํฌ๋„ŒํŠธ ์„ธํŠธ์ž…๋‹ˆ๋‹ค. ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ์™€์˜ ์ถฉ๋Œ์„ ๋ฐฉ์ง€ํ•˜๊ณ , ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ(์—”ํ‹ฐํ‹ฐ ์กฐ์ธ, ๋‹ค๊ตญ์–ด ์ง€์›, ์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ๋“ฑ)์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. + +### ํ•ต์‹ฌ ์›์น™ + +- ๋ชจ๋“  V2 ์ปดํฌ๋„ŒํŠธ๋Š” `v2-` ์ ‘๋‘์‚ฌ๋ฅผ ์‚ฌ์šฉ +- ์›๋ณธ ์ปดํฌ๋„ŒํŠธ๋Š” ๊ธฐ์กด ํ™”๋ฉด ํ˜ธํ™˜์„ฑ ์œ ์ง€์šฉ์œผ๋กœ ๋ณด์กด +- ์ƒˆ๋กœ์šด ํ™”๋ฉด ๊ฐœ๋ฐœ ์‹œ ๋ฐ˜๋“œ์‹œ V2 ์ปดํฌ๋„ŒํŠธ๋งŒ ์‚ฌ์šฉ +- Definition ์ด๋ฆ„์— `V2` ์ ‘๋‘์‚ฌ ์‚ฌ์šฉ (์˜ˆ: `V2TableListDefinition`) + +### ํŒŒ์ผ ๊ฒฝ๋กœ + +``` +frontend/lib/registry/components/ +โ”œโ”€โ”€ v2-button-primary/ โ† V2 ์ปดํฌ๋„ŒํŠธ (์ˆ˜์ • ๋Œ€์ƒ) +โ”œโ”€โ”€ v2-table-list/ โ† V2 ์ปดํฌ๋„ŒํŠธ (์ˆ˜์ • ๋Œ€์ƒ) +โ”œโ”€โ”€ v2-split-panel-layout/ โ† V2 ์ปดํฌ๋„ŒํŠธ (์ˆ˜์ • ๋Œ€์ƒ) +โ”œโ”€โ”€ ... +โ”œโ”€โ”€ button-primary/ โ† ์›๋ณธ (์ˆ˜์ • ๊ธˆ์ง€) +โ”œโ”€โ”€ table-list/ โ† ์›๋ณธ (์ˆ˜์ • ๊ธˆ์ง€) +โ””โ”€โ”€ ... +``` + +--- + +## V2 ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก (18๊ฐœ) + +| ์ปดํฌ๋„ŒํŠธ ID | ์ด๋ฆ„ | ์นดํ…Œ๊ณ ๋ฆฌ | ์šฉ๋„ | +|------------|------|----------|------| +| `v2-table-list` | ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | DISPLAY | ๋ฐ์ดํ„ฐ ๋ชฉ๋ก ํ‘œ์‹œ (ํ…Œ์ด๋ธ”/์นด๋“œ ๋ชจ๋“œ) | +| `v2-table-grouped` | ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” | DISPLAY | **NEW** ๊ทธ๋ฃน๋ณ„ ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ ํ…Œ์ด๋ธ” | +| `v2-split-panel-layout` | ๋ถ„ํ•  ํŒจ๋„ | DISPLAY | ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ๋ ˆ์ด์•„์›ƒ | +| `v2-unified-repeater` | ํ†ตํ•ฉ ๋ฆฌํ”ผํ„ฐ | UNIFIED | ๋ฐ˜๋ณต ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ (์ธ๋ผ์ธ/๋ชจ๋‹ฌ/๋ฒ„ํŠผ) | +| `v2-pivot-grid` | ํ”ผ๋ฒ— ๊ทธ๋ฆฌ๋“œ | DISPLAY | ๋‹ค์ฐจ์› ๋ฐ์ดํ„ฐ ๋ถ„์„ ํ”ผ๋ฒ— ํ…Œ์ด๋ธ” | +| `v2-button-primary` | ๊ธฐ๋ณธ ๋ฒ„ํŠผ | ACTION | ์ €์žฅ/์‚ญ์ œ ๋“ฑ ์•ก์…˜ ๋ฒ„ํŠผ | +| `v2-text-display` | ํ…์ŠคํŠธ ํ‘œ์‹œ | DISPLAY | ํ…์ŠคํŠธ/๋ผ๋ฒจ ํ‘œ์‹œ | +| `v2-divider-line` | ๊ตฌ๋ถ„์„  | DISPLAY | ์‹œ๊ฐ์  ๊ตฌ๋ถ„์„  | +| `v2-card-display` | ์นด๋“œ ๋””์Šคํ”Œ๋ ˆ์ด | DISPLAY | ์นด๋“œ ํ˜•ํƒœ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ | +| `v2-numbering-rule` | ์ฑ„๋ฒˆ ๊ทœ์น™ | DISPLAY | ์ฝ”๋“œ ์ž๋™ ์ฑ„๋ฒˆ ์„ค์ • | +| `v2-table-search-widget` | ๊ฒ€์ƒ‰ ํ•„ํ„ฐ | DISPLAY | ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰/ํ•„ํ„ฐ ์œ„์ ฏ | +| `v2-section-paper` | ์„น์…˜ ํŽ˜์ดํผ | LAYOUT | ์„น์…˜ ๊ตฌ๋ถ„ ์ปจํ…Œ์ด๋„ˆ | +| `v2-section-card` | ์„น์…˜ ์นด๋“œ | LAYOUT | ์นด๋“œํ˜• ์„น์…˜ ์ปจํ…Œ์ด๋„ˆ | +| `v2-tabs-widget` | ํƒญ ์œ„์ ฏ | LAYOUT | ํƒญ ๊ธฐ๋ฐ˜ ์ฝ˜ํ…์ธ  ์ „ํ™˜ | +| `v2-location-swap-selector` | ์œ„์น˜ ์„ ํƒ | INPUT | ์ถœ๋ฐœ์ง€/๋„์ฐฉ์ง€ ์Šค์™‘ ์„ ํƒ | +| `v2-rack-structure` | ๋ ‰ ๊ตฌ์กฐ | DISPLAY | ์ฐฝ๊ณ  ๋ ‰ ์‹œ๊ฐํ™” | +| `v2-aggregation-widget` | ์ง‘๊ณ„ ์œ„์ ฏ | DISPLAY | ๋ฐ์ดํ„ฐ ์ง‘๊ณ„ (ํ•ฉ๊ณ„/ํ‰๊ท /๊ฐœ์ˆ˜) | +| `v2-repeat-container` | ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ | LAYOUT | ๋ฐ์ดํ„ฐ ์ˆ˜๋งŒํผ ๋ฐ˜๋ณต ๋ Œ๋”๋ง | + +--- + +## ์ฃผ์š” ์ปดํฌ๋„ŒํŠธ ์ƒ์„ธ ๋ถ„์„ + +### 1. v2-table-list (ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ) + +**์šฉ๋„**: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ๋ฅผ ํ…Œ์ด๋ธ”/์นด๋“œ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ + +#### ์ฃผ์š” ํŠน์ง• + +- ํ…Œ์ด๋ธ” ๋ชจ๋“œ / ์นด๋“œ ๋ชจ๋“œ ์ „ํ™˜ ๊ฐ€๋Šฅ +- ํŽ˜์ด์ง€๋„ค์ด์…˜, ์ •๋ ฌ, ํ•„ํ„ฐ๋ง ์ง€์› +- ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ (๋‹จ์ผ/๋‹ค์ค‘) +- ๊ฐ€๋กœ ์Šคํฌ๋กค ๋ฐ ์ปฌ๋Ÿผ ๊ณ ์ • +- ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ ์ง€์› +- ์ธ๋ผ์ธ ํŽธ์ง‘ ๊ธฐ๋Šฅ +- Excel ๋‚ด๋ณด๋‚ด๊ธฐ + +#### ๋ฐ์ดํ„ฐ ํ๋ฆ„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-table-list โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ‘  config.selectedTable / customTableName ํ™•์ธ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ก tableTypeApi.getData() ํ˜ธ์ถœ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ข entityJoinApi.getEntityJoinColumns() ์กฐ์ธ ์ปฌ๋Ÿผ ๋กœ๋“œ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ฃ ๋ฐ์ดํ„ฐ + ์กฐ์ธ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ค ํ…Œ์ด๋ธ”/์นด๋“œ ๋ชจ๋“œ๋กœ ๋ Œ๋”๋ง โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ฅ onRowClick / onSelectionChange ์ด๋ฒคํŠธ ๋ฐœ์ƒ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +#### ์ฃผ์š” ์„ค์ • ์ธํ„ฐํŽ˜์ด์Šค + +```typescript +interface TableListConfig { + // ํ‘œ์‹œ ๋ชจ๋“œ + displayMode: "table" | "card"; + + // ์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ์„ค์ • + customTableName?: string; // ์ปค์Šคํ…€ ํ…Œ์ด๋ธ” + useCustomTable?: boolean; // ์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ ์—ฌ๋ถ€ + isReadOnly?: boolean; // ์ฝ๊ธฐ์ „์šฉ + + // ์ปฌ๋Ÿผ ์„ค์ • + columns: ColumnConfig[]; + + // ํŽ˜์ด์ง€๋„ค์ด์…˜ + pagination: { + enabled: boolean; + pageSize: number; + showSizeSelector: boolean; + pageSizeOptions: number[]; + }; + + // ์ฒดํฌ๋ฐ•์Šค + checkbox: { + enabled: boolean; + multiple: boolean; // true: ์ฒดํฌ๋ฐ•์Šค, false: ๋ผ๋””์˜ค + position: "left" | "right"; + selectAll: boolean; + }; + + // ํ•„ํ„ฐ + filter: { + enabled: boolean; + filters: FilterConfig[]; + }; + + // ์—ฐ๊ฒฐ๋œ ํ•„ํ„ฐ (๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ ๊ฐ’์œผ๋กœ ํ•„ํ„ฐ๋ง) + linkedFilters?: LinkedFilterConfig[]; + + // ์ œ์™ธ ํ•„ํ„ฐ (๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์กด์žฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ์ œ์™ธ) + excludeFilter?: ExcludeFilterConfig; + + // ๊ฐ€๋กœ ์Šคํฌ๋กค ์„ค์ • + horizontalScroll: { + enabled: boolean; + maxVisibleColumns?: number; + minColumnWidth?: number; + maxColumnWidth?: number; + }; +} +``` + +#### ์ปฌ๋Ÿผ ์„ค์ • + +```typescript +interface ColumnConfig { + columnName: string; // ์ปฌ๋Ÿผ๋ช… + displayName: string; // ํ‘œ์‹œ๋ช… + visible: boolean; // ํ‘œ์‹œ ์—ฌ๋ถ€ + sortable: boolean; // ์ •๋ ฌ ๊ฐ€๋Šฅ + searchable: boolean; // ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅ + width?: number; // ๋„ˆ๋น„ + align: "left" | "center" | "right"; // ์ •๋ ฌ + format?: "text" | "number" | "date" | "currency" | "boolean"; + + // ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ + isEntityJoin?: boolean; // ์กฐ์ธ ์ปฌ๋Ÿผ ์—ฌ๋ถ€ + entityJoinInfo?: { + sourceTable: string; + sourceColumn: string; + joinAlias: string; + }; + + // ์ปฌ๋Ÿผ ๊ณ ์ • + fixed?: "left" | "right" | false; + + // ์ž๋™์ƒ์„ฑ + autoGeneration?: { + type: "uuid" | "current_user" | "current_time" | "sequence" | "numbering_rule"; + enabled: boolean; + }; + + // ํŽธ์ง‘ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ + editable?: boolean; +} +``` + +--- + +### 2. v2-split-panel-layout (๋ถ„ํ•  ํŒจ๋„) + +**์šฉ๋„**: ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ํŒจํ„ด์˜ ์ขŒ์šฐ ๋ถ„ํ•  ๋ ˆ์ด์•„์›ƒ + +#### ์ฃผ์š” ํŠน์ง• + +- ์ขŒ์ธก: ๋งˆ์Šคํ„ฐ ๋ชฉ๋ก (๋ฆฌ์ŠคํŠธ/ํ…Œ์ด๋ธ”/์ปค์Šคํ…€ ๋ชจ๋“œ) +- ์šฐ์ธก: ๋””ํ…Œ์ผ ์ •๋ณด (์—ฐ๊ด€ ๋ฐ์ดํ„ฐ) +- ์ขŒ์šฐ ๋น„์œจ ์กฐ์ ˆ ๊ฐ€๋Šฅ (๋“œ๋ž˜๊ทธ ๋ฆฌ์‚ฌ์ด์ฆˆ) +- ๋‹ค์ค‘ ํƒญ ์ง€์› (์šฐ์ธก ํŒจ๋„) +- N:M ๊ด€๊ณ„ ๋ฐ์ดํ„ฐ ์ง€์› +- ์ค‘๋ณต ์ œ๊ฑฐ ๊ธฐ๋Šฅ +- **์ปค์Šคํ…€ ๋ชจ๋“œ (NEW)**: ํŒจ๋„ ๋‚ด๋ถ€์— ์ž์œ ๋กญ๊ฒŒ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ + +#### ์ปค์Šคํ…€ ๋ชจ๋“œ (displayMode: "custom") + +ํŒจ๋„ ๋‚ด๋ถ€์— ๋‹ค์–‘ํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ๋ฐฐ์น˜ํ•  ์ˆ˜ ์žˆ๋Š” ๋ชจ๋“œ์ž…๋‹ˆ๋‹ค. **v2-tabs-widget๊ณผ ๋™์ผํ•œ ๊ตฌ์กฐ**๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. + +**ํŠน์ง•:** +- ํŒจ๋„ ๋‚ด๋ถ€์— ๋ฒ„ํŠผ, ํ…Œ์ด๋ธ”, ์ž…๋ ฅ ํ•„๋“œ ๋“ฑ ๋ชจ๋“  V2 ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ ๊ฐ€๋Šฅ +- ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ ์ด๋™ +- ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค๋กœ ํฌ๊ธฐ ์กฐ์ ˆ +- **์ปดํฌ๋„ŒํŠธ ํด๋ฆญ ์‹œ ์ขŒ์ธก ์„ค์ • ํŒจ๋„์—์„œ ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ ์†์„ฑ ํŽธ์ง‘** +- ๋””์ž์ธ ๋ชจ๋“œ์—์„œ ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋˜์–ด ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฐ€๋Šฅ + +**์‚ฌ์šฉ ๋ฐฉ๋ฒ•:** +1. ์ขŒ์ธก/์šฐ์ธก ํŒจ๋„์˜ `displayMode`๋ฅผ `"custom"`์œผ๋กœ ์„ค์ • +2. ์ปดํฌ๋„ŒํŠธ ํŒ”๋ ˆํŠธ์—์„œ ์›ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ํŒจ๋„๋กœ ๋“œ๋ž˜๊ทธ +3. ๋ฐฐ์น˜๋œ ์ปดํฌ๋„ŒํŠธ ํด๋ฆญ โ†’ ์ขŒ์ธก ํŒจ๋„์—์„œ ์†์„ฑ ํŽธ์ง‘ +4. ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค(์ƒ๋‹จ)๋กœ ์ด๋™, ๋ชจ์„œ๋ฆฌ ํ•ธ๋“ค๋กœ ํฌ๊ธฐ ์กฐ์ ˆ + +**์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ:** +```typescript +interface PanelInlineComponent { + id: string; + componentType: string; + label?: string; + position?: { x: number; y: number }; + size?: { width: number; height: number }; + componentConfig?: Record; + style?: Record; +} +``` + +#### ๋ฐ์ดํ„ฐ ํ๋ฆ„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-split-panel-layout โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ขŒ์ธก ํŒจ๋„ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’ โ”‚ ์šฐ์ธก ํŒจ๋„ โ”‚ โ”‚ +โ”‚ โ”‚ (๋งˆ์Šคํ„ฐ) โ”‚ ์„ ํƒ ์ด๋ฒคํŠธโ”‚ (๋””ํ…Œ์ผ) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ†“ โ†“ โ”‚ +โ”‚ leftPanel.tableName rightPanel.tableName โ”‚ +โ”‚ leftPanel.columns rightPanel.relation โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ†“ โ†“ โ”‚ +โ”‚ ์ขŒ์ธก ๋ฐ์ดํ„ฐ ์กฐํšŒ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’ ๊ด€๊ณ„ ์„ค์ •์— ๋”ฐ๋ผ ์šฐ์ธก ํ•„ํ„ฐ๋ง โ”‚ +โ”‚ (๋…๋ฆฝ API ํ˜ธ์ถœ) (FK/์กฐ์ธ ํ‚ค ๊ธฐ๋ฐ˜) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +#### ์ฃผ์š” ์„ค์ • ์ธํ„ฐํŽ˜์ด์Šค + +```typescript +interface SplitPanelLayoutConfig { + // ์ขŒ์ธก ํŒจ๋„ + leftPanel: { + title: string; + tableName?: string; + useCustomTable?: boolean; + customTableName?: string; + displayMode?: "list" | "table" | "custom"; // ๐Ÿ†• ์ปค์Šคํ…€ ๋ชจ๋“œ ์ถ”๊ฐ€ + showSearch?: boolean; + showAdd?: boolean; + showEdit?: boolean; + showDelete?: boolean; + columns?: ColumnConfig[]; + tableConfig?: TableDisplayConfig; + dataFilter?: DataFilterConfig; + + // ๐Ÿ†• ์ปค์Šคํ…€ ๋ชจ๋“œ ์ „์šฉ: ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์—ด + components?: PanelInlineComponent[]; + }; + + // ์šฐ์ธก ํŒจ๋„ + rightPanel: { + title: string; + tableName?: string; + displayMode?: "list" | "table" | "custom"; // ๐Ÿ†• ์ปค์Šคํ…€ ๋ชจ๋“œ ์ถ”๊ฐ€ + columns?: ColumnConfig[]; + + // ๐Ÿ†• ์ปค์Šคํ…€ ๋ชจ๋“œ ์ „์šฉ: ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์—ด + components?: PanelInlineComponent[]; + + // ๊ด€๊ณ„ ์„ค์ • + relation?: { + type?: "join" | "detail"; + leftColumn?: string; // ์ขŒ์ธก ์กฐ์ธ ์ปฌ๋Ÿผ + rightColumn?: string; // ์šฐ์ธก ์กฐ์ธ ์ปฌ๋Ÿผ + foreignKey?: string; // FK ์ปฌ๋Ÿผ + keys?: Array<{ // ๋ณตํ•ฉํ‚ค ์ง€์› + leftColumn: string; + rightColumn: string; + }>; + }; + + // ์ถ”๊ฐ€ ์„ค์ • (N:M ๊ด€๊ณ„) + addConfig?: { + targetTable?: string; // ์‹ค์ œ INSERT ํ…Œ์ด๋ธ” + autoFillColumns?: Record; + leftPanelColumn?: string; + targetColumn?: string; + }; + + // ์ค‘๋ณต ์ œ๊ฑฐ + deduplication?: { + enabled: boolean; + groupByColumn: string; + keepStrategy: "latest" | "earliest" | "base_price" | "current_date"; + }; + + // ์ถ”๊ฐ€ ํƒญ + additionalTabs?: AdditionalTabConfig[]; + }; + + // ๋ ˆ์ด์•„์›ƒ + splitRatio?: number; // ์ขŒ์šฐ ๋น„์œจ (0-100) + resizable?: boolean; // ํฌ๊ธฐ ์กฐ์ ˆ ๊ฐ€๋Šฅ + minLeftWidth?: number; + minRightWidth?: number; + + // ๋™์ž‘ + autoLoad?: boolean; + syncSelection?: boolean; +} +``` + +--- + +### 3. v2-unified-repeater (ํ†ตํ•ฉ ๋ฆฌํ”ผํ„ฐ) + +**์šฉ๋„**: ๋ฐ˜๋ณต ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ (๊ธฐ์กด ์—ฌ๋Ÿฌ ๋ฆฌํ”ผํ„ฐ ํ†ตํ•ฉ) + +#### ์ฃผ์š” ํŠน์ง• + +- 3๊ฐ€์ง€ ๋ Œ๋”๋ง ๋ชจ๋“œ: ์ธ๋ผ์ธ/๋ชจ๋‹ฌ/๋ฒ„ํŠผ +- ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ FK ์ž๋™ ์—ฐ๊ฒฐ +- ์ €์žฅ ํ…Œ์ด๋ธ” ๋ถ„๋ฆฌ ๊ฐ€๋Šฅ +- ํ–‰ ์ถ”๊ฐ€/์‚ญ์ œ, ๋“œ๋ž˜๊ทธ ์ •๋ ฌ +- ์„ ํƒ ๊ธฐ๋Šฅ (๋‹จ์ผ/๋‹ค์ค‘) + +#### ๋ฐ์ดํ„ฐ ํ๋ฆ„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-unified-repeater โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ‘  ๋งˆ์Šคํ„ฐ ์ €์žฅ ์ด๋ฒคํŠธ ์ˆ˜์‹  (repeaterSave) โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ก masterRecordId ์ „๋‹ฌ๋ฐ›์Œ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ข foreignKeyColumn์— masterRecordId ์ž๋™ ์„ค์ • โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ฃ dataSource.tableName์œผ๋กœ ๋ฐ์ดํ„ฐ ์ €์žฅ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ค ์ €์žฅ ์™„๋ฃŒ ํ›„ onDataChange ์ด๋ฒคํŠธ ๋ฐœ์ƒ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +#### ์ฃผ์š” ์„ค์ • ์ธํ„ฐํŽ˜์ด์Šค + +```typescript +interface UnifiedRepeaterConfig { + // ๋ Œ๋”๋ง ๋ชจ๋“œ + renderMode: "inline" | "modal" | "button" | "mixed"; + + // ๋ฐ์ดํ„ฐ ์†Œ์Šค + dataSource: { + tableName: string; // ์ €์žฅ ํ…Œ์ด๋ธ” + foreignKey: string; // FK ์ปฌ๋Ÿผ + referenceKey: string; // ์ฐธ์กฐํ•  PK ์ปฌ๋Ÿผ + }; + + // ์ปฌ๋Ÿผ ์„ค์ • + columns: ColumnConfig[]; + + // ๋ชจ๋‹ฌ ์„ค์ • + modal: { + size: "sm" | "md" | "lg" | "xl"; + }; + + // ๋ฒ„ํŠผ ์„ค์ • + button: { + sourceType: "manual" | "auto"; + manualButtons: ButtonConfig[]; + layout: "horizontal" | "vertical"; + style: "outline" | "solid"; + }; + + // ๊ธฐ๋Šฅ ์„ค์ • + features: { + showAddButton: boolean; + showDeleteButton: boolean; + inlineEdit: boolean; + dragSort: boolean; + showRowNumber: boolean; + selectable: boolean; + multiSelect: boolean; + }; +} +``` + +#### ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ธํ„ฐํŽ˜์ด์Šค + +v2-unified-repeater๋Š” **DataProvidable**๊ณผ **DataReceivable** ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์™€ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +**DataProvidable ๊ตฌํ˜„**: + +```typescript +// ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ด ๋ฆฌํ”ผํ„ฐ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ๊ฐˆ ์ˆ˜ ์žˆ์Œ +const dataProvider: DataProvidable = { + componentId: parentId || config.fieldName || "unified-repeater", + componentType: "unified-repeater", + + // ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + getSelectedData: () => { + return Array.from(selectedRows).map((idx) => data[idx]).filter(Boolean); + }, + + // ์ „์ฒด ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + getAllData: () => { + return [...data]; + }, + + // ์„ ํƒ ์ดˆ๊ธฐํ™” + clearSelection: () => { + setSelectedRows(new Set()); + }, +}; +``` + +**DataReceivable ๊ตฌํ˜„**: + +```typescript +// ์™ธ๋ถ€์—์„œ ์ด ๋ฆฌํ”ผํ„ฐ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ +const dataReceiver: DataReceivable = { + componentId: parentId || config.fieldName || "unified-repeater", + componentType: "repeater", + + // ๋ฐ์ดํ„ฐ ์ˆ˜์‹  (append, replace, merge ๋ชจ๋“œ ์ง€์›) + receiveData: async (incomingData: any[], config: DataReceiverConfig) => { + // ๋งคํ•‘ ๊ทœ์น™ ์ ์šฉ ํ›„ ๋ชจ๋“œ์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ + switch (config.mode) { + case "replace": setData(mappedData); break; + case "merge": /* ์ค‘๋ณต ์ œ๊ฑฐ ํ›„ ๋ณ‘ํ•ฉ */ break; + case "append": /* ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€ */ break; + } + }, + + // ํ˜„์žฌ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + getData: () => [...data], +}; +``` + +**ScreenContext ์ž๋™ ๋“ฑ๋ก**: + +```typescript +// ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ScreenContext์— ์ž๋™ ๋“ฑ๋ก +useEffect(() => { + if (screenContext && componentId) { + screenContext.registerDataProvider(componentId, dataProvider); + screenContext.registerDataReceiver(componentId, dataReceiver); + + return () => { + screenContext.unregisterDataProvider(componentId); + screenContext.unregisterDataReceiver(componentId); + }; + } +}, [screenContext, componentId]); +``` + +#### V2 ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ + +**๋ฐœํ–‰ ์ด๋ฒคํŠธ**: + +| ์ด๋ฒคํŠธ | ๋ฐœํ–‰ ์‹œ์  | ๋ฐ์ดํ„ฐ | +|--------|----------|--------| +| `repeaterDataChange` | ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ | `{ componentId, tableName, data, selectedData }` | + +```typescript +// ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ V2 ํ‘œ์ค€ ์ด๋ฒคํŠธ ๋ฐœํ–‰ +import { V2_EVENTS, dispatchV2Event } from "@/types/component-events"; + +useEffect(() => { + if (data.length !== prevDataLengthRef.current) { + dispatchV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, { + componentId: parentId || config.fieldName || "unified-repeater", + tableName: config.dataSource?.tableName || "", + data: data, + selectedData: Array.from(selectedRows).map((idx) => data[idx]).filter(Boolean), + }); + } +}, [data, selectedRows]); +``` + +**๊ตฌ๋… ์ด๋ฒคํŠธ**: + +| ์ด๋ฒคํŠธ | ์šฉ๋„ | +|--------|------| +| `beforeFormSave` | ์ €์žฅ ์ „ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ | +| `repeaterSave` | ๋งˆ์Šคํ„ฐ ์ €์žฅ ํ›„ FK ์„ค์ • | +| `componentDataTransfer` | ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ˆ˜์‹  | +| `splitPanelDataTransfer` | ๋ถ„ํ•  ํŒจ๋„ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ˆ˜์‹  | + +--- + +### 4. v2-pivot-grid (ํ”ผ๋ฒ— ๊ทธ๋ฆฌ๋“œ) + +**์šฉ๋„**: ๋‹ค์ฐจ์› ๋ฐ์ดํ„ฐ ๋ถ„์„์šฉ ํ”ผ๋ฒ— ํ…Œ์ด๋ธ” + +#### ์ฃผ์š” ํŠน์ง• + +- ํ–‰/์—ด/๋ฐ์ดํ„ฐ/ํ•„ํ„ฐ ์˜์—ญ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ +- ๋‹ค์–‘ํ•œ ์ง‘๊ณ„ ํ•จ์ˆ˜ (ํ•ฉ๊ณ„, ํ‰๊ท , ๊ฐœ์ˆ˜, ์ตœ๋Œ€, ์ตœ์†Œ, ๊ณ ์œ  ๊ฐœ์ˆ˜) +- ์†Œ๊ณ„/์ด๊ณ„ ํ‘œ์‹œ (์œ„์น˜ ์„ค์ • ๊ฐ€๋Šฅ) +- ์กฐ๊ฑด๋ถ€ ์„œ์‹ (์ƒ‰์ƒ ์Šค์ผ€์ผ, ๋ฐ์ดํ„ฐ ๋ฐ”, ์•„์ด์ฝ˜) +- ์ฐจํŠธ ์—ฐ๋™ +- Excel ๋‚ด๋ณด๋‚ด๊ธฐ +- ๋‚ ์งœ ๊ทธ๋ฃนํ™” (์—ฐ/๋ถ„๊ธฐ/์›”/์ฃผ/์ผ) + +#### ๋ฐ์ดํ„ฐ ํ๋ฆ„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-pivot-grid โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ‘  dataSource ์„ค์ • (ํ…Œ์ด๋ธ”/API/์ •์  ๋ฐ์ดํ„ฐ) โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ก fields ์„ค์ • (ํ–‰/์—ด/๋ฐ์ดํ„ฐ ํ•„๋“œ ๋ฐฐ์น˜) โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ข processPivotData() ๋กœ ํ”ผ๋ฒ— ๊ณ„์‚ฐ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ฃ ์ง‘๊ณ„ ํ•จ์ˆ˜ ์ ์šฉ (sum, avg, count ๋“ฑ) โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ค PivotResult ์ƒ์„ฑ (rowHeaders, columnHeaders, dataMatrix)โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ฅ ์กฐ๊ฑด๋ถ€ ์„œ์‹ ์ ์šฉ ํ›„ ๋ Œ๋”๋ง โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +#### ์ฃผ์š” ์„ค์ • ์ธํ„ฐํŽ˜์ด์Šค + +```typescript +interface PivotGridComponentConfig { + // ๋ฐ์ดํ„ฐ ์†Œ์Šค + dataSource?: { + type: "table" | "api" | "static"; + tableName?: string; + apiEndpoint?: string; + staticData?: any[]; + filterConditions?: FilterCondition[]; + joinConfigs?: JoinConfig[]; + }; + + // ํ•„๋“œ ์„ค์ • + fields?: Array<{ + field: string; // ๋ฐ์ดํ„ฐ ํ•„๋“œ๋ช… + caption: string; // ํ‘œ์‹œ ๋ผ๋ฒจ + area: "row" | "column" | "data" | "filter"; + areaIndex?: number; // ์˜์—ญ ๋‚ด ์ˆœ์„œ + + // ์ง‘๊ณ„ (data ์˜์—ญ์šฉ) + summaryType?: "sum" | "count" | "avg" | "min" | "max" | "countDistinct"; + + // ๋‚ ์งœ ๊ทธ๋ฃนํ™” + groupInterval?: "year" | "quarter" | "month" | "week" | "day"; + + // ํฌ๋งท + format?: { + type: "number" | "currency" | "percent" | "date" | "text"; + precision?: number; + thousandSeparator?: boolean; + prefix?: string; + suffix?: string; + }; + }>; + + // ์ดํ•ฉ๊ณ„ ์„ค์ • + totals?: { + showRowGrandTotals?: boolean; + showRowTotals?: boolean; + showColumnGrandTotals?: boolean; + showColumnTotals?: boolean; + rowGrandTotalPosition?: "top" | "bottom"; + columnGrandTotalPosition?: "left" | "right"; + }; + + // ์Šคํƒ€์ผ + style?: { + theme: "default" | "compact" | "modern"; + alternateRowColors?: boolean; + highlightTotals?: boolean; + conditionalFormats?: ConditionalFormatRule[]; + }; + + // ํ•„๋“œ ์„ ํƒ๊ธฐ + fieldChooser?: { + enabled: boolean; + allowSearch?: boolean; + }; + + // ์ฐจํŠธ ์—ฐ๋™ + chart?: { + enabled: boolean; + type: "bar" | "line" | "area" | "pie" | "stackedBar"; + position: "top" | "bottom" | "left" | "right"; + }; +} +``` + +--- + +### 5. v2-aggregation-widget (์ง‘๊ณ„ ์œ„์ ฏ) + +**์šฉ๋„**: ๋ฐ์ดํ„ฐ ์ง‘๊ณ„ ๊ฒฐ๊ณผ ํ‘œ์‹œ (ํ•ฉ๊ณ„, ํ‰๊ท , ๊ฐœ์ˆ˜ ๋“ฑ) + +#### ์ฃผ์š” ํŠน์ง• + +- ๋‹ค์–‘ํ•œ ์ง‘๊ณ„ ํƒ€์ž… (SUM, AVG, COUNT, MIN, MAX) +- ํ•„ํ„ฐ๋ง ์ง€์› (ํผ ๋ฐ์ดํ„ฐ ์—ฐ๋™) +- ๊ฐ€๋กœ/์„ธ๋กœ ๋ ˆ์ด์•„์›ƒ +- ์•„์ด์ฝ˜ ํ‘œ์‹œ +- ํผ ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ์ƒˆ๋กœ๊ณ ์นจ + +#### ๋ฐ์ดํ„ฐ ํ๋ฆ„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-aggregation-widget โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ‘  dataSourceType ํ™•์ธ (table / repeater) โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ก filters ์ ์šฉ (ํ•„ํ„ฐ ์กฐ๊ฑด ๊ตฌ์„ฑ) โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ข items ์ˆœํšŒํ•˜๋ฉฐ ๊ฐ ์ง‘๊ณ„ ํ•จ์ˆ˜ ์‹คํ–‰ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ฃ ์ง‘๊ณ„ ๊ฒฐ๊ณผ ํฌ๋งทํŒ… (์ฒœ๋‹จ์œ„ ๊ตฌ๋ถ„, ์ ‘๋‘์‚ฌ/์ ‘๋ฏธ์‚ฌ) โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ค layout์— ๋”ฐ๋ผ ๋ Œ๋”๋ง (horizontal / vertical) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +#### ์ฃผ์š” ์„ค์ • ์ธํ„ฐํŽ˜์ด์Šค + +```typescript +interface AggregationWidgetConfig { + // ๋ฐ์ดํ„ฐ ์†Œ์Šค + dataSourceType: "table" | "repeater"; + + // ์ง‘๊ณ„ ํ•ญ๋ชฉ + items: Array<{ + id: string; + label: string; + columnName: string; + aggregationType: "sum" | "avg" | "count" | "min" | "max"; + format?: { + prefix?: string; + suffix?: string; + thousandSeparator?: boolean; + decimalPlaces?: number; + }; + icon?: string; + color?: string; + }>; + + // ํ•„ํ„ฐ ์กฐ๊ฑด + filters: Array<{ + column: string; + operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN"; + value?: any; + valueSource?: "static" | "formData" | "url"; + valueField?: string; + }>; + filterLogic: "AND" | "OR"; + + // ๋ ˆ์ด์•„์›ƒ + layout: "horizontal" | "vertical"; + gap: string; + + // ์Šคํƒ€์ผ + showLabels: boolean; + showIcons: boolean; + backgroundColor: string; + borderRadius: string; + padding: string; + + // ๋™์ž‘ + autoRefresh: boolean; + refreshOnFormChange: boolean; +} +``` + +#### V2 ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ + +v2-aggregation-widget์€ V2 ํ‘œ์ค€ ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์˜ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค. + +**๊ตฌ๋… ์ด๋ฒคํŠธ**: + +| ์ด๋ฒคํŠธ | ์šฉ๋„ | ๋ฐœํ–‰์ž | +|--------|------|--------| +| `tableListDataChange` | ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ง‘๊ณ„ ๊ฐฑ์‹  | v2-table-list | +| `repeaterDataChange` | ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ง‘๊ณ„ ๊ฐฑ์‹  | v2-unified-repeater | + +```typescript +import { V2_EVENTS, subscribeV2Event, type TableListDataChangeDetail, type RepeaterDataChangeDetail } from "@/types/component-events"; + +useEffect(() => { + // ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๊ตฌ๋… + const unsubscribeTableList = subscribeV2Event( + V2_EVENTS.TABLE_LIST_DATA_CHANGE, + (event: CustomEvent) => { + const { data } = event.detail; + // ํ•„ํ„ฐ ์ ์šฉ ํ›„ ์ง‘๊ณ„ ์žฌ๊ณ„์‚ฐ + const filteredData = applyFilters(data, filters, filterLogic, formData, selectedRows); + setData(filteredData); + } + ); + + // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๊ตฌ๋… + const unsubscribeRepeater = subscribeV2Event( + V2_EVENTS.REPEATER_DATA_CHANGE, + (event: CustomEvent) => { + const { data, selectedData } = event.detail; + const rows = selectedData || data || []; + const filteredData = applyFilters(rows, filters, filterLogic, formData, selectedRows); + setData(filteredData); + } + ); + + return () => { + unsubscribeTableList(); + unsubscribeRepeater(); + }; +}, [dataSourceType, isDesignMode, filterLogic]); +``` + +**์ฐธ๊ณ **: ์ด์ „์— ์‚ฌ์šฉํ•˜๋˜ ์ค‘๋ณต ์ด๋ฒคํŠธ(`selectionChange`, `tableSelectionChange`, `rowSelectionChange` ๋“ฑ)๋Š” ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. V2 ํ‘œ์ค€ ์ด๋ฒคํŠธ๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + +--- + +### 6. v2-table-search-widget (๊ฒ€์ƒ‰ ํ•„ํ„ฐ) + +**์šฉ๋„**: ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๊ฒ€์ƒ‰ ๋ฐ ํ•„ํ„ฐ๋ง + +#### ์ฃผ์š” ํŠน์ง• + +- ๋™์ /๊ณ ์ • ํ•„ํ„ฐ ๋ชจ๋“œ +- ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ง€์› +- ํƒญ๋ณ„ ํ•„ํ„ฐ ๊ฐ’ ์ €์žฅ +- ํ…์ŠคํŠธ/์ˆซ์ž/๋‚ ์งœ/์…€๋ ‰ํŠธ ํ•„ํ„ฐ ํƒ€์ž… +- ๋‹ค์ค‘์„ ํƒ ์ง€์› +- ๋Œ€์ƒ ํŒจ๋„ ์ง€์ • ๊ฐ€๋Šฅ + +#### ๋ฐ์ดํ„ฐ ํ๋ฆ„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-table-search-widget โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ‘  TableOptionsContext์—์„œ ๋“ฑ๋ก๋œ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์กฐํšŒ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ก targetPanelPosition์— ๋”ฐ๋ผ ๋Œ€์ƒ ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ข ํ™œ์„ฑ ํ•„ํ„ฐ ๋ชฉ๋ก ๋กœ๋“œ (localStorage์—์„œ ๋ณต์›) โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ฃ ํ•„ํ„ฐ ๊ฐ’ ์ž…๋ ฅ โ†’ handleFilterChange() โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ค currentTable.onFilterChange(filters) ํ˜ธ์ถœ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ฅ ์—ฐ๊ฒฐ๋œ ํ…Œ์ด๋ธ”์ด ์ž๋™์œผ๋กœ ๋ฐ์ดํ„ฐ ์žฌ์กฐํšŒ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +#### ์ฃผ์š” ์„ค์ • ์ธํ„ฐํŽ˜์ด์Šค + +```typescript +interface TableSearchWidgetConfig { + // ์ž๋™ ์„ ํƒ + autoSelectFirstTable?: boolean; + showTableSelector?: boolean; + + // ํ•„ํ„ฐ ๋ชจ๋“œ + filterMode?: "dynamic" | "preset"; + + // ๊ณ ์ • ํ•„ํ„ฐ (preset ๋ชจ๋“œ) + presetFilters?: Array<{ + id: string; + columnName: string; + columnLabel: string; + filterType: "text" | "number" | "date" | "select"; + width?: number; + multiSelect?: boolean; + }>; + + // ๋Œ€์ƒ ํŒจ๋„ ์œ„์น˜ + targetPanelPosition?: "left" | "right" | "auto"; +} +``` + +--- + +### 7. v2-repeat-container (๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ) + +**์šฉ๋„**: ๋ฐ์ดํ„ฐ ์ˆ˜๋งŒํผ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐ˜๋ณต ๋ Œ๋”๋ง + +#### ์ฃผ์š” ํŠน์ง• + +- ์ˆ˜๋™/ํ…Œ์ด๋ธ”/๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์†Œ์Šค +- ์„ธ๋กœ/๊ฐ€๋กœ/๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ +- ํŽ˜์ด์ง• ์ง€์› +- ํด๋ฆญ ์ด๋ฒคํŠธ (๋‹จ์ผ/๋‹ค์ค‘ ์„ ํƒ) +- ์•„์ดํ…œ ์ œ๋ชฉ ํ…œํ”Œ๋ฆฟ + +#### ๋ฐ์ดํ„ฐ ํ๋ฆ„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-repeat-container โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ‘  dataSourceType์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ๋กœ๋“œ โ”‚ +โ”‚ - manual: ์ˆ˜๋™ ์ž…๋ ฅ ๋ฐ์ดํ„ฐ โ”‚ +โ”‚ - table: DB ํ…Œ์ด๋ธ”์—์„œ ์กฐํšŒ โ”‚ +โ”‚ - repeater: ๋ฆฌํ”ผํ„ฐ ์ปดํฌ๋„ŒํŠธ ๋ฐ์ดํ„ฐ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ก layout์— ๋”ฐ๋ผ ๋ฐฐ์น˜ (vertical / horizontal / grid) โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ข ๊ฐ ์•„์ดํ…œ์— ๋Œ€ํ•ด children ๋ Œ๋”๋ง โ”‚ +โ”‚ (RepeatItemContext๋กœ ํ˜„์žฌ ์•„์ดํ…œ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ) โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ฃ ํด๋ฆญ ์‹œ ์„ ํƒ ์ƒํƒœ ๊ด€๋ฆฌ (selectionMode: single/multi) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +#### ์ฃผ์š” ์„ค์ • ์ธํ„ฐํŽ˜์ด์Šค + +```typescript +interface RepeatContainerConfig { + // ๋ฐ์ดํ„ฐ ์†Œ์Šค + dataSourceType: "manual" | "table" | "repeater"; + tableName?: string; + repeaterComponentId?: string; + manualData?: any[]; + + // ๋ ˆ์ด์•„์›ƒ + layout: "vertical" | "horizontal" | "grid"; + gridColumns: number; + gap: string; + + // ์Šคํƒ€์ผ + showBorder: boolean; + showShadow: boolean; + borderRadius: string; + backgroundColor: string; + padding: string; + + // ์•„์ดํ…œ ์ œ๋ชฉ + showItemTitle: boolean; + itemTitleTemplate: string; // ์˜ˆ: "${name} - ${code}" + titleFontSize: string; + titleColor: string; + titleFontWeight: string; + + // ๋นˆ ์ƒํƒœ + emptyMessage: string; + + // ํŽ˜์ด์ง• + usePaging: boolean; + pageSize: number; + + // ์„ ํƒ + clickable: boolean; + showSelectedState: boolean; + selectionMode: "single" | "multi"; +} +``` + +#### V2 ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ + +v2-repeat-container๋Š” V2 ํ‘œ์ค€ ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์˜ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•˜๊ณ  ๋ฐ˜๋ณต ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค. + +**๊ตฌ๋… ์ด๋ฒคํŠธ**: + +| ์ด๋ฒคํŠธ | ์šฉ๋„ | ๋ฐœํ–‰์ž | +|--------|------|--------| +| `tableListDataChange` | ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ๋ฐ˜๋ณต ํ•ญ๋ชฉ ๊ฐฑ์‹  | v2-table-list | +| `repeaterDataChange` | ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ๋ฐ˜๋ณต ํ•ญ๋ชฉ ๊ฐฑ์‹  | v2-unified-repeater | + +```typescript +import { V2_EVENTS, subscribeV2Event, type TableListDataChangeDetail, type RepeaterDataChangeDetail } from "@/types/component-events"; + +useEffect(() => { + // ๊ณตํ†ต ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜ + const processIncomingData = (componentId: string | undefined, tableName: string | undefined, eventData: any[]) => { + // dataSourceComponentId๊ฐ€ ์„ค์ •๋œ ๊ฒฝ์šฐ ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋งŒ ๋งค์นญ + if (dataSourceComponentId && componentId === dataSourceComponentId) { + setData(eventData); + setCurrentPage(1); + setSelectedIndices([]); + } + // ํ…Œ์ด๋ธ”๋ช…์œผ๋กœ ๋งค์นญ + else if (effectiveTableName && tableName === effectiveTableName) { + setData(eventData); + setCurrentPage(1); + setSelectedIndices([]); + } + }; + + // V2 ํ‘œ์ค€ ์ด๋ฒคํŠธ ๊ตฌ๋… + const unsubscribeTableList = subscribeV2Event(V2_EVENTS.TABLE_LIST_DATA_CHANGE, (event) => { + const { componentId, tableName, data } = event.detail; + processIncomingData(componentId, tableName, data); + }); + + const unsubscribeRepeater = subscribeV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, (event) => { + const { componentId, tableName, data } = event.detail; + processIncomingData(componentId, tableName, data); + }); + + return () => { + unsubscribeTableList(); + unsubscribeRepeater(); + }; +}, [dataSourceComponentId, effectiveTableName, isDesignMode]); +``` + +--- + +## ๊ณตํ†ต ๋ฐ์ดํ„ฐ ํ๋ฆ„ ํŒจํ„ด + +### 1. ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + +๋ชจ๋“  ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ V2 ์ปดํฌ๋„ŒํŠธ๋Š” ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ํ๋ฆ„ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ‘  entityJoinApi.getEntityJoinColumns(tableName) โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ก ์‘๋‹ต: { joinTables, availableColumns } โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ข ConfigPanel์—์„œ ์กฐ์ธ ์ปฌ๋Ÿผ ์„ ํƒ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ฃ entityJoinApi.getTableDataWithJoins() ๋ฐ์ดํ„ฐ ์กฐํšŒ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ค "ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" ํ˜•์‹์œผ๋กœ ์กฐ์ธ ๋ฐ์ดํ„ฐ ํฌํ•จ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2. ํผ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ + +V2 ์ปดํฌ๋„ŒํŠธ๋Š” ํ†ตํ•ฉ ํผ ์‹œ์Šคํ…œ(UnifiedFormContext)์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ํผ ๋ฐ์ดํ„ฐ ํ๋ฆ„ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ‘  ์ปดํฌ๋„ŒํŠธ์—์„œ useFormCompatibility() ํ›… ์‚ฌ์šฉ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ก getValue(fieldName) - ๊ฐ’ ์ฝ๊ธฐ โ”‚ +โ”‚ โ‘ข setValue(fieldName, value) - ๊ฐ’ ์„ค์ • โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ฃ ๊ฐ’ ๋ณ€๊ฒฝ์ด ์ „์ฒด ํผ ์‹œ์Šคํ…œ์— ์ „ํŒŒ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ค ์ €์žฅ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ beforeFormSave ์ด๋ฒคํŠธ ๋ฐœ์ƒ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ฅ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ˜„์žฌ ๊ฐ’์„ formData์— ์ถ”๊ฐ€ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ‘ฆ API ํ˜ธ์ถœํ•˜์—ฌ ์ €์žฅ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 3. ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ํ†ต์‹  + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ํ†ต์‹  โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ†’ โ”‚ ๋ฆฌํ”ผํ„ฐ ์ปดํฌ๋„ŒํŠธโ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ repeaterโ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ v2-button โ”‚ Save โ”‚ v2-unified- โ”‚ โ”‚ +โ”‚ โ”‚ -primary โ”‚ ์ด๋ฒคํŠธ โ”‚ repeater โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ masterRecordId โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๊ฒ€์ƒ‰ ์œ„์ ฏ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ†’ โ”‚ ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ onFilterโ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ v2-table- โ”‚ Change โ”‚ v2-table- โ”‚ โ”‚ +โ”‚ โ”‚ search-widgetโ”‚ โ”‚ list โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก ๊ตฌ์กฐ + +```typescript +// frontend/lib/registry/components/index.ts + +// V2 ์ปดํฌ๋„ŒํŠธ๋“ค (ํ™”๋ฉด๊ด€๋ฆฌ ์ „์šฉ) +import "./v2-unified-repeater/UnifiedRepeaterRenderer"; +import "./v2-button-primary/ButtonPrimaryRenderer"; +import "./v2-split-panel-layout/SplitPanelLayoutRenderer"; +import "./v2-aggregation-widget/AggregationWidgetRenderer"; +import "./v2-card-display/CardDisplayRenderer"; +import "./v2-numbering-rule/NumberingRuleRenderer"; +import "./v2-table-list/TableListRenderer"; +import "./v2-text-display/TextDisplayRenderer"; +import "./v2-pivot-grid/PivotGridRenderer"; +import "./v2-divider-line/DividerLineRenderer"; +import "./v2-repeat-container/RepeatContainerRenderer"; +import "./v2-section-card/SectionCardRenderer"; +import "./v2-section-paper/SectionPaperRenderer"; +import "./v2-rack-structure/RackStructureRenderer"; +import "./v2-location-swap-selector/LocationSwapSelectorRenderer"; +import "./v2-table-search-widget"; +import "./v2-tabs-widget/tabs-component"; +``` + +--- + +## ํŒŒ์ผ ๊ตฌ์กฐ ํ‘œ์ค€ + +๊ฐ V2 ์ปดํฌ๋„ŒํŠธ ํด๋”๋Š” ๋‹ค์Œ ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค: + +``` +v2-{component-name}/ +โ”œโ”€โ”€ index.ts # ์ปดํฌ๋„ŒํŠธ Definition (V2 ์ ‘๋‘์‚ฌ) +โ”œโ”€โ”€ types.ts # TypeScript ํƒ€์ž… ์ •์˜ +โ”œโ”€โ”€ {Component}Component.tsx # ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ +โ”œโ”€โ”€ {Component}ConfigPanel.tsx # ์„ค์ • ํŒจ๋„ +โ”œโ”€โ”€ {Component}Renderer.tsx # ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋“ฑ๋ก ๋ฐ ๋ž˜ํผ +โ”œโ”€โ”€ config.ts # ๊ธฐ๋ณธ ์„ค์ •๊ฐ’ (์„ ํƒ) +โ””โ”€โ”€ README.md # ์‚ฌ์šฉ ๊ฐ€์ด๋“œ (์„ ํƒ) +``` + +--- + +## ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ๋ผ์ธ + +### ์ƒˆ V2 ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ ์‹œ + +1. `v2-` ์ ‘๋‘์‚ฌ๋กœ ํด๋” ์ƒ์„ฑ +2. Definition ์ด๋ฆ„์— `V2` ์ ‘๋‘์‚ฌ ์‚ฌ์šฉ (์˜ˆ: `V2NewComponentDefinition`) +3. `index.ts`์—์„œ import ์ถ”๊ฐ€ +4. ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ง€์› ํ•„์ˆ˜ ๊ตฌํ˜„ +5. ๋‹ค๊ตญ์–ด ํ‚ค ํ•„๋“œ ์ถ”๊ฐ€ (`langKeyId`, `langKey`) + +### ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +- [ ] V2 ํด๋”์—์„œ ์ž‘์—… ์ค‘์ธ์ง€ ํ™•์ธ +- [ ] ์›๋ณธ ํด๋”๋Š” ์ˆ˜์ •ํ•˜์ง€ ์•Š์Œ +- [ ] ์ปดํฌ๋„ŒํŠธ ID์— `v2-` ์ ‘๋‘์‚ฌ ์‚ฌ์šฉ +- [ ] Definition ์ด๋ฆ„์— `V2` ์ ‘๋‘์‚ฌ ์‚ฌ์šฉ +- [ ] ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ ์ง€์› +- [ ] ์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ์„ค์ • ์ง€์› +- [ ] ๋‹ค๊ตญ์–ด ํ•„๋“œ ์ถ”๊ฐ€ + +--- + +## ๊ด€๋ จ ํŒŒ์ผ ๋ชฉ๋ก + +| ํŒŒ์ผ | ์—ญํ•  | +|------|------| +| `frontend/lib/api/entityJoin.ts` | ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ API | +| `frontend/hooks/useFormCompatibility.ts` | ํผ ํ˜ธํ™˜์„ฑ ๋ธŒ๋ฆฟ์ง€ | +| `frontend/components/unified/UnifiedFormContext.tsx` | ํ†ตํ•ฉ ํผ Context | +| `frontend/lib/utils/multilangLabelExtractor.ts` | ๋‹ค๊ตญ์–ด ๋ผ๋ฒจ ์ถ”์ถœ/๋งคํ•‘ | +| `frontend/contexts/ScreenMultiLangContext.tsx` | ๋‹ค๊ตญ์–ด ๋ฒˆ์—ญ Context | +| `frontend/lib/registry/components/index.ts` | ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก | + +--- + +## ์ฐธ๊ณ  ๋ฌธ์„œ + +- [component-development-guide.mdc](.cursor/rules/component-development-guide.mdc) - ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ ์ƒ์„ธ ๊ฐ€์ด๋“œ +- [table-list-component-guide.mdc](.cursor/rules/table-list-component-guide.mdc) - ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ๊ฐ€์ด๋“œ diff --git a/docs/V2_์ปดํฌ๋„ŒํŠธ_์—ฐ๋™_๊ฐ€์ด๋“œ.md b/docs/V2_์ปดํฌ๋„ŒํŠธ_์—ฐ๋™_๊ฐ€์ด๋“œ.md new file mode 100644 index 00000000..a8496926 --- /dev/null +++ b/docs/V2_์ปดํฌ๋„ŒํŠธ_์—ฐ๋™_๊ฐ€์ด๋“œ.md @@ -0,0 +1,1519 @@ +# V2 ์ปดํฌ๋„ŒํŠธ ์—ฐ๋™ ๊ฐ€์ด๋“œ + +## ๋ชฉ์ฐจ + +1. [๊ฐœ์š”](#1-๊ฐœ์š”) +2. [V2 ํ‘œ์ค€ ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ](#2-v2-ํ‘œ์ค€-์ด๋ฒคํŠธ-์‹œ์Šคํ…œ) +3. [์ด๋ฒคํŠธ ์‹œ์Šคํ…œ ์ƒ์„ธ](#3-์ด๋ฒคํŠธ-์‹œ์Šคํ…œ-์ƒ์„ธ) +4. [Context ์‹œ์Šคํ…œ](#4-context-์‹œ์Šคํ…œ) +5. [๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ธํ„ฐํŽ˜์ด์Šค](#5-๋ฐ์ดํ„ฐ-์ „๋‹ฌ-์ธํ„ฐํŽ˜์ด์Šค) +6. [์ปดํฌ๋„ŒํŠธ๋ณ„ ์—ฐ๋™ ๋Šฅ๋ ฅ](#6-์ปดํฌ๋„ŒํŠธ๋ณ„-์—ฐ๋™-๋Šฅ๋ ฅ) +7. [์—ฐ๋™ ๊ฐ€๋Šฅํ•œ ์กฐํ•ฉ](#7-์—ฐ๋™-๊ฐ€๋Šฅํ•œ-์กฐํ•ฉ) +8. [์—ฐ๋™ ์„ค์ • ๋ฐฉ๋ฒ•](#8-์—ฐ๋™-์„ค์ •-๋ฐฉ๋ฒ•) + +--- + +## 1. ๊ฐœ์š” + +V2 ์ปดํฌ๋„ŒํŠธ๋“ค์€ ์„ธ ๊ฐ€์ง€ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ํ†ตํ•ด ์ƒํ˜ธ ํ†ต์‹ ํ•ฉ๋‹ˆ๋‹ค: + +| ๋ฉ”์ปค๋‹ˆ์ฆ˜ | ์šฉ๋„ | ํŠน์ง• | +|----------|------|------| +| **์ด๋ฒคํŠธ ์‹œ์Šคํ…œ** | ๋น„๋™๊ธฐ ํ†ต์‹ , ๋А์Šจํ•œ ๊ฒฐํ•ฉ | V2 ํ‘œ์ค€ ์ด๋ฒคํŠธ ํƒ€์ž… ์‚ฌ์šฉ | +| **Context ์‹œ์Šคํ…œ** | ์ƒํƒœ ๊ณต์œ , ๋™๊ธฐ ํ†ต์‹  | React Context API | +| **๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ธํ„ฐํŽ˜์ด์Šค** | ๋ช…์‹œ์  ๋ฐ์ดํ„ฐ ์ „์†ก | `DataProvidable` / `DataReceivable` | + +--- + +## 2. V2 ํ‘œ์ค€ ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ + +### 2.1 ์ด๋ฒคํŠธ ํƒ€์ž… ์ •์˜ ํŒŒ์ผ + +**ํŒŒ์ผ ์œ„์น˜**: `frontend/types/component-events.ts` + +๋ชจ๋“  V2 ์ปดํฌ๋„ŒํŠธ๋Š” ์ด ํŒŒ์ผ์— ์ •์˜๋œ **ํƒ€์ž… ์•ˆ์ „ํ•œ ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ**์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + +### 2.2 ์ด๋ฒคํŠธ ์ด๋ฆ„ ์ƒ์ˆ˜ + +```typescript +import { V2_EVENTS } from "@/types/component-events"; + +// ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ด๋ฒคํŠธ +V2_EVENTS.TABLE_LIST_DATA_CHANGE // "tableListDataChange" +V2_EVENTS.REPEATER_DATA_CHANGE // "repeaterDataChange" +V2_EVENTS.BEFORE_FORM_SAVE // "beforeFormSave" +V2_EVENTS.AFTER_FORM_SAVE // "afterFormSave" +V2_EVENTS.REPEATER_SAVE // "repeaterSave" +V2_EVENTS.REFRESH_TABLE // "refreshTable" +V2_EVENTS.REFRESH_CARD_DISPLAY // "refreshCardDisplay" +V2_EVENTS.COMPONENT_DATA_TRANSFER // "componentDataTransfer" +V2_EVENTS.SPLIT_PANEL_DATA_TRANSFER // "splitPanelDataTransfer" +``` + +### 2.3 ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ + +#### ํƒ€์ž… ์•ˆ์ „ํ•œ ์ด๋ฒคํŠธ ๋ฐœํ–‰ + +```typescript +import { dispatchV2Event, V2_EVENTS } from "@/types/component-events"; + +// ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ• (ํƒ€์ž… ์•ˆ์ „) +dispatchV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, { + componentId: "my-repeater", + tableName: "order_detail", + data: rows, + selectedData: selectedRows, +}); + +// ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ• (ํƒ€์ž… ์˜ค๋ฅ˜ ๋ฐœ์ƒ) +dispatchV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, { + wrongField: "value", // ํƒ€์ž… ์—๋Ÿฌ! +}); +``` + +#### ํƒ€์ž… ์•ˆ์ „ํ•œ ์ด๋ฒคํŠธ ๊ตฌ๋… + +```typescript +import { subscribeV2Event, V2_EVENTS, type RepeaterDataChangeDetail } from "@/types/component-events"; + +useEffect(() => { + // ๊ตฌ๋… (์ž๋™ cleanup ํ•จ์ˆ˜ ๋ฐ˜ํ™˜) + const unsubscribe = subscribeV2Event( + V2_EVENTS.REPEATER_DATA_CHANGE, + (event: CustomEvent) => { + const { componentId, data } = event.detail; + // ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ + } + ); + + return () => unsubscribe(); +}, []); +``` + +### 2.4 ์ด๋ฒคํŠธ ์ƒ์„ธ ํƒ€์ž… + +```typescript +// ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ +interface TableListDataChangeDetail { + componentId: string; + tableName: string; + data: any[]; + selectedRows: string[] | number[]; +} + +// ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ +interface RepeaterDataChangeDetail { + componentId: string; + tableName: string; + data: any[]; + selectedData?: any[]; +} + +// ํผ ์ €์žฅ ์ „ +interface BeforeFormSaveDetail { + formData: Record; + skipDefaultSave?: boolean; +} + +// ๋ฆฌํ”ผํ„ฐ ์ €์žฅ (๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ FK ์—ฐ๊ฒฐ์šฉ) +interface RepeaterSaveDetail { + parentId?: string | number; + masterRecordId: string | number; + mainFormData: Record; + tableName: string; +} + +// ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ +interface ComponentDataTransferDetail { + sourceComponentId: string; + targetComponentId: string; + data: any[]; + mode: "append" | "replace" | "merge"; + mappingRules?: MappingRule[]; +} +``` + +### 2.5 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ + +**์ด์ „ ๋ฐฉ์‹ (์‚ฌ์šฉ ๊ธˆ์ง€)**: + +```typescript +// โŒ ํƒ€์ž… ์•ˆ์ „ํ•˜์ง€ ์•Š์Œ +window.addEventListener("tableListDataChange" as any, handler); +window.dispatchEvent(new CustomEvent("repeaterDataChange", { detail })); +``` + +**์ƒˆ๋กœ์šด ๋ฐฉ์‹ (๊ถŒ์žฅ)**: + +```typescript +// โœ… ํƒ€์ž… ์•ˆ์ „ํ•จ +import { subscribeV2Event, dispatchV2Event, V2_EVENTS } from "@/types/component-events"; + +const unsubscribe = subscribeV2Event(V2_EVENTS.TABLE_LIST_DATA_CHANGE, handler); +dispatchV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, detail); +``` + +--- + +## 3. ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ ์ƒ์„ธ + +### 3.1 ์ €์žฅ ๊ด€๋ จ ์ด๋ฒคํŠธ + +#### `beforeFormSave` + +ํผ ์ €์žฅ ์ง์ „์— ๋ฐœ์ƒํ•˜์—ฌ ๊ฐ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•  ๊ธฐํšŒ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **๋ฐœํ–‰์ž** | `buttonActions.ts`, `UnifiedFormContext.tsx` | +| **๊ตฌ๋…์ž** | `UnifiedRepeater`, `SimpleRepeaterTable`, `ModalRepeaterTable`, `SelectedItemsDetailInput`, `RepeatScreenModal`, `UniversalFormModal` | +| **๋ฐ์ดํ„ฐ ๊ตฌ์กฐ** | `{ formData: Record, skipDefaultSave?: boolean }` | + +```typescript +// ๋ฐœํ–‰ ์˜ˆ์‹œ +window.dispatchEvent(new CustomEvent("beforeFormSave", { + detail: { formData: {}, skipDefaultSave: false } +})); + +// ๊ตฌ๋… ์˜ˆ์‹œ +window.addEventListener("beforeFormSave", (event: CustomEvent) => { + const { formData } = event.detail; + formData["myField"] = myValue; // ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ +}); +``` + +#### `afterFormSave` + +ํผ ์ €์žฅ ์™„๋ฃŒ ํ›„ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **๋ฐœํ–‰์ž** | `UnifiedFormContext.tsx` | +| **๋ฐ์ดํ„ฐ ๊ตฌ์กฐ** | `{ success: boolean, data?: any }` | + +#### `repeaterSave` + +๋งˆ์Šคํ„ฐ ์ €์žฅ ํ›„ ๋ฆฌํ”ผํ„ฐ์— FK๋ฅผ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **๋ฐœํ–‰์ž** | `InteractiveScreenViewerDynamic.tsx` | +| **๊ตฌ๋…์ž** | `UnifiedRepeater.tsx` | +| **๋ฐ์ดํ„ฐ ๊ตฌ์กฐ** | `{ parentId, masterRecordId, mainFormData, tableName }` | + +```typescript +// ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ์ €์žฅ ํ๋ฆ„ +// 1. ๋งˆ์Šคํ„ฐ ์ €์žฅ ์™„๋ฃŒ +// 2. repeaterSave ์ด๋ฒคํŠธ ๋ฐœํ–‰ +window.dispatchEvent(new CustomEvent("repeaterSave", { + detail: { + masterRecordId: savedId, // ๋งˆ์Šคํ„ฐ ID + tableName: "receiving_mng", + mainFormData: formData + } +})); + +// 3. UnifiedRepeater์—์„œ ์ˆ˜์‹  +// โ†’ ๋ชจ๋“  ํ–‰์˜ foreignKeyColumn์— masterRecordId ์„ค์ • +// โ†’ ๋””ํ…Œ์ผ ํ…Œ์ด๋ธ”์— ์ €์žฅ +``` + +--- + +### 3.2 ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ + +#### `tableListDataChange` + +ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **๋ฐœํ–‰์ž** | `v2-table-list`, `table-list` | +| **๊ตฌ๋…์ž** | `v2-repeat-container`, `v2-aggregation-widget`, `repeat-container`, `aggregation-widget` | +| **๋ฐ์ดํ„ฐ ๊ตฌ์กฐ** | `{ componentId, tableName, data: any[], selectedRows: string[] }` | + +```typescript +// ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ โ†’ ์ง‘๊ณ„ ์œ„์ ฏ ์—ฐ๋™ +// ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ž๋™์œผ๋กœ ์ง‘๊ณ„ ๊ฐฑ์‹  +``` + +#### `repeaterDataChange` + +๋ฆฌํ”ผํ„ฐ ์ปดํฌ๋„ŒํŠธ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **๊ตฌ๋…์ž** | `v2-repeat-container`, `v2-aggregation-widget` | + +--- + +### 3.3 UI ๊ฐฑ์‹  ์ด๋ฒคํŠธ + +#### `refreshTable` + +ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **๋ฐœํ–‰์ž** | `v2-button-primary`, `InteractiveScreenViewerDynamic`, `ScreenModal`, `buttonActions.ts` | +| **๊ตฌ๋…์ž** | `v2-table-list`, `v2-split-panel-layout`, `InteractiveDataTable` | + +```typescript +// ์ €์žฅ ํ›„ ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ +window.dispatchEvent(new CustomEvent("refreshTable")); +``` + +#### `refreshCardDisplay` + +์นด๋“œ ๋””์Šคํ”Œ๋ ˆ์ด๋ฅผ ๋‹ค์‹œ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **๋ฐœํ–‰์ž** | `InteractiveScreenViewerDynamic`, `buttonActions.ts` | +| **๊ตฌ๋…์ž** | `v2-card-display`, `card-display` | + +--- + +### 3.4 ๋ชจ๋‹ฌ ์ œ์–ด ์ด๋ฒคํŠธ + +#### `openEditModal` + +ํŽธ์ง‘ ๋ชจ๋‹ฌ์„ ์—ฝ๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **๋ฐœํ–‰์ž** | `SplitPanelLayout2`, `InteractiveScreenViewer`, `InteractiveDataTable` | +| **๊ตฌ๋…์ž** | `EditModal.tsx`, ํ™”๋ฉด ํŽ˜์ด์ง€ | + +#### `closeEditModal` + +ํŽธ์ง‘ ๋ชจ๋‹ฌ์„ ๋‹ซ์Šต๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **๋ฐœํ–‰์ž** | `v2-button-primary`, `buttonActions.ts` | +| **๊ตฌ๋…์ž** | `EditModal.tsx` | + +#### `saveSuccessInModal` + +๋ชจ๋‹ฌ ๋‚ด ์ €์žฅ ์„ฑ๊ณต ์‹œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **๋ฐœํ–‰์ž** | `v2-button-primary`, `buttonActions.ts` | +| **๊ตฌ๋…์ž** | `ScreenModal.tsx` | + +--- + +### 3.5 ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ด๋ฒคํŠธ + +#### `componentDataTransfer` + +์ปดํฌ๋„ŒํŠธ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์‹œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **๋ฐœํ–‰์ž** | `buttonActions.ts` | +| **๊ตฌ๋…์ž** | `UnifiedRepeater.tsx` | + +#### `splitPanelDataTransfer` + +๋ถ„ํ•  ํŒจ๋„ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์‹œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **๋ฐœํ–‰์ž** | `buttonActions.ts` | +| **๊ตฌ๋…์ž** | `UnifiedRepeater.tsx`, `RepeaterFieldGroupRenderer.tsx` | + +#### `screenDataTransfer` + +ํ™”๋ฉด ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์‹œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **๋ฐœํ–‰์ž** | `buttonActions.ts`, `useScreenDataTransfer.ts` | +| **๊ตฌ๋…์ž** | `useScreenDataTransfer.ts` | + +--- + +### 3.6 ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ + +#### `related-button-select` + +์—ฐ๊ด€ ๋ฐ์ดํ„ฐ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **๋ฐœํ–‰์ž** | `RelatedDataButtonsComponent.tsx` | +| **๊ตฌ๋…์ž** | `v2-table-list`, `table-list`, `InteractiveDataTable` | +| **๋ฐ์ดํ„ฐ ๊ตฌ์กฐ** | `{ targetTable, filterColumn, filterValue, selectedData }` | + +#### `related-button-register` / `related-button-unregister` + +์—ฐ๊ด€ ๋ฐ์ดํ„ฐ ๋ฒ„ํŠผ์ด ๋Œ€์ƒ ํ…Œ์ด๋ธ”์„ ๋“ฑ๋ก/ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **๋ฐœํ–‰์ž** | `RelatedDataButtonsComponent.tsx` | +| **๊ตฌ๋…์ž** | `v2-table-list`, `table-list` | + +--- + +### 3.7 ์ด๋ฒคํŠธ ํ๋ฆ„ ๋‹ค์ด์–ด๊ทธ๋žจ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ์ €์žฅ ํ”Œ๋กœ์šฐ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ [์ €์žฅ ๋ฒ„ํŠผ ํด๋ฆญ] โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ beforeFormSave โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ UnifiedRepeater โ”‚ โ”‚ SimpleRepeater โ”‚ โ”‚ ModalRepeater โ”‚ ... โ”‚ +โ”‚ โ”‚ (๋ฐ์ดํ„ฐ ์ˆ˜์ง‘) โ”‚ โ”‚ (๋ฐ์ดํ„ฐ ์ˆ˜์ง‘) โ”‚ โ”‚ (๋ฐ์ดํ„ฐ ์ˆ˜์ง‘) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ API ์ €์žฅ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ afterFormSave โ”‚ โ”‚ repeaterSave โ”‚ (๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ์‹œ) โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ refreshTable โ”‚ โ”‚ UnifiedRepeater โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ (FK ์„ค์ • ํ›„ ์ €์žฅ)โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ํ”Œ๋กœ์šฐ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-list โ”‚ โ”‚ +โ”‚ โ”‚ (๋ฐ์ดํ„ฐ ๋กœ๋“œ/๋ณ€๊ฒฝ)โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ tableListDataChange โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ–ผ โ–ผ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚v2-aggregation- โ”‚ โ”‚v2-repeat- โ”‚ โ”‚ ๊ธฐํƒ€ ๊ตฌ๋…์ž โ”‚ โ”‚ +โ”‚ โ”‚widget (์ง‘๊ณ„๊ฐฑ์‹ ) โ”‚ โ”‚container โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 4. Context ์‹œ์Šคํ…œ + +### 4.1 TableOptionsContext + +**์—ญํ• **: ํ™”๋ฉด ๋‚ด ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก/๊ด€๋ฆฌ ๋ฐ ํ•„ํ„ฐ๋ง ์—ฐ๋™ + +**ํŒŒ์ผ**: `frontend/contexts/TableOptionsContext.tsx` + +#### ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ + +| ํ•จ์ˆ˜/์ƒํƒœ | ์„ค๋ช… | +|-----------|------| +| `registeredTables` | ๋“ฑ๋ก๋œ ํ…Œ์ด๋ธ” Map | +| `selectedTableId` | ํ˜„์žฌ ์„ ํƒ๋œ ํ…Œ์ด๋ธ” ID | +| `registerTable(tableId, registration)` | ํ…Œ์ด๋ธ” ๋“ฑ๋ก | +| `unregisterTable(tableId)` | ํ…Œ์ด๋ธ” ํ•ด์ œ | +| `getTable(tableId)` | ํ…Œ์ด๋ธ” ์กฐํšŒ | +| `setSelectedTableId(id)` | ์„ ํƒ ํ…Œ์ด๋ธ” ์„ค์ • | +| `updateTableDataCount(tableId, count)` | ๋ฐ์ดํ„ฐ ๊ฑด์ˆ˜ ์—…๋ฐ์ดํŠธ | +| `getActiveTabTables()` | ํ™œ์„ฑ ํƒญ์˜ ํ…Œ์ด๋ธ”๋งŒ ๋ฐ˜ํ™˜ | + +#### TableRegistration ๊ตฌ์กฐ + +```typescript +interface TableRegistration { + tableId: string; + tableName: string; + columns: ColumnInfo[]; + dataCount: number; + parentTabId?: string; // ์†Œ์† ํƒญ ID + onFilterChange: (filters: TableFilter[]) => void; + getColumnUniqueValues: (columnName: string) => Promise; +} +``` + +#### ์—ฐ๋™ ํ๋ฆ„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ TableOptionsContext ์—ฐ๋™ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-list โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ registerTable() โ”‚ +โ”‚ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ TableOptionsContext โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ registeredTables โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - tableId โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - onFilterChange() โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - columns โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-search-widget โ”‚ โ”‚ +โ”‚ โ”‚ - ๋“ฑ๋ก๋œ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ํ‘œ์‹œ โ”‚ โ”‚ +โ”‚ โ”‚ - ํ•„ํ„ฐ ์ž…๋ ฅ โ”‚ โ”‚ +โ”‚ โ”‚ - currentTable.onFilterChange() โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-list (์ž๋™ ์žฌ์กฐํšŒ) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +#### ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ + +| ์ปดํฌ๋„ŒํŠธ | ์‚ฌ์šฉ ๋ฐฉ์‹ | +|----------|----------| +| `v2-table-list` | ํ…Œ์ด๋ธ” ๋“ฑ๋ก/ํ•ด์ œ, ๋ฐ์ดํ„ฐ ๊ฑด์ˆ˜ ์—…๋ฐ์ดํŠธ | +| `v2-table-search-widget` | ๋“ฑ๋ก๋œ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์กฐํšŒ, ํ•„ํ„ฐ ์ ์šฉ | +| `v2-split-panel-layout` | ๋‚ด๋ถ€ ํ…Œ์ด๋ธ” ๋“ฑ๋ก/ํ•ด์ œ | +| `v2-card-display` | ํ…Œ์ด๋ธ” ๋“ฑ๋ก (์„ ํƒ์ ) | + +--- + +### 4.2 SplitPanelContext + +**์—ญํ• **: ์ขŒ์šฐ ๋ถ„ํ•  ํŒจ๋„ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๋ฐ ์ƒํƒœ ๊ด€๋ฆฌ + +**ํŒŒ์ผ**: `frontend/contexts/SplitPanelContext.tsx` + +#### ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ + +| ํ•จ์ˆ˜/์ƒํƒœ | ์„ค๋ช… | +|-----------|------| +| `splitPanelId` | ๋ถ„ํ•  ํŒจ๋„ ID | +| `leftScreenId`, `rightScreenId` | ์ขŒ์šฐ ํ™”๋ฉด ID | +| `selectedLeftData` | ์ขŒ์ธก ์„ ํƒ ๋ฐ์ดํ„ฐ | +| `setSelectedLeftData(data)` | ์ขŒ์ธก ์„ ํƒ ๋ฐ์ดํ„ฐ ์„ค์ • | +| `addedItemIds` | ์šฐ์ธก์— ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ID Set | +| `addItemIds(ids)` | ํ•ญ๋ชฉ ID ์ถ”๊ฐ€ | +| `registerReceiver(receiver)` | ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž ๋“ฑ๋ก | +| `transferToOtherSide(data)` | ๋ฐ˜๋Œ€ํŽธ์œผ๋กœ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ | +| `linkedFilters` | ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์„ค์ • | +| `parentDataMapping` | ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์„ค์ • | + +#### ์—ฐ๋™ ํ๋ฆ„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ SplitPanelContext ์—ฐ๋™ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-split-panel-layout โ”‚ โ”‚ +โ”‚ โ”‚ (SplitPanelProvider) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ขŒ์ธก ํŒจ๋„ โ”‚ โ”‚ ์šฐ์ธก ํŒจ๋„ โ”‚ โ”‚ +โ”‚ โ”‚ (CardDisplay) โ”‚ โ”‚ (TableList) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ ํ–‰ ํด๋ฆญ โ”‚ โ”‚ +โ”‚ โ–ผ โ”‚ โ”‚ +โ”‚ setSelectedLeftData(rowData) โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ relation ์„ค์ •์— ๋”ฐ๋ผ โ”‚ โ”‚ +โ”‚ ์ž๋™ ํ•„ํ„ฐ๋ง (FK ๊ธฐ๋ฐ˜) โ”‚ โ”‚ +โ”‚ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ โ”‚ โ”‚ +โ”‚ โ”‚ ํ‘œ์‹œ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +#### ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ + +| ์ปดํฌ๋„ŒํŠธ | ์‚ฌ์šฉ ๋ฐฉ์‹ | +|----------|----------| +| `v2-split-panel-layout` | Provider ์ œ๊ณต, ์ขŒ์šฐ ํŒจ๋„ ๊ด€๋ฆฌ | +| `v2-table-list` | ๋ถ„ํ•  ํŒจ๋„ ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž๋กœ ๋“ฑ๋ก | +| `v2-card-display` | ๋ถ„ํ•  ํŒจ๋„ ์œ„์น˜ ํ™•์ธ, ๋ฐ์ดํ„ฐ ์ˆ˜์‹  | +| `v2-button-primary` | ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ ํ™•์ธ | + +--- + +### 4.3 ScreenContext + +**์—ญํ• **: ๊ฐ™์€ ํ™”๋ฉด ๋‚ด ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ํ†ต์‹  (๋ฐ์ดํ„ฐ ์ œ๊ณต์ž/์ˆ˜์‹ ์ž ๋“ฑ๋ก) + +**ํŒŒ์ผ**: `frontend/contexts/ScreenContext.tsx` + +#### ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ + +| ํ•จ์ˆ˜/์ƒํƒœ | ์„ค๋ช… | +|-----------|------| +| `screenId` | ํ™”๋ฉด ID | +| `tableName` | ํ…Œ์ด๋ธ”๋ช… | +| `menuObjid` | ๋ฉ”๋‰ด OBJID (์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ์šฉ) | +| `splitPanelPosition` | ๋ถ„ํ•  ํŒจ๋„ ์œ„์น˜ (`left` \| `right`) | +| `formData` | ํผ ๋ฐ์ดํ„ฐ | +| `updateFormData(field, value)` | ํผ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ | +| `registerDataProvider(provider)` | ๋ฐ์ดํ„ฐ ์ œ๊ณต์ž ๋“ฑ๋ก | +| `registerDataReceiver(receiver)` | ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž ๋“ฑ๋ก | +| `getDataProvider(id)` | ๋ฐ์ดํ„ฐ ์ œ๊ณต์ž ์กฐํšŒ | +| `getDataReceiver(id)` | ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž ์กฐํšŒ | +| `getAllDataProviders()` | ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ œ๊ณต์ž ์กฐํšŒ | + +#### ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ + +| ์ปดํฌ๋„ŒํŠธ | ์‚ฌ์šฉ ๋ฐฉ์‹ | +|----------|----------| +| `v2-table-list` | ๋ฐ์ดํ„ฐ ์ œ๊ณต์ž/์ˆ˜์‹ ์ž๋กœ ๋“ฑ๋ก | +| `v2-card-display` | ํ™”๋ฉด ์ปจํ…์ŠคํŠธ ํ™•์ธ | +| `v2-button-primary` | ํ™”๋ฉด ์ปจํ…์ŠคํŠธ ํ™•์ธ, ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์‹คํ–‰ | +| `repeater-field-group` | ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž ๋“ฑ๋ก, formData ์‚ฌ์šฉ | + +--- + +### 4.4 UnifiedFormContext + +**์—ญํ• **: ํผ ์ƒํƒœ ๊ด€๋ฆฌ, ์กฐ๊ฑด๋ถ€ ๋กœ์ง, ์ €์žฅ/๊ฒ€์ฆ/์ดˆ๊ธฐํ™” + +**ํŒŒ์ผ**: `frontend/components/unified/UnifiedFormContext.tsx` + +#### ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ + +| ํ•จ์ˆ˜/์ƒํƒœ | ์„ค๋ช… | +|-----------|------| +| `formData` | ํผ ๋ฐ์ดํ„ฐ | +| `originalData` | ์›๋ณธ ๋ฐ์ดํ„ฐ (์ˆ˜์ • ๋ชจ๋“œ) | +| `status` | ํผ ์ƒํƒœ (isSubmitting, isDirty, isValid ๋“ฑ) | +| `errors` | ํ•„๋“œ ์—๋Ÿฌ ๋ฐฐ์—ด | +| `getValue(field)`, `setValue(field, value)` | ๊ฐ’ ๊ด€๋ฆฌ | +| `submit(options)` | ํผ ์ €์žฅ | +| `reset()` | ํผ ์ดˆ๊ธฐํ™” | +| `validate()` | ํผ ๊ฒ€์ฆ | +| `evaluateCondition(condition)` | ์กฐ๊ฑด ํ‰๊ฐ€ | +| `getRepeaterData(key)`, `setRepeaterData(key, data)` | ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ | + +--- + +### 4.5 ActiveTabContext + +**์—ญํ• **: ํƒญ ์ปดํฌ๋„ŒํŠธ์˜ ํ™œ์„ฑ ํƒญ ์ถ”์  + +**ํŒŒ์ผ**: `frontend/contexts/ActiveTabContext.tsx` + +#### ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ + +| ํ•จ์ˆ˜/์ƒํƒœ | ์„ค๋ช… | +|-----------|------| +| `activeTabs` | ํ™œ์„ฑ ํƒญ ์ •๋ณด Map | +| `setActiveTab(tabsId, tabId)` | ํ™œ์„ฑ ํƒญ ์„ค์ • | +| `getActiveTabId(tabsId)` | ํŠน์ • ํƒญ ์ปดํฌ๋„ŒํŠธ์˜ ํ™œ์„ฑ ํƒญ ID | +| `getAllActiveTabIds()` | ์ „์ฒด ํ™œ์„ฑ ํƒญ ID ๋ชฉ๋ก | + +#### ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ + +| ์ปดํฌ๋„ŒํŠธ | ์‚ฌ์šฉ ๋ฐฉ์‹ | +|----------|----------| +| `v2-table-search-widget` | ํ™œ์„ฑ ํƒญ ๊ธฐ๋ฐ˜ ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง | +| `v2-tabs-widget` | ํƒญ ํ™œ์„ฑํ™” ๊ด€๋ฆฌ | + +--- + +### 4.6 ScreenPreviewContext + +**์—ญํ• **: ๋””์ž์ด๋„ˆ ๋ชจ๋“œ์™€ ์‹ค์ œ ํ™”๋ฉด ๋ชจ๋“œ ๊ตฌ๋ถ„ + +**ํŒŒ์ผ**: `frontend/contexts/ScreenPreviewContext.tsx` + +#### ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ + +| ํ•จ์ˆ˜/์ƒํƒœ | ์„ค๋ช… | +|-----------|------| +| `isPreviewMode` | ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ชจ๋“œ ์—ฌ๋ถ€ | + +#### ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ + +| ์ปดํฌ๋„ŒํŠธ | ์‚ฌ์šฉ ๋ฐฉ์‹ | +|----------|----------| +| `v2-table-search-widget` | ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ชจ๋“œ์—์„œ ์„ค์ • ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™” | +| `v2-button-primary` | ํ”„๋ฆฌ๋ทฐ ๋ชจ๋“œ ํ™•์ธ | + +--- + +### 4.7 Context ๊ณ„์ธต ๊ตฌ์กฐ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Context ๊ณ„์ธต ๊ตฌ์กฐ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ScreenPreviewContext (์ตœ์ƒ์œ„ - ๋””์ž์ด๋„ˆ/์‹ค์ œ ํ™”๋ฉด ๊ตฌ๋ถ„) โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€ ScreenContext (ํ™”๋ฉด ๋ ˆ๋ฒจ) โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€ TableOptionsContext (ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ) โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€ ActiveTabContext (ํƒญ ํ•„ํ„ฐ๋ง) โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€ SplitPanelContext (๋ถ„ํ•  ํŒจ๋„ - ์„ ํƒ์ ) โ”‚ +โ”‚ โ”‚ +โ”‚ UnifiedFormContext (ํผ ์ƒํƒœ ๊ด€๋ฆฌ - ๋…๋ฆฝ์ , ์„ ํƒ์  ์‚ฌ์šฉ) โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 5. ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ธํ„ฐํŽ˜์ด์Šค + +### 5.1 DataProvidable ์ธํ„ฐํŽ˜์ด์Šค + +**๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ**๊ฐ€ ๊ตฌํ˜„ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค + +```typescript +interface DataProvidable { + componentId: string; + componentType: string; + + // ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + getSelectedData(): any[]; + + // ๋ชจ๋“  ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + getAllData(): any[]; + + // ์„ ํƒ ์ดˆ๊ธฐํ™” + clearSelection(): void; +} +``` + +#### ๊ตฌํ˜„ ์ปดํฌ๋„ŒํŠธ + +| ์ปดํฌ๋„ŒํŠธ | ์ œ๊ณต ๋ฐ์ดํ„ฐ | +|----------|------------| +| `v2-table-list` | ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ, ์ „์ฒด ๋ฐ์ดํ„ฐ | +| `v2-card-display` | ์„ ํƒ๋œ ์นด๋“œ ๋ฐ์ดํ„ฐ | +| `select-basic` | ์„ ํƒ๋œ ๊ฐ’ | +| `conditional-container` | ์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ์˜ ์„ ํƒ ๊ฐ’ | + +### 5.2 DataReceivable ์ธํ„ฐํŽ˜์ด์Šค + +**๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์‹ ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ**๊ฐ€ ๊ตฌํ˜„ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค + +```typescript +interface DataReceivable { + componentId: string; + componentType: DataReceivableComponentType; + + // ๋ฐ์ดํ„ฐ ์ˆ˜์‹  + receiveData(data: any[], config: DataReceiverConfig): Promise; + + // ํ˜„์žฌ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + getData(): any; +} + +type DataReceivableComponentType = + | "table-list" + | "unified-repeater" + | "repeater-field-group" + | "simple-repeater-table"; +``` + +#### ๊ตฌํ˜„ ์ปดํฌ๋„ŒํŠธ + +| ์ปดํฌ๋„ŒํŠธ | ์ˆ˜์‹  ๋ชจ๋“œ | +|----------|----------| +| `v2-table-list` | append, replace, merge | +| `repeater-field-group` | append | +| `embedded-screen` | ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ ์ˆ˜์‹  | + +### 5.3 DataReceiverConfig + +๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์‹œ ์„ค์ • + +```typescript +interface DataReceiverConfig { + // ํƒ€๊ฒŸ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด + targetComponentId: string; + targetComponentType: DataReceivableComponentType; + + // ์ˆ˜์‹  ๋ชจ๋“œ + mode: "append" | "replace" | "merge"; + + // ํ•„๋“œ ๋งคํ•‘ ๊ทœ์น™ + mappingRules: Array<{ + sourceField: string; // ์†Œ์Šค ํ•„๋“œ + targetField: string; // ํƒ€๊ฒŸ ํ•„๋“œ + transform?: string; // ๋ณ€ํ™˜ ํ•จ์ˆ˜ (์„ ํƒ) + defaultValue?: any; // ๊ธฐ๋ณธ๊ฐ’ (์„ ํƒ) + }>; + + // ์กฐ๊ฑด๋ถ€ ์ „๋‹ฌ + condition?: { + field: string; + operator: "=" | "!=" | ">" | "<"; + value: any; + }; + + // ๊ฒ€์ฆ ๊ทœ์น™ + validation?: { + required: string[]; // ํ•„์ˆ˜ ํ•„๋“œ + unique?: string[]; // ์ค‘๋ณต ๋ถˆ๊ฐ€ ํ•„๋“œ + }; +} +``` + +### 5.4 ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ํ๋ฆ„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ํ๋ฆ„ (๋ฒ„ํŠผ ์•ก์…˜) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-button- โ”‚ โ”‚ +โ”‚ โ”‚ primary โ”‚ action.type = "transferData" โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ handleTransferDataAction() โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”‚ 1. ScreenContext์—์„œ ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ ์กฐํšŒ โ”‚ โ”‚ +โ”‚ โ”‚ getDataProvider(sourceComponentId) โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ 2. ์†Œ์Šค์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ โ”‚ โ”‚ +โ”‚ โ”‚ source.getSelectedData() ๋˜๋Š” source.getAllData() โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ 3. ๋งคํ•‘ ๊ทœ์น™ ์ ์šฉ โ”‚ โ”‚ +โ”‚ โ”‚ mappingRules.forEach(rule => ...) โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ 4. ScreenContext์—์„œ ํƒ€๊ฒŸ ์ปดํฌ๋„ŒํŠธ ์กฐํšŒ โ”‚ โ”‚ +โ”‚ โ”‚ getDataReceiver(targetComponentId) โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ 5. ํƒ€๊ฒŸ์— ๋ฐ์ดํ„ฐ ์ „๋‹ฌ โ”‚ โ”‚ +โ”‚ โ”‚ target.receiveData(mappedData, config) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํƒ€๊ฒŸ ์ปดํฌ๋„ŒํŠธ โ”‚ โ”‚ +โ”‚ โ”‚ (v2-table-list, โ”‚ โ”‚ +โ”‚ โ”‚ repeater ๋“ฑ) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 6. ์ปดํฌ๋„ŒํŠธ๋ณ„ ์—ฐ๋™ ๋Šฅ๋ ฅ + +### 6.1 ์—ฐ๋™ ๋Šฅ๋ ฅ ๋งคํŠธ๋ฆญ์Šค + +| ์ปดํฌ๋„ŒํŠธ | ์ด๋ฒคํŠธ ๋ฐœํ–‰ | ์ด๋ฒคํŠธ ๊ตฌ๋… | DataProvider | DataReceiver | Context ์‚ฌ์šฉ | +|----------|:-----------:|:-----------:|:------------:|:------------:|:------------:| +| `v2-table-list` | โœ… | โœ… | โœ… | โœ… | TableOptions, Screen, SplitPanel | +| `v2-split-panel-layout` | โœ… | โœ… | โŒ | โŒ | TableOptions (Provider) | +| `v2-unified-repeater` | โœ… | โœ… | โœ… | โœ… | Screen | +| `v2-button-primary` | โœ… | โŒ | โŒ | โŒ | Screen, SplitPanel | +| `v2-table-search-widget` | โŒ | โŒ | โŒ | โŒ | TableOptions, ActiveTab | +| `v2-aggregation-widget` | โŒ | โœ… | โŒ | โŒ | - | +| `v2-repeat-container` | โŒ | โœ… | โŒ | โŒ | - | +| `v2-card-display` | โœ… | โœ… | โœ… | โŒ | TableOptions, Screen, SplitPanel | +| `v2-pivot-grid` | โŒ | โŒ | โŒ | โŒ | - | +| `v2-tabs-widget` | โŒ | โŒ | โŒ | โŒ | ActiveTab | + +### 6.2 ์ปดํฌ๋„ŒํŠธ๋ณ„ ์ƒ์„ธ + +#### v2-table-list + +**๋ฐœํ–‰ ์ด๋ฒคํŠธ**: +- `tableListDataChange` - ๋ฐ์ดํ„ฐ ๋กœ๋“œ/๋ณ€๊ฒฝ ์‹œ + +**๊ตฌ๋… ์ด๋ฒคํŠธ**: +- `refreshTable` - ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ +- `related-button-select` - ์—ฐ๊ด€ ๋ฒ„ํŠผ ์„ ํƒ +- `related-button-register/unregister` - ์—ฐ๊ด€ ๋ฒ„ํŠผ ๋“ฑ๋ก/ํ•ด์ œ + +**DataProvidable ๊ตฌํ˜„**: +```typescript +getSelectedData(): any[] // ์ฒดํฌ๋œ ํ–‰ ๋ฐ์ดํ„ฐ +getAllData(): any[] // ์ „์ฒด ๋ฐ์ดํ„ฐ +clearSelection(): void // ์„ ํƒ ์ดˆ๊ธฐํ™” +``` + +**DataReceivable ๊ตฌํ˜„**: +```typescript +receiveData(data, config): Promise +// mode: "append" - ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€ +// mode: "replace" - ๋ฐ์ดํ„ฐ ๊ต์ฒด +// mode: "merge" - ํ‚ค ๊ธฐ์ค€ ๋ณ‘ํ•ฉ +``` + +--- + +#### v2-unified-repeater + +**๋ฐœํ–‰ ์ด๋ฒคํŠธ**: +- `repeaterDataChange` - ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ (V2 ํ‘œ์ค€ ์ด๋ฒคํŠธ) + +**๊ตฌ๋… ์ด๋ฒคํŠธ**: +- `beforeFormSave` - ์ €์žฅ ์ „ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ +- `repeaterSave` - ๋งˆ์Šคํ„ฐ ์ €์žฅ ํ›„ FK ์„ค์ • +- `componentDataTransfer` - ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ +- `splitPanelDataTransfer` - ๋ถ„ํ•  ํŒจ๋„ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ + +**DataProvidable ๊ตฌํ˜„**: +- `getSelectedData()` - ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ +- `getAllData()` - ์ „์ฒด ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ +- `clearSelection()` - ์„ ํƒ ์ดˆ๊ธฐํ™” + +**DataReceivable ๊ตฌํ˜„**: +- `receiveData(data, config)` - ๋ฐ์ดํ„ฐ ์ˆ˜์‹  (append, replace, merge ๋ชจ๋“œ ์ง€์›) +- `getData()` - ํ˜„์žฌ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + +**Context ๋“ฑ๋ก**: +- ScreenContext์— DataProvider/DataReceiver ์ž๋™ ๋“ฑ๋ก + +--- + +#### v2-button-primary + +**๋ฐœํ–‰ ์ด๋ฒคํŠธ**: +- `refreshTable` - ์ €์žฅ ํ›„ ํ…Œ์ด๋ธ” ๊ฐฑ์‹  +- `closeEditModal` - ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ +- `saveSuccessInModal` - ๋ชจ๋‹ฌ ์ €์žฅ ์„ฑ๊ณต + +**์—ญํ• **: +- ์ €์žฅ, ์‚ญ์ œ, ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๋“ฑ ์•ก์…˜ ์‹คํ–‰ +- `buttonActions.ts`์˜ ํ•จ์ˆ˜๋“ค ํ˜ธ์ถœ + +--- + +#### v2-table-search-widget + +**Context ์˜์กด**: +- `TableOptionsContext` - ๋“ฑ๋ก๋œ ํ…Œ์ด๋ธ” ์กฐํšŒ, ํ•„ํ„ฐ ์ ์šฉ +- `ActiveTabContext` - ํ™œ์„ฑ ํƒญ ๊ธฐ๋ฐ˜ ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง + +**๋™์ž‘**: +1. `TableOptionsContext.registeredTables`์—์„œ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์กฐํšŒ +2. ์‚ฌ์šฉ์ž๊ฐ€ ํ•„ํ„ฐ ์ž…๋ ฅ +3. `currentTable.onFilterChange(filters)` ํ˜ธ์ถœ +4. ํ•ด๋‹น ํ…Œ์ด๋ธ”์ด ์ž๋™์œผ๋กœ ์žฌ์กฐํšŒ + +--- + +#### v2-aggregation-widget + +**๊ตฌ๋… ์ด๋ฒคํŠธ**: +- `tableListDataChange` - ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ง‘๊ณ„ ๊ฐฑ์‹  +- `repeaterDataChange` - ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ง‘๊ณ„ ๊ฐฑ์‹  + +--- + +#### v2-split-panel-layout + +**Provider ์ œ๊ณต**: +- `SplitPanelContext` - ์ขŒ์šฐ ํŒจ๋„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ + +**๋ฐœํ–‰ ์ด๋ฒคํŠธ**: +- `openScreenModal` - ํ™”๋ฉด ๋ชจ๋‹ฌ ์—ด๊ธฐ + +**๊ตฌ๋… ์ด๋ฒคํŠธ**: +- `refreshTable` - ๋‚ด๋ถ€ ํ…Œ์ด๋ธ” ๊ฐฑ์‹  + +**์ปค์Šคํ…€ ๋ชจ๋“œ (displayMode: "custom")**: +- ํŒจ๋„ ๋‚ด๋ถ€์— ์ž์œ ๋กญ๊ฒŒ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ ๊ฐ€๋Šฅ (v2-tabs-widget๊ณผ ๋™์ผ ๊ตฌ์กฐ) +- ์ปดํฌ๋„ŒํŠธ ํด๋ฆญ ์‹œ ์ขŒ์ธก ์„ค์ • ํŒจ๋„์—์„œ ์†์„ฑ ํŽธ์ง‘ +- ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ ์ด๋™, ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค๋กœ ํฌ๊ธฐ ์กฐ์ ˆ +- ๋””์ž์ธ ๋ชจ๋“œ์—์„œ ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง (๋ฏธ๋ฆฌ๋ณด๊ธฐ) + +```typescript +// ์ปค์Šคํ…€ ๋ชจ๋“œ ์„ค์ • ์˜ˆ์‹œ +leftPanel: { + displayMode: "custom", + components: [ + { + id: "btn-1", + componentType: "v2-button-primary", + label: "์ €์žฅ", + position: { x: 10, y: 10 }, + size: { width: 100, height: 40 }, + componentConfig: { buttonAction: "save" } + } + ] +} +``` + +--- + +## 7. ์—ฐ๋™ ๊ฐ€๋Šฅํ•œ ์กฐํ•ฉ + +### 7.1 ๊ฒ€์ƒ‰/ํ•„ํ„ฐ ์—ฐ๋™ + +#### v2-table-search-widget โ†” v2-table-list + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๊ฒ€์ƒ‰ ์œ„์ ฏ โ†” ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-search-widget โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ์ด๋ฆ„ โ”‚ โ”‚ ๋‚ ์งœ๋ฒ”์œ„ โ”‚ โ”‚ ์ƒํƒœ์„ ํƒ โ”‚ โ”‚ ์ดˆ๊ธฐํ™” โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”ฌโ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ onFilterChange(filters) โ”‚ +โ”‚ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-list โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ์—ฐ๊ฒฐ ๋ฐฉ์‹: TableOptionsContext โ”‚ +โ”‚ ์„ค์ •: v2-table-search-widget์˜ targetPanelPosition ์„ค์ • โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์„ค์ • ๋ฐฉ๋ฒ•**: +- `v2-table-search-widget`์˜ `filterMode`: `"dynamic"` ๋˜๋Š” `"preset"` +- `v2-table-search-widget`์˜ `targetPanelPosition`: `"left"`, `"right"`, `"auto"` +- `v2-table-list`๋Š” ์ž๋™์œผ๋กœ `TableOptionsContext`์— ๋“ฑ๋ก๋จ + +--- + +### 7.2 ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ์—ฐ๋™ + +#### v2-split-panel-layout (์ขŒ์ธก โ†” ์šฐ์ธก) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ (๋ถ„ํ•  ํŒจ๋„) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-split-panel-layout โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ์ขŒ์ธก ํŒจ๋„ โ”‚ โ”‚ โ”‚ ์šฐ์ธก ํŒจ๋„ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ (๋งˆ์Šคํ„ฐ ๋ชฉ๋ก) โ”‚ โ”‚ โ”‚ (๋””ํ…Œ์ผ ์ •๋ณด) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - ๋ถ€์„œ ๋ชฉ๋ก โ”‚ โ”‚ โ”‚ - ์„ ํƒ๋œ ๋ถ€์„œ์˜ ์ง์› ๋ชฉ๋ก โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - dept_info โ”‚ โ”€โ”€โ–ถ โ”‚ - user_info โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ - dept_code = ์„ ํƒ๊ฐ’ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ์—ฐ๊ฒฐ ๋ฐฉ์‹: SplitPanelContext + relation ์„ค์ • โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์„ค์ • ๋ฐฉ๋ฒ•**: +```typescript +rightPanel: { + tableName: "user_info", + relation: { + type: "detail", + leftColumn: "dept_code", // ์ขŒ์ธก ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ + rightColumn: "dept_code", // ์šฐ์ธก ํ…Œ์ด๋ธ”์˜ ํ•„ํ„ฐ ์ปฌ๋Ÿผ + // ๋˜๋Š” ๋ณตํ•ฉํ‚ค + keys: [ + { leftColumn: "company_id", rightColumn: "company_id" }, + { leftColumn: "dept_code", rightColumn: "dept_code" } + ] + } +} +``` + +--- + +### 7.3 ํผ ์ €์žฅ ์—ฐ๋™ + +#### v2-button-primary โ†’ v2-unified-repeater + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ํผ ์ €์žฅ + ๋ฆฌํ”ผํ„ฐ ์ €์žฅ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [ํผ ์ž…๋ ฅ ํ•„๋“œ๋“ค] โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ์ž…๊ณ ๋ฒˆํ˜ธ โ”‚ โ”‚ ์ž…๊ณ ์ผ์ž โ”‚ โ”‚ ๊ฑฐ๋ž˜์ฒ˜ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-unified-repeater (์ž…๊ณ  ์ƒ์„ธ) โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ํ’ˆ๋ชฉ์ฝ”๋“œ โ”‚ ํ’ˆ๋ชฉ๋ช… โ”‚ ์ˆ˜๋Ÿ‰ โ”‚ ๋‹จ๊ฐ€ โ”‚ ๊ธˆ์•ก โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ITEM001 โ”‚ ... โ”‚ 10 โ”‚ 1000 โ”‚ 10000 โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ITEM002 โ”‚ ... โ”‚ 5 โ”‚ 2000 โ”‚ 10000 โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [์ €์žฅ ๋ฒ„ํŠผ] โ”‚ โ”‚ +โ”‚ โ”‚ v2-button- โ”‚ โ”‚ +โ”‚ โ”‚ primary โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ โ”‚ +โ”‚ 1. beforeFormSave ๋ฐœํ–‰ โ†’ ๋ฆฌํ”ผํ„ฐ๊ฐ€ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ โ”‚ +โ”‚ 2. ๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ” ์ €์žฅ (receiving_mng) โ”‚ +โ”‚ 3. repeaterSave ๋ฐœํ–‰ โ†’ ๋ฆฌํ”ผํ„ฐ๊ฐ€ FK ์„ค์ • ํ›„ ์ €์žฅ โ”‚ +โ”‚ 4. ๋””ํ…Œ์ผ ํ…Œ์ด๋ธ” ์ €์žฅ (receiving_detail) โ”‚ +โ”‚ 5. refreshTable ๋ฐœํ–‰ โ†’ ํ…Œ์ด๋ธ” ๊ฐฑ์‹  โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์„ค์ • ๋ฐฉ๋ฒ•**: +```typescript +// v2-button-primary +action: { + type: "save", + saveMode: "withRepeater", // ๋ฆฌํ”ผํ„ฐ์™€ ํ•จ๊ป˜ ์ €์žฅ + tableName: "receiving_mng" +} + +// v2-unified-repeater +dataSource: { + tableName: "receiving_detail", + foreignKey: "receiving_id", // ๋งˆ์Šคํ„ฐ FK ์ปฌ๋Ÿผ + referenceKey: "id" // ๋งˆ์Šคํ„ฐ PK ์ปฌ๋Ÿผ +} +``` + +--- + +### 7.4 ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์—ฐ๋™ + +#### v2-table-list โ†’ v2-unified-repeater (๋ฒ„ํŠผ์œผ๋กœ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ํ…Œ์ด๋ธ” ์„ ํƒ โ†’ ๋ฆฌํ”ผํ„ฐ ์ถ”๊ฐ€ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-list (ํ’ˆ๋ชฉ ์„ ํƒ) โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ˜‘ โ”‚ ํ’ˆ๋ชฉ์ฝ”๋“œ โ”‚ ํ’ˆ๋ชฉ๋ช… โ”‚ ๋‹จ๊ฐ€ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ˜‘ โ”‚ ITEM001 โ”‚ ๋…ธํŠธ๋ถ โ”‚ 1,000,000 โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ˜‘ โ”‚ ITEM002 โ”‚ ๋งˆ์šฐ์Šค โ”‚ 50,000 โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ˜ โ”‚ ITEM003 โ”‚ ํ‚ค๋ณด๋“œ โ”‚ 100,000 โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [์ถ”๊ฐ€ ๋ฒ„ํŠผ] โ”‚ action.type = "transferData" โ”‚ +โ”‚ โ”‚ v2-button- โ”‚ โ”‚ +โ”‚ โ”‚ primary โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-unified-repeater (์ฃผ๋ฌธ ์ƒ์„ธ) โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ํ’ˆ๋ชฉ์ฝ”๋“œ โ”‚ ํ’ˆ๋ชฉ๋ช… โ”‚ ์ˆ˜๋Ÿ‰ โ”‚ ๋‹จ๊ฐ€ โ”‚ ๊ธˆ์•ก โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ITEM001 โ”‚ ๋…ธํŠธ๋ถ โ”‚ 1 โ”‚ 1,000,000 โ”‚ 1,000,000โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ITEM002 โ”‚ ๋งˆ์šฐ์Šค โ”‚ 1 โ”‚ 50,000 โ”‚ 50,000 โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์„ค์ • ๋ฐฉ๋ฒ•**: +```typescript +// v2-button-primary +action: { + type: "transferData", + sourceComponentId: "item-table-list", + targetComponentId: "order-detail-repeater", + mappingRules: [ + { sourceField: "item_code", targetField: "item_code" }, + { sourceField: "item_name", targetField: "item_name" }, + { sourceField: "unit_price", targetField: "unit_price" }, + { sourceField: "", targetField: "quantity", defaultValue: 1 } + ], + mode: "append" +} +``` + +--- + +### 7.5 ๋ฐ์ดํ„ฐ ์ง‘๊ณ„ ์—ฐ๋™ + +#### v2-table-list โ†’ v2-aggregation-widget + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ โ†’ ์ง‘๊ณ„ ์œ„์ ฏ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-aggregation-widget โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ๐Ÿ“Š ์ด ๊ฑด์ˆ˜ โ”‚ โ”‚ ๐Ÿ’ฐ ์ด ๊ธˆ์•ก โ”‚ โ”‚ ๐Ÿ“ˆ ํ‰๊ท  ๋‹จ๊ฐ€ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ 15๊ฑด โ”‚ โ”‚ โ‚ฉ3,500,000 โ”‚ โ”‚ โ‚ฉ233,333 โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ–ฒ โ”‚ +โ”‚ โ”‚ tableListDataChange ์ด๋ฒคํŠธ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-list โ”‚ โ”‚ +โ”‚ โ”‚ (๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ž๋™์œผ๋กœ ์ด๋ฒคํŠธ ๋ฐœํ–‰) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ์—ฐ๊ฒฐ ๋ฐฉ์‹: tableListDataChange ์ด๋ฒคํŠธ ์ž๋™ ๊ตฌ๋… โ”‚ +โ”‚ ์„ค์ •: v2-aggregation-widget์˜ dataSourceType = "table" โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์„ค์ • ๋ฐฉ๋ฒ•**: +```typescript +// v2-aggregation-widget +{ + dataSourceType: "table", + items: [ + { columnName: "id", aggregationType: "count", label: "์ด ๊ฑด์ˆ˜" }, + { columnName: "amount", aggregationType: "sum", label: "์ด ๊ธˆ์•ก" }, + { columnName: "unit_price", aggregationType: "avg", label: "ํ‰๊ท  ๋‹จ๊ฐ€" } + ] +} +``` + +--- + +### 7.6 ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ ๋ฒ„ํŠผ ์—ฐ๋™ + +#### related-data-buttons โ†” v2-table-list + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ ๋ฒ„ํŠผ โ†” ํ…Œ์ด๋ธ” โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ์ขŒ์ธก ํŒจ๋„ (๊ฑฐ๋ž˜์ฒ˜ ์„ ํƒ) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-card-display (๊ฑฐ๋ž˜์ฒ˜ ๋ชฉ๋ก) โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ [์„ ํƒ๋จ] ABC์ƒ์‚ฌ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ DEF๋ฌผ์‚ฐ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ ์„ ํƒ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ related-data-buttons โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ํ’ˆ๋ชฉ์ •๋ณด โ”‚ โ”‚ ๋‹จ๊ฐ€์ •๋ณด โ”‚ โ”‚ ๊ฑฐ๋ž˜๋‚ด์—ญ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ (5) โ”‚ โ”‚ (3) โ”‚ โ”‚ (12) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ related-button-select ์ด๋ฒคํŠธ โ”‚ +โ”‚ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-list (ํ’ˆ๋ชฉ์ •๋ณด) โ”‚ โ”‚ +โ”‚ โ”‚ - customer_code = "ABC์ƒ์‚ฌ" ์กฐ๊ฑด์œผ๋กœ ํ•„ํ„ฐ๋ง โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +### 7.7 ํƒญ ๊ธฐ๋ฐ˜ ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง + +#### v2-tabs-widget โ†’ v2-table-search-widget โ†’ v2-table-list + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ํƒญ ๊ธฐ๋ฐ˜ ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-tabs-widget โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ [์ฃผ๋ฌธ] โ”‚ โ”‚ ์ž…๊ณ  โ”‚ โ”‚ ์žฌ๊ณ  โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ ActiveTabContext โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-search-widget โ”‚ โ”‚ +โ”‚ โ”‚ (ํ™œ์„ฑ ํƒญ์— ํ•ด๋‹นํ•˜๋Š” ํ…Œ์ด๋ธ”๋งŒ ๋Œ€์ƒ์œผ๋กœ ํ‘œ์‹œ) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ–ผ onFilterChange โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-list (์ฃผ๋ฌธ ํƒญ ๋‚ด) โ”‚ โ”‚ +โ”‚ โ”‚ - parentTabId๊ฐ€ ํ™œ์„ฑ ํƒญ๊ณผ ์ผ์น˜ํ•˜๋Š” ํ…Œ์ด๋ธ”๋งŒ ํ•„ํ„ฐ ์ ์šฉ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 8. ์—ฐ๋™ ์„ค์ • ๋ฐฉ๋ฒ• + +### 8.1 ๊ฒ€์ƒ‰ ์œ„์ ฏ + ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ + +**ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ**: +- `v2-table-search-widget` +- `v2-table-list` + +**์„ค์ •**: + +```typescript +// v2-table-search-widget ์„ค์ • +{ + filterMode: "preset", // ๋˜๋Š” "dynamic" + presetFilters: [ + { columnName: "name", filterType: "text", columnLabel: "์ด๋ฆ„" }, + { columnName: "created_at", filterType: "date", columnLabel: "๋“ฑ๋ก์ผ" }, + { columnName: "status", filterType: "select", columnLabel: "์ƒํƒœ" } + ], + targetPanelPosition: "auto" // "left" | "right" | "auto" +} + +// v2-table-list๋Š” ํŠน๋ณ„ํ•œ ์„ค์ • ๋ถˆํ•„์š” (์ž๋™ ๋“ฑ๋ก) +``` + +--- + +### 8.2 ๋ถ„ํ•  ํŒจ๋„ ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ + +**ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ**: +- `v2-split-panel-layout` + +**์„ค์ •**: + +```typescript +{ + leftPanel: { + title: "๋ถ€์„œ ๋ชฉ๋ก", + tableName: "dept_info", + displayMode: "list", + columns: [ + { name: "dept_code", label: "๋ถ€์„œ์ฝ”๋“œ" }, + { name: "dept_name", label: "๋ถ€์„œ๋ช…" } + ] + }, + rightPanel: { + title: "์ง์› ๋ชฉ๋ก", + tableName: "user_info", + displayMode: "table", + columns: [ + { name: "user_id", label: "์‚ฌ๋ฒˆ" }, + { name: "user_name", label: "์ด๋ฆ„" }, + { name: "position", label: "์ง์ฑ…" } + ], + relation: { + type: "detail", + leftColumn: "dept_code", + rightColumn: "dept_code" + } + }, + splitRatio: 30, + resizable: true +} +``` + +--- + +### 8.3 ํผ + ๋ฆฌํ”ผํ„ฐ ์ €์žฅ + +**ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ**: +- ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ๋“ค (text-input, date-input ๋“ฑ) +- `v2-unified-repeater` +- `v2-button-primary` + +**์„ค์ •**: + +```typescript +// v2-unified-repeater ์„ค์ • +{ + renderMode: "inline", + dataSource: { + tableName: "order_detail", + foreignKey: "order_id", // ๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ”์˜ FK + referenceKey: "id" // ๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ”์˜ PK + }, + columns: [ + { name: "item_code", label: "ํ’ˆ๋ชฉ์ฝ”๋“œ" }, + { name: "quantity", label: "์ˆ˜๋Ÿ‰" }, + { name: "unit_price", label: "๋‹จ๊ฐ€" } + ], + features: { + showAddButton: true, + showDeleteButton: true, + inlineEdit: true + } +} + +// v2-button-primary ์„ค์ • +{ + text: "์ €์žฅ", + action: { + type: "save", + saveMode: "withRepeater" + } +} +``` + +--- + +### 8.4 ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (ํ…Œ์ด๋ธ” โ†’ ๋ฆฌํ”ผํ„ฐ) + +**ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ**: +- `v2-table-list` (์†Œ์Šค) +- `v2-button-primary` (์ „๋‹ฌ ํŠธ๋ฆฌ๊ฑฐ) +- `v2-unified-repeater` (ํƒ€๊ฒŸ) + +**์„ค์ •**: + +```typescript +// v2-button-primary ์„ค์ • +{ + text: "์ถ”๊ฐ€", + action: { + type: "transferData", + config: { + sourceComponentId: "item-selection-table", + targetComponentId: "order-detail-repeater", + mode: "append", + mappingRules: [ + { sourceField: "item_code", targetField: "item_code" }, + { sourceField: "item_name", targetField: "item_name" }, + { sourceField: "unit_price", targetField: "unit_price" }, + { sourceField: "", targetField: "quantity", defaultValue: 1 } + ], + validation: { + unique: ["item_code"] // ์ค‘๋ณต ๋ฐฉ์ง€ + } + } + } +} +``` + +--- + +### 8.5 ์ง‘๊ณ„ ์œ„์ ฏ ์—ฐ๋™ + +**ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ**: +- `v2-table-list` ๋˜๋Š” `v2-unified-repeater` +- `v2-aggregation-widget` + +**์„ค์ •**: + +```typescript +// v2-aggregation-widget ์„ค์ • +{ + dataSourceType: "table", // ๋˜๋Š” "repeater" + // sourceComponentId๋Š” ์ž๋™ ๊ฐ์ง€ (๊ฐ™์€ ํ™”๋ฉด์˜ ์ฒซ ๋ฒˆ์งธ ํ…Œ์ด๋ธ”) + items: [ + { + id: "total-count", + label: "์ด ๊ฑด์ˆ˜", + columnName: "id", + aggregationType: "count", + icon: "FileText" + }, + { + id: "total-amount", + label: "์ด ๊ธˆ์•ก", + columnName: "amount", + aggregationType: "sum", + format: { + prefix: "โ‚ฉ", + thousandSeparator: true + } + } + ], + layout: "horizontal", + refreshOnFormChange: true +} +``` + +--- + +## ์—ฐ๋™ ์กฐํ•ฉ ์š”์•ฝํ‘œ + +| ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ | ํƒ€๊ฒŸ ์ปดํฌ๋„ŒํŠธ | ์—ฐ๋™ ๋ฐฉ์‹ | ์šฉ๋„ | +|--------------|--------------|----------|------| +| `v2-table-search-widget` | `v2-table-list` | TableOptionsContext | ๊ฒ€์ƒ‰/ํ•„ํ„ฐ | +| `v2-split-panel-layout` ์ขŒ | `v2-split-panel-layout` ์šฐ | SplitPanelContext | ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ | +| `v2-button-primary` | `v2-unified-repeater` | beforeFormSave/repeaterSave | ์ €์žฅ | +| `v2-table-list` | `v2-unified-repeater` | DataProvidable/DataReceivable | ๋ฐ์ดํ„ฐ ์ „๋‹ฌ | +| `v2-table-list` | `v2-aggregation-widget` | tableListDataChange | ์ง‘๊ณ„ | +| `v2-unified-repeater` | `v2-aggregation-widget` | repeaterDataChange | ์ง‘๊ณ„ | +| `v2-tabs-widget` | `v2-table-search-widget` | ActiveTabContext | ํƒญ ํ•„ํ„ฐ๋ง | +| `related-data-buttons` | `v2-table-list` | related-button-select | ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ | +| `v2-button-primary` | `v2-table-list` | refreshTable | ์ƒˆ๋กœ๊ณ ์นจ | +| `v2-card-display` | `v2-table-list` | SplitPanelContext | ์„ ํƒ ์—ฐ๋™ | + +--- + +## ๊ด€๋ จ ํŒŒ์ผ ์ฐธ์กฐ + +| ํŒŒ์ผ | ์—ญํ•  | +|------|------| +| `frontend/lib/utils/buttonActions.ts` | ๋ฒ„ํŠผ ์•ก์…˜ ์‹คํ–‰, ์ด๋ฒคํŠธ ๋ฐœํ–‰ | +| `frontend/contexts/TableOptionsContext.tsx` | ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ Context | +| `frontend/contexts/SplitPanelContext.tsx` | ๋ถ„ํ•  ํŒจ๋„ Context | +| `frontend/contexts/ScreenContext.tsx` | ํ™”๋ฉด Context | +| `frontend/contexts/ActiveTabContext.tsx` | ํ™œ์„ฑ ํƒญ Context | +| `frontend/components/unified/UnifiedFormContext.tsx` | ํผ Context | +| `frontend/types/data-transfer.ts` | ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ํƒ€์ž… | diff --git a/docs/image-file-storage-guide.md b/docs/image-file-storage-guide.md new file mode 100644 index 00000000..73c99baa --- /dev/null +++ b/docs/image-file-storage-guide.md @@ -0,0 +1,214 @@ +# ์ด๋ฏธ์ง€/ํŒŒ์ผ ์ €์žฅ ๋ฐฉ์‹ ๊ฐ€์ด๋“œ + +## ๊ฐœ์š” + +WACE ์†”๋ฃจ์…˜์—์„œ ์ด๋ฏธ์ง€ ๋ฐ ํŒŒ์ผ์€ **attach_file_info ํ…Œ์ด๋ธ”**์— ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ , ์‹ค์ œ ํŒŒ์ผ์€ **์„œ๋ฒ„ ๋””์Šคํฌ**์— ์ €์žฅํ•˜๋Š” ์ด์ค‘ ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 1. ๋ฐ์ดํ„ฐ ํ๋ฆ„ + +``` +[์‚ฌ์šฉ์ž ์—…๋กœ๋“œ] โ†’ [๋ฐฑ์—”๋“œ API] โ†’ [๋””์Šคํฌ ์ €์žฅ] + [DB ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ] + โ†“ โ†“ + /uploads/COMPANY_7/ attach_file_info ํ…Œ์ด๋ธ” + 2026/02/06/ (objid, file_path, ...) + 1770346704685_5.png +``` + +### ์ €์žฅ ๊ณผ์ • + +1. ์‚ฌ์šฉ์ž๊ฐ€ ํŒŒ์ผ ์—…๋กœ๋“œ โ†’ `POST /api/files/upload` +2. ๋ฐฑ์—”๋“œ๊ฐ€ ํŒŒ์ผ์„ ๋””์Šคํฌ์— ์ €์žฅ: `/uploads/{company_code}/{YYYY}/{MM}/{DD}/{timestamp}_{filename}` +3. `attach_file_info` ํ…Œ์ด๋ธ”์— ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ INSERT (objid, file_path, target_objid ๋“ฑ) +4. ๋น„์ฆˆ๋‹ˆ์Šค ํ…Œ์ด๋ธ”์˜ ์ด๋ฏธ์ง€ ์ปฌ๋Ÿผ์— **ํŒŒ์ผ objid** ์ €์žฅ (์˜ˆ: `item_info.image = '433765011963536400'`) + +### ์กฐํšŒ ๊ณผ์ • + +1. ๋น„์ฆˆ๋‹ˆ์Šค ํ…Œ์ด๋ธ”์—์„œ ์ด๋ฏธ์ง€ ์ปฌ๋Ÿผ ๊ฐ’(objid) ๋กœ๋“œ +2. `GET /api/files/preview/{objid}` ๋กœ ์ด๋ฏธ์ง€ ํ”„๋ฆฌ๋ทฐ ์š”์ฒญ +3. ๋ฐฑ์—”๋“œ๊ฐ€ `attach_file_info`์—์„œ objid๋กœ ํŒŒ์ผ ์ •๋ณด ์กฐํšŒ +4. ๋””์Šคํฌ์—์„œ ์‹ค์ œ ํŒŒ์ผ์„ ์ฝ์–ด ์‘๋‹ต + +--- + +## 2. ํ…Œ์ด๋ธ” ๊ตฌ์กฐ + +### attach_file_info (ํŒŒ์ผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ) + +| ์ปฌ๋Ÿผ | ํƒ€์ž… | ์„ค๋ช… | +|------|------|------| +| objid | numeric | ํŒŒ์ผ ๊ณ ์œ  ID (PK, ํฐ ์ˆซ์ž) | +| real_file_name | varchar | ์›๋ณธ ํŒŒ์ผ๋ช… | +| saved_file_name | varchar | ์ €์žฅ๋œ ํŒŒ์ผ๋ช… (timestamp_์›๋ณธ๋ช…) | +| file_path | varchar | ์ €์žฅ ๊ฒฝ๋กœ (/uploads/COMPANY_7/2026/02/06/...) | +| file_ext | varchar | ํŒŒ์ผ ํ™•์žฅ์ž | +| file_size | numeric | ํŒŒ์ผ ํฌ๊ธฐ (bytes) | +| target_objid | varchar | ์—ฐ๊ฒฐ ๋Œ€์ƒ (์•„๋ž˜ ํŒจํ„ด ์ฐธ์กฐ) | +| company_code | varchar | ํšŒ์‚ฌ ์ฝ”๋“œ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ) | +| status | varchar | ์ƒํƒœ (ACTIVE, DELETED) | +| writer | varchar | ์—…๋กœ๋” ID | +| regdate | timestamp | ๋“ฑ๋ก์ผ์‹œ | +| is_representative | boolean | ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ์—ฌ๋ถ€ | + +### ๋น„์ฆˆ๋‹ˆ์Šค ํ…Œ์ด๋ธ” (์˜ˆ: item_info, company_mng) + +์ด๋ฏธ์ง€ ์ปฌ๋Ÿผ์— `attach_file_info.objid` ๊ฐ’์„ ๋ฌธ์ž์—ด๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + +```sql +-- item_info.image = '433765011963536400' +-- company_mng.company_image = '413276787660035200' +``` + +--- + +## 3. target_objid ํŒจํ„ด + +`attach_file_info.target_objid`๋Š” ํŒŒ์ผ์ด ์–ด๋””์— ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. + +| ํŒจํ„ด | ์˜ˆ์‹œ | ์„ค๋ช… | +|------|------|------| +| ํ…œํ”Œ๋ฆฟ ๋ชจ๋“œ | `screen_files:140:comp_z4yffowb:image` | ํ™”๋ฉด ์„ค๊ณ„ ์‹œ ์—…๋กœ๋“œ (screenId:componentId:columnName) | +| ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ | `item_info:uuid-xxx:image` | ํŠน์ • ๋ ˆ์ฝ”๋“œ์— ์—ฐ๊ฒฐ (tableName:recordId:columnName) | + +--- + +## 4. ํŒŒ์ผ ์กฐํšŒ API + +### GET /api/files/preview/{objid} + +์ด๋ฏธ์ง€ ํ”„๋ฆฌ๋ทฐ (๊ณต๊ฐœ ์ ‘๊ทผ ํ—ˆ์šฉ). + +``` +GET /api/files/preview/433765011963536400 +โ†’ 200 OK (์ด๋ฏธ์ง€ ๋ฐ”์ด๋„ˆ๋ฆฌ) +``` + +**์ฃผ์˜: objid๋ฅผ parseInt()๋กœ ๋ณ€ํ™˜ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.** JavaScript์˜ `Number.MAX_SAFE_INTEGER`(9007199254740991)๋ฅผ ์ดˆ๊ณผํ•˜๋Š” ํฐ ์ˆซ์ž์ด๋ฏ€๋กœ **์ •๋ฐ€๋„ ์†์‹ค**์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋“œ์‹œ **๋ฌธ์ž์—ด**๋กœ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + +```typescript +// ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ• +const fileRecord = await query("SELECT * FROM attach_file_info WHERE objid = $1", [parseInt(objid)]); +// โ†’ parseInt("433765011963536400") = 433765011963536416 (16 ์ฐจ์ด!) +// โ†’ DB์—์„œ ์ฐพ์„ ์ˆ˜ ์—†์Œ โ†’ 404 + +// ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ• +const fileRecord = await query("SELECT * FROM attach_file_info WHERE objid = $1", [objid]); +// โ†’ PostgreSQL์ด ๋ฌธ์ž์—ด โ†’ numeric ์ž๋™ ์บ์ŠคํŒ… +``` + +### GET /api/files/component-files + +์ปดํฌ๋„ŒํŠธ๋ณ„ ํŒŒ์ผ ๋ชฉ๋ก ์กฐํšŒ (์ธ์ฆ ํ•„์š”). + +``` +GET /api/files/component-files?screenId=149&componentId=comp_z4yffowb&tableName=item_info&recordId=uuid-xxx&columnName=image +``` + +**์กฐํšŒ ์šฐ์„ ์ˆœ์œ„:** +1. **๋ฐ์ดํ„ฐ ํŒŒ์ผ**: `target_objid = '{tableName}:{recordId}:{columnName}'` ํŒจํ„ด์œผ๋กœ ์กฐํšŒ +2. **ํ…œํ”Œ๋ฆฟ ํŒŒ์ผ**: `target_objid = 'screen_files:{screenId}:{componentId}:{columnName}'` ํŒจํ„ด์œผ๋กœ ์กฐํšŒ +3. **๋ ˆ์ฝ”๋“œ ์ปฌ๋Ÿผ ๊ฐ’ ์กฐํšŒ (fallback)**: ์œ„ ๋‘ ๋ฐฉ๋ฒ•์œผ๋กœ ํŒŒ์ผ์„ ์ฐพ์ง€ ๋ชปํ•˜๋ฉด, ๋น„์ฆˆ๋‹ˆ์Šค ํ…Œ์ด๋ธ”์˜ ๋ ˆ์ฝ”๋“œ์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ ๊ฐ’(ํŒŒ์ผ objid)์„ ์ฝ์–ด ์ง์ ‘ ์กฐํšŒ + +```sql +-- fallback: ๋ ˆ์ฝ”๋“œ์˜ image ์ปฌ๋Ÿผ์— ์ €์žฅ๋œ objid๋กœ ์ง์ ‘ ์กฐํšŒ +SELECT "image" FROM "item_info" WHERE id = $1; +-- โ†’ '433765011963536400' +SELECT * FROM attach_file_info WHERE objid = '433765011963536400' AND status = 'ACTIVE'; +``` + +--- + +## 5. ํ”„๋ก ํŠธ์—”๋“œ ์ปดํฌ๋„ŒํŠธ + +### v2-file-upload (FileUploadComponent.tsx) + +ํ˜„์žฌ ์‚ฌ์šฉ๋˜๋Š” V2 ํŒŒ์ผ ์—…๋กœ๋“œ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค. + +**ํŒŒ์ผ ๊ฒฝ๋กœ**: `frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx` + +#### ์ด๋ฏธ์ง€ ๋กœ๋“œ ๋ฐฉ์‹ + +1. **formData์˜ ์ปฌ๋Ÿผ ๊ฐ’์œผ๋กœ ๋กœ๋“œ**: `formData[columnName]`์— ํŒŒ์ผ objid๊ฐ€ ์žˆ์œผ๋ฉด `/api/files/preview/{objid}`๋กœ ์ด๋ฏธ์ง€ ํ‘œ์‹œ +2. **getComponentFiles API๋กœ ๋กœ๋“œ**: target_objid ํŒจํ„ด์œผ๋กœ ์„œ๋ฒ„์—์„œ ํŒŒ์ผ ๋ชฉ๋ก ์กฐํšŒ + +#### ์ƒํƒœ ๊ด€๋ฆฌ + +- `uploadedFiles` state: ํ˜„์žฌ ํ‘œ์‹œ ์ค‘์ธ ํŒŒ์ผ ๋ชฉ๋ก +- `localStorage` ๋ฐฑ์—…: `fileUpload_{componentId}_{columnName}` ํ‚ค๋กœ ์ €์žฅ +- `window.globalFileState`: ์ „์—ญ ํŒŒ์ผ ์ƒํƒœ (์ปดํฌ๋„ŒํŠธ ๊ฐ„ ๋™๊ธฐํ™”) + +#### ๋“ฑ๋ก/์ˆ˜์ • ๋ชจ๋“œ ๊ตฌ๋ถ„ + +- **์ˆ˜์ • ๋ชจ๋“œ** (isRecordMode=true, recordId ์žˆ์Œ): localStorage/์„œ๋ฒ„์—์„œ ๊ธฐ์กด ํŒŒ์ผ ๋ณต์› +- **๋“ฑ๋ก ๋ชจ๋“œ** (isRecordMode=false, recordId ์—†์Œ): localStorage ๋ณต์› ์Šคํ‚ต, ๋นˆ ์ƒํƒœ๋กœ ์‹œ์ž‘ +- **๋‹จ์ผ ํผ ํ™”๋ฉด** (ํšŒ์‚ฌ์ •๋ณด ๋“ฑ): `formData[columnName]`์˜ objid ๊ฐ’์œผ๋กœ ์ด๋ฏธ์ง€ ์ž๋™ ๋กœ๋“œ + +### file-upload (๋ ˆ๊ฑฐ์‹œ) + +**ํŒŒ์ผ ๊ฒฝ๋กœ**: `frontend/lib/registry/components/file-upload/FileUploadComponent.tsx` + +V2MediaRenderer์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ ˆ๊ฑฐ์‹œ ์ปดํฌ๋„ŒํŠธ. v2-file-upload์™€ ์œ ์‚ฌํ•˜์ง€๋งŒ ๋ณ„๋„ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค. + +### ImageWidget + +**ํŒŒ์ผ ๊ฒฝ๋กœ**: `frontend/components/screen/widgets/types/ImageWidget.tsx` + +๋‹จ์ˆœ ์ด๋ฏธ์ง€ ํ‘œ์‹œ์šฉ ์œ„์ ฏ. ํŒŒ์ผ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์€ ์žˆ์œผ๋‚˜, `getFullImageUrl()`๋กœ URL์„ ๋ณ€ํ™˜ํ•˜์—ฌ `` ํƒœ๊ทธ๋กœ ์ง์ ‘ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ํŒŒ์ผ ๊ด€๋ฆฌ(๋ชฉ๋ก, ์‚ญ์ œ ๋“ฑ) ๊ธฐ๋Šฅ์€ ์—†์Šต๋‹ˆ๋‹ค. + +--- + +## 6. ๋””์Šคํฌ ์ €์žฅ ๊ตฌ์กฐ + +``` +backend-node/uploads/ +โ”œโ”€โ”€ COMPANY_7/ # ํšŒ์‚ฌ๋ณ„ ๊ฒฉ๋ฆฌ +โ”‚ โ”œโ”€โ”€ 2026/ +โ”‚ โ”‚ โ”œโ”€โ”€ 01/ +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ 08/ +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ 1767863580718_img.jpg +โ”‚ โ”‚ โ””โ”€โ”€ 02/ +โ”‚ โ”‚ โ””โ”€โ”€ 06/ +โ”‚ โ”‚ โ”œโ”€โ”€ 1770346704685_5.png +โ”‚ โ”‚ โ””โ”€โ”€ 1770352493105_5.png +โ”œโ”€โ”€ COMPANY_9/ +โ”‚ โ””โ”€โ”€ ... +โ””โ”€โ”€ company_*/ # ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ + โ””โ”€โ”€ ... +``` + +--- + +## 7. ์ˆ˜์ • ์ด๋ ฅ (2026-02-06) + +### parseInt ์ •๋ฐ€๋„ ์†์‹ค ์ˆ˜์ • + +**ํŒŒ์ผ**: `backend-node/src/controllers/fileController.ts` + +`attach_file_info.objid`๋Š” `numeric` ํƒ€์ž…์œผ๋กœ `433765011963536400` ๊ฐ™์€ ๋งค์šฐ ํฐ ์ˆซ์ž์ž…๋‹ˆ๋‹ค. JavaScript์˜ `parseInt()`๋Š” `Number.MAX_SAFE_INTEGER`(์•ฝ 9 * 10^15)๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ์ •๋ฐ€๋„ ์†์‹ค์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. + +| objid (์›๋ณธ) | parseInt ๊ฒฐ๊ณผ | ์ฐจ์ด | +|:---|:---|:---:| +| 396361999644927100 | 396361999644927104 | -4 | +| 433765011963536400 | 433765011963536384 | +16 | +| 1128460590844245000 | 1128460590844244992 | +8 | + +**์ˆ˜์ •**: `parseInt(objid)` โ†’ `objid` (๋ฌธ์ž์—ด ์ง์ ‘ ์ „๋‹ฌ, 8๊ณณ) + +### getComponentFiles fallback ์ถ”๊ฐ€ + +**ํŒŒ์ผ**: `backend-node/src/controllers/fileController.ts` + +์ˆ˜์ • ๋ชจ๋‹ฌ์—์„œ ์ด๋ฏธ์ง€๊ฐ€ ์•ˆ ๋ณด์ด๋Š” ๋ฌธ์ œ. `target_objid` ํŒจํ„ด์ด ์ผ์น˜ํ•˜์ง€ ์•Š์„ ๋•Œ, ๋น„์ฆˆ๋‹ˆ์Šค ํ…Œ์ด๋ธ”์˜ ๋ ˆ์ฝ”๋“œ ์ปฌ๋Ÿผ ๊ฐ’์œผ๋กœ ํŒŒ์ผ์„ ์ง์ ‘ ์กฐํšŒํ•˜๋Š” fallback ๋กœ์ง ์ถ”๊ฐ€. + +### v2-file-upload ๋“ฑ๋ก ๋ชจ๋“œ ํŒŒ์ผ ์ž”์กด ๋ฐฉ์ง€ + +**ํŒŒ์ผ**: `frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx` + +์—ฐ์† ๋“ฑ๋ก ์‹œ ์ด์ „ ๋“ฑ๋ก์˜ ์ด๋ฏธ์ง€๊ฐ€ ๋‚จ์•„์žˆ๋Š” ๋ฌธ์ œ. `loadComponentFiles`์™€ fallback ๋กœ์ง์—์„œ ๋“ฑ๋ก ๋ชจ๋“œ(recordId ์—†์Œ)์ผ ๋•Œ ํŒŒ์ผ ๋ณต์›์„ ์Šคํ‚ตํ•˜๋„๋ก ์ˆ˜์ •. + +### ORDER BY ๊ธฐ๋ณธ ์ •๋ ฌ ์ถ”๊ฐ€ + +**ํŒŒ์ผ**: `backend-node/src/services/tableManagementService.ts` + +`sortBy` ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์—†์„ ๋•Œ `ORDER BY created_date DESC`๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ ์šฉ. 4๊ณณ ์ˆ˜์ •. diff --git a/docs/kjs/SAVED_DATA_NESTED_STRUCTURE_BUG_FIX.md b/docs/kjs/SAVED_DATA_NESTED_STRUCTURE_BUG_FIX.md new file mode 100644 index 00000000..1cdf3af1 --- /dev/null +++ b/docs/kjs/SAVED_DATA_NESTED_STRUCTURE_BUG_FIX.md @@ -0,0 +1,149 @@ +# ์ €์žฅ ํ›„ ํ”Œ๋กœ์šฐ ์‹คํ–‰ ์‹œ ํผ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์˜ค๋ฅ˜ ์ˆ˜์ • + +## ์˜ค๋ฅ˜ ํ˜„์ƒ + +์‚ฌ์šฉ์ž๊ฐ€ ํผ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•œ ํ›„, ์—ฐ๊ฒฐ๋œ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ(์˜ˆ: ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž๋™ ์„ค์ •)๊ฐ€ ์‹คํ–‰๋  ๋•Œ `sabun` ๊ฐ’์ด `undefined`๋กœ ์ „๋‹ฌ๋˜์–ด UPDATE ์ฟผ๋ฆฌ์˜ WHERE ์กฐ๊ฑด์ด ์ž‘๋™ํ•˜์ง€ ์•Š๋Š” ๋ฌธ์ œ. + +### ์ฆ์ƒ +- ์ €์žฅ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ INSERT๋Š” ์ •์ƒ ์ž‘๋™ +- ์ €์žฅ ํ›„ ์‹คํ–‰๋˜๋Š” ๋…ธ๋“œ ํ”Œ๋กœ์šฐ์—์„œ `user_password` UPDATE๊ฐ€ ์‹คํŒจ (0๊ฑด ์—…๋ฐ์ดํŠธ) +- ์ฝ˜์†” ๋กœ๊ทธ์—์„œ `savedData.sabun: undefined` ์ถœ๋ ฅ + +``` +๐Ÿ“ฆ [executeAfterSaveControl] savedData ํ•„๋“œ: ['id', 'screenId', 'tableName', 'data', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'] +๐Ÿ“ฆ [executeAfterSaveControl] savedData.sabun: undefined +``` + +--- + +## ์›์ธ ๋ถ„์„ + +### API ์‘๋‹ต ๊ตฌ์กฐ์˜ 3๋‹จ๊ณ„ ์ค‘์ฒฉ + +์ €์žฅ API(`DynamicFormApi.saveFormData`)์˜ ์‘๋‹ต์ด 3๋‹จ๊ณ„๋กœ ์ค‘์ฒฉ๋˜์–ด ์žˆ์—ˆ์Œ: + +```typescript +// 1๋‹จ๊ณ„: Axios ์‘๋‹ต +saveResult = { + data: { ... } // API ์‘๋‹ต +} + +// 2๋‹จ๊ณ„: API ์‘๋‹ต ๋ž˜ํ•‘ (ApiResponse ์ธํ„ฐํŽ˜์ด์Šค) +saveResult.data = { + success: true, + data: { ... }, // ์ €์žฅ๋œ ๋ ˆ์ฝ”๋“œ + message: "์ €์žฅ ์™„๋ฃŒ" +} + +// 3๋‹จ๊ณ„: ์ €์žฅ๋œ ๋ ˆ์ฝ”๋“œ (dynamic_form_data ํ…Œ์ด๋ธ” ๊ตฌ์กฐ) +saveResult.data.data = { + id: 123, + screenId: 106, + tableName: "user_info", + data: { sabun: "20260205-087", user_name: "TEST", ... }, // โ† ์‹ค์ œ ํผ ๋ฐ์ดํ„ฐ + createdAt: "2026-02-05T...", + updatedAt: "2026-02-05T...", + createdBy: "admin", + updatedBy: "admin" +} + +// 4๋‹จ๊ณ„: ์‹ค์ œ ํผ ๋ฐ์ดํ„ฐ (์šฐ๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ) +saveResult.data.data.data = { + sabun: "20260205-087", + user_name: "TEST", + user_id: "Kim1542", + ... +} +``` + +### ๊ธฐ์กด ์ฝ”๋“œ์˜ ๋ฌธ์ œ์  + +```typescript +// ๊ธฐ์กด ์ฝ”๋“œ (buttonActions.ts:1619-1621) +const savedData = saveResult?.data?.data || saveResult?.data || {}; +const formData = savedData; // โ† 2๋‹จ๊ณ„๊นŒ์ง€๋งŒ ์ถ”์ถœ + +// savedData = { id, screenId, tableName, data: {...}, createdAt, ... } +// savedData.sabun = undefined โ† ๋ฌธ์ œ ๋ฐœ์ƒ! +``` + +๊ธฐ์กด ์ฝ”๋“œ๋Š” 2๋‹จ๊ณ„(`saveResult.data.data`)๊นŒ์ง€๋งŒ ์ถ”์ถœํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—, `savedData`๊ฐ€ ์ €์žฅ๋œ ๋ ˆ์ฝ”๋“œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€๋ฆฌํ‚ค๊ณ  ์žˆ์—ˆ์Œ. ์‹ค์ œ ํผ ๋ฐ์ดํ„ฐ๋Š” `savedData.data` ์•ˆ์— ์žˆ์—ˆ์Œ. + +--- + +## ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• + +### ์ˆ˜์ •๋œ ์ฝ”๋“œ + +```typescript +// ์ˆ˜์ •๋œ ์ฝ”๋“œ (buttonActions.ts:1619-1628) +// ๐Ÿ”ง ์ˆ˜์ •: saveResult.data๊ฐ€ 3๋‹จ๊ณ„๋กœ ์ค‘์ฒฉ๋œ ๊ฒฝ์šฐ ์‹ค์ œ ํผ ๋ฐ์ดํ„ฐ ์ถ”์ถœ +// saveResult.data = API ์‘๋‹ต { success, data, message } +// saveResult.data.data = ์ €์žฅ๋œ ๋ ˆ์ฝ”๋“œ { id, screenId, tableName, data, createdAt... } +// saveResult.data.data.data = ์‹ค์ œ ํผ ๋ฐ์ดํ„ฐ { sabun, user_name... } +const savedRecord = saveResult?.data?.data || saveResult?.data || {}; +const actualFormData = savedRecord?.data || savedRecord; // โ† 3๋‹จ๊ณ„๊นŒ์ง€ ์ถ”์ถœ +const formData = (Object.keys(actualFormData).length > 0 ? actualFormData : context.formData || {}); +``` + +### ์ˆ˜์ • ํ•ต์‹ฌ +1. `savedRecord`: ์ €์žฅ๋œ ๋ ˆ์ฝ”๋“œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ (`{ id, screenId, tableName, data, ... }`) +2. `actualFormData`: `savedRecord.data`๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๊ฒƒ์„ ์‚ฌ์šฉ, ์—†์œผ๋ฉด `savedRecord` ์ž์ฒด ์‚ฌ์šฉ +3. ํด๋ฐฑ: `actualFormData`๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด `context.formData` ์‚ฌ์šฉ + +--- + +## ์ˆ˜์ •๋œ ํŒŒ์ผ + +| ํŒŒ์ผ | ์ˆ˜์ • ๋‚ด์šฉ | +|------|-----------| +| `frontend/lib/utils/buttonActions.ts` | 3๋‹จ๊ณ„ ์ค‘์ฒฉ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์—์„œ ์‹ค์ œ ํผ ๋ฐ์ดํ„ฐ ์ถ”์ถœ ๋กœ์ง ์ˆ˜์ • (๋ผ์ธ 1619-1628) | + +--- + +## ๊ฒ€์ฆ ๊ฒฐ๊ณผ + +### ์ˆ˜์ • ์ „ +``` +๐Ÿ“ฆ [executeAfterSaveControl] savedData ํ•„๋“œ: ['id', 'screenId', 'tableName', 'data', ...] +๐Ÿ“ฆ [executeAfterSaveControl] savedData.sabun: undefined +``` + +### ์ˆ˜์ • ํ›„ +``` +๐Ÿ“ฆ [executeAfterSaveControl] savedRecord ๊ตฌ์กฐ: ['id', 'screenId', 'tableName', 'data', ...] +๐Ÿ“ฆ [executeAfterSaveControl] actualFormData ์ถ”์ถœ: ['sabun', 'user_id', 'user_password', ...] +๐Ÿ“ฆ [executeAfterSaveControl] formData.sabun: 20260205-087 +``` + +### DB ํ™•์ธ +```sql +SELECT sabun, user_name, user_password FROM user_info WHERE sabun = '20260205-087'; +-- ๊ฒฐ๊ณผ: sabun: "20260205-087", user_name: "TEST", user_password: "1e538e2abdd9663437343212a4853591" +``` + +--- + +## ๊ตํ›ˆ + +1. **API ์‘๋‹ต ๊ตฌ์กฐ ํ™•์ธ**: API ์‘๋‹ต์ด ์—ฌ๋Ÿฌ ๋‹จ๊ณ„๋กœ ๋ž˜ํ•‘๋  ์ˆ˜ ์žˆ์Œ. ํ”„๋ก ํŠธ์—”๋“œ์—์„œ `apiClient`๊ฐ€ ํ•œ ๋ฒˆ, `ApiResponse` ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ํ•œ ๋ฒˆ, ๊ทธ๋ฆฌ๊ณ  ์‹ค์ œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๊ฐ€ ๋˜ ๋‹ค๋ฅธ ๋ ˆ๋ฒจ์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Œ. + +2. **๋กœ๊ทธ ์ถ”๊ฐ€์˜ ์ค‘์š”์„ฑ**: ์ค‘๊ฐ„ ๋‹จ๊ณ„๋งˆ๋‹ค ๋กœ๊ทธ๋ฅผ ์ฐ์–ด ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ๋””๋ฒ„๊น…์— ํ•„์ˆ˜์ . + +3. **ํด๋ฐฑ ์ฒ˜๋ฆฌ**: ๋ฐ์ดํ„ฐ ์ถ”์ถœ ์‹œ ์—ฌ๋Ÿฌ ๋‹จ๊ณ„์˜ ํด๋ฐฑ์„ ๋‘์–ด ๋‹ค์–‘ํ•œ ์‘๋‹ต ๊ตฌ์กฐ์— ๋Œ€์‘. + +--- + +## ๊ด€๋ จ ์ด์Šˆ + +- ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž๋™ ์„ค์ • ๋…ธ๋“œ ํ”Œ๋กœ์šฐ๊ฐ€ ์ €์žฅ ํ›„ ์‹คํ–‰๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ +- ์ €์žฅ ํ›„ ์—ฐ๊ฒฐ๋œ UPDATE ํ”Œ๋กœ์šฐ์—์„œ WHERE ์กฐ๊ฑด์ด ์ž‘๋™ํ•˜์ง€ ์•Š๋Š” ๋ฌธ์ œ + +--- + +## ์ž‘์„ฑ ์ •๋ณด + +- **์ž‘์„ฑ์ผ**: 2026-02-05 +- **์ž‘์„ฑ์ž**: AI Assistant +- **๊ด€๋ จ ํ™”๋ฉด**: ๋ถ€์„œ๊ด€๋ฆฌ > ์‚ฌ์šฉ์ž ๋“ฑ๋ก ๋ชจ๋‹ฌ +- **๊ด€๋ จ ํ”Œ๋กœ์šฐ**: flowId: 120 (๋ถ€์„œ๊ด€๋ฆฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž๋™์„ธํŒ…) diff --git a/docs/multi-agent-system-plan.md b/docs/multi-agent-system-plan.md new file mode 100644 index 00000000..46a1df3c --- /dev/null +++ b/docs/multi-agent-system-plan.md @@ -0,0 +1,989 @@ +# Multi-Agent ํ˜‘์—… ์‹œ์Šคํ…œ ์„ค๊ณ„์„œ + +> Cursor ์—์ด์ „ํŠธ ๊ฐ„ ํ˜‘์—…์„ ํ†ตํ•œ ํšจ์œจ์ ์ธ ๊ฐœ๋ฐœ ์‹œ์Šคํ…œ + +## ๋ชฉ์ฐจ + +1. [๊ฐœ์š”](#๊ฐœ์š”) +2. [์•„ํ‚คํ…์ฒ˜](#์•„ํ‚คํ…์ฒ˜) +3. [์—์ด์ „ํŠธ ์—ญํ•  ์ •์˜](#์—์ด์ „ํŠธ-์—ญํ• -์ •์˜) +4. [ํ†ต์‹  ํ”„๋กœํ† ์ฝœ](#ํ†ต์‹ -ํ”„๋กœํ† ์ฝœ) +5. [์›Œํฌํ”Œ๋กœ์šฐ](#์›Œํฌํ”Œ๋กœ์šฐ) +6. [ํ”„๋กฌํ”„ํŠธ ํ…œํ”Œ๋ฆฟ](#ํ”„๋กฌํ”„ํŠธ-ํ…œํ”Œ๋ฆฟ) +7. [MCP ์„œ๋ฒ„ ๊ตฌํ˜„](#mcp-์„œ๋ฒ„-๊ตฌํ˜„) +8. [๋น„์šฉ ๋ถ„์„](#๋น„์šฉ-๋ถ„์„) +9. [ํ•œ๊ณ„์  ๋ฐ ํ•ด๊ฒฐ๋ฐฉ์•ˆ](#ํ•œ๊ณ„์ -๋ฐ-ํ•ด๊ฒฐ๋ฐฉ์•ˆ) + +--- + +## ๊ฐœ์š” + +### ๋ฌธ์ œ์ : ๋‹จ์ผ ์—์ด์ „ํŠธ์˜ ํ•œ๊ณ„ + +``` +๋‹จ์ผ ์—์ด์ „ํŠธ ๋ฌธ์ œ: +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ€ข ์ปจํ…์ŠคํŠธ ํญ๋ฐœ (50k+ ํ† ํฐ โ†’ ๊นŒ๋จน์Œ) โ”‚ +โ”‚ โ€ข ์ „๋ฌธ์„ฑ ๋ถ„์‚ฐ (๋ชจ๋“  ์˜์—ญ ์–•๊ฒŒ ์•Ž) โ”‚ +โ”‚ โ€ข ์žฌ์ž‘์—… ๋นˆ๋ฒˆ (์‹ค์ˆ˜, ๋ˆ„๋ฝ) โ”‚ +โ”‚ โ€ข ๊ฒ€์ฆ ๋ถ€์žฌ (ํฌ๋กœ์Šค์ฒดํฌ ์—†์Œ) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### ํ•ด๊ฒฐ์ฑ…: Multi-Agent ํ˜‘์—… + +``` +๋ฉ€ํ‹ฐ ์—์ด์ „ํŠธ ์žฅ์ : +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ€ข ์ปจํ…์ŠคํŠธ ๋ถ„๋ฆฌ (๊ฐ์ž ์ž‘์€ ์ปจํ…์ŠคํŠธ) โ”‚ +โ”‚ โ€ข ์ „๋ฌธ์„ฑ ์ง‘์ค‘ (์˜์—ญ๋ณ„ ๊นŠ์€ ์ดํ•ด) โ”‚ +โ”‚ โ€ข ํฌ๋กœ์Šค์ฒดํฌ (์„œ๋กœ ๊ฒ€์ฆ) โ”‚ +โ”‚ โ€ข ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ (๋™์‹œ ์ž‘์—…) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### ๋ชจ๋ธ ํ‹ฐ์–ด๋ง ์ „๋žต + +| ์—์ด์ „ํŠธ | ๋ชจ๋ธ | ์—ญํ•  | ๋น„์šฉ | +|----------|------|------|------| +| Agent A (PM) | Claude Opus 4.5 | ๋ถ„์„, ๊ณ„ํš, ์กฐ์œจ | ๋†’์Œ | +| Agent B (Backend) | Claude Sonnet | ๋ฐฑ์—”๋“œ ๊ตฌํ˜„ | ๋‚ฎ์Œ | +| Agent C (DB) | Claude Sonnet | DB/์ฟผ๋ฆฌ ๋‹ด๋‹น | ๋‚ฎ์Œ | +| Agent D (Frontend) | Claude Sonnet | ํ”„๋ก ํŠธ ๊ตฌํ˜„ | ๋‚ฎ์Œ | + +**์˜ˆ์ƒ ๋น„์šฉ ์ ˆ๊ฐ: 50-60%** + +--- + +## ์•„ํ‚คํ…์ฒ˜ + +### ์ „์ฒด ๊ตฌ์กฐ + +``` + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ USER โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Agent A (PM) โ”‚ + โ”‚ Claude Opus 4.5 โ”‚ + โ”‚ โ”‚ + โ”‚ โ€ข ์‚ฌ์šฉ์ž ์˜๋„ ํŒŒ์•… โ”‚ + โ”‚ โ€ข ์ž‘์—… ๋ถ„๋ฐฐ โ”‚ + โ”‚ โ€ข ๊ฒฐ๊ณผ ํ†ตํ•ฉ โ”‚ + โ”‚ โ€ข ํ’ˆ์งˆ ๊ฒ€์ฆ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ โ”‚ + โ–ผ โ–ผ โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Agent B โ”‚ โ”‚ Agent C โ”‚ โ”‚ Agent D โ”‚ + โ”‚ (Backend) โ”‚ โ”‚ (Database) โ”‚ โ”‚ (Frontend) โ”‚ + โ”‚ Sonnet โ”‚ โ”‚ Sonnet โ”‚ โ”‚ Sonnet โ”‚ + โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ + โ”‚ โ€ข API ์„ค๊ณ„/๊ตฌํ˜„ โ”‚ โ”‚ โ€ข ์Šคํ‚ค๋งˆ ์„ค๊ณ„ โ”‚ โ”‚ โ€ข ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ โ”‚ + โ”‚ โ€ข ์„œ๋น„์Šค ๋กœ์ง โ”‚ โ”‚ โ€ข ์ฟผ๋ฆฌ ์ž‘์„ฑ โ”‚ โ”‚ โ€ข ํŽ˜์ด์ง€ ๊ตฌํ˜„ โ”‚ + โ”‚ โ€ข ๋ผ์šฐํŒ… โ”‚ โ”‚ โ€ข ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ โ”‚ โ”‚ โ€ข ์Šคํƒ€์ผ๋ง โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ MCP Orchestrator โ”‚ + โ”‚ โ”‚ + โ”‚ โ€ข ๋ฉ”์‹œ์ง€ ๋ผ์šฐํŒ… โ”‚ + โ”‚ โ€ข ๋ณ‘๋ ฌ ์‹คํ–‰ โ”‚ + โ”‚ โ€ข ๊ฒฐ๊ณผ ์ˆ˜์ง‘ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### ํด๋”๋ณ„ ๋‹ด๋‹น ์˜์—ญ + +| ์—์ด์ „ํŠธ | ๋‹ด๋‹น ํด๋” | ํŒŒ์ผ ์œ ํ˜• | +|----------|-----------|-----------| +| Agent B (Backend) | `backend-node/src/` | `.ts`, `.js` | +| Agent C (DB) | `src/com/pms/mapper/`, `db/` | `.xml`, `.sql` | +| Agent D (Frontend) | `frontend/` | `.tsx`, `.ts`, `.css` | +| Agent A (PM) | ์ „์ฒด ์กฐ์œจ | ๋ชจ๋“  ํŒŒ์ผ (์ฝ๊ธฐ ์œ„์ฃผ) | + +--- + +## ์—์ด์ „ํŠธ ์—ญํ•  ์ •์˜ + +### Agent A (PM) - ํ”„๋กœ์ ํŠธ ๋งค๋‹ˆ์ € + +```yaml +์—ญํ• : ์ „์ฒด ์กฐ์œจ ๋ฐ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค +๋ชจ๋ธ: Claude Opus 4.5 + +ํ•ต์‹ฌ ์ฑ…์ž„: + ์˜๋„ ํŒŒ์•…: + - ์‚ฌ์šฉ์ž ์š”์ฒญ ๋ถ„์„ + - ๋ชจํ˜ธํ•œ ์š”์ฒญ ๋ช…ํ™•ํ™” + - ์ˆจ๊ฒจ์ง„ ์š”๊ตฌ์‚ฌํ•ญ ๋ฐœ๊ตด + + ์ž‘์—… ๋ถ„๋ฐฐ: + - ์ž‘์—…์„ ์„ธ๋ถ€ ํƒœ์Šคํฌ๋กœ ๋ถ„ํ•ด + - ์ ์ ˆํ•œ ์—์ด์ „ํŠธ์—๊ฒŒ ํ• ๋‹น + - ์šฐ์„ ์ˆœ์œ„ ๋ฐ ์˜์กด์„ฑ ๊ฒฐ์ • + + ํ’ˆ์งˆ ๊ด€๋ฆฌ: + - ๊ฒฐ๊ณผ๋ฌผ ๊ฒ€์ฆ + - ์ผ๊ด€์„ฑ ์ฒดํฌ + - ์ถฉ๋Œ ํ•ด๊ฒฐ + + ํ†ตํ•ฉ: + - ๊ฐœ๋ณ„ ๊ฒฐ๊ณผ๋ฌผ ์ทจํ•ฉ + - ์ตœ์ข… ๊ฒฐ๊ณผ ์ƒ์„ฑ + - ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด๊ณ  + +ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ: + - ์ง์ ‘ ์ฝ”๋“œ ๊ตฌํ˜„ (์ „๋ฌธ๊ฐ€์—๊ฒŒ ์œ„์ž„) + - ํŠน์ • ์˜์—ญ ๊นŠ์ด ๋ถ„์„ (์ „๋ฌธ๊ฐ€์—๊ฒŒ ์š”์ฒญ) +``` + +### Agent B (Backend) - ๋ฐฑ์—”๋“œ ์ „๋ฌธ๊ฐ€ + +```yaml +์—ญํ• : API ๋ฐ ์„œ๋ฒ„ ๋กœ์ง ๋‹ด๋‹น +๋ชจ๋ธ: Claude Sonnet + +๋‹ด๋‹น ์˜์—ญ: + ํด๋”: + - backend-node/src/controllers/ + - backend-node/src/services/ + - backend-node/src/routes/ + - backend-node/src/middleware/ + - backend-node/src/utils/ + + ์ž‘์—…: + - REST API ์—”๋“œํฌ์ธํŠธ ์„ค๊ณ„/๊ตฌํ˜„ + - ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ตฌํ˜„ + - ๋ฏธ๋“ค์›จ์–ด ์ž‘์„ฑ + - ์—๋Ÿฌ ํ•ธ๋“ค๋ง + - ์ธ์ฆ/์ธ๊ฐ€ ๋กœ์ง + +๋‹ด๋‹น ์•„๋‹Œ ๊ฒƒ: + - frontend/ ํด๋” (Agent D ๋‹ด๋‹น) + - SQL ์ฟผ๋ฆฌ ์ง์ ‘ ์ž‘์„ฑ (Agent C์—๊ฒŒ ์š”์ฒญ) + - DB ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ (Agent C ๋‹ด๋‹น) + +ํ˜‘์—… ํ•„์š” ์‹œ: + - DB ์ฟผ๋ฆฌ ํ•„์š” โ†’ Agent C์—๊ฒŒ ์š”์ฒญ + - ํ”„๋ก ํŠธ ์—ฐ๋™ ๋ฌธ์ œ โ†’ Agent D์™€ ํ˜‘์˜ +``` + +### Agent C (Database) - DB ์ „๋ฌธ๊ฐ€ + +```yaml +์—ญํ• : ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐ ์ฟผ๋ฆฌ ๋‹ด๋‹น +๋ชจ๋ธ: Claude Sonnet + +๋‹ด๋‹น ์˜์—ญ: + ํด๋”: + - src/com/pms/mapper/ + - db/ + - backend-node/src/database/ + + ์ž‘์—…: + - ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ์„ค๊ณ„ + - MyBatis ๋งคํผ XML ์ž‘์„ฑ + - SQL ์ฟผ๋ฆฌ ์ตœ์ ํ™” + - ์ธ๋ฑ์Šค ์„ค๊ณ„ + - ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ + +๋‹ด๋‹น ์•„๋‹Œ ๊ฒƒ: + - API ๋กœ์ง (Agent B ๋‹ด๋‹น) + - ํ”„๋ก ํŠธ์—”๋“œ (Agent D ๋‹ด๋‹น) + - ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํŒ๋‹จ (Agent A์—๊ฒŒ ํ™•์ธ) + +ํ˜‘์—… ํ•„์š” ์‹œ: + - API์—์„œ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ โ†’ Agent B์™€ ํ˜‘์˜ + - ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ์‚ฌ์šฉ๋ฒ• โ†’ Agent B์—๊ฒŒ ์ „๋‹ฌ +``` + +### Agent D (Frontend) - ํ”„๋ก ํŠธ์—”๋“œ ์ „๋ฌธ๊ฐ€ + +```yaml +์—ญํ• : UI/UX ๋ฐ ํด๋ผ์ด์–ธํŠธ ๋กœ์ง ๋‹ด๋‹น +๋ชจ๋ธ: Claude Sonnet + +๋‹ด๋‹น ์˜์—ญ: + ํด๋”: + - frontend/components/ + - frontend/pages/ + - frontend/lib/ + - frontend/hooks/ + - frontend/styles/ + + ์ž‘์—…: + - React ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ + - ํŽ˜์ด์ง€ ๋ ˆ์ด์•„์›ƒ + - ์ƒํƒœ ๊ด€๋ฆฌ + - API ์—ฐ๋™ (ํ˜ธ์ถœ) + - ์Šคํƒ€์ผ๋ง + +๋‹ด๋‹น ์•„๋‹Œ ๊ฒƒ: + - API ๊ตฌํ˜„ (Agent B ๋‹ด๋‹น) + - DB ์ฟผ๋ฆฌ (Agent C ๋‹ด๋‹น) + - API ์ŠคํŽ™ ๊ฒฐ์ • (Agent A/B์™€ ํ˜‘์˜) + +ํ˜‘์—… ํ•„์š” ์‹œ: + - API ์—”๋“œํฌ์ธํŠธ ํ•„์š” โ†’ Agent B์—๊ฒŒ ์š”์ฒญ + - ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ํ™•์ธ โ†’ Agent C์—๊ฒŒ ๋ฌธ์˜ +``` + +--- + +## ํ†ต์‹  ํ”„๋กœํ† ์ฝœ + +### ๋ฉ”์‹œ์ง€ ํฌ๋งท + +```typescript +// ์š”์ฒญ ๋ฉ”์‹œ์ง€ +interface TaskRequest { + id: string; // ๊ณ ์œ  ID (์˜ˆ: "task-001") + from: 'A' | 'B' | 'C' | 'D'; // ๋ฐœ์‹ ์ž + to: 'A' | 'B' | 'C' | 'D'; // ์ˆ˜์‹ ์ž + type: 'info_request' | 'work_request' | 'question'; + priority: 'high' | 'medium' | 'low'; + content: { + task: string; // ์ž‘์—… ๋‚ด์šฉ + context?: string; // ๋ฐฐ๊ฒฝ ์ •๋ณด + expected_output?: string; // ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ + depends_on?: string[]; // ์„ ํ–‰ ์ž‘์—… ID + }; + timestamp: string; +} + +// ์‘๋‹ต ๋ฉ”์‹œ์ง€ +interface TaskResponse { + id: string; // ์š”์ฒญ ID์™€ ๋งค์นญ + from: 'A' | 'B' | 'C' | 'D'; + to: 'A' | 'B' | 'C' | 'D'; + status: 'success' | 'partial' | 'failed' | 'need_clarification'; + confidence: 'high' | 'medium' | 'low'; + + result?: { + summary: string; // ํ•œ ์ค„ ์š”์•ฝ + details: string; // ์ƒ์„ธ ๋‚ด์šฉ + files_affected?: string[]; // ์˜ํ–ฅ๋ฐ›๋Š” ํŒŒ์ผ + code_changes?: CodeChange[]; // ์ฝ”๋“œ ๋ณ€๊ฒฝ์‚ฌํ•ญ + }; + + // ๋ฉ”ํƒ€ ์ •๋ณด + scope_violations?: string[]; // ์Šค์ฝ”ํ”„ ๋ฒ—์–ด๋‚œ ์š”์ฒญ + dependencies?: string[]; // ํ•„์š”ํ•œ ์„ ํ–‰ ์ž‘์—… + side_effects?: string[]; // ๋ถ€์ž‘์šฉ + alternatives?: string[]; // ๋Œ€์•ˆ + + // ์ถ”๊ฐ€ ์š”์ฒญ + questions?: string[]; // ๋ช…ํ™•ํ™” ํ•„์š” + needs_from_others?: { + agent: 'A' | 'B' | 'C' | 'D'; + request: string; + }[]; + + timestamp: string; +} + +// ์ฝ”๋“œ ๋ณ€๊ฒฝ +interface CodeChange { + file: string; + action: 'create' | 'modify' | 'delete'; + content?: string; // ์ „์ฒด ์ฝ”๋“œ ๋˜๋Š” diff + line_start?: number; + line_end?: number; +} +``` + +### ์ƒํƒœ ์ฝ”๋“œ ์ •์˜ + +| ์ƒํƒœ | ์˜๋ฏธ | ํ›„์† ์กฐ์น˜ | +|------|------|-----------| +| `success` | ์™„์ „ํžˆ ์™„๋ฃŒ | ๊ฒฐ๊ณผ ์‚ฌ์šฉ ๊ฐ€๋Šฅ | +| `partial` | ๋ถ€๋ถ„ ์™„๋ฃŒ | ์ถ”๊ฐ€ ์ž‘์—… ํ•„์š” | +| `failed` | ์‹คํŒจ | ์—๋Ÿฌ ํ™•์ธ ํ›„ ์žฌ์‹œ๋„ | +| `need_clarification` | ๋ช…ํ™•ํ™” ํ•„์š” | ์งˆ๋ฌธ์— ๋‹ต๋ณ€ ํ›„ ์žฌ์š”์ฒญ | + +### ํ™•์‹ ๋„ ์ •์˜ + +| ํ™•์‹ ๋„ | ์˜๋ฏธ | ๊ถŒ์žฅ ์กฐ์น˜ | +|--------|------|-----------| +| `high` | ํ™•์‹คํ•จ | ๋ฐ”๋กœ ์ ์šฉ ๊ฐ€๋Šฅ | +| `medium` | ๋Œ€์ฒด๋กœ ๋งž์Œ | ๊ฒ€ํ†  ํ›„ ์ ์šฉ | +| `low` | ์ถ”์ธก์ž„ | ๋ฐ˜๋“œ์‹œ ๊ฒ€์ฆ ํ•„์š” | + +--- + +## ์›Œํฌํ”Œ๋กœ์šฐ + +### Phase 1: ์ •๋ณด ์ˆ˜์ง‘ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Phase 1: ์ •๋ณด ์ˆ˜์ง‘ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ 1. User โ†’ Agent A: "์ฃผ๋ฌธ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ๋งŒ๋“ค์–ด์ค˜" โ”‚ +โ”‚ โ”‚ +โ”‚ 2. Agent A ๋ถ„์„: โ”‚ +โ”‚ - ๊ธฐ๋Šฅ ๋ฒ”์œ„ ํŒŒ์•… โ”‚ +โ”‚ - ํ•„์š”ํ•œ ์ •๋ณด ์‹๋ณ„ โ”‚ +โ”‚ - ์ •๋ณด ์ˆ˜์ง‘ ์š”์ฒญ ์ƒ์„ฑ โ”‚ +โ”‚ โ”‚ +โ”‚ 3. Agent A โ†’ B, C, D (๋ณ‘๋ ฌ): โ”‚ +โ”‚ - B์—๊ฒŒ: "ํ˜„์žฌ order ๊ด€๋ จ API ๊ตฌ์กฐ ๋ถ„์„ํ•ด์ค˜" โ”‚ +โ”‚ - C์—๊ฒŒ: "orders ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ์•Œ๋ ค์ค˜" โ”‚ +โ”‚ - D์—๊ฒŒ: "์ฃผ๋ฌธ ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ ํ˜„ํ™ฉ ์•Œ๋ ค์ค˜" โ”‚ +โ”‚ โ”‚ +โ”‚ 4. B, C, D โ†’ Agent A (์‘๋‹ต): โ”‚ +โ”‚ - B: API ํ˜„ํ™ฉ ๋ณด๊ณ  โ”‚ +โ”‚ - C: ์Šคํ‚ค๋งˆ ์ •๋ณด ๋ณด๊ณ  โ”‚ +โ”‚ - D: ์ปดํฌ๋„ŒํŠธ ํ˜„ํ™ฉ ๋ณด๊ณ  โ”‚ +โ”‚ โ”‚ +โ”‚ 5. Agent A: ์ •๋ณด ์ทจํ•ฉ ๋ฐ ๊ณ„ํš ์ˆ˜๋ฆฝ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Phase 2: ์ž‘์—… ๋ถ„๋ฐฐ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Phase 2: ์ž‘์—… ๋ถ„๋ฐฐ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ 1. Agent A: ์ข…ํ•ฉ ๊ณ„ํš ์ˆ˜๋ฆฝ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋ถ„์„ ๊ฒฐ๊ณผ: โ”‚ โ”‚ +โ”‚ โ”‚ - API์— pagination ์ถ”๊ฐ€ ํ•„์š” โ”‚ โ”‚ +โ”‚ โ”‚ - DB๋Š” ํ˜„์žฌ ๊ตฌ์กฐ ์œ ์ง€ โ”‚ โ”‚ +โ”‚ โ”‚ - ํ”„๋ก ํŠธ ๋ฌดํ•œ์Šคํฌ๋กค โ†’ ํŽ˜์ด์ง€๋„ค์ด์…˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ ์ž‘์—… ์ˆœ์„œ: โ”‚ โ”‚ +โ”‚ โ”‚ 1. C: ํŽ˜์ด์ง• ์ฟผ๋ฆฌ ์ค€๋น„ โ”‚ โ”‚ +โ”‚ โ”‚ 2. B: API ์ˆ˜์ • (C ๊ฒฐ๊ณผ ์˜์กด) โ”‚ โ”‚ +โ”‚ โ”‚ 3. D: ํ”„๋ก ํŠธ ์ˆ˜์ • (B ๊ฒฐ๊ณผ ์˜์กด) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ 2. Agent A โ†’ B, C, D: ์ž‘์—… ํ• ๋‹น โ”‚ +โ”‚ - C์—๊ฒŒ: "cursor ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง• ์ฟผ๋ฆฌ ์ž‘์„ฑ" โ”‚ +โ”‚ - B์—๊ฒŒ: "GET /api/orders์— pagination ์ถ”๊ฐ€" (C ๋Œ€๊ธฐ) โ”‚ +โ”‚ - D์—๊ฒŒ: "Pagination ์ปดํฌ๋„ŒํŠธ ์ ์šฉ" (B ๋Œ€๊ธฐ) โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Phase 3: ์‹คํ–‰ ๋ฐ ํ†ตํ•ฉ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Phase 3: ์‹คํ–‰ ๋ฐ ํ†ตํ•ฉ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ 1. ์ˆœ์ฐจ/๋ณ‘๋ ฌ ์‹คํ–‰: โ”‚ +โ”‚ - C: ์ฟผ๋ฆฌ ์ž‘์„ฑ โ†’ ์™„๋ฃŒ ๋ณด๊ณ  โ”‚ +โ”‚ - B: API ์ˆ˜์ • (C ์™„๋ฃŒ ํ›„) โ†’ ์™„๋ฃŒ ๋ณด๊ณ  โ”‚ +โ”‚ - D: ํ”„๋ก ํŠธ ์ˆ˜์ • (B ์™„๋ฃŒ ํ›„) โ†’ ์™„๋ฃŒ ๋ณด๊ณ  โ”‚ +โ”‚ โ”‚ +โ”‚ 2. Agent A: ๊ฒฐ๊ณผ ๊ฒ€์ฆ โ”‚ +โ”‚ - ์ผ๊ด€์„ฑ ์ฒดํฌ โ”‚ +โ”‚ - ๋ˆ„๋ฝ ํ™•์ธ โ”‚ +โ”‚ - ์ถฉ๋Œ ํ•ด๊ฒฐ โ”‚ +โ”‚ โ”‚ +โ”‚ 3. Agent A โ†’ User: ์ตœ์ข… ๋ณด๊ณ  โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์™„๋ฃŒ๋œ ์ž‘์—…: โ”‚ โ”‚ +โ”‚ โ”‚ โœ… orders.xml - ํŽ˜์ด์ง• ์ฟผ๋ฆฌ ์ถ”๊ฐ€ โ”‚ โ”‚ +โ”‚ โ”‚ โœ… OrderController.ts - pagination ์ ์šฉ โ”‚ โ”‚ +โ”‚ โ”‚ โœ… OrderListPage.tsx - UI ์ˆ˜์ • โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ ํ…Œ์ŠคํŠธ ํ•„์š”: โ”‚ โ”‚ +โ”‚ โ”‚ - GET /api/orders?page=1&limit=10 โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## ํ”„๋กฌํ”„ํŠธ ํ…œํ”Œ๋ฆฟ + +### Agent A (PM) ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ + +```markdown +# ์—ญํ•  +๋„ˆ๋Š” PM(Project Manager) ์—์ด์ „ํŠธ์•ผ. +์‚ฌ์šฉ์ž ์š”์ฒญ์„ ๋ถ„์„ํ•˜๊ณ , ์ „๋ฌธ๊ฐ€ ์—์ด์ „ํŠธ๋“ค(Backend, DB, Frontend)์—๊ฒŒ +์ž‘์—…์„ ๋ถ„๋ฐฐํ•˜๊ณ , ๊ฒฐ๊ณผ๋ฅผ ํ†ตํ•ฉํ•ด์„œ ์ตœ์ข… ๊ฒฐ๊ณผ๋ฌผ์„ ๋งŒ๋“ค์–ด. + +# ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋„๊ตฌ +- ask_backend_agent: ๋ฐฑ์—”๋“œ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ์งˆ๋ฌธ/์ž‘์—… ์š”์ฒญ +- ask_db_agent: DB ์ „๋ฌธ๊ฐ€์—๊ฒŒ ์งˆ๋ฌธ/์ž‘์—… ์š”์ฒญ +- ask_frontend_agent: ํ”„๋ก ํŠธ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ์งˆ๋ฌธ/์ž‘์—… ์š”์ฒญ +- parallel_ask: ์—ฌ๋Ÿฌ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ๋™์‹œ์— ์š”์ฒญ + +# ์ž‘์—… ํ”„๋กœ์„ธ์Šค + +## Phase 1: ๋ถ„์„ +1. ์‚ฌ์šฉ์ž ์š”์ฒญ ๋ถ„์„ +2. ํ•„์š”ํ•œ ์ •๋ณด ์‹๋ณ„ +3. ์ •๋ณด ์ˆ˜์ง‘ ์š”์ฒญ (parallel_ask ํ™œ์šฉ) + +## Phase 2: ๊ณ„ํš +1. ์ˆ˜์ง‘๋œ ์ •๋ณด ๋ถ„์„ +2. ์ž‘์—… ๋ถ„ํ•ด ๋ฐ ์˜์กด์„ฑ ํŒŒ์•… +3. ์šฐ์„ ์ˆœ์œ„ ๊ฒฐ์ • +4. ์ž‘์—… ๋ถ„๋ฐฐ ๊ณ„ํš ์ˆ˜๋ฆฝ + +## Phase 3: ์‹คํ–‰ +1. ์˜์กด์„ฑ ์ˆœ์„œ๋Œ€๋กœ ์ž‘์—… ์š”์ฒญ +2. ๊ฒฐ๊ณผ ๊ฒ€์ฆ +3. ํ•„์š”์‹œ ์žฌ์š”์ฒญ + +## Phase 4: ํ†ตํ•ฉ +1. ๋ชจ๋“  ๊ฒฐ๊ณผ ์ทจํ•ฉ +2. ์ผ๊ด€์„ฑ ๊ฒ€์ฆ +3. ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด๊ณ  + +# ์ž‘์—… ๋ถ„๋ฐฐ ๊ธฐ์ค€ +- Backend Agent: API, ์„œ๋น„์Šค ๋กœ์ง, ๋ผ์šฐํŒ… (backend-node/) +- DB Agent: ์Šคํ‚ค๋งˆ, ์ฟผ๋ฆฌ, ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (mapper/, db/) +- Frontend Agent: ์ปดํฌ๋„ŒํŠธ, ํŽ˜์ด์ง€, ์Šคํƒ€์ผ (frontend/) + +# ํŒ๋‹จ ๊ธฐ์ค€ +- ๋ถˆํ™•์‹คํ•˜๋ฉด ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฌผ์–ด๋ด +- ์—์ด์ „ํŠธ ๊ฒฐ๊ณผ๊ฐ€ ์ด์ƒํ•˜๋ฉด ์žฌ์š”์ฒญ +- ์˜ํ–ฅ ๋ฒ”์œ„ ํฌ๋ฉด ์‚ฌ์šฉ์ž ํ™•์ธ +- ์ถฉ๋Œ ์‹œ ๋” ์•ˆ์ „ํ•œ ๋ฐฉํ–ฅ ์„ ํƒ + +# ์‘๋‹ต ํ˜•์‹ +์ž‘์—… ๋ถ„๋ฐฐ ์‹œ: +```json +{ + "phase": "info_gathering | work_distribution | integration", + "reasoning": "์™œ ์ด๋ ‡๊ฒŒ ๋ถ„๋ฐฐํ•˜๋Š”์ง€", + "tasks": [ + { + "agent": "backend | db | frontend", + "priority": 1, + "task": "๊ตฌ์ฒด์ ์ธ ์ž‘์—… ๋‚ด์šฉ", + "depends_on": [], + "expected_output": "๊ธฐ๋Œ€ ๊ฒฐ๊ณผ" + } + ] +} +``` + +์ตœ์ข… ๋ณด๊ณ  ์‹œ: +```json +{ + "summary": "ํ•œ ์ค„ ์š”์•ฝ", + "completed_tasks": ["์™„๋ฃŒ๋œ ์ž‘์—…๋“ค"], + "files_changed": ["๋ณ€๊ฒฝ๋œ ํŒŒ์ผ๋“ค"], + "next_steps": ["๋‹ค์Œ ๋‹จ๊ณ„ (์žˆ๋‹ค๋ฉด)"], + "test_instructions": ["ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•"] +} +``` +``` + +### Agent B (Backend) ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ + +```markdown +# ์—ญํ•  +๋„ˆ๋Š” Backend ์ „๋ฌธ๊ฐ€ ์—์ด์ „ํŠธ์•ผ. +backend-node/ ํด๋”์˜ API, ์„œ๋น„์Šค, ๋ผ์šฐํŒ…์„ ๋‹ด๋‹นํ•ด. + +# ๋‹ด๋‹น ์˜์—ญ (์ด๊ฒƒ๋งŒ!) +- backend-node/src/controllers/ +- backend-node/src/services/ +- backend-node/src/routes/ +- backend-node/src/middleware/ +- backend-node/src/utils/ + +# ๋‹ด๋‹น ์•„๋‹Œ ๊ฒƒ (์ ˆ๋Œ€ ๊ฑด๋“ค์ง€ ๋งˆ) +- frontend/ โ†’ Frontend Agent ๋‹ด๋‹น +- src/com/pms/mapper/ โ†’ DB Agent ๋‹ด๋‹น +- SQL ์ฟผ๋ฆฌ ์ง์ ‘ ์ž‘์„ฑ โ†’ DB Agent์—๊ฒŒ ์š”์ฒญ + +# ์ฝ”๋“œ ์ž‘์„ฑ ๊ทœ์น™ +1. TypeScript ์‚ฌ์šฉ +2. ์—๋Ÿฌ ํ•ธ๋“ค๋ง ํ•„์ˆ˜ +3. ์ฃผ์„์€ ํ•œ๊ธ€๋กœ +4. ๊ธฐ์กด ์ฝ”๋“œ ์Šคํƒ€์ผ ๋”ฐ๋ฅด๊ธฐ +5. ... ์ƒ๋žต ์—†์ด ์™„์ „ํ•œ ์ฝ”๋“œ + +# ์‘๋‹ต ํ˜•์‹ +```json +{ + "status": "success | partial | failed | need_clarification", + "confidence": "high | medium | low", + "result": { + "summary": "ํ•œ ์ค„ ์š”์•ฝ", + "details": "์ƒ์„ธ ์„ค๋ช…", + "files_affected": ["ํŒŒ์ผ ๊ฒฝ๋กœ๋“ค"], + "code_changes": [ + { + "file": "๊ฒฝ๋กœ", + "action": "create | modify | delete", + "content": "์ „์ฒด ์ฝ”๋“œ" + } + ] + }, + "needs_from_others": [ + {"agent": "db", "request": "ํ•„์š”ํ•œ ๊ฒƒ"} + ], + "side_effects": ["์˜ํ–ฅ๋ฐ›๋Š” ๊ฒƒ๋“ค"], + "questions": ["๋ช…ํ™•ํ•˜์ง€ ์•Š์€ ๊ฒƒ๋“ค"] +} +``` + +# ํ˜‘์—… ๊ทœ์น™ +1. ๋‚ด ์˜์—ญ ์•„๋‹ˆ๋ฉด ์ฆ‰์‹œ ๋ณด๊ณ  (scope_violation) +2. ํ™•์‹คํ•˜์ง€ ์•Š์œผ๋ฉด confidence: "low" +3. ๋‹ค๋ฅธ ์—์ด์ „ํŠธ ํ•„์š”ํ•˜๋ฉด needs_from_others์— ๋ช…์‹œ +4. ๋ถ€์ž‘์šฉ ์žˆ์œผ๋ฉด ๋ฐ˜๋“œ์‹œ ๋ณด๊ณ  +``` + +### Agent C (Database) ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ + +```markdown +# ์—ญํ•  +๋„ˆ๋Š” Database ์ „๋ฌธ๊ฐ€ ์—์ด์ „ํŠธ์•ผ. +DB ์Šคํ‚ค๋งˆ, ์ฟผ๋ฆฌ, ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ๋‹ด๋‹นํ•ด. + +# ๋‹ด๋‹น ์˜์—ญ (์ด๊ฒƒ๋งŒ!) +- src/com/pms/mapper/ (MyBatis XML) +- db/ (์Šคํ‚ค๋งˆ, ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜) +- backend-node/src/database/ + +# ๋‹ด๋‹น ์•„๋‹Œ ๊ฒƒ (์ ˆ๋Œ€ ๊ฑด๋“ค์ง€ ๋งˆ) +- API ๋กœ์ง โ†’ Backend Agent ๋‹ด๋‹น +- ํ”„๋ก ํŠธ์—”๋“œ โ†’ Frontend Agent ๋‹ด๋‹น +- ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํŒ๋‹จ โ†’ PM์—๊ฒŒ ํ™•์ธ + +# ์ฝ”๋“œ ์ž‘์„ฑ ๊ทœ์น™ +1. PostgreSQL ๋ฌธ๋ฒ• ์‚ฌ์šฉ +2. ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ (#{}) ํ•„์ˆ˜ - SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ +3. ์ธ๋ฑ์Šค ๊ณ ๋ ค +4. ์„ฑ๋Šฅ ์ตœ์ ํ™” (EXPLAIN ๊ฒฐ๊ณผ ๊ณ ๋ ค) + +# MyBatis ๋งคํผ ๊ทœ์น™ +```xml + +WHERE id = #{id} + + + + AND name LIKE '%' || #{name} || '%' + + + +LIMIT #{limit} OFFSET #{offset} +``` + +# ์‘๋‹ต ํ˜•์‹ +```json +{ + "status": "success | partial | failed | need_clarification", + "confidence": "high | medium | low", + "result": { + "summary": "ํ•œ ์ค„ ์š”์•ฝ", + "details": "์ƒ์„ธ ์„ค๋ช…", + "schema_info": { + "tables": ["๊ด€๋ จ ํ…Œ์ด๋ธ”"], + "columns": ["์ฃผ์š” ์ปฌ๋Ÿผ"], + "indexes": ["์ธ๋ฑ์Šค"] + }, + "code_changes": [ + { + "file": "๊ฒฝ๋กœ", + "action": "create | modify", + "content": "์ฟผ๋ฆฌ/์Šคํ‚ค๋งˆ" + } + ] + }, + "performance_notes": ["์„ฑ๋Šฅ ๊ด€๋ จ ์ฐธ๊ณ ์‚ฌํ•ญ"], + "questions": ["๋ช…ํ™•ํ•˜์ง€ ์•Š์€ ๊ฒƒ๋“ค"] +} +``` +``` + +### Agent D (Frontend) ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ + +```markdown +# ์—ญํ•  +๋„ˆ๋Š” Frontend ์ „๋ฌธ๊ฐ€ ์—์ด์ „ํŠธ์•ผ. +React/Next.js ๊ธฐ๋ฐ˜ UI ๊ตฌํ˜„์„ ๋‹ด๋‹นํ•ด. + +# ๋‹ด๋‹น ์˜์—ญ (์ด๊ฒƒ๋งŒ!) +- frontend/components/ +- frontend/pages/ (๋˜๋Š” app/) +- frontend/lib/ +- frontend/hooks/ +- frontend/styles/ + +# ๋‹ด๋‹น ์•„๋‹Œ ๊ฒƒ (์ ˆ๋Œ€ ๊ฑด๋“ค์ง€ ๋งˆ) +- backend-node/ โ†’ Backend Agent ๋‹ด๋‹น +- DB ๊ด€๋ จ โ†’ DB Agent ๋‹ด๋‹น +- API ์ŠคํŽ™ ๊ฒฐ์ • โ†’ PM/Backend์™€ ํ˜‘์˜ + +# ์ฝ”๋“œ ์ž‘์„ฑ ๊ทœ์น™ +1. TypeScript ์‚ฌ์šฉ +2. React ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ +3. ์ปค์Šคํ…€ ํ›… ํ™œ์šฉ +4. ์ฃผ์„์€ ํ•œ๊ธ€๋กœ +5. Tailwind CSS ๋˜๋Š” ๊ธฐ์กด ์Šคํƒ€์ผ ์‹œ์Šคํ…œ ๋”ฐ๋ฅด๊ธฐ + +# API ํ˜ธ์ถœ ๊ทœ์น™ +- ์ ˆ๋Œ€ fetch ์ง์ ‘ ์‚ฌ์šฉ ๊ธˆ์ง€ +- lib/api/ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ +- ์—๋Ÿฌ ํ•ธ๋“ค๋ง ํ•„์ˆ˜ + +# ์‘๋‹ต ํ˜•์‹ +```json +{ + "status": "success | partial | failed | need_clarification", + "confidence": "high | medium | low", + "result": { + "summary": "ํ•œ ์ค„ ์š”์•ฝ", + "details": "์ƒ์„ธ ์„ค๋ช…", + "components_affected": ["์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก"], + "code_changes": [ + { + "file": "๊ฒฝ๋กœ", + "action": "create | modify", + "content": "์ „์ฒด ์ฝ”๋“œ" + } + ] + }, + "needs_from_others": [ + {"agent": "backend", "request": "ํ•„์š”ํ•œ API"} + ], + "ui_notes": ["UX ๊ด€๋ จ ์ฐธ๊ณ ์‚ฌํ•ญ"], + "questions": ["๋ช…ํ™•ํ•˜์ง€ ์•Š์€ ๊ฒƒ๋“ค"] +} +``` +``` + +--- + +## MCP ์„œ๋ฒ„ ๊ตฌํ˜„ + +### ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ + +``` +mcp-agent-orchestrator/ +โ”œโ”€โ”€ package.json +โ”œโ”€โ”€ tsconfig.json +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ index.ts # ๋ฉ”์ธ ์„œ๋ฒ„ +โ”‚ โ”œโ”€โ”€ agents/ +โ”‚ โ”‚ โ”œโ”€โ”€ types.ts # ํƒ€์ž… ์ •์˜ +โ”‚ โ”‚ โ”œโ”€โ”€ pm.ts # PM ์—์ด์ „ํŠธ ํ”„๋กฌํ”„ํŠธ +โ”‚ โ”‚ โ”œโ”€โ”€ backend.ts # Backend ์—์ด์ „ํŠธ ํ”„๋กฌํ”„ํŠธ +โ”‚ โ”‚ โ”œโ”€โ”€ database.ts # DB ์—์ด์ „ํŠธ ํ”„๋กฌํ”„ํŠธ +โ”‚ โ”‚ โ””โ”€โ”€ frontend.ts # Frontend ์—์ด์ „ํŠธ ํ”„๋กฌํ”„ํŠธ +โ”‚ โ””โ”€โ”€ utils/ +โ”‚ โ””โ”€โ”€ logger.ts # ๋กœ๊น… +โ””โ”€โ”€ build/ + โ””โ”€โ”€ index.js # ์ปดํŒŒ์ผ๋œ ํŒŒ์ผ +``` + +### ํ•ต์‹ฌ ์ฝ”๋“œ + +```typescript +// src/index.ts +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import Anthropic from "@anthropic-ai/sdk"; +import { PM_PROMPT, BACKEND_PROMPT, DB_PROMPT, FRONTEND_PROMPT } from "./agents"; + +const server = new Server({ + name: "agent-orchestrator", + version: "1.0.0", +}); + +const anthropic = new Anthropic(); + +// ์—์ด์ „ํŠธ๋ณ„ ์„ค์ • +const AGENT_CONFIG = { + pm: { model: "claude-opus-4-5-20250214", prompt: PM_PROMPT }, + backend: { model: "claude-sonnet-4-20250514", prompt: BACKEND_PROMPT }, + db: { model: "claude-sonnet-4-20250514", prompt: DB_PROMPT }, + frontend: { model: "claude-sonnet-4-20250514", prompt: FRONTEND_PROMPT }, +}; + +// ๋„๊ตฌ ๋ชฉ๋ก +server.setRequestHandler("tools/list", async () => ({ + tools: [ + { + name: "ask_backend_agent", + description: "๋ฐฑ์—”๋“œ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ์งˆ๋ฌธํ•˜๊ฑฐ๋‚˜ ์ž‘์—… ์š”์ฒญ", + inputSchema: { + type: "object", + properties: { + task: { type: "string", description: "์ž‘์—… ๋‚ด์šฉ" }, + context: { type: "string", description: "๋ฐฐ๊ฒฝ ์ •๋ณด (์„ ํƒ)" }, + }, + required: ["task"], + }, + }, + { + name: "ask_db_agent", + description: "DB ์ „๋ฌธ๊ฐ€์—๊ฒŒ ์งˆ๋ฌธํ•˜๊ฑฐ๋‚˜ ์ž‘์—… ์š”์ฒญ", + inputSchema: { + type: "object", + properties: { + task: { type: "string", description: "์ž‘์—… ๋‚ด์šฉ" }, + context: { type: "string", description: "๋ฐฐ๊ฒฝ ์ •๋ณด (์„ ํƒ)" }, + }, + required: ["task"], + }, + }, + { + name: "ask_frontend_agent", + description: "ํ”„๋ก ํŠธ์—”๋“œ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ์งˆ๋ฌธํ•˜๊ฑฐ๋‚˜ ์ž‘์—… ์š”์ฒญ", + inputSchema: { + type: "object", + properties: { + task: { type: "string", description: "์ž‘์—… ๋‚ด์šฉ" }, + context: { type: "string", description: "๋ฐฐ๊ฒฝ ์ •๋ณด (์„ ํƒ)" }, + }, + required: ["task"], + }, + }, + { + name: "parallel_ask", + description: "์—ฌ๋Ÿฌ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ๋™์‹œ์— ์งˆ๋ฌธ (๋ณ‘๋ ฌ ์‹คํ–‰)", + inputSchema: { + type: "object", + properties: { + requests: { + type: "array", + items: { + type: "object", + properties: { + agent: { + type: "string", + enum: ["backend", "db", "frontend"] + }, + task: { type: "string" }, + context: { type: "string" }, + }, + required: ["agent", "task"], + }, + }, + }, + required: ["requests"], + }, + }, + ], +})); + +// ๋„๊ตฌ ์‹คํ–‰ +server.setRequestHandler("tools/call", async (request) => { + const { name, arguments: args } = request.params; + + const callAgent = async (agentType: string, task: string, context?: string) => { + const config = AGENT_CONFIG[agentType]; + const response = await anthropic.messages.create({ + model: config.model, + max_tokens: 8192, + system: config.prompt, + messages: [ + { + role: "user", + content: context ? `${task}\n\n๋ฐฐ๊ฒฝ ์ •๋ณด:\n${context}` : task, + }, + ], + }); + return response.content[0].text; + }; + + switch (name) { + case "ask_backend_agent": + return { + content: [ + { type: "text", text: await callAgent("backend", args.task, args.context) }, + ], + }; + + case "ask_db_agent": + return { + content: [ + { type: "text", text: await callAgent("db", args.task, args.context) }, + ], + }; + + case "ask_frontend_agent": + return { + content: [ + { type: "text", text: await callAgent("frontend", args.task, args.context) }, + ], + }; + + case "parallel_ask": + const results = await Promise.all( + args.requests.map(async (req) => ({ + agent: req.agent, + result: await callAgent(req.agent, req.task, req.context), + })) + ); + return { + content: [ + { type: "text", text: JSON.stringify(results, null, 2) }, + ], + }; + + default: + throw new Error(`Unknown tool: ${name}`); + } +}); + +// ์„œ๋ฒ„ ์‹œ์ž‘ +const transport = new StdioServerTransport(); +await server.connect(transport); +``` + +### Cursor ์„ค์ • + +```json +// .cursor/mcp.json +{ + "mcpServers": { + "agent-orchestrator": { + "command": "node", + "args": ["C:/Users/defaultuser0/mcp-agent-orchestrator/build/index.js"], + "env": { + "ANTHROPIC_API_KEY": "your-api-key-here" + } + } + } +} +``` + +--- + +## ๋น„์šฉ ๋ถ„์„ + +### ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰ ๋น„๊ต + +| ์‹œ๋‚˜๋ฆฌ์˜ค | ๋‹จ์ผ ์—์ด์ „ํŠธ | ๋ฉ€ํ‹ฐ ์—์ด์ „ํŠธ | ์ ˆ๊ฐ | +|----------|--------------|--------------|------| +| ๊ธฐ๋Šฅ 1๊ฐœ ์ถ”๊ฐ€ | 100,000 ํ† ํฐ | 60,000 ํ† ํฐ | 40% | +| ์‹œ์Šคํ…œ ๋ฆฌํŒฉํ† ๋ง | 300,000 ํ† ํฐ | 150,000 ํ† ํฐ | 50% | +| ์ƒˆ ๋ชจ๋“ˆ ๊ฐœ๋ฐœ | 500,000 ํ† ํฐ | 200,000 ํ† ํฐ | 60% | + +### ๋น„์šฉ ๊ณ„์‚ฐ (์˜ˆ์‹œ) + +``` +๋‹จ์ผ ์—์ด์ „ํŠธ (์ „๋ถ€ Opus): +- 300,000 ํ† ํฐ ร— $15/M = $4.50 + +๋ฉ€ํ‹ฐ ์—์ด์ „ํŠธ (Opus PM + Sonnet Workers): +- PM (Opus): 50,000 ํ† ํฐ ร— $15/M = $0.75 +- Workers (Sonnet): 100,000 ํ† ํฐ ร— $3/M = $0.30 +- ์ด: $1.05 + +์ ˆ๊ฐ: $4.50 - $1.05 = $3.45 (76% ์ ˆ๊ฐ!) +``` + +### ROI ๋ถ„์„ + +``` +์ดˆ๊ธฐ ํˆฌ์ž: +- MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ: 4-6์‹œ๊ฐ„ +- ํ”„๋กฌํ”„ํŠธ ํŠœ๋‹: 2-4์‹œ๊ฐ„ +- ํ…Œ์ŠคํŠธ: 2์‹œ๊ฐ„ +- ์ด: 8-12์‹œ๊ฐ„ + +ํšŒ์ˆ˜: +- ๋Œ€๊ทœ๋ชจ ์ž‘์—…๋‹น $3-5 ์ ˆ๊ฐ +- ์žฌ์ž‘์—… ์‹œ๊ฐ„ 70% ๊ฐ์†Œ +- ํ’ˆ์งˆ 30% ํ–ฅ์ƒ + +์†์ต๋ถ„๊ธฐ์ : ๋Œ€๊ทœ๋ชจ ์ž‘์—… 3-5ํšŒ +``` + +--- + +## ํ•œ๊ณ„์  ๋ฐ ํ•ด๊ฒฐ๋ฐฉ์•ˆ + +### ํ˜„์žฌ ํ•œ๊ณ„ + +| ํ•œ๊ณ„ | ์„ค๋ช… | ํ•ด๊ฒฐ๋ฐฉ์•ˆ | +|------|------|----------| +| ์™„์ „ ์ž๋™ํ™” ๋ถˆ๊ฐ€ | Cursor ์—์ด์ „ํŠธ ๊ฐ„ ์ง์ ‘ ํ†ต์‹  ์—†์Œ | MCP ์„œ๋ฒ„๋กœ ์šฐํšŒ | +| ํŒŒ์ผ ์ฝ๊ธฐ ์ œํ•œ | ๊ฐ ์—์ด์ „ํŠธ๊ฐ€ ๋ชจ๋“  ํŒŒ์ผ ์ ‘๊ทผ ์–ด๋ ค์›€ | ์ปจํ…์ŠคํŠธ์— ํ•„์š”ํ•œ ์ •๋ณด ์ „๋‹ฌ | +| ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™” | ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ฆ‰์‹œ ๋ฐ˜์˜ ์–ด๋ ค์›€ | ๋ช…์‹œ์  ๊ฐฑ์‹  ์š”์ฒญ | +| ์—๋Ÿฌ ๋ณต๊ตฌ | ์ž๋™ ๋กค๋ฐฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ์—†์Œ | ์ˆ˜๋™ ๋ณต๊ตฌ ๋˜๋Š” git ํ™œ์šฉ | + +### ํ–ฅํ›„ ๊ฐœ์„  ๋ฐฉํ–ฅ + +1. **ํŒŒ์ผ ์‹œ์Šคํ…œ ์—ฐ๋™** + - MCP ์„œ๋ฒ„์— ํŒŒ์ผ ์ฝ๊ธฐ/์“ฐ๊ธฐ ๋„๊ตฌ ์ถ”๊ฐ€ + - ์—์ด์ „ํŠธ๊ฐ€ ์ง์ ‘ ์ฝ”๋“œ ํ™•์ธ ๊ฐ€๋Šฅ + +2. **๊ฒฐ๊ณผ ์ž๋™ ์ ์šฉ** + - ์ฝ”๋“œ ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ž๋™ ์ ์šฉ + - git ์ปค๋ฐ‹ ์ž๋™ํ™” + +3. **ํ”ผ๋“œ๋ฐฑ ๋ฃจํ”„** + - ํ…Œ์ŠคํŠธ ์ž๋™ ์‹คํ–‰ + - ์‹คํŒจ ์‹œ ์ž๋™ ์žฌ์‹œ๋„ + +4. **ํžˆ์Šคํ† ๋ฆฌ ๊ด€๋ฆฌ** + - ๋Œ€ํ™” ์ด๋ ฅ ์ €์žฅ + - ์ปจํ…์ŠคํŠธ ์บ์‹ฑ + +--- + +## ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### ๊ตฌํ˜„ ์ „ ์ค€๋น„ + +- [ ] Node.js 18+ ์„ค์น˜ +- [ ] Anthropic API ํ‚ค ๋ฐœ๊ธ‰ +- [ ] ํ”„๋กœ์ ํŠธ ํด๋” ์ƒ์„ฑ + +### MCP ์„œ๋ฒ„ ๊ตฌํ˜„ + +- [ ] package.json ์„ค์ • +- [ ] TypeScript ์„ค์ • +- [ ] ๊ธฐ๋ณธ ์„œ๋ฒ„ ๊ตฌ์กฐ +- [ ] ๋„๊ตฌ ์ •์˜ (4๊ฐœ) +- [ ] ์—์ด์ „ํŠธ ํ”„๋กฌํ”„ํŠธ ์ž‘์„ฑ +- [ ] ๋นŒ๋“œ ๋ฐ ํ…Œ์ŠคํŠธ + +### Cursor ์—ฐ๋™ + +- [ ] mcp.json ์„ค์ • +- [ ] Cursor ์žฌ์‹œ์ž‘ +- [ ] ๋„๊ตฌ ํ˜ธ์ถœ ํ…Œ์ŠคํŠธ +- [ ] ์‹ค์ œ ์ž‘์—… ํ…Œ์ŠคํŠธ + +### ํŠœ๋‹ + +- [ ] ํ”„๋กฌํ”„ํŠธ ๊ฐœ์„  +- [ ] ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๊ฐ•ํ™” +- [ ] ๋กœ๊น… ์ถ”๊ฐ€ +- [ ] ์„ฑ๋Šฅ ์ตœ์ ํ™” + +--- + +## ์ฐธ๊ณ  ์ž๋ฃŒ + +- [MCP SDK ๋ฌธ์„œ](https://modelcontextprotocol.io/) +- [Anthropic API ๋ฌธ์„œ](https://docs.anthropic.com/) +- [CrewAI](https://github.com/joaomdmoura/crewAI) - ๋ฉ€ํ‹ฐ์—์ด์ „ํŠธ ํ”„๋ ˆ์ž„์›Œํฌ ์ฐธ๊ณ  +- [AutoGen](https://github.com/microsoft/autogen) - Microsoft ๋ฉ€ํ‹ฐ์—์ด์ „ํŠธ ์ฐธ๊ณ  + +--- + +*์ž‘์„ฑ์ผ: 2026-02-05* +*๋ฒ„์ „: 1.0* diff --git a/docs/screen-implementation-guide/00_analysis/full-screen-analysis.md b/docs/screen-implementation-guide/00_analysis/full-screen-analysis.md new file mode 100644 index 00000000..9b4a9908 --- /dev/null +++ b/docs/screen-implementation-guide/00_analysis/full-screen-analysis.md @@ -0,0 +1,331 @@ +# ํ™”๋ฉด ์ „์ฒด ๋ถ„์„ ๋ณด๊ณ ์„œ + +> **๋ถ„์„ ๋Œ€์ƒ**: `/Users/kimjuseok/Downloads/ํ™”๋ฉด๊ฐœ๋ฐœ 8` ํด๋” ๋‚ด ํ•ต์‹ฌ ์—…๋ฌด ํ™”๋ฉด +> **๋ถ„์„ ๊ธฐ์ค€**: ๋ฉ”๋‰ด๋ณ„ ๋ถ„๋ฅ˜, 3๊ฐœ ์ด์ƒ ์žฌํ™œ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ ์‹๋ณ„ +> **๋ถ„์„ ์ผ์ž**: 2026-01-30 + +--- + +## 1. ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ V2 ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก + +> **์ค‘์š”**: v2- ์ ‘๋‘์‚ฌ๊ฐ€ ๋ถ™์€ ์ปดํฌ๋„ŒํŠธ๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + +### ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| `v2-input` | V2 ์ž…๋ ฅ | ํ…์ŠคํŠธ, ์ˆซ์ž, ๋น„๋ฐ€๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ ๋“ฑ ์ž…๋ ฅ | +| `v2-select` | V2 ์„ ํƒ | ๋“œ๋กญ๋‹ค์šด, ์ฝค๋ณด๋ฐ•์Šค, ๋ผ๋””์˜ค, ์ฒดํฌ๋ฐ•์Šค | +| `v2-date` | V2 ๋‚ ์งœ | ๋‚ ์งœ, ์‹œ๊ฐ„, ๋‚ ์งœ๋ฒ”์œ„ ์ž…๋ ฅ | + +### ํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| `v2-text-display` | ํ…์ŠคํŠธ ํ‘œ์‹œ | ๋ผ๋ฒจ, ํ…์ŠคํŠธ ํ‘œ์‹œ | +| `v2-card-display` | ์นด๋“œ ๋””์Šคํ”Œ๋ ˆ์ด | ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ๋ฅผ ์นด๋“œ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ | +| `v2-aggregation-widget` | ์ง‘๊ณ„ ์œ„์ ฏ | ํ•ฉ๊ณ„, ํ‰๊ท , ๊ฐœ์ˆ˜ ๋“ฑ ์ง‘๊ณ„ ํ‘œ์‹œ | + +### ํ…Œ์ด๋ธ”/๋ฐ์ดํ„ฐ ์ปดํฌ๋„ŒํŠธ +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| `v2-table-list` | ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ํ‘œ์‹œ, ํŽ˜์ด์ง€๋„ค์ด์…˜, ์ •๋ ฌ, ํ•„ํ„ฐ | +| `v2-table-search-widget` | ๊ฒ€์ƒ‰ ํ•„ํ„ฐ | ํ™”๋ฉด ๋‚ด ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰/ํ•„ํ„ฐ/๊ทธ๋ฃน ๊ธฐ๋Šฅ | +| `v2-pivot-grid` | ํ”ผ๋ฒ— ๊ทธ๋ฆฌ๋“œ | ๋‹ค์ฐจ์› ๋ฐ์ดํ„ฐ ๋ถ„์„ (ํ”ผ๋ฒ— ํ…Œ์ด๋ธ”) | + +### ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| `v2-split-panel-layout` | ๋ถ„ํ•  ํŒจ๋„ | ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ์ขŒ์šฐ ๋ถ„ํ•  ๋ ˆ์ด์•„์›ƒ | +| `v2-tabs-widget` | ํƒญ ์œ„์ ฏ | ํƒญ ์ „ํ™˜, ํƒญ ๋‚ด ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ | +| `v2-section-card` | Section Card | ์ œ๋ชฉ/ํ…Œ๋‘๋ฆฌ๊ฐ€ ์žˆ๋Š” ๊ทธ๋ฃนํ™” ์ปจํ…Œ์ด๋„ˆ | +| `v2-section-paper` | Section Paper | ๋ฐฐ๊ฒฝ์ƒ‰ ๊ธฐ๋ฐ˜ ๋ฏธ๋‹ˆ๋ฉ€ ๊ทธ๋ฃนํ™” ์ปจํ…Œ์ด๋„ˆ | +| `v2-divider-line` | ๊ตฌ๋ถ„์„  | ์˜์—ญ ๊ตฌ๋ถ„ | +| `v2-repeat-container` | ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ | ๋ฐ์ดํ„ฐ ์ˆ˜๋งŒํผ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ๋ฐ˜๋ณต ๋ Œ๋”๋ง | +| `v2-repeater` | ๋ฆฌํ”ผํ„ฐ | ๋ฐ˜๋ณต ์ปจํŠธ๋กค | + +### ์•ก์…˜/๊ธฐํƒ€ ์ปดํฌ๋„ŒํŠธ +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| `v2-button-primary` | ๊ธฐ๋ณธ ๋ฒ„ํŠผ | ์ €์žฅ, ์‚ญ์ œ ๋“ฑ ์•ก์…˜ ๋ฒ„ํŠผ | +| `v2-numbering-rule` | ์ฑ„๋ฒˆ๊ทœ์น™ | ์ž๋™ ์ฝ”๋“œ/๋ฒˆํ˜ธ ์ƒ์„ฑ | +| `v2-category-manager` | ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ์ž | ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ | +| `v2-location-swap-selector` | ์œ„์น˜ ๊ตํ™˜ ์„ ํƒ๊ธฐ | ์œ„์น˜ ๊ตํ™˜ ๊ธฐ๋Šฅ | +| `v2-rack-structure` | ๋ž™ ๊ตฌ์กฐ | ์ฐฝ๊ณ  ๋ž™ ์‹œ๊ฐํ™” | +| `v2-media` | ๋ฏธ๋””์–ด | ๋ฏธ๋””์–ด ํ‘œ์‹œ | + +**์ด 23๊ฐœ V2 ์ปดํฌ๋„ŒํŠธ** + +--- + +## 2. ํ™”๋ฉด ๋ถ„๋ฅ˜ (๋ฉ”๋‰ด๋ณ„) + +### 01. ๊ธฐ์ค€์ •๋ณด (master-data) +| ํ™”๋ฉด๋ช… | ํŒŒ์ผ๋ช… | ํŒจํ„ด | ๊ตฌํ˜„ ๊ฐ€๋Šฅ | +|--------|--------|------|----------| +| ํšŒ์‚ฌ์ •๋ณด | ํšŒ์‚ฌ์ •๋ณด.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ๋ถ€์„œ์ •๋ณด | ๋ถ€์„œ์ •๋ณด.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ํ’ˆ๋ชฉ์ •๋ณด | ํ’ˆ๋ชฉ์ •๋ณด.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ”+๊ทธ๋ฃนํ™” | โš ๏ธ ๊ทธ๋ฃนํ™” ๋ฏธ์ง€์› | +| BOM๊ด€๋ฆฌ | BOM๊ด€๋ฆฌ.html | ๋ถ„ํ• ํŒจ๋„+ํŠธ๋ฆฌ | โš ๏ธ ํŠธ๋ฆฌ๋ทฐ ๋ฏธ์ง€์› | +| ๊ณต์ •์ •๋ณด๊ด€๋ฆฌ | ๊ณต์ •์ •๋ณด๊ด€๋ฆฌ.html | ๋ถ„ํ• ํŒจ๋„+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ๊ณต์ •์ž‘์—…๊ธฐ์ค€ | ๊ณต์ •์ž‘์—…๊ธฐ์ค€๊ด€๋ฆฌ.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ํ’ˆ๋ชฉ๋ผ์šฐํŒ… | ํ’ˆ๋ชฉ๋ผ์šฐํŒ…๊ด€๋ฆฌ.html | ๋ถ„ํ• ํŒจ๋„+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | + +### 02. ์˜์—…๊ด€๋ฆฌ (sales) +| ํ™”๋ฉด๋ช… | ํŒŒ์ผ๋ช… | ํŒจํ„ด | ๊ตฌํ˜„ ๊ฐ€๋Šฅ | +|--------|--------|------|----------| +| ์ˆ˜์ฃผ๊ด€๋ฆฌ | ์ˆ˜์ฃผ๊ด€๋ฆฌ.html | ๋ถ„ํ• ํŒจ๋„+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ๊ฒฌ์ ๊ด€๋ฆฌ | ๊ฒฌ์ ๊ด€๋ฆฌ.html | ๋ถ„ํ• ํŒจ๋„+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ๊ฑฐ๋ž˜์ฒ˜๊ด€๋ฆฌ | ๊ฑฐ๋ž˜์ฒ˜๊ด€๋ฆฌ.html | ๋ถ„ํ• ํŒจ๋„+ํƒญ+๊ทธ๋ฃนํ™” | โš ๏ธ ๊ทธ๋ฃนํ™” ๋ฏธ์ง€์› | +| ํŒ๋งคํ’ˆ๋ชฉ์ •๋ณด | ํŒ๋งคํ’ˆ๋ชฉ์ •๋ณด.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ์ถœํ•˜๊ณ„ํš๊ด€๋ฆฌ | ์ถœํ•˜๊ณ„ํš๊ด€๋ฆฌ.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | + +### 03. ์ƒ์‚ฐ๊ด€๋ฆฌ (production) +| ํ™”๋ฉด๋ช… | ํŒŒ์ผ๋ช… | ํŒจํ„ด | ๊ตฌํ˜„ ๊ฐ€๋Šฅ | +|--------|--------|------|----------| +| ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ | ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ.html | ๋ถ„ํ• ํŒจ๋„+ํƒญ+ํƒ€์ž„๋ผ์ธ | โŒ ํƒ€์ž„๋ผ์ธ ๋ฏธ์ง€์› | +| ์ƒ์‚ฐ๊ด€๋ฆฌ | ์ƒ์‚ฐ๊ด€๋ฆฌ.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ์ƒ์‚ฐ์‹ค์ ๊ด€๋ฆฌ | ์ƒ์‚ฐ์‹ค์ ๊ด€๋ฆฌ.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ์ž‘์—…์ง€์‹œ | ์ž‘์—…์ง€์‹œ.html | ํƒญ+๊ทธ๋ฃนํ™”ํ…Œ์ด๋ธ”+๋ถ„ํ• ํŒจ๋„ | โš ๏ธ ๊ทธ๋ฃนํ™” ๋ฏธ์ง€์› | +| ๊ณต์ •๊ด€๋ฆฌ | ๊ณต์ •๊ด€๋ฆฌ.html | ๋ถ„ํ• ํŒจ๋„+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | + +### 04. ๊ตฌ๋งค๊ด€๋ฆฌ (purchase) +| ํ™”๋ฉด๋ช… | ํŒŒ์ผ๋ช… | ํŒจํ„ด | ๊ตฌํ˜„ ๊ฐ€๋Šฅ | +|--------|--------|------|----------| +| ๋ฐœ์ฃผ๊ด€๋ฆฌ | ๋ฐœ์ฃผ๊ด€๋ฆฌ.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ๊ณต๊ธ‰์—…์ฒด๊ด€๋ฆฌ | ๊ณต๊ธ‰์—…์ฒด๊ด€๋ฆฌ.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ๊ตฌ๋งค์ž…๊ณ  | pages/๊ตฌ๋งค์ž…๊ณ .html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | + +### 05. ์„ค๋น„๊ด€๋ฆฌ (equipment) +| ํ™”๋ฉด๋ช… | ํŒŒ์ผ๋ช… | ํŒจํ„ด | ๊ตฌํ˜„ ๊ฐ€๋Šฅ | +|--------|--------|------|----------| +| ์„ค๋น„์ •๋ณด | ์„ค๋น„์ •๋ณด.html | ๋ถ„ํ• ํŒจ๋„+์นด๋“œ+ํƒญ | โœ… v2-card-display ํ™œ์šฉ | + +### 06. ๋ฌผ๋ฅ˜๊ด€๋ฆฌ (logistics) +| ํ™”๋ฉด๋ช… | ํŒŒ์ผ๋ช… | ํŒจํ„ด | ๊ตฌํ˜„ ๊ฐ€๋Šฅ | +|--------|--------|------|----------| +| ์ฐฝ๊ณ ๊ด€๋ฆฌ | ์ฐฝ๊ณ ๊ด€๋ฆฌ.html | ๋ชจ๋ฐ”์ผ์•ฑ์Šคํƒ€์ผ+iframe | โŒ ๋ณ„๋„๊ฐœ๋ฐœ ํ•„์š” | +| ์ฐฝ๊ณ ์ •๋ณด๊ด€๋ฆฌ | ์ฐฝ๊ณ ์ •๋ณด๊ด€๋ฆฌ.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ์ž…์ถœ๊ณ ๊ด€๋ฆฌ | ์ž…์ถœ๊ณ ๊ด€๋ฆฌ.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ”+๊ทธ๋ฃนํ™” | โš ๏ธ ๊ทธ๋ฃนํ™” ๋ฏธ์ง€์› | +| ์žฌ๊ณ ํ˜„ํ™ฉ | ์žฌ๊ณ ํ˜„ํ™ฉ.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | + +### 07. ํ’ˆ์งˆ๊ด€๋ฆฌ (quality) +| ํ™”๋ฉด๋ช… | ํŒŒ์ผ๋ช… | ํŒจํ„ด | ๊ตฌํ˜„ ๊ฐ€๋Šฅ | +|--------|--------|------|----------| +| ๊ฒ€์‚ฌ๊ธฐ์ค€ | ๊ฒ€์‚ฌ๊ธฐ์ค€.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ๊ฒ€์‚ฌ์ •๋ณด๊ด€๋ฆฌ | ๊ฒ€์‚ฌ์ •๋ณด๊ด€๋ฆฌ.html | ํƒญ+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ๊ฒ€์‚ฌ์žฅ๋น„๊ด€๋ฆฌ | ๊ฒ€์‚ฌ์žฅ๋น„๊ด€๋ฆฌ.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ๋ถˆ๋Ÿ‰๊ด€๋ฆฌ | ๋ถˆ๋Ÿ‰๊ด€๋ฆฌ.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | +| ํด๋ ˆ์ž„๊ด€๋ฆฌ | ํด๋ ˆ์ž„๊ด€๋ฆฌ.html | ๊ฒ€์ƒ‰+ํ…Œ์ด๋ธ” | โœ… ์™„์ „ | + +--- + +## 3. ํ™”๋ฉด UI ํŒจํ„ด ๋ถ„์„ + +### ํŒจํ„ด A: ๊ฒ€์ƒ‰ + ํ…Œ์ด๋ธ” (๊ฐ€์žฅ ๊ธฐ๋ณธ) +**ํ•ด๋‹น ํ™”๋ฉด**: ์•ฝ 60% (15๊ฐœ ์ด์ƒ) + +**์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ**: +- `v2-table-search-widget`: ๊ฒ€์ƒ‰ ํ•„ํ„ฐ +- `v2-table-list`: ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๊ฒ€์ƒ‰ํ•„๋“œ๋“ค...] [์กฐํšŒ] [์—‘์…€] โ”‚ โ† v2-table-search-widget +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ…Œ์ด๋ธ” ์ œ๋ชฉ [์‹ ๊ทœ๋“ฑ๋ก] [์‚ญ์ œ] โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ โ–ก | ์ฝ”๋“œ | ์ด๋ฆ„ | ์ƒํƒœ | ๋“ฑ๋ก์ผ | โ”‚ โ† v2-table-list +โ”‚ โ–ก | A001 | ํ…Œ์ŠคํŠธ| ์‚ฌ์šฉ | 2026-01-30 | โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### ํŒจํ„ด B: ๋ถ„ํ•  ํŒจ๋„ (๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ) +**ํ•ด๋‹น ํ™”๋ฉด**: ์•ฝ 25% (8๊ฐœ) + +**์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ**: +- `v2-split-panel-layout`: ์ขŒ์šฐ ๋ถ„ํ•  +- `v2-table-list`: ๋งˆ์Šคํ„ฐ/๋””ํ…Œ์ผ ํ…Œ์ด๋ธ” +- `v2-tabs-widget`: ์ƒ์„ธ ํƒญ (์„ ํƒ) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๋งˆ์Šคํ„ฐ ๋ฆฌ์ŠคํŠธ โ”‚ ์ƒ์„ธ ์ •๋ณด / ํƒญ โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ–ก A001 ์ œํ’ˆA โ”‚ โ”‚๊ธฐ๋ณธโ”‚์ด๋ ฅโ”‚์ฒจ๋ถ€โ”‚ โ”‚ +โ”‚ โ–ก A002 ์ œํ’ˆB โ† โ”‚ โ””โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ–ก A003 ์ œํ’ˆC โ”‚ [ํ…Œ์ด๋ธ” or ํผ] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### ํŒจํ„ด C: ํƒญ + ํ…Œ์ด๋ธ” +**ํ•ด๋‹น ํ™”๋ฉด**: ์•ฝ 10% (3๊ฐœ) + +**์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ**: +- `v2-tabs-widget`: ํƒญ ์ „ํ™˜ +- `v2-table-list`: ํƒญ๋ณ„ ํ…Œ์ด๋ธ” + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [ํƒญ1] [ํƒญ2] [ํƒญ3] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [ํ…Œ์ด๋ธ” ์˜์—ญ] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### ํŒจํ„ด D: ํŠน์ˆ˜ UI +**ํ•ด๋‹น ํ™”๋ฉด**: ์•ฝ 5% (2๊ฐœ) + +- ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ: ํƒ€์ž„๋ผ์ธ/๊ฐ„ํŠธ ์ฐจํŠธ โ†’ **v2-timeline ๋ฏธ์กด์žฌ** +- ์ฐฝ๊ณ ๊ด€๋ฆฌ: ๋ชจ๋ฐ”์ผ ์•ฑ ์Šคํƒ€์ผ โ†’ **๋ณ„๋„ ๊ฐœ๋ฐœ ํ•„์š”** + +--- + +## 4. ์‹ ๊ทœ ์ปดํฌ๋„ŒํŠธ ๋ถ„์„ (3๊ฐœ ์ด์ƒ ์žฌํ™œ์šฉ ๊ธฐ์ค€) + +### 4.1 v2-grouped-table (๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ”) +**์žฌํ™œ์šฉ ํ™”๋ฉด ์ˆ˜**: 5๊ฐœ ์ด์ƒ โœ… + +| ํ™”๋ฉด | ๊ทธ๋ฃนํ™” ๊ธฐ์ค€ | +|------|------------| +| ํ’ˆ๋ชฉ์ •๋ณด | ํ’ˆ๋ชฉ๊ตฌ๋ถ„, ์นดํ…Œ๊ณ ๋ฆฌ | +| ๊ฑฐ๋ž˜์ฒ˜๊ด€๋ฆฌ | ๊ฑฐ๋ž˜์ฒ˜์œ ํ˜•, ์ง€์—ญ | +| ์ž‘์—…์ง€์‹œ | ์ž‘์—…์ผ์ž, ๊ณต์ • | +| ์ž…์ถœ๊ณ ๊ด€๋ฆฌ | ์ž…์ถœ๊ณ ๊ตฌ๋ถ„, ์ฐฝ๊ณ  | +| ๊ฒฌ์ ๊ด€๋ฆฌ | ์ƒํƒœ, ๊ฑฐ๋ž˜์ฒ˜ | + +**๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ**: +- ํŠน์ • ์ปฌ๋Ÿผ ๊ธฐ์ค€ ๊ทธ๋ฃนํ•‘ +- ๊ทธ๋ฃน ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ +- ๊ทธ๋ฃน ํ—ค๋”์— ์ง‘๊ณ„ ํ‘œ์‹œ +- ๋‹ค์ค‘ ๊ทธ๋ฃนํ•‘ ์ง€์› + +**๊ตฌํ˜„ ๋ณต์žก๋„**: ์ค‘ + +### 4.2 v2-tree-view (ํŠธ๋ฆฌ ๋ทฐ) +**์žฌํ™œ์šฉ ํ™”๋ฉด ์ˆ˜**: 3๊ฐœ โœ… + +| ํ™”๋ฉด | ํŠธ๋ฆฌ ์šฉ๋„ | +|------|----------| +| BOM๊ด€๋ฆฌ | BOM ๊ตฌ์กฐ (์ •์ „๊ฐœ/์—ญ์ „๊ฐœ) | +| ๋ถ€์„œ์ •๋ณด | ์กฐ์ง๋„ | +| ๋ฉ”๋‰ด๊ด€๋ฆฌ | ๋ฉ”๋‰ด ๊ณ„์ธต | + +**๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ**: +- ๋…ธ๋“œ ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ +- ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ (์„ ํƒ) +- ์ •์ „๊ฐœ/์—ญ์ „๊ฐœ ์ „ํ™˜ +- ๋…ธ๋“œ ์„ ํƒ ์ด๋ฒคํŠธ + +**๊ตฌํ˜„ ๋ณต์žก๋„**: ์ค‘์ƒ + +### 4.3 v2-timeline-scheduler (ํƒ€์ž„๋ผ์ธ) +**์žฌํ™œ์šฉ ํ™”๋ฉด ์ˆ˜**: 1~2๊ฐœ (๊ธฐ์ค€ ๋ฏธ๋‹ฌ) + +| ํ™”๋ฉด | ์šฉ๋„ | +|------|------| +| ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ | ๊ฐ„ํŠธ ์ฐจํŠธ | +| ์„ค๋น„ ๊ฐ€๋™ ํ˜„ํ™ฉ | ํƒ€์ž„๋ผ์ธ | + +**๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ**: +- ์‹œ๊ฐ„์ถ• ๊ธฐ๋ฐ˜ ๋ฐฐ์น˜ +- ๋“œ๋ž˜๊ทธ๋กœ ์ผ์ • ๋ณ€๊ฒฝ +- ๊ณต์ •๋ณ„ ์ƒ‰์ƒ ๊ตฌ๋ถ„ +- ์คŒ ์ธ/์•„์›ƒ + +**๊ตฌํ˜„ ๋ณต์žก๋„**: ์ƒ + +> **์ฐธ๊ณ **: 3๊ฐœ ๋ฏธ๋งŒ์ด๋ฏ€๋กœ ์šฐ์„ ์ˆœ์œ„ ํ•˜ํ–ฅ + +--- + +## 5. ์ปดํฌ๋„ŒํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ + +### ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ๋กœ ๊ตฌํ˜„ ๊ฐ€๋Šฅ +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 17๊ฐœ ํ™”๋ฉด (65%) โ”‚ +โ”‚ - ๊ธฐ๋ณธ ๊ฒ€์ƒ‰ + ํ…Œ์ด๋ธ” ํŒจํ„ด โ”‚ +โ”‚ - ๋ถ„ํ•  ํŒจ๋„ โ”‚ +โ”‚ - ํƒญ ์ „ํ™˜ โ”‚ +โ”‚ - ์นด๋“œ ๋””์Šคํ”Œ๋ ˆ์ด โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### v2-grouped-table ๊ฐœ๋ฐœ ํ›„ +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ +5๊ฐœ ํ™”๋ฉด (22๊ฐœ, 85%) โ”‚ +โ”‚ - ํ’ˆ๋ชฉ์ •๋ณด, ๊ฑฐ๋ž˜์ฒ˜๊ด€๋ฆฌ, ์ž‘์—…์ง€์‹œ โ”‚ +โ”‚ - ์ž…์ถœ๊ณ ๊ด€๋ฆฌ, ๊ฒฌ์ ๊ด€๋ฆฌ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### v2-tree-view ๊ฐœ๋ฐœ ํ›„ +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ +2๊ฐœ ํ™”๋ฉด (24๊ฐœ, 92%) โ”‚ +โ”‚ - BOM๊ด€๋ฆฌ, ๋ถ€์„œ์ •๋ณด(๊ณ„์ธต) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### ๋ณ„๋„ ๊ฐœ๋ฐœ ํ•„์š” +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 2๊ฐœ ํ™”๋ฉด (8%) โ”‚ +โ”‚ - ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ (ํƒ€์ž„๋ผ์ธ) โ”‚ +โ”‚ - ์ฐฝ๊ณ ๊ด€๋ฆฌ (๋ชจ๋ฐ”์ผ ์•ฑ ์Šคํƒ€์ผ) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 6. ์‹ ๊ทœ ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ ์šฐ์„ ์ˆœ์œ„ + +| ์ˆœ์œ„ | ์ปดํฌ๋„ŒํŠธ | ์žฌํ™œ์šฉ ํ™”๋ฉด ์ˆ˜ | ๋ณต์žก๋„ | ROI | +|------|----------|--------------|--------|-----| +| 1 | v2-grouped-table | 5+ | ์ค‘ | โญโญโญโญโญ | +| 2 | v2-tree-view | 3 | ์ค‘์ƒ | โญโญโญโญ | +| 3 | v2-timeline-scheduler | 1~2 | ์ƒ | โญโญ | + +--- + +## 7. ๊ถŒ์žฅ ๊ตฌํ˜„ ์ „๋žต + +### Phase 1: ์ฆ‰์‹œ ๊ตฌํ˜„ (ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ) +- ํšŒ์‚ฌ์ •๋ณด, ๋ถ€์„œ์ •๋ณด +- ๋ฐœ์ฃผ๊ด€๋ฆฌ, ๊ณต๊ธ‰์—…์ฒด๊ด€๋ฆฌ +- ๊ฒ€์‚ฌ๊ธฐ์ค€, ๊ฒ€์‚ฌ์žฅ๋น„๊ด€๋ฆฌ, ๋ถˆ๋Ÿ‰๊ด€๋ฆฌ +- ์ฐฝ๊ณ ์ •๋ณด๊ด€๋ฆฌ, ์žฌ๊ณ ํ˜„ํ™ฉ +- ๊ณต์ •์ž‘์—…๊ธฐ์ค€๊ด€๋ฆฌ +- ์ˆ˜์ฃผ๊ด€๋ฆฌ, ๊ฒฌ์ ๊ด€๋ฆฌ, ๊ณต์ •๊ด€๋ฆฌ +- ์„ค๋น„์ •๋ณด (v2-card-display ํ™œ์šฉ) +- ๊ฒ€์‚ฌ์ •๋ณด๊ด€๋ฆฌ + +### Phase 2: v2-grouped-table ๊ฐœ๋ฐœ ํ›„ +- ํ’ˆ๋ชฉ์ •๋ณด, ๊ฑฐ๋ž˜์ฒ˜๊ด€๋ฆฌ, ์ž…์ถœ๊ณ ๊ด€๋ฆฌ +- ์ž‘์—…์ง€์‹œ + +### Phase 3: v2-tree-view ๊ฐœ๋ฐœ ํ›„ +- BOM๊ด€๋ฆฌ +- ๋ถ€์„œ์ •๋ณด (๊ณ„์ธต ๋ทฐ) + +### Phase 4: ๊ฐœ๋ณ„ ๊ฐœ๋ฐœ +- ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ (ํƒ€์ž„๋ผ์ธ) +- ์ฐฝ๊ณ ๊ด€๋ฆฌ (๋ชจ๋ฐ”์ผ ์Šคํƒ€์ผ) + +--- + +## 8. ์š”์•ฝ + +| ํ•ญ๋ชฉ | ์ˆ˜์น˜ | +|------|------| +| ์ „์ฒด ๋ถ„์„ ํ™”๋ฉด ์ˆ˜ | 26๊ฐœ | +| ํ˜„์žฌ ์ฆ‰์‹œ ๊ตฌํ˜„ ๊ฐ€๋Šฅ | 17๊ฐœ (65%) | +| v2-grouped-table ์ถ”๊ฐ€ ์‹œ | 22๊ฐœ (85%) | +| v2-tree-view ์ถ”๊ฐ€ ์‹œ | 24๊ฐœ (92%) | +| ๋ณ„๋„ ๊ฐœ๋ฐœ ํ•„์š” | 2๊ฐœ (8%) | + +**ํ•ต์‹ฌ ๊ฒฐ๋ก **: +1. **ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ**๋กœ 65% ํ™”๋ฉด ๊ตฌํ˜„ ๊ฐ€๋Šฅ +2. **v2-grouped-table** 1๊ฐœ ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ๋กœ 85%๊นŒ์ง€ ํ™•๋Œ€ +3. **v2-tree-view** ์ถ”๊ฐ€๋กœ 92% ๋„๋‹ฌ +4. ๋‚˜๋จธ์ง€ 8%๋Š” ํ™”๋ฉด๋ณ„ ํŠน์ˆ˜ UI (ํƒ€์ž„๋ผ์ธ, ๋ชจ๋ฐ”์ผ ์Šคํƒ€์ผ)๋กœ ๊ฐœ๋ณ„ ๊ฐœ๋ฐœ ํ•„์š” diff --git a/docs/screen-implementation-guide/00_analysis/next-component-development-plan.md b/docs/screen-implementation-guide/00_analysis/next-component-development-plan.md new file mode 100644 index 00000000..58c8cd3f --- /dev/null +++ b/docs/screen-implementation-guide/00_analysis/next-component-development-plan.md @@ -0,0 +1,581 @@ +# ๋‹ค์Œ ๊ตฌํ˜„ ํ•„์š” ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ ๊ณ„ํš + +> **์ž‘์„ฑ์ผ**: 2026-01-30 +> **์ƒํƒœ**: ๊ณ„ํš ์ˆ˜๋ฆฝ ์™„๋ฃŒ +> **์šฐ์„ ์ˆœ์œ„**: v2-table-grouped (1์ˆœ์œ„) โ†’ v2-timeline-scheduler (2์ˆœ์œ„) + +--- + +## ๊ฐœ์š” + +์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ ํ™”๋ฉด์˜ ์ •์‹ ๋ฒ„์ „ ๊ตฌํ˜„์„ ์œ„ํ•ด ํ•„์š”ํ•œ 2๊ฐœ์˜ ์‹ ๊ทœ ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ ๊ณ„ํš์ž…๋‹ˆ๋‹ค. + +| ์ปดํฌ๋„ŒํŠธ | ์šฉ๋„ | ๋‚œ์ด๋„ | ์˜ˆ์ƒ ์ž‘์—…๋Ÿ‰ | +|----------|------|:------:|:----------:| +| `v2-table-grouped` | ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” (์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ) | ์ค‘ | 2-3์ผ | +| `v2-timeline-scheduler` | ํƒ€์ž„๋ผ์ธ/๊ฐ„ํŠธ์ฐจํŠธ ์Šค์ผ€์ค„๋Ÿฌ | ์ƒ | 5-7์ผ | + +--- + +## 1. v2-table-grouped (๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ”) + +### 1.1 ์ปดํฌ๋„ŒํŠธ ๊ฐœ์š” + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **์ปดํฌ๋„ŒํŠธ ID** | `v2-table-grouped` | +| **์นดํ…Œ๊ณ ๋ฆฌ** | DISPLAY | +| **์šฉ๋„** | ๋ฐ์ดํ„ฐ๋ฅผ ํŠน์ • ์ปฌ๋Ÿผ ๊ธฐ์ค€์œผ๋กœ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ ๊ธฐ๋Šฅ ์ œ๊ณต | +| **๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ** | `v2-table-list` ํ™•์žฅ | +| **์ฐธ๊ณ  UI** | Excel ๊ทธ๋ฃนํ™”, VS Code ํŒŒ์ผ ๊ทธ๋ฃนํ™” | + +### 1.2 ํ•ต์‹ฌ ๊ธฐ๋Šฅ + +| ๊ธฐ๋Šฅ | ์„ค๋ช… | ์šฐ์„ ์ˆœ์œ„ | +|------|------|:--------:| +| ๊ทธ๋ฃนํ™” | ์ง€์ •๋œ ์ปฌ๋Ÿผ ๊ธฐ์ค€์œผ๋กœ ๋ฐ์ดํ„ฐ ๊ทธ๋ฃนํ•‘ | ํ•„์ˆ˜ | +| ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ | ๊ทธ๋ฃน ํ–‰ ํด๋ฆญ ์‹œ ํ•˜์œ„ ํ•ญ๋ชฉ ํ† ๊ธ€ | ํ•„์ˆ˜ | +| ๊ทธ๋ฃน ์š”์•ฝ | ๊ทธ๋ฃน๋ณ„ ํ•ฉ๊ณ„/๊ฐœ์ˆ˜ ํ‘œ์‹œ | ํ•„์ˆ˜ | +| ๋‹ค์ค‘ ๊ทธ๋ฃน | ์—ฌ๋Ÿฌ ์ปฌ๋Ÿผ ๊ธฐ์ค€ ์ค‘์ฒฉ ๊ทธ๋ฃนํ™” | ์„ ํƒ | +| ๊ทธ๋ฃน ์„ ํƒ | ๊ทธ๋ฃน ์ฒดํฌ๋ฐ•์Šค๋กœ ํ•˜์œ„ ์ „์ฒด ์„ ํƒ | ํ•„์ˆ˜ | +| ์ „์ฒด ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ | ๋ชจ๋“  ๊ทธ๋ฃน ์ผ๊ด„ ํ† ๊ธ€ | ํ•„์ˆ˜ | + +### 1.3 UI ๋ชฉ์—… + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [์ „์ฒด ํŽผ์น˜๊ธฐ] [์ „์ฒด ์ ‘๊ธฐ] [3๊ฐœ ๊ทธ๋ฃน] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ–ผ โ–ก ํ’ˆ๋ชฉA (P001) ์ˆ˜๋Ÿ‰: 150 3๊ฑด โ”‚ +โ”‚ โ”œโ”€ โ–ก 2026-01-15 ์ƒ์‚ฐ๊ณ„ํš001 50๊ฐœ ์„ค๋น„A โ”‚ +โ”‚ โ”œโ”€ โ–ก 2026-01-16 ์ƒ์‚ฐ๊ณ„ํš002 50๊ฐœ ์„ค๋น„B โ”‚ +โ”‚ โ””โ”€ โ–ก 2026-01-17 ์ƒ์‚ฐ๊ณ„ํš003 50๊ฐœ ์„ค๋น„A โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ–บ โ–ก ํ’ˆ๋ชฉB (P002) ์ˆ˜๋Ÿ‰: 200 2๊ฑด โ”‚ โ† ์ ‘ํžŒ ์ƒํƒœ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ–ผ โ–ก ํ’ˆ๋ชฉC (P003) ์ˆ˜๋Ÿ‰: 100 1๊ฑด โ”‚ +โ”‚ โ””โ”€ โ–ก 2026-01-18 ์ƒ์‚ฐ๊ณ„ํš004 100๊ฐœ ์„ค๋น„C โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 1.4 ํƒ€์ž… ์ •์˜ (types.ts) + +```typescript +import { ColumnConfig } from "../v2-table-list/types"; + +/** + * ๊ทธ๋ฃนํ™” ์„ค์ • + */ +export interface GroupConfig { + /** ๊ทธ๋ฃนํ™” ๊ธฐ์ค€ ์ปฌ๋Ÿผ */ + groupByColumn: string; + + /** ๊ทธ๋ฃน ํ‘œ์‹œ ํ˜•์‹ (์˜ˆ: "{item_name} ({item_code})") */ + groupLabelFormat?: string; + + /** ๊ทธ๋ฃน ์š”์•ฝ ์„ค์ • */ + summary?: { + /** ํ•ฉ๊ณ„ ์ปฌ๋Ÿผ */ + sumColumns?: string[]; + /** ๊ฐœ์ˆ˜ ํ‘œ์‹œ ์—ฌ๋ถ€ */ + showCount?: boolean; + }; + + /** ์ดˆ๊ธฐ ํŽผ์นจ ์ƒํƒœ */ + defaultExpanded?: boolean; + + /** ์ค‘์ฒฉ ๊ทธ๋ฃน (๋‹ค์ค‘ ๊ทธ๋ฃนํ™”) */ + nestedGroup?: GroupConfig; +} + +/** + * ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” ์„ค์ • + */ +export interface TableGroupedConfig { + /** ํ…Œ์ด๋ธ”๋ช… */ + selectedTable?: string; + + /** ์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ */ + useCustomTable?: boolean; + customTableName?: string; + + /** ๊ทธ๋ฃนํ™” ์„ค์ • */ + groupConfig: GroupConfig; + + /** ์ปฌ๋Ÿผ ์„ค์ • (v2-table-list์™€ ๋™์ผ) */ + columns?: ColumnConfig[]; + + /** ์ฒดํฌ๋ฐ•์Šค ํ‘œ์‹œ */ + showCheckbox?: boolean; + + /** ์ฒดํฌ๋ฐ•์Šค ๋ชจ๋“œ */ + checkboxMode?: "single" | "multi"; + + /** ํŽ˜์ด์ง€๋„ค์ด์…˜ (๊ทธ๋ฃน ๋‹จ์œ„) */ + pagination?: { + enabled: boolean; + pageSize: number; + }; + + /** ์ •๋ ฌ ์„ค์ • */ + defaultSort?: { + column: string; + direction: "asc" | "desc"; + }; +} + +/** + * ๊ทธ๋ฃน ์ƒํƒœ + */ +export interface GroupState { + /** ๊ทธ๋ฃน ํ‚ค (groupByColumn ๊ฐ’) */ + groupKey: string; + + /** ํŽผ์นจ ์—ฌ๋ถ€ */ + expanded: boolean; + + /** ๊ทธ๋ฃน ๋‚ด ๋ฐ์ดํ„ฐ */ + items: any[]; + + /** ๊ทธ๋ฃน ์š”์•ฝ ๋ฐ์ดํ„ฐ */ + summary?: Record; +} +``` + +### 1.5 ํŒŒ์ผ ๊ตฌ์กฐ + +``` +frontend/lib/registry/components/v2-table-grouped/ +โ”œโ”€โ”€ index.ts # Definition (V2TableGroupedDefinition) +โ”œโ”€โ”€ types.ts # ํƒ€์ž… ์ •์˜ +โ”œโ”€โ”€ config.ts # ๊ธฐ๋ณธ ์„ค์ •๊ฐ’ +โ”œโ”€โ”€ TableGroupedComponent.tsx # ๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ +โ”œโ”€โ”€ TableGroupedConfigPanel.tsx # ์„ค์ • ํŒจ๋„ +โ”œโ”€โ”€ TableGroupedRenderer.tsx # ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋“ฑ๋ก +โ”œโ”€โ”€ components/ +โ”‚ โ”œโ”€โ”€ GroupHeader.tsx # ๊ทธ๋ฃน ํ—ค๋” (ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ) +โ”‚ โ”œโ”€โ”€ GroupSummary.tsx # ๊ทธ๋ฃน ์š”์•ฝ +โ”‚ โ””โ”€โ”€ GroupCheckbox.tsx # ๊ทธ๋ฃน ์ฒดํฌ๋ฐ•์Šค +โ”œโ”€โ”€ hooks/ +โ”‚ โ””โ”€โ”€ useGroupedData.ts # ๊ทธ๋ฃนํ™” ๋กœ์ง ํ›… +โ””โ”€โ”€ README.md +``` + +### 1.6 ๊ตฌํ˜„ ๋‹จ๊ณ„ + +| ๋‹จ๊ณ„ | ์ž‘์—… ๋‚ด์šฉ | ์˜ˆ์ƒ ์‹œ๊ฐ„ | +|:----:|----------|:---------:| +| 1 | ํƒ€์ž… ์ •์˜ ๋ฐ ๊ธฐ๋ณธ ๊ตฌ์กฐ ์ƒ์„ฑ | 2์‹œ๊ฐ„ | +| 2 | `useGroupedData` ํ›… ๊ตฌํ˜„ (๋ฐ์ดํ„ฐ ๊ทธ๋ฃนํ™” ๋กœ์ง) | 4์‹œ๊ฐ„ | +| 3 | `GroupHeader` ์ปดํฌ๋„ŒํŠธ (ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ UI) | 2์‹œ๊ฐ„ | +| 4 | `TableGroupedComponent` ๋ฉ”์ธ ๊ตฌํ˜„ | 6์‹œ๊ฐ„ | +| 5 | ๊ทธ๋ฃน ์ฒดํฌ๋ฐ•์Šค ์—ฐ๋™ | 2์‹œ๊ฐ„ | +| 6 | ๊ทธ๋ฃน ์š”์•ฝ (ํ•ฉ๊ณ„/๊ฐœ์ˆ˜) | 2์‹œ๊ฐ„ | +| 7 | `TableGroupedConfigPanel` ์„ค์ • ํŒจ๋„ | 4์‹œ๊ฐ„ | +| 8 | ํ…Œ์ŠคํŠธ ๋ฐ ๋ฌธ์„œํ™” | 2์‹œ๊ฐ„ | + +**์ด ์˜ˆ์ƒ: 24์‹œ๊ฐ„ (์•ฝ 3์ผ)** + +### 1.7 v2-table-list์™€์˜ ์ฐจ์ด์  + +| ํ•ญ๋ชฉ | v2-table-list | v2-table-grouped | +|------|---------------|------------------| +| ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ | ํ”Œ๋žซ ๋ฆฌ์ŠคํŠธ | ๊ณ„์ธต ๊ตฌ์กฐ (๊ทธ๋ฃน > ์•„์ดํ…œ) | +| ๋ Œ๋”๋ง | ํ–‰ ๋‹จ์œ„ | ๊ทธ๋ฃน ํ—ค๋” + ์ƒ์„ธ ํ–‰ | +| ์„ ํƒ | ๊ฐœ๋ณ„ ํ–‰ | ๊ทธ๋ฃน ๋‹จ์œ„ / ๊ฐœ๋ณ„ ๋‹จ์œ„ | +| ์š”์•ฝ | ์ „์ฒด ํ•ฉ๊ณ„ (์„ ํƒ) | ๊ทธ๋ฃน๋ณ„ ์š”์•ฝ | +| ํŽ˜์ด์ง€๋„ค์ด์…˜ | ํ–‰ ๋‹จ์œ„ | ๊ทธ๋ฃน ๋‹จ์œ„ | + +--- + +## 2. v2-timeline-scheduler (ํƒ€์ž„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ) + +### 2.1 ์ปดํฌ๋„ŒํŠธ ๊ฐœ์š” + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **์ปดํฌ๋„ŒํŠธ ID** | `v2-timeline-scheduler` | +| **์นดํ…Œ๊ณ ๋ฆฌ** | DISPLAY | +| **์šฉ๋„** | ๊ฐ„ํŠธ์ฐจํŠธ ํ˜•ํƒœ์˜ ์ผ์ •/๊ณ„ํš ์‹œ๊ฐํ™” ๋ฐ ํŽธ์ง‘ | +| **์ฐธ๊ณ  UI** | MS Project, Jira Timeline, dhtmlxGantt | +| **์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ** | ๊ณ ๋ ค ์ค‘: `@tanstack/react-virtual` (๊ฐ€์ƒ ์Šคํฌ๋กค) | + +### 2.2 ํ•ต์‹ฌ ๊ธฐ๋Šฅ + +| ๊ธฐ๋Šฅ | ์„ค๋ช… | ์šฐ์„ ์ˆœ์œ„ | +|------|------|:--------:| +| ํƒ€์ž„๋ผ์ธ ๊ทธ๋ฆฌ๋“œ | ๋‚ ์งœ ๊ธฐ์ค€ ๊ทธ๋ฆฌ๋“œ ํ‘œ์‹œ (์ผ/์ฃผ/์›”) | ํ•„์ˆ˜ | +| ์Šค์ผ€์ค„ ๋ฐ” | ์‹œ์ž‘~์ข…๋ฃŒ ๊ธฐ๊ฐ„ ๋ฐ” ๋ Œ๋”๋ง | ํ•„์ˆ˜ | +| ๋ฆฌ์†Œ์Šค ํ–‰ | ์„ค๋น„/์ž‘์—…์ž๋ณ„ ํ–‰ ๊ตฌ๋ถ„ | ํ•„์ˆ˜ | +| ๋“œ๋ž˜๊ทธ ์ด๋™ | ์Šค์ผ€์ค„ ๋ฐ” ๋“œ๋ž˜๊ทธ๋กœ ๋‚ ์งœ ๋ณ€๊ฒฝ | ํ•„์ˆ˜ | +| ๋ฆฌ์‚ฌ์ด์ฆˆ | ๋ฐ” ์–‘์ชฝ ํ•ธ๋“ค๋กœ ๊ธฐ๊ฐ„ ์กฐ์ • | ํ•„์ˆ˜ | +| ์คŒ ๋ ˆ๋ฒจ | ์ผ/์ฃผ/์›” ๋‹จ์œ„ ์ „ํ™˜ | ํ•„์ˆ˜ | +| ์ถฉ๋Œ ํ‘œ์‹œ | ๊ฐ™์€ ๋ฆฌ์†Œ์Šค ์‹œ๊ฐ„ ๊ฒน์นจ ๊ฒฝ๊ณ  | ์„ ํƒ | +| ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ | ๋ฐ” ๋‚ด๋ถ€ ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ | ์„ ํƒ | +| ๋งˆ์ผ์Šคํ†ค | ๋‹จ์ผ ์ผ์ • ๋งˆ์ปค | ์„ ํƒ | + +### 2.3 UI ๋ชฉ์—… + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [โ—€ ์ด์ „] [์˜ค๋Š˜] [๋‹ค์Œ โ–ถ] 2026๋…„ 1์›” [์ผ] [์ฃผ] [์›”] [+ ์ถ”๊ฐ€] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ 15(์ˆ˜) โ”‚ 16(๋ชฉ) โ”‚ 17(๊ธˆ) โ”‚ 18(ํ† ) โ”‚ 19(์ผ) โ”‚ 20(์›”) โ”‚ 21(ํ™”) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ์„ค๋น„A โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ โ”‚ +โ”‚ โ”‚ [์ƒ์‚ฐ๊ณ„ํš001] โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ์„ค๋น„B โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ โ”‚ +โ”‚ โ”‚ [์ƒ์‚ฐ๊ณ„ํš002 ] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ์„ค๋น„C โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ โ”‚ +โ”‚ โ”‚ [์ƒ์‚ฐ๊ณ„ํš003] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +๋ฒ”๋ก€: โ–ˆโ–ˆ ์ง„ํ–‰์ค‘ โ–ˆโ–ˆ ์™„๋ฃŒ โ–ˆโ–ˆ ์ง€์—ฐ โ—† ๋งˆ์ผ์Šคํ†ค +``` + +### 2.4 ํƒ€์ž… ์ •์˜ (types.ts) + +```typescript +/** + * ์คŒ ๋ ˆ๋ฒจ + */ +export type ZoomLevel = "day" | "week" | "month"; + +/** + * ์Šค์ผ€์ค„ ์ƒํƒœ + */ +export type ScheduleStatus = "planned" | "in_progress" | "completed" | "delayed" | "cancelled"; + +/** + * ์Šค์ผ€์ค„ ํ•ญ๋ชฉ + */ +export interface ScheduleItem { + /** ๊ณ ์œ  ID */ + id: string; + + /** ๋ฆฌ์†Œ์Šค ID (์„ค๋น„/์ž‘์—…์ž) */ + resourceId: string; + + /** ํ‘œ์‹œ ์ œ๋ชฉ */ + title: string; + + /** ์‹œ์ž‘ ์ผ์‹œ */ + startDate: string; // ISO 8601 format + + /** ์ข…๋ฃŒ ์ผ์‹œ */ + endDate: string; + + /** ์ƒํƒœ */ + status: ScheduleStatus; + + /** ์ง„ํ–‰๋ฅ  (0-100) */ + progress?: number; + + /** ์ƒ‰์ƒ (CSS color) */ + color?: string; + + /** ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ */ + data?: Record; +} + +/** + * ๋ฆฌ์†Œ์Šค (ํ–‰) + */ +export interface Resource { + /** ๋ฆฌ์†Œ์Šค ID */ + id: string; + + /** ํ‘œ์‹œ๋ช… */ + name: string; + + /** ๊ทธ๋ฃน (์„ ํƒ) */ + group?: string; + + /** ์•„์ด์ฝ˜ (์„ ํƒ) */ + icon?: string; + + /** ์šฉ๋Ÿ‰ (์„ ํƒ, ์ถฉ๋Œ ๊ณ„์‚ฐ์šฉ) */ + capacity?: number; +} + +/** + * ํƒ€์ž„๋ผ์ธ ์„ค์ • + */ +export interface TimelineSchedulerConfig { + /** ํ…Œ์ด๋ธ”๋ช… (์Šค์ผ€์ค„ ๋ฐ์ดํ„ฐ) */ + selectedTable?: string; + + /** ๋ฆฌ์†Œ์Šค ํ…Œ์ด๋ธ”๋ช… */ + resourceTable?: string; + + /** ํ•„๋“œ ๋งคํ•‘ */ + fieldMapping: { + id: string; + resourceId: string; + title: string; + startDate: string; + endDate: string; + status?: string; + progress?: string; + color?: string; + }; + + /** ๋ฆฌ์†Œ์Šค ํ•„๋“œ ๋งคํ•‘ */ + resourceFieldMapping?: { + id: string; + name: string; + group?: string; + }; + + /** ์ดˆ๊ธฐ ์คŒ ๋ ˆ๋ฒจ */ + defaultZoomLevel?: ZoomLevel; + + /** ์ดˆ๊ธฐ ํ‘œ์‹œ ๋‚ ์งœ */ + initialDate?: string; + + /** ํŽธ์ง‘ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ */ + editable?: boolean; + + /** ๋“œ๋ž˜๊ทธ ์ด๋™ ํ—ˆ์šฉ */ + allowDrag?: boolean; + + /** ๋ฆฌ์‚ฌ์ด์ฆˆ ํ—ˆ์šฉ */ + allowResize?: boolean; + + /** ์ถฉ๋Œ ์ฒดํฌ */ + checkConflicts?: boolean; + + /** ์ƒํƒœ๋ณ„ ์ƒ‰์ƒ */ + statusColors?: Record; + + /** ๋ฆฌ์†Œ์Šค ์ปฌ๋Ÿผ ๋„ˆ๋น„ */ + resourceColumnWidth?: number; + + /** ํ–‰ ๋†’์ด */ + rowHeight?: number; + + /** ์…€ ๋„ˆ๋น„ (์คŒ ๋ ˆ๋ฒจ๋ณ„) */ + cellWidth?: { + day: number; + week: number; + month: number; + }; + + /** ํˆด๋ฐ” ํ‘œ์‹œ */ + showToolbar?: boolean; + + /** ๋ฒ”๋ก€ ํ‘œ์‹œ */ + showLegend?: boolean; +} + +/** + * ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ + */ +export interface TimelineEvents { + /** ์Šค์ผ€์ค„ ํด๋ฆญ */ + onScheduleClick?: (schedule: ScheduleItem) => void; + + /** ์Šค์ผ€์ค„ ๋”๋ธ”ํด๋ฆญ */ + onScheduleDoubleClick?: (schedule: ScheduleItem) => void; + + /** ๋“œ๋ž˜๊ทธ ์™„๋ฃŒ */ + onScheduleDrag?: (schedule: ScheduleItem, newStart: Date, newEnd: Date) => void; + + /** ๋ฆฌ์‚ฌ์ด์ฆˆ ์™„๋ฃŒ */ + onScheduleResize?: (schedule: ScheduleItem, newStart: Date, newEnd: Date) => void; + + /** ๋นˆ ์˜์—ญ ํด๋ฆญ (์ƒˆ ์Šค์ผ€์ค„ ์ถ”๊ฐ€์šฉ) */ + onEmptyClick?: (resourceId: string, date: Date) => void; +} +``` + +### 2.5 ํŒŒ์ผ ๊ตฌ์กฐ + +``` +frontend/lib/registry/components/v2-timeline-scheduler/ +โ”œโ”€โ”€ index.ts # Definition (V2TimelineSchedulerDefinition) +โ”œโ”€โ”€ types.ts # ํƒ€์ž… ์ •์˜ +โ”œโ”€โ”€ config.ts # ๊ธฐ๋ณธ ์„ค์ •๊ฐ’ +โ”œโ”€โ”€ TimelineSchedulerComponent.tsx # ๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ +โ”œโ”€โ”€ TimelineSchedulerConfigPanel.tsx # ์„ค์ • ํŒจ๋„ +โ”œโ”€โ”€ TimelineSchedulerRenderer.tsx # ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋“ฑ๋ก +โ”œโ”€โ”€ components/ +โ”‚ โ”œโ”€โ”€ TimelineHeader.tsx # ๋‚ ์งœ ํ—ค๋” +โ”‚ โ”œโ”€โ”€ TimelineGrid.tsx # ๊ทธ๋ฆฌ๋“œ ๋ฐฐ๊ฒฝ +โ”‚ โ”œโ”€โ”€ ResourceColumn.tsx # ๋ฆฌ์†Œ์Šค ์ปฌ๋Ÿผ (์ขŒ์ธก) +โ”‚ โ”œโ”€โ”€ ScheduleBar.tsx # ์Šค์ผ€์ค„ ๋ฐ” (๋“œ๋ž˜๊ทธ/๋ฆฌ์‚ฌ์ด์ฆˆ) +โ”‚ โ”œโ”€โ”€ TimelineToolbar.tsx # ํˆด๋ฐ” (์คŒ, ๋„ค๋น„๊ฒŒ์ด์…˜) +โ”‚ โ”œโ”€โ”€ TimelineLegend.tsx # ๋ฒ”๋ก€ +โ”‚ โ””โ”€โ”€ ConflictIndicator.tsx # ์ถฉ๋Œ ํ‘œ์‹œ +โ”œโ”€โ”€ hooks/ +โ”‚ โ”œโ”€โ”€ useTimelineState.ts # ํƒ€์ž„๋ผ์ธ ์ƒํƒœ ๊ด€๋ฆฌ +โ”‚ โ”œโ”€โ”€ useScheduleDrag.ts # ๋“œ๋ž˜๊ทธ ๋กœ์ง +โ”‚ โ”œโ”€โ”€ useScheduleResize.ts # ๋ฆฌ์‚ฌ์ด์ฆˆ ๋กœ์ง +โ”‚ โ””โ”€โ”€ useDateCalculation.ts # ๋‚ ์งœ/์œ„์น˜ ๊ณ„์‚ฐ +โ”œโ”€โ”€ utils/ +โ”‚ โ”œโ”€โ”€ dateUtils.ts # ๋‚ ์งœ ์œ ํ‹ธ๋ฆฌํ‹ฐ +โ”‚ โ””โ”€โ”€ conflictDetection.ts # ์ถฉ๋Œ ๊ฐ์ง€ +โ””โ”€โ”€ README.md +``` + +### 2.6 ๊ตฌํ˜„ ๋‹จ๊ณ„ + +| ๋‹จ๊ณ„ | ์ž‘์—… ๋‚ด์šฉ | ์˜ˆ์ƒ ์‹œ๊ฐ„ | +|:----:|----------|:---------:| +| 1 | ํƒ€์ž… ์ •์˜ ๋ฐ ๊ธฐ๋ณธ ๊ตฌ์กฐ ์ƒ์„ฑ | 3์‹œ๊ฐ„ | +| 2 | `TimelineHeader` (๋‚ ์งœ ํ—ค๋”, ์คŒ ๋ ˆ๋ฒจ) | 4์‹œ๊ฐ„ | +| 3 | `TimelineGrid` (๊ทธ๋ฆฌ๋“œ ๋ฐฐ๊ฒฝ) | 3์‹œ๊ฐ„ | +| 4 | `ResourceColumn` (๋ฆฌ์†Œ์Šค ๋ชฉ๋ก) | 2์‹œ๊ฐ„ | +| 5 | `ScheduleBar` ๊ธฐ๋ณธ ๋ Œ๋”๋ง | 4์‹œ๊ฐ„ | +| 6 | ๋“œ๋ž˜๊ทธ ์ด๋™ ๊ตฌํ˜„ | 6์‹œ๊ฐ„ | +| 7 | ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ตฌํ˜„ | 4์‹œ๊ฐ„ | +| 8 | ์คŒ ๋ ˆ๋ฒจ ์ „ํ™˜ (์ผ/์ฃผ/์›”) | 3์‹œ๊ฐ„ | +| 9 | ๋‚ ์งœ ๋„ค๋น„๊ฒŒ์ด์…˜ | 2์‹œ๊ฐ„ | +| 10 | ์ถฉ๋Œ ๊ฐ์ง€ ๋ฐ ํ‘œ์‹œ | 4์‹œ๊ฐ„ | +| 11 | ๊ฐ€์ƒ ์Šคํฌ๋กค (๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ) | 4์‹œ๊ฐ„ | +| 12 | `TimelineSchedulerConfigPanel` | 4์‹œ๊ฐ„ | +| 13 | API ์—ฐ๋™ (์ €์žฅ/๋กœ๋“œ) | 4์‹œ๊ฐ„ | +| 14 | ํ…Œ์ŠคํŠธ ๋ฐ ๋ฌธ์„œํ™” | 3์‹œ๊ฐ„ | + +**์ด ์˜ˆ์ƒ: 50์‹œ๊ฐ„ (์•ฝ 6-7์ผ)** + +### 2.7 ํ•ต์‹ฌ ์•Œ๊ณ ๋ฆฌ์ฆ˜ + +#### ๋‚ ์งœ โ†’ ํ”ฝ์…€ ์œ„์น˜ ๋ณ€ํ™˜ + +```typescript +function dateToPosition(date: Date, viewStart: Date, cellWidth: number, zoomLevel: ZoomLevel): number { + const diffMs = date.getTime() - viewStart.getTime(); + + switch (zoomLevel) { + case "day": + const diffDays = diffMs / (1000 * 60 * 60 * 24); + return diffDays * cellWidth; + case "week": + const diffWeeks = diffMs / (1000 * 60 * 60 * 24 * 7); + return diffWeeks * cellWidth; + case "month": + // ์›” ๋‹จ์œ„๋Š” ์ผ์ˆ˜๊ฐ€ ๋‹ค๋ฅด๋ฏ€๋กœ ๋ณ„๋„ ๊ณ„์‚ฐ + return calculateMonthPosition(date, viewStart, cellWidth); + } +} +``` + +#### ์ถฉ๋Œ ๊ฐ์ง€ + +```typescript +function detectConflicts(schedules: ScheduleItem[], resourceId: string): ScheduleItem[][] { + const resourceSchedules = schedules + .filter(s => s.resourceId === resourceId) + .sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime()); + + const conflicts: ScheduleItem[][] = []; + + for (let i = 0; i < resourceSchedules.length; i++) { + const current = resourceSchedules[i]; + const overlapping = resourceSchedules.filter(s => + s.id !== current.id && + new Date(s.startDate) < new Date(current.endDate) && + new Date(s.endDate) > new Date(current.startDate) + ); + + if (overlapping.length > 0) { + conflicts.push([current, ...overlapping]); + } + } + + return conflicts; +} +``` + +--- + +## 3. ๊ตฌํ˜„ ์šฐ์„ ์ˆœ์œ„ ๋ฐ ์ผ์ • + +### 3.1 ๊ถŒ์žฅ ์ˆœ์„œ + +``` +1๋‹จ๊ณ„: v2-table-grouped (2-3์ผ) + โ†“ +2๋‹จ๊ณ„: v2-timeline-scheduler (5-7์ผ) + โ†“ +3๋‹จ๊ณ„: ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ ์ •์‹ ๋ฒ„์ „ ํ™”๋ฉด ๊ตฌ์„ฑ (1-2์ผ) +``` + +### 3.2 ์ด์œ  + +1. **v2-table-grouped ๋จผ์ €**: + - `v2-table-list` ๊ธฐ๋ฐ˜ ํ™•์žฅ์œผ๋กœ ๋‚œ์ด๋„ ๋‚ฎ์Œ + - ์ƒ์‚ฐ๊ณ„ํš ์™ธ ๋‹ค๋ฅธ ํ™”๋ฉด(BOM, ์ˆ˜์ฃผ ๋“ฑ)์—์„œ๋„ ํ™œ์šฉ ๊ฐ€๋Šฅ + - ํƒ€์ž„๋ผ์ธ ๊ฐœ๋ฐœ ์ค‘์—๋„ ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ + +2. **v2-timeline-scheduler ๋‚˜์ค‘**: + - ๋ณต์žก๋„๊ฐ€ ๋†’์•„ ์ง‘์ค‘ ๊ฐœ๋ฐœ ํ•„์š” + - ๋“œ๋ž˜๊ทธ/๋ฆฌ์‚ฌ์ด์ฆˆ ๋“ฑ ์ธํ„ฐ๋ž™์…˜ ํ…Œ์ŠคํŠธ ํ•„์š” + - ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ ์ „์šฉ ์ปดํฌ๋„ŒํŠธ + +### 3.3 ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +#### v2-table-grouped โœ… ๊ตฌํ˜„ ์™„๋ฃŒ (2026-01-30) + +- [x] ํƒ€์ž… ์ •์˜ ์™„๋ฃŒ +- [x] ๊ธฐ๋ณธ ๊ตฌ์กฐ ์ƒ์„ฑ +- [x] useGroupedData ํ›… ๊ตฌํ˜„ +- [x] GroupHeader ์ปดํฌ๋„ŒํŠธ +- [x] ๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ +- [x] ๊ทธ๋ฃน ์ฒดํฌ๋ฐ•์Šค ์—ฐ๋™ +- [x] ๊ทธ๋ฃน ์š”์•ฝ (ํ•ฉ๊ณ„/๊ฐœ์ˆ˜) +- [x] ์„ค์ • ํŒจ๋„ ๊ตฌํ˜„ +- [x] ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋“ฑ๋ก +- [x] ๋ฌธ์„œํ™” (README.md) + +#### v2-timeline-scheduler โœ… ๊ตฌํ˜„ ์™„๋ฃŒ (2026-01-30) + +- [x] ํƒ€์ž… ์ •์˜ ์™„๋ฃŒ +- [x] ๊ธฐ๋ณธ ๊ตฌ์กฐ ์ƒ์„ฑ +- [x] TimelineHeader (๋‚ ์งœ) +- [x] TimelineGrid (๋ฐฐ๊ฒฝ) +- [x] ResourceColumn (๋ฆฌ์†Œ์Šค) +- [x] ScheduleBar ๊ธฐ๋ณธ ๋ Œ๋”๋ง +- [x] ๋“œ๋ž˜๊ทธ ์ด๋™ (๊ธฐ๋ณธ) +- [x] ๋ฆฌ์‚ฌ์ด์ฆˆ (๊ธฐ๋ณธ) +- [x] ์คŒ ๋ ˆ๋ฒจ ์ „ํ™˜ +- [x] ๋‚ ์งœ ๋„ค๋น„๊ฒŒ์ด์…˜ +- [ ] ์ถฉ๋Œ ๊ฐ์ง€ (ํ–ฅํ›„) +- [ ] ๊ฐ€์ƒ ์Šคํฌ๋กค (ํ–ฅํ›„) +- [x] ์„ค์ • ํŒจ๋„ ๊ตฌํ˜„ +- [x] API ์—ฐ๋™ +- [x] ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋“ฑ๋ก +- [ ] ํ…Œ์ŠคํŠธ ์™„๋ฃŒ +- [x] ๋ฌธ์„œํ™” (README.md) + +--- + +## 4. ์ฐธ๊ณ  ์ž๋ฃŒ + +### ๊ธฐ์กด V2 ์ปดํฌ๋„ŒํŠธ ์ฐธ๊ณ  + +- `v2-table-list`: ํ…Œ์ด๋ธ” ๋ Œ๋”๋ง, ์ฒดํฌ๋ฐ•์Šค, ํŽ˜์ด์ง€๋„ค์ด์…˜ +- `v2-pivot-grid`: ๋ณต์žกํ•œ ๊ทธ๋ฆฌ๋“œ ๋ Œ๋”๋ง, ๊ฐ€์ƒ ์Šคํฌ๋กค +- `v2-split-panel-layout`: ์ปค์Šคํ…€ ๋ชจ๋“œ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ + +### ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ฒ€ํ†  + +| ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ | ์šฉ๋„ | ๊ณ ๋ ค ์‚ฌํ•ญ | +|----------|------|----------| +| `@tanstack/react-virtual` | ๊ฐ€์ƒ ์Šคํฌ๋กค | ์ด๋ฏธ ์‚ฌ์šฉ ์ค‘, ํ™•์žฅ ์šฉ์ด | +| `date-fns` | ๋‚ ์งœ ๊ณ„์‚ฐ | ์ด๋ฏธ ์‚ฌ์šฉ ์ค‘ | +| `react-dnd` | ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ | ๊ฒ€ํ†  ํ•„์š”, ํ˜„์žฌ ๋„ค์ดํ‹ฐ๋ธŒ ๊ตฌํ˜„ | + +### ๊ด€๋ จ ๋ฌธ์„œ + +- [์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ ํ™”๋ฉด ์„ค๊ณ„](../03_production/production-plan.md) +- [V2 ์ปดํฌ๋„ŒํŠธ ๋ถ„์„ ๊ฐ€์ด๋“œ](../../V2_์ปดํฌ๋„ŒํŠธ_๋ถ„์„_๊ฐ€์ด๋“œ.md) +- [์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ](../../../frontend/docs/component-development-guide.md) + +--- + +**์ž‘์„ฑ์ž**: Claude AI +**์ตœ์ข… ์ˆ˜์ •**: 2026-01-30 diff --git a/docs/screen-implementation-guide/00_analysis/schedule-auto-generation-guide.md b/docs/screen-implementation-guide/00_analysis/schedule-auto-generation-guide.md new file mode 100644 index 00000000..02699843 --- /dev/null +++ b/docs/screen-implementation-guide/00_analysis/schedule-auto-generation-guide.md @@ -0,0 +1,894 @@ +# ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ + +> ๋ฒ„์ „: 2.0 +> ์ตœ์ข… ์ˆ˜์ •: 2025-02-02 +> ์ ์šฉ ํ™”๋ฉด: ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ, ์„ค๋น„๊ณ„ํš๊ด€๋ฆฌ, ์ถœํ•˜๊ณ„ํš๊ด€๋ฆฌ ๋“ฑ + +## 1. ๊ฐœ์š” + +### 1.1 ๊ธฐ๋Šฅ ์„ค๋ช… + +์ขŒ์ธก ํ…Œ์ด๋ธ”์—์„œ ์„ ํƒํ•œ ๋ฐ์ดํ„ฐ(์ˆ˜์ฃผ, ์ž‘์—…์ง€์‹œ ๋“ฑ)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์šฐ์ธก ํƒ€์ž„๋ผ์ธ์— ์Šค์ผ€์ค„์„ ์ž๋™ ์ƒ์„ฑํ•˜๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. + +### 1.2 ์ฃผ์š” ํŠน์ง• + +- **๋ฒ”์šฉ์„ฑ**: ์„ค์ • ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ค์–‘ํ•œ ํ™”๋ฉด์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ +- **๋ฏธ๋ฆฌ๋ณด๊ธฐ**: ์ ์šฉ ์ „ ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ™•์ธ ๊ฐ€๋Šฅ +- **์†Œ์Šค ์ถ”์ **: ์Šค์ผ€์ค„์ด ์–ด๋””์„œ ์ƒ์„ฑ๋˜์—ˆ๋Š”์ง€ ์ถ”์  ๊ฐ€๋Šฅ +- **์—ฐ๊ฒฐ ํ•„ํ„ฐ**: ์ขŒ์ธก ์„ ํƒ ์‹œ ์šฐ์ธก ํƒ€์ž„๋ผ์ธ ์ž๋™ ํ•„ํ„ฐ๋ง +- **์ด๋ฒคํŠธ ๋ฒ„์Šค ๊ธฐ๋ฐ˜**: ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ๋А์Šจํ•œ ๊ฒฐํ•ฉ (Loose Coupling) + +### 1.3 ์•„ํ‚คํ…์ฒ˜ ์›์น™ + +**์ด๋ฒคํŠธ ๋ฒ„์Šค ํŒจํ„ด**์„ ํ™œ์šฉํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ์ง์ ‘ ์ฐธ์กฐ๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” ์ด๋ฒคํŠธ ๋ฐœ์†ก โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-button โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ โ”‚ EventBus โ”‚ +โ”‚ (๋ฐœ์†ก๋งŒ ํ•จ) โ”‚ โ”‚ (์ค‘์žฌ์ž) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ โ”‚ + โ–ผ โ–ผ โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ ScheduleService โ”‚ โ”‚ v2-timeline โ”‚ โ”‚ ๊ธฐํƒ€ ๋ฆฌ์Šค๋„ˆ โ”‚ + โ”‚ (์ฒ˜๋ฆฌ ๋‹ด๋‹น) โ”‚ โ”‚ (๊ฐฑ์‹ ) โ”‚ โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์žฅ์ **: +- ๋ฒ„ํŠผ์€ ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋””์„œ ์˜ค๋Š”์ง€ ์•Œ ํ•„์š” ์—†์Œ +- ํ…Œ์ด๋ธ”์€ ๋ˆ„๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š”์ง€ ์•Œ ํ•„์š” ์—†์Œ +- ์ปดํฌ๋„ŒํŠธ ๊ต์ฒด/์ถ”๊ฐ€ ์‹œ ๊ธฐ์กด ์ฝ”๋“œ ์ˆ˜์ • ๋ถˆํ•„์š” + +--- + +## 2. ๋ฐ์ดํ„ฐ ํ๋ฆ„ + +### 2.1 ์ „์ฒด ํ๋ฆ„๋„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๋ถ„ํ•  ํŒจ๋„ (SplitPanelLayout) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ์ขŒ์ธก ํŒจ๋„ โ”‚ ์šฐ์ธก ํŒจ๋„ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-grouped โ”‚ โ”‚ โ”‚ ์ž๋™ ์Šค์ผ€์ค„ ์ƒ์„ฑ ๋ฒ„ํŠผ โ”‚ โ”‚ +โ”‚ โ”‚ (์ˆ˜์ฃผ ๋ชฉ๋ก) โ”‚ โ”‚ โ”‚ โ†“ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ‘  ์ขŒ์ธก ์„ ํƒ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ โ”‚ โ”‚ +โ”‚ โ”‚ โ˜‘ ITEM-001 (ํƒ•ํ•‘ A) โ”‚โ”€โ”€โ”ผโ”€โ”€โ”‚ โ‘ก ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ (๋ฏธ๋ฆฌ๋ณด๊ธฐ) โ”‚ โ”‚ +โ”‚ โ”‚ โ”” SO-2025-101 โ”‚ โ”‚ โ”‚ โ‘ข ๋ณ€๊ฒฝ์‚ฌํ•ญ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ โ”‚ โ”‚ +โ”‚ โ”‚ โ”” SO-2025-102 โ”‚ โ”‚ โ”‚ โ‘ฃ ์ ์šฉ API ํ˜ธ์ถœ โ”‚ โ”‚ +โ”‚ โ”‚ โ˜ ITEM-002 (ํƒ•ํ•‘ B) โ”‚ โ”‚ โ”‚ โ‘ค ํƒ€์ž„๋ผ์ธ ์ƒˆ๋กœ๊ณ ์นจ โ”‚ โ”‚ +โ”‚ โ”‚ โ”” SO-2025-201 โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ linkedFilter โ”‚ โ”‚ v2-timeline-scheduler โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”‚ (์ƒ์‚ฐ ํƒ€์ž„๋ผ์ธ) โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ part_code = ์„ ํƒ๋œ ํ’ˆ๋ชฉ ํ•„ํ„ฐ๋ง โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2.2 ๋‹จ๊ณ„๋ณ„ ๋ฐ์ดํ„ฐ ํ๋ฆ„ + +| ๋‹จ๊ณ„ | ๋™์ž‘ | ๋ฐ์ดํ„ฐ | +|------|------|--------| +| 1 | ์ขŒ์ธก ํ…Œ์ด๋ธ”์—์„œ ํ’ˆ๋ชฉ ์„ ํƒ | `selectedItems[]` (๊ทธ๋ฃน ์„ ํƒ ์‹œ ์ž์‹ ํฌํ•จ) | +| 2 | ์ž๋™ ์Šค์ผ€์ค„ ์ƒ์„ฑ ๋ฒ„ํŠผ ํด๋ฆญ | ๋ฒ„ํŠผ ์•ก์…˜ ์‹คํ–‰ | +| 3 | ๋ฏธ๋ฆฌ๋ณด๊ธฐ API ํ˜ธ์ถœ | `{ config, sourceData, period }` | +| 4 | ๋ณ€๊ฒฝ์‚ฌํ•ญ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ | `{ toCreate, toDelete, summary }` | +| 5 | ์ ์šฉ API ํ˜ธ์ถœ | `{ config, preview, options }` | +| 6 | ํƒ€์ž„๋ผ์ธ ์ƒˆ๋กœ๊ณ ์นจ | `TABLE_REFRESH` ์ด๋ฒคํŠธ ๋ฐœ์†ก | +| 7 | ๋‹ค์Œ ๋ฐฉ๋ฌธ ์‹œ ์ขŒ์ธก ์„ ํƒ | `linkedFilter`๋กœ ์šฐ์ธก ์ž๋™ ํ•„ํ„ฐ๋ง | + +--- + +## 3. ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ์„ค๊ณ„ + +### 3.1 ๋ฒ”์šฉ ์Šค์ผ€์ค„ ํ…Œ์ด๋ธ” (schedule_mng) + +```sql +CREATE TABLE schedule_mng ( + schedule_id SERIAL PRIMARY KEY, + company_code VARCHAR(20) NOT NULL, + + -- ์Šค์ผ€์ค„ ๊ธฐ๋ณธ ์ •๋ณด + schedule_type VARCHAR(50) NOT NULL, -- 'PRODUCTION', 'SHIPPING', 'MAINTENANCE' ๋“ฑ + schedule_name VARCHAR(200), + + -- ๋ฆฌ์†Œ์Šค ์—ฐ๊ฒฐ (ํƒ€์ž„๋ผ์ธ Y์ถ•) + resource_type VARCHAR(50) NOT NULL, -- 'ITEM', 'MACHINE', 'WORKER' ๋“ฑ + resource_id VARCHAR(50) NOT NULL, -- ํ’ˆ๋ชฉ์ฝ”๋“œ, ์„ค๋น„์ฝ”๋“œ ๋“ฑ + resource_name VARCHAR(200), + + -- ์ผ์ • + start_date TIMESTAMP NOT NULL, + end_date TIMESTAMP NOT NULL, + + -- ์ˆ˜๋Ÿ‰/๊ฐ’ + plan_qty NUMERIC(15,3), + actual_qty NUMERIC(15,3), + + -- ์ƒํƒœ + status VARCHAR(20) DEFAULT 'PLANNED', -- PLANNED, IN_PROGRESS, COMPLETED, CANCELLED + + -- ์†Œ์Šค ์ถ”์  (์–ด๋””์„œ ์ƒ์„ฑ๋˜์—ˆ๋Š”์ง€) + source_table VARCHAR(100), -- 'sales_order_mng', 'work_order_mng' ๋“ฑ + source_id VARCHAR(50), -- ์†Œ์Šค ํ…Œ์ด๋ธ”์˜ PK + source_group_key VARCHAR(100), -- ๊ทธ๋ฃน ํ‚ค (ํ’ˆ๋ชฉ์ฝ”๋“œ ๋“ฑ) + + -- ์ž๋™ ์ƒ์„ฑ ์—ฌ๋ถ€ + auto_generated BOOLEAN DEFAULT FALSE, + generated_at TIMESTAMP, + generated_by VARCHAR(50), + + -- ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ (์ถ”๊ฐ€ ์ •๋ณด JSON) + metadata JSONB, + + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + + CONSTRAINT fk_schedule_company FOREIGN KEY (company_code) + REFERENCES company_mng(company_code) +); + +-- ์ธ๋ฑ์Šค +CREATE INDEX idx_schedule_company ON schedule_mng(company_code); +CREATE INDEX idx_schedule_type ON schedule_mng(schedule_type); +CREATE INDEX idx_schedule_resource ON schedule_mng(resource_type, resource_id); +CREATE INDEX idx_schedule_source ON schedule_mng(source_table, source_id); +CREATE INDEX idx_schedule_date ON schedule_mng(start_date, end_date); +CREATE INDEX idx_schedule_status ON schedule_mng(status); +``` + +### 3.2 ์†Œ์Šค-์Šค์ผ€์ค„ ๋งคํ•‘ ํ…Œ์ด๋ธ” (N:M ๊ด€๊ณ„) + +```sql +-- ํ•˜๋‚˜์˜ ์Šค์ผ€์ค„์ด ์—ฌ๋Ÿฌ ์†Œ์Šค์—์„œ ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ์Œ +CREATE TABLE schedule_source_mapping ( + mapping_id SERIAL PRIMARY KEY, + company_code VARCHAR(20) NOT NULL, + schedule_id INTEGER REFERENCES schedule_mng(schedule_id) ON DELETE CASCADE, + + -- ์†Œ์Šค ์ •๋ณด + source_table VARCHAR(100) NOT NULL, + source_id VARCHAR(50) NOT NULL, + source_qty NUMERIC(15,3), -- ํ•ด๋‹น ์†Œ์Šค์—์„œ ๊ธฐ์—ฌํ•œ ์ˆ˜๋Ÿ‰ + + created_at TIMESTAMP DEFAULT NOW(), + + CONSTRAINT fk_mapping_company FOREIGN KEY (company_code) + REFERENCES company_mng(company_code) +); + +CREATE INDEX idx_mapping_schedule ON schedule_source_mapping(schedule_id); +CREATE INDEX idx_mapping_source ON schedule_source_mapping(source_table, source_id); +``` + +### 3.3 ํ…Œ์ด๋ธ” ๊ด€๊ณ„๋„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ sales_order_mng โ”‚ โ”‚ schedule_mng โ”‚ โ”‚ schedule_source_ โ”‚ +โ”‚ (์†Œ์Šค ํ…Œ์ด๋ธ”) โ”‚ โ”‚ (์Šค์ผ€์ค„ ํ…Œ์ด๋ธ”) โ”‚ โ”‚ mapping โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ order_id (PK) โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ source_id โ”‚ โ”‚ mapping_id (PK) โ”‚ +โ”‚ part_code โ”‚ โ”‚ schedule_id (PK) โ”‚โ”€โ”€1:Nโ”€โ”€โ”‚ schedule_id (FK) โ”‚ +โ”‚ order_qty โ”‚ โ”‚ resource_id โ”‚ โ”‚ source_table โ”‚ +โ”‚ balance_qty โ”‚ โ”‚ start_date โ”‚ โ”‚ source_id โ”‚ +โ”‚ due_date โ”‚ โ”‚ end_date โ”‚ โ”‚ source_qty โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ plan_qty โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ status โ”‚ + โ”‚ auto_generated โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 4. ์Šค์ผ€์ค„ ์ƒ์„ฑ ์„ค์ • ๊ตฌ์กฐ + +### 4.1 TypeScript ์ธํ„ฐํŽ˜์ด์Šค + +```typescript +// ํ™”๋ฉด ๋ ˆ๋ฒจ ์„ค์ • (screen_definitions ๋˜๋Š” screen_layouts_v2์— ์ €์žฅ) +interface ScheduleGenerationConfig { + // ์Šค์ผ€์ค„ ํƒ€์ž… + scheduleType: "PRODUCTION" | "SHIPPING" | "MAINTENANCE" | "WORK_ASSIGN"; + + // ์†Œ์Šค ์„ค์ • (์ปดํฌ๋„ŒํŠธ ID ๋ถˆํ•„์š” - ์ด๋ฒคํŠธ๋กœ ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ) + source: { + tableName: string; // ์†Œ์Šค ํ…Œ์ด๋ธ”๋ช… + groupByField: string; // ๊ทธ๋ฃนํ™” ๊ธฐ์ค€ ํ•„๋“œ (part_code) + quantityField: string; // ์ˆ˜๋Ÿ‰ ํ•„๋“œ (order_qty, balance_qty) + dueDateField?: string; // ๋‚ฉ๊ธฐ์ผ ํ•„๋“œ (์„ ํƒ) + }; + + // ๋ฆฌ์†Œ์Šค ๋งคํ•‘ (ํƒ€์ž„๋ผ์ธ Y์ถ•) + resource: { + type: string; // 'ITEM', 'MACHINE', 'WORKER' ๋“ฑ + idField: string; // part_code, machine_code ๋“ฑ + nameField: string; // part_name, machine_name ๋“ฑ + }; + + // ์ƒ์„ฑ ๊ทœ์น™ + rules: { + leadTimeDays?: number; // ๋ฆฌ๋“œํƒ€์ž„ (์ผ) + dailyCapacity?: number; // ์ผ์ผ ์ƒ์‚ฐ๋Šฅ๋ ฅ + workingDays?: number[]; // ์ž‘์—…์ผ [1,2,3,4,5] = ์›”~๊ธˆ + considerStock?: boolean; // ์žฌ๊ณ  ๊ณ ๋ ค ์—ฌ๋ถ€ + stockTableName?: string; // ์žฌ๊ณ  ํ…Œ์ด๋ธ”๋ช… + stockQtyField?: string; // ์žฌ๊ณ  ์ˆ˜๋Ÿ‰ ํ•„๋“œ + safetyStockField?: string; // ์•ˆ์ „์žฌ๊ณ  ํ•„๋“œ + }; + + // ํƒ€๊ฒŸ ์„ค์ • + target: { + tableName: string; // ์Šค์ผ€์ค„ ํ…Œ์ด๋ธ”๋ช… (schedule_mng ๋˜๋Š” ์ „์šฉ ํ…Œ์ด๋ธ”) + }; +} +``` + +> **์ฃผ์˜**: ๊ธฐ์กด ์„ค๊ณ„์™€ ๋‹ฌ๋ฆฌ `source.componentId`์™€ `target.timelineComponentId`๊ฐ€ ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. +> ์ด๋ฒคํŠธ ๋ฒ„์Šค๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๊ฐ€ ์ „๋‹ฌ๋˜๋ฏ€๋กœ ์ปดํฌ๋„ŒํŠธ ID๋ฅผ ์ง์ ‘ ์ฐธ์กฐํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. + +### 4.2 ํ™”๋ฉด๋ณ„ ์„ค์ • ์˜ˆ์‹œ + +#### ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ ํ™”๋ฉด + +```json +{ + "scheduleType": "PRODUCTION", + "source": { + "tableName": "sales_order_mng", + "groupByField": "part_code", + "quantityField": "balance_qty", + "dueDateField": "due_date" + }, + "resource": { + "type": "ITEM", + "idField": "part_code", + "nameField": "part_name" + }, + "rules": { + "leadTimeDays": 3, + "dailyCapacity": 100, + "workingDays": [1, 2, 3, 4, 5], + "considerStock": true, + "stockTableName": "inventory_mng", + "stockQtyField": "current_qty", + "safetyStockField": "safety_stock" + }, + "target": { + "tableName": "schedule_mng" + } +} +``` + +#### ์„ค๋น„๊ณ„ํš๊ด€๋ฆฌ ํ™”๋ฉด + +```json +{ + "scheduleType": "MAINTENANCE", + "source": { + "tableName": "work_order_mng", + "groupByField": "machine_code", + "quantityField": "work_hours" + }, + "resource": { + "type": "MACHINE", + "idField": "machine_code", + "nameField": "machine_name" + }, + "rules": { + "workingDays": [1, 2, 3, 4, 5, 6] + }, + "target": { + "tableName": "schedule_mng" + } +} +``` + +--- + +## 5. ๋ฐฑ์—”๋“œ API ์„ค๊ณ„ + +### 5.1 ๋ฏธ๋ฆฌ๋ณด๊ธฐ API + +```typescript +// POST /api/schedule/preview +interface PreviewRequest { + config: ScheduleGenerationConfig; + sourceData: any[]; // ์„ ํƒ๋œ ์†Œ์Šค ๋ฐ์ดํ„ฐ + period: { + start: string; // ISO ๋‚ ์งœ ๋ฌธ์ž์—ด + end: string; + }; +} + +interface PreviewResponse { + success: boolean; + preview: { + toCreate: ScheduleItem[]; // ์ƒ์„ฑ๋  ์Šค์ผ€์ค„ + toDelete: ScheduleItem[]; // ์‚ญ์ œ๋  ๊ธฐ์กด ์Šค์ผ€์ค„ + toUpdate: ScheduleItem[]; // ์ˆ˜์ •๋  ์Šค์ผ€์ค„ + summary: { + createCount: number; + deleteCount: number; + updateCount: number; + totalQty: number; + }; + }; +} +``` + +### 5.2 ์ ์šฉ API + +```typescript +// POST /api/schedule/apply +interface ApplyRequest { + config: ScheduleGenerationConfig; + preview: PreviewResponse["preview"]; + options: { + deleteExisting: boolean; // ๊ธฐ์กด ์Šค์ผ€์ค„ ์‚ญ์ œ ์—ฌ๋ถ€ + updateMode: "replace" | "merge"; + }; +} + +interface ApplyResponse { + success: boolean; + applied: { + created: number; + deleted: number; + updated: number; + }; +} +``` + +### 5.3 ์Šค์ผ€์ค„ ์กฐํšŒ API (ํƒ€์ž„๋ผ์ธ์šฉ) + +```typescript +// GET /api/schedule/list +interface ListQuery { + scheduleType: string; + resourceType: string; + resourceId?: string; // ํ•„ํ„ฐ๋ง (linkedFilter์—์„œ ์‚ฌ์šฉ) + startDate: string; + endDate: string; +} + +interface ListResponse { + success: boolean; + data: ScheduleItem[]; + total: number; +} +``` + +--- + +## 6. ์ด๋ฒคํŠธ ๋ฒ„์Šค ๊ธฐ๋ฐ˜ ๊ตฌํ˜„ + +### 6.1 ์ด๋ฒคํŠธ ํƒ€์ž… ์ •์˜ + +```typescript +// frontend/lib/v2-core/events/types.ts์— ์ถ”๊ฐ€ + +export const V2_EVENTS = { + // ... ๊ธฐ์กด ์ด๋ฒคํŠธ๋“ค + + // ์Šค์ผ€์ค„ ์ƒ์„ฑ ์ด๋ฒคํŠธ + SCHEDULE_GENERATE_REQUEST: "v2:schedule:generate:request", + SCHEDULE_GENERATE_PREVIEW: "v2:schedule:generate:preview", + SCHEDULE_GENERATE_APPLY: "v2:schedule:generate:apply", + SCHEDULE_GENERATE_COMPLETE: "v2:schedule:generate:complete", + SCHEDULE_GENERATE_ERROR: "v2:schedule:generate:error", +} as const; + +/** ์Šค์ผ€์ค„ ์ƒ์„ฑ ์š”์ฒญ ์ด๋ฒคํŠธ */ +export interface V2ScheduleGenerateRequestEvent { + requestId: string; + scheduleType: "PRODUCTION" | "MAINTENANCE" | "SHIPPING" | "WORK_ASSIGN"; + sourceData?: any[]; // ์„ ํƒ ๋ฐ์ดํ„ฐ (์—†์œผ๋ฉด TABLE_SELECTION_CHANGE๋กœ ๋ฐ›์€ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ) + period?: { start: string; end: string }; +} + +/** ์Šค์ผ€์ค„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฒฐ๊ณผ ์ด๋ฒคํŠธ */ +export interface V2ScheduleGeneratePreviewEvent { + requestId: string; + preview: { + toCreate: any[]; + toDelete: any[]; + summary: { createCount: number; deleteCount: number; totalQty: number }; + }; +} + +/** ์Šค์ผ€์ค„ ์ ์šฉ ์ด๋ฒคํŠธ */ +export interface V2ScheduleGenerateApplyEvent { + requestId: string; + confirmed: boolean; +} + +/** ์Šค์ผ€์ค„ ์ƒ์„ฑ ์™„๋ฃŒ ์ด๋ฒคํŠธ */ +export interface V2ScheduleGenerateCompleteEvent { + requestId: string; + success: boolean; + applied: { created: number; deleted: number }; + scheduleType: string; +} +``` + +### 6.2 ๋ฒ„ํŠผ ์„ค์ • (๊ฐ„์†Œํ™”) + +```json +{ + "componentType": "v2-button-primary", + "componentId": "btn_auto_schedule", + "componentConfig": { + "label": "์ž๋™ ์Šค์ผ€์ค„ ์ƒ์„ฑ", + "variant": "default", + "icon": "Calendar", + "action": { + "type": "event", + "eventName": "SCHEDULE_GENERATE_REQUEST", + "eventPayload": { + "scheduleType": "PRODUCTION" + } + } + } +} +``` + +> **ํ•ต์‹ฌ**: ๋ฒ„ํŠผ์€ ์ด๋ฒคํŠธ๋งŒ ๋ฐœ์†กํ•˜๊ณ , ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋””์„œ ์˜ค๋Š”์ง€ ์•Œ ํ•„์š” ์—†์Œ + +### 6.3 ์Šค์ผ€์ค„ ์ƒ์„ฑ ์„œ๋น„์Šค (์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ) + +```typescript +// frontend/lib/v2-core/services/ScheduleGeneratorService.ts + +import { v2EventBus, V2_EVENTS } from "@/lib/v2-core"; +import apiClient from "@/lib/api/client"; +import { toast } from "sonner"; + +export function useScheduleGenerator(scheduleConfig: ScheduleGenerationConfig) { + const [selectedData, setSelectedData] = useState([]); + const [previewResult, setPreviewResult] = useState(null); + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const [currentRequestId, setCurrentRequestId] = useState(""); + + // 1. ํ…Œ์ด๋ธ” ์„ ํƒ ๋ฐ์ดํ„ฐ ์ถ”์  (TABLE_SELECTION_CHANGE ์ด๋ฒคํŠธ ์ˆ˜์‹ ) + useEffect(() => { + const unsubscribe = v2EventBus.on( + V2_EVENTS.TABLE_SELECTION_CHANGE, + (payload) => { + // ์„ค์ •๋œ ์†Œ์Šค ํ…Œ์ด๋ธ”๊ณผ ์ผ์น˜ํ•˜๋Š” ๊ฒฝ์šฐ์—๋งŒ ์ €์žฅ + if (payload.tableName === scheduleConfig.source.tableName) { + setSelectedData(payload.selectedRows); + } + } + ); + return unsubscribe; + }, [scheduleConfig.source.tableName]); + + // 2. ์Šค์ผ€์ค„ ์ƒ์„ฑ ์š”์ฒญ ์ฒ˜๋ฆฌ (SCHEDULE_GENERATE_REQUEST ์ˆ˜์‹ ) + useEffect(() => { + const unsubscribe = v2EventBus.on( + V2_EVENTS.SCHEDULE_GENERATE_REQUEST, + async (payload) => { + // ์Šค์ผ€์ค„ ํƒ€์ž…์ด ์ผ์น˜ํ•˜๋Š” ๊ฒฝ์šฐ์—๋งŒ ์ฒ˜๋ฆฌ + if (payload.scheduleType !== scheduleConfig.scheduleType) { + return; + } + + const dataToUse = payload.sourceData || selectedData; + + if (dataToUse.length === 0) { + toast.warning("ํ’ˆ๋ชฉ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); + return; + } + + setCurrentRequestId(payload.requestId); + + try { + // ๋ฏธ๋ฆฌ๋ณด๊ธฐ API ํ˜ธ์ถœ + const response = await apiClient.post("/api/schedule/preview", { + config: scheduleConfig, + sourceData: dataToUse, + period: payload.period || getDefaultPeriod(), + }); + + if (!response.data.success) { + toast.error(response.data.message); + v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_ERROR, { + requestId: payload.requestId, + error: response.data.message, + }); + return; + } + + setPreviewResult(response.data.preview); + setShowConfirmDialog(true); + + // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฒฐ๊ณผ ์ด๋ฒคํŠธ ๋ฐœ์†ก (๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Œ) + v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_PREVIEW, { + requestId: payload.requestId, + preview: response.data.preview, + }); + } catch (error: any) { + toast.error("์Šค์ผ€์ค„ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_ERROR, { + requestId: payload.requestId, + error: error.message, + }); + } + } + ); + return unsubscribe; + }, [selectedData, scheduleConfig]); + + // 3. ์Šค์ผ€์ค„ ์ ์šฉ ์ฒ˜๋ฆฌ (SCHEDULE_GENERATE_APPLY ์ˆ˜์‹ ) + useEffect(() => { + const unsubscribe = v2EventBus.on( + V2_EVENTS.SCHEDULE_GENERATE_APPLY, + async (payload) => { + if (payload.requestId !== currentRequestId) return; + if (!payload.confirmed) { + setShowConfirmDialog(false); + return; + } + + try { + const response = await apiClient.post("/api/schedule/apply", { + config: scheduleConfig, + preview: previewResult, + options: { deleteExisting: true, updateMode: "replace" }, + }); + + if (!response.data.success) { + toast.error(response.data.message); + return; + } + + // ์™„๋ฃŒ ์ด๋ฒคํŠธ ๋ฐœ์†ก + v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_COMPLETE, { + requestId: payload.requestId, + success: true, + applied: response.data.applied, + scheduleType: scheduleConfig.scheduleType, + }); + + // ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฐœ์†ก + v2EventBus.emit(V2_EVENTS.TABLE_REFRESH, { + tableName: scheduleConfig.target.tableName, + }); + + toast.success(`${response.data.applied.created}๊ฑด์˜ ์Šค์ผ€์ค„์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); + setShowConfirmDialog(false); + } catch (error: any) { + toast.error("์Šค์ผ€์ค„ ์ ์šฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + } + ); + return unsubscribe; + }, [currentRequestId, previewResult, scheduleConfig]); + + // ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ•ธ๋“ค๋Ÿฌ + const handleConfirm = (confirmed: boolean) => { + v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_APPLY, { + requestId: currentRequestId, + confirmed, + }); + }; + + return { + showConfirmDialog, + previewResult, + handleConfirm, + }; +} + +function getDefaultPeriod() { + const now = new Date(); + const start = new Date(now.getFullYear(), now.getMonth(), 1); + const end = new Date(now.getFullYear(), now.getMonth() + 1, 0); + return { + start: start.toISOString().split("T")[0], + end: end.toISOString().split("T")[0], + }; +} +``` + +### 6.4 ํƒ€์ž„๋ผ์ธ ์ปดํฌ๋„ŒํŠธ (์ด๋ฒคํŠธ ์ˆ˜์‹ ) + +```typescript +// v2-timeline-scheduler์—์„œ ์ด๋ฒคํŠธ ์ˆ˜์‹  + +useEffect(() => { + // ์Šค์ผ€์ค„ ์ƒ์„ฑ ์™„๋ฃŒ ์‹œ ์ž๋™ ์ƒˆ๋กœ๊ณ ์นจ + const unsubscribe1 = v2EventBus.on( + V2_EVENTS.SCHEDULE_GENERATE_COMPLETE, + (payload) => { + if (payload.success && payload.scheduleType === config.scheduleType) { + fetchSchedules(); + } + } + ); + + // TABLE_REFRESH ์ด๋ฒคํŠธ๋กœ๋„ ์ƒˆ๋กœ๊ณ ์นจ + const unsubscribe2 = v2EventBus.on( + V2_EVENTS.TABLE_REFRESH, + (payload) => { + if (payload.tableName === config.selectedTable) { + fetchSchedules(); + } + } + ); + + return () => { + unsubscribe1(); + unsubscribe2(); + }; +}, [config.selectedTable, config.scheduleType]); +``` + +### 6.5 ๋ฒ„ํŠผ ์•ก์…˜ ํ•ธ๋“ค๋Ÿฌ (์ด๋ฒคํŠธ ๋ฐœ์†ก) + +```typescript +// frontend/lib/utils/buttonActions.ts + +// ๊ธฐ์กด handleButtonAction์— ์ถ”๊ฐ€ +case "event": + const eventName = action.eventName as keyof typeof V2_EVENTS; + const eventPayload = { + requestId: crypto.randomUUID(), + ...action.eventPayload, + }; + + v2EventBus.emit(V2_EVENTS[eventName], eventPayload); + return true; +``` + +--- + +## 7. ์ปดํฌ๋„ŒํŠธ ์—ฐ๋™ ์„ค์ • + +### 7.1 ๋ถ„ํ•  ํŒจ๋„ ์—ฐ๊ฒฐ ํ•„ํ„ฐ (linkedFilters) + +์ขŒ์ธก ํ…Œ์ด๋ธ” ์„ ํƒ ์‹œ ์šฐ์ธก ํƒ€์ž„๋ผ์ธ ์ž๋™ ํ•„ํ„ฐ๋ง: + +```json +{ + "componentType": "v2-split-panel-layout", + "componentConfig": { + "linkedFilters": [ + { + "sourceComponentId": "order_table", + "sourceField": "part_code", + "targetColumn": "resource_id" + } + ] + } +} +``` + +### 7.2 ํƒ€์ž„๋ผ์ธ ์„ค์ • + +```json +{ + "componentType": "v2-timeline-scheduler", + "componentId": "production_timeline", + "componentConfig": { + "selectedTable": "production_plan_mng", + "fieldMapping": { + "id": "schedule_id", + "resourceId": "resource_id", + "title": "schedule_name", + "startDate": "start_date", + "endDate": "end_date", + "status": "status" + }, + "useLinkedFilter": true + } +} +``` + +### 7.3 ์ด๋ฒคํŠธ ํ๋ฆ„๋„ (Event-Driven) + +``` +[์ขŒ์ธก ํ…Œ์ด๋ธ” ์„ ํƒ] + โ”‚ + โ–ผ +v2-table-grouped.onSelectionChange + โ”‚ + โ–ผ emit(TABLE_SELECTION_CHANGE) + โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ + โ–ผ โ–ผ +ScheduleGeneratorService SplitPanelContext + (selectedData ์ €์žฅ) (linkedFilter ์—…๋ฐ์ดํŠธ) + โ”‚ + โ–ผ + v2-timeline-scheduler + (์ž๋™ ํ•„ํ„ฐ๋ง) + + +[์ž๋™ ์Šค์ผ€์ค„ ์ƒ์„ฑ ๋ฒ„ํŠผ ํด๋ฆญ] + โ”‚ + โ–ผ emit(SCHEDULE_GENERATE_REQUEST) + โ”‚ + โ–ผ +ScheduleGeneratorService (์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ) + โ”‚ + โ”œโ”€โ”€โ”€ selectedData (์ด๋ฏธ ์ €์žฅ๋จ) + โ”‚ + โ–ผ +POST /api/schedule/preview + โ”‚ + โ–ผ emit(SCHEDULE_GENERATE_PREVIEW) + โ”‚ + โ–ผ +ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ + โ”‚ + โ–ผ (ํ™•์ธ ํด๋ฆญ) emit(SCHEDULE_GENERATE_APPLY) + โ”‚ + โ–ผ +POST /api/schedule/apply + โ”‚ + โ”œโ”€โ”€โ”€ emit(SCHEDULE_GENERATE_COMPLETE) + โ”‚ + โ”œโ”€โ”€โ”€ emit(TABLE_REFRESH) + โ”‚ + โ–ผ +v2-timeline-scheduler (on TABLE_REFRESH) + โ”‚ + โ–ผ +fetchSchedules() โ†’ ํ™”๋ฉด ๊ฐฑ์‹  +``` + +### 7.4 ์ด๋ฒคํŠธ ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Table โ”‚ โ”‚ Button โ”‚ โ”‚ ScheduleSvc โ”‚ โ”‚ Backend โ”‚ โ”‚ Timeline โ”‚ +โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ + โ”‚ SELECT โ”‚ โ”‚ โ”‚ โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ โ”‚ โ”‚ โ”‚ + โ”‚ TABLE_SELECTION_CHANGE โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ CLICK โ”‚ โ”‚ โ”‚ + โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ SCHEDULE_GENERATE_REQUEST โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ POST /preview โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ + โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ CONFIRM DIALOG โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ POST /apply โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ + โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ + โ”‚ โ”‚ โ”‚ SCHEDULE_GENERATE_COMPLETE โ”‚ + โ”‚ โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ + โ”‚ โ”‚ โ”‚ TABLE_REFRESH โ”‚ + โ”‚ โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ–ถ refresh + โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +``` + +--- + +## 8. ๋ฒ”์šฉ์„ฑ ํ™œ์šฉ ๊ฐ€์ด๋“œ + +### 8.1 ๋‹ค๋ฅธ ํ™”๋ฉด์—์„œ ์žฌ์‚ฌ์šฉ + +| ํ™”๋ฉด | ์†Œ์Šค ํ…Œ์ด๋ธ” | ๊ทธ๋ฃน ํ•„๋“œ | ์Šค์ผ€์ค„ ํƒ€์ž… | ๋ฆฌ์†Œ์Šค ํƒ€์ž… | +|------|------------|----------|------------|------------| +| ์ƒ์‚ฐ๊ณ„ํš | sales_order_mng | part_code | PRODUCTION | ITEM | +| ์„ค๋น„๊ณ„ํš | work_order_mng | machine_code | MAINTENANCE | MACHINE | +| ์ถœํ•˜๊ณ„ํš | shipment_order_mng | customer_code | SHIPPING | CUSTOMER | +| ์ž‘์—…์ž ๋ฐฐ์น˜ | task_mng | worker_id | WORK_ASSIGN | WORKER | + +### 8.2 ์ƒˆ ํ™”๋ฉด ์ถ”๊ฐ€ ์‹œ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +- [ ] ์†Œ์Šค ํ…Œ์ด๋ธ” ์ •์˜ (์–ด๋–ค ๋ฐ์ดํ„ฐ๋ฅผ ์„ ํƒํ•  ๊ฒƒ์ธ์ง€) +- [ ] ๊ทธ๋ฃนํ™” ๊ธฐ์ค€ ํ•„๋“œ ์ •์˜ (ํ’ˆ๋ชฉ, ์„ค๋น„, ๊ณ ๊ฐ ๋“ฑ) +- [ ] ์Šค์ผ€์ค„ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ๋˜๋Š” ๊ธฐ์กด schedule_mng ์‚ฌ์šฉ +- [ ] ScheduleGenerationConfig ์ž‘์„ฑ +- [ ] ๋ฒ„ํŠผ์— scheduleConfig ์„ค์ • +- [ ] ๋ถ„ํ•  ํŒจ๋„ linkedFilters ์„ค์ • +- [ ] ํƒ€์ž„๋ผ์ธ fieldMapping ์„ค์ • + +--- + +## 9. ๊ตฌํ˜„ ์ˆœ์„œ + +| ๋‹จ๊ณ„ | ์ž‘์—… | ์ƒํƒœ | +|------|------|------| +| 1 | ํ…Œ์ด๋ธ” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (schedule_mng, schedule_source_mapping) | ๋Œ€๊ธฐ | +| 2 | ๋ฐฑ์—”๋“œ API (scheduleController, scheduleService) | ๋Œ€๊ธฐ | +| 3 | ๋ฒ„ํŠผ ์•ก์…˜ ํ•ธ๋“ค๋Ÿฌ (autoGenerateSchedule) | ๋Œ€๊ธฐ | +| 4 | ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ (๊ธฐ์กด AlertDialog ํ™œ์šฉ) | ๋Œ€๊ธฐ | +| 5 | ํƒ€์ž„๋ผ์ธ linkedFilter ์—ฐ๋™ | ๋Œ€๊ธฐ | +| 6 | ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ฆ | ๋Œ€๊ธฐ | + +--- + +## 10. ์ฐธ๊ณ  ์‚ฌํ•ญ + +### ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ + +- `v2-table-grouped`: ๊ทธ๋ฃนํ™”๋œ ํ…Œ์ด๋ธ” (์†Œ์Šค ๋ฐ์ดํ„ฐ, TABLE_SELECTION_CHANGE ๋ฐœ์†ก) +- `v2-timeline-scheduler`: ํƒ€์ž„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ (TABLE_REFRESH ์ˆ˜์‹ ) +- `v2-button-primary`: ์•ก์…˜ ๋ฒ„ํŠผ (SCHEDULE_GENERATE_REQUEST ๋ฐœ์†ก) +- `v2-split-panel-layout`: ๋ถ„ํ•  ํŒจ๋„ + +### ๊ด€๋ จ ํŒŒ์ผ + +- `frontend/lib/v2-core/events/types.ts`: ์ด๋ฒคํŠธ ํƒ€์ž… ์ •์˜ +- `frontend/lib/v2-core/events/EventBus.ts`: ์ด๋ฒคํŠธ ๋ฒ„์Šค +- `frontend/lib/v2-core/services/ScheduleGeneratorService.ts`: ์Šค์ผ€์ค„ ์ƒ์„ฑ ์„œ๋น„์Šค (์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ) +- `frontend/lib/utils/buttonActions.ts`: ๋ฒ„ํŠผ ์•ก์…˜ ํ•ธ๋“ค๋Ÿฌ (์ด๋ฒคํŠธ ๋ฐœ์†ก) +- `backend-node/src/services/scheduleService.ts`: ์Šค์ผ€์ค„ ์„œ๋น„์Šค +- `backend-node/src/controllers/scheduleController.ts`: ์Šค์ผ€์ค„ ์ปจํŠธ๋กค๋Ÿฌ + +### ํŠน์ด ์‚ฌํ•ญ + +- v2-table-grouped์˜ `selectedItems`๋Š” ๊ทธ๋ฃน ์„ ํƒ ์‹œ ์ž์‹ ํ–‰๊นŒ์ง€ ํฌํ•จ๋จ +- ์Šค์ผ€์ค„ ์ƒ์„ฑ ์‹œ ๊ธฐ์กด ์Šค์ผ€์ค„๊ณผ ๋น„๊ตํ•˜์—ฌ ๋ณ€๊ฒฝ์‚ฌํ•ญ๋งŒ ์ ์šฉ (๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ œ๊ณต) +- source_table, source_id๋กœ ์†Œ์Šค ์ถ”์  ๊ฐ€๋Šฅ +- **์ปดํฌ๋„ŒํŠธ ID ์ง์ ‘ ์ฐธ์กฐ ์—†์Œ** - ์ด๋ฒคํŠธ ๋ฒ„์Šค๋กœ ๋А์Šจํ•œ ๊ฒฐํ•ฉ + +--- + +## 11. ์ด๋ฒคํŠธ ๋ฒ„์Šค ํŒจํ„ด์˜ ์žฅ์  + +### 11.1 ๊ธฐ์กด ๋ฐฉ์‹ vs ์ด๋ฒคํŠธ ๋ฒ„์Šค ๋ฐฉ์‹ + +| ํ•ญ๋ชฉ | ๊ธฐ์กด (์ง์ ‘ ์ฐธ์กฐ) | ์ด๋ฒคํŠธ ๋ฒ„์Šค | +|------|------------------|-------------| +| ๊ฒฐํ•ฉ๋„ | ๊ฐ• (componentId ํ•„์š”) | ์•ฝ (์ด๋ฒคํŠธ๋ช…๋งŒ ํ•„์š”) | +| ๋ฒ„ํŠผ ์„ค์ • | `source.componentId: "order_table"` | `eventPayload.scheduleType: "PRODUCTION"` | +| ์ปดํฌ๋„ŒํŠธ ๊ต์ฒด | ์„ค์ • ์ˆ˜์ • ํ•„์š” | ์ด๋ฒคํŠธ๋งŒ ๋ฐœ์†ก/์ˆ˜์‹ ํ•˜๋ฉด ๋จ | +| ํ…Œ์ŠคํŠธ | ์ปดํฌ๋„ŒํŠธ ๋ชจํ‚น ํ•„์š” | ์ด๋ฒคํŠธ ๋ฐœ์†ก์œผ๋กœ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ | +| ๋””๋ฒ„๊น… | ์‰ฌ์›€ | ์ด๋ฒคํŠธ ๋กœ๊น… ํ•„์š” | + +### 11.2 ํ™•์žฅ์„ฑ + +์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ ์‹œ: +1. ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ ์ˆ˜์ • ๋ถˆํ•„์š” +2. ์ƒˆ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ด๋ฒคํŠธ ๊ตฌ๋…๋งŒ ์ถ”๊ฐ€ +3. ์ด๋ฒคํŠธ ํŽ˜์ด๋กœ๋“œ ๊ตฌ์กฐ๋งŒ ์œ ์ง€ํ•˜๋ฉด ๋จ + +```typescript +// ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ์—์„œ ์Šค์ผ€์ค„ ์ƒ์„ฑ ์™„๋ฃŒ ์ด๋ฒคํŠธ ๊ตฌ๋… +useEffect(() => { + const unsubscribe = v2EventBus.on( + V2_EVENTS.SCHEDULE_GENERATE_COMPLETE, + (payload) => { + // ์ƒˆ๋กœ์šด ๋กœ์ง ์ถ”๊ฐ€ + console.log("์Šค์ผ€์ค„ ์ƒ์„ฑ ์™„๋ฃŒ:", payload); + } + ); + return unsubscribe; +}, []); +``` + +### 11.3 ๋””๋ฒ„๊น… ํŒ + +```typescript +// ์ด๋ฒคํŠธ ๋””๋ฒ„๊น…์šฉ ์ „์—ญ ๋ฆฌ์Šค๋„ˆ (๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ) +if (process.env.NODE_ENV === "development") { + v2EventBus.on("*", (event, payload) => { + console.log(`[EventBus] ${event}:`, payload); + }); +} +``` diff --git a/docs/screen-implementation-guide/00_analysis/v2-component-usage-guide.md b/docs/screen-implementation-guide/00_analysis/v2-component-usage-guide.md new file mode 100644 index 00000000..e32e68cc --- /dev/null +++ b/docs/screen-implementation-guide/00_analysis/v2-component-usage-guide.md @@ -0,0 +1,580 @@ +# V2 ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ + +> **๋ชฉ์ **: ๋‹ค์–‘ํ•œ ํšŒ์‚ฌ์—์„œ V2 ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํ™”๋ฉด์„ ๊ฐœ๋ฐœํ•  ๋•Œ ์ฐธ๊ณ ํ•˜๋Š” ๋ฒ”์šฉ ๊ฐ€์ด๋“œ +> **๋Œ€์ƒ**: ํ™”๋ฉด ์„ค๊ณ„์ž, ๊ฐœ๋ฐœ์ž +> **๋ฒ„์ „**: 1.0.0 +> **์ž‘์„ฑ์ผ**: 2026-01-30 + +--- + +## 1. V2 ์ปดํฌ๋„ŒํŠธ๋กœ ๊ฐ€๋Šฅํ•œ ๊ฒƒ / ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฒƒ + +### 1.1 ๊ฐ€๋Šฅํ•œ ํ™”๋ฉด ์œ ํ˜• + +| ํ™”๋ฉด ์œ ํ˜• | ์„ค๋ช… | ๋Œ€ํ‘œ ์˜ˆ์‹œ | +|-----------|------|----------| +| ๋งˆ์Šคํ„ฐ ๊ด€๋ฆฌ | ๋‹จ์ผ ํ…Œ์ด๋ธ” CRUD | ํšŒ์‚ฌ์ •๋ณด, ๋ถ€์„œ์ •๋ณด, ์ฝ”๋“œ๊ด€๋ฆฌ | +| ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ | ์ขŒ์ธก ์„ ํƒ โ†’ ์šฐ์ธก ์ƒ์„ธ | ๊ณต์ •๊ด€๋ฆฌ, ํ’ˆ๋ชฉ๋ผ์šฐํŒ…, ๊ฒฌ์ ๊ด€๋ฆฌ | +| ํƒญ ๊ธฐ๋ฐ˜ ํ™”๋ฉด | ํƒญ๋ณ„ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”/๋ทฐ | ๊ฒ€์‚ฌ์ •๋ณด๊ด€๋ฆฌ, ๊ฑฐ๋ž˜์ฒ˜๊ด€๋ฆฌ | +| ์นด๋“œ ๋ทฐ | ์ด๋ฏธ์ง€+์ •๋ณด ์นด๋“œ ํ˜•ํƒœ | ์„ค๋น„์ •๋ณด, ๋Œ€์‹œ๋ณด๋“œ | +| ํ”ผ๋ฒ— ๋ถ„์„ | ๋‹ค์ฐจ์› ์ง‘๊ณ„ | ๋งค์ถœ๋ถ„์„, ์žฌ๊ณ ํ˜„ํ™ฉ | +| ๋ฐ˜๋ณต ์ปจํ…Œ์ด๋„ˆ | ๋ฐ์ดํ„ฐ ์ˆ˜๋งŒํผ UI ๋ฐ˜๋ณต | ์ฃผ๋ฌธ ์ƒ์„ธ, ํ•ญ๋ชฉ ๋ฆฌ์ŠคํŠธ | + +### 1.2 ๋ถˆ๊ฐ€๋Šฅํ•œ ํ™”๋ฉด ์œ ํ˜• (๋ณ„๋„ ๊ฐœ๋ฐœ ํ•„์š”) + +| ํ™”๋ฉด ์œ ํ˜• | ์ด์œ  | ํ•ด๊ฒฐ ๋ฐฉ์•ˆ | +|-----------|------|----------| +| ๊ฐ„ํŠธ ์ฐจํŠธ / ํƒ€์ž„๋ผ์ธ | ์‹œ๊ฐ„์ถ• ๊ธฐ๋ฐ˜ UI ์—†์Œ | ๋ณ„๋„ ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ or ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ | +| ํŠธ๋ฆฌ ๋ทฐ (๊ณ„์ธต ๊ตฌ์กฐ) | ํŠธ๋ฆฌ ์ปดํฌ๋„ŒํŠธ ๋ฏธ์กด์žฌ | `v2-tree-view` ๊ฐœ๋ฐœ ํ•„์š” | +| ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” | ๊ทธ๋ฃนํ•‘ ๊ธฐ๋Šฅ ๋ฏธ์ง€์› | `v2-grouped-table` ๊ฐœ๋ฐœ ํ•„์š” | +| ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ๋ณด๋“œ | ์นธ๋ฐ˜ ์Šคํƒ€์ผ UI ์—†์Œ | ๋ณ„๋„ ๊ฐœ๋ฐœ | +| ๋ชจ๋ฐ”์ผ ์•ฑ ์Šคํƒ€์ผ | ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ UI | ๋ณ„๋„ ๊ฐœ๋ฐœ | +| ๋ณต์žกํ•œ ์ฐจํŠธ | ๊ธฐ๋ณธ ์ง‘๊ณ„ ์™ธ ์‹œ๊ฐํ™” | ์ฐจํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—ฐ๋™ | + +--- + +## 2. V2 ์ปดํฌ๋„ŒํŠธ ์ „์ฒด ๋ชฉ๋ก (23๊ฐœ) + +### 2.1 ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ (3๊ฐœ) + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | ์ฃผ์š” ์˜ต์…˜ | +|----|------|------|----------| +| `v2-input` | ์ž…๋ ฅ | ํ…์ŠคํŠธ, ์ˆซ์ž, ๋น„๋ฐ€๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ, URL, ์—ฌ๋Ÿฌ ์ค„ | inputType, required, readonly, maxLength | +| `v2-select` | ์„ ํƒ | ๋“œ๋กญ๋‹ค์šด, ์ฝค๋ณด๋ฐ•์Šค, ๋ผ๋””์˜ค, ์ฒดํฌ๋ฐ•์Šค | mode, source(distinct/static/code/entity), multiple | +| `v2-date` | ๋‚ ์งœ | ๋‚ ์งœ, ์‹œ๊ฐ„, ๋‚ ์งœ์‹œ๊ฐ„, ๋‚ ์งœ๋ฒ”์œ„, ์›”, ์—ฐ๋„ | dateType, format, showTime | + +### 2.2 ํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ (3๊ฐœ) + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | ์ฃผ์š” ์˜ต์…˜ | +|----|------|------|----------| +| `v2-text-display` | ํ…์ŠคํŠธ ํ‘œ์‹œ | ๋ผ๋ฒจ, ์ œ๋ชฉ, ์„ค๋ช… ํ…์ŠคํŠธ | fontSize, fontWeight, color, textAlign | +| `v2-card-display` | ์นด๋“œ ๋””์Šคํ”Œ๋ ˆ์ด | ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ๋ฅผ ์นด๋“œ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ | cardsPerRow, showImage, columnMapping | +| `v2-aggregation-widget` | ์ง‘๊ณ„ ์œ„์ ฏ | ํ•ฉ๊ณ„, ํ‰๊ท , ๊ฐœ์ˆ˜, ์ตœ๋Œ€, ์ตœ์†Œ | items, filters, layout | + +### 2.3 ํ…Œ์ด๋ธ”/๋ฐ์ดํ„ฐ ์ปดํฌ๋„ŒํŠธ (3๊ฐœ) + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | ์ฃผ์š” ์˜ต์…˜ | +|----|------|------|----------| +| `v2-table-list` | ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | ๋ฐ์ดํ„ฐ ์กฐํšŒ/ํŽธ์ง‘ ํ…Œ์ด๋ธ” | selectedTable, columns, pagination, filter | +| `v2-table-search-widget` | ๊ฒ€์ƒ‰ ํ•„ํ„ฐ | ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰/ํ•„ํ„ฐ/๊ทธ๋ฃน | autoSelectFirstTable, showTableSelector | +| `v2-pivot-grid` | ํ”ผ๋ฒ— ๊ทธ๋ฆฌ๋“œ | ๋‹ค์ฐจ์› ๋ถ„์„ (ํ–‰/์—ด/๋ฐ์ดํ„ฐ ์˜์—ญ) | fields, totals, aggregation | + +### 2.4 ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ (8๊ฐœ) + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | ์ฃผ์š” ์˜ต์…˜ | +|----|------|------|----------| +| `v2-split-panel-layout` | ๋ถ„ํ•  ํŒจ๋„ | ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ์ขŒ์šฐ ๋ถ„ํ•  | splitRatio, resizable, relation, **displayMode: custom** | +| `v2-tabs-widget` | ํƒญ ์œ„์ ฏ | ํƒญ ์ „ํ™˜, ํƒญ ๋‚ด ์ปดํฌ๋„ŒํŠธ | tabs, activeTabId | +| `v2-section-card` | ์„น์…˜ ์นด๋“œ | ์ œ๋ชฉ+ํ…Œ๋‘๋ฆฌ ๊ทธ๋ฃนํ™” | title, collapsible, padding | +| `v2-section-paper` | ์„น์…˜ ํŽ˜์ดํผ | ๋ฐฐ๊ฒฝ์ƒ‰ ๊ทธ๋ฃนํ™” | backgroundColor, padding, shadow | +| `v2-divider-line` | ๊ตฌ๋ถ„์„  | ์˜์—ญ ๊ตฌ๋ถ„ | orientation, thickness | +| `v2-repeat-container` | ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ | ๋ฐ์ดํ„ฐ ์ˆ˜๋งŒํผ ๋ฐ˜๋ณต ๋ Œ๋”๋ง | dataSourceType, layout, gridColumns | +| `v2-repeater` | ๋ฆฌํ”ผํ„ฐ | ๋ฐ˜๋ณต ์ปจํŠธ๋กค | - | +| `v2-repeat-screen-modal` | ๋ฐ˜๋ณต ํ™”๋ฉด ๋ชจ๋‹ฌ | ๋ชจ๋‹ฌ ๋ฐ˜๋ณต | - | + +### 2.5 ์•ก์…˜/ํŠน์ˆ˜ ์ปดํฌ๋„ŒํŠธ (6๊ฐœ) + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | ์ฃผ์š” ์˜ต์…˜ | +|----|------|------|----------| +| `v2-button-primary` | ๊ธฐ๋ณธ ๋ฒ„ํŠผ | ์ €์žฅ, ์‚ญ์ œ ๋“ฑ ์•ก์…˜ | text, actionType, variant | +| `v2-numbering-rule` | ์ฑ„๋ฒˆ๊ทœ์น™ | ์ž๋™ ์ฝ”๋“œ/๋ฒˆํ˜ธ ์ƒ์„ฑ | rule, prefix, format | +| `v2-category-manager` | ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ์ž | ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ UI | - | +| `v2-location-swap-selector` | ์œ„์น˜ ๊ตํ™˜ | ์œ„์น˜ ์„ ํƒ/๊ตํ™˜ | - | +| `v2-rack-structure` | ๋ž™ ๊ตฌ์กฐ | ์ฐฝ๊ณ  ๋ž™ ์‹œ๊ฐํ™” | - | +| `v2-media` | ๋ฏธ๋””์–ด | ์ด๋ฏธ์ง€/๋™์˜์ƒ ํ‘œ์‹œ | - | + +--- + +## 3. ํ™”๋ฉด ํŒจํ„ด๋ณ„ ์ปดํฌ๋„ŒํŠธ ์กฐํ•ฉ + +### 3.1 ํŒจํ„ด A: ๊ธฐ๋ณธ ๋งˆ์Šคํ„ฐ ํ™”๋ฉด (๊ฐ€์žฅ ํ”ํ•จ) + +**์ ์šฉ ํ™”๋ฉด**: ์ฝ”๋“œ๊ด€๋ฆฌ, ์‚ฌ์šฉ์ž๊ด€๋ฆฌ, ๋ถ€์„œ์ •๋ณด, ์ฐฝ๊ณ ์ •๋ณด ๋“ฑ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-table-search-widget โ”‚ +โ”‚ [๊ฒ€์ƒ‰ํ•„๋“œ1] [๊ฒ€์ƒ‰ํ•„๋“œ2] [์กฐํšŒ] [์—‘์…€] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ v2-table-list โ”‚ +โ”‚ ์ œ๋ชฉ [์‹ ๊ทœ] [์‚ญ์ œ] โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ โ–ก | ์ฝ”๋“œ | ์ด๋ฆ„ | ์ƒํƒœ | ๋“ฑ๋ก์ผ | โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**ํ•„์ˆ˜ ์ปดํฌ๋„ŒํŠธ**: +- `v2-table-search-widget` (1๊ฐœ) +- `v2-table-list` (1๊ฐœ) + +**์„ค์ • ํฌ์ธํŠธ**: +- ํ…Œ์ด๋ธ”๋ช… ์ง€์ • +- ๊ฒ€์ƒ‰ ๋Œ€์ƒ ์ปฌ๋Ÿผ ์„ค์ • +- ์ปฌ๋Ÿผ ํ‘œ์‹œ/์ˆจ๊น€ ์„ค์ • + +--- + +### 3.2 ํŒจํ„ด B: ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ํ™”๋ฉด + +**์ ์šฉ ํ™”๋ฉด**: ๊ณต์ •๊ด€๋ฆฌ, ๊ฒฌ์ ๊ด€๋ฆฌ, ์ˆ˜์ฃผ๊ด€๋ฆฌ, ํ’ˆ๋ชฉ๋ผ์šฐํŒ… ๋“ฑ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-table-list โ”‚ v2-table-list ๋˜๋Š” ํผ โ”‚ +โ”‚ (๋งˆ์Šคํ„ฐ) โ”‚ (๋””ํ…Œ์ผ) โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”‚ +โ”‚ โ–ก A001 ํ•ญ๋ชฉ1 โ”‚ [์ƒ์„ธ ์ •๋ณด] โ”‚ +โ”‚ โ–ก A002 ํ•ญ๋ชฉ2 โ† โ”‚ โ”‚ +โ”‚ โ–ก A003 ํ•ญ๋ชฉ3 โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + v2-split-panel-layout +``` + +**ํ•„์ˆ˜ ์ปดํฌ๋„ŒํŠธ**: +- `v2-split-panel-layout` (1๊ฐœ) +- `v2-table-list` (2๊ฐœ: ๋งˆ์Šคํ„ฐ, ๋””ํ…Œ์ผ) + +**์„ค์ • ํฌ์ธํŠธ**: +- `splitRatio`: ์ขŒ์šฐ ๋น„์œจ (๊ธฐ๋ณธ 30:70) +- `relation.type`: join / detail / custom +- `relation.foreignKey`: ์—ฐ๊ฒฐ ํ‚ค ์ปฌ๋Ÿผ + +--- + +### 3.3 ํŒจํ„ด C: ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ + ํƒญ + +**์ ์šฉ ํ™”๋ฉด**: ๊ฑฐ๋ž˜์ฒ˜๊ด€๋ฆฌ, ํ’ˆ๋ชฉ์ •๋ณด, ์„ค๋น„์ •๋ณด ๋“ฑ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-table-list โ”‚ v2-tabs-widget โ”‚ +โ”‚ (๋งˆ์Šคํ„ฐ) โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ”‚๊ธฐ๋ณธโ”‚์ด๋ ฅโ”‚์ฒจ๋ถ€โ”‚ โ”‚ +โ”‚ โ–ก A001 ๊ฑฐ๋ž˜์ฒ˜1 โ”‚ โ””โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ–ก A002 ๊ฑฐ๋ž˜์ฒ˜2 โ† โ”‚ [ํƒญ๋ณ„ ์ปจํ…์ธ ] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**ํ•„์ˆ˜ ์ปดํฌ๋„ŒํŠธ**: +- `v2-split-panel-layout` (1๊ฐœ) +- `v2-table-list` (1๊ฐœ: ๋งˆ์Šคํ„ฐ) +- `v2-tabs-widget` (1๊ฐœ) + +**์„ค์ • ํฌ์ธํŠธ**: +- ํƒญ๋ณ„ ํ‘œ์‹œํ•  ํ…Œ์ด๋ธ”/ํผ ์„ค์ • +- ๋งˆ์Šคํ„ฐ ์„ ํƒ ์‹œ ํƒญ ์ปจํ…์ธ  ์—ฐ๋™ + +--- + +### 3.4 ํŒจํ„ด D: ์นด๋“œ ๋ทฐ + +**์ ์šฉ ํ™”๋ฉด**: ์„ค๋น„์ •๋ณด, ๋Œ€์‹œ๋ณด๋“œ, ์ƒํ’ˆ ์นดํƒˆ๋กœ๊ทธ ๋“ฑ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-table-search-widget โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ v2-card-display โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [์ด๋ฏธ์ง€]โ”‚ โ”‚ [์ด๋ฏธ์ง€]โ”‚ โ”‚ [์ด๋ฏธ์ง€]โ”‚ โ”‚ +โ”‚ โ”‚ ์ œ๋ชฉ โ”‚ โ”‚ ์ œ๋ชฉ โ”‚ โ”‚ ์ œ๋ชฉ โ”‚ โ”‚ +โ”‚ โ”‚ ์„ค๋ช… โ”‚ โ”‚ ์„ค๋ช… โ”‚ โ”‚ ์„ค๋ช… โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**ํ•„์ˆ˜ ์ปดํฌ๋„ŒํŠธ**: +- `v2-table-search-widget` (1๊ฐœ) +- `v2-card-display` (1๊ฐœ) + +**์„ค์ • ํฌ์ธํŠธ**: +- `cardsPerRow`: ํ•œ ํ–‰๋‹น ์นด๋“œ ์ˆ˜ +- `columnMapping`: ์ œ๋ชฉ, ๋ถ€์ œ๋ชฉ, ์ด๋ฏธ์ง€, ์ƒํƒœ ํ•„๋“œ ๋งคํ•‘ +- `cardStyle`: ์ด๋ฏธ์ง€ ์œ„์น˜, ํฌ๊ธฐ + +--- + +### 3.5 ํŒจํ„ด E: ํ”ผ๋ฒ— ๋ถ„์„ + +**์ ์šฉ ํ™”๋ฉด**: ๋งค์ถœ๋ถ„์„, ์žฌ๊ณ ํ˜„ํ™ฉ, ์ƒ์‚ฐ์‹ค์  ๋ถ„์„ ๋“ฑ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-pivot-grid โ”‚ +โ”‚ โ”‚ 2024๋…„ โ”‚ 2025๋…„ โ”‚ 2026๋…„ โ”‚ ํ•ฉ๊ณ„ โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ ์ง€์—ญA โ”‚ 1,000 โ”‚ 1,200 โ”‚ 1,500 โ”‚ 3,700 โ”‚ +โ”‚ ์ง€์—ญB โ”‚ 2,000 โ”‚ 2,500 โ”‚ 3,000 โ”‚ 7,500 โ”‚ +โ”‚ ํ•ฉ๊ณ„ โ”‚ 3,000 โ”‚ 3,700 โ”‚ 4,500 โ”‚ 11,200 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**ํ•„์ˆ˜ ์ปดํฌ๋„ŒํŠธ**: +- `v2-pivot-grid` (1๊ฐœ) + +**์„ค์ • ํฌ์ธํŠธ**: +- `fields[].area`: row / column / data / filter +- `fields[].summaryType`: sum / avg / count / min / max +- `fields[].groupInterval`: ๋‚ ์งœ ๊ทธ๋ฃนํ™” (year/quarter/month) + +--- + +## 4. ํšŒ์‚ฌ๋ณ„ ๊ฐœ๋ฐœ ์‹œ ํ•ต์‹ฌ ์ฒดํฌํฌ์ธํŠธ + +### 4.1 ํ…Œ์ด๋ธ” ์„ค๊ณ„ ํ™•์ธ + +**๊ฐ€์žฅ ๋จผ์ € ํ™•์ธ**: +1. ํšŒ์‚ฌ์—์„œ ์‚ฌ์šฉํ•  ํ…Œ์ด๋ธ” ๋ชฉ๋ก +2. ํ…Œ์ด๋ธ” ๊ฐ„ ๊ด€๊ณ„ (FK) +3. ์กฐํšŒ ์กฐ๊ฑด์œผ๋กœ ์“ธ ์ปฌ๋Ÿผ + +``` +โœ… ์ฒดํฌ๋ฆฌ์ŠคํŠธ: +โ–ก ํ…Œ์ด๋ธ”๋ช…์ด DB์— ์กด์žฌํ•˜๋Š”๊ฐ€? +โ–ก company_code ์ปฌ๋Ÿผ์ด ์žˆ๋Š”๊ฐ€? (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ) +โ–ก ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ๊ด€๊ณ„์˜ FK๊ฐ€ ์ •์˜๋˜์–ด ์žˆ๋Š”๊ฐ€? +โ–ก ๊ฒ€์ƒ‰ ๋Œ€์ƒ ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค๊ฐ€ ์žˆ๋Š”๊ฐ€? +``` + +### 4.2 ํ™”๋ฉด ํŒจํ„ด ํŒ๋‹จ + +**์งˆ๋ฌธ์„ ํ†ตํ•œ ํŒ๋‹จ**: + +| ์งˆ๋ฌธ | ์˜ˆ โ†’ ํŒจํ„ด | +|------|----------| +| ๋‹จ์ผ ํ…Œ์ด๋ธ”๋งŒ ์กฐํšŒ/ํŽธ์ง‘? | ํŒจํ„ด A (๊ธฐ๋ณธ ๋งˆ์Šคํ„ฐ) | +| ๋งˆ์Šคํ„ฐ ์„ ํƒ ์‹œ ๋””ํ…Œ์ผ ํ‘œ์‹œ? | ํŒจํ„ด B (๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ) | +| ์ƒ์„ธ์— ํƒญ์ด ํ•„์š”? | ํŒจํ„ด C (๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ+ํƒญ) | +| ์ด๋ฏธ์ง€+์ •๋ณด ์นด๋“œ ํ˜•ํƒœ? | ํŒจํ„ด D (์นด๋“œ ๋ทฐ) | +| ๋‹ค์ฐจ์› ์ง‘๊ณ„/๋ถ„์„? | ํŒจํ„ด E (ํ”ผ๋ฒ—) | + +### 4.3 ์ปดํฌ๋„ŒํŠธ ์„ค์ • ํ•„์ˆ˜ ํ•ญ๋ชฉ + +#### v2-table-list ํ•„์ˆ˜ ์„ค์ • + +```typescript +{ + selectedTable: "ํ…Œ์ด๋ธ”๋ช…", // ํ•„์ˆ˜ + columns: [ // ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ + { columnName: "id", displayName: "ID", visible: true, sortable: true }, + // ... + ], + pagination: { + enabled: true, + pageSize: 20 + } +} +``` + +#### v2-split-panel-layout ํ•„์ˆ˜ ์„ค์ • + +```typescript +{ + leftPanel: { + tableName: "๋งˆ์Šคํ„ฐ_ํ…Œ์ด๋ธ”๋ช…" + }, + rightPanel: { + tableName: "๋””ํ…Œ์ผ_ํ…Œ์ด๋ธ”๋ช…", + relation: { + type: "detail", // join | detail | custom + foreignKey: "master_id" // ์—ฐ๊ฒฐ ํ‚ค + } + }, + splitRatio: 30 // ์ขŒ์ธก ๋น„์œจ +} +``` + +#### v2-split-panel-layout ์ปค์Šคํ…€ ๋ชจ๋“œ (NEW) + +ํŒจ๋„ ๋‚ด๋ถ€์— ์ž์œ ๋กญ๊ฒŒ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐฐ์น˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (v2-tabs-widget๊ณผ ๋™์ผ ๊ตฌ์กฐ) + +```typescript +{ + leftPanel: { + displayMode: "custom", // ์ปค์Šคํ…€ ๋ชจ๋“œ ํ™œ์„ฑํ™” + components: [ // ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์—ด + { + id: "btn-save", + componentType: "v2-button-primary", + label: "์ €์žฅ", + position: { x: 10, y: 10 }, + size: { width: 100, height: 40 }, + componentConfig: { buttonAction: "save" } + }, + { + id: "tbl-list", + componentType: "v2-table-list", + label: "๋ชฉ๋ก", + position: { x: 10, y: 60 }, + size: { width: 400, height: 300 }, + componentConfig: { selectedTable: "ํ…Œ์ด๋ธ”๋ช…" } + } + ] + }, + rightPanel: { + displayMode: "table" // ๊ธฐ์กด ๋ชจ๋“œ ์œ ์ง€ + } +} +``` + +**๋””์ž์ธ ๋ชจ๋“œ ๊ธฐ๋Šฅ**: +- ์ปดํฌ๋„ŒํŠธ ํด๋ฆญ โ†’ ์ขŒ์ธก ์„ค์ • ํŒจ๋„์—์„œ ์†์„ฑ ํŽธ์ง‘ +- ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค(์ƒ๋‹จ)๋กœ ์ด๋™ +- ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค(๋ชจ์„œ๋ฆฌ)๋กœ ํฌ๊ธฐ ์กฐ์ ˆ +- ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ Œ๋”๋ง + +#### v2-card-display ํ•„์ˆ˜ ์„ค์ • + +```typescript +{ + dataSource: "table", + columnMapping: { + title: "name", // ์ œ๋ชฉ ํ•„๋“œ + subtitle: "code", // ๋ถ€์ œ๋ชฉ ํ•„๋“œ + image: "image_url", // ์ด๋ฏธ์ง€ ํ•„๋“œ (์„ ํƒ) + status: "status" // ์ƒํƒœ ํ•„๋“œ (์„ ํƒ) + }, + cardsPerRow: 3 +} +``` + +--- + +## 5. ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ ํ•œ๊ณ„์  + +### 5.1 ํ˜„์žฌ ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ธฐ๋Šฅ + +| ๊ธฐ๋Šฅ | ์ƒํƒœ | ๋Œ€์•ˆ | +|------|------|------| +| ํŠธ๋ฆฌ ๋ทฐ (BOM, ์กฐ์ง๋„) | โŒ ๋ฏธ์ง€์› | ํ…Œ์ด๋ธ”๋กœ ๋Œ€์ฒด or ๋ณ„๋„ ๊ฐœ๋ฐœ | +| ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” | โŒ ๋ฏธ์ง€์› | ์ผ๋ฐ˜ ํ…Œ์ด๋ธ”๋กœ ๋Œ€์ฒด or ๋ณ„๋„ ๊ฐœ๋ฐœ | +| ๊ฐ„ํŠธ ์ฐจํŠธ | โŒ ๋ฏธ์ง€์› | ๋ณ„๋„ ๊ฐœ๋ฐœ ํ•„์š” | +| ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ์ •๋ ฌ | โŒ ๋ฏธ์ง€์› | ์ˆœ์„œ ์ปฌ๋Ÿผ์œผ๋กœ ๋Œ€์ฒด | +| ์ธ๋ผ์ธ ํŽธ์ง‘ | โš ๏ธ ์ œํ•œ์  | ๋ชจ๋‹ฌ ํŽธ์ง‘์œผ๋กœ ๋Œ€์ฒด | +| ๋ณต์žกํ•œ ์ฐจํŠธ | โŒ ๋ฏธ์ง€์› | ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—ฐ๋™ | + +### 5.2 ๊ถŒ์žฅํ•˜์ง€ ์•Š๋Š” ์กฐํ•ฉ + +| ์กฐํ•ฉ | ์ด์œ  | +|------|------| +| 3๋‹จ๊ณ„ ์ด์ƒ ์ค‘์ฒฉ ๋ถ„ํ•  | ํ™”๋ฉด ๋ณต์žก๋„ ์ฆ๊ฐ€, ์„ฑ๋Šฅ ์ €ํ•˜ | +| ํƒญ ์•ˆ์— ํƒญ | ์‚ฌ์šฉ์„ฑ ์ €ํ•˜ | +| ํ•œ ํ™”๋ฉด์— 3๊ฐœ ์ด์ƒ ํ…Œ์ด๋ธ” | ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์„ฑ๋Šฅ | +| ํ”ผ๋ฒ— + ์ƒ์„ธ ํ…Œ์ด๋ธ” ๋™์‹œ | ๋ฐ์ดํ„ฐ ๊ณผ๋ถ€ํ•˜ | + +--- + +## 6. ์ œ์–ด๊ด€๋ฆฌ (๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง) - ๋ณ„๋„ ์„ค์ • ํ•„์ˆ˜ + +> **ํ•ต์‹ฌ**: V2 ์ปดํฌ๋„ŒํŠธ๋Š” **UI๋งŒ ๋‹ด๋‹น**ํ•ฉ๋‹ˆ๋‹ค. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ **์ œ์–ด๊ด€๋ฆฌ**์—์„œ ๋ณ„๋„ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + +### 6.1 UI vs ์ œ์–ด ๋ถ„๋ฆฌ ๊ตฌ์กฐ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ํ™”๋ฉด ๊ตฌ์„ฑ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ UI ๋ ˆ์ด์•„์›ƒ โ”‚ ์ œ์–ด๊ด€๋ฆฌ โ”‚ +โ”‚ (screen_layouts_v2) โ”‚ (dataflow_diagrams) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ€ข ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ โ”‚ โ€ข ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์•ก์…˜ โ”‚ +โ”‚ โ€ข ๊ฒ€์ƒ‰ ํ•„๋“œ ๊ตฌ์„ฑ โ”‚ โ€ข INSERT/UPDATE/DELETE ์„ค์ • โ”‚ +โ”‚ โ€ข ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ํ‘œ์‹œ โ”‚ โ€ข ์กฐ๊ฑด๋ถ€ ์‹คํ–‰ โ”‚ +โ”‚ โ€ข ์นด๋“œ/ํƒญ ๋ ˆ์ด์•„์›ƒ โ”‚ โ€ข ๋‹ค์ค‘ ํ–‰ ์ฒ˜๋ฆฌ โ”‚ +โ”‚ โ”‚ โ€ข ํ…Œ์ด๋ธ” ๊ฐ„ ๋ฐ์ดํ„ฐ ์ด๋™ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 6.2 HTML์—์„œ ํŒŒ์•… ๊ฐ€๋Šฅ/๋ถˆ๊ฐ€๋Šฅ + +| ๊ตฌ๋ถ„ | HTML์—์„œ ํŒŒ์•… | ์ด์œ  | +|------|--------------|------| +| ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ | โœ… ๊ฐ€๋Šฅ | HTML ๊ตฌ์กฐ์—์„œ ๋ณด์ž„ | +| ๊ฒ€์ƒ‰ ํ•„๋“œ | โœ… ๊ฐ€๋Šฅ | input ํƒœ๊ทธ๋กœ ํ™•์ธ | +| ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ | โœ… ๊ฐ€๋Šฅ | thead์—์„œ ํ™•์ธ | +| **์ €์žฅ ํ…Œ์ด๋ธ”** | โŒ ๋ถˆ๊ฐ€๋Šฅ | JS/๋ฐฑ์—”๋“œ์—์„œ ์ฒ˜๋ฆฌ | +| **๋ฒ„ํŠผ ์•ก์…˜** | โŒ ๋ถˆ๊ฐ€๋Šฅ | ์ œ์–ด๊ด€๋ฆฌ์—์„œ ์„ค์ • | +| **์ „/ํ›„ ์ฒ˜๋ฆฌ** | โŒ ๋ถˆ๊ฐ€๋Šฅ | ์ œ์–ด๊ด€๋ฆฌ์—์„œ ์„ค์ • | +| **๋‹ค์ค‘ ํ–‰ ์ฒ˜๋ฆฌ** | โŒ ๋ถˆ๊ฐ€๋Šฅ | ์ œ์–ด๊ด€๋ฆฌ์—์„œ ์„ค์ • | +| **ํ…Œ์ด๋ธ” ๊ฐ„ ๊ด€๊ณ„** | โŒ ๋ถˆ๊ฐ€๋Šฅ | DB/์ œ์–ด๊ด€๋ฆฌ์—์„œ ์„ค์ • | + +### 6.3 ์ œ์–ด๊ด€๋ฆฌ ์„ค์ • ํ•ญ๋ชฉ + +#### ํŠธ๋ฆฌ๊ฑฐ ํƒ€์ž… +- **๋ฒ„ํŠผ ํด๋ฆญ ์ „ (before)**: ํด๋ฆญ ์ง์ „ ์‹คํ–‰ +- **๋ฒ„ํŠผ ํด๋ฆญ ํ›„ (after)**: ํด๋ฆญ ์™„๋ฃŒ ํ›„ ์‹คํ–‰ + +#### ์•ก์…˜ ํƒ€์ž… +- **INSERT**: ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ์‚ฝ์ž… +- **UPDATE**: ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์ˆ˜์ • +- **DELETE**: ๋ฐ์ดํ„ฐ ์‚ญ์ œ + +#### ์กฐ๊ฑด ์„ค์ • +```typescript +// ์˜ˆ: ์„ ํƒ๋œ ํ–‰์˜ ์ƒํƒœ๊ฐ€ '๋Œ€๊ธฐ'์ธ ๊ฒฝ์šฐ์—๋งŒ ์‹คํ–‰ +{ + field: "status", + operator: "=", + value: "๋Œ€๊ธฐ", + dataType: "string" +} +``` + +#### ํ•„๋“œ ๋งคํ•‘ +```typescript +// ์˜ˆ: ์†Œ์Šค ํ…Œ์ด๋ธ”์˜ ๊ฐ’์„ ํƒ€๊ฒŸ ํ…Œ์ด๋ธ”๋กœ ์ด๋™ +{ + sourceTable: "order_master", + sourceField: "order_no", + targetTable: "order_history", + targetField: "order_no" +} +``` + +### 6.4 ์ œ์–ด๊ด€๋ฆฌ ์˜ˆ์‹œ: ์ˆ˜์ฃผ ํ™•์ • ๋ฒ„ํŠผ + +**์‹œ๋‚˜๋ฆฌ์˜ค**: ์ˆ˜์ฃผ ๋ชฉ๋ก์—์„œ 3๊ฑด ์„ ํƒ ํ›„ [ํ™•์ •] ๋ฒ„ํŠผ ํด๋ฆญ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [ํ™•์ •] ๋ฒ„ํŠผ ํด๋ฆญ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ 1. ์กฐ๊ฑด ์ฒดํฌ: status = '๋Œ€๊ธฐ' ์ธ ํ–‰๋งŒ โ”‚ +โ”‚ 2. UPDATE order_master SET status = 'ํ™•์ •' WHERE id IN (์„ ํƒ) โ”‚ +โ”‚ 3. INSERT order_history (์ˆ˜์ฃผ์ด๋ ฅ ํ…Œ์ด๋ธ”์— ๊ธฐ๋ก) โ”‚ +โ”‚ 4. ์™ธ๋ถ€ ์‹œ์Šคํ…œ ํ˜ธ์ถœ (ERP ์—ฐ๋™) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ œ์–ด๊ด€๋ฆฌ ์„ค์ •**: +```json +{ + "triggerType": "after", + "actions": [ + { + "actionType": "update", + "targetTable": "order_master", + "conditions": [{ "field": "status", "operator": "=", "value": "๋Œ€๊ธฐ" }], + "fieldMappings": [{ "targetField": "status", "defaultValue": "ํ™•์ •" }] + }, + { + "actionType": "insert", + "targetTable": "order_history", + "fieldMappings": [ + { "sourceField": "order_no", "targetField": "order_no" }, + { "sourceField": "customer_name", "targetField": "customer_name" } + ] + } + ] +} +``` + +### 6.5 ํšŒ์‚ฌ๋ณ„ ๊ฐœ๋ฐœ ์‹œ ์ œ์–ด๊ด€๋ฆฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +``` +โ–ก ๋ฒ„ํŠผ๋ณ„ ์•ก์…˜ ์ •์˜ + - ์–ด๋–ค ๋ฒ„ํŠผ์ด ์žˆ๋Š”๊ฐ€? + - ๊ฐ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋ฌด์Šจ ๋™์ž‘? + +โ–ก ์ €์žฅ/์ˆ˜์ •/์‚ญ์ œ ๋Œ€์ƒ ํ…Œ์ด๋ธ” + - ๋ฉ”์ธ ํ…Œ์ด๋ธ”์€? + - ์ด๋ ฅ ํ…Œ์ด๋ธ”์€? + - ์—ฐ๊ด€ ํ…Œ์ด๋ธ”์€? + +โ–ก ์กฐ๊ฑด๋ถ€ ์‹คํ–‰ + - ํŠน์ • ์ƒํƒœ์ผ ๋•Œ๋งŒ ์‹คํ–‰? + - ํŠน์ • ๊ฐ’ ์ฒดํฌ ํ•„์š”? + +โ–ก ๋‹ค์ค‘ ํ–‰ ์ฒ˜๋ฆฌ + - ์—ฌ๋Ÿฌ ํ–‰ ์„ ํƒ ํ›„ ์ผ๊ด„ ์ฒ˜๋ฆฌ? + - ๊ฐ ํ–‰๋ณ„ ๊ฐœ๋ณ„ ์ฒ˜๋ฆฌ? + +โ–ก ์™ธ๋ถ€ ์—ฐ๋™ + - ERP/MES ๋“ฑ ์™ธ๋ถ€ ์‹œ์Šคํ…œ ํ˜ธ์ถœ? + - API ์—ฐ๋™ ํ•„์š”? +``` + +--- + +## 7. ํšŒ์‚ฌ๋ณ„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์˜์—ญ + +### 7.1 ์ปดํฌ๋„ŒํŠธ๋กœ ์ฒ˜๋ฆฌ๋˜๋Š” ์˜์—ญ (ํ‘œ์ค€ํ™”) + +| ์˜์—ญ | ์„ค๋ช… | +|------|------| +| UI ๋ ˆ์ด์•„์›ƒ | ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜, ํฌ๊ธฐ, ์œ„์น˜ | +| ๊ฒ€์ƒ‰ ์กฐ๊ฑด | ํ™”๋ฉด ๋””์ž์ด๋„ˆ์—์„œ ์„ค์ • | +| ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ | ํ‘œ์‹œ/์ˆจ๊น€, ์ˆœ์„œ, ๋„ˆ๋น„ | +| ๊ธฐ๋ณธ CRUD | ์กฐํšŒ, ์ €์žฅ, ์‚ญ์ œ ์ž๋™ ์ฒ˜๋ฆฌ | +| ํŽ˜์ด์ง€๋„ค์ด์…˜ | ์ž๋™ ์ฒ˜๋ฆฌ | +| ์ •๋ ฌ/ํ•„ํ„ฐ | ์ž๋™ ์ฒ˜๋ฆฌ | + +### 7.2 ํšŒ์‚ฌ๋ณ„ ๊ฐœ๋ฐœ ํ•„์š” ์˜์—ญ + +| ์˜์—ญ | ์„ค๋ช… | ๊ฐœ๋ฐœ ๋ฐฉ๋ฒ• | +|------|------|----------| +| ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง | ์ €์žฅ ์ „/ํ›„ ๊ฒ€์ฆ, ๊ณ„์‚ฐ | ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ๋˜๋Š” ๋ฐฑ์—”๋“œ API | +| ํŠน์ˆ˜ UI | ๊ฐ„ํŠธ, ํŠธ๋ฆฌ, ์ฐจํŠธ ๋“ฑ | ๋ณ„๋„ ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ | +| ์™ธ๋ถ€ ์—ฐ๋™ | ERP, MES ๋“ฑ ์—ฐ๊ณ„ | ์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • | +| ๋ฆฌํฌํŠธ/์ธ์‡„ | ์ „ํ‘œ, ๋ผ๋ฒจ ์ถœ๋ ฅ | ๋ฆฌํฌํŠธ ์ปดํฌ๋„ŒํŠธ | +| ๊ฒฐ์žฌ ํ”„๋กœ์„ธ์Šค | ์Šน์ธ/๋ฐ˜๋ ค ํ๋ฆ„ | ์›Œํฌํ”Œ๋กœ์šฐ ์„ค์ • | + +--- + +## 8. ๋น ๋ฅธ ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ + +### Step 1: ํ™”๋ฉด ๋ถ„์„ +1. ์–ด๋–ค ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•˜๋Š”๊ฐ€? +2. ํ…Œ์ด๋ธ” ๊ฐ„ ๊ด€๊ณ„๋Š”? +3. ์–ด๋–ค ํŒจํ„ด์ธ๊ฐ€? (A/B/C/D/E) + +### Step 2: ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ +1. ํ™”๋ฉด ๋””์ž์ด๋„ˆ์—์„œ ํŒจํ„ด์— ๋งž๋Š” ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ +2. ๊ฐ ์ปดํฌ๋„ŒํŠธ์— ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ ์„ค์ • + +### Step 3: ์—ฐ๋™ ์„ค์ • +1. ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ๊ด€๊ณ„ ์„ค์ • (FK) +2. ๊ฒ€์ƒ‰ ์กฐ๊ฑด ์„ค์ • +3. ๋ฒ„ํŠผ ์•ก์…˜ ์„ค์ • + +### Step 4: ํ…Œ์ŠคํŠธ +1. ๋ฐ์ดํ„ฐ ์กฐํšŒ ํ™•์ธ +2. ๋งˆ์Šคํ„ฐ ์„ ํƒ ์‹œ ๋””ํ…Œ์ผ ์—ฐ๋™ ํ™•์ธ +3. ์ €์žฅ/์‚ญ์ œ ๋™์ž‘ ํ™•์ธ + +--- + +## 9. ์š”์•ฝ + +### V2 ์ปดํฌ๋„ŒํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ + +| ํ™”๋ฉด ์œ ํ˜• | ์ง€์› ์—ฌ๋ถ€ | ์ฃผ์š” ์ปดํฌ๋„ŒํŠธ | +|-----------|----------|--------------| +| ๊ธฐ๋ณธ CRUD | โœ… ์™„์ „ | v2-table-list, v2-table-search-widget | +| ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ | โœ… ์™„์ „ | v2-split-panel-layout | +| ํƒญ ํ™”๋ฉด | โœ… ์™„์ „ | v2-tabs-widget | +| ์นด๋“œ ๋ทฐ | โœ… ์™„์ „ | v2-card-display | +| ํ”ผ๋ฒ— ๋ถ„์„ | โœ… ์™„์ „ | v2-pivot-grid | +| ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” | โŒ ๋ฏธ์ง€์› | ๊ฐœ๋ฐœ ํ•„์š” | +| ํŠธ๋ฆฌ ๋ทฐ | โŒ ๋ฏธ์ง€์› | ๊ฐœ๋ฐœ ํ•„์š” | +| ๊ฐ„ํŠธ ์ฐจํŠธ | โŒ ๋ฏธ์ง€์› | ๊ฐœ๋ฐœ ํ•„์š” | + +### ๊ฐœ๋ฐœ ์‹œ ํ•ต์‹ฌ ์›์น™ + +1. **ํ…Œ์ด๋ธ” ๋จผ์ €**: DB ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ํ™•์ธ์ด ์ตœ์šฐ์„  +2. **ํŒจํ„ด ํŒ๋‹จ**: 5๊ฐ€์ง€ ํŒจํ„ด ์ค‘ ์–ด๋””์— ํ•ด๋‹นํ•˜๋Š”์ง€ ํŒ๋‹จ +3. **ํ‘œ์ค€ ์กฐํ•ฉ**: ๊ฒ€์ฆ๋œ ์ปดํฌ๋„ŒํŠธ ์กฐํ•ฉ ์‚ฌ์šฉ +4. **ํ•œ๊ณ„ ์ธ์‹**: ๋ถˆ๊ฐ€๋Šฅํ•œ UI๋Š” ์กฐ๊ธฐ์— ์‹๋ณ„ํ•˜์—ฌ ๋ณ„๋„ ๊ฐœ๋ฐœ ๊ณ„ํš +5. **๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ**: ๋ชจ๋“  ํ…Œ์ด๋ธ”์— company_code ํ•„ํ„ฐ๋ง ํ•„์ˆ˜ +6. **์ œ์–ด๊ด€๋ฆฌ ํ•„์ˆ˜**: UI ์™„์„ฑ ํ›„ ๋ฒ„ํŠผ๋ณ„ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์„ค์ • ํ•„์ˆ˜ + +### UI vs ์ œ์–ด ๊ตฌ๋ถ„ + +| ์˜์—ญ | ๋‹ด๋‹น | ์„ค์ • ์œ„์น˜ | +|------|------|----------| +| ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ | V2 ์ปดํฌ๋„ŒํŠธ | ํ™”๋ฉด ๋””์ž์ด๋„ˆ | +| ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง | ์ œ์–ด๊ด€๋ฆฌ | dataflow_diagrams | +| ์™ธ๋ถ€ ์—ฐ๋™ | ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • | external_call_configs | + +**HTML์—์„œ ๋ฐฐ๋‚„ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ**: UI ๊ตฌ์กฐ๋งŒ +**๋ณ„๋„ ์„ค์ • ํ•„์š”ํ•œ ๊ฒƒ**: ์ €์žฅ ํ…Œ์ด๋ธ”, ๋ฒ„ํŠผ ์•ก์…˜, ์กฐ๊ฑด ์ฒ˜๋ฆฌ, ๋‹ค์ค‘ ํ–‰ ์ฒ˜๋ฆฌ diff --git a/docs/screen-implementation-guide/01_master-data/bom.md b/docs/screen-implementation-guide/01_master-data/bom.md new file mode 100644 index 00000000..6e626289 --- /dev/null +++ b/docs/screen-implementation-guide/01_master-data/bom.md @@ -0,0 +1,255 @@ +# BOM๊ด€๋ฆฌ ํ™”๋ฉด ๊ตฌํ˜„ ๊ฐ€์ด๋“œ + +> **ํ™”๋ฉด๋ช…**: BOM๊ด€๋ฆฌ +> **ํŒŒ์ผ**: BOM๊ด€๋ฆฌ.html +> **๋ถ„๋ฅ˜**: ๊ธฐ์ค€์ •๋ณด +> **๊ตฌํ˜„ ๊ฐ€๋Šฅ**: โš ๏ธ ๋ถ€๋ถ„ (ํŠธ๋ฆฌ ๋ทฐ ์ปดํฌ๋„ŒํŠธ ํ•„์š”) + +--- + +## 1. ํ™”๋ฉด ๊ฐœ์š” + +BOM(Bill of Materials) ๊ด€๋ฆฌ ํ™”๋ฉด์œผ๋กœ, ์ œํ’ˆ์˜ ๋ถ€ํ’ˆ ๊ตฌ์„ฑ์„ ํŠธ๋ฆฌ ๊ตฌ์กฐ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + +### ํ•ต์‹ฌ ๊ธฐ๋Šฅ +- BOM ๋ชฉ๋ก ์กฐํšŒ/๊ฒ€์ƒ‰ +- BOM ๊ตฌ์กฐ ํŠธ๋ฆฌ ํ‘œ์‹œ (์ •์ „๊ฐœ/์—ญ์ „๊ฐœ) +- BOM ๋“ฑ๋ก/์ˆ˜์ •/์‚ญ์ œ +- ๋ฒ„์ „/์ฐจ์ˆ˜ ๊ด€๋ฆฌ +- ์—‘์…€ ์—…๋กœ๋“œ/๋‹ค์šด๋กœ๋“œ + +--- + +## 2. ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [ํ’ˆ๋ชฉ์ฝ”๋“œ] [ํ’ˆ๋ชฉ๋ช…] [ํ’ˆ๋ชฉ๊ตฌ๋ถ„โ–ผ] [๋ฒ„์ „โ–ผ] [์‚ฌ์šฉ์—ฌ๋ถ€โ–ผ] [์ดˆ๊ธฐํ™”][์กฐํšŒ]โ”‚ +โ”‚ [์‚ฌ์šฉ์ž์˜ต์…˜][์—…๋กœ๋“œ][๋‹ค์šด๋กœ๋“œ]โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ๐Ÿ“ฆ BOM ๋ชฉ๋ก โ”‚ ๐Ÿ“‹ BOM ์ƒ์„ธ์ •๋ณด โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ [์ด๋ ฅ] [๋ฒ„์ „] [์ˆ˜์ •] [์‚ญ์ œ] โ”‚ +โ”‚ [์‹ ๊ทœ๋“ฑ๋ก] โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ํ’ˆ๋ชฉ์ฝ”๋“œ: PRD-001 โ”‚ +โ”‚ โ”‚โ–ก|์ฝ”๋“œ|ํ’ˆ๋ชฉ๋ช…|๊ตฌ๋ถ„..โ”‚ โ”‚ ํ’ˆ๋ชฉ๋ช…: ์ œํ’ˆA โ”‚ +โ”‚ โ”‚โ–ก|P01|์ œํ’ˆA |์ œํ’ˆ โ”‚ โ”‚ ๊ธฐ์ค€์ˆ˜๋Ÿ‰: 1 โ”‚ +โ”‚ โ”‚โ–ก|P02|์ œํ’ˆB |์ œํ’ˆ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ ๐ŸŒณ BOM ๊ตฌ์กฐ โ”‚ +โ”‚ โ”‚ ๊ธฐ์ค€์ˆ˜๋Ÿ‰:[1] [ํŠธ๋ฆฌ|๋ ˆ๋ฒจ] [์ •์ „๊ฐœ|์—ญ์ „๊ฐœ] โ”‚ +โ”‚ ๋ฆฌ์‚ฌ์ด์ € โ†” โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ โ”‚ โ–ผ PRD-001 ์ œํ’ˆA (1.00 EA) โ”‚ +โ”‚ โ”‚ โ”œโ”€ MAT-001 ์›์ž์žฌA (2.00 KG) โ”‚ +โ”‚ โ”‚ โ””โ”€ SEM-001 ๋ฐ˜์ œํ’ˆA (1.00 EA) โ”‚ +โ”‚ โ”‚ โ””โ”€ MAT-002 ์›์ž์žฌB (0.50 KG) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 3. V2 ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +### 3.1 ๊ตฌํ˜„ ๊ฐ€๋Šฅ ์˜์—ญ + +| HTML ์˜์—ญ | V2 ์ปดํฌ๋„ŒํŠธ | ์ƒํƒœ | +|-----------|-------------|------| +| ๊ฒ€์ƒ‰ ์„น์…˜ | `v2-table-search-widget` | โœ… ๊ฐ€๋Šฅ | +| BOM ๋ชฉ๋ก ํ…Œ์ด๋ธ” | `v2-table-list` | โœ… ๊ฐ€๋Šฅ | +| ๋ถ„ํ•  ํŒจ๋„ | `v2-split-panel-layout` | โš ๏ธ ๋ถ€๋ถ„ | + +### 3.2 ์‹ ๊ทœ ์ปดํฌ๋„ŒํŠธ ํ•„์š” + +| HTML ์˜์—ญ | ํ•„์š” ์ปดํฌ๋„ŒํŠธ | ์žฌํ™œ์šฉ ๊ฐ€๋Šฅ์„ฑ | +|-----------|---------------|--------------| +| BOM ํŠธ๋ฆฌ ๊ตฌ์กฐ | `v2-tree-view` | 3๊ฐœ ํ™”๋ฉด (๋ถ€์„œ์ •๋ณด, ๋ฉ”๋‰ด๊ด€๋ฆฌ) | +| BOM ๋“ฑ๋ก ๋ชจ๋‹ฌ | `v2-modal-form` | ๋ชจ๋“  ํ™”๋ฉด | + +--- + +## 4. ํ…Œ์ด๋ธ” ์ •์˜ + +### 4.1 BOM ๋ชฉ๋ก ํ…Œ์ด๋ธ” (์ขŒ์ธก) + +```typescript +columns: [ + { id: 'checkbox', type: 'checkbox', width: 50 }, + { id: 'item_code', label: 'ํ’ˆ๋ชฉ์ฝ”๋“œ', width: 100 }, + { id: 'item_name', label: 'ํ’ˆ๋ชฉ๋ช…', width: 150 }, + { id: 'item_type', label: 'ํ’ˆ๋ชฉ๊ตฌ๋ถ„', width: 80 }, + { id: 'version', label: '๋ฒ„์ „', width: 70 }, + { id: 'revision', label: '์ฐจ์ˆ˜', width: 70 }, + { id: 'status', label: '์‚ฌ์šฉ์—ฌ๋ถ€', width: 80 }, + { id: 'reg_date', label: '๋“ฑ๋ก์ผ', width: 100 } +] +``` + +### 4.2 BOM ์ƒ์„ธ ํ•„๋“œ + +```typescript +detailFields: [ + { id: 'item_code', label: 'ํ’ˆ๋ชฉ์ฝ”๋“œ' }, + { id: 'item_name', label: 'ํ’ˆ๋ชฉ๋ช…' }, + { id: 'item_type', label: 'ํ’ˆ๋ชฉ๊ตฌ๋ถ„' }, + { id: 'unit', label: '๋‹จ์œ„' }, + { id: 'base_qty', label: '๊ธฐ์ค€์ˆ˜๋Ÿ‰' }, + { id: 'version', label: '๋ฒ„์ „' }, + { id: 'revision', label: '์ฐจ์ˆ˜' }, + { id: 'status', label: '์‚ฌ์šฉ์—ฌ๋ถ€' }, + { id: 'remark', label: '๋น„๊ณ ' } +] +``` + +--- + +## 5. ๊ฒ€์ƒ‰ ์กฐ๊ฑด + +| ํ•„๋“œ๋ช… | ์ปดํฌ๋„ŒํŠธ | ์˜ต์…˜ | +|--------|----------|------| +| ํ’ˆ๋ชฉ์ฝ”๋“œ | `v2-input` | placeholder: "ํ’ˆ๋ชฉ์ฝ”๋“œ" | +| ํ’ˆ๋ชฉ๋ช… | `v2-input` | placeholder: "ํ’ˆ๋ชฉ๋ช…" | +| ํ’ˆ๋ชฉ๊ตฌ๋ถ„ | `v2-select` | ์ œํ’ˆ, ๋ฐ˜์ œํ’ˆ, ์›์ž์žฌ | +| ๋ฒ„์ „ | `v2-select` | 1.0, 2.0, 3.0 | +| ์‚ฌ์šฉ์—ฌ๋ถ€ | `v2-select` | ์‚ฌ์šฉ, ๋ฏธ์‚ฌ์šฉ | + +--- + +## 6. ํŠน์ˆ˜ ๊ธฐ๋Šฅ: BOM ํŠธ๋ฆฌ (์‹ ๊ทœ ์ปดํฌ๋„ŒํŠธ ํ•„์š”) + +### 6.1 ํŠธ๋ฆฌ ๋…ธ๋“œ ๊ตฌ์กฐ + +```typescript +interface BomTreeNode { + id: string; + itemCode: string; + itemName: string; + itemType: string; + quantity: number; + unit: string; + level: number; + children: BomTreeNode[]; + expanded: boolean; +} +``` + +### 6.2 ์ •์ „๊ฐœ vs ์—ญ์ „๊ฐœ + +| ๋ชจ๋“œ | ์„ค๋ช… | +|------|------| +| ์ •์ „๊ฐœ (Forward) | ์„ ํƒ ํ’ˆ๋ชฉ โ†’ ํ•˜์œ„ ๊ตฌ์„ฑํ’ˆ๋ชฉ ํ‘œ์‹œ | +| ์—ญ์ „๊ฐœ (Reverse) | ์„ ํƒ ํ’ˆ๋ชฉ โ†’ ์ƒ์œ„ ์‚ฌ์šฉ์ฒ˜ ํ‘œ์‹œ | + +### 6.3 ํ•„์š” ์ธํ„ฐ๋ž™์…˜ + +- ๋…ธ๋“œ ํด๋ฆญ: ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ +- ์ „์ฒด ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ ๋ฒ„ํŠผ +- ๋ ˆ๋ฒจ ๋ทฐ/ํŠธ๋ฆฌ ๋ทฐ ์ „ํ™˜ +- ๊ธฐ์ค€์ˆ˜๋Ÿ‰ ๋ณ€๊ฒฝ ์‹œ ์ˆ˜๋Ÿ‰ ์žฌ๊ณ„์‚ฐ + +--- + +## 7. ๋ชจ๋‹ฌ ํผ ์ •์˜ + +### 7.1 BOM ๋“ฑ๋ก ๋ชจ๋‹ฌ + +```typescript +modalFields: [ + { id: 'item_code', label: 'ํ’ˆ๋ชฉ์ฝ”๋“œ', type: 'autocomplete', required: true }, + { id: 'item_name', label: 'ํ’ˆ๋ชฉ๋ช…', type: 'autocomplete', required: true }, + { id: 'item_type', label: 'ํ’ˆ๋ชฉ๊ตฌ๋ถ„', type: 'select', required: true }, + { id: 'unit', label: '๋‹จ์œ„', type: 'select', required: true }, + { id: 'base_qty', label: '๊ธฐ์ค€์ˆ˜๋Ÿ‰', type: 'number', required: true }, + { id: 'version', label: '๋ฒ„์ „', type: 'text', readonly: true }, + { id: 'revision', label: '์ฐจ์ˆ˜', type: 'text', readonly: true }, + { id: 'status', label: '์‚ฌ์šฉ์—ฌ๋ถ€', type: 'radio', options: ['์‚ฌ์šฉ', '๋ฏธ์‚ฌ์šฉ'] }, + { id: 'remark', label: '๋น„๊ณ ', type: 'textarea' } +] + +// ํ•˜์œ„ ํ’ˆ๋ชฉ ์„น์…˜ +childItemsSection: { + title: 'ํ•˜์œ„ ํ’ˆ๋ชฉ ๊ตฌ์„ฑ', + addButton: 'ํ’ˆ๋ชฉ์ถ”๊ฐ€', + columns: [ + { id: 'item_code', label: 'ํ’ˆ๋ชฉ์ฝ”๋“œ' }, + { id: 'item_name', label: 'ํ’ˆ๋ชฉ๋ช…' }, + { id: 'quantity', label: '์†Œ์š”๋Ÿ‰' }, + { id: 'unit', label: '๋‹จ์œ„' }, + { id: 'loss_rate', label: '๋กœ์Šค์œจ(%)' }, + { id: 'actions', label: '' } + ] +} +``` + +--- + +## 8. ํ˜„์žฌ ๊ตฌํ˜„ ๊ฐ€๋Šฅ ๋ฒ”์œ„ + +### โœ… ๊ฐ€๋Šฅ +- ๊ฒ€์ƒ‰ ์˜์—ญ (v2-table-search-widget) +- BOM ๋ชฉ๋ก ํ…Œ์ด๋ธ” (v2-table-list) +- ๋ถ„ํ•  ํŒจ๋„ ๋ ˆ์ด์•„์›ƒ (v2-split-panel-layout) +- ๊ธฐ๋ณธ ์ƒ์„ธ ์ •๋ณด ํ‘œ์‹œ + +### โš ๏ธ ๋ถ€๋ถ„ ๊ฐ€๋Šฅ (๋Œ€์ฒด ๊ตฌํ˜„) +- BOM ๊ตฌ์กฐ: ํŠธ๋ฆฌ ๋Œ€์‹  ๋ ˆ๋ฒจ ํ…Œ์ด๋ธ”๋กœ ํ‘œ์‹œ ๊ฐ€๋Šฅ + +### โŒ ๋ถˆ๊ฐ€๋Šฅ (์‹ ๊ทœ ๊ฐœ๋ฐœ ํ•„์š”) +- ์ง„์ •ํ•œ ํŠธ๋ฆฌ ๋ทฐ (์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ) +- ์ •์ „๊ฐœ/์—ญ์ „๊ฐœ ์ „ํ™˜ +- ํ•˜์œ„ ํ’ˆ๋ชฉ ๋™์  ์ถ”๊ฐ€ ๋ชจ๋‹ฌ + +--- + +## 9. ๊ฐ„์†Œํ™” ๊ตฌํ˜„ JSON + +```json +{ + "screen_code": "BOM_MAIN", + "screen_name": "BOM๊ด€๋ฆฌ", + "components": [ + { + "type": "v2-table-search-widget", + "config": { + "searchFields": [ + { "type": "input", "id": "item_code", "placeholder": "ํ’ˆ๋ชฉ์ฝ”๋“œ" }, + { "type": "input", "id": "item_name", "placeholder": "ํ’ˆ๋ชฉ๋ช…" }, + { "type": "select", "id": "item_type", "placeholder": "ํ’ˆ๋ชฉ๊ตฌ๋ถ„" }, + { "type": "select", "id": "status", "placeholder": "์‚ฌ์šฉ์—ฌ๋ถ€" } + ], + "buttons": [ + { "label": "์ดˆ๊ธฐํ™”", "action": "reset" }, + { "label": "์กฐํšŒ", "action": "search", "variant": "primary" } + ] + } + }, + { + "type": "v2-split-panel-layout", + "config": { + "masterPanel": { + "title": "BOM ๋ชฉ๋ก", + "entityId": "bom_header", + "columns": [ + { "id": "item_code", "label": "ํ’ˆ๋ชฉ์ฝ”๋“œ", "width": 100 }, + { "id": "item_name", "label": "ํ’ˆ๋ชฉ๋ช…", "width": 150 }, + { "id": "item_type", "label": "ํ’ˆ๋ชฉ๊ตฌ๋ถ„", "width": 80 }, + { "id": "version", "label": "๋ฒ„์ „", "width": 70 } + ] + }, + "detailPanel": { + "title": "BOM ์ƒ์„ธ์ •๋ณด", + "entityId": "bom_detail", + "relationType": "one-to-many" + } + } + } + ] +} +``` + +--- + +## 10. ๊ฐœ๋ฐœ ๊ถŒ์žฅ์‚ฌํ•ญ + +1. **1๋‹จ๊ณ„**: ํ˜„์žฌ ์ปดํฌ๋„ŒํŠธ๋กœ ๊ธฐ๋ณธ CRUD ๊ตฌํ˜„ +2. **2๋‹จ๊ณ„**: `v2-tree-view` ๊ฐœ๋ฐœ ํ›„ BOM ๊ตฌ์กฐ ํ†ตํ•ฉ +3. **3๋‹จ๊ณ„**: ๋ฒ„์ „/์ฐจ์ˆ˜ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ + +**์˜ˆ์ƒ ์žฌํ™œ์šฉ**: `v2-tree-view`๋Š” ๋ถ€์„œ์ •๋ณด, ๋ฉ”๋‰ด๊ด€๋ฆฌ์—์„œ๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅ (3๊ฐœ ํ™”๋ฉด) diff --git a/docs/screen-implementation-guide/01_master-data/item-info.md b/docs/screen-implementation-guide/01_master-data/item-info.md new file mode 100644 index 00000000..b0ddd9e0 --- /dev/null +++ b/docs/screen-implementation-guide/01_master-data/item-info.md @@ -0,0 +1,1298 @@ +# ํ’ˆ๋ชฉ์ •๋ณด (Item Info) + +> Screen ID: /screens/140 +> ๋ฉ”๋‰ด ๊ฒฝ๋กœ: ๊ธฐ์ค€์ •๋ณด > ํ’ˆ๋ชฉ์ •๋ณด +> ํ…Œ์ด๋ธ”: `item_info` + +--- + +## โš ๏ธ ๋ฌธ์„œ ์‚ฌ์šฉ ์•ˆ๋‚ด + +> **์ด ๋ฌธ์„œ๋Š” "ํ’ˆ๋ชฉ์ •๋ณด" ํ™”๋ฉด์˜ ๊ตฌํ˜„ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.** +> +> ### ๐Ÿ“Œ ์ค‘์š”: JSON ๋ฐ์ดํ„ฐ๋Š” ์ฐธ๊ณ ์šฉ์ž…๋‹ˆ๋‹ค! +> +> ์ด ๋ฌธ์„œ์— ํฌํ•จ๋œ JSON ์„ค์ •(๋ ˆ์ด์•„์›ƒ, ์ปดํฌ๋„ŒํŠธ ๊ตฌ์„ฑ ๋“ฑ)์€ **ํ’ˆ๋ชฉ์ •๋ณด ํ™”๋ฉด์— ํŠนํ™”๋œ ์˜ˆ์‹œ**์ž…๋‹ˆ๋‹ค. +> +> **๋‹ค๋ฅธ ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•  ๋•Œ:** +> 1. ์ด JSON์„ ๊ทธ๋Œ€๋กœ ๋ณต์‚ฌํ•ด์„œ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š” +> 2. ํ•ด๋‹น ํ™”๋ฉด์˜ **ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋ฅผ ๋จผ์ € ๋ถ„์„**ํ•˜์„ธ์š” +> 3. ํ™”๋ฉด์˜ **์š”๊ตฌ์‚ฌํ•ญ๊ณผ ๊ธฐ๋Šฅ์„ ํŒŒ์•…**ํ•˜์„ธ์š” +> 4. ๋ถ„์„ ๊ฒฐ๊ณผ์— ๋งž๋Š” **์ƒˆ๋กœ์šด JSON ๊ตฌ์กฐ๋ฅผ ์ž‘์„ฑ**ํ•˜์„ธ์š” +> +> ### ์ฐธ๊ณ ํ•ด์•ผ ํ•  ํ•ญ๋ชฉ +> - โœ… ๋ฌธ์„œ ๊ตฌ์กฐ ๋ฐ ์ž‘์„ฑ ํ˜•์‹ +> - โœ… V2 ์ปดํฌ๋„ŒํŠธ ์ข…๋ฅ˜ ๋ฐ ์‚ฌ์šฉ๋ฒ• +> - โœ… API ํ˜ธ์ถœ ๋ฐฉ์‹ ๋ฐ DB INSERT ์ ˆ์ฐจ +> - โœ… ์ปดํฌ๋„ŒํŠธ ์„ค์ • ํŒจํ„ด (position, size, overrides ๊ตฌ์กฐ) +> +> ### ๋ณต์‚ฌํ•˜๋ฉด ์•ˆ ๋˜๋Š” ํ•ญ๋ชฉ +> - โŒ ํ…Œ์ด๋ธ”๋ช… (`item_info` โ†’ ํ•ด๋‹น ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋กœ ๋ณ€๊ฒฝ) +> - โŒ ์ปฌ๋Ÿผ ์„ค์ • (ํ•ด๋‹น ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ์— ๋งž๊ฒŒ ์ž‘์„ฑ) +> - โŒ ํ•„๋“œ๋ช… (`fieldName`, `columnName` ๋“ฑ) +> - โŒ ํ™”๋ฉด๋ช…, screen_code, company_code +> - โŒ screen_id, targetScreenId (๋™์  ์ƒ์„ฑ๋˜๋Š” ๊ฐ’) +> +> ### ๐Ÿšจ ์ปดํฌ๋„ŒํŠธ ๋ถ€์กฑ ์‹œ ํ•„์ˆ˜ ๋ช…์‹œ ์‚ฌํ•ญ +> +> ํ™”๋ฉด ๋ถ„์„ ๊ฒฐ๊ณผ, **ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ๋กœ ๊ตฌํ˜„์ด ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ธฐ๋Šฅ**์ด ์žˆ์„ ๊ฒฝ์šฐ: +> +> 1. ๋ฌธ์„œ์— **"๊ตฌํ˜„ ๋ถˆ๊ฐ€ ํ•ญ๋ชฉ"** ์„น์…˜์„ ๋ฐ˜๋“œ์‹œ ์ถ”๊ฐ€ +> 2. ๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๋ช…์‹œ: +> +> ```markdown +> ## ๐Ÿšซ ๊ตฌํ˜„ ๋ถˆ๊ฐ€ ํ•ญ๋ชฉ (์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ ํ•„์š”) +> +> | ๊ธฐ๋Šฅ | ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ | ํ˜„์žฌ ์ƒํƒœ | ๋น„๊ณ  | +> |------|-----------------|-----------|------| +> | ํŠธ๋ฆฌ ๊ตฌ์กฐ ํ‘œ์‹œ | v2-tree-view | ๋ฏธ๊ตฌํ˜„ | ๊ณ„์ธตํ˜• ๋ฐ์ดํ„ฐ ํ‘œ์‹œ ํ•„์š” | +> | ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ | v2-drag-drop | ๋ฏธ๊ตฌํ˜„ | ์ˆœ์„œ ๋ณ€๊ฒฝ ๊ธฐ๋Šฅ | +> ``` +> +> 3. ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ **์šฐ์„ ์ˆœ์œ„/์ค‘์š”๋„** ๋ช…์‹œ + +## 1. ํ…Œ์ด๋ธ” ์„ ํƒ ๋ฐ ํ™”๋ฉด ๊ตฌ์กฐ + +### 1.1 ์‚ฌ์šฉ ํ…Œ์ด๋ธ” + +| ํ…Œ์ด๋ธ”๋ช… | ์šฉ๋„ | ๋น„๊ณ  | +|----------|------|------| +| `item_info` | ํ’ˆ๋ชฉ ๊ธฐ๋ณธ์ •๋ณด | ์ฃผ ํ…Œ์ด๋ธ” | + +### 1.2 ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •์˜ (์‹ค์ œ DB ๊ธฐ์ค€) + +| ์ปฌ๋Ÿผ๋ช… | ํ‘œ์‹œ๋ช… | ํƒ€์ž… | ํ•„์ˆ˜ | ์„ค๋ช… | +|--------|--------|------|------|------| +| `id` | ID | varchar(500) | PK | UUID ์ž๋™ ์ƒ์„ฑ | +| `item_number` | ํ’ˆ๋ฒˆ์ฝ”๋“œ | varchar(500) | | ํ’ˆ๋ชฉ ๊ณ ์œ  ์ฝ”๋“œ | +| `item_name` | ํ’ˆ๋ช… | varchar(500) | | ํ’ˆ๋ชฉ๋ช… | +| `status` | ์ƒํƒœ | varchar(500) | | ์ •์ƒ, ํ’ˆ์ ˆ, ๋Œ€๊ธฐ, ๋‹จ์ข… | +| `size` | ๊ทœ๊ฒฉ | varchar(500) | | ๊ทœ๊ฒฉ ์ •๋ณด | +| `material` | ์žฌ์งˆ | varchar(500) | | ์žฌ์งˆ ์ •๋ณด | +| `inventory_unit` | ์žฌ๊ณ ๋‹จ์œ„ | varchar(500) | | EA, kg, L, Sheet, Box | +| `weight` | ์ค‘๋Ÿ‰ | varchar(500) | | ์ค‘๋Ÿ‰ ๊ฐ’ | +| `unit` | ๋‹จ์œ„ | varchar(500) | | g, kg, kg/L, t | +| `image` | ์ด๋ฏธ์ง€ | varchar(500) | | ํ’ˆ๋ชฉ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ | +| `division` | ๊ตฌ๋ถ„ | varchar(500) | | ์›์ž์žฌ, ์ค‘๊ฐ„์žฌ, ์™„์ œํ’ˆ, ํฌ์žฅ์žฌ (์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ) | +| `type` | ์œ ํ˜• | varchar(500) | | ์šฉ๋„๋ณ„ ์œ ํ˜• | +| `meno` | ๋ฉ”๋ชจ | varchar(500) | | ๋น„๊ณ  (์˜คํƒ€: memo) | +| `selling_price` | ํŒ๋งค๊ฐ€ | varchar(500) | | ๊ธฐ๋ณธ๊ฐ’ '0' | +| `standard_price` | ๊ธฐ์ค€๊ฐ€ | varchar(500) | | ๊ธฐ๋ณธ๊ฐ’ '0' | +| `currency_code` | ํ†ตํ™”์ฝ”๋“œ | varchar(500) | | ๊ธฐ๋ณธ๊ฐ’ 'KRW' | +| `writer` | ๋“ฑ๋ก์ž | varchar(500) | | ์ž‘์„ฑ์ž ID | +| `company_code` | ํšŒ์‚ฌ์ฝ”๋“œ | varchar(500) | | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ | +| `created_date` | ๋“ฑ๋ก์ผ | timestamp | | ์ž๋™ ์ƒ์„ฑ | +| `updated_date` | ์ˆ˜์ •์ผ | timestamp | | ์ž๋™ ๊ฐฑ์‹  | + +### 1.3 ํ™”๋ฉด ๊ตฌ์กฐ ๊ฐœ์š” + +- **ํ™”๋ฉด ์œ ํ˜•**: ๋ชฉ๋กํ˜• (๋‹จ์ผ ํ…Œ์ด๋ธ” CRUD) +- **์ฃผ์š” ๊ธฐ๋Šฅ**: + - ํ’ˆ๋ชฉ ์กฐํšŒ/๊ฒ€์ƒ‰/ํ•„ํ„ฐ๋ง + - ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ •/์‚ญ์ œ + - ๊ทธ๋ฃนํ•‘ (Group By) + - ์ฝ”๋“œ ๋ณ€๊ฒฝ/ํ•ฉ๋ณ‘ + - ์—‘์…€ ์—…๋กœ๋“œ + - ์ปฌ๋Ÿผ ํ‘œ์‹œ/์ˆจ๊ธฐ๊ธฐ ์„ค์ • + +--- + +## 2. ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜๋„ + +### 2.1 ์ „์ฒด ๋ ˆ์ด์•„์›ƒ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๊ฒ€์ƒ‰ ์˜์—ญ] โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-search-widget โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ์ƒํƒœ โ”‚ โ”‚ ํ’ˆ๋ฒˆ์ฝ”๋“œ โ”‚ โ”‚ ํ’ˆ๋ช… โ”‚ โ”‚ [๊ฒ€์ƒ‰] โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ (select) โ”‚ โ”‚ (text) โ”‚ โ”‚ (text) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [ํ…Œ์ด๋ธ” ํ—ค๋” + ์•ก์…˜ ๋ฒ„ํŠผ] โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [์ฝ”๋“œ๋ณ€๊ฒฝ][์—…๋กœ๋“œ][๋‹ค์šด๋กœ๋“œ] [๋“ฑ๋ก][๋ณต์‚ฌ][์ˆ˜์ •][์‚ญ์ œ] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ”] โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-list โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚โ˜ โ”‚์ƒํƒœโ”‚ํ’ˆ๋ฒˆ์ฝ”๋“œโ”‚ํ’ˆ๋ช… โ”‚๊ทœ๊ฒฉ โ”‚์žฌ์งˆ โ”‚์žฌ๊ณ ๋‹จ์œ„โ”‚์ค‘๋Ÿ‰ โ”‚๋‹จ์œ„ โ”‚๊ตฌ๋ถ„ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚โ˜ โ”‚์ •์ƒโ”‚R_001 โ”‚ํ…Œ์ŠคํŠธA โ”‚100mm โ”‚SUS304โ”‚EA โ”‚1.5 โ”‚kg โ”‚์›์ž์žฌ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚โ˜ โ”‚๋Œ€๊ธฐโ”‚R_002 โ”‚ํ…Œ์ŠคํŠธB โ”‚200mm โ”‚AL โ”‚kg โ”‚2.0 โ”‚kg โ”‚์™„์ œํ’ˆ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2.2 ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ V2 ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก + +> ๐Ÿ“Œ **V2 ์ปดํฌ๋„ŒํŠธ ์ „์ฒด ๋ชฉ๋ก** - ํ™”๋ฉด ๊ตฌ์„ฑ ์‹œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ + +| ์ปดํฌ๋„ŒํŠธ ID | ์„ค๋ช… | ์นดํ…Œ๊ณ ๋ฆฌ | +|-------------|------|----------| +| `v2-input` | ํ…์ŠคํŠธ, ์ˆซ์ž, ๋น„๋ฐ€๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ ๋“ฑ ์ž…๋ ฅ | ์ž…๋ ฅ | +| `v2-select` | ๋“œ๋กญ๋‹ค์šด, ์ฝค๋ณด๋ฐ•์Šค, ๋ผ๋””์˜ค, ์ฒดํฌ๋ฐ•์Šค | ์ž…๋ ฅ | +| `v2-date` | ๋‚ ์งœ/์‹œ๊ฐ„ ์ž…๋ ฅ | ์ž…๋ ฅ | +| `v2-button-primary` | ๋ฒ„ํŠผ | ์•ก์…˜ | +| `v2-table-list` | ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ (CRUD) | ํ…Œ์ด๋ธ” | +| `v2-table-search-widget` | ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰/ํ•„ํ„ฐ ์œ„์ ฏ | ์œ ํ‹ธ๋ฆฌํ‹ฐ | +| `v2-pivot-grid` | ํ”ผ๋ฒ— ๊ทธ๋ฆฌ๋“œ (๋‹ค์ฐจ์› ๋ถ„์„) | ํ…Œ์ด๋ธ” | +| `v2-aggregation-widget` | ์ง‘๊ณ„ ์œ„์ ฏ | ์œ„์ ฏ | +| `v2-text-display` | ํ…์ŠคํŠธ ํ‘œ์‹œ (์ฝ๊ธฐ ์ „์šฉ) | ํ‘œ์‹œ | +| `v2-card-display` | ์นด๋“œ ํ‘œ์‹œ | ํ‘œ์‹œ | +| `v2-divider-line` | ๊ตฌ๋ถ„์„  | ๋ ˆ์ด์•„์›ƒ | +| `v2-section-card` | ์„น์…˜ ์นด๋“œ (๊ทธ๋ฃนํ•‘) | ๋ ˆ์ด์•„์›ƒ | +| `v2-section-paper` | ์„น์…˜ ํŽ˜์ดํผ (๊ทธ๋ฃนํ•‘) | ๋ ˆ์ด์•„์›ƒ | +| `v2-split-panel-layout` | ๋ถ„ํ•  ํŒจ๋„ ๋ ˆ์ด์•„์›ƒ | ๋ ˆ์ด์•„์›ƒ | +| `v2-repeat-container` | ๋ฐ˜๋ณต ์ปจํ…Œ์ด๋„ˆ | ๋ ˆ์ด์•„์›ƒ | +| `v2-repeater` | ๋ฆฌํ”ผํ„ฐ (๋™์  ํ–‰) | ๋ ˆ์ด์•„์›ƒ | +| `v2-category-manager` | ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ | ํŠน์ˆ˜ | +| `v2-numbering-rule` | ์ฑ„๋ฒˆ๊ทœ์น™ | ํŠน์ˆ˜ | +| `v2-media` | ๋ฏธ๋””์–ด (์ด๋ฏธ์ง€/์˜์ƒ) | ๋ฏธ๋””์–ด | +| `v2-rack-structure` | ๋ž™ ๊ตฌ์กฐ (์ฐฝ๊ณ ) | ํŠน์ˆ˜ | +| `v2-location-swap-selector` | ์œ„์น˜ ์Šค์™‘ ์„ ํƒ๊ธฐ | ํŠน์ˆ˜ | + +### 2.3 ์ด ํ™”๋ฉด์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ + +| ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… | ์—ญํ•  | +|---------------|------| +| `v2-table-search-widget` | ๊ฒ€์ƒ‰ ํ•„ํ„ฐ | +| `v2-table-list` | ํ’ˆ๋ชฉ ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” | +| `v2-button-primary` | ์ฝ”๋“œ๋ณ€๊ฒฝ | +| `v2-button-primary` | ์—…๋กœ๋“œ (์—‘์…€) | +| `v2-button-primary` | ๋‹ค์šด๋กœ๋“œ (์—‘์…€) | +| `v2-button-primary` | ๋“ฑ๋ก (๋ชจ๋‹ฌ ์—ด๊ธฐ) | +| `v2-button-primary` | ๋ณต์‚ฌ (๋ชจ๋‹ฌ ์—ด๊ธฐ) | +| `v2-button-primary` | ์ˆ˜์ • (๋ชจ๋‹ฌ ์—ด๊ธฐ) | +| `v2-button-primary` | ์‚ญ์ œ | +| `v2-input` | ๋ชจ๋‹ฌ - ํ…์ŠคํŠธ ์ž…๋ ฅ ํ•„๋“œ | +| `v2-select` | ๋ชจ๋‹ฌ - ์„ ํƒ ํ•„๋“œ | + +--- + +## 3. ํ™”๋ฉด ๋””์ž์ด๋„ˆ ์„ค์ • ๊ฐ€์ด๋“œ + +### 3.1 v2-table-search-widget (๊ฒ€์ƒ‰ ํ•„ํ„ฐ) ์„ค์ • + +1. ์ขŒ์ธก ์ปดํฌ๋„ŒํŠธ ํŒจ๋„์—์„œ `v2-table-search-widget` ๋“œ๋ž˜๊ทธํ•˜์—ฌ ํ™”๋ฉด ์ƒ๋‹จ์— ๋ฐฐ์น˜ +2. ๋Œ€์ƒ ํ…Œ์ด๋ธ”๋กœ ์•„๋ž˜์— ๋ฐฐ์น˜ํ•  ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ์„ ํƒ + +> ๐Ÿ’ก **์ฐธ๊ณ **: ๊ฒ€์ƒ‰ ํ•„ํ„ฐ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋Ÿฐํƒ€์ž„์—์„œ ์›ํ•˜๋Š” ํ•„๋“œ๋ฅผ ์ง์ ‘ ์ถ”๊ฐ€/์‚ญ์ œํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณ„๋„์˜ ํ•„๋“œ ์„ค์ •์ด ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค. + +--- + +### 3.2 v2-table-list (ํ’ˆ๋ชฉ ํ…Œ์ด๋ธ”) ์„ค์ • + +#### Step 1: ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ +1. ์ขŒ์ธก ์ปดํฌ๋„ŒํŠธ ํŒจ๋„์—์„œ `v2-table-list` ๋“œ๋ž˜๊ทธํ•˜์—ฌ ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ์•„๋ž˜์— ๋ฐฐ์น˜ + +#### Step 2: ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ํ…Œ์ด๋ธ” ์„ ํƒ | `item_info` | +| ์ž๋™ ์ปฌ๋Ÿผ ์ƒ์„ฑ | โœ… ์ฒดํฌ (ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ž๋™ ๋กœ๋“œ) | + +#### Step 3: ์ปฌ๋Ÿผ ์„ค์ • + +**[์ปฌ๋Ÿผ ์„ค์ •]** ํŒจ๋„์—์„œ ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ์„ ํƒ ๋ฐ ์ˆœ์„œ ์กฐ์ •: + +| ์ˆœ์„œ | ์ปฌ๋Ÿผ | ํ‘œ์‹œ๋ช… | ๋„ˆ๋น„ | ์ •๋ ฌ | ํ‘œ์‹œ | ํŠน์ˆ˜ ์„ค์ • | +|------|------|--------|------|------|------|-----------| +| 1 | `status` | ์ƒํƒœ | 80 | ์ค‘์•™ | โœ… | ๋ฑƒ์ง€ ์Šคํƒ€์ผ (์ƒ‰์ƒ๋ณ„) | +| 2 | `item_number` | ํ’ˆ๋ฒˆ์ฝ”๋“œ | 140 | ์ขŒ์ธก | โœ… | | +| 3 | `item_name` | ํ’ˆ๋ช… | 200 | ์ขŒ์ธก | โœ… | ๊ตต๊ฒŒ ํ‘œ์‹œ | +| 4 | `size` | ๊ทœ๊ฒฉ | 150 | ์ขŒ์ธก | โœ… | | +| 5 | `material` | ์žฌ์งˆ | 150 | ์ขŒ์ธก | โœ… | | +| 6 | `inventory_unit` | ์žฌ๊ณ ๋‹จ์œ„ | 100 | ์ค‘์•™ | โœ… | | +| 7 | `weight` | ์ค‘๋Ÿ‰ | 80 | ์šฐ์ธก | โœ… | | +| 8 | `unit` | ๋‹จ์œ„ | 80 | ์ค‘์•™ | โœ… | | +| 9 | `image` | ์ด๋ฏธ์ง€ | 80 | ์ค‘์•™ | โœ… | ์ด๋ฏธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ | +| 10 | `division` | ๊ตฌ๋ถ„ | 100 | ์ค‘์•™ | โœ… | ์นดํ…Œ๊ณ ๋ฆฌ ํ‘œ์‹œ | +| 11 | `type` | ์œ ํ˜• | 100 | ์ค‘์•™ | โœ… | | +| 12 | `selling_price` | ํŒ๋งค๊ฐ€ | 100 | ์šฐ์ธก | โ˜ | ์ˆซ์ž ํฌ๋งท | +| 13 | `standard_price` | ๊ธฐ์ค€๊ฐ€ | 100 | ์šฐ์ธก | โ˜ | ์ˆซ์ž ํฌ๋งท | +| 14 | `meno` | ๋ฉ”๋ชจ | 180 | ์ขŒ์ธก | โ˜ | | +| 15 | `writer` | ๋“ฑ๋ก์ž | 100 | ์ขŒ์ธก | โ˜ | ์ฝ๊ธฐ ์ „์šฉ | +| 16 | `created_date` | ๋“ฑ๋ก์ผ | 120 | ์ค‘์•™ | โ˜ | ์ฝ๊ธฐ ์ „์šฉ | +| 17 | `updated_date` | ์ˆ˜์ •์ผ | 120 | ์ค‘์•™ | โ˜ | ์ฝ๊ธฐ ์ „์šฉ | + +#### Step 4: ๊ธฐ๋Šฅ ์„ค์ • + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | ์„ค๋ช… | +|-----------|---------|------| +| ์ฒดํฌ๋ฐ•์Šค | โœ… ์‚ฌ์šฉ | ๋‹ค์ค‘ ์„ ํƒ ํ™œ์„ฑํ™” | +| ํŽ˜์ด์ง€๋„ค์ด์…˜ | โœ… ์‚ฌ์šฉ | | +| ํŽ˜์ด์ง€ ํฌ๊ธฐ | 20 | ๊ธฐ๋ณธ ํ‘œ์‹œ ํ–‰ ์ˆ˜ | +| ์ •๋ ฌ | โœ… ์‚ฌ์šฉ | ์ปฌ๋Ÿผ ํ—ค๋” ํด๋ฆญ ์ •๋ ฌ | +| ์ปฌ๋Ÿผ ๋ฆฌ์‚ฌ์ด์ฆˆ | โœ… ์‚ฌ์šฉ | ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์กฐ์ • | +| ๊ทธ๋ฃนํ•‘ | โœ… ์‚ฌ์šฉ | Group By ๊ธฐ๋Šฅ | + +#### Step 5: ๊ทธ๋ฃนํ•‘ ์˜ต์…˜ ์„ค์ • + +Group By ๋“œ๋กญ๋‹ค์šด์— ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ์„ ํƒ: +- โœ… `status` (์ƒํƒœ) +- โœ… `division` (๊ตฌ๋ถ„) +- โœ… `type` (์œ ํ˜•) +- โœ… `inventory_unit` (์žฌ๊ณ ๋‹จ์œ„) +- โœ… `writer` (๋“ฑ๋ก์ž) + +--- + +### 3.3 ๋ฒ„ํŠผ ์„ค์ • + +#### ์ขŒ์ธก ๋ฒ„ํŠผ ๊ทธ๋ฃน + +##### ์ฝ”๋“œ๋ณ€๊ฒฝ ๋ฒ„ํŠผ + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ๋ผ๋ฒจ | `์ฝ”๋“œ๋ณ€๊ฒฝ` | +| ์•ก์…˜ ํƒ€์ž… | `code_merge` | +| ์Šคํƒ€์ผ | `secondary` | +| ์„ ํƒ ํ•„์ˆ˜ | โœ… ์ฒดํฌ (๋ณต์ˆ˜ ์„ ํƒ) | +| ๋ณ‘ํ•ฉ ๋Œ€์ƒ ์ปฌ๋Ÿผ | `item_number` | +| ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ์—ฐ๊ฒฐ | ํ’ˆ๋ฒˆ์ฝ”๋“œ ํ†ตํ•ฉ (flow_id: 18) | + +##### ์—…๋กœ๋“œ ๋ฒ„ํŠผ + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ๋ผ๋ฒจ | `์—…๋กœ๋“œ` | +| ์•ก์…˜ ํƒ€์ž… | `excel_upload` | +| ์Šคํƒ€์ผ | `secondary` | +| ๋Œ€์ƒ ํ…Œ์ด๋ธ” | `item_info` | + +##### ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ๋ผ๋ฒจ | `๋‹ค์šด๋กœ๋“œ` | +| ์•ก์…˜ ํƒ€์ž… | `excel_download` | +| ์Šคํƒ€์ผ | `secondary` | +| ๋Œ€์ƒ | ํ˜„์žฌ ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | + +#### ์šฐ์ธก ๋ฒ„ํŠผ ๊ทธ๋ฃน + +##### ๋“ฑ๋ก ๋ฒ„ํŠผ + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ๋ผ๋ฒจ | `๋“ฑ๋ก` | +| ์•ก์…˜ ํƒ€์ž… | `modal` | +| ์Šคํƒ€์ผ | `default` | +| ์—ฐ๊ฒฐ ํ™”๋ฉด | ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ • ํ™”๋ฉด (์•„๋ž˜ 3.4 ์ฐธ์กฐ) | +| ๋ชจ๋‹ฌ ์ œ๋ชฉ | ํ’ˆ๋ชฉ ๋“ฑ๋ก | +| ๋ชจ๋‹ฌ ์‚ฌ์ด์ฆˆ | `md` | + +##### ๋ณต์‚ฌ ๋ฒ„ํŠผ + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ๋ผ๋ฒจ | `๋ณต์‚ฌ` | +| ์•ก์…˜ ํƒ€์ž… | `copy` | +| ์Šคํƒ€์ผ | `default` | +| ์„ ํƒ ํ•„์ˆ˜ | โœ… ์ฒดํฌ (1๊ฐœ๋งŒ) | +| ์—ฐ๊ฒฐ ํ™”๋ฉด | ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ • ํ™”๋ฉด (์•„๋ž˜ 3.4 ์ฐธ์กฐ) | +| ๋™์ž‘ | ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณต์‚ฌํ•˜์—ฌ ์‹ ๊ทœ ๋“ฑ๋ก ํผ์— ์ฑ„์›€ | + +##### ์ˆ˜์ • ๋ฒ„ํŠผ + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ๋ผ๋ฒจ | `์ˆ˜์ •` | +| ์•ก์…˜ ํƒ€์ž… | `edit` | +| ์Šคํƒ€์ผ | `default` | +| ์„ ํƒ ํ•„์ˆ˜ | โœ… ์ฒดํฌ (1๊ฐœ๋งŒ) | +| ์—ฐ๊ฒฐ ํ™”๋ฉด | ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ • ํ™”๋ฉด (์•„๋ž˜ 3.4 ์ฐธ์กฐ) | +| ๋™์ž‘ | ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ˆ˜์ • ๋ชจ๋“œ๋กœ ํผ ์—ด๊ธฐ | + +##### ์‚ญ์ œ ๋ฒ„ํŠผ + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ๋ผ๋ฒจ | `์‚ญ์ œ` | +| ์•ก์…˜ ํƒ€์ž… | `delete` | +| ์Šคํƒ€์ผ | `default` | +| ์„ ํƒ ํ•„์ˆ˜ | โœ… ์ฒดํฌ (๋ณต์ˆ˜ ์„ ํƒ ๊ฐ€๋Šฅ) | +| ํ™•์ธ ๋ฉ”์‹œ์ง€ | ์„ ํƒํ•œ ํ’ˆ๋ชฉ์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? | +| ์‚ญ์ œ ํ›„ ๋™์ž‘ | ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ | + +--- + +### 3.4 ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ • ํ™”๋ฉด (๋ชจ๋‹ฌ์šฉ ํ™”๋ฉด) + +> ๐Ÿ“Œ **๋ณ„๋„ ํ™”๋ฉด ์ƒ์„ฑ ํ•„์š”**: ๋“ฑ๋ก/๋ณต์‚ฌ/์ˆ˜์ • ๋ฒ„ํŠผ์— ์—ฐ๊ฒฐํ•  ๋ชจ๋‹ฌ ํ™”๋ฉด์„ ์ƒˆ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. +> +> ๐Ÿ’ก **๋™์ผ ํ™”๋ฉด ๊ณต์œ **: ๋“ฑ๋ก, ๋ณต์‚ฌ, ์ˆ˜์ • ๋ฒ„ํŠผ ๋ชจ๋‘ ๋™์ผํ•œ ํผ ํ™”๋ฉด์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. +> - **๋“ฑ๋ก**: ๋นˆ ํผ์œผ๋กœ ์—ด๋ฆผ +> - **๋ณต์‚ฌ**: ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ฑ„์›Œ์ง„ ์ƒํƒœ๋กœ ์—ด๋ฆผ (์‹ ๊ทœ ๋“ฑ๋ก) +> - **์ˆ˜์ •**: ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ฑ„์›Œ์ง„ ์ƒํƒœ๋กœ ์—ด๋ฆผ (๊ธฐ์กด ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ) + +#### Step 1: ์ƒˆ ํ™”๋ฉด ์ƒ์„ฑ + +1. ํ™”๋ฉด ๊ด€๋ฆฌ์—์„œ **[+ ์ƒˆ ํ™”๋ฉด]** ํด๋ฆญ +2. ํ™”๋ฉด ์ •๋ณด ์ž…๋ ฅ: + - ํ™”๋ฉด๋ช…: `ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ •` + - ํ…Œ์ด๋ธ”: `item_info` + - ํ™”๋ฉด ์œ ํ˜•: `๋ชจ๋‹ฌ` + +#### Step 2: ํผ ํ•„๋“œ ๋ฐฐ์น˜ + +**๋ชจ๋‹ฌ ๋ ˆ์ด์•„์›ƒ ๋ฐฐ์น˜๋„**: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ • [โœ•] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ’ˆ๋ฒˆ์ฝ”๋“œ * โ”‚ โ”‚ ํ’ˆ๋ช… * โ”‚ โ”‚ +โ”‚ โ”‚ [____________________] โ”‚ โ”‚ [____________________] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๊ทœ๊ฒฉ โ”‚ โ”‚ ์žฌ์งˆ โ”‚ โ”‚ +โ”‚ โ”‚ [____________________] โ”‚ โ”‚ [____________________] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์žฌ๊ณ ๋‹จ์œ„ * โ”‚ โ”‚ ์ค‘๋Ÿ‰ โ”‚ โ”‚ ์ค‘๋Ÿ‰๋‹จ์œ„ โ”‚ โ”‚ +โ”‚ โ”‚ [EA โ–ผ] โ”‚ โ”‚ [_______] โ”‚ โ”‚ [kg โ–ผ] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๊ตฌ๋ถ„ * โ”‚ โ”‚ ์œ ํ˜• โ”‚ โ”‚ +โ”‚ โ”‚ [์›์ž์žฌ โ–ผ] โ”‚ โ”‚ [๋ฐ˜๋„์ฒด์šฉ โ–ผ] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํŒ๋งค๊ฐ€ โ”‚ โ”‚ ๊ธฐ์ค€๊ฐ€ โ”‚ โ”‚ +โ”‚ โ”‚ [____________________] โ”‚ โ”‚ [____________________] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋ฉ”๋ชจ โ”‚ โ”‚ +โ”‚ โ”‚ [__________________________________________________]โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ƒํƒœ * โ”‚ โ”‚ +โ”‚ โ”‚ [์ •์ƒ โ–ผ] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [์ทจ์†Œ] [๐Ÿ’พ ์ €์žฅ] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**ํ•„๋“œ ๋ชฉ๋ก**: + +| ์ˆœ์„œ | ํ•„๋“œ (์ปฌ๋Ÿผ๋ช…) | ๋ผ๋ฒจ | ์ž…๋ ฅ ํƒ€์ž… | ํ•„์ˆ˜ | ๋น„๊ณ  | +|------|---------------|------|-----------|------|------| +| 1 | `item_number` | ํ’ˆ๋ฒˆ์ฝ”๋“œ | text | โœ… | | +| 2 | `item_name` | ํ’ˆ๋ช… | text | โœ… | | +| 3 | `size` | ๊ทœ๊ฒฉ | text | | | +| 4 | `material` | ์žฌ์งˆ | text | | | +| 5 | `inventory_unit` | ์žฌ๊ณ ๋‹จ์œ„ | select | โœ… | ์˜ต์…˜: EA, kg, L, Sheet, Box | +| 6 | `weight` | ์ค‘๋Ÿ‰ | number | | | +| 7 | `unit` | ์ค‘๋Ÿ‰๋‹จ์œ„ | select | | ์˜ต์…˜: g, kg, kg/L, t | +| 8 | `division` | ๊ตฌ๋ถ„ | category | โœ… | ํ’ˆ๋ชฉ ๊ตฌ๋ถ„ ์นดํ…Œ๊ณ ๋ฆฌ | +| 9 | `type` | ์œ ํ˜• | select | | ์˜ต์…˜: ๋ฐ˜๋„์ฒด์šฉ, ํƒœ์–‘๊ด‘์šฉ, ์‚ฐ์—…์šฉ, ์˜๋ฃŒ์šฉ, ๊ฑด์ถ•์šฉ, ์‚ฌ์ถœ์šฉ, ํ™”์žฅํ’ˆ์šฉ | +| 10 | `selling_price` | ํŒ๋งค๊ฐ€ | number | | | +| 11 | `standard_price` | ๊ธฐ์ค€๊ฐ€ | number | | | +| 12 | `meno` | ๋ฉ”๋ชจ | text | | | +| 13 | `status` | ์ƒํƒœ | select | โœ… | ์˜ต์…˜: ์ •์ƒ, ํ’ˆ์ ˆ, ๋Œ€๊ธฐ, ๋‹จ์ข… | + +#### Step 3: ๋ฒ„ํŠผ ๋ฐฐ์น˜ + +| ๋ฒ„ํŠผ | ์•ก์…˜ ํƒ€์ž… | ์Šคํƒ€์ผ | ์„ค์ • | +|------|-----------|--------|------| +| ์ €์žฅ | `์ €์žฅ` | primary | ์ €์žฅ ํ›„ ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ, ๋ถ€๋ชจ ํ™”๋ฉด ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ | +| ์ทจ์†Œ | `๋ชจ๋‹ฌ ๋‹ซ๊ธฐ` | secondary | | + +#### Step 4: ๋ฒ„ํŠผ์— ํ™”๋ฉด ์—ฐ๊ฒฐ + +1. ๋ฉ”์ธ ํ™”๋ฉด(ํ’ˆ๋ชฉ์ •๋ณด)์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ +2. **๋“ฑ๋ก ๋ฒ„ํŠผ** ์„ ํƒ โ†’ ์„ค์ • ํŒจ๋„์—์„œ: + - ์•ก์…˜ ํƒ€์ž…: `modal` + - ์—ฐ๊ฒฐ ํ™”๋ฉด: `ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ •` ์„ ํƒ + - ๋ชจ๋‹ฌ ์ œ๋ชฉ: `ํ’ˆ๋ชฉ ๋“ฑ๋ก` +3. **๋ณต์‚ฌ ๋ฒ„ํŠผ** ์„ ํƒ โ†’ ์„ค์ • ํŒจ๋„์—์„œ: + - ์•ก์…˜ ํƒ€์ž…: `copy` + - ์—ฐ๊ฒฐ ํ™”๋ฉด: `ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ •` ์„ ํƒ + - ์„ ํƒ ํ•„์ˆ˜: โœ… ์ฒดํฌ + - ๋™์ž‘: ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณต์‚ฌํ•˜์—ฌ ํผ์— ์ฑ„์›€ (์‹ ๊ทœ ๋“ฑ๋ก) +4. **์ˆ˜์ • ๋ฒ„ํŠผ** ์„ ํƒ โ†’ ์„ค์ • ํŒจ๋„์—์„œ: + - ์•ก์…˜ ํƒ€์ž…: `edit` + - ์—ฐ๊ฒฐ ํ™”๋ฉด: `ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ •` ์„ ํƒ + - ์„ ํƒ ํ•„์ˆ˜: โœ… ์ฒดํฌ + - ๋™์ž‘: ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ • ๋ชจ๋“œ๋กœ ํผ์— ์ฑ„์›€ + +> ๐Ÿ’ก **์ฐธ๊ณ **: ์ปฌ๋Ÿผ๋ณ„ ์Šคํƒ€์ผ(๋ฑƒ์ง€ ์ƒ‰์ƒ, ์นดํ…Œ๊ณ ๋ฆฌ ํ‘œ์‹œ ๋“ฑ)์€ ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ณธ ์Šคํƒ€์ผ์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ํ•„์š”์‹œ ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ์—์„œ ์ปฌ๋Ÿผ๋ณ„ ์ƒ์„ธ ์„ค์ •์„ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +--- + +## 4. ์ปดํฌ๋„ŒํŠธ ์—ฐ๋™ ์„ค์ • + +### 4.1 ์ด๋ฒคํŠธ ํ๋ฆ„ + +``` +[๊ฒ€์ƒ‰ ์ž…๋ ฅ] + โ”‚ + โ–ผ +v2-table-search-widget + โ”‚ onFilterChange + โ–ผ +v2-table-list (์ž๋™ ์žฌ์กฐํšŒ) + โ”‚ + โ–ผ +[๋ฐ์ดํ„ฐ ํ‘œ์‹œ] + + +[๋“ฑ๋ก/๋ณต์‚ฌ/์ˆ˜์ • ๋ฒ„ํŠผ ํด๋ฆญ] + โ”‚ + โ–ผ +[๋ชจ๋‹ฌ ์—ด๊ธฐ] โ†’ [ํผ ์ž…๋ ฅ] โ†’ [์ €์žฅ] + โ”‚ โ”‚ + โ”‚ โ–ผ + โ”‚ refreshTable ์ด๋ฒคํŠธ + โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + v2-table-list (์žฌ์กฐํšŒ) +``` + +### 4.2 ์—ฐ๋™ ์„ค์ • + +| ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ | ์ด๋ฒคํŠธ/์•ก์…˜ | ๋Œ€์ƒ ์ปดํฌ๋„ŒํŠธ | ๋™์ž‘ | +|---------------|-------------|---------------|------| +| ๊ฒ€์ƒ‰ ์œ„์ ฏ | onFilterChange | ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | ํ•„ํ„ฐ ์ ์šฉ, ์žฌ์กฐํšŒ | +| ๋“ฑ๋ก ๋ฒ„ํŠผ | click | ๋ชจ๋‹ฌ | ๋นˆ ํผ์œผ๋กœ ๋ชจ๋‹ฌ ์—ด๊ธฐ | +| ๋ณต์‚ฌ ๋ฒ„ํŠผ | click | ๋ชจ๋‹ฌ | ์„ ํƒ ๋ฐ์ดํ„ฐ๊ฐ€ ์ฑ„์›Œ์ง„ ํผ ์—ด๊ธฐ (์‹ ๊ทœ) | +| ์ˆ˜์ • ๋ฒ„ํŠผ | click | ๋ชจ๋‹ฌ | ์„ ํƒ ๋ฐ์ดํ„ฐ๊ฐ€ ์ฑ„์›Œ์ง„ ํผ ์—ด๊ธฐ (์ˆ˜์ •) | +| ์‚ญ์ œ ๋ฒ„ํŠผ | click | ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | ์„ ํƒ ํ•ญ๋ชฉ ์‚ญ์ œ | +| ๋ชจ๋‹ฌ ์ €์žฅ | afterSave | ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | refreshTable | + +### 4.3 TableOptionsContext ์—ฐ๋™ + +``` +v2-table-search-widget โ”€โ”€โ”€โ”€ TableOptionsContext โ”€โ”€โ”€โ”€ v2-table-list + โ”‚ โ”‚ โ”‚ + โ”‚ registeredTables์—์„œ โ”‚ โ”‚ + โ”‚ item-table ์ฐธ์กฐ โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ + โ””โ”€โ”€ onFilterChange() โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + ํ•„ํ„ฐ ์กฐ๊ฑด ์ „๋‹ฌ & ์žฌ์กฐํšŒ +``` + +--- + +## 5. ์‚ฌ์šฉ์ž ์‚ฌ์šฉ ์˜ˆ์‹œ ์‹œ๋‚˜๋ฆฌ์˜ค + +### ์‹œ๋‚˜๋ฆฌ์˜ค 1: ํ’ˆ๋ชฉ ์กฐํšŒ + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|------|-------------|-----------| +| 1 | ํ™”๋ฉด ์ง„์ž… | ์ „์ฒด ํ’ˆ๋ชฉ ๋ชฉ๋ก ํ‘œ์‹œ | +| 2 | ์ƒํƒœ ํ•„ํ„ฐ๋ฅผ "์ •์ƒ"์œผ๋กœ ์„ ํƒ | ์ž๋™ ํ•„ํ„ฐ๋ง | +| 3 | ํ’ˆ๋ช…์— "ํด๋ฆฌ๋จธ" ์ž…๋ ฅ ํ›„ ๊ฒ€์ƒ‰ | ํ’ˆ๋ช…์— "ํด๋ฆฌ๋จธ" ํฌํ•จ๋œ ํ’ˆ๋ชฉ ํ‘œ์‹œ | +| 4 | Group by์—์„œ "๊ตฌ๋ถ„" ์„ ํƒ | division๋ณ„ ๊ทธ๋ฃนํ•‘ | + +### ์‹œ๋‚˜๋ฆฌ์˜ค 2: ํ’ˆ๋ชฉ ๋“ฑ๋ก + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|------|-------------|-----------| +| 1 | [๋“ฑ๋ก] ๋ฒ„ํŠผ ํด๋ฆญ | ๋นˆ ํผ ๋ชจ๋‹ฌ ํ‘œ์‹œ | +| 2 | ๋ฐ์ดํ„ฐ ์ž…๋ ฅ (ํ’ˆ๋ฒˆ์ฝ”๋“œ, ํ’ˆ๋ช…, ๊ทœ๊ฒฉ ๋“ฑ) | ์ž…๋ ฅ ํ•„๋“œ ์ฑ„์›€ | +| 3 | [์ €์žฅ] ๋ฒ„ํŠผ ํด๋ฆญ | ์ €์žฅ ์™„๋ฃŒ, ๋ชจ๋‹ฌ ๋‹ซํž˜, ๋ชฉ๋ก ๊ฐฑ์‹  | + +### ์‹œ๋‚˜๋ฆฌ์˜ค 3: ํ’ˆ๋ชฉ ๋ณต์‚ฌ + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|------|-------------|-----------| +| 1 | ํ…Œ์ด๋ธ”์—์„œ ๋ณต์‚ฌํ•  ํ–‰ ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ | ํ–‰ ์„ ํƒ ํ‘œ์‹œ | +| 2 | [๋ณต์‚ฌ] ๋ฒ„ํŠผ ํด๋ฆญ | ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ฑ„์›Œ์ง„ ํผ ๋ชจ๋‹ฌ ํ‘œ์‹œ | +| 3 | ํ•„์š”์‹œ ๋ฐ์ดํ„ฐ ์ˆ˜์ • (ํ’ˆ๋ฒˆ์ฝ”๋“œ ๋“ฑ) | ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ | +| 4 | [์ €์žฅ] ๋ฒ„ํŠผ ํด๋ฆญ | ์‹ ๊ทœ ๋“ฑ๋ก ์™„๋ฃŒ, ๋ชฉ๋ก ๊ฐฑ์‹  | + +### ์‹œ๋‚˜๋ฆฌ์˜ค 4: ํ’ˆ๋ชฉ ์ˆ˜์ • + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|------|-------------|-----------| +| 1 | ํ…Œ์ด๋ธ”์—์„œ ํ–‰ ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ | ํ–‰ ์„ ํƒ ํ‘œ์‹œ | +| 2 | [์ˆ˜์ •] ๋ฒ„ํŠผ ํด๋ฆญ | ์ˆ˜์ • ๋ชจ๋‹ฌ ํ‘œ์‹œ (๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋กœ๋“œ) | +| 3 | ๋ฐ์ดํ„ฐ ์ˆ˜์ • | ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ | +| 4 | [์ €์žฅ] ๋ฒ„ํŠผ ํด๋ฆญ | ์ €์žฅ ์™„๋ฃŒ, ๋ชฉ๋ก ๊ฐฑ์‹  | + +### ์‹œ๋‚˜๋ฆฌ์˜ค 5: ํ’ˆ๋ชฉ ์‚ญ์ œ + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|------|-------------|-----------| +| 1 | ์‚ญ์ œํ•  ํ–‰ ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ (๋‹ค์ค‘ ๊ฐ€๋Šฅ) | ํ–‰ ์„ ํƒ ํ‘œ์‹œ | +| 2 | [์‚ญ์ œ] ๋ฒ„ํŠผ ํด๋ฆญ | ์‚ญ์ œ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ | +| 3 | ํ™•์ธ | ์‚ญ์ œ ์™„๋ฃŒ, ๋ชฉ๋ก ๊ฐฑ์‹  | + +--- + +## 6. ๊ฒ€์ฆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### ๊ธฐ๋ณธ ๊ธฐ๋Šฅ +- [ ] ๋ฐ์ดํ„ฐ ์กฐํšŒ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ๊ฒ€์ƒ‰ ํ•„ํ„ฐ (์ƒํƒœ, ํ’ˆ๋ฒˆ์ฝ”๋“œ, ํ’ˆ๋ช…)๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์‹ ๊ทœ ๋“ฑ๋ก์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ๋ณต์‚ฌ ๊ธฐ๋Šฅ์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ˆ˜์ •์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์‚ญ์ œ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ฝ”๋“œ๋ณ€๊ฒฝ์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์—‘์…€ ์—…๋กœ๋“œ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์—‘์…€ ๋‹ค์šด๋กœ๋“œ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? + +### ํ…Œ์ด๋ธ” ๊ธฐ๋Šฅ +- [ ] ํŽ˜์ด์ง€๋„ค์ด์…˜์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ •๋ ฌ์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์กฐ์ •์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? + +### ๊ฒ€์ƒ‰ ์œ„์ ฏ ์—ฐ๋™ +- [ ] v2-table-search-widget๊ณผ v2-table-list ์—ฐ๋™์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ํ•„ํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ์žฌ์กฐํšŒ๊ฐ€ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ดˆ๊ธฐํ™” ๋ฒ„ํŠผ์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? + +### ๊ทธ๋ฃนํ•‘ ๊ธฐ๋Šฅ +- [ ] Group by ์„ ํƒ ์‹œ ๊ทธ๋ฃนํ•‘์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ๋‹ค์ค‘ ๊ทธ๋ฃนํ•‘์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? + +--- + +## 7. ์ฐธ๊ณ  ์‚ฌํ•ญ + +### ๊ด€๋ จ ํ…Œ์ด๋ธ” +- `customer_item_mapping` - ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ํ’ˆ๋ชฉ ๋งคํ•‘ +- `supplier_item_mapping` - ๊ณต๊ธ‰์—…์ฒด๋ณ„ ํ’ˆ๋ชฉ ๋งคํ•‘ +- `item_inspection_info` - ํ’ˆ๋ชฉ ๊ฒ€์‚ฌ ์ •๋ณด +- `item_routing_version` - ํ’ˆ๋ชฉ๋ณ„ ๊ณต์ • ๋ฒ„์ „ +- `item_routing_detail` - ํ’ˆ๋ชฉ๋ณ„ ๊ณต์ • ์ƒ์„ธ + +### ํŠน์ด ์‚ฌํ•ญ +- `division` ์ปฌ๋Ÿผ์€ ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ (์˜ˆ: CATEGORY_191259)๋กœ ์ €์žฅ๋จ +- `meno` ์ปฌ๋Ÿผ์€ ์˜คํƒ€๋กœ ๋ณด์ž„ (์›๋ž˜ memo) +- `selling_price`, `standard_price`๋Š” varchar๋กœ ์ €์žฅ๋จ (์ˆซ์ž ํ˜•์‹ ๋ฌธ์ž์—ด) +- `company_code`๋Š” ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ์šฉ ํšŒ์‚ฌ ์ฝ”๋“œ + +--- + +## 8. DB INSERT์šฉ JSON ์„ค์ • (screen_layouts_v2 ๋ฐฉ์‹) + +> ๐Ÿ“Œ ์‹ค์ œ ํ™”๋ฉด ์ €์žฅ์€ `screen_definitions` + `screen_layouts_v2` ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. +> `screen_layouts_v2`๋Š” ์ „์ฒด ๋ ˆ์ด์•„์›ƒ์„ ํ•˜๋‚˜์˜ JSON (`layout_data`)์œผ๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + +> โš ๏ธ **์ฃผ์˜: ์•„๋ž˜ JSON์€ "ํ’ˆ๋ชฉ์ •๋ณด" ํ™”๋ฉด ์ „์šฉ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค!** +> +> ๋‹ค๋ฅธ ํ™”๋ฉด ๊ตฌํ˜„ ์‹œ: +> 1. **ํ…Œ์ด๋ธ” ๋ถ„์„ ํ•„์ˆ˜** - ํ•ด๋‹น ํ™”๋ฉด์ด ์‚ฌ์šฉํ•˜๋Š” ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ํŒŒ์•… +> 2. **์ปฌ๋Ÿผ ์žฌ์ •์˜** - `columns`, `fieldName` ๋“ฑ์„ ํ•ด๋‹น ํ…Œ์ด๋ธ”์— ๋งž๊ฒŒ ์ž‘์„ฑ +> 3. **๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ ๋ฐ˜์˜** - ๋ฒ„ํŠผ, ๋ชจ๋‹ฌ, ์•ก์…˜ ๋“ฑ์„ ํ™”๋ฉด ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž๊ฒŒ ๊ตฌ์„ฑ +> 4. **ID ๊ฐ’ ๋™์  ์ฒ˜๋ฆฌ** - `screen_id`, `targetScreenId`๋Š” ์ƒ์„ฑ ์‹œ ํ• ๋‹น๋˜๋Š” ๊ฐ’ ์‚ฌ์šฉ + +### 8.1 ํ…Œ์ด๋ธ” ๊ตฌ์กฐ + +#### screen_definitions + +| ์ปฌ๋Ÿผ๋ช… | ํƒ€์ž… | ํ•„์ˆ˜ | ๊ธฐ๋ณธ๊ฐ’ | ์„ค๋ช… | +|--------|------|------|--------|------| +| `screen_id` | integer | PK | ์ž๋™ ์ƒ์„ฑ (์‹œํ€€์Šค) | ํ™”๋ฉด ๊ณ ์œ  ID | +| `screen_name` | varchar(100) | โœ… | - | ํ™”๋ฉด๋ช… | +| `screen_code` | varchar(50) | โœ… | **์ž๋™ ์ƒ์„ฑ** | `{company_code}_{์ˆœ๋ฒˆ}` ํ˜•์‹ | +| `table_name` | varchar(100) | | - | ๊ธฐ๋ณธ ํ…Œ์ด๋ธ”๋ช… | +| `company_code` | varchar(50) | โœ… | - | ํšŒ์‚ฌ ์ฝ”๋“œ | +| `description` | text | | - | ํ™”๋ฉด ์„ค๋ช… | +| `is_active` | char(1) | | `'Y'` | Y=ํ™œ์„ฑ, N=๋น„ํ™œ์„ฑ, D=์‚ญ์ œ | +| `created_date` | timestamp | | `CURRENT_TIMESTAMP` | ์ƒ์„ฑ์ผ์‹œ | +| `db_source_type` | varchar(10) | | `'internal'` | internal/external | +| `data_source_type` | varchar(20) | | `'database'` | database/rest_api | + +#### screen_layouts_v2 + +| ์ปฌ๋Ÿผ๋ช… | ํƒ€์ž… | ํ•„์ˆ˜ | ๊ธฐ๋ณธ๊ฐ’ | ์„ค๋ช… | +|--------|------|------|--------|------| +| `layout_id` | integer | PK | ์ž๋™ ์ƒ์„ฑ (์‹œํ€€์Šค) | ๋ ˆ์ด์•„์›ƒ ๊ณ ์œ  ID | +| `screen_id` | integer | โœ… | - | ํ™”๋ฉด ID (FK) | +| `company_code` | varchar(20) | โœ… | - | ํšŒ์‚ฌ ์ฝ”๋“œ | +| `layout_data` | jsonb | โœ… | `'{}'` | ์ „์ฒด ๋ ˆ์ด์•„์›ƒ JSON | +| `created_at` | timestamp | | `now()` | ์ƒ์„ฑ์ผ์‹œ | +| `updated_at` | timestamp | | `now()` | ์ˆ˜์ •์ผ์‹œ | + +### 8.2 ํ™”๋ฉด ์ •์˜ (screen_definitions) + +> โš ๏ธ `screen_code`๋Š” API ํ˜ธ์ถœ ์‹œ ์ž๋™ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. (`{company_code}_{์ˆœ๋ฒˆ}` ํ˜•์‹) + +**ํ•„์ˆ˜ ์ž…๋ ฅ ํ•„๋“œ:** + +```json +{ + "screenName": "ํ’ˆ๋ชฉ์ •๋ณด", + "tableName": "item_info", + "companyCode": "COMPANY_7", + "description": "ํ’ˆ๋ชฉ ๊ธฐ๋ณธ์ •๋ณด ๊ด€๋ฆฌ ํ™”๋ฉด" +} +``` + +**์ „์ฒด ํ•„๋“œ (์ž๋™ ์ƒ์„ฑ ํฌํ•จ):** + +```json +{ + "screen_id": 140, + "screen_name": "ํ’ˆ๋ชฉ์ •๋ณด", + "screen_code": "COMPANY_7_3", + "table_name": "item_info", + "company_code": "COMPANY_7", + "description": "ํ’ˆ๋ชฉ ๊ธฐ๋ณธ์ •๋ณด ๊ด€๋ฆฌ ํ™”๋ฉด", + "is_active": "Y", + "db_source_type": "internal", + "data_source_type": "database", + "created_date": "2025-01-29T00:00:00.000Z" +} +``` + +### 8.2 ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ (screen_layouts_v2.layout_data) + +> ์ „์ฒด ๋ ˆ์ด์•„์›ƒ์„ ํ•˜๋‚˜์˜ JSON์œผ๋กœ ์ €์žฅ + +```json +{ + "version": "2.0", + "components": [ + { + "id": "comp_search", + "url": "@/lib/registry/components/v2-table-search-widget", + "size": { "width": 1920, "height": 80 }, + "position": { "x": 0, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-table-search-widget", + "label": "๊ฒ€์ƒ‰ ํ•„ํ„ฐ", + "webTypeConfig": {} + }, + "displayOrder": 0 + }, + { + "id": "comp_table", + "url": "@/lib/registry/components/v2-table-list", + "size": { "width": 1920, "height": 930 }, + "position": { "x": 0, "y": 150, "z": 1 }, + "overrides": { + "type": "v2-table-list", + "label": "ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ", + "filter": { "enabled": true, "filters": [] }, + "height": "auto", + "actions": { "actions": [], "bulkActions": false, "showActions": false }, + "columns": [ + { "align": "left", "order": 0, "format": "text", "visible": true, "sortable": true, "columnName": "status", "searchable": true, "displayName": "status" }, + { "align": "left", "order": 1, "format": "text", "visible": true, "sortable": true, "columnName": "item_number", "searchable": true, "displayName": "item_number" }, + { "align": "left", "order": 2, "format": "text", "visible": true, "sortable": true, "columnName": "item_name", "searchable": true, "displayName": "item_name" }, + { "align": "left", "order": 3, "format": "text", "visible": true, "sortable": true, "columnName": "size", "searchable": true, "displayName": "size" }, + { "align": "left", "order": 4, "format": "text", "visible": true, "sortable": true, "columnName": "material", "searchable": true, "displayName": "material" }, + { "align": "left", "order": 5, "format": "text", "visible": true, "sortable": true, "columnName": "inventory_unit", "searchable": true, "displayName": "inventory_unit" }, + { "align": "left", "order": 6, "format": "text", "visible": true, "sortable": true, "columnName": "weight", "searchable": true, "displayName": "weight" }, + { "align": "left", "order": 7, "format": "text", "visible": true, "sortable": true, "columnName": "unit", "searchable": true, "displayName": "unit" }, + { "align": "left", "order": 8, "format": "text", "visible": true, "sortable": true, "columnName": "division", "searchable": true, "displayName": "division" }, + { "align": "left", "order": 9, "format": "text", "visible": true, "sortable": true, "columnName": "type", "searchable": true, "displayName": "type" }, + { "align": "left", "order": 10, "format": "text", "visible": true, "sortable": true, "columnName": "writer", "searchable": true, "displayName": "writer" } + ], + "autoLoad": true, + "checkbox": { "enabled": true, "multiple": true, "position": "left", "selectAll": true }, + "pagination": { "enabled": true, "pageSize": 20, "showPageInfo": true, "pageSizeOptions": [10, 20, 50, 100], "showSizeSelector": true }, + "showFooter": true, + "showHeader": true, + "tableStyle": { "theme": "default", "rowHeight": "normal", "borderStyle": "light", "headerStyle": "default", "hoverEffect": true, "alternateRows": true }, + "displayMode": "table", + "stickyHeader": false, + "selectedTable": "item_info", + "webTypeConfig": {}, + "horizontalScroll": { "enabled": true, "maxColumnWidth": 300, "minColumnWidth": 100, "maxVisibleColumns": 8 } + }, + "displayOrder": 1 + }, + { + "id": "comp_btn_code_merge", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 88, "height": 40 }, + "position": { "x": 10, "y": 100, "z": 1 }, + "overrides": { + "text": "์ฝ”๋“œ๋ณ€๊ฒฝ", + "type": "v2-button-primary", + "label": "๊ธฐ๋ณธ ๋ฒ„ํŠผ", + "action": { + "type": "code_merge", + "errorMessage": "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + "successMessage": "์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + "mergeColumnName": "item_number" + }, + "variant": "primary", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 2 + }, + { + "id": "comp_btn_upload", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 88, "height": 40 }, + "position": { "x": 110, "y": 100, "z": 1 }, + "overrides": { + "text": "์—…๋กœ๋“œ", + "type": "v2-button-primary", + "label": "๊ธฐ๋ณธ ๋ฒ„ํŠผ", + "action": { + "type": "excel_upload", + "errorMessage": "์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + "successMessage": "์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + }, + "variant": "primary", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 3 + }, + { + "id": "comp_btn_download", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 88, "height": 40 }, + "position": { "x": 210, "y": 100, "z": 1 }, + "overrides": { + "text": "๋‹ค์šด๋กœ๋“œ", + "type": "v2-button-primary", + "label": "๊ธฐ๋ณธ ๋ฒ„ํŠผ", + "action": { + "type": "excel_download", + "errorMessage": "๋‹ค์šด๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + "successMessage": "๋‹ค์šด๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + }, + "variant": "primary", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 4 + }, + { + "id": "comp_btn_register", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1550, "y": 100, "z": 1 }, + "overrides": { + "text": "๋“ฑ๋ก", + "type": "v2-button-primary", + "label": "๊ธฐ๋ณธ ๋ฒ„ํŠผ", + "action": { + "type": "modal", + "modalSize": "lg", + "modalTitle": "ํ’ˆ๋ชฉ ๋“ฑ๋ก", + "targetScreenId": {{modal_screen_id}}, + "errorMessage": "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + "successMessage": "์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + }, + "variant": "primary", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 5 + }, + { + "id": "comp_btn_copy", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1640, "y": 100, "z": 1 }, + "overrides": { + "text": "๋ณต์‚ฌ", + "type": "v2-button-primary", + "label": "๊ธฐ๋ณธ ๋ฒ„ํŠผ", + "action": { + "type": "copy", + "modalSize": "lg", + "modalTitle": "ํ’ˆ๋ชฉ ๋ณต์‚ฌ", + "targetScreenId": {{modal_screen_id}}, + "errorMessage": "๋ณต์‚ฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + "successMessage": "๋ณต์‚ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + }, + "variant": "primary", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 6 + }, + { + "id": "comp_btn_edit", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1730, "y": 100, "z": 1 }, + "overrides": { + "text": "์ˆ˜์ •", + "type": "v2-button-primary", + "label": "๊ธฐ๋ณธ ๋ฒ„ํŠผ", + "action": { + "type": "edit", + "modalSize": "lg", + "modalTitle": "ํ’ˆ๋ชฉ ์ˆ˜์ •", + "targetScreenId": {{modal_screen_id}}, + "errorMessage": "์ˆ˜์ • ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + "successMessage": "์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + }, + "variant": "primary", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 7 + }, + { + "id": "comp_btn_delete", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1820, "y": 100, "z": 1 }, + "overrides": { + "text": "์‚ญ์ œ", + "type": "v2-button-primary", + "label": "๊ธฐ๋ณธ ๋ฒ„ํŠผ", + "action": { + "type": "delete", + "errorMessage": "์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + "successMessage": "์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + }, + "variant": "danger", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 8 + } + ] +} +``` + +### 8.3 ๋ชจ๋‹ฌ ํ™”๋ฉด (ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ •) + +#### ํ™”๋ฉด ์ •์˜ (ํ•„์ˆ˜ ์ž…๋ ฅ) + +```json +{ + "screenName": "ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ •", + "tableName": "item_info", + "companyCode": "COMPANY_7", + "description": "ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ • ํผ ํ™”๋ฉด" +} +``` + +#### ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ (screen_layouts_v2.layout_data) + +> ๐Ÿ“Œ **์‹ค์ œ ์ ์šฉ๋œ ๋ ˆ์ด์•„์›ƒ** - ํ’ˆ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ • ํผ (๋ชจ๋‹ฌ์šฉ) + +```json +{ + "version": "2.0", + "components": [ + { + "id": "comp_item_number", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "ํ’ˆ๋ฒˆ์ฝ”๋“œ", + "fieldName": "item_number", + "placeholder": "ํ’ˆ๋ฒˆ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”", + "required": true + }, + "displayOrder": 0 + }, + { + "id": "comp_item_name", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "ํ’ˆ๋ช…", + "fieldName": "item_name", + "placeholder": "ํ’ˆ๋ช…์„ ์ž…๋ ฅํ•˜์„ธ์š”", + "required": true + }, + "displayOrder": 1 + }, + { + "id": "comp_size", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 100, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "๊ทœ๊ฒฉ", + "fieldName": "size", + "placeholder": "๊ทœ๊ฒฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 2 + }, + { + "id": "comp_material", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 100, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "์žฌ์งˆ", + "fieldName": "material", + "placeholder": "์žฌ์งˆ์„ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 3 + }, + { + "id": "comp_inventory_unit", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 180, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "์žฌ๊ณ ๋‹จ์œ„", + "fieldName": "inventory_unit", + "placeholder": "์žฌ๊ณ ๋‹จ์œ„๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 4 + }, + { + "id": "comp_weight", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 180, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "์ค‘๋Ÿ‰", + "fieldName": "weight", + "placeholder": "์ค‘๋Ÿ‰์„ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 5 + }, + { + "id": "comp_unit", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 260, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "๋‹จ์œ„", + "fieldName": "unit", + "placeholder": "๋‹จ์œ„๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 6 + }, + { + "id": "comp_division", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 260, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "๊ตฌ๋ถ„", + "fieldName": "division", + "placeholder": "๊ตฌ๋ถ„์„ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 7 + }, + { + "id": "comp_type", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 340, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "์œ ํ˜•", + "fieldName": "type", + "placeholder": "์œ ํ˜•์„ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 8 + }, + { + "id": "comp_status", + "url": "@/lib/registry/components/v2-select", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 340, "z": 1 }, + "overrides": { + "type": "v2-select", + "label": "์ƒํƒœ", + "fieldName": "status", + "config": { + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "์ •์ƒ", "label": "์ •์ƒ" }, + { "value": "ํ’ˆ์ ˆ", "label": "ํ’ˆ์ ˆ" }, + { "value": "๋Œ€๊ธฐ", "label": "๋Œ€๊ธฐ" }, + { "value": "๋‹จ์ข…", "label": "๋‹จ์ข…" } + ] + } + }, + "displayOrder": 9 + }, + { + "id": "comp_btn_save", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 540, "y": 420, "z": 1 }, + "overrides": { + "text": "์ €์žฅ", + "type": "v2-button-primary", + "label": "์ €์žฅ ๋ฒ„ํŠผ", + "action": { + "type": "save", + "closeModalAfterSave": true, + "refreshParentTable": true, + "successMessage": "์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + "errorMessage": "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." + }, + "variant": "primary", + "actionType": "button" + }, + "displayOrder": 10 + } + ] +} +``` + +### 8.4 API ํ˜ธ์ถœ ๋ฐฉ์‹ + +> ๐Ÿ“Œ ์‹ค์ œ ํ™”๋ฉด ์ƒ์„ฑ์€ API๋ฅผ ํ†ตํ•ด ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค. `screen_code`๋Š” ์„œ๋ฒ„์—์„œ ์ž๋™ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. + +#### Step 1: ํ™”๋ฉด ์ฝ”๋“œ ์ž๋™ ์ƒ์„ฑ API + +```http +GET /api/screens/generate-code?companyCode=COMPANY_7 +``` + +**์‘๋‹ต:** +```json +{ + "success": true, + "data": { "screenCode": "COMPANY_7_4" } +} +``` + +#### Step 2: ํ™”๋ฉด ์ƒ์„ฑ API + +```http +POST /api/screens +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "screenName": "ํ’ˆ๋ชฉ์ •๋ณด", + "screenCode": "COMPANY_7_4", + "tableName": "item_info", + "companyCode": "COMPANY_7", + "description": "ํ’ˆ๋ชฉ ๊ธฐ๋ณธ์ •๋ณด ๊ด€๋ฆฌ ํ™”๋ฉด" +} +``` + +**์‘๋‹ต:** +```json +{ + "success": true, + "data": { + "screenId": 141, + "screenCode": "COMPANY_7_4", + "screenName": "ํ’ˆ๋ชฉ์ •๋ณด" + } +} +``` + +#### Step 3: ๋ ˆ์ด์•„์›ƒ ์ €์žฅ API + +```http +PUT /api/screens/141/layout-v2 +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "layoutData": { + "version": "2.0", + "components": [ /* 8.2์˜ components ๋ฐฐ์—ด */ ] + } +} +``` + +### 8.5 SQL ์ง์ ‘ INSERT (์‹ค์ œ ์ ์šฉ ๋ฐฉ์‹) + +> ๐Ÿ“Œ **์‹ค์ œ ํ…Œ์ŠคํŠธ ์™„๋ฃŒ๋œ ๋ฐฉ์‹**์ž…๋‹ˆ๋‹ค. Docker psql์„ ํ†ตํ•ด ์ง์ ‘ DB์— ์‚ฝ์ž…ํ•ฉ๋‹ˆ๋‹ค. + +#### Step 1: ๋ชจ๋‹ฌ ํ™”๋ฉด ๋จผ์ € ์ƒ์„ฑ (๋“ฑ๋ก/์ˆ˜์ • ํผ) + +```sql +-- ๋ชจ๋‹ฌ ํ™”๋ฉด ์ •์˜ +INSERT INTO screen_definitions ( + screen_code, screen_name, table_name, company_code, description, is_active +) VALUES ( + 'COMPANY_19_ITEM_MODAL', 'Item Register/Edit Modal', 'item_info', 'COMPANY_19', + 'Item registration and edit form modal', 'Y' +) RETURNING screen_id; +-- ์˜ˆ: screen_id = 3731 ๋ฐ˜ํ™˜๋จ +``` + +#### Step 2: ๋ชจ๋‹ฌ ๋ ˆ์ด์•„์›ƒ ์ €์žฅ + +```sql +-- ๋ ˆ์ด์•„์›ƒ JSON์„ ํŒŒ์ผ๋กœ ์ €์žฅ ํ›„ INSERT (ํ•œ๊ธ€ ์ธ์ฝ”๋”ฉ ๋ฌธ์ œ ๋ฐฉ์ง€) +INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data, created_at, updated_at) +VALUES ( + 3731, -- Step 1์—์„œ ๋ฐ˜ํ™˜๋œ screen_id + 'COMPANY_19', + '{"version":"2.0","components":[/* 8.3์˜ ๋ชจ๋‹ฌ components ๋ฐฐ์—ด */]}'::jsonb, + NOW(), NOW() +); +``` + +#### Step 3: ๋ฉ”์ธ ํ™”๋ฉด ์ƒ์„ฑ + +```sql +-- ๋ฉ”์ธ ํ™”๋ฉด ์ •์˜ +INSERT INTO screen_definitions ( + screen_code, screen_name, table_name, company_code, description, is_active +) VALUES ( + 'COMPANY_19_ITEM_INFO', 'Item Info', 'item_info', 'COMPANY_19', + 'Item master data management', 'Y' +) RETURNING screen_id; +-- ์˜ˆ: screen_id = 3730 ๋ฐ˜ํ™˜๋จ +``` + +#### Step 4: ๋ฉ”์ธ ๋ ˆ์ด์•„์›ƒ ์ €์žฅ (๋ชจ๋‹ฌ ์—ฐ๊ฒฐ) + +```sql +-- targetScreenId๋ฅผ Step 1์—์„œ ์ƒ์„ฑํ•œ ๋ชจ๋‹ฌ screen_id๋กœ ์น˜ํ™˜ +INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data, created_at, updated_at) +VALUES ( + 3730, -- Step 3์—์„œ ๋ฐ˜ํ™˜๋œ screen_id + 'COMPANY_19', + '{"version":"2.0","components":[/* 8.2์˜ components ๋ฐฐ์—ด, targetScreenId: 3731 */]}'::jsonb, + NOW(), NOW() +); +``` + +#### Step 5: ๋ฉ”๋‰ด์— ํ™”๋ฉด ์—ฐ๊ฒฐ (์„ ํƒ์‚ฌํ•ญ) + +```sql +-- ๊ธฐ์กด ๋ฉ”๋‰ด์— ํ™”๋ฉด ์—ฐ๊ฒฐ (screen_menu_assignments ํ…Œ์ด๋ธ” ์‚ฌ์šฉ) +INSERT INTO screen_menu_assignments (screen_id, menu_id, company_code, display_order) +VALUES (3730, 55566, 'COMPANY_19', 1); +``` + +### 8.6 ํ™”๋ฉด ์ƒ์„ฑ ์ˆœ์„œ (์ค‘์š”!) + +``` +1. ๋ชจ๋‹ฌ ํ™”๋ฉด ์ƒ์„ฑ (screen_definitions INSERT) + โ”‚ + โ–ผ +2. ๋ชจ๋‹ฌ ๋ ˆ์ด์•„์›ƒ ์ €์žฅ (screen_layouts_v2 INSERT) + โ”‚ + โ–ผ +3. ๋ฉ”์ธ ํ™”๋ฉด ์ƒ์„ฑ (screen_definitions INSERT) + โ”‚ + โ–ผ +4. ๋ฉ”์ธ ๋ ˆ์ด์•„์›ƒ ์ €์žฅ (screen_layouts_v2 INSERT) + โ””โ”€โ”€ targetScreenId์— ๋ชจ๋‹ฌ screen_id ์‚ฌ์šฉ! + โ”‚ + โ–ผ +5. (์„ ํƒ) ๋ฉ”๋‰ด์— ํ™”๋ฉด ์—ฐ๊ฒฐ +``` + +### 8.7 ์ฃผ์˜์‚ฌํ•ญ + +| ํ•ญ๋ชฉ | ์„ค๋ช… | +|------|------| +| `screen_code` | ํšŒ์‚ฌ๋ณ„ ๊ณ ์œ , ํ˜•์‹: `{COMPANY_CODE}_{์šฉ๋„}` (์˜ˆ: `COMPANY_19_ITEM_INFO`) | +| `screen_id` | AUTO INCREMENT, INSERT ํ›„ RETURNING์œผ๋กœ ํš๋“ | +| `component.id` | `comp_` prefix ๊ถŒ์žฅ, ํ™”๋ฉด ๋‚ด ์ค‘๋ณต ๋ถˆ๊ฐ€ | +| `component.url` | `@/lib/registry/components/v2-xxx` ํ˜•์‹ ์ •ํ™•ํžˆ ์‚ฌ์šฉ | +| `component.type` | `overrides.type`๊ณผ URL ๋งˆ์ง€๋ง‰ ๋ถ€๋ถ„ ์ผ์น˜ ํ•„์š” | +| `targetScreenId` | **์ˆซ์ž** (๋ฌธ์ž์—ด ์•„๋‹˜), ๋ชจ๋‹ฌ ํ™”๋ฉด ๋จผ์ € ์ƒ์„ฑ ํ•„์š” | +| `version` | ๋ฐ˜๋“œ์‹œ `"2.0"` ์‚ฌ์šฉ | +| `layout_data` | JSONB ํƒ€์ž…, ๋ณต์žกํ•œ JSON์€ ํŒŒ์ผ๋กœ ์ €์žฅ ํ›„ `-f` ์˜ต์…˜์œผ๋กœ ์‹คํ–‰ | +| UNIQUE ์ œ์•ฝ | `(screen_id, company_code)` ์กฐํ•ฉ์ด ์œ ๋‹ˆํฌ | +| ํ•œ๊ธ€ ์ฒ˜๋ฆฌ | Docker psql์—์„œ ํ•œ๊ธ€ ์ง์ ‘ ์ž…๋ ฅ ์‹œ ์ธ์ฝ”๋”ฉ ๋ฌธ์ œ โ†’ ์˜๋ฌธ ์‚ฌ์šฉ ๋˜๋Š” ํŒŒ์ผ ์‚ฌ์šฉ | + +### 8.8 ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ๋ ˆํผ๋Ÿฐ์Šค + +| ์ปดํฌ๋„ŒํŠธ | URL | type (overrides) | +|----------|-----|------------------| +| ํ…์ŠคํŠธ ์ž…๋ ฅ | `v2-input` | `v2-input` | +| ์„ ํƒ (๋“œ๋กญ๋‹ค์šด) | `v2-select` | `v2-select` | +| ๋‚ ์งœ ์ž…๋ ฅ | `v2-date` | `v2-date` | +| ๋ฒ„ํŠผ | `v2-button-primary` | `v2-button-primary` | +| ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | `v2-table-list` | `v2-table-list` | +| ๊ฒ€์ƒ‰ ์œ„์ ฏ | `v2-table-search-widget` | `v2-table-search-widget` | +| ํ…์ŠคํŠธ ํ‘œ์‹œ | `v2-text-display` | `v2-text-display` | +| ๊ตฌ๋ถ„์„  | `v2-divider-line` | `v2-divider-line` | +| ์„น์…˜ ์นด๋“œ | `v2-section-card` | `v2-section-card` | + +--- + +## 9. ํ™”๋ฉด ๊ตฌํ˜„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +> ๐Ÿ“‹ ์ƒˆ๋กœ์šด ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•  ๋•Œ ์•„๋ž˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ํ™•์ธํ•˜์„ธ์š”. + +### 9.1 ๋ถ„์„ ๋‹จ๊ณ„ + +| ์ฒดํฌ | ํ•ญ๋ชฉ | ์„ค๋ช… | +|:----:|------|------| +| โ˜ | **ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ๋ถ„์„** | ํ•ด๋‹น ํ™”๋ฉด์ด ์‚ฌ์šฉํ•  ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ํ™•์ธ (์ปฌ๋Ÿผ๋ช…, ํƒ€์ž…, ํ•„์ˆ˜ ์—ฌ๋ถ€) | +| โ˜ | **ํ™”๋ฉด ๊ธฐ๋Šฅ ํŒŒ์•…** | ์กฐํšŒ/๋“ฑ๋ก/์ˆ˜์ •/์‚ญ์ œ, ๊ฒ€์ƒ‰, ํ•„ํ„ฐ, ๊ทธ๋ฃนํ•‘ ๋“ฑ ํ•„์š” ๊ธฐ๋Šฅ ๋ชฉ๋กํ™” | +| โ˜ | **์ปดํฌ๋„ŒํŠธ ๋งคํ•‘** | ํ•„์š” ๊ธฐ๋Šฅ โ†’ V2 ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ (2.2 ๋ชฉ๋ก ์ฐธ์กฐ) | +| โ˜ | **๊ตฌํ˜„ ๋ถˆ๊ฐ€ ํ•ญ๋ชฉ ํ™•์ธ** | ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ๋กœ ๊ตฌํ˜„ ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ธฐ๋Šฅ ํŒŒ์•… | +| โ˜ | **๋Œ€์ฒด ๋ฐฉ์•ˆ ๊ฒ€ํ† ** | ๊ตฌํ˜„ ๋ถˆ๊ฐ€ ํ•ญ๋ชฉ์— ๋Œ€ํ•ด ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ ์กฐํ•ฉ์œผ๋กœ ๋Œ€์ฒด ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํ™•์ธ | + +### 9.2 ๋ฌธ์„œ ์ž‘์„ฑ ๋‹จ๊ณ„ + +| ์ฒดํฌ | ํ•ญ๋ชฉ | ์„ค๋ช… | +|:----:|------|------| +| โ˜ | **ํ…Œ์ด๋ธ” ์ •์˜ ์ž‘์„ฑ** | 1.1~1.2 ํ˜•์‹์œผ๋กœ ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ ์ •๋ณด ์ž‘์„ฑ | +| โ˜ | **๋ ˆ์ด์•„์›ƒ ๋ฐฐ์น˜๋„ ์ž‘์„ฑ** | 2.1 ํ˜•์‹์œผ๋กœ ASCII ๋‹ค์ด์–ด๊ทธ๋žจ ์ž‘์„ฑ | +| โ˜ | **์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก** | 2.3 ํ˜•์‹์œผ๋กœ ์ด ํ™”๋ฉด์—์„œ ์‚ฌ์šฉํ•  ์ปดํฌ๋„ŒํŠธ ์ •๋ฆฌ | +| โ˜ | **๋ชจ๋‹ฌ ํ™”๋ฉด ์ •์˜** | ๋“ฑ๋ก/์ˆ˜์ • ๋ชจ๋‹ฌ์ด ํ•„์š”ํ•˜๋ฉด ๋ณ„๋„ ๋ ˆ์ด์•„์›ƒ ์ž‘์„ฑ | +| โ˜ | **JSON ์„ค์ • ์ž‘์„ฑ** | 8.2~8.3 ํ˜•์‹์œผ๋กœ layout_data JSON ์ž‘์„ฑ | +| โ˜ | **๊ตฌํ˜„ ๋ถˆ๊ฐ€/๋Œ€์ฒด ๋ฐฉ์•ˆ ๋ช…์‹œ** | ํ•ด๋‹น ์‚ฌํ•ญ ์žˆ์œผ๋ฉด ๋ฌธ์„œ์— ์„น์…˜ ์ถ”๊ฐ€ | + +### 9.3 INSERT ์ „ ํ™•์ธ + +| ์ฒดํฌ | ํ•ญ๋ชฉ | ์„ค๋ช… | +|:----:|------|------| +| โ˜ | **company_code ํ™•์ธ** | ๋Œ€์ƒ ํšŒ์‚ฌ ์ฝ”๋“œ ์ •ํ™•ํ•œ์ง€ ํ™•์ธ | +| โ˜ | **screen_code ํ˜•์‹** | `{COMPANY_CODE}_{์šฉ๋„}` ํ˜•์‹ ์ค€์ˆ˜ | +| โ˜ | **๋ชจ๋‹ฌ ๋จผ์ € ์ƒ์„ฑ** | ๋ชจ๋‹ฌ ํ™”๋ฉด์ด ์žˆ์œผ๋ฉด ๋ฐ˜๋“œ์‹œ ๋จผ์ € INSERT | +| โ˜ | **JSON ํŒŒ์ผ ์ค€๋น„** | ๋ณต์žกํ•œ JSON์€ ํŒŒ์ผ๋กœ ์ €์žฅ (ํ•œ๊ธ€ ์ธ์ฝ”๋”ฉ ๋Œ€๋น„) | +| โ˜ | **์ปดํฌ๋„ŒํŠธ ID ๊ณ ์œ ์„ฑ** | `comp_` prefix, ํ™”๋ฉด ๋‚ด ์ค‘๋ณต ์—†๋Š”์ง€ ํ™•์ธ | +| โ˜ | **์ปดํฌ๋„ŒํŠธ URL/type ์ผ์น˜** | `url`์˜ ๋งˆ์ง€๋ง‰ ๋ถ€๋ถ„๊ณผ `overrides.type` ๋™์ผํ•œ์ง€ ํ™•์ธ | +| โ˜ | **targetScreenId ์น˜ํ™˜** | ๋ชจ๋‹ฌ screen_id๋ฅผ ์ˆซ์ž๋กœ ์ •ํ™•ํžˆ ์ž…๋ ฅ | + +### 9.4 INSERT ํ›„ ๊ฒ€์ฆ + +| ์ฒดํฌ | ํ•ญ๋ชฉ | ์„ค๋ช… | +|:----:|------|------| +| โ˜ | **ํ™”๋ฉด ์ ‘์† ํ…Œ์ŠคํŠธ** | `/screens/{screen_id}` URL๋กœ ์ ‘์† | +| โ˜ | **์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ํ™•์ธ** | ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๊ฐ€ "๋ฏธ๊ตฌํ˜„" ์—†์ด ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธ | +| โ˜ | **๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ** | ๊ฒ€์ƒ‰ ์œ„์ ฏ ๋™์ž‘ ํ™•์ธ | +| โ˜ | **ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋กœ๋“œ** | ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธ | +| โ˜ | **๋ฒ„ํŠผ ๋™์ž‘ ํ…Œ์ŠคํŠธ** | ๋“ฑ๋ก/์ˆ˜์ •/์‚ญ์ œ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋ชจ๋‹ฌ/์•ก์…˜ ๋™์ž‘ ํ™•์ธ | +| โ˜ | **๋ชจ๋‹ฌ ํผ ํ…Œ์ŠคํŠธ** | ๋ชจ๋‹ฌ ์—ด๋ฆผ, ์ž…๋ ฅ ํ•„๋“œ ํ‘œ์‹œ, ์ €์žฅ ๋™์ž‘ ํ™•์ธ | +| โ˜ | **๋ฉ”๋‰ด ์—ฐ๊ฒฐ ํ™•์ธ** | (์—ฐ๊ฒฐํ•œ ๊ฒฝ์šฐ) ๋ฉ”๋‰ด์—์„œ ํ™”๋ฉด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธ | + +### 9.5 ๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ํ™•์ธ ์‚ฌํ•ญ + +| ์ฆ์ƒ | ํ™•์ธ ์‚ฌํ•ญ | +|------|-----------| +| ํ™”๋ฉด์ด ์•ˆ ๋ณด์ž„ | `screen_layouts_v2`์— ๋ฐ์ดํ„ฐ ์žˆ๋Š”์ง€ ํ™•์ธ, `company_code` ์ผ์น˜ ์—ฌ๋ถ€ | +| "๋ฏธ๊ตฌํ˜„ ์ปดํฌ๋„ŒํŠธ" ํ‘œ์‹œ | `url`๊ณผ `overrides.type` ์ผ์น˜ ์—ฌ๋ถ€, ์ปดํฌ๋„ŒํŠธ๋ช… ์˜คํƒ€ ํ™•์ธ | +| ๋ชจ๋‹ฌ์ด ์•ˆ ์—ด๋ฆผ | `targetScreenId`๊ฐ€ ์ˆซ์ž์ธ์ง€, ํ•ด๋‹น screen_id ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ | +| ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์—†์Œ | `selectedTable` ๊ฐ’ ํ™•์ธ, ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ ์กด์žฌ ์—ฌ๋ถ€ | +| ๋ฒ„ํŠผ ๋™์ž‘ ์•ˆ ํ•จ | `action.type` ๊ฐ’ ํ™•์ธ, `actionType: "button"` ์„ค์ • ์—ฌ๋ถ€ | diff --git a/docs/screen-implementation-guide/02_sales/customer.md b/docs/screen-implementation-guide/02_sales/customer.md new file mode 100644 index 00000000..7699f798 --- /dev/null +++ b/docs/screen-implementation-guide/02_sales/customer.md @@ -0,0 +1,256 @@ +# ๊ฑฐ๋ž˜์ฒ˜๊ด€๋ฆฌ ํ™”๋ฉด ๊ตฌํ˜„ ๊ฐ€์ด๋“œ + +> **ํ™”๋ฉด๋ช…**: ๊ฑฐ๋ž˜์ฒ˜๊ด€๋ฆฌ +> **ํŒŒ์ผ**: ๊ฑฐ๋ž˜์ฒ˜๊ด€๋ฆฌ.html +> **๋ถ„๋ฅ˜**: ์˜์—…๊ด€๋ฆฌ +> **๊ตฌํ˜„ ๊ฐ€๋Šฅ**: โš ๏ธ ๋ถ€๋ถ„ (๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” ํ•„์š”) + +--- + +## 1. ํ™”๋ฉด ๊ฐœ์š” + +๊ณ ๊ฐ์‚ฌ ๋ฐ ๊ณต๊ธ‰์—…์ฒด ์ •๋ณด๋ฅผ ํ†ตํ•ฉ ๊ด€๋ฆฌํ•˜๋Š” ํ™”๋ฉด์ž…๋‹ˆ๋‹ค. + +### ํ•ต์‹ฌ ๊ธฐ๋Šฅ +- ๊ฑฐ๋ž˜์ฒ˜ ๋ชฉ๋ก ์กฐํšŒ/๊ฒ€์ƒ‰ +- ๊ทธ๋ฃนํ™” ๊ธฐ๋Šฅ (๊ฑฐ๋ž˜์ฒ˜์œ ํ˜•, ์ง€์—ญ๋ณ„) +- ๊ฑฐ๋ž˜์ฒ˜ ๋“ฑ๋ก/์ˆ˜์ •/์‚ญ์ œ +- ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ํ’ˆ๋ชฉ์ฝ”๋“œ/๋‹จ๊ฐ€ ๊ด€๋ฆฌ +- ๋‹ด๋‹น์ž ์ •๋ณด ๊ด€๋ฆฌ + +--- + +## 2. ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๊ฑฐ๋ž˜์ฒ˜์ฝ”๋“œ] [๊ฑฐ๋ž˜์ฒ˜๋ช…] [๊ฑฐ๋ž˜์ฒ˜์œ ํ˜•โ–ผ] [์‚ฌ์šฉ์—ฌ๋ถ€โ–ผ] [์ดˆ๊ธฐํ™”][์กฐํšŒ] โ”‚ +โ”‚ [์‚ฌ์šฉ์ž์˜ต์…˜][์—…๋กœ๋“œ][๋‹ค์šด๋กœ๋“œ]โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ๐Ÿ“‹ ๊ฑฐ๋ž˜์ฒ˜ ๋ชฉ๋ก โ”‚ [๊ธฐ๋ณธ์ •๋ณด][ํ’ˆ๋ชฉ์ฝ”๋“œ][๋‹จ๊ฐ€์ •๋ณด][๋‹ด๋‹น์ž] โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ Group by: [๊ฑฐ๋ž˜์ฒ˜์œ ํ˜•โ–ผ] โ”‚ ๊ฑฐ๋ž˜์ฒ˜์ฝ”๋“œ: C-001 โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ๊ฑฐ๋ž˜์ฒ˜๋ช…: (์ฃผ)ํ…Œ์ŠคํŠธ โ”‚ +โ”‚ โ”‚โ–ผ ๊ณ ๊ฐ์‚ฌ (15) โ”‚ โ”‚ ์‚ฌ์—…์ž๋ฒˆํ˜ธ: 123-45-67890 โ”‚ +โ”‚ โ”‚ C-001 | A์‚ฌ โ”‚ โ”‚ ๋Œ€ํ‘œ์ž: ํ™๊ธธ๋™ โ”‚ +โ”‚ โ”‚ C-002 | B์‚ฌ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚โ–ผ ๊ณต๊ธ‰์—…์ฒด (8) โ”‚ โ”‚ [ํ’ˆ๋ชฉ์ฝ”๋“œ ํƒญ ๋‚ด์šฉ] โ”‚ +โ”‚ โ”‚ S-001 | ์›์ž์žฌ์‚ฌ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚๊ฑฐ๋ž˜์ฒ˜ํ’ˆ๋ชฉ์ฝ”๋“œ|ํ’ˆ๋ชฉ๋ช…|์ž์‚ฌํ’ˆ๋ชฉ์ฝ”๋“œโ”‚ โ”‚ +โ”‚ โ”‚ โ”‚CP-001 |์›๋ฃŒA |M-001 โ”‚ โ”‚ +โ”‚ ๋ฆฌ์‚ฌ์ด์ € โ†” โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 3. V2 ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +| HTML ์˜์—ญ | V2 ์ปดํฌ๋„ŒํŠธ | ์ƒํƒœ | +|-----------|-------------|------| +| ๊ฒ€์ƒ‰ ์„น์…˜ | `v2-table-search-widget` | โœ… ๊ฐ€๋Šฅ | +| ๊ฑฐ๋ž˜์ฒ˜ ๋ชฉ๋ก (๊ทธ๋ฃนํ™”) | `v2-table-list` | โš ๏ธ ๊ทธ๋ฃนํ™” ๋ฏธ์ง€์› | +| ๋ถ„ํ•  ํŒจ๋„ | `v2-split-panel-layout` | โœ… ๊ฐ€๋Šฅ | +| ์ƒ์„ธ ํƒญ | `v2-tabs-widget` | โœ… ๊ฐ€๋Šฅ | + +--- + +## 4. ํ…Œ์ด๋ธ” ์ •์˜ + +### 4.1 ๊ฑฐ๋ž˜์ฒ˜ ๋ชฉ๋ก + +```typescript +columns: [ + { id: 'checkbox', type: 'checkbox', width: 50 }, + { id: 'customer_code', label: '๊ฑฐ๋ž˜์ฒ˜์ฝ”๋“œ', width: 100 }, + { id: 'customer_name', label: '๊ฑฐ๋ž˜์ฒ˜๋ช…', width: 200 }, + { id: 'customer_type', label: '๊ฑฐ๋ž˜์ฒ˜์œ ํ˜•', width: 100 }, + { id: 'business_no', label: '์‚ฌ์—…์ž๋ฒˆํ˜ธ', width: 120 }, + { id: 'ceo_name', label: '๋Œ€ํ‘œ์ž', width: 100 }, + { id: 'tel', label: '์ „ํ™”๋ฒˆํ˜ธ', width: 120 }, + { id: 'status', label: '์‚ฌ์šฉ์—ฌ๋ถ€', width: 80 } +] +``` + +### 4.2 ํ’ˆ๋ชฉ์ฝ”๋“œ ํƒญ + +```typescript +itemCodeColumns: [ + { id: 'customer_item_code', label: '๊ฑฐ๋ž˜์ฒ˜ํ’ˆ๋ชฉ์ฝ”๋“œ', width: 150 }, + { id: 'item_name', label: 'ํ’ˆ๋ชฉ๋ช…', width: 200 }, + { id: 'our_item_code', label: '์ž์‚ฌํ’ˆ๋ชฉ์ฝ”๋“œ', width: 150 }, + { id: 'spec', label: '๊ทœ๊ฒฉ', width: 150 } +] +``` + +### 4.3 ๋‹จ๊ฐ€์ •๋ณด ํƒญ + +```typescript +priceColumns: [ + { id: 'item_code', label: 'ํ’ˆ๋ชฉ์ฝ”๋“œ', width: 120 }, + { id: 'item_name', label: 'ํ’ˆ๋ชฉ๋ช…', width: 200 }, + { id: 'unit_price', label: '๋‹จ๊ฐ€', width: 100, format: 'currency' }, + { id: 'currency', label: 'ํ†ตํ™”', width: 60 }, + { id: 'apply_date', label: '์ ์šฉ์ผ', width: 100 }, + { id: 'remark', label: '๋น„๊ณ ', width: 150 } +] +``` + +--- + +## 5. ๊ทธ๋ฃนํ™” ๊ธฐ๋Šฅ (์‹ ๊ทœ ์ปดํฌ๋„ŒํŠธ ํ•„์š”) + +### 5.1 ๊ทธ๋ฃนํ™” ์˜ต์…˜ + +```typescript +groupByOptions: [ + { id: 'customer_type', label: '๊ฑฐ๋ž˜์ฒ˜์œ ํ˜•' }, + { id: 'region', label: '์ง€์—ญ' }, + { id: 'status', label: '์‚ฌ์šฉ์—ฌ๋ถ€' } +] +``` + +### 5.2 ๊ทธ๋ฃน ํ—ค๋” ํ‘œ์‹œ + +``` +โ–ผ ๊ณ ๊ฐ์‚ฌ (15) โ† ๊ทธ๋ฃน๋ช… + ๊ฑด์ˆ˜ + โ”‚ C-001 โ”‚ (์ฃผ)A์‚ฌ โ”‚ ... + โ”‚ C-002 โ”‚ (์ฃผ)B์‚ฌ โ”‚ ... +โ–ผ ๊ณต๊ธ‰์—…์ฒด (8) + โ”‚ S-001 โ”‚ ์›์ž์žฌ์‚ฌ โ”‚ ... +``` + +### 5.3 ํ•„์š” ์ธํ„ฐ๋ž™์…˜ + +- ๊ทธ๋ฃน ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ +- ๊ทธ๋ฃน ์ „์ฒด ์„ ํƒ ์ฒดํฌ๋ฐ•์Šค +- ๋‹ค์ค‘ ๊ทธ๋ฃนํ•‘ (์„ ํƒ) + +--- + +## 6. ํƒญ ๊ตฌ์„ฑ + +```typescript +tabs: [ + { + id: 'basic', + label: '๊ธฐ๋ณธ์ •๋ณด', + fields: [ + { id: 'customer_code', label: '๊ฑฐ๋ž˜์ฒ˜์ฝ”๋“œ' }, + { id: 'customer_name', label: '๊ฑฐ๋ž˜์ฒ˜๋ช…' }, + { id: 'customer_type', label: '๊ฑฐ๋ž˜์ฒ˜์œ ํ˜•' }, + { id: 'business_no', label: '์‚ฌ์—…์ž๋ฒˆํ˜ธ' }, + { id: 'ceo_name', label: '๋Œ€ํ‘œ์ž' }, + { id: 'address', label: '์ฃผ์†Œ' }, + { id: 'tel', label: '์ „ํ™”๋ฒˆํ˜ธ' }, + { id: 'fax', label: 'ํŒฉ์Šค' }, + { id: 'email', label: '์ด๋ฉ”์ผ' } + ] + }, + { + id: 'item_codes', + label: 'ํ’ˆ๋ชฉ์ฝ”๋“œ', + type: 'table', + entityId: 'customer_item_mapping' + }, + { + id: 'prices', + label: '๋‹จ๊ฐ€์ •๋ณด', + type: 'table', + entityId: 'customer_prices' + }, + { + id: 'contacts', + label: '๋‹ด๋‹น์ž', + type: 'table', + entityId: 'customer_contacts' + } +] +``` + +--- + +## 7. ํ˜„์žฌ ๊ตฌํ˜„ ๊ฐ€๋Šฅ ๋ฒ”์œ„ + +### โœ… ๊ฐ€๋Šฅ +- ๊ฒ€์ƒ‰ ์˜์—ญ +- ๋ถ„ํ•  ํŒจ๋„ ๋ ˆ์ด์•„์›ƒ +- ์ƒ์„ธ ํƒญ +- ํ’ˆ๋ชฉ์ฝ”๋“œ/๋‹จ๊ฐ€/๋‹ด๋‹น์ž ํ…Œ์ด๋ธ” + +### โš ๏ธ ๋ถ€๋ถ„ ๊ฐ€๋Šฅ +- ๊ฑฐ๋ž˜์ฒ˜ ๋ชฉ๋ก: ๊ทธ๋ฃนํ™” ์—†์ด ์ผ๋ฐ˜ ํ…Œ์ด๋ธ”๋กœ ๊ตฌํ˜„ + +### โŒ ๋ถˆ๊ฐ€๋Šฅ +- ๋™์  ๊ทธ๋ฃนํ™” (๊ทธ๋ฃน ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ) + +--- + +## 8. ๊ฐ„์†Œํ™” ๊ตฌํ˜„ JSON (๊ทธ๋ฃนํ™” ์ œ์™ธ) + +```json +{ + "screen_code": "CUSTOMER_MAIN", + "screen_name": "๊ฑฐ๋ž˜์ฒ˜๊ด€๋ฆฌ", + "components": [ + { + "type": "v2-table-search-widget", + "config": { + "searchFields": [ + { "type": "input", "id": "customer_code", "placeholder": "๊ฑฐ๋ž˜์ฒ˜์ฝ”๋“œ" }, + { "type": "input", "id": "customer_name", "placeholder": "๊ฑฐ๋ž˜์ฒ˜๋ช…" }, + { "type": "select", "id": "customer_type", "placeholder": "๊ฑฐ๋ž˜์ฒ˜์œ ํ˜•", + "options": [ + { "value": "customer", "label": "๊ณ ๊ฐ์‚ฌ" }, + { "value": "supplier", "label": "๊ณต๊ธ‰์—…์ฒด" }, + { "value": "both", "label": "๊ณ ๊ฐ์‚ฌ/๊ณต๊ธ‰์—…์ฒด" } + ] + }, + { "type": "select", "id": "status", "placeholder": "์‚ฌ์šฉ์—ฌ๋ถ€" } + ] + } + }, + { + "type": "v2-split-panel-layout", + "config": { + "masterPanel": { + "title": "๊ฑฐ๋ž˜์ฒ˜ ๋ชฉ๋ก", + "entityId": "customer_master", + "columns": [ + { "id": "customer_code", "label": "๊ฑฐ๋ž˜์ฒ˜์ฝ”๋“œ", "width": 100 }, + { "id": "customer_name", "label": "๊ฑฐ๋ž˜์ฒ˜๋ช…", "width": 200 }, + { "id": "customer_type", "label": "๊ฑฐ๋ž˜์ฒ˜์œ ํ˜•", "width": 100 }, + { "id": "ceo_name", "label": "๋Œ€ํ‘œ์ž", "width": 100 } + ] + }, + "detailPanel": { + "tabs": [ + { "id": "basic", "label": "๊ธฐ๋ณธ์ •๋ณด", "type": "form" }, + { "id": "items", "label": "ํ’ˆ๋ชฉ์ฝ”๋“œ", "type": "table", "entityId": "customer_items" }, + { "id": "prices", "label": "๋‹จ๊ฐ€์ •๋ณด", "type": "table", "entityId": "customer_prices" }, + { "id": "contacts", "label": "๋‹ด๋‹น์ž", "type": "table", "entityId": "customer_contacts" } + ] + } + } + } + ] +} +``` + +--- + +## 9. v2-grouped-table ๊ฐœ๋ฐœ ์‹œ ์ถ”๊ฐ€ ๊ตฌํ˜„ + +```typescript +// ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” ์„ค์ • +groupedTableConfig: { + groupBy: 'customer_type', + groupByOptions: ['customer_type', 'region', 'status'], + showGroupCount: true, + expandAll: false, + groupCheckbox: true +} +``` + +**์˜ˆ์ƒ ์žฌํ™œ์šฉ**: `v2-grouped-table`์€ 5๊ฐœ ์ด์ƒ ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ +- ๊ฑฐ๋ž˜์ฒ˜๊ด€๋ฆฌ, ํ’ˆ๋ชฉ์ •๋ณด, ์ž‘์—…์ง€์‹œ, ์ž…์ถœ๊ณ ๊ด€๋ฆฌ, ๊ฒฌ์ ๊ด€๋ฆฌ diff --git a/docs/screen-implementation-guide/02_sales/order.md b/docs/screen-implementation-guide/02_sales/order.md new file mode 100644 index 00000000..ff302b67 --- /dev/null +++ b/docs/screen-implementation-guide/02_sales/order.md @@ -0,0 +1,1276 @@ +# ์ˆ˜์ฃผ๊ด€๋ฆฌ (Sales Order Management) + +> Screen ID: /screens/156 +> ๋ฉ”๋‰ด ๊ฒฝ๋กœ: ์˜์—…๊ด€๋ฆฌ > ์ˆ˜์ฃผ๊ด€๋ฆฌ +> ํ…Œ์ด๋ธ”: `sales_order_mng` + +--- + +## โš ๏ธ ๋ฌธ์„œ ์‚ฌ์šฉ ์•ˆ๋‚ด + +> **์ด ๋ฌธ์„œ๋Š” "์ˆ˜์ฃผ๊ด€๋ฆฌ" ํ™”๋ฉด์˜ ๊ตฌํ˜„ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.** +> +> ### ๐Ÿ“Œ ์ค‘์š”: JSON ๋ฐ์ดํ„ฐ๋Š” ์ฐธ๊ณ ์šฉ์ž…๋‹ˆ๋‹ค! +> +> ์ด ๋ฌธ์„œ์— ํฌํ•จ๋œ JSON ์„ค์ •(๋ ˆ์ด์•„์›ƒ, ์ปดํฌ๋„ŒํŠธ ๊ตฌ์„ฑ ๋“ฑ)์€ **์ˆ˜์ฃผ๊ด€๋ฆฌ ํ™”๋ฉด์— ํŠนํ™”๋œ ์˜ˆ์‹œ**์ž…๋‹ˆ๋‹ค. +> +> **๋‹ค๋ฅธ ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•  ๋•Œ:** +> 1. ์ด JSON์„ ๊ทธ๋Œ€๋กœ ๋ณต์‚ฌํ•ด์„œ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š” +> 2. ํ•ด๋‹น ํ™”๋ฉด์˜ **ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋ฅผ ๋จผ์ € ๋ถ„์„**ํ•˜์„ธ์š” +> 3. ํ™”๋ฉด์˜ **์š”๊ตฌ์‚ฌํ•ญ๊ณผ ๊ธฐ๋Šฅ์„ ํŒŒ์•…**ํ•˜์„ธ์š” +> 4. ๋ถ„์„ ๊ฒฐ๊ณผ์— ๋งž๋Š” **์ƒˆ๋กœ์šด JSON ๊ตฌ์กฐ๋ฅผ ์ž‘์„ฑ**ํ•˜์„ธ์š” + +--- + +## 1. ํ…Œ์ด๋ธ” ์„ ํƒ ๋ฐ ํ™”๋ฉด ๊ตฌ์กฐ + +### 1.1 ์‚ฌ์šฉ ํ…Œ์ด๋ธ” + +| ํ…Œ์ด๋ธ”๋ช… | ์šฉ๋„ | ๋น„๊ณ  | +|----------|------|------| +| `sales_order_mng` | ์ˆ˜์ฃผ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ | ์ฃผ ํ…Œ์ด๋ธ” | +| `customer_mng` | ๊ฑฐ๋ž˜์ฒ˜ ์ •๋ณด | FK: partner_id | +| `item_info` | ํ’ˆ๋ชฉ ์ •๋ณด | FK: part_code | + +### 1.2 ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •์˜ (์‹ค์ œ DB ๊ธฐ์ค€) + +| ์ปฌ๋Ÿผ๋ช… | ํ‘œ์‹œ๋ช… | ํƒ€์ž… | ํ•„์ˆ˜ | ์„ค๋ช… | +|--------|--------|------|------|------| +| `id` | ID | integer | PK | ์ž๋™ ์ƒ์„ฑ (์‹œํ€€์Šค) | +| `company_code` | ํšŒ์‚ฌ์ฝ”๋“œ | varchar | โœ… | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ | +| `order_no` | ์ˆ˜์ฃผ๋ฒˆํ˜ธ | varchar | โœ… | ์ˆ˜์ฃผ ๊ณ ์œ  ์ฝ”๋“œ | +| `order_date` | ์ˆ˜์ฃผ์ผ | date | | ์ˆ˜์ฃผ ๋“ฑ๋ก์ผ | +| `due_date` | ๋‚ฉ๊ธฐ์ผ | date | | ๋‚ฉํ’ˆ ์˜ˆ์ •์ผ | +| `partner_id` | ๊ฑฐ๋ž˜์ฒ˜ID | varchar | | ๊ฑฐ๋ž˜์ฒ˜ ์ฝ”๋“œ (FK) | +| `delivery_partner_id` | ๋‚ฉํ’ˆ์ฒ˜ID | varchar | | ๋‚ฉํ’ˆ์ฒ˜ ์ฝ”๋“œ | +| `delivery_address` | ๋‚ฉํ’ˆ์žฅ์†Œ | text | | ๋‚ฉํ’ˆ ์ฃผ์†Œ | +| `shipping_method` | ๋ฐฐ์†ก๋ฐฉ๋ฒ• | varchar | | ํƒ๋ฐฐ, ํ™”๋ฌผ, ์ง์†ก ๋“ฑ | +| `part_code` | ํ’ˆ๋ชฉ์ฝ”๋“œ | varchar | | ํ’ˆ๋ชฉ ์ฝ”๋“œ (FK) | +| `part_name` | ํ’ˆ๋ช… | varchar | | ํ’ˆ๋ชฉ๋ช… | +| `spec` | ๊ทœ๊ฒฉ | varchar | | ๊ทœ๊ฒฉ ์ •๋ณด | +| `material` | ์žฌ์งˆ | varchar | | ์žฌ์งˆ ์ •๋ณด | +| `order_qty` | ์ˆ˜์ฃผ์ˆ˜๋Ÿ‰ | numeric | | ๊ธฐ๋ณธ๊ฐ’ 0 | +| `ship_qty` | ์ถœํ•˜์ˆ˜๋Ÿ‰ | numeric | | ๊ธฐ๋ณธ๊ฐ’ 0 | +| `balance_qty` | ์ž”๋Ÿ‰ | numeric | | ๊ธฐ๋ณธ๊ฐ’ 0 (์ˆ˜์ฃผ์ˆ˜๋Ÿ‰ - ์ถœํ•˜์ˆ˜๋Ÿ‰) | +| `inventory_qty` | ํ˜„์žฌ๊ณ  | numeric | | ๊ธฐ๋ณธ๊ฐ’ 0 | +| `plan_ship_qty` | ์ถœํ•˜๊ณ„ํš๋Ÿ‰ | numeric | | ๊ธฐ๋ณธ๊ฐ’ 0 | +| `unit_price` | ๋‹จ๊ฐ€ | numeric | | ๊ธฐ๋ณธ๊ฐ’ 0 | +| `total_amount` | ๊ธˆ์•ก | numeric | | ๊ธฐ๋ณธ๊ฐ’ 0 (์ˆ˜์ฃผ์ˆ˜๋Ÿ‰ ร— ๋‹จ๊ฐ€) | +| `status` | ์ƒํƒœ | varchar | | ์ˆ˜์ฃผ, ์ง„ํ–‰์ค‘, ์™„๋ฃŒ, ์ทจ์†Œ (๊ธฐ๋ณธ๊ฐ’: ์ˆ˜์ฃผ) | +| `manager_id` | ๋‹ด๋‹น์žID | varchar | | ๋‹ด๋‹น์ž ID | +| `manager_name` | ๋‹ด๋‹น์ž๋ช… | varchar | | ๋‹ด๋‹น์ž ์ด๋ฆ„ | +| `memo` | ๋ฉ”๋ชจ | text | | ๋น„๊ณ  | +| `sales_type` | ์˜์—…์œ ํ˜• | varchar | | ๋‚ด์ˆ˜, ์ˆ˜์ถœ ๋“ฑ | +| `part_name_eng` | ํ’ˆ๋ช…(์˜๋ฌธ) | varchar | | ์˜๋ฌธ ํ’ˆ๋ชฉ๋ช… | +| `item_due_date` | ํ’ˆ๋ชฉ๋‚ฉ๊ธฐ์ผ | varchar | | ํ’ˆ๋ชฉ๋ณ„ ๋‚ฉ๊ธฐ์ผ | +| `incoterms` | ์ธ์ฝ”ํ…€์ฆˆ | varchar | | ๋ฌด์—ญ์กฐ๊ฑด (์ˆ˜์ถœ์šฉ) | +| `payment_term` | ๊ฒฐ์ œ์กฐ๊ฑด | varchar | | ๊ฒฐ์ œ ์กฐ๊ฑด | +| `port_of_loading` | ์„ ์ ํ•ญ | varchar | | ์„ ์  ํ•ญ๊ตฌ (์ˆ˜์ถœ์šฉ) | +| `port_of_discharge` | ๋„์ฐฉํ•ญ | varchar | | ๋„์ฐฉ ํ•ญ๊ตฌ (์ˆ˜์ถœ์šฉ) | +| `hs_code` | HS์ฝ”๋“œ | varchar | | ๊ด€์„ธ ์ฝ”๋“œ (์ˆ˜์ถœ์šฉ) | +| `currency` | ํ†ตํ™” | varchar | | ํ†ตํ™” ์ฝ”๋“œ | +| `created_date` | ๋“ฑ๋ก์ผ | timestamp | | ์ž๋™ ์ƒ์„ฑ | +| `created_by` | ๋“ฑ๋ก์ž | varchar | | ๋“ฑ๋ก์ž ID | +| `updated_date` | ์ˆ˜์ •์ผ | timestamp | | ์ž๋™ ๊ฐฑ์‹  | +| `updated_by` | ์ˆ˜์ •์ž | varchar | | ์ˆ˜์ •์ž ID | +| `writer` | ์ž‘์„ฑ์ž | varchar | | ์ž‘์„ฑ์ž ID | + +### 1.3 ํ™”๋ฉด ๊ตฌ์กฐ ๊ฐœ์š” + +- **ํ™”๋ฉด ์œ ํ˜•**: ๋ชฉ๋กํ˜• (๋‹จ์ผ ํ…Œ์ด๋ธ” CRUD) +- **์ฃผ์š” ๊ธฐ๋Šฅ**: + - ์ˆ˜์ฃผ ์กฐํšŒ/๊ฒ€์ƒ‰/ํ•„ํ„ฐ๋ง + - ์ˆ˜์ฃผ ๋“ฑ๋ก/์ˆ˜์ •/์‚ญ์ œ + - ๊ทธ๋ฃนํ•‘ (Group By) + - ์ถœํ•˜๊ณ„ํš ์—ฐ๋™ + - ์—‘์…€ ์—…๋กœ๋“œ/๋‹ค์šด๋กœ๋“œ + - ํ†ต๊ณ„ ํ‘œ์‹œ (์ด ๊ธˆ์•ก, ์ด ์ˆ˜๋Ÿ‰) + +--- + +## 2. ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜๋„ + +### 2.1 ์ „์ฒด ๋ ˆ์ด์•„์›ƒ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๊ฒ€์ƒ‰ ์˜์—ญ] โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-search-widget โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ์ˆ˜์ฃผ๋ฒˆํ˜ธ โ”‚ โ”‚ ๊ฑฐ๋ž˜์ฒ˜ โ”‚ โ”‚ ํ’ˆ๋ชฉ๋ช… โ”‚ โ”‚ ์ƒํƒœ โ”‚ โ”‚ ์ˆ˜์ฃผ์ผ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ (text) โ”‚ โ”‚ (select) โ”‚ โ”‚ (text) โ”‚ โ”‚ (select) โ”‚ โ”‚ (date) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ์‚ฌ์šฉ์ž โ”‚ โ”‚ ์—‘์…€ โ”‚ โ”‚์—‘์…€ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ์˜ต์…˜ โ”‚ โ”‚ ์—…๋กœ๋“œ โ”‚ โ”‚๋‹ค์šด โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [ํ…Œ์ด๋ธ” ํ—ค๋” + ์•ก์…˜ ๋ฒ„ํŠผ + ํ†ต๊ณ„] โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿ“‹ ์ˆ˜์ฃผ ๋ชฉ๋ก (10) ์ด ๊ธˆ์•ก: 1,234,000์› ์ด ์ˆ˜๋Ÿ‰: 5,000๊ฐœ [Group by โ–ผ]โ”‚ โ”‚ +โ”‚ โ”‚ [์ˆ˜์ฃผ๋“ฑ๋ก][์ˆ˜์ •][์‚ญ์ œ][์ถœํ•˜๊ณ„ํš] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ”] โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-table-list โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚โ˜ โ”‚์ˆ˜์ฃผ๋ฒˆํ˜ธโ”‚๊ฑฐ๋ž˜์ฒ˜ โ”‚ํ’ˆ๋ชฉ์ฝ”๋“œโ”‚ํ’ˆ๋ช… โ”‚๊ทœ๊ฒฉ โ”‚์žฌ์งˆ โ”‚๋‹จ์œ„ โ”‚์ˆ˜์ฃผ์ˆ˜๋Ÿ‰โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”œโ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚โ˜ โ”‚ORD-001 โ”‚์‚ผ์„ฑ์ „์žโ”‚ITEM001 โ”‚๋ณผํŠธ M8 โ”‚M8x20 โ”‚SUS304โ”‚EA โ”‚1,000 โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚โ˜ โ”‚ORD-002 โ”‚LG์ „์ž โ”‚ITEM002 โ”‚๋„ˆํŠธ M8 โ”‚M8 โ”‚SUS304โ”‚EA โ”‚2,000 โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ (์ˆ˜ํ‰ ์Šคํฌ๋กค: ์ถœํ•˜์ˆ˜๋Ÿ‰, ์ž”๋Ÿ‰, ํ˜„์žฌ๊ณ , ์ถœํ•˜๊ณ„ํš๋Ÿ‰, ๋‹จ๊ฐ€, ๊ธˆ์•ก, ๋‚ฉํ’ˆ์ฒ˜, โ”‚ โ”‚ +โ”‚ โ”‚ ๋‚ฉํ’ˆ์žฅ์†Œ, ๋ฐฐ์†ก๋ฐฉ๋ฒ•, ๋‚ฉ๊ธฐ์ผ, ์ˆ˜์ฃผ์ผ, ์ƒํƒœ, ๋‹ด๋‹น์ž, ๋ฉ”๋ชจ) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2.2 ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ V2 ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก + +> ๐Ÿ“Œ **V2 ์ปดํฌ๋„ŒํŠธ ์ „์ฒด ๋ชฉ๋ก** - ํ™”๋ฉด ๊ตฌ์„ฑ ์‹œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ + +| ์ปดํฌ๋„ŒํŠธ ID | ์„ค๋ช… | ์นดํ…Œ๊ณ ๋ฆฌ | +|-------------|------|----------| +| `v2-input` | ํ…์ŠคํŠธ, ์ˆซ์ž, ๋น„๋ฐ€๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ ๋“ฑ ์ž…๋ ฅ | ์ž…๋ ฅ | +| `v2-select` | ๋“œ๋กญ๋‹ค์šด, ์ฝค๋ณด๋ฐ•์Šค, ๋ผ๋””์˜ค, ์ฒดํฌ๋ฐ•์Šค | ์ž…๋ ฅ | +| `v2-date` | ๋‚ ์งœ/์‹œ๊ฐ„ ์ž…๋ ฅ | ์ž…๋ ฅ | +| `v2-button-primary` | ๋ฒ„ํŠผ | ์•ก์…˜ | +| `v2-table-list` | ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ (CRUD) | ํ…Œ์ด๋ธ” | +| `v2-table-search-widget` | ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰/ํ•„ํ„ฐ ์œ„์ ฏ | ์œ ํ‹ธ๋ฆฌํ‹ฐ | +| `v2-aggregation-widget` | ์ง‘๊ณ„ ์œ„์ ฏ | ์œ„์ ฏ | +| `v2-text-display` | ํ…์ŠคํŠธ ํ‘œ์‹œ (์ฝ๊ธฐ ์ „์šฉ) | ํ‘œ์‹œ | + +### 2.3 ์ด ํ™”๋ฉด์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ + +| ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… | ์—ญํ•  | +|---------------|------| +| `v2-table-search-widget` | ๊ฒ€์ƒ‰ ํ•„ํ„ฐ (์ˆ˜์ฃผ๋ฒˆํ˜ธ, ๊ฑฐ๋ž˜์ฒ˜, ํ’ˆ๋ชฉ๋ช…, ์ƒํƒœ, ์ˆ˜์ฃผ์ผ) | +| `v2-table-list` | ์ˆ˜์ฃผ ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” | +| `v2-button-primary` | ์‚ฌ์šฉ์ž์˜ต์…˜ | +| `v2-button-primary` | ์—‘์…€ ์—…๋กœ๋“œ | +| `v2-button-primary` | ์—‘์…€ ๋‹ค์šด๋กœ๋“œ | +| `v2-button-primary` | ์ˆ˜์ฃผ๋“ฑ๋ก (๋ชจ๋‹ฌ ์—ด๊ธฐ) | +| `v2-button-primary` | ์ˆ˜์ • (๋ชจ๋‹ฌ ์—ด๊ธฐ) | +| `v2-button-primary` | ์‚ญ์ œ | +| `v2-button-primary` | ์ถœํ•˜๊ณ„ํš | +| `v2-aggregation-widget` | ํ†ต๊ณ„ ํ‘œ์‹œ (์ด ๊ธˆ์•ก, ์ด ์ˆ˜๋Ÿ‰) | +| `v2-input` | ๋ชจ๋‹ฌ - ํ…์ŠคํŠธ ์ž…๋ ฅ ํ•„๋“œ | +| `v2-select` | ๋ชจ๋‹ฌ - ์„ ํƒ ํ•„๋“œ | +| `v2-date` | ๋ชจ๋‹ฌ - ๋‚ ์งœ ์ž…๋ ฅ ํ•„๋“œ | + +--- + +## 3. ํ™”๋ฉด ๋””์ž์ด๋„ˆ ์„ค์ • ๊ฐ€์ด๋“œ + +### 3.1 v2-table-search-widget (๊ฒ€์ƒ‰ ํ•„ํ„ฐ) ์„ค์ • + +1. ์ขŒ์ธก ์ปดํฌ๋„ŒํŠธ ํŒจ๋„์—์„œ `v2-table-search-widget` ๋“œ๋ž˜๊ทธํ•˜์—ฌ ํ™”๋ฉด ์ƒ๋‹จ์— ๋ฐฐ์น˜ +2. ๋Œ€์ƒ ํ…Œ์ด๋ธ”๋กœ ์•„๋ž˜์— ๋ฐฐ์น˜ํ•  ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ์„ ํƒ + +> ๐Ÿ’ก **์ฐธ๊ณ **: ๊ฒ€์ƒ‰ ํ•„ํ„ฐ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋Ÿฐํƒ€์ž„์—์„œ ์›ํ•˜๋Š” ํ•„๋“œ๋ฅผ ์ง์ ‘ ์ถ”๊ฐ€/์‚ญ์ œํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +--- + +### 3.2 v2-table-list (์ˆ˜์ฃผ ํ…Œ์ด๋ธ”) ์„ค์ • + +#### Step 1: ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ +1. ์ขŒ์ธก ์ปดํฌ๋„ŒํŠธ ํŒจ๋„์—์„œ `v2-table-list` ๋“œ๋ž˜๊ทธํ•˜์—ฌ ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ์•„๋ž˜์— ๋ฐฐ์น˜ + +#### Step 2: ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ํ…Œ์ด๋ธ” ์„ ํƒ | `sales_order_mng` | +| ์ž๋™ ์ปฌ๋Ÿผ ์ƒ์„ฑ | โœ… ์ฒดํฌ (ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ž๋™ ๋กœ๋“œ) | + +#### Step 3: ์ปฌ๋Ÿผ ์„ค์ • + +**[์ปฌ๋Ÿผ ์„ค์ •]** ํŒจ๋„์—์„œ ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ์„ ํƒ ๋ฐ ์ˆœ์„œ ์กฐ์ •: + +| ์ˆœ์„œ | ์ปฌ๋Ÿผ | ํ‘œ์‹œ๋ช… | ๋„ˆ๋น„ | ์ •๋ ฌ | ํ‘œ์‹œ | ํŠน์ˆ˜ ์„ค์ • | +|------|------|--------|------|------|------|-----------| +| 1 | `order_no` | ์ˆ˜์ฃผ๋ฒˆํ˜ธ | 130 | ์ขŒ์ธก | โœ… | ๊ตต๊ฒŒ ํ‘œ์‹œ | +| 2 | `partner_id` | ๊ฑฐ๋ž˜์ฒ˜ | 150 | ์ขŒ์ธก | โœ… | ์กฐ์ธ: customer_mng.name | +| 3 | `part_code` | ํ’ˆ๋ชฉ์ฝ”๋“œ | 130 | ์ขŒ์ธก | โœ… | | +| 4 | `part_name` | ํ’ˆ๋ช… | 180 | ์ขŒ์ธก | โœ… | | +| 5 | `spec` | ๊ทœ๊ฒฉ | 120 | ์ขŒ์ธก | โœ… | | +| 6 | `material` | ์žฌ์งˆ | 100 | ์ขŒ์ธก | โœ… | | +| 7 | `unit` | ๋‹จ์œ„ | 80 | ์ค‘์•™ | โœ… | ๊ธฐ๋ณธ๊ฐ’: EA | +| 8 | `order_qty` | ์ˆ˜์ฃผ์ˆ˜๋Ÿ‰ | 100 | ์šฐ์ธก | โœ… | ์ˆซ์ž ํฌ๋งท | +| 9 | `ship_qty` | ์ถœํ•˜์ˆ˜๋Ÿ‰ | 100 | ์šฐ์ธก | โœ… | ์ˆซ์ž ํฌ๋งท | +| 10 | `balance_qty` | ์ž”๋Ÿ‰ | 100 | ์šฐ์ธก | โœ… | ์ˆซ์ž ํฌ๋งท, ๊ตต๊ฒŒ | +| 11 | `inventory_qty` | ํ˜„์žฌ๊ณ  | 100 | ์šฐ์ธก | โœ… | ์ˆซ์ž ํฌ๋งท | +| 12 | `plan_ship_qty` | ์ถœํ•˜๊ณ„ํš๋Ÿ‰ | 100 | ์šฐ์ธก | โœ… | ์ˆซ์ž ํฌ๋งท | +| 13 | `unit_price` | ๋‹จ๊ฐ€ | 120 | ์šฐ์ธก | โœ… | ์ˆซ์ž ํฌ๋งท | +| 14 | `total_amount` | ๊ธˆ์•ก | 140 | ์šฐ์ธก | โœ… | ์ˆซ์ž ํฌ๋งท, ๊ตต๊ฒŒ | +| 15 | `delivery_partner_id` | ๋‚ฉํ’ˆ์ฒ˜ | 150 | ์ขŒ์ธก | โœ… | | +| 16 | `delivery_address` | ๋‚ฉํ’ˆ์žฅ์†Œ | 150 | ์ขŒ์ธก | โœ… | | +| 17 | `shipping_method` | ๋ฐฐ์†ก๋ฐฉ๋ฒ• | 120 | ์ค‘์•™ | โœ… | | +| 18 | `due_date` | ๋‚ฉ๊ธฐ์ผ | 120 | ์ค‘์•™ | โœ… | ๋‚ ์งœ ํฌ๋งท | +| 19 | `order_date` | ์ˆ˜์ฃผ์ผ | 120 | ์ค‘์•™ | โœ… | ๋‚ ์งœ ํฌ๋งท | +| 20 | `status` | ์ƒํƒœ | 100 | ์ค‘์•™ | โœ… | ๋ฑƒ์ง€ ์Šคํƒ€์ผ | +| 21 | `manager_name` | ๋‹ด๋‹น์ž | 100 | ์ขŒ์ธก | โœ… | | +| 22 | `memo` | ๋ฉ”๋ชจ | 200 | ์ขŒ์ธก | โœ… | | + +#### Step 4: ๊ธฐ๋Šฅ ์„ค์ • + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | ์„ค๋ช… | +|-----------|---------|------| +| ์ฒดํฌ๋ฐ•์Šค | โœ… ์‚ฌ์šฉ | ๋‹ค์ค‘ ์„ ํƒ ํ™œ์„ฑํ™” | +| ํŽ˜์ด์ง€๋„ค์ด์…˜ | โœ… ์‚ฌ์šฉ | | +| ํŽ˜์ด์ง€ ํฌ๊ธฐ | 20 | ๊ธฐ๋ณธ ํ‘œ์‹œ ํ–‰ ์ˆ˜ | +| ์ •๋ ฌ | โœ… ์‚ฌ์šฉ | ์ปฌ๋Ÿผ ํ—ค๋” ํด๋ฆญ ์ •๋ ฌ | +| ์ปฌ๋Ÿผ ๋ฆฌ์‚ฌ์ด์ฆˆ | โœ… ์‚ฌ์šฉ | ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์กฐ์ • | +| ๊ทธ๋ฃนํ•‘ | โœ… ์‚ฌ์šฉ | Group By ๊ธฐ๋Šฅ | +| ์ˆ˜ํ‰ ์Šคํฌ๋กค | โœ… ์‚ฌ์šฉ | ์ปฌ๋Ÿผ ์ˆ˜๊ฐ€ ๋งŽ์œผ๋ฏ€๋กœ ํ•„์ˆ˜ | + +#### Step 5: ๊ทธ๋ฃนํ•‘ ์˜ต์…˜ ์„ค์ • + +Group By ๋“œ๋กญ๋‹ค์šด์— ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ์„ ํƒ: +- โœ… `partner_id` (๊ฑฐ๋ž˜์ฒ˜) +- โœ… `status` (์ƒํƒœ) +- โœ… `part_name` (ํ’ˆ๋ชฉ๋ช…) +- โœ… `material` (์žฌ์งˆ) + +--- + +### 3.3 ๋ฒ„ํŠผ ์„ค์ • + +#### ๊ฒ€์ƒ‰ ์˜์—ญ ์šฐ์ธก ๋ฒ„ํŠผ + +##### ์‚ฌ์šฉ์ž์˜ต์…˜ ๋ฒ„ํŠผ + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ๋ผ๋ฒจ | `์‚ฌ์šฉ์ž์˜ต์…˜` | +| ์•„์ด์ฝ˜ | โš™๏ธ | +| ์•ก์…˜ ํƒ€์ž… | `custom` | +| ์Šคํƒ€์ผ | `secondary` | +| ๋™์ž‘ | ์‚ฌ์šฉ์ž ์˜ต์…˜ ๋ชจ๋‹ฌ ์—ด๊ธฐ | + +##### ์—‘์…€ ์—…๋กœ๋“œ ๋ฒ„ํŠผ + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ๋ผ๋ฒจ | `์—‘์…€ ์—…๋กœ๋“œ` | +| ์•„์ด์ฝ˜ | ๐Ÿ“ฅ | +| ์•ก์…˜ ํƒ€์ž… | `excel_upload` | +| ์Šคํƒ€์ผ | `secondary` | +| ๋Œ€์ƒ ํ…Œ์ด๋ธ” | `sales_order_mng` | + +##### ์—‘์…€ ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ๋ผ๋ฒจ | `์—‘์…€ ๋‹ค์šด๋กœ๋“œ` | +| ์•„์ด์ฝ˜ | ๐Ÿ“ค | +| ์•ก์…˜ ํƒ€์ž… | `excel_download` | +| ์Šคํƒ€์ผ | `secondary` | +| ๋Œ€์ƒ | ํ˜„์žฌ ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | + +#### ํ…Œ์ด๋ธ” ํ—ค๋” ์šฐ์ธก ๋ฒ„ํŠผ + +##### ์ˆ˜์ฃผ๋“ฑ๋ก ๋ฒ„ํŠผ + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ๋ผ๋ฒจ | `์ˆ˜์ฃผ ๋“ฑ๋ก` | +| ์•„์ด์ฝ˜ | โž• | +| ์•ก์…˜ ํƒ€์ž… | `modal` | +| ์Šคํƒ€์ผ | `success` | +| ์—ฐ๊ฒฐ ํ™”๋ฉด | ์ˆ˜์ฃผ ๋“ฑ๋ก/์ˆ˜์ • ํ™”๋ฉด (์•„๋ž˜ 3.4 ์ฐธ์กฐ) | +| ๋ชจ๋‹ฌ ์ œ๋ชฉ | ์ˆ˜์ฃผ ๋“ฑ๋ก | +| ๋ชจ๋‹ฌ ์‚ฌ์ด์ฆˆ | `lg` | + +##### ์ˆ˜์ • ๋ฒ„ํŠผ + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ๋ผ๋ฒจ | `์ˆ˜์ •` | +| ์•„์ด์ฝ˜ | โœ๏ธ | +| ์•ก์…˜ ํƒ€์ž… | `edit` | +| ์Šคํƒ€์ผ | `secondary` | +| ์„ ํƒ ํ•„์ˆ˜ | โœ… ์ฒดํฌ (1๊ฐœ๋งŒ) | +| ์—ฐ๊ฒฐ ํ™”๋ฉด | ์ˆ˜์ฃผ ๋“ฑ๋ก/์ˆ˜์ • ํ™”๋ฉด (์•„๋ž˜ 3.4 ์ฐธ์กฐ) | + +##### ์‚ญ์ œ ๋ฒ„ํŠผ + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ๋ผ๋ฒจ | `์‚ญ์ œ` | +| ์•„์ด์ฝ˜ | ๐Ÿ—‘๏ธ | +| ์•ก์…˜ ํƒ€์ž… | `delete` | +| ์Šคํƒ€์ผ | `secondary` | +| ์„ ํƒ ํ•„์ˆ˜ | โœ… ์ฒดํฌ (๋ณต์ˆ˜ ์„ ํƒ ๊ฐ€๋Šฅ) | +| ํ™•์ธ ๋ฉ”์‹œ์ง€ | ์„ ํƒํ•œ ์ˆ˜์ฃผ๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? | + +##### ์ถœํ•˜๊ณ„ํš ๋ฒ„ํŠผ + +| ์„ค์ • ํ•ญ๋ชฉ | ์„ค์ • ๊ฐ’ | +|-----------|---------| +| ๋ผ๋ฒจ | `์ถœํ•˜๊ณ„ํš` | +| ์•„์ด์ฝ˜ | ๐Ÿšš | +| ์•ก์…˜ ํƒ€์ž… | `custom` | +| ์Šคํƒ€์ผ | `secondary` | +| ์„ ํƒ ํ•„์ˆ˜ | โœ… ์ฒดํฌ (๋ณต์ˆ˜ ์„ ํƒ ๊ฐ€๋Šฅ) | +| ๋™์ž‘ | ์ถœํ•˜๊ณ„ํš ์Šฌ๋ผ์ด๋“œ ํŒจ๋„ ์—ด๊ธฐ | + +--- + +### 3.4 ์ˆ˜์ฃผ ๋“ฑ๋ก/์ˆ˜์ • ํ™”๋ฉด (๋ชจ๋‹ฌ์šฉ ํ™”๋ฉด) + +> ๐Ÿ“Œ **๋ณ„๋„ ํ™”๋ฉด ์ƒ์„ฑ ํ•„์š”**: ์ˆ˜์ฃผ๋“ฑ๋ก/์ˆ˜์ • ๋ฒ„ํŠผ์— ์—ฐ๊ฒฐํ•  ๋ชจ๋‹ฌ ํ™”๋ฉด์„ ์ƒˆ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + +#### Step 1: ์ƒˆ ํ™”๋ฉด ์ƒ์„ฑ + +1. ํ™”๋ฉด ๊ด€๋ฆฌ์—์„œ **[+ ์ƒˆ ํ™”๋ฉด]** ํด๋ฆญ +2. ํ™”๋ฉด ์ •๋ณด ์ž…๋ ฅ: + - ํ™”๋ฉด๋ช…: `์ˆ˜์ฃผ ๋“ฑ๋ก/์ˆ˜์ •` + - ํ…Œ์ด๋ธ”: `sales_order_mng` + - ํ™”๋ฉด ์œ ํ˜•: `๋ชจ๋‹ฌ` + +#### Step 2: ํผ ํ•„๋“œ ๋ฐฐ์น˜ + +**๋ชจ๋‹ฌ ๋ ˆ์ด์•„์›ƒ ๋ฐฐ์น˜๋„**: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ์ˆ˜์ฃผ ๋“ฑ๋ก/์ˆ˜์ • [โœ•] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ˆ˜์ฃผ๋ฒˆํ˜ธ * โ”‚ โ”‚ ์ˆ˜์ฃผ์ผ * โ”‚ โ”‚ +โ”‚ โ”‚ [____________________] โ”‚ โ”‚ [____________________] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๊ฑฐ๋ž˜์ฒ˜ * โ”‚ โ”‚ ํ’ˆ๋ชฉ์ฝ”๋“œ * โ”‚ โ”‚ +โ”‚ โ”‚ [์‚ผ์„ฑ์ „์ž โ–ผ] โ”‚ โ”‚ [____________________] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ’ˆ๋ช… โ”‚ โ”‚ ๊ทœ๊ฒฉ โ”‚ โ”‚ +โ”‚ โ”‚ [____________________] โ”‚ โ”‚ [____________________] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์žฌ์งˆ โ”‚ โ”‚ ๋‹จ์œ„ โ”‚ โ”‚ +โ”‚ โ”‚ [____________________] โ”‚ โ”‚ [EA โ–ผ] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ˆ˜์ฃผ์ˆ˜๋Ÿ‰ * โ”‚ โ”‚ ๋‹จ๊ฐ€ * โ”‚ โ”‚ +โ”‚ โ”‚ [____________________] โ”‚ โ”‚ [____________________] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋‚ฉ๊ธฐ์ผ โ”‚ โ”‚ ์ƒํƒœ * โ”‚ โ”‚ +โ”‚ โ”‚ [____________________] โ”‚ โ”‚ [์ˆ˜์ฃผ โ–ผ] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋‚ฉํ’ˆ์ฒ˜ โ”‚ โ”‚ ๋‚ฉํ’ˆ์žฅ์†Œ โ”‚ โ”‚ +โ”‚ โ”‚ [____________________] โ”‚ โ”‚ [____________________] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋ฐฐ์†ก๋ฐฉ๋ฒ• โ”‚ โ”‚ ๋‹ด๋‹น์ž โ”‚ โ”‚ +โ”‚ โ”‚ [ํƒ๋ฐฐ โ–ผ] โ”‚ โ”‚ [____________________] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋ฉ”๋ชจ โ”‚ โ”‚ +โ”‚ โ”‚ [______________________________________________________]โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [์ทจ์†Œ] [๐Ÿ’พ ์ €์žฅ] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**ํ•„๋“œ ๋ชฉ๋ก**: + +| ์ˆœ์„œ | ํ•„๋“œ (์ปฌ๋Ÿผ๋ช…) | ๋ผ๋ฒจ | ์ž…๋ ฅ ํƒ€์ž… | ํ•„์ˆ˜ | ๋น„๊ณ  | +|------|---------------|------|-----------|------|------| +| 1 | `order_no` | ์ˆ˜์ฃผ๋ฒˆํ˜ธ | text | โœ… | ์ž๋™์ฑ„๋ฒˆ ๋˜๋Š” ์ˆ˜๋™์ž…๋ ฅ | +| 2 | `order_date` | ์ˆ˜์ฃผ์ผ | date | โœ… | ๊ธฐ๋ณธ๊ฐ’: ์˜ค๋Š˜ | +| 3 | `partner_id` | ๊ฑฐ๋ž˜์ฒ˜ | select | โœ… | ๊ฑฐ๋ž˜์ฒ˜ ๋ชฉ๋ก์—์„œ ์„ ํƒ | +| 4 | `part_code` | ํ’ˆ๋ชฉ์ฝ”๋“œ | text | โœ… | ํ’ˆ๋ชฉ ๊ฒ€์ƒ‰ | +| 5 | `part_name` | ํ’ˆ๋ช… | text | | ํ’ˆ๋ชฉ์ฝ”๋“œ ์„ ํƒ ์‹œ ์ž๋™ ์ž…๋ ฅ | +| 6 | `spec` | ๊ทœ๊ฒฉ | text | | ํ’ˆ๋ชฉ์ฝ”๋“œ ์„ ํƒ ์‹œ ์ž๋™ ์ž…๋ ฅ | +| 7 | `material` | ์žฌ์งˆ | text | | | +| 8 | `unit` | ๋‹จ์œ„ | select | | ์˜ต์…˜: EA, kg, L, Box ๋“ฑ | +| 9 | `order_qty` | ์ˆ˜์ฃผ์ˆ˜๋Ÿ‰ | number | โœ… | | +| 10 | `unit_price` | ๋‹จ๊ฐ€ | number | โœ… | | +| 11 | `due_date` | ๋‚ฉ๊ธฐ์ผ | date | | | +| 12 | `status` | ์ƒํƒœ | select | โœ… | ์˜ต์…˜: ์ˆ˜์ฃผ, ์ง„ํ–‰์ค‘, ์™„๋ฃŒ, ์ทจ์†Œ | +| 13 | `delivery_partner_id` | ๋‚ฉํ’ˆ์ฒ˜ | text | | | +| 14 | `delivery_address` | ๋‚ฉํ’ˆ์žฅ์†Œ | text | | | +| 15 | `shipping_method` | ๋ฐฐ์†ก๋ฐฉ๋ฒ• | select | | ์˜ต์…˜: ํƒ๋ฐฐ, ํ™”๋ฌผ, ์ง์†ก, ํ€ต์„œ๋น„์Šค ๋“ฑ | +| 16 | `manager_name` | ๋‹ด๋‹น์ž | text | | | +| 17 | `memo` | ๋ฉ”๋ชจ | textarea | | | + +#### Step 3: ๋ฒ„ํŠผ ๋ฐฐ์น˜ + +| ๋ฒ„ํŠผ | ์•ก์…˜ ํƒ€์ž… | ์Šคํƒ€์ผ | ์„ค์ • | +|------|-----------|--------|------| +| ์ €์žฅ | `์ €์žฅ` | primary | ์ €์žฅ ํ›„ ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ, ๋ถ€๋ชจ ํ™”๋ฉด ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ | +| ์ทจ์†Œ | `๋ชจ๋‹ฌ ๋‹ซ๊ธฐ` | secondary | | + +--- + +## 4. ์ปดํฌ๋„ŒํŠธ ์—ฐ๋™ ์„ค์ • + +### 4.1 ์ด๋ฒคํŠธ ํ๋ฆ„ + +``` +[๊ฒ€์ƒ‰ ์ž…๋ ฅ] + โ”‚ + โ–ผ +v2-table-search-widget + โ”‚ onFilterChange + โ–ผ +v2-table-list (์ž๋™ ์žฌ์กฐํšŒ) + โ”‚ + โ–ผ +[๋ฐ์ดํ„ฐ ํ‘œ์‹œ] + โ”‚ + โ–ผ +v2-aggregation-widget (ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ) + + +[์ˆ˜์ฃผ๋“ฑ๋ก/์ˆ˜์ • ๋ฒ„ํŠผ ํด๋ฆญ] + โ”‚ + โ–ผ +[๋ชจ๋‹ฌ ์—ด๊ธฐ] โ†’ [ํผ ์ž…๋ ฅ] โ†’ [์ €์žฅ] + โ”‚ โ”‚ + โ”‚ โ–ผ + โ”‚ refreshTable ์ด๋ฒคํŠธ + โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + v2-table-list (์žฌ์กฐํšŒ) + โ”‚ + โ–ผ + v2-aggregation-widget (ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ) +``` + +### 4.2 ์—ฐ๋™ ์„ค์ • + +| ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ | ์ด๋ฒคํŠธ/์•ก์…˜ | ๋Œ€์ƒ ์ปดํฌ๋„ŒํŠธ | ๋™์ž‘ | +|---------------|-------------|---------------|------| +| ๊ฒ€์ƒ‰ ์œ„์ ฏ | onFilterChange | ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | ํ•„ํ„ฐ ์ ์šฉ, ์žฌ์กฐํšŒ | +| ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | onDataChange | ์ง‘๊ณ„ ์œ„์ ฏ | ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ | +| ์ˆ˜์ฃผ๋“ฑ๋ก ๋ฒ„ํŠผ | click | ๋ชจ๋‹ฌ | ๋นˆ ํผ์œผ๋กœ ๋ชจ๋‹ฌ ์—ด๊ธฐ | +| ์ˆ˜์ • ๋ฒ„ํŠผ | click | ๋ชจ๋‹ฌ | ์„ ํƒ ๋ฐ์ดํ„ฐ๊ฐ€ ์ฑ„์›Œ์ง„ ํผ ์—ด๊ธฐ (์ˆ˜์ •) | +| ์‚ญ์ œ ๋ฒ„ํŠผ | click | ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | ์„ ํƒ ํ•ญ๋ชฉ ์‚ญ์ œ | +| ์ถœํ•˜๊ณ„ํš ๋ฒ„ํŠผ | click | ์Šฌ๋ผ์ด๋“œ ํŒจ๋„ | ์„ ํƒ ํ•ญ๋ชฉ ๊ธฐ๋ฐ˜ ์ถœํ•˜๊ณ„ํš ์ƒ์„ฑ | +| ๋ชจ๋‹ฌ ์ €์žฅ | afterSave | ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | refreshTable | + +--- + +## 5. ์‚ฌ์šฉ์ž ์‚ฌ์šฉ ์˜ˆ์‹œ ์‹œ๋‚˜๋ฆฌ์˜ค + +### ์‹œ๋‚˜๋ฆฌ์˜ค 1: ์ˆ˜์ฃผ ์กฐํšŒ + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|------|-------------|-----------| +| 1 | ํ™”๋ฉด ์ง„์ž… | ์ „์ฒด ์ˆ˜์ฃผ ๋ชฉ๋ก ํ‘œ์‹œ, ํ†ต๊ณ„(์ด ๊ธˆ์•ก, ์ด ์ˆ˜๋Ÿ‰) ํ‘œ์‹œ | +| 2 | ๊ฑฐ๋ž˜์ฒ˜ ํ•„ํ„ฐ๋ฅผ "์‚ผ์„ฑ์ „์ž"๋กœ ์„ ํƒ | ์ž๋™ ํ•„ํ„ฐ๋ง, ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ | +| 3 | ์ƒํƒœ๋ฅผ "์ง„ํ–‰์ค‘"์œผ๋กœ ์„ ํƒ | ์ถ”๊ฐ€ ํ•„ํ„ฐ๋ง | +| 4 | Group by์—์„œ "๊ฑฐ๋ž˜์ฒ˜" ์„ ํƒ | ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๊ทธ๋ฃนํ•‘ ํ‘œ์‹œ | + +### ์‹œ๋‚˜๋ฆฌ์˜ค 2: ์ˆ˜์ฃผ ๋“ฑ๋ก + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|------|-------------|-----------| +| 1 | [์ˆ˜์ฃผ ๋“ฑ๋ก] ๋ฒ„ํŠผ ํด๋ฆญ | ๋นˆ ํผ ๋ชจ๋‹ฌ ํ‘œ์‹œ | +| 2 | ๊ฑฐ๋ž˜์ฒ˜ ์„ ํƒ, ํ’ˆ๋ชฉ์ฝ”๋“œ ์ž…๋ ฅ | ํ’ˆ๋ช…, ๊ทœ๊ฒฉ ์ž๋™ ์ž…๋ ฅ | +| 3 | ์ˆ˜์ฃผ์ˆ˜๋Ÿ‰, ๋‹จ๊ฐ€ ์ž…๋ ฅ | ๊ธˆ์•ก ์ž๋™ ๊ณ„์‚ฐ | +| 4 | [์ €์žฅ] ๋ฒ„ํŠผ ํด๋ฆญ | ์ €์žฅ ์™„๋ฃŒ, ๋ชจ๋‹ฌ ๋‹ซํž˜, ๋ชฉ๋ก ๊ฐฑ์‹ , ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ | + +### ์‹œ๋‚˜๋ฆฌ์˜ค 3: ์ˆ˜์ฃผ ์ˆ˜์ • + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|------|-------------|-----------| +| 1 | ํ…Œ์ด๋ธ”์—์„œ ํ–‰ ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ | ํ–‰ ์„ ํƒ ํ‘œ์‹œ | +| 2 | [์ˆ˜์ •] ๋ฒ„ํŠผ ํด๋ฆญ | ์ˆ˜์ • ๋ชจ๋‹ฌ ํ‘œ์‹œ (๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋กœ๋“œ) | +| 3 | ๋ฐ์ดํ„ฐ ์ˆ˜์ • | ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ | +| 4 | [์ €์žฅ] ๋ฒ„ํŠผ ํด๋ฆญ | ์ €์žฅ ์™„๋ฃŒ, ๋ชฉ๋ก ๊ฐฑ์‹  | + +### ์‹œ๋‚˜๋ฆฌ์˜ค 4: ์ˆ˜์ฃผ ์‚ญ์ œ + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|------|-------------|-----------| +| 1 | ์‚ญ์ œํ•  ํ–‰ ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ (๋‹ค์ค‘ ๊ฐ€๋Šฅ) | ํ–‰ ์„ ํƒ ํ‘œ์‹œ | +| 2 | [์‚ญ์ œ] ๋ฒ„ํŠผ ํด๋ฆญ | ์‚ญ์ œ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ | +| 3 | ํ™•์ธ | ์‚ญ์ œ ์™„๋ฃŒ, ๋ชฉ๋ก ๊ฐฑ์‹ , ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ | + +### ์‹œ๋‚˜๋ฆฌ์˜ค 5: ์ถœํ•˜๊ณ„ํš ์ƒ์„ฑ + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|------|-------------|-----------| +| 1 | ์ถœํ•˜ํ•  ์ˆ˜์ฃผ ํ–‰ ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ (๋‹ค์ค‘) | ํ–‰ ์„ ํƒ ํ‘œ์‹œ | +| 2 | [์ถœํ•˜๊ณ„ํš] ๋ฒ„ํŠผ ํด๋ฆญ | ์ถœํ•˜๊ณ„ํš ์Šฌ๋ผ์ด๋“œ ํŒจ๋„ ์—ด๋ฆผ | +| 3 | ์ถœํ•˜ ์ˆ˜๋Ÿ‰ ์ž…๋ ฅ, ์ถœํ•˜์ผ ์„ ํƒ | ์ถœํ•˜๊ณ„ํš ๋ฐ์ดํ„ฐ ์„ค์ • | +| 4 | [์ ์šฉ] ๋ฒ„ํŠผ ํด๋ฆญ | ์ถœํ•˜๊ณ„ํš ์ €์žฅ, ์ˆ˜์ฃผ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ | + +--- + +## 6. ๊ฒ€์ฆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### ๊ธฐ๋ณธ ๊ธฐ๋Šฅ +- [ ] ๋ฐ์ดํ„ฐ ์กฐํšŒ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ๊ฒ€์ƒ‰ ํ•„ํ„ฐ (์ˆ˜์ฃผ๋ฒˆํ˜ธ, ๊ฑฐ๋ž˜์ฒ˜, ํ’ˆ๋ชฉ๋ช…, ์ƒํƒœ, ์ˆ˜์ฃผ์ผ)๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์‹ ๊ทœ ๋“ฑ๋ก์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ˆ˜์ •์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์‚ญ์ œ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์—‘์…€ ์—…๋กœ๋“œ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์—‘์…€ ๋‹ค์šด๋กœ๋“œ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? + +### ํ…Œ์ด๋ธ” ๊ธฐ๋Šฅ +- [ ] ํŽ˜์ด์ง€๋„ค์ด์…˜์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ •๋ ฌ์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์กฐ์ •์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ˆ˜ํ‰ ์Šคํฌ๋กค์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? + +### ๊ฒ€์ƒ‰ ์œ„์ ฏ ์—ฐ๋™ +- [ ] v2-table-search-widget๊ณผ v2-table-list ์—ฐ๋™์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ํ•„ํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ์žฌ์กฐํšŒ๊ฐ€ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ดˆ๊ธฐํ™” ๋ฒ„ํŠผ์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? + +### ๊ทธ๋ฃนํ•‘ ๊ธฐ๋Šฅ +- [ ] Group by ์„ ํƒ ์‹œ ๊ทธ๋ฃนํ•‘์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ๊ทธ๋ฃน ํ—ค๋”์— ๊ฑด์ˆ˜, ์ˆ˜๋Ÿ‰, ๊ธˆ์•ก์ด ํ‘œ์‹œ๋˜๋Š”๊ฐ€? +- [ ] ๊ทธ๋ฃน ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? + +### ํ†ต๊ณ„ ๊ธฐ๋Šฅ +- [ ] ์ด ๊ธˆ์•ก์ด ์ •ํ™•ํžˆ ๊ณ„์‚ฐ๋˜๋Š”๊ฐ€? +- [ ] ์ด ์ˆ˜๋Ÿ‰์ด ์ •ํ™•ํžˆ ๊ณ„์‚ฐ๋˜๋Š”๊ฐ€? +- [ ] ํ•„ํ„ฐ๋ง ์‹œ ํ†ต๊ณ„๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋Š”๊ฐ€? + +### ์ถœํ•˜๊ณ„ํš ์—ฐ๋™ +- [ ] ์„ ํƒํ•œ ์ˆ˜์ฃผ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ถœํ•˜๊ณ„ํš์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š”๊ฐ€? +- [ ] ์ถœํ•˜๊ณ„ํš ์ ์šฉ ํ›„ ์ˆ˜์ฃผ ๋ฐ์ดํ„ฐ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋Š”๊ฐ€? + +--- + +## 7. ์ฐธ๊ณ  ์‚ฌํ•ญ + +### ๊ด€๋ จ ํ…Œ์ด๋ธ” +- `customer_mng` - ๊ฑฐ๋ž˜์ฒ˜ ์ •๋ณด (partner_id ์ฐธ์กฐ) +- `item_info` - ํ’ˆ๋ชฉ ์ •๋ณด (part_code ์ฐธ์กฐ) +- `sales_order_detail` - ์ˆ˜์ฃผ ์ƒ์„ธ (๋‹ค์ค‘ ํ’ˆ๋ชฉ ๊ด€๋ฆฌ ์‹œ) +- `shipment_mng` - ์ถœํ•˜ ์ •๋ณด (์ถœํ•˜๊ณ„ํš ์—ฐ๋™) + +### ํŠน์ด ์‚ฌํ•ญ +- `partner_id`๋Š” ๊ฑฐ๋ž˜์ฒ˜ ํ…Œ์ด๋ธ”์˜ ID๋ฅผ ์ฐธ์กฐ (์กฐ์ธ ํ•„์š”) +- `balance_qty` = `order_qty` - `ship_qty` (์ž”๋Ÿ‰ ์ž๋™ ๊ณ„์‚ฐ) +- `total_amount` = `order_qty` ร— `unit_price` (๊ธˆ์•ก ์ž๋™ ๊ณ„์‚ฐ) +- ์ƒํƒœ๋ณ„ ๋ฑƒ์ง€ ์ƒ‰์ƒ: + - ์ˆ˜์ฃผ: ํŒŒ๋ž€์ƒ‰ (#dbeafe, #1e40af) + - ์ง„ํ–‰์ค‘: ๋…ธ๋ž€์ƒ‰ (#fef3c7, #92400e) + - ์™„๋ฃŒ: ์ดˆ๋ก์ƒ‰ (#d1fae5, #065f46) + - ์ทจ์†Œ: ๋นจ๊ฐ„์ƒ‰ (#fee2e2, #991b1b) +- ์ˆ˜์ถœ์šฉ ํ•„๋“œ: incoterms, payment_term, port_of_loading, port_of_discharge, hs_code, currency + +--- + +## 8. DB INSERT์šฉ JSON ์„ค์ • (screen_layouts_v2 ๋ฐฉ์‹) + +> ๐Ÿ“Œ ์‹ค์ œ ํ™”๋ฉด ์ €์žฅ์€ `screen_definitions` + `screen_layouts_v2` ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + +> โš ๏ธ **์ฃผ์˜: ์•„๋ž˜ JSON์€ "์ˆ˜์ฃผ๊ด€๋ฆฌ" ํ™”๋ฉด ์ „์šฉ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค!** + +### 8.1 ํ™”๋ฉด ์ •์˜ (screen_definitions) + +**ํ•„์ˆ˜ ์ž…๋ ฅ ํ•„๋“œ:** + +```json +{ + "screenName": "์ˆ˜์ฃผ๊ด€๋ฆฌ", + "tableName": "sales_order_mng", + "companyCode": "COMPANY_7", + "description": "์ˆ˜์ฃผ ๊ด€๋ฆฌ ํ™”๋ฉด" +} +``` + +### 8.2 ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ (screen_layouts_v2.layout_data) + +```json +{ + "version": "2.0", + "components": [ + { + "id": "comp_search", + "url": "@/lib/registry/components/v2-table-search-widget", + "size": { "width": 1920, "height": 80 }, + "position": { "x": 0, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-table-search-widget", + "label": "๊ฒ€์ƒ‰ ํ•„ํ„ฐ", + "webTypeConfig": {} + }, + "displayOrder": 0 + }, + { + "id": "comp_table", + "url": "@/lib/registry/components/v2-table-list", + "size": { "width": 1920, "height": 800 }, + "position": { "x": 0, "y": 150, "z": 1 }, + "overrides": { + "type": "v2-table-list", + "label": "์ˆ˜์ฃผ ๋ชฉ๋ก", + "filter": { "enabled": true, "filters": [] }, + "height": "auto", + "actions": { "actions": [], "bulkActions": false, "showActions": false }, + "columns": [ + { "align": "left", "order": 0, "format": "text", "visible": true, "sortable": true, "columnName": "order_no", "searchable": true, "displayName": "์ˆ˜์ฃผ๋ฒˆํ˜ธ" }, + { "align": "left", "order": 1, "format": "text", "visible": true, "sortable": true, "columnName": "partner_id", "searchable": true, "displayName": "๊ฑฐ๋ž˜์ฒ˜" }, + { "align": "left", "order": 2, "format": "text", "visible": true, "sortable": true, "columnName": "part_code", "searchable": true, "displayName": "ํ’ˆ๋ชฉ์ฝ”๋“œ" }, + { "align": "left", "order": 3, "format": "text", "visible": true, "sortable": true, "columnName": "part_name", "searchable": true, "displayName": "ํ’ˆ๋ช…" }, + { "align": "left", "order": 4, "format": "text", "visible": true, "sortable": true, "columnName": "spec", "searchable": true, "displayName": "๊ทœ๊ฒฉ" }, + { "align": "left", "order": 5, "format": "text", "visible": true, "sortable": true, "columnName": "material", "searchable": true, "displayName": "์žฌ์งˆ" }, + { "align": "right", "order": 6, "format": "number", "visible": true, "sortable": true, "columnName": "order_qty", "searchable": false, "displayName": "์ˆ˜์ฃผ์ˆ˜๋Ÿ‰" }, + { "align": "right", "order": 7, "format": "number", "visible": true, "sortable": true, "columnName": "ship_qty", "searchable": false, "displayName": "์ถœํ•˜์ˆ˜๋Ÿ‰" }, + { "align": "right", "order": 8, "format": "number", "visible": true, "sortable": true, "columnName": "balance_qty", "searchable": false, "displayName": "์ž”๋Ÿ‰" }, + { "align": "right", "order": 9, "format": "number", "visible": true, "sortable": true, "columnName": "inventory_qty", "searchable": false, "displayName": "ํ˜„์žฌ๊ณ " }, + { "align": "right", "order": 10, "format": "number", "visible": true, "sortable": true, "columnName": "plan_ship_qty", "searchable": false, "displayName": "์ถœํ•˜๊ณ„ํš๋Ÿ‰" }, + { "align": "right", "order": 11, "format": "number", "visible": true, "sortable": true, "columnName": "unit_price", "searchable": false, "displayName": "๋‹จ๊ฐ€" }, + { "align": "right", "order": 12, "format": "number", "visible": true, "sortable": true, "columnName": "total_amount", "searchable": false, "displayName": "๊ธˆ์•ก" }, + { "align": "left", "order": 13, "format": "text", "visible": true, "sortable": true, "columnName": "delivery_partner_id", "searchable": true, "displayName": "๋‚ฉํ’ˆ์ฒ˜" }, + { "align": "left", "order": 14, "format": "text", "visible": true, "sortable": true, "columnName": "delivery_address", "searchable": true, "displayName": "๋‚ฉํ’ˆ์žฅ์†Œ" }, + { "align": "center", "order": 15, "format": "text", "visible": true, "sortable": true, "columnName": "shipping_method", "searchable": true, "displayName": "๋ฐฐ์†ก๋ฐฉ๋ฒ•" }, + { "align": "center", "order": 16, "format": "date", "visible": true, "sortable": true, "columnName": "due_date", "searchable": false, "displayName": "๋‚ฉ๊ธฐ์ผ" }, + { "align": "center", "order": 17, "format": "date", "visible": true, "sortable": true, "columnName": "order_date", "searchable": false, "displayName": "์ˆ˜์ฃผ์ผ" }, + { "align": "center", "order": 18, "format": "text", "visible": true, "sortable": true, "columnName": "status", "searchable": true, "displayName": "์ƒํƒœ" }, + { "align": "left", "order": 19, "format": "text", "visible": true, "sortable": true, "columnName": "manager_name", "searchable": true, "displayName": "๋‹ด๋‹น์ž" }, + { "align": "left", "order": 20, "format": "text", "visible": true, "sortable": true, "columnName": "memo", "searchable": true, "displayName": "๋ฉ”๋ชจ" } + ], + "autoLoad": true, + "checkbox": { "enabled": true, "multiple": true, "position": "left", "selectAll": true }, + "pagination": { "enabled": true, "pageSize": 20, "showPageInfo": true, "pageSizeOptions": [10, 20, 50, 100], "showSizeSelector": true }, + "showFooter": true, + "showHeader": true, + "tableStyle": { "theme": "default", "rowHeight": "normal", "borderStyle": "light", "headerStyle": "default", "hoverEffect": true, "alternateRows": true }, + "displayMode": "table", + "stickyHeader": false, + "selectedTable": "sales_order_mng", + "webTypeConfig": {}, + "horizontalScroll": { "enabled": true, "maxColumnWidth": 300, "minColumnWidth": 80, "maxVisibleColumns": 10 } + }, + "displayOrder": 1 + }, + { + "id": "comp_btn_user_options", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 1500, "y": 30, "z": 1 }, + "overrides": { + "text": "์‚ฌ์šฉ์ž์˜ต์…˜", + "type": "v2-button-primary", + "label": "์‚ฌ์šฉ์ž์˜ต์…˜ ๋ฒ„ํŠผ", + "action": { "type": "custom" }, + "variant": "secondary", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 2 + }, + { + "id": "comp_btn_upload", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 1610, "y": 30, "z": 1 }, + "overrides": { + "text": "์—‘์…€ ์—…๋กœ๋“œ", + "type": "v2-button-primary", + "label": "์—‘์…€ ์—…๋กœ๋“œ ๋ฒ„ํŠผ", + "action": { "type": "excel_upload" }, + "variant": "secondary", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 3 + }, + { + "id": "comp_btn_download", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 1720, "y": 30, "z": 1 }, + "overrides": { + "text": "์—‘์…€ ๋‹ค์šด๋กœ๋“œ", + "type": "v2-button-primary", + "label": "์—‘์…€ ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ", + "action": { "type": "excel_download" }, + "variant": "secondary", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 4 + }, + { + "id": "comp_btn_register", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 1500, "y": 100, "z": 1 }, + "overrides": { + "text": "์ˆ˜์ฃผ ๋“ฑ๋ก", + "type": "v2-button-primary", + "label": "์ˆ˜์ฃผ ๋“ฑ๋ก ๋ฒ„ํŠผ", + "action": { + "type": "modal", + "modalSize": "lg", + "modalTitle": "์ˆ˜์ฃผ ๋“ฑ๋ก", + "targetScreenId": "{{modal_screen_id}}", + "successMessage": "์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + "errorMessage": "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." + }, + "variant": "success", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 5 + }, + { + "id": "comp_btn_edit", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1610, "y": 100, "z": 1 }, + "overrides": { + "text": "์ˆ˜์ •", + "type": "v2-button-primary", + "label": "์ˆ˜์ • ๋ฒ„ํŠผ", + "action": { + "type": "edit", + "modalSize": "lg", + "modalTitle": "์ˆ˜์ฃผ ์ˆ˜์ •", + "targetScreenId": "{{modal_screen_id}}", + "successMessage": "์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + "errorMessage": "์ˆ˜์ • ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." + }, + "variant": "secondary", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 6 + }, + { + "id": "comp_btn_delete", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1700, "y": 100, "z": 1 }, + "overrides": { + "text": "์‚ญ์ œ", + "type": "v2-button-primary", + "label": "์‚ญ์ œ ๋ฒ„ํŠผ", + "action": { + "type": "delete", + "successMessage": "์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + "errorMessage": "์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." + }, + "variant": "secondary", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 7 + }, + { + "id": "comp_btn_shipment", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 1790, "y": 100, "z": 1 }, + "overrides": { + "text": "์ถœํ•˜๊ณ„ํš", + "type": "v2-button-primary", + "label": "์ถœํ•˜๊ณ„ํš ๋ฒ„ํŠผ", + "action": { "type": "custom" }, + "variant": "secondary", + "actionType": "button", + "webTypeConfig": { "variant": "secondary", "actionType": "custom" } + }, + "displayOrder": 8 + } + ] +} +``` + +### 8.3 ๋ชจ๋‹ฌ ํ™”๋ฉด (์ˆ˜์ฃผ ๋“ฑ๋ก/์ˆ˜์ •) + +#### ํ™”๋ฉด ์ •์˜ (ํ•„์ˆ˜ ์ž…๋ ฅ) + +```json +{ + "screenName": "์ˆ˜์ฃผ ๋“ฑ๋ก/์ˆ˜์ •", + "tableName": "sales_order_mng", + "companyCode": "COMPANY_7", + "description": "์ˆ˜์ฃผ ๋“ฑ๋ก/์ˆ˜์ • ํผ ํ™”๋ฉด" +} +``` + +#### ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ (screen_layouts_v2.layout_data) + +```json +{ + "version": "2.0", + "components": [ + { + "id": "comp_order_no", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "์ˆ˜์ฃผ๋ฒˆํ˜ธ", + "fieldName": "order_no", + "placeholder": "์ˆ˜์ฃผ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”", + "required": true + }, + "displayOrder": 0 + }, + { + "id": "comp_order_date", + "url": "@/lib/registry/components/v2-date", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-date", + "label": "์ˆ˜์ฃผ์ผ", + "fieldName": "order_date", + "required": true + }, + "displayOrder": 1 + }, + { + "id": "comp_partner_id", + "url": "@/lib/registry/components/v2-select", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 100, "z": 1 }, + "overrides": { + "type": "v2-select", + "label": "๊ฑฐ๋ž˜์ฒ˜", + "fieldName": "partner_id", + "required": true, + "config": { + "mode": "dropdown", + "source": "table", + "sourceTable": "customer_mng", + "valueField": "id", + "labelField": "name" + } + }, + "displayOrder": 2 + }, + { + "id": "comp_part_code", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 100, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "ํ’ˆ๋ชฉ์ฝ”๋“œ", + "fieldName": "part_code", + "placeholder": "ํ’ˆ๋ชฉ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”", + "required": true + }, + "displayOrder": 3 + }, + { + "id": "comp_part_name", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 180, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "ํ’ˆ๋ช…", + "fieldName": "part_name", + "placeholder": "ํ’ˆ๋ช…์„ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 4 + }, + { + "id": "comp_spec", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 180, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "๊ทœ๊ฒฉ", + "fieldName": "spec", + "placeholder": "๊ทœ๊ฒฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 5 + }, + { + "id": "comp_material", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 260, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "์žฌ์งˆ", + "fieldName": "material", + "placeholder": "์žฌ์งˆ์„ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 6 + }, + { + "id": "comp_order_qty", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 260, "z": 1 }, + "overrides": { + "type": "v2-input", + "inputType": "number", + "label": "์ˆ˜์ฃผ์ˆ˜๋Ÿ‰", + "fieldName": "order_qty", + "placeholder": "์ˆ˜์ฃผ์ˆ˜๋Ÿ‰์„ ์ž…๋ ฅํ•˜์„ธ์š”", + "required": true + }, + "displayOrder": 7 + }, + { + "id": "comp_unit_price", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 340, "z": 1 }, + "overrides": { + "type": "v2-input", + "inputType": "number", + "label": "๋‹จ๊ฐ€", + "fieldName": "unit_price", + "placeholder": "๋‹จ๊ฐ€๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”", + "required": true + }, + "displayOrder": 8 + }, + { + "id": "comp_due_date", + "url": "@/lib/registry/components/v2-date", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 340, "z": 1 }, + "overrides": { + "type": "v2-date", + "label": "๋‚ฉ๊ธฐ์ผ", + "fieldName": "due_date" + }, + "displayOrder": 9 + }, + { + "id": "comp_status", + "url": "@/lib/registry/components/v2-select", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 420, "z": 1 }, + "overrides": { + "type": "v2-select", + "label": "์ƒํƒœ", + "fieldName": "status", + "required": true, + "config": { + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "์ˆ˜์ฃผ", "label": "์ˆ˜์ฃผ" }, + { "value": "์ง„ํ–‰์ค‘", "label": "์ง„ํ–‰์ค‘" }, + { "value": "์™„๋ฃŒ", "label": "์™„๋ฃŒ" }, + { "value": "์ทจ์†Œ", "label": "์ทจ์†Œ" } + ] + } + }, + "displayOrder": 10 + }, + { + "id": "comp_shipping_method", + "url": "@/lib/registry/components/v2-select", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 420, "z": 1 }, + "overrides": { + "type": "v2-select", + "label": "๋ฐฐ์†ก๋ฐฉ๋ฒ•", + "fieldName": "shipping_method", + "config": { + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "ํƒ๋ฐฐ", "label": "ํƒ๋ฐฐ" }, + { "value": "ํ™”๋ฌผ", "label": "ํ™”๋ฌผ" }, + { "value": "์ง์†ก", "label": "์ง์†ก" }, + { "value": "ํ€ต์„œ๋น„์Šค", "label": "ํ€ต์„œ๋น„์Šค" }, + { "value": "ํ•ด์ƒ์šด์†ก", "label": "ํ•ด์ƒ์šด์†ก" } + ] + } + }, + "displayOrder": 11 + }, + { + "id": "comp_delivery_address", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 620, "height": 60 }, + "position": { "x": 20, "y": 500, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "๋‚ฉํ’ˆ์žฅ์†Œ", + "fieldName": "delivery_address", + "placeholder": "๋‚ฉํ’ˆ์žฅ์†Œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 12 + }, + { + "id": "comp_manager_name", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 580, "z": 1 }, + "overrides": { + "type": "v2-input", + "label": "๋‹ด๋‹น์ž", + "fieldName": "manager_name", + "placeholder": "๋‹ด๋‹น์ž๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 13 + }, + { + "id": "comp_memo", + "url": "@/lib/registry/components/v2-input", + "size": { "width": 620, "height": 80 }, + "position": { "x": 20, "y": 660, "z": 1 }, + "overrides": { + "type": "v2-input", + "inputType": "textarea", + "label": "๋ฉ”๋ชจ", + "fieldName": "memo", + "placeholder": "๋ฉ”๋ชจ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 14 + }, + { + "id": "comp_btn_save", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 540, "y": 760, "z": 1 }, + "overrides": { + "text": "์ €์žฅ", + "type": "v2-button-primary", + "label": "์ €์žฅ ๋ฒ„ํŠผ", + "action": { + "type": "save", + "closeModalAfterSave": true, + "refreshParentTable": true, + "successMessage": "์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + "errorMessage": "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." + }, + "variant": "primary", + "actionType": "button" + }, + "displayOrder": 15 + } + ] +} +``` + +### 8.4 ํ™”๋ฉด ์ƒ์„ฑ ์ˆœ์„œ (์ค‘์š”!) + +``` +1. ๋ชจ๋‹ฌ ํ™”๋ฉด ์ƒ์„ฑ (screen_definitions INSERT) + โ”‚ + โ–ผ +2. ๋ชจ๋‹ฌ ๋ ˆ์ด์•„์›ƒ ์ €์žฅ (screen_layouts_v2 INSERT) + โ”‚ + โ–ผ +3. ๋ฉ”์ธ ํ™”๋ฉด ์ƒ์„ฑ (screen_definitions INSERT) + โ”‚ + โ–ผ +4. ๋ฉ”์ธ ๋ ˆ์ด์•„์›ƒ ์ €์žฅ (screen_layouts_v2 INSERT) + โ””โ”€โ”€ targetScreenId์— ๋ชจ๋‹ฌ screen_id ์‚ฌ์šฉ! + โ”‚ + โ–ผ +5. (์„ ํƒ) ๋ฉ”๋‰ด์— ํ™”๋ฉด ์—ฐ๊ฒฐ +``` + +--- + +## 9. ํ™”๋ฉด ๊ตฌํ˜„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +> ๐Ÿ“‹ ์ƒˆ๋กœ์šด ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•  ๋•Œ ์•„๋ž˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ํ™•์ธํ•˜์„ธ์š”. + +### 9.1 ๋ถ„์„ ๋‹จ๊ณ„ + +| ์ฒดํฌ | ํ•ญ๋ชฉ | ์„ค๋ช… | +|:----:|------|------| +| โ˜ | **ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ๋ถ„์„** | `sales_order_mng` ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ํ™•์ธ ์™„๋ฃŒ | +| โ˜ | **ํ™”๋ฉด ๊ธฐ๋Šฅ ํŒŒ์•…** | ์กฐํšŒ/๋“ฑ๋ก/์ˆ˜์ •/์‚ญ์ œ, ๊ฒ€์ƒ‰, ํ•„ํ„ฐ, ๊ทธ๋ฃนํ•‘, ์ถœํ•˜๊ณ„ํš ์—ฐ๋™ | +| โ˜ | **์ปดํฌ๋„ŒํŠธ ๋งคํ•‘** | ํ•„์š” ๊ธฐ๋Šฅ โ†’ V2 ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ ์™„๋ฃŒ | +| โ˜ | **๊ตฌํ˜„ ๋ถˆ๊ฐ€ ํ•ญ๋ชฉ ํ™•์ธ** | ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ๋กœ ๊ตฌํ˜„ ๊ฐ€๋Šฅ | + +### 9.2 INSERT ํ›„ ๊ฒ€์ฆ + +| ์ฒดํฌ | ํ•ญ๋ชฉ | ์„ค๋ช… | +|:----:|------|------| +| โ˜ | **ํ™”๋ฉด ์ ‘์† ํ…Œ์ŠคํŠธ** | `/screens/{screen_id}` URL๋กœ ์ ‘์† | +| โ˜ | **์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ํ™•์ธ** | ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ •์ƒ ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธ | +| โ˜ | **๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ** | ๊ฒ€์ƒ‰ ์œ„์ ฏ ๋™์ž‘ ํ™•์ธ | +| โ˜ | **ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋กœ๋“œ** | ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธ | +| โ˜ | **๋ฒ„ํŠผ ๋™์ž‘ ํ…Œ์ŠคํŠธ** | ๋“ฑ๋ก/์ˆ˜์ •/์‚ญ์ œ/์ถœํ•˜๊ณ„ํš ๋ฒ„ํŠผ ๋™์ž‘ ํ™•์ธ | +| โ˜ | **๋ชจ๋‹ฌ ํผ ํ…Œ์ŠคํŠธ** | ๋ชจ๋‹ฌ ์—ด๋ฆผ, ์ž…๋ ฅ ํ•„๋“œ ํ‘œ์‹œ, ์ €์žฅ ๋™์ž‘ ํ™•์ธ | +| โ˜ | **ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ** | ์ด ๊ธˆ์•ก, ์ด ์ˆ˜๋Ÿ‰์ด ์ •ํ™•ํžˆ ํ‘œ์‹œ๋˜๋Š”์ง€ ํ™•์ธ | + +--- + +## 10. ๋ฉ”๋‰ด์— ํ™”๋ฉด ์—ฐ๊ฒฐํ•˜๊ธฐ + +> ๐Ÿ“‹ ํ™”๋ฉด ์ƒ์„ฑ ํ›„, ํŠน์ • ๋ฉ”๋‰ด์— ์—ฐ๊ฒฐํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + +### 10.1 ๋ฉ”๋‰ด ์—ฐ๊ฒฐ ์ ˆ์ฐจ + +``` +1. ๋Œ€์ƒ ๋ฉ”๋‰ด ์ฐพ๊ธฐ (menu_info ํ…Œ์ด๋ธ”์—์„œ objid ํ™•์ธ) + โ”‚ + โ–ผ +2. screen_menu_assignments ํ…Œ์ด๋ธ”์— ํ• ๋‹น ๋ ˆ์ฝ”๋“œ INSERT + โ”‚ + โ–ผ +3. menu_info ํ…Œ์ด๋ธ”์˜ menu_url, screen_code ์—…๋ฐ์ดํŠธ + โ”‚ + โ–ผ +4. ์—ฐ๊ฒฐ ๊ฒฐ๊ณผ ํ™•์ธ +``` + +### 10.2 ๋ฉ”๋‰ด ์ฐพ๊ธฐ + +```sql +-- ๋ฉ”๋‰ด ์ด๋ฆ„์œผ๋กœ ๊ฒ€์ƒ‰ +SELECT objid, menu_name_kor, menu_url, screen_code, company_code +FROM menu_info +WHERE menu_name_kor = '55566' -- ๋ฉ”๋‰ด ์ด๋ฆ„ + AND company_code = 'COMPANY_19'; -- ํšŒ์‚ฌ ์ฝ”๋“œ + +-- ๊ฒฐ๊ณผ ์˜ˆ์‹œ: +-- objid: 1769415229091 +``` + +### 10.3 ๊ธฐ์กด ํ• ๋‹น ํ™•์ธ ๋ฐ ์ œ๊ฑฐ (์ค‘๋ณต ๋ฐฉ์ง€) + +> โš ๏ธ **์ค‘์š”**: ์ƒˆ ํ™”๋ฉด์„ ํ• ๋‹นํ•˜๊ธฐ ์ „์— ํ•ด๋‹น ๋ฉ”๋‰ด์— ์ด๋ฏธ ํ• ๋‹น๋œ ํ™”๋ฉด์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ค‘๋ณต ํ• ๋‹น ์‹œ ํ™”๋ฉด์ด ์ •์ƒ์ ์œผ๋กœ ํ‘œ์‹œ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +```sql +-- 1. ํ•ด๋‹น ๋ฉ”๋‰ด์— ์ด๋ฏธ ํ• ๋‹น๋œ ํ™”๋ฉด ํ™•์ธ +SELECT + sma.assignment_id, + sma.screen_id, + sd.screen_name, + sd.screen_code +FROM screen_menu_assignments sma +JOIN screen_definitions sd ON sma.screen_id = sd.screen_id +WHERE sma.menu_objid = '1769415229091'; -- ๋Œ€์ƒ ๋ฉ”๋‰ด objid + +-- 2. ๊ธฐ์กด ํ• ๋‹น์ด ์žˆ๋‹ค๋ฉด ์‚ญ์ œ +DELETE FROM screen_menu_assignments +WHERE menu_objid = '1769415229091'; -- ๋ชจ๋“  ๊ธฐ์กด ํ• ๋‹น ์‚ญ์ œ + +-- ๋˜๋Š” ํŠน์ • ํ™”๋ฉด๋งŒ ๋‚จ๊ธฐ๊ณ  ์‚ญ์ œ +DELETE FROM screen_menu_assignments +WHERE menu_objid = '1769415229091' + AND screen_id != 3733; -- 3733(์ˆ˜์ฃผ๊ด€๋ฆฌ)๋งŒ ๋‚จ๊ธฐ๊ณ  ์‚ญ์ œ +``` + +### 10.4 ํ™”๋ฉด-๋ฉ”๋‰ด ํ• ๋‹น INSERT + +```sql +-- screen_menu_assignments์— ํ• ๋‹น ๋ ˆ์ฝ”๋“œ ์ถ”๊ฐ€ +INSERT INTO screen_menu_assignments ( + screen_id, + menu_objid, + company_code, + display_order, + is_active, + created_date +) VALUES ( + 3733, -- ๋ฉ”์ธ ํ™”๋ฉด์˜ screen_id + '1769415229091', -- menu_info์˜ objid (๋ฌธ์ž์—ด๋กœ ์ €์žฅ) + 'COMPANY_19', -- ํšŒ์‚ฌ ์ฝ”๋“œ + 1, -- ํ‘œ์‹œ ์ˆœ์„œ + 'Y', -- ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + NOW() +) RETURNING assignment_id; +``` + +### 10.6 ๋ฉ”๋‰ด URL ๋ฐ screen_code ์—…๋ฐ์ดํŠธ (ํ•„์ˆ˜!) + +> โš ๏ธ **์ค‘์š”**: `screen_menu_assignments`์— ๋ ˆ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด๋„ `menu_info`์˜ `menu_url`๊ณผ `screen_code`๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์ง€ ์•Š์œผ๋ฉด ๋ฉ”๋‰ด ํด๋ฆญ ์‹œ ํ™”๋ฉด์ด ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + +```sql +-- menu_info ํ…Œ์ด๋ธ”์˜ menu_url, screen_code ์—…๋ฐ์ดํŠธ +UPDATE menu_info +SET menu_url = '/screens/3733', -- ํ™”๋ฉด URL + screen_code = 'COMPANY_19_SO_MAIN' -- ํ™”๋ฉด ์ฝ”๋“œ +WHERE objid = 1769415229091; +``` + +### 10.7 ์—ฐ๊ฒฐ ํ™•์ธ + +```sql +-- ๋ฉ”๋‰ด-ํ™”๋ฉด ์—ฐ๊ฒฐ ์ƒํƒœ ํ™•์ธ +SELECT + mi.objid, + mi.menu_name_kor, + mi.menu_url, + mi.screen_code, + sd.screen_id, + sd.screen_name +FROM menu_info mi +JOIN screen_definitions sd ON mi.screen_code = sd.screen_code +WHERE mi.objid = 1769415229091; + +-- ์˜ˆ์ƒ ๊ฒฐ๊ณผ: +-- objid: 1769415229091 +-- menu_name_kor: 55566 +-- menu_url: /screens/3733 +-- screen_code: COMPANY_19_SO_MAIN +-- screen_id: 3733 +-- screen_name: ์ˆ˜์ฃผ๊ด€๋ฆฌ +``` + +### 10.8 ์ „์ฒด SQL ์˜ˆ์‹œ (์ˆ˜์ฃผ๊ด€๋ฆฌ ํ™”๋ฉด โ†’ 55566 ๋ฉ”๋‰ด) + +```sql +-- 1. ๋ฉ”๋‰ด ์ฐพ๊ธฐ +SELECT objid, menu_name_kor FROM menu_info +WHERE menu_name_kor = '55566' AND company_code = 'COMPANY_19'; +-- ๊ฒฐ๊ณผ: objid = 1769415229091 + +-- 2. ๊ธฐ์กด ํ• ๋‹น ํ™•์ธ ๋ฐ ์‚ญ์ œ (์ค‘๋ณต ๋ฐฉ์ง€!) +SELECT sma.assignment_id, sma.screen_id, sd.screen_name +FROM screen_menu_assignments sma +JOIN screen_definitions sd ON sma.screen_id = sd.screen_id +WHERE sma.menu_objid = '1769415229091'; + +-- ๊ธฐ์กด ํ• ๋‹น์ด ์žˆ๋‹ค๋ฉด ์‚ญ์ œ +DELETE FROM screen_menu_assignments +WHERE menu_objid = '1769415229091'; + +-- 3. ์ƒˆ ํ™”๋ฉด ํ• ๋‹น +INSERT INTO screen_menu_assignments (screen_id, menu_objid, company_code, display_order, is_active, created_date) +VALUES (3733, '1769415229091', 'COMPANY_19', 1, 'Y', NOW()); + +-- 4. ๋ฉ”๋‰ด URL ์—…๋ฐ์ดํŠธ (ํ•„์ˆ˜!) +UPDATE menu_info +SET menu_url = '/screens/3733', + screen_code = 'COMPANY_19_SO_MAIN' +WHERE objid = 1769415229091; +``` + +### 10.9 ๋ฉ”๋‰ด ์—ฐ๊ฒฐ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +| ์ฒดํฌ | ํ•ญ๋ชฉ | ์„ค๋ช… | +|:----:|------|------| +| โ˜ | **๋Œ€์ƒ ๋ฉ”๋‰ด ํ™•์ธ** | `menu_info`์—์„œ ๋ฉ”๋‰ด objid ํ™•์ธ | +| โ˜ | **๊ธฐ์กด ํ• ๋‹น ํ™•์ธ** | `screen_menu_assignments`์—์„œ ์ค‘๋ณต ํ• ๋‹น ์—ฌ๋ถ€ ํ™•์ธ | +| โ˜ | **๊ธฐ์กด ํ• ๋‹น ์‚ญ์ œ** | ์ค‘๋ณต ํ• ๋‹น์ด ์žˆ๋‹ค๋ฉด ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ DELETE | +| โ˜ | **์ƒˆ ํ™”๋ฉด ํ• ๋‹น INSERT** | `screen_menu_assignments` ํ…Œ์ด๋ธ”์— ์ƒˆ ๋ ˆ์ฝ”๋“œ ์ถ”๊ฐ€ | +| โ˜ | **menu_url ์—…๋ฐ์ดํŠธ** | `/screens/{screen_id}` ํ˜•์‹์œผ๋กœ ์—…๋ฐ์ดํŠธ | +| โ˜ | **screen_code ์—…๋ฐ์ดํŠธ** | ํ™”๋ฉด์˜ screen_code๋กœ ์—…๋ฐ์ดํŠธ | +| โ˜ | **๋ฉ”๋‰ด ํด๋ฆญ ํ…Œ์ŠคํŠธ** | ํ•ด๋‹น ํšŒ์‚ฌ๋กœ ๋กœ๊ทธ์ธํ•˜์—ฌ ๋ฉ”๋‰ด ํด๋ฆญ ์‹œ ํ™”๋ฉด ํ‘œ์‹œ ํ™•์ธ | diff --git a/docs/screen-implementation-guide/02_sales/quote.md b/docs/screen-implementation-guide/02_sales/quote.md new file mode 100644 index 00000000..eac09315 --- /dev/null +++ b/docs/screen-implementation-guide/02_sales/quote.md @@ -0,0 +1,308 @@ +# ๊ฒฌ์ ๊ด€๋ฆฌ ํ™”๋ฉด ๊ตฌํ˜„ ๊ฐ€์ด๋“œ + +> **ํ™”๋ฉด๋ช…**: ๊ฒฌ์ ๊ด€๋ฆฌ +> **ํŒŒ์ผ**: ๊ฒฌ์ ๊ด€๋ฆฌ.html +> **๋ถ„๋ฅ˜**: ์˜์—…๊ด€๋ฆฌ +> **๊ตฌํ˜„ ๊ฐ€๋Šฅ**: โœ… ์™„์ „ (ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ) + +--- + +## 1. ํ™”๋ฉด ๊ฐœ์š” + +๊ฒฌ์ ์„œ ์ƒ์„ฑ ๋ฐ ๊ด€๋ฆฌ ํ™”๋ฉด์œผ๋กœ, ๊ณ ๊ฐ ์š”์ฒญ์— ๋Œ€ํ•œ ๊ฒฌ์ ์„ ์ž‘์„ฑํ•˜๊ณ  ์ˆ˜์ฃผ๋กœ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + +### ํ•ต์‹ฌ ๊ธฐ๋Šฅ +- ๊ฒฌ์  ๋ชฉ๋ก ์กฐํšŒ/๊ฒ€์ƒ‰ +- ๊ฒฌ์  ๋“ฑ๋ก/์ˆ˜์ •/์‚ญ์ œ +- ๊ฒฌ์  ์ƒ์„ธ ๋ฐ ํ’ˆ๋ชฉ ๋‚ด์—ญ ๊ด€๋ฆฌ +- ๊ฒฌ์ ์„œ ์ธ์‡„/PDF ์ถœ๋ ฅ +- ์ˆ˜์ฃผ ์ „ํ™˜ + +--- + +## 2. ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๊ธฐ๊ฐ„] [๊ฑฐ๋ž˜์ฒ˜] [๊ฒฌ์ ๋ฒˆํ˜ธ] [ํ’ˆ๋ชฉ๋ช…] [์ƒํƒœโ–ผ] [์ดˆ๊ธฐํ™”][์กฐํšŒ] โ”‚ +โ”‚ [์‚ฌ์šฉ์ž์˜ต์…˜][์—…๋กœ๋“œ][๋‹ค์šด๋กœ๋“œ]โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ๐Ÿ“‹ ๊ฒฌ์  ๋ชฉ๋ก โ”‚ ๐Ÿ“„ ๊ฒฌ์  ์ƒ์„ธ โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ [์ธ์‡„] [๋ณต์‚ฌ] [์ˆ˜์ฃผ์ „ํ™˜] [์ˆ˜์ •] [์‚ญ์ œ] โ”‚ +โ”‚ [์‹ ๊ทœ๋“ฑ๋ก] โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ๊ฒฌ์ ๋ฒˆํ˜ธ: QT-2026-0001 โ”‚ +โ”‚ โ”‚๊ฒฌ์ ๋ฒˆํ˜ธ|๊ฑฐ๋ž˜์ฒ˜|๊ธˆ์•ก..โ”‚ โ”‚ ๊ฑฐ๋ž˜์ฒ˜: (์ฃผ)ํ…Œ์ŠคํŠธ โ”‚ +โ”‚ โ”‚QT-001 |A์‚ฌ|1,000..โ”‚ โ”‚ ๊ฒฌ์ ์ผ: 2026-01-30 โ”‚ +โ”‚ โ”‚QT-002 |B์‚ฌ|2,500..โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ [๊ธฐ๋ณธ์ •๋ณด] [ํ’ˆ๋ชฉ๋‚ด์—ญ] [์ฒจ๋ถ€ํŒŒ์ผ] โ”‚ +โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ ๋ฆฌ์‚ฌ์ด์ € โ†” โ”‚ โ”‚ํ’ˆ๋ชฉ์ฝ”๋“œ|ํ’ˆ๋ชฉ๋ช…|์ˆ˜๋Ÿ‰|๋‹จ๊ฐ€|๊ธˆ์•ก|๋น„๊ณ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚P-001 |์ œํ’ˆA|100|1,000|100,000| โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 3. V2 ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +| HTML ์˜์—ญ | V2 ์ปดํฌ๋„ŒํŠธ | ์ƒํƒœ | +|-----------|-------------|------| +| ๊ฒ€์ƒ‰ ์„น์…˜ | `v2-table-search-widget` | โœ… ๊ฐ€๋Šฅ | +| ๊ฒฌ์  ๋ชฉ๋ก | `v2-table-list` | โœ… ๊ฐ€๋Šฅ | +| ๋ถ„ํ•  ํŒจ๋„ | `v2-split-panel-layout` | โœ… ๊ฐ€๋Šฅ | +| ์ƒ์„ธ ํƒญ | `v2-tabs-widget` | โœ… ๊ฐ€๋Šฅ | +| ํ’ˆ๋ชฉ ๋‚ด์—ญ ํ…Œ์ด๋ธ” | `v2-table-list` | โœ… ๊ฐ€๋Šฅ | + +--- + +## 4. ํ…Œ์ด๋ธ” ์ •์˜ + +### 4.1 ๊ฒฌ์  ๋ชฉ๋ก (์ขŒ์ธก) + +```typescript +columns: [ + { id: 'checkbox', type: 'checkbox', width: 50 }, + { id: 'quote_no', label: '๊ฒฌ์ ๋ฒˆํ˜ธ', width: 120 }, + { id: 'quote_date', label: '๊ฒฌ์ ์ผ', width: 100 }, + { id: 'customer_name', label: '๊ฑฐ๋ž˜์ฒ˜', width: 150 }, + { id: 'total_amount', label: '๊ฒฌ์ ๊ธˆ์•ก', width: 120, align: 'right', format: 'currency' }, + { id: 'status', label: '์ƒํƒœ', width: 80 }, + { id: 'valid_date', label: '์œ ํšจ๊ธฐ๊ฐ„', width: 100 }, + { id: 'manager', label: '๋‹ด๋‹น์ž', width: 100 } +] +``` + +### 4.2 ํ’ˆ๋ชฉ ๋‚ด์—ญ (์šฐ์ธก ํƒญ) + +```typescript +detailColumns: [ + { id: 'seq', label: 'No', width: 50 }, + { id: 'item_code', label: 'ํ’ˆ๋ชฉ์ฝ”๋“œ', width: 100 }, + { id: 'item_name', label: 'ํ’ˆ๋ชฉ๋ช…', width: 200 }, + { id: 'spec', label: '๊ทœ๊ฒฉ', width: 150 }, + { id: 'quantity', label: '์ˆ˜๋Ÿ‰', width: 80, align: 'right' }, + { id: 'unit', label: '๋‹จ์œ„', width: 60 }, + { id: 'unit_price', label: '๋‹จ๊ฐ€', width: 100, align: 'right', format: 'currency' }, + { id: 'amount', label: '๊ธˆ์•ก', width: 120, align: 'right', format: 'currency' }, + { id: 'remark', label: '๋น„๊ณ ', width: 150 } +] +``` + +--- + +## 5. ๊ฒ€์ƒ‰ ์กฐ๊ฑด + +| ํ•„๋“œ๋ช… | ์ปดํฌ๋„ŒํŠธ | ์„ค์ • | +|--------|----------|------| +| ๊ธฐ๊ฐ„ | `v2-date` | dateRange: true | +| ๊ฑฐ๋ž˜์ฒ˜ | `v2-input` | placeholder: "๊ฑฐ๋ž˜์ฒ˜" | +| ๊ฒฌ์ ๋ฒˆํ˜ธ | `v2-input` | placeholder: "๊ฒฌ์ ๋ฒˆํ˜ธ" | +| ํ’ˆ๋ชฉ๋ช… | `v2-input` | placeholder: "ํ’ˆ๋ชฉ๋ช…" | +| ์ƒํƒœ | `v2-select` | ์ž‘์„ฑ์ค‘, ์ œ์ถœ, ์Šน์ธ, ๋ฐ˜๋ ค, ์ˆ˜์ฃผ์ „ํ™˜ | + +--- + +## 6. ์ƒ์„ธ ํƒญ ๊ตฌ์„ฑ + +```typescript +tabs: [ + { + id: 'basic', + label: '๊ธฐ๋ณธ์ •๋ณด', + fields: [ + { id: 'quote_no', label: '๊ฒฌ์ ๋ฒˆํ˜ธ' }, + { id: 'quote_date', label: '๊ฒฌ์ ์ผ' }, + { id: 'customer_code', label: '๊ฑฐ๋ž˜์ฒ˜์ฝ”๋“œ' }, + { id: 'customer_name', label: '๊ฑฐ๋ž˜์ฒ˜๋ช…' }, + { id: 'manager', label: '๋‹ด๋‹น์ž' }, + { id: 'valid_date', label: '์œ ํšจ๊ธฐ๊ฐ„' }, + { id: 'delivery_date', label: '๋‚ฉ๊ธฐ์ผ' }, + { id: 'payment_term', label: '๊ฒฐ์ œ์กฐ๊ฑด' }, + { id: 'remark', label: '๋น„๊ณ ' } + ] + }, + { + id: 'items', + label: 'ํ’ˆ๋ชฉ๋‚ด์—ญ', + type: 'table', + entityId: 'quote_items' + }, + { + id: 'files', + label: '์ฒจ๋ถ€ํŒŒ์ผ', + type: 'file-list' + } +] +``` + +--- + +## 7. ๋ฒ„ํŠผ ์•ก์…˜ + +### 7.1 ๋ชฉ๋ก ๋ฒ„ํŠผ +| ๋ฒ„ํŠผ | ์•ก์…˜ | +|------|------| +| ์‹ ๊ทœ๋“ฑ๋ก | ๊ฒฌ์  ๋“ฑ๋ก ๋ชจ๋‹ฌ ์—ด๊ธฐ | + +### 7.2 ์ƒ์„ธ ๋ฒ„ํŠผ +| ๋ฒ„ํŠผ | ์•ก์…˜ | +|------|------| +| ์ธ์‡„ | ๊ฒฌ์ ์„œ PDF ์ถœ๋ ฅ | +| ๋ณต์‚ฌ | ์„ ํƒ ๊ฒฌ์  ๋ณต์‚ฌํ•˜์—ฌ ์‹ ๊ทœ ์ƒ์„ฑ | +| ์ˆ˜์ฃผ์ „ํ™˜ | ๊ฒฌ์  โ†’ ์ˆ˜์ฃผ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ | +| ์ˆ˜์ • | ๊ฒฌ์  ์ˆ˜์ • ๋ชจ๋‹ฌ ์—ด๊ธฐ | +| ์‚ญ์ œ | ๊ฒฌ์  ์‚ญ์ œ (ํ™•์ธ ํ›„) | + +--- + +## 8. ๊ตฌํ˜„ JSON + +```json +{ + "screen_code": "QUOTE_MAIN", + "screen_name": "๊ฒฌ์ ๊ด€๋ฆฌ", + "components": [ + { + "type": "v2-table-search-widget", + "position": { "x": 0, "y": 0, "w": 12, "h": 2 }, + "config": { + "searchFields": [ + { "type": "date", "id": "date_range", "placeholder": "๊ธฐ๊ฐ„", "dateRange": true }, + { "type": "input", "id": "customer_name", "placeholder": "๊ฑฐ๋ž˜์ฒ˜" }, + { "type": "input", "id": "quote_no", "placeholder": "๊ฒฌ์ ๋ฒˆํ˜ธ" }, + { "type": "input", "id": "item_name", "placeholder": "ํ’ˆ๋ชฉ๋ช…" }, + { "type": "select", "id": "status", "placeholder": "์ƒํƒœ", + "options": [ + { "value": "draft", "label": "์ž‘์„ฑ์ค‘" }, + { "value": "submitted", "label": "์ œ์ถœ" }, + { "value": "approved", "label": "์Šน์ธ" }, + { "value": "rejected", "label": "๋ฐ˜๋ ค" }, + { "value": "converted", "label": "์ˆ˜์ฃผ์ „ํ™˜" } + ] + } + ], + "buttons": [ + { "label": "์ดˆ๊ธฐํ™”", "action": "reset", "variant": "outline" }, + { "label": "์กฐํšŒ", "action": "search", "variant": "primary" } + ], + "rightButtons": [ + { "label": "์‚ฌ์šฉ์ž์˜ต์…˜", "action": "userOptions", "variant": "outline" }, + { "label": "์—‘์…€์—…๋กœ๋“œ", "action": "excelUpload", "variant": "outline" }, + { "label": "์—‘์…€๋‹ค์šด๋กœ๋“œ", "action": "excelDownload", "variant": "outline" } + ] + } + }, + { + "type": "v2-split-panel-layout", + "position": { "x": 0, "y": 2, "w": 12, "h": 10 }, + "config": { + "masterPanel": { + "title": "๊ฒฌ์  ๋ชฉ๋ก", + "entityId": "quote_header", + "buttons": [ + { "label": "์‹ ๊ทœ๋“ฑ๋ก", "action": "create", "variant": "primary" } + ], + "columns": [ + { "id": "quote_no", "label": "๊ฒฌ์ ๋ฒˆํ˜ธ", "width": 120 }, + { "id": "quote_date", "label": "๊ฒฌ์ ์ผ", "width": 100 }, + { "id": "customer_name", "label": "๊ฑฐ๋ž˜์ฒ˜", "width": 150 }, + { "id": "total_amount", "label": "๊ฒฌ์ ๊ธˆ์•ก", "width": 120, "align": "right" }, + { "id": "status", "label": "์ƒํƒœ", "width": 80 }, + { "id": "manager", "label": "๋‹ด๋‹น์ž", "width": 100 } + ] + }, + "detailPanel": { + "title": "๊ฒฌ์  ์ƒ์„ธ", + "buttons": [ + { "label": "์ธ์‡„", "action": "print", "variant": "outline" }, + { "label": "๋ณต์‚ฌ", "action": "copy", "variant": "outline" }, + { "label": "์ˆ˜์ฃผ์ „ํ™˜", "action": "convert", "variant": "secondary" }, + { "label": "์ˆ˜์ •", "action": "edit", "variant": "outline" }, + { "label": "์‚ญ์ œ", "action": "delete", "variant": "destructive" } + ], + "tabs": [ + { + "id": "basic", + "label": "๊ธฐ๋ณธ์ •๋ณด", + "type": "form" + }, + { + "id": "items", + "label": "ํ’ˆ๋ชฉ๋‚ด์—ญ", + "type": "table", + "entityId": "quote_items", + "relationType": "one-to-many", + "relationKey": "quote_id" + }, + { + "id": "files", + "label": "์ฒจ๋ถ€ํŒŒ์ผ", + "type": "file" + } + ] + }, + "defaultRatio": 40, + "resizable": true + } + } + ] +} +``` + +--- + +## 9. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ” + +### quote_header (๊ฒฌ์  ํ—ค๋”) +```sql +CREATE TABLE quote_header ( + id SERIAL PRIMARY KEY, + company_code VARCHAR(20) NOT NULL, + quote_no VARCHAR(50) NOT NULL, + quote_date DATE NOT NULL, + customer_code VARCHAR(50), + customer_name VARCHAR(200), + total_amount NUMERIC(15,2), + tax_amount NUMERIC(15,2), + status VARCHAR(20) DEFAULT 'draft', + valid_date DATE, + delivery_date DATE, + payment_term VARCHAR(100), + manager VARCHAR(100), + remark TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +### quote_items (๊ฒฌ์  ํ’ˆ๋ชฉ) +```sql +CREATE TABLE quote_items ( + id SERIAL PRIMARY KEY, + company_code VARCHAR(20) NOT NULL, + quote_id INTEGER REFERENCES quote_header(id), + seq INTEGER, + item_code VARCHAR(50), + item_name VARCHAR(200), + spec VARCHAR(200), + quantity NUMERIC(15,3), + unit VARCHAR(20), + unit_price NUMERIC(15,2), + amount NUMERIC(15,2), + remark TEXT +); +``` + +--- + +## 10. ๊ตฌํ˜„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +- [x] ๊ฒ€์ƒ‰ ์˜์—ญ: v2-table-search-widget +- [x] ๋ถ„ํ•  ํŒจ๋„: v2-split-panel-layout +- [x] ๋ชฉ๋ก ํ…Œ์ด๋ธ”: v2-table-list +- [x] ์ƒ์„ธ ํƒญ: v2-tabs-widget +- [x] ํ’ˆ๋ชฉ ๋‚ด์—ญ ํ…Œ์ด๋ธ”: v2-table-list (nested) +- [ ] ์ธ์‡„ ๊ธฐ๋Šฅ: ๋ณ„๋„ ๊ตฌํ˜„ ํ•„์š” +- [ ] ์ˆ˜์ฃผ ์ „ํ™˜: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ตฌํ˜„ + +**ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ๋กœ 100% ๊ตฌํ˜„ ๊ฐ€๋Šฅ** diff --git a/docs/screen-implementation-guide/03_production/.gitkeep b/docs/screen-implementation-guide/03_production/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docs/screen-implementation-guide/03_production/production-plan.md b/docs/screen-implementation-guide/03_production/production-plan.md new file mode 100644 index 00000000..aa09cb47 --- /dev/null +++ b/docs/screen-implementation-guide/03_production/production-plan.md @@ -0,0 +1,1326 @@ +# ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ (Production Plan Management) + +> **Screen ID**: /screens/{TBD} +> **๋ฉ”๋‰ด ๊ฒฝ๋กœ**: ์ƒ์‚ฐ๊ด€๋ฆฌ > ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ + +> โš ๏ธ **๊ตฌํ˜„ ์ƒํƒœ ์•ˆ๋‚ด** +> - **๊ฐ„์†Œํ™” ๋ฒ„์ „ (ํŒจํ„ด A)**: โœ… ์ฆ‰์‹œ ๊ตฌํ˜„ ๊ฐ€๋Šฅ +> - **์ •์‹ ๋ฒ„์ „ (๋ณตํ•ฉ ๋ ˆ์ด์•„์›ƒ)**: โš ๏ธ ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ ํ›„ ๊ตฌํ˜„ ๊ฐ€๋Šฅ +> - ~~`v2-split-panel-layout` ์ปค์Šคํ…€ ๋ชจ๋“œ ํ™•์žฅ~~ โœ… **๊ตฌํ˜„ ์™„๋ฃŒ** +> - `v2-table-grouped`, `v2-timeline-scheduler` ์‹ ๊ทœ ๊ฐœ๋ฐœ (2๊ฐœ) + +--- + +## 1. ํ™”๋ฉด ๊ฐœ์š” + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +|------|------| +| **ํ™”๋ฉด๋ช…** | ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ | +| **์˜๋ฌธ๋ช…** | Production Plan Management | +| **Screen ID** | /screens/{TBD} | +| **๋ฉ”๋‰ด ๊ฒฝ๋กœ** | ์ƒ์‚ฐ๊ด€๋ฆฌ > ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ | +| **ํ™”๋ฉด ์œ ํ˜• (์ „์ฒด)** | ๋ณตํ•ฉํ˜• (์ขŒ์šฐ ๋ถ„ํ•  ํŒจ๋„ + ํƒ€์ž„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ) - โš ๏ธ ์ปดํฌ๋„ŒํŠธ ํ™•์žฅ ํ›„ ๊ตฌํ˜„ | +| **ํ™”๋ฉด ์œ ํ˜• (๊ฐ„์†Œํ™”)** | **ํŒจํ„ด A** (๊ธฐ๋ณธ ๋งˆ์Šคํ„ฐ ํ™”๋ฉด) - โœ… ์ฆ‰์‹œ ๊ตฌํ˜„ ๊ฐ€๋Šฅ | +| **๋ฉ”์ธ ํ…Œ์ด๋ธ”** | `production_plan_mng` | +| **๊ด€๋ จ ํ…Œ์ด๋ธ”** | `sales_order_mng`, `item_info`, `equipment_info`, `bom_info` | +| **์ฃผ์š” ๊ธฐ๋Šฅ** | ์ˆ˜์ฃผ ๊ธฐ๋ฐ˜ ์ƒ์‚ฐ๊ณ„ํš ์ˆ˜๋ฆฝ, ํƒ€์ž„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ, ์ž๋™ ์Šค์ผ€์ค„ ์ƒ์„ฑ, ๋ฐ˜์ œํ’ˆ ๊ณ„ํš ์—ฐ๋™ | +| **ํ•„์š” ์ž‘์—…** | ~~1. `v2-split-panel-layout` ํ™•์žฅ~~ โœ… ์™„๋ฃŒ / 2. `v2-table-grouped` ๊ฐœ๋ฐœ / 3. `v2-timeline-scheduler` ๊ฐœ๋ฐœ | + +--- + +## 2. ํ…Œ์ด๋ธ” ์„ ํƒ ๋ฐ ํ™”๋ฉด ๊ตฌ์กฐ + +### 2.1 ์‚ฌ์šฉ ํ…Œ์ด๋ธ” + +| ํ…Œ์ด๋ธ”๋ช… | ์šฉ๋„ | ๋น„๊ณ  | +|----------|------|------| +| `production_plan_mng` | ์ƒ์‚ฐ๊ณ„ํš ๋งˆ์Šคํ„ฐ | ์ฃผ ํ…Œ์ด๋ธ” | +| `sales_order_mng` | ์ˆ˜์ฃผ ์ •๋ณด | ์ฐธ์กฐ (์ˆ˜์ฃผ ๊ธฐ๋ฐ˜ ๊ณ„ํš) | +| `item_info` | ํ’ˆ๋ชฉ ์ •๋ณด | ์ฐธ์กฐ (ํ’ˆ๋ชฉ์ฝ”๋“œ, ํ’ˆ๋ช…) | +| `equipment_info` | ์„ค๋น„ ์ •๋ณด | ์ฐธ์กฐ (์„ค๋น„ ํ• ๋‹น) | +| `bom_info` | BOM ์ •๋ณด | ์ฐธ์กฐ (๋ฐ˜์ œํ’ˆ ๊ณ„ํš ์ƒ์„ฑ) | + +### 2.2 ํ…Œ์ด๋ธ” ๊ด€๊ณ„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ sales_order_mng โ”‚ โ”‚ production_plan_mng โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ order_no (PK) โ”‚โ”€โ”€N:1โ”€โ”€โ”‚ order_no (FK) โ”‚ +โ”‚ item_code โ”‚ โ”‚ id (PK) โ”‚ +โ”‚ ... โ”‚ โ”‚ plan_no โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ item_code โ”‚ + โ”‚ equipment_id (FK) โ”‚ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ parent_plan_id (FK) โ”‚ +โ”‚ item_info โ”‚ โ”‚ ... โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โ”‚ item_code (PK) โ”‚โ”€โ”€1:Nโ”€โ”€ โ”‚ +โ”‚ item_name โ”‚ โ”‚ +โ”‚ ... โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ + โ”‚ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ equipment_info โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ id (PK) โ”‚โ”€โ”€1:Nโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โ”‚ equipment_code โ”‚ +โ”‚ equipment_name โ”‚ +โ”‚ ... โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2.3 ํ™”๋ฉด ๊ตฌ์กฐ ๋ถ„์„ (์ „์ฒด ๋ฒ„์ „) + +#### 2.3.1 ๋ ˆ์ด์•„์›ƒ ๊ตฌ์กฐ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๊ฒ€์ƒ‰ ์„น์…˜ โ”‚ +โ”‚ [๊ฒ€์ƒ‰ํ•„๋“œ๋“ค...] [์‚ฌ์šฉ์ž์˜ต์…˜] [์—‘์…€์—…๋กœ๋“œ] [์—‘์…€๋‹ค์šด๋กœ๋“œ] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ์™ผ์ชฝ ํŒจ๋„ (50%, ๋ฆฌ์‚ฌ์ด์ฆˆ) โ”‚ โ”‚ ์˜ค๋ฅธ์ชฝ ํŒจ๋„ (50%) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚๋ฆฌโ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [์ˆ˜์ฃผ๋ฐ์ดํ„ฐ] [์•ˆ์ „์žฌ๊ณ ๋ถ€์กฑ] โ”‚ โ”‚์‚ฌโ”‚ โ”‚ [์™„์ œํ’ˆ ์ƒ์‚ฐ๊ณ„ํš] [๋ฐ˜์ œํ’ˆ ์ƒ์‚ฐ๊ณ„ํš] โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚์ดโ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚์ฆˆโ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ ์ˆ˜์ฃผ ๋ชฉ๋ก ํ…Œ์ด๋ธ” โ”‚ โ”‚ํ•ธโ”‚ โ”‚ ํƒ€์ž„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ โ”‚ +โ”‚ โ”‚ (๊ทธ๋ฃนํ™”๋œ ํ’ˆ๋ชฉ๋ณ„ ์ˆ˜์ฃผ) โ”‚ โ”‚๋“คโ”‚ โ”‚ (๊ฐ„ํŠธ์ฐจํŠธ ํ˜•ํƒœ) โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ - ์ฒดํฌ๋ฐ•์Šค โ”‚ โ”‚ โ”‚ โ”‚ - ๋‚ ์งœ๋ณ„ ๊ทธ๋ฆฌ๋“œ โ”‚ +โ”‚ โ”‚ - ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ ํ† ๊ธ€ โ”‚ โ”‚ โ”‚ โ”‚ - ์ƒ์‚ฐ๊ณ„ํš ๋ฐ” (๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅ) โ”‚ +โ”‚ โ”‚ - ํ’ˆ๋ชฉ๋ณ„ ๊ทธ๋ฃน ํ–‰ โ”‚ โ”‚ โ”‚ โ”‚ - ๋‚ฉ๊ธฐ์ผ ๋งˆ์ปค โ”‚ +โ”‚ โ”‚ - ์ˆ˜์ฃผ ์ƒ์„ธ ํ–‰ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ [๊ณ„ํš์— ์—†๋Š” ํ’ˆ๋ชฉ๋งŒ] [์„ ํƒํ’ˆ๋ชฉ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ] [์ƒˆ๋กœ๊ณ ์นจ] โ”‚ [์ž๋™์Šค์ผ€์ค„] [์ €์žฅ] [์ดˆ๊ธฐํ™”] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +#### 2.3.2 ํƒญ ๊ตฌ์กฐ + +**์™ผ์ชฝ ํŒจ๋„ ํƒญ**: +1. **์ˆ˜์ฃผ๋ฐ์ดํ„ฐ**: ์ˆ˜์ฃผ ๋ชฉ๋ก (ํ’ˆ๋ชฉ๋ณ„ ๊ทธ๋ฃนํ•‘) +2. **์•ˆ์ „์žฌ๊ณ  ๋ถ€์กฑ๋ถ„**: ์•ˆ์ „์žฌ๊ณ  ๋ฏธ๋‹ฌ ํ’ˆ๋ชฉ ๋ชฉ๋ก + +**์˜ค๋ฅธ์ชฝ ํŒจ๋„ ํƒญ**: +1. **์™„์ œํ’ˆ ์ƒ์‚ฐ๊ณ„ํš**: ์™„์ œํ’ˆ ํƒ€์ž„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ +2. **๋ฐ˜์ œํ’ˆ ์ƒ์‚ฐ๊ณ„ํš**: ๋ฐ˜์ œํ’ˆ ํƒ€์ž„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ + +--- + +## 3. ํ…Œ์ด๋ธ” ์ •์˜ + +### 3.1 ๋ฉ”์ธ ํ…Œ์ด๋ธ”: `production_plan_mng` + +| ์ปฌ๋Ÿผ๋ช… | ํƒ€์ž… | NULL | ์„ค๋ช… | +|--------|------|------|------| +| id | SERIAL | NO | PK | +| company_code | VARCHAR(20) | NO | ํšŒ์‚ฌ ์ฝ”๋“œ | +| plan_no | VARCHAR(50) | NO | ์ƒ์‚ฐ๊ณ„ํš๋ฒˆํ˜ธ | +| plan_date | DATE | NO | ๊ณ„ํš์ผ์ž | +| item_code | VARCHAR(50) | NO | ํ’ˆ๋ชฉ์ฝ”๋“œ | +| item_name | VARCHAR(200) | YES | ํ’ˆ๋ช… | +| plan_qty | NUMERIC(15,3) | NO | ๊ณ„ํš์ˆ˜๋Ÿ‰ | +| start_date | DATE | NO | ์‹œ์ž‘์ผ | +| end_date | DATE | NO | ์ข…๋ฃŒ์ผ | +| due_date | DATE | YES | ๋‚ฉ๊ธฐ์ผ | +| equipment_id | INTEGER | YES | ์„ค๋น„ ID (FK) | +| equipment_name | VARCHAR(100) | YES | ์„ค๋น„๋ช… | +| status | VARCHAR(20) | YES | ์ƒํƒœ (๊ณ„ํš/์ง€์‹œ/์ง„ํ–‰/์™„๋ฃŒ) | +| priority | VARCHAR(20) | YES | ์šฐ์„ ์ˆœ์œ„ | +| work_shift | VARCHAR(20) | YES | ์ž‘์—…์กฐ (์ฃผ๊ฐ„/์•ผ๊ฐ„/์ฃผ์•ผ) | +| manager_name | VARCHAR(100) | YES | ๋‹ด๋‹น์ž | +| work_order_no | VARCHAR(50) | YES | ์ž‘์—…์ง€์‹œ๋ฒˆํ˜ธ | +| remarks | TEXT | YES | ๋น„๊ณ  | +| order_no | VARCHAR(50) | YES | ๊ด€๋ จ ์ˆ˜์ฃผ๋ฒˆํ˜ธ | +| partner_id | VARCHAR(50) | YES | ๊ฑฐ๋ž˜์ฒ˜ ID | +| hourly_capacity | NUMERIC(15,3) | YES | ์‹œ๊ฐ„๋‹น ์ƒ์‚ฐ๋Šฅ๋ ฅ | +| daily_capacity | NUMERIC(15,3) | YES | ์ผ์ผ ์ƒ์‚ฐ๋Šฅ๋ ฅ | +| lead_time | INTEGER | YES | ๋ฆฌ๋“œํƒ€์ž„ (์ผ) | +| product_type | VARCHAR(20) | YES | ์ œํ’ˆ์œ ํ˜• (์™„์ œํ’ˆ/๋ฐ˜์ œํ’ˆ) | +| parent_plan_id | INTEGER | YES | ๋ชจํ’ˆ๋ชฉ ๊ณ„ํš ID (๋ฐ˜์ œํ’ˆ์šฉ) | +| created_date | TIMESTAMPTZ | YES | ์ƒ์„ฑ์ผ์‹œ | +| created_by | VARCHAR(50) | YES | ์ƒ์„ฑ์ž | +| updated_date | TIMESTAMPTZ | YES | ์ˆ˜์ •์ผ์‹œ | +| updated_by | VARCHAR(50) | YES | ์ˆ˜์ •์ž | + +### 3.2 ๊ด€๋ จ ํ…Œ์ด๋ธ” + +#### `equipment_info` (์„ค๋น„ ์ •๋ณด) + +| ์ปฌ๋Ÿผ๋ช… | ํƒ€์ž… | ์„ค๋ช… | +|--------|------|------| +| id | SERIAL | PK | +| equipment_code | VARCHAR(50) | ์„ค๋น„์ฝ”๋“œ | +| equipment_name | VARCHAR(100) | ์„ค๋น„๋ช… | +| equipment_type | VARCHAR(50) | ์„ค๋น„์œ ํ˜• | +| capacity_per_hour | NUMERIC(15,3) | ์‹œ๊ฐ„๋‹น ์ƒ์‚ฐ๋Šฅ๋ ฅ | +| status | VARCHAR(20) | ์ƒํƒœ | + +--- + +## 4. ๊ตฌํ˜„ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ๋ถ„์„ + +### 4.1 ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ๋กœ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•œ ๊ธฐ๋Šฅ + +| ๊ธฐ๋Šฅ | ๊ฐ€๋Šฅ ์—ฌ๋ถ€ | ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ | ๋น„๊ณ  | +|------|:---------:|--------------|------| +| ๊ฒ€์ƒ‰ ํ•„ํ„ฐ | โœ… | `v2-table-search-widget` | | +| ๊ธฐ๋ณธ ๋ฒ„ํŠผ (์—‘์…€, ์‚ฌ์šฉ์ž์˜ต์…˜) | โœ… | `v2-button-primary` | | +| ๋‹จ์ผ ํ…Œ์ด๋ธ” ๋ชฉ๋ก | โœ… | `v2-table-list` | | +| ๊ธฐ๋ณธ ๋ชจ๋‹ฌ ํผ | โœ… | ๋ชจ๋‹ฌ ํ™”๋ฉด | | +| ์ขŒ์šฐ ๋ถ„ํ•  ํŒจ๋„ (์ปค์Šคํ…€ ๋ชจ๋“œ) | โœ… | `v2-split-panel-layout` | **displayMode: "custom" ์ง€์›** | +| ํƒญ ์ปดํฌ๋„ŒํŠธ (๊ธฐ๋ณธ) | โš ๏ธ | `v2-tabs-widget` | ๋””์ž์ธ ๋ชจ๋“œ์—์„œ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ | + +### 4.2 ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ์˜ ์ œํ•œ ์‚ฌํ•ญ ๋ฐ ํ™•์žฅ ๋ฐฉ์•ˆ + +#### `v2-split-panel-layout` ํ˜„์žฌ ๊ธฐ๋Šฅ + +| ๊ธฐ๋Šฅ | ์ง€์› | ์„ค๋ช… | +|------|:----:|------| +| ์ขŒ์šฐ ๋ถ„ํ•  ํŒจ๋„ | โœ… | ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ฐ€๋Šฅ | +| ํ…Œ์ด๋ธ”/๋ฆฌ์ŠคํŠธ ํ‘œ์‹œ | โœ… | `displayMode: "list" \| "table"` | +| ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ๊ด€๊ณ„ | โœ… | `relation` ์„ค์ • | +| ์šฐ์ธก ์ถ”๊ฐ€ ํƒญ (`additionalTabs`) | โš ๏ธ | **ํƒ€์ž…๋งŒ ์ •์˜, ๋ Œ๋”๋ง ๋ฏธ๊ตฌํ˜„** | +| **ํŒจ๋„ ๋‚ด ์ž„์˜ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜** | โœ… | **`displayMode: "custom"` - ๊ตฌํ˜„ ์™„๋ฃŒ!** | + +#### โœ… ์ปค์Šคํ…€ ๋ชจ๋“œ (๊ตฌํ˜„ ์™„๋ฃŒ) + +`v2-tabs-widget`๊ณผ ๋™์ผํ•œ ๊ตฌ์กฐ๋กœ ํŒจ๋„ ์•ˆ์— ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ๋ฐฐ์น˜: + +```typescript +leftPanel: { + displayMode: "list" | "table" | "custom", // โœ… ์ปค์Šคํ…€ ๋ชจ๋“œ ์ง€์› + + // ๊ธฐ์กด ๋ชจ๋“œ (list, table) + tableName?: string, + columns?: [...], + + // โœ… ์ปค์Šคํ…€ ๋ชจ๋“œ - ํŒจ๋„ ์•ˆ์— ์ž์œ ๋กญ๊ฒŒ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ + components?: PanelInlineComponent[], +} + +// PanelInlineComponent ๊ตฌ์กฐ +interface PanelInlineComponent { + id: string; + componentType: string; + label?: string; + position?: { x: number; y: number }; + size?: { width: number; height: number }; + componentConfig?: Record; +} +``` + +**์ปค์Šคํ…€ ๋ชจ๋“œ ๊ธฐ๋Šฅ**: +- โœ… ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ ์ด๋™ +- โœ… ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค๋กœ ํฌ๊ธฐ ์กฐ์ ˆ +- โœ… ์ปดํฌ๋„ŒํŠธ ํด๋ฆญ ์‹œ ์ขŒ์ธก ์„ค์ • ํŒจ๋„์—์„œ ์†์„ฑ ํŽธ์ง‘ +- โœ… ๋””์ž์ธ ๋ชจ๋“œ์—์„œ ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ Œ๋”๋ง +- โœ… ํƒญ, ํ…Œ์ด๋ธ”, ๋ฒ„ํŠผ ๋“ฑ ๋ชจ๋“  V2 ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ ๊ฐ€๋Šฅ + +#### `v2-tabs-widget` ํ˜„์žฌ ๊ธฐ๋Šฅ + +| ๊ธฐ๋Šฅ | ์ง€์› | ์„ค๋ช… | +|------|:----:|------| +| ํƒญ๋ณ„ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ | โœ… | `components[]` | +| ๋””์ž์ธ ๋ชจ๋“œ ๋“œ๋ž˜๊ทธ&๋“œ๋กญ | โœ… | ์œ„์น˜/ํฌ๊ธฐ ์กฐ์ • | +| ๋Ÿฐํƒ€์ž„ ๋ Œ๋”๋ง | โœ… | `DynamicComponentRenderer` ์‚ฌ์šฉ | + +**`v2-tabs-widget` ๊ตฌ์กฐ** (์ฐธ๊ณ ์šฉ): +```typescript +interface TabItem { + id: string; + label: string; + components?: TabInlineComponent[]; // ํƒญ ์•ˆ์— ๋ฐฐ์น˜๋œ ์ปดํฌ๋„ŒํŠธ๋“ค +} + +interface TabInlineComponent { + id: string; + componentType: string; // "v2-table-list", "v2-timeline-scheduler" ๋“ฑ + position?: { x: number; y: number }; + size?: { width: number; height: number }; + componentConfig?: Record; +} +``` + +### 4.3 ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ์— ํ•„์š”ํ•œ ๊ธฐ๋Šฅ vs ํ˜„์žฌ ์ง€์› + +| ํ•„์š” ๊ธฐ๋Šฅ | ํ˜„์žฌ ์ง€์› | ํ•ด๊ฒฐ ๋ฐฉ์•ˆ | +|----------|:---------:|----------| +| ์ขŒ์šฐ ๋ถ„ํ•  ํŒจ๋„ | โœ… | `v2-split-panel-layout` | +| ์™ผ์ชฝ ํŒจ๋„ ํƒญ (์ˆ˜์ฃผ/์•ˆ์ „์žฌ๊ณ ) | โœ… | **์ปค์Šคํ…€ ๋ชจ๋“œ + `v2-tabs-widget` ์กฐํ•ฉ (๊ตฌํ˜„ ์™„๋ฃŒ)** | +| ์˜ค๋ฅธ์ชฝ ํŒจ๋„ ํƒญ (์™„์ œํ’ˆ/๋ฐ˜์ œํ’ˆ) | โœ… | **์ปค์Šคํ…€ ๋ชจ๋“œ + `v2-tabs-widget` ์กฐํ•ฉ (๊ตฌํ˜„ ์™„๋ฃŒ)** | +| **๊ทธ๋ฃนํ™”๋œ ํ…Œ์ด๋ธ”** (ํ’ˆ๋ชฉ๋ณ„ ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ) | โŒ | **์‹ ๊ทœ ๊ฐœ๋ฐœ ํ•„์š”** | +| **ํƒ€์ž„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ** (๊ฐ„ํŠธ์ฐจํŠธ) | โŒ | **์‹ ๊ทœ ๊ฐœ๋ฐœ ํ•„์š”** | +| ๋“œ๋ž˜๊ทธ&๋“œ๋กญ ์Šค์ผ€์ค„ ์ด๋™ | โŒ | `v2-timeline-scheduler`์— ํฌํ•จ | +| ๋ณต์žกํ•œ ์ƒ์„ธ ๋ชจ๋‹ฌ | โœ… | ๊ธฐ์กด ๋ชจ๋‹ฌ ํ™”๋ฉด์œผ๋กœ ์ถฉ๋ถ„ | + +### 4.4 ํ–ฅํ›„ ๊ฐœ๋ฐœ ํ•„์š” ์‚ฌํ•ญ + +#### ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ ํ™•์žฅ (1๊ฐœ) + +| ์ปดํฌ๋„ŒํŠธ | ์ž‘์—… ๋‚ด์šฉ | ์ƒํƒœ | +|----------|----------|:------:| +| `v2-split-panel-layout` | `displayMode: "custom"` ์ถ”๊ฐ€ + `components[]` ์ง€์› | โœ… **์™„๋ฃŒ** | + +#### ์‹ ๊ทœ ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ (2๊ฐœ) - ๋Œ€์ฒด ๋ถˆ๊ฐ€ + +| ์ปดํฌ๋„ŒํŠธ | ํ•„์š” ์ด์œ  | ์ƒํƒœ | +|----------|----------|:------:| +| ~~**`v2-table-grouped`**~~ | ํ’ˆ๋ชฉ๋ณ„ ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” | โœ… **์™„๋ฃŒ** | +| **`v2-timeline-scheduler`** | ๊ฐ„ํŠธ์ฐจํŠธ ํ˜•ํƒœ์˜ ํƒ€์ž„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ | โณ ๋Œ€๊ธฐ | + +#### ๋ถˆํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ (์ด์ „ ๋ถ„์„์—์„œ ์ œ์™ธ) + +| ์ปดํฌ๋„ŒํŠธ | ์ œ์™ธ ์ด์œ  | +|----------|----------| +| ~~`v2-split-panel-enhanced`~~ | ์ปค์Šคํ…€ ๋ชจ๋“œ๋กœ ๋Œ€์ฒด ๊ฐ€๋Šฅ | +| ~~`v2-modal-complex`~~ | ๊ธฐ์กด ๋ชจ๋‹ฌ ํ™”๋ฉด์œผ๋กœ ์ถฉ๋ถ„ | + +``` +์ตœ์ข… ํ•„์š” ์ž‘์—…: +1. โœ… v2-split-panel-layout ํ™•์žฅ - displayMode: "custom" ์ถ”๊ฐ€ [์™„๋ฃŒ] +2. โœ… v2-table-grouped ์‹ ๊ทœ ๊ฐœ๋ฐœ - ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” (์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ) [์™„๋ฃŒ] +3. v2-timeline-scheduler ์‹ ๊ทœ ๊ฐœ๋ฐœ - ํƒ€์ž„๋ผ์ธ/๊ฐ„ํŠธ์ฐจํŠธ (ํ•ต์‹ฌ!) +``` + +### 4.5 ํ˜„์žฌ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•œ ์ตœ๋Œ€ ๋ฒ”์œ„ + +ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ๋กœ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•œ **์ตœ๋Œ€ ๋ฒ”์œ„**: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๊ฒ€์ƒ‰ ์„น์…˜ (v2-table-search-widget) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ์ƒ์‚ฐ๊ณ„ํš ํ…Œ์ด๋ธ” (v2-table-list) - ๋‹จ์ผ ํ…Œ์ด๋ธ”, ๊ทธ๋ฃนํ™” ์—†์Œ โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [๋“ฑ๋ก] [์ˆ˜์ •] [์‚ญ์ œ] (v2-button-primary) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**๊ตฌํ˜„ ๋ถˆ๊ฐ€๋Šฅํ•œ ํ•ต์‹ฌ ๊ธฐ๋Šฅ**: +- ํ’ˆ๋ชฉ๋ณ„ ๊ทธ๋ฃนํ•‘ (์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ) +- ๊ฐ„ํŠธ์ฐจํŠธ ํƒ€์ž„๋ผ์ธ +- ์ž๋™ ์Šค์ผ€์ค„ ์ƒ์„ฑ +- ๋“œ๋ž˜๊ทธ๋กœ ์Šค์ผ€์ค„ ์ด๋™ + +### 4.6 ์ •์‹ ๋ฒ„์ „ ๊ตฌํ˜„ ๊ตฌ์กฐ (ํ™•์žฅ ํ›„) + +`v2-split-panel-layout`์— ์ปค์Šคํ…€ ๋ชจ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•œ ํ›„์˜ **์ •์‹ ๋ฒ„์ „ ๊ตฌํ˜„ ๊ตฌ์กฐ**: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-split-panel-layout (์–‘์ชฝ ํŒจ๋„ displayMode: "custom") โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ leftPanel.components: [ โ”‚ rightPanel.components: [ โ”‚ +โ”‚ v2-tabs-widget { โ”‚ v2-tabs-widget { โ”‚ +โ”‚ tabs: [ โ”‚ tabs: [ โ”‚ +โ”‚ { โ”‚ { โ”‚ +โ”‚ label: "์ˆ˜์ฃผ๋ฐ์ดํ„ฐ", โ”‚ label: "์™„์ œํ’ˆ ์ƒ์‚ฐ๊ณ„ํš", โ”‚ +โ”‚ components: [ โ”‚ components: [ โ”‚ +โ”‚ v2-table-grouped โ”‚ v2-timeline-scheduler โ”‚ +โ”‚ ] โ”‚ ] โ”‚ +โ”‚ }, โ”‚ }, โ”‚ +โ”‚ { โ”‚ { โ”‚ +โ”‚ label: "์•ˆ์ „์žฌ๊ณ ๋ถ€์กฑ", โ”‚ label: "๋ฐ˜์ œํ’ˆ ์ƒ์‚ฐ๊ณ„ํš", โ”‚ +โ”‚ components: [ โ”‚ components: [ โ”‚ +โ”‚ v2-table-grouped โ”‚ v2-timeline-scheduler โ”‚ +โ”‚ ] โ”‚ ] โ”‚ +โ”‚ } โ”‚ } โ”‚ +โ”‚ ] โ”‚ ] โ”‚ +โ”‚ } โ”‚ } โ”‚ +โ”‚ ] โ”‚ ] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ปดํฌ๋„ŒํŠธ ์ค‘์ฒฉ ๊ตฌ์กฐ**: +``` +v2-split-panel-layout (displayMode: "custom") +โ”œโ”€โ”€ leftPanel.components[] +โ”‚ โ””โ”€โ”€ v2-tabs-widget +โ”‚ โ”œโ”€โ”€ tabs[0]: "์ˆ˜์ฃผ๋ฐ์ดํ„ฐ" +โ”‚ โ”‚ โ””โ”€โ”€ components[] +โ”‚ โ”‚ โ””โ”€โ”€ v2-table-grouped (ํ’ˆ๋ชฉ๋ณ„ ๊ทธ๋ฃนํ™”) +โ”‚ โ””โ”€โ”€ tabs[1]: "์•ˆ์ „์žฌ๊ณ ๋ถ€์กฑ" +โ”‚ โ””โ”€โ”€ components[] +โ”‚ โ””โ”€โ”€ v2-table-grouped +โ”‚ +โ””โ”€โ”€ rightPanel.components[] + โ””โ”€โ”€ v2-tabs-widget + โ”œโ”€โ”€ tabs[0]: "์™„์ œํ’ˆ ์ƒ์‚ฐ๊ณ„ํš" + โ”‚ โ””โ”€โ”€ components[] + โ”‚ โ””โ”€โ”€ v2-timeline-scheduler (๊ฐ„ํŠธ์ฐจํŠธ) + โ””โ”€โ”€ tabs[1]: "๋ฐ˜์ œํ’ˆ ์ƒ์‚ฐ๊ณ„ํš" + โ””โ”€โ”€ components[] + โ””โ”€โ”€ v2-timeline-scheduler +``` + +**์žฅ์ **: +- ๊ธฐ์กด `v2-tabs-widget` ๋กœ์ง 100% ์žฌ์‚ฌ์šฉ +- ํŒจ๋„ ์•ˆ์— ์–ด๋–ค ์ปดํฌ๋„ŒํŠธ๋“  ๋ฐฐ์น˜ ๊ฐ€๋Šฅ +- ๋ณ„๋„ `v2-split-panel-enhanced` ๊ฐœ๋ฐœ ๋ถˆํ•„์š” +- ํ™•์žฅ์„ฑ ๋›ฐ์–ด๋‚จ (ํ–ฅํ›„ ๋‹ค๋ฅธ ๋ณตํ•ฉ ํ™”๋ฉด์—๋„ ์ ์šฉ ๊ฐ€๋Šฅ) + +--- + +## 5. ๋‹จ์ˆœํ™”๋œ ๊ตฌํ˜„ ๋ฐฉ์•ˆ (์ž„์‹œ) + +> ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ๋งŒ์œผ๋กœ **๊ฐ„์†Œํ™”๋œ ๋ฒ„์ „**์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +### 5.1 ๊ฐ„์†Œํ™” ๋ฒ„์ „ ๋ ˆ์ด์•„์›ƒ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๊ฒ€์ƒ‰ ์„น์…˜ โ”‚ +โ”‚ [ํ’ˆ๋ชฉ์ฝ”๋“œ] [ํ’ˆ๋ช…] [๊ณ„ํš๊ธฐ๊ฐ„] [์ƒํƒœ] [์‚ฌ์šฉ์ž์˜ต์…˜] [์—‘์…€๋‹ค์šด๋กœ๋“œ] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ์ƒ์‚ฐ๊ณ„ํš ๋ชฉ๋ก ํ…Œ์ด๋ธ” โ”‚ +โ”‚ (๋‹จ์ผ ํ…Œ์ด๋ธ”, ๊ทธ๋ฃนํ™” ์—†์Œ) โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [์‹ ๊ทœ๋“ฑ๋ก] [์ˆ˜์ •] [์‚ญ์ œ] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 5.2 ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜๋„ (๊ฐ„์†Œํ™” ๋ฒ„์ „) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-table-search-widget (comp_search) โ”‚ +โ”‚ [ํ’ˆ๋ชฉ์ฝ”๋“œ] [ํ’ˆ๋ช…] [๊ณ„ํš๊ธฐ๊ฐ„: ์‹œ์ž‘~์ข…๋ฃŒ] [์ƒํƒœ] [์กฐํšŒ] [์—‘์…€๋‹ค์šด๋กœ๋“œ] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ์ƒ์‚ฐ๊ณ„ํš ๋ชฉ๋ก [์‹ ๊ทœ๋“ฑ๋ก] [์ˆ˜์ •] [์‚ญ์ œ] [์ž‘์—…์ง€์‹œ ์ƒ์„ฑ] โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ v2-table-list (comp_table) โ”‚ +โ”‚ โ–ก | ๊ณ„ํš๋ฒˆํ˜ธ | ๊ณ„ํš์ผ์ž | ํ’ˆ๋ชฉ์ฝ”๋“œ | ํ’ˆ๋ช… | ๊ณ„ํš์ˆ˜๋Ÿ‰ | ์‹œ์ž‘์ผ | ์ข…๋ฃŒ์ผ | ... โ”‚ +โ”‚ โ–ก | PP-001 | 2026-01 | ITEM001 | ์ œํ’ˆA| 1,000 | 01-30 | 02-05 | ... โ”‚ +โ”‚ โ–ก | PP-002 | 2026-01 | ITEM002 | ์ œํ’ˆB| 500 | 02-01 | 02-10 | ... โ”‚ +โ”‚ โ”‚ +โ”‚ [1] [2] [3] ... [10] ํŽ˜์ด์ง€๋‹น: [20] โ–ผ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 5.3 ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก (๊ฐ„์†Œํ™” ๋ฒ„์ „) + +| ์ปดํฌ๋„ŒํŠธ ID | ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… | ์—ญํ•  | +|-------------|---------------|------| +| `comp_search` | v2-table-search-widget | ๊ฒ€์ƒ‰ ํ•„ํ„ฐ | +| `comp_table` | v2-table-list | ์ƒ์‚ฐ๊ณ„ํš ๋ชฉ๋ก | +| `comp_btn_register` | v2-button-primary | ์‹ ๊ทœ ๋“ฑ๋ก | +| `comp_btn_edit` | v2-button-primary | ์ˆ˜์ • | +| `comp_btn_delete` | v2-button-primary | ์‚ญ์ œ | +| `comp_btn_work_order` | v2-button-primary | ์ž‘์—…์ง€์‹œ ์ƒ์„ฑ | + +### 5.4 ๊ฐ„์†Œํ™” ๋ฒ„์ „ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ + +| ์ˆœ์„œ | ์ปฌ๋Ÿผ๋ช… | ํ‘œ์‹œ๋ช… | ์ •๋ ฌ | ํ˜•์‹ | ๋„ˆ๋น„ | +|:----:|--------|--------|:----:|------|:----:| +| 1 | plan_no | ๊ณ„ํš๋ฒˆํ˜ธ | left | text | 120 | +| 2 | plan_date | ๊ณ„ํš์ผ์ž | center | date | 100 | +| 3 | item_code | ํ’ˆ๋ชฉ์ฝ”๋“œ | left | text | 100 | +| 4 | item_name | ํ’ˆ๋ช… | left | text | 150 | +| 5 | plan_qty | ๊ณ„ํš์ˆ˜๋Ÿ‰ | right | number | 100 | +| 6 | start_date | ์‹œ์ž‘์ผ | center | date | 100 | +| 7 | end_date | ์ข…๋ฃŒ์ผ | center | date | 100 | +| 8 | due_date | ๋‚ฉ๊ธฐ์ผ | center | date | 100 | +| 9 | equipment_name | ์„ค๋น„ | left | text | 120 | +| 10 | status | ์ƒํƒœ | center | badge | 80 | +| 11 | manager_name | ๋‹ด๋‹น์ž | left | text | 100 | +| 12 | product_type | ์ œํ’ˆ์œ ํ˜• | center | text | 80 | + +### 5.5 ๊ฐ„์†Œํ™” ๋ฒ„์ „ ๋ชจ๋‹ฌ ํ•„๋“œ + +| ํ•„๋“œ๋ช… | ๋ผ๋ฒจ | ํƒ€์ž… | ํ•„์ˆ˜ | ๋น„๊ณ  | +|--------|------|------|:----:|------| +| plan_no | ๊ณ„ํš๋ฒˆํ˜ธ | text (readonly) | โœ… | ์ž๋™์ฑ„๋ฒˆ: PP-YYYYMMDD-0001 | +| plan_date | ๊ณ„ํš์ผ์ž | date | โœ… | ๊ธฐ๋ณธ๊ฐ’: ์˜ค๋Š˜ | +| item_code | ํ’ˆ๋ชฉ์ฝ”๋“œ | select (ํ’ˆ๋ชฉ ๊ฒ€์ƒ‰) | โœ… | ์—”ํ‹ฐํ‹ฐ: item_info | +| item_name | ํ’ˆ๋ช… | text (readonly) | | ์ž๋™ ์ž…๋ ฅ | +| plan_qty | ๊ณ„ํš์ˆ˜๋Ÿ‰ | number | โœ… | min: 1 | +| start_date | ์‹œ์ž‘์ผ | date | โœ… | | +| end_date | ์ข…๋ฃŒ์ผ | date | โœ… | start_date ์ดํ›„ | +| due_date | ๋‚ฉ๊ธฐ์ผ | date | | | +| equipment_id | ์„ค๋น„ | select | | ์—”ํ‹ฐํ‹ฐ: equipment_info | +| equipment_name | ์„ค๋น„๋ช… | text (readonly) | | ์ž๋™ ์ž…๋ ฅ | +| status | ์ƒํƒœ | select | โœ… | ๊ณตํ†ต์ฝ”๋“œ: PROD_PLAN_STATUS | +| priority | ์šฐ์„ ์ˆœ์œ„ | select | | ๊ณตํ†ต์ฝ”๋“œ: PRIORITY | +| work_shift | ์ž‘์—…์กฐ | select | | ๊ณตํ†ต์ฝ”๋“œ: WORK_SHIFT | +| manager_name | ๋‹ด๋‹น์ž | text | | | +| product_type | ์ œํ’ˆ์œ ํ˜• | select | | ๊ณตํ†ต์ฝ”๋“œ: PRODUCT_TYPE | +| remarks | ๋น„๊ณ  | textarea | | | + +### 5.6 ์ƒํƒœ ๋ฐฐ์ง€ ๋ฐ ๊ณตํ†ต์ฝ”๋“œ + +#### ์ƒํƒœ (PROD_PLAN_STATUS) + +| ์ฝ”๋“œ | ํ‘œ์‹œ๋ช… | ๋ฐฐ๊ฒฝ์ƒ‰ | ๊ธ€์ž์ƒ‰ | +|------|--------|--------|--------| +| ๊ณ„ํš | ๊ณ„ํš | #dbeafe | #1e40af | +| ์ง€์‹œ | ์ง€์‹œ | #fef3c7 | #92400e | +| ์ง„ํ–‰ | ์ง„ํ–‰ | #d1fae5 | #065f46 | +| ์™„๋ฃŒ | ์™„๋ฃŒ | #f3f4f6 | #4b5563 | + +#### ์šฐ์„ ์ˆœ์œ„ (PRIORITY) + +| ์ฝ”๋“œ | ํ‘œ์‹œ๋ช… | +|------|--------| +| ๊ธด๊ธ‰ | ๊ธด๊ธ‰ | +| ๋†’์Œ | ๋†’์Œ | +| ๋ณดํ†ต | ๋ณดํ†ต | +| ๋‚ฎ์Œ | ๋‚ฎ์Œ | + +#### ์ž‘์—…์กฐ (WORK_SHIFT) + +| ์ฝ”๋“œ | ํ‘œ์‹œ๋ช… | +|------|--------| +| ์ฃผ๊ฐ„ | ์ฃผ๊ฐ„ | +| ์•ผ๊ฐ„ | ์•ผ๊ฐ„ | +| ์ฃผ์•ผ | ์ฃผ์•ผ | + +#### ์ œํ’ˆ์œ ํ˜• (PRODUCT_TYPE) + +| ์ฝ”๋“œ | ํ‘œ์‹œ๋ช… | +|------|--------| +| ์™„์ œํ’ˆ | ์™„์ œํ’ˆ | +| ๋ฐ˜์ œํ’ˆ | ๋ฐ˜์ œํ’ˆ | + +--- + +## 6. ์›๋ณธ HTML ๊ธฐ๋Šฅ ์ƒ์„ธ ๋ถ„์„ + +### 6.1 ์ˆ˜์ฃผ๋ฐ์ดํ„ฐ ํƒญ (์™ผ์ชฝ ํŒจ๋„) + +**ํ…Œ์ด๋ธ” ๊ตฌ์กฐ**: +- ํ’ˆ๋ชฉ๋ณ„ ๊ทธ๋ฃน ํ–‰ (์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ ๊ฐ€๋Šฅ) +- ์ˆ˜์ฃผ ์ƒ์„ธ ํ–‰ (๊ทธ๋ฃน ํ–‰ ํ•˜์œ„) + +**ํ’ˆ๋ชฉ ๊ทธ๋ฃน ํ–‰ ์ปฌ๋Ÿผ**: +| ์ปฌ๋Ÿผ | ์„ค๋ช… | +|------|------| +| ์ฒดํฌ๋ฐ•์Šค | ํ’ˆ๋ชฉ ๊ทธ๋ฃน ์„ ํƒ | +| ํ† ๊ธ€ | ์ƒ์„ธ ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ | +| ํ’ˆ๋ชฉ์ฝ”๋“œ | | +| ํ’ˆ๋ชฉ๋ช… | | +| ์ด์ˆ˜์ฃผ๋Ÿ‰ | ํ•ด๋‹น ํ’ˆ๋ชฉ์˜ ๋ชจ๋“  ์ˆ˜์ฃผ ํ•ฉ๊ณ„ | +| ์ถœ๊ณ ๋Ÿ‰ | | +| ์ž”๋Ÿ‰ | ์ด์ˆ˜์ฃผ๋Ÿ‰ - ์ถœ๊ณ ๋Ÿ‰ | +| ํ˜„์žฌ๊ณ  | | +| ์•ˆ์ „์žฌ๊ณ  | | +| ์ถœํ•˜๊ณ„ํš๋Ÿ‰ | | +| ๊ธฐ์ƒ์‚ฐ๊ณ„ํš๋Ÿ‰ | ์ด๋ฏธ ๋“ฑ๋ก๋œ ์ƒ์‚ฐ๊ณ„ํš ์ˆ˜๋Ÿ‰ | +| ์ƒ์‚ฐ์ง„ํ–‰ | ํ˜„์žฌ ์ƒ์‚ฐ ์ค‘์ธ ์ˆ˜๋Ÿ‰ | +| ํ•„์š”์ƒ์‚ฐ๊ณ„ํš | ์ถ”๊ฐ€๋กœ ๊ณ„ํšํ•ด์•ผ ํ•  ์ˆ˜๋Ÿ‰ (๋นจ๊ฐ„์ƒ‰ ๊ฐ•์กฐ) | + +**์ˆ˜์ฃผ ์ƒ์„ธ ํ–‰**: +- ์ˆ˜์ฃผ๋ฒˆํ˜ธ, ๊ฑฐ๋ž˜์ฒ˜, ์ƒํƒœ ๋ฐฐ์ง€ +- ์ˆ˜์ฃผ๋Ÿ‰, ์ถœ๊ณ ๋Ÿ‰, ์ž”๋Ÿ‰ +- ๋‚ฉ๊ธฐ์ผ + +**๋ฒ„ํŠผ**: +- `๊ณ„ํš์— ์—†๋Š” ํ’ˆ๋ชฉ๋งŒ` ์ฒดํฌ๋ฐ•์Šค ํ•„ํ„ฐ +- `์„ ํƒ ํ’ˆ๋ชฉ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ`: ์„ ํƒํ•œ ํ’ˆ๋ชฉ์„ ์ƒ์‚ฐ๊ณ„ํš์œผ๋กœ ๋“ฑ๋ก +- `์ƒˆ๋กœ๊ณ ์นจ` + +### 6.2 ์•ˆ์ „์žฌ๊ณ  ๋ถ€์กฑ๋ถ„ ํƒญ (์™ผ์ชฝ ํŒจ๋„) + +**ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ**: +| ์ปฌ๋Ÿผ | ์„ค๋ช… | +|------|------| +| ์ฒดํฌ๋ฐ•์Šค | | +| ํ’ˆ๋ชฉ์ฝ”๋“œ | | +| ํ’ˆ๋ชฉ๋ช… | | +| ํ˜„์žฌ๊ณ  | | +| ์•ˆ์ „์žฌ๊ณ  | | +| ๋ถ€์กฑ์ˆ˜๋Ÿ‰ | ๋นจ๊ฐ„์ƒ‰ (๋งˆ์ด๋„ˆ์Šค) | +| ๊ถŒ์žฅ์ƒ์‚ฐ๋Ÿ‰ | ๋…น์ƒ‰ | +| ์ตœ์ข…์ž…๊ณ ์ผ | | + +### 6.3 ์™„์ œํ’ˆ ์ƒ์‚ฐ๊ณ„ํš ํƒญ (์˜ค๋ฅธ์ชฝ ํŒจ๋„) + +**์Šค์ผ€์ค„ ์˜ต์…˜**: +- ์•ˆ์ „๋ฆฌ๋“œํƒ€์ž„ (์ผ) +- ํ‘œ์‹œ ๊ธฐ๊ฐ„ (์ฃผ) +- ๋ฏธ์ง„ํ–‰ ๊ณ„ํš ์žฌ๊ณ„์‚ฐ ์ฒดํฌ๋ฐ•์Šค + +**๋ฒ”๋ก€**: +- ๊ณ„ํš (ํŒŒ๋ž€์ƒ‰) +- ์ง€์‹œ (์ฃผํ™ฉ์ƒ‰) +- ์ง„ํ–‰ (๋…น์ƒ‰) +- ์™„๋ฃŒ (ํšŒ์ƒ‰) +- ๋‚ฉ๊ธฐ์ผ (๋นจ๊ฐ„ ํ…Œ๋‘๋ฆฌ) +- ๊ธด๊ธ‰ (๋นจ๊ฐ„ ์•„์ด์ฝ˜) + +**ํƒ€์ž„๋ผ์ธ ๊ตฌ์กฐ**: +- ๋‚ ์งœ๋ณ„ ํ—ค๋” (์ผ/์›” ํ‘œ์‹œ, ์ฃผ๋ง ๊ฐ•์กฐ, ์˜ค๋Š˜ ๊ฐ•์กฐ) +- ํ’ˆ๋ชฉ๋ณ„ ํ–‰ +- ์ƒ์‚ฐ๊ณ„ํš ๋ฐ” (๋“œ๋ž˜๊ทธ๋กœ ์ด๋™ ๊ฐ€๋Šฅ) +- ํด๋ฆญ ์‹œ ์ƒ์„ธ ๋ชจ๋‹ฌ ์˜คํ”ˆ + +**๋ฒ„ํŠผ**: +- `์ƒˆ๋กœ๊ณ ์นจ` +- `์ž๋™ ์Šค์ผ€์ค„ ์ƒ์„ฑ`: ์„ ํƒ๋œ ํ’ˆ๋ชฉ์— ๋Œ€ํ•ด ์ž๋™์œผ๋กœ ์ƒ์‚ฐ๊ณ„ํš ์ƒ์„ฑ +- `์„ ํƒ ๊ณ„ํš ๋ณ‘ํ•ฉ`: ๊ฐ™์€ ํ’ˆ๋ชฉ์˜ ๊ณ„ํš์„ ํ•˜๋‚˜๋กœ ๋ณ‘ํ•ฉ +- `์„ ํƒ ํ’ˆ๋ชฉ โ†’ ๋ฐ˜์ œํ’ˆ ๊ณ„ํš`: BOM ๊ธฐ๋ฐ˜ ๋ฐ˜์ œํ’ˆ ๊ณ„ํš ์ƒ์„ฑ +- `์ €์žฅ` +- `์ดˆ๊ธฐํ™”` + +### 6.4 ๋ฐ˜์ œํ’ˆ ์ƒ์‚ฐ๊ณ„ํš ํƒญ (์˜ค๋ฅธ์ชฝ ํŒจ๋„) + +**์˜ต์…˜**: +- ํ˜„์žฌ๊ณ  ๋ฐ ์•ˆ์ „์žฌ๊ณ  ๊ฐ์•ˆ +- ์ง„ํ–‰์ค‘์ธ ๊ณ„ํš ์œ ์ง€ํ•˜๊ณ  ์žฌ๊ณ„์‚ฐ +- ํˆฌ์ž… ์™„๋ฃŒ๋œ ๋ฐ˜์ œํ’ˆ ์ œ์™ธ + +**์•ˆ๋‚ด**: +- ์™„์ œํ’ˆ ์ƒ์‚ฐ๊ณ„ํš ๊ธฐ์ค€์œผ๋กœ ํ•„์š”ํ•œ ๋ฐ˜์ œํ’ˆ ๊ณ„ํš ์ž๋™ ์ƒ์„ฑ +- ๋ชจํ’ˆ๋ชฉ ์ƒ์‚ฐ ์‹œ์ž‘์ผ ๊ณ ๋ คํ•˜์—ฌ ๋‚ฉ๊ธฐ์ผ ์„ค์ • +- BOM(์ž์žฌ๋ช…์„ธ์„œ) ์ •๋ณด ๊ธฐ๋ฐ˜ ํ•„์š” ์ˆ˜๋Ÿ‰ ๊ณ„์‚ฐ + +### 6.5 ์ƒ์‚ฐ ์Šค์ผ€์ค„ ์ƒ์„ธ ๋ชจ๋‹ฌ + +**๊ธฐ๋ณธ ์ •๋ณด**: +- ํ’ˆ๋ชฉ์ฝ”๋“œ (์ฝ๊ธฐ์ „์šฉ) +- ํ’ˆ๋ชฉ๋ช… (์ฝ๊ธฐ์ „์šฉ) + +**๊ทผ๊ฑฐ ์ •๋ณด**: +- ์ˆ˜์ฃผ๋ฒˆํ˜ธ, ์•ˆ์ „์žฌ๊ณ , ์žฌ๊ณ ๋ถ€์กฑ ๋“ฑ ํ‘œ์‹œ + +**์ƒ์‚ฐ ์ •๋ณด**: +- ์ด ์ƒ์‚ฐ์ˆ˜๋Ÿ‰ +- ๋‚ฉ๊ธฐ์ผ (์ฝ๊ธฐ์ „์šฉ) +- ๊ณ„ํš ์‹œ์ž‘์ผ (์ˆ˜์ • ๊ฐ€๋Šฅ) +- ๊ณ„ํš ์ข…๋ฃŒ์ผ (์ˆ˜์ • ๊ฐ€๋Šฅ) +- ์ƒ์‚ฐ ๊ธฐ๊ฐ„ (์ž๋™ ๊ณ„์‚ฐ) + +**๊ณผ๊ฑฐ ๊ณ„ํš ๊ฒฝ๊ณ **: +- ์‹œ์ž‘์ผ์ด ๊ณผ๊ฑฐ์ธ ๊ฒฝ์šฐ ๊ฒฝ๊ณ  ํ‘œ์‹œ +- `์˜ค๋Š˜๋ถ€ํ„ฐ ์žฌ์กฐ์ •` ๋ฒ„ํŠผ +- `์ž‘์—…์ง€์‹œ ์ฆ‰์‹œ ์ƒ์„ฑ` ๋ฒ„ํŠผ + +**๊ณ„ํš ๋ถ„ํ• **: +- ๋ถ„ํ•  ๊ฐœ์ˆ˜ ์„ ํƒ (2~4๊ฐœ) +- ๊ฐ ๋ถ„ํ•  ์ˆ˜๋Ÿ‰ ์ž…๋ ฅ +- ๋ถ„ํ•  ์‹คํ–‰ + +**์„ค๋น„ ํ• ๋‹น**: +- ์„ค๋น„ ์„ ํƒ ๋ฒ„ํŠผ +- ์„ ํƒ๋œ ์„ค๋น„ ๋ชฉ๋ก + +**์ƒ์‚ฐ ์ƒํƒœ**: +- ์ƒํƒœ (์ž๋™ ๊ด€๋ฆฌ): ๊ณ„ํš๋จ/์ž‘์—…์ง€์‹œ/์ง„ํ–‰์ค‘/์™„๋ฃŒ + +**์ถ”๊ฐ€ ์ •๋ณด**: +- ๋‹ด๋‹น์ž +- ์ž‘์—…์ง€์‹œ๋ฒˆํ˜ธ +- ๋น„๊ณ  + +**๋ฒ„ํŠผ**: +- ์‚ญ์ œ +- ์ทจ์†Œ +- ์ €์žฅ + +--- + +## 7. ๊ตฌํ˜„ ์šฐ์„ ์ˆœ์œ„ + +### Phase 1: ๊ฐ„์†Œํ™” ๋ฒ„์ „ (ํ˜„์žฌ ๊ตฌํ˜„ ๊ฐ€๋Šฅ) + +V2 ์ปดํฌ๋„ŒํŠธ๋กœ ๊ธฐ๋ณธ CRUD ํ™”๋ฉด ๊ตฌํ˜„: +- ๊ฒ€์ƒ‰ ์œ„์ ฏ +- ๋‹จ์ผ ํ…Œ์ด๋ธ” (๊ทธ๋ฃนํ™” ์—†์Œ) +- ๊ธฐ๋ณธ ๋ชจ๋‹ฌ ํผ +- ์ƒํƒœ ๋ฐฐ์ง€ + +### Phase 2: ์ปดํฌ๋„ŒํŠธ ํ™•์žฅ ๋ฐ ๊ฐœ๋ฐœ + +1. ~~**`v2-split-panel-layout` ํ™•์žฅ**~~ โœ… **์™„๋ฃŒ** + - โœ… `displayMode: "custom"` ์ถ”๊ฐ€ + - โœ… `components[]` ํ•„๋“œ ์ง€์› (ํƒญ ์ปดํฌ๋„ŒํŠธ์™€ ๋™์ผํ•œ ๊ตฌ์กฐ) + - โœ… ๋””์ž์ธ ๋ชจ๋“œ์—์„œ ์ปดํฌ๋„ŒํŠธ ๋“œ๋ž˜๊ทธ&๋“œ๋กญ ์ง€์› + - โœ… ์ปดํฌ๋„ŒํŠธ ํด๋ฆญ ์‹œ ์ขŒ์ธก ์„ค์ • ํŒจ๋„์—์„œ ์†์„ฑ ํŽธ์ง‘ + +2. ~~**`v2-table-grouped` ์‹ ๊ทœ ๊ฐœ๋ฐœ**~~ โœ… **์™„๋ฃŒ** + - โœ… ํ’ˆ๋ชฉ๋ณ„ ๊ทธ๋ฃนํ•‘ (์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ) + - โœ… ๊ทธ๋ฃน ํ–‰๊ณผ ์ƒ์„ธ ํ–‰ ๊ตฌ๋ถ„ + - โœ… ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ (๊ทธ๋ฃน ๋‹จ์œ„ / ๊ฐœ๋ณ„ ๋‹จ์œ„) + - โœ… ๊ทธ๋ฃน ์š”์•ฝ (๊ฐœ์ˆ˜, ํ•ฉ๊ณ„, ํ‰๊ท  ๋“ฑ) + +### Phase 3: ํƒ€์ž„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ + +1. **`v2-timeline-scheduler` ์‹ ๊ทœ ๊ฐœ๋ฐœ** + - ๊ฐ„ํŠธ์ฐจํŠธ ํ˜•ํƒœ ํƒ€์ž„๋ผ์ธ + - ๋‚ ์งœ ๊ทธ๋ฆฌ๋“œ (์ผ/์ฃผ/์›” ๋‹จ์œ„) + - ์ƒ์‚ฐ๊ณ„ํš ๋ฐ” ๋ Œ๋”๋ง + +2. **๋“œ๋ž˜๊ทธ&๋“œ๋กญ ๊ธฐ๋Šฅ** + - ๊ณ„ํš ๋ฐ” ๋“œ๋ž˜๊ทธ๋กœ ๋‚ ์งœ ์ด๋™ + - ๊ณ„ํš ๋ฐ” ํฌ๊ธฐ ์กฐ์ • (๊ธฐ๊ฐ„ ๋ณ€๊ฒฝ) + +3. **๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง** + - ์ž๋™ ์Šค์ผ€์ค„ ์ƒ์„ฑ API + - BOM ๊ธฐ๋ฐ˜ ๋ฐ˜์ œํ’ˆ ๊ณ„ํš ์—ฐ๋™ + - ์„ค๋น„๋ณ„ ์šฉ๋Ÿ‰ ์ฒดํฌ + +--- + +## 8. ์ฐธ๊ณ  ์‚ฌํ•ญ + +### 8.1 ์ƒํƒœ ๋ฐฐ์ง€ ์Šคํƒ€์ผ + +| ์ƒํƒœ | ๋ฐฐ๊ฒฝ์ƒ‰ | ๊ธ€์ž์ƒ‰ | ์„ค๋ช… | +|------|--------|--------|------| +| ๊ณ„ํš | #dbeafe | #1e40af | ํŒŒ๋ž€์ƒ‰ | +| ์ง€์‹œ | #fef3c7 | #92400e | ์ฃผํ™ฉ์ƒ‰ | +| ์ง„ํ–‰ | #d1fae5 | #065f46 | ๋…น์ƒ‰ | +| ์™„๋ฃŒ | #f3f4f6 | #4b5563 | ํšŒ์ƒ‰ | +| ๊ธด๊ธ‰ | #fee2e2 | #991b1b | ๋นจ๊ฐ„์ƒ‰ | + +### 8.2 ์ž๋™ ์Šค์ผ€์ค„ ์ƒ์„ฑ ๋กœ์ง + +``` +1. ์„ ํƒ๋œ ํ’ˆ๋ชฉ์˜ ํ•„์š” ์ƒ์‚ฐ์ˆ˜๋Ÿ‰ ๊ณ„์‚ฐ + - ํ•„์š”์ˆ˜๋Ÿ‰ = ์ž”๋Ÿ‰ + ์•ˆ์ „์žฌ๊ณ  - ํ˜„์žฌ๊ณ  - ๊ธฐ์ƒ์‚ฐ๊ณ„ํš๋Ÿ‰ + +2. ๋‚ฉ๊ธฐ์ผ์—์„œ ์•ˆ์ „๋ฆฌ๋“œํƒ€์ž„ ์ฐจ๊ฐํ•˜์—ฌ ์™„๋ฃŒ์ผ ๊ณ„์‚ฐ + +3. ์ผ์ผ ์ƒ์‚ฐ๋Šฅ๋ ฅ์œผ๋กœ ํ•„์š” ์ƒ์‚ฐ์ผ์ˆ˜ ๊ณ„์‚ฐ + +4. ์™„๋ฃŒ์ผ์—์„œ ์—ญ์‚ฐํ•˜์—ฌ ์‹œ์ž‘์ผ ๊ณ„์‚ฐ + +5. ์„ค๋น„ ๊ฐ€์šฉ์„ฑ ํ™•์ธ ๋ฐ ์ž๋™ ํ• ๋‹น + +6. ๋ฐ˜์ œํ’ˆ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ BOM ๊ธฐ๋ฐ˜ ๋ฐ˜์ œํ’ˆ ๊ณ„ํš ์ƒ์„ฑ +``` + +### 8.3 ๊ณ„ํš ๋ถ„ํ•  ๋กœ์ง + +``` +1. ์›๋ณธ ๊ณ„ํš์˜ ์ด ์ˆ˜๋Ÿ‰ ํ™•์ธ + +2. ๋ถ„ํ•  ๊ฐœ์ˆ˜ ์„ ํƒ (2~4๊ฐœ) + +3. ๊ฐ ๋ถ„ํ•  ์ˆ˜๋Ÿ‰ ์ž…๋ ฅ (ํ•ฉ๊ณ„ = ์›๋ณธ ์ˆ˜๋Ÿ‰) + +4. ๋ถ„ํ•  ์‹คํ–‰ ์‹œ: + - ์›๋ณธ ๊ณ„ํš ์‚ญ์ œ + - ์ƒˆ๋กœ์šด N๊ฐœ์˜ ๊ณ„ํš ์ƒ์„ฑ + - ๊ฐ๊ฐ ๋ณ„๋„์˜ ์‹œ์ž‘์ผ/์ข…๋ฃŒ์ผ ์„ค์ • ๊ฐ€๋Šฅ +``` + +--- + +## 9. DB INSERT JSON (๊ฐ„์†Œํ™” ๋ฒ„์ „) + +> โš ๏ธ ์ด JSON์€ **๊ฐ„์†Œํ™” ๋ฒ„์ „**์ž…๋‹ˆ๋‹ค. ์ „์ฒด ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์‹œ ๋ณ„๋„ ๊ฐœ๋ฐœ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. + +### 9.1 screen_definitions + +```json +{ + "screen_name": "์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ", + "screen_code": "{COMPANY_CODE}_PP_MAIN", + "table_name": "production_plan_mng", + "company_code": "{COMPANY_CODE}", + "description": "์ƒ์‚ฐ๊ณ„ํš ๊ด€๋ฆฌ ํ™”๋ฉด (๊ฐ„์†Œํ™” ๋ฒ„์ „)", + "is_active": "Y", + "db_source_type": "internal", + "data_source_type": "database" +} +``` + +### 9.2 screen_layouts_v2.layout_data (๊ฐ„์†Œํ™” ๋ฒ„์ „) + +```json +{ + "version": "2.0", + "components": [ + { + "id": "comp_search", + "url": "@/lib/registry/components/v2-table-search-widget", + "size": { "width": 1920, "height": 80 }, + "position": { "x": 0, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-table-search-widget", + "label": "๊ฒ€์ƒ‰ ํ•„ํ„ฐ", + "searchFields": [ + { "field": "item_code", "label": "ํ’ˆ๋ชฉ์ฝ”๋“œ", "type": "text" }, + { "field": "item_name", "label": "ํ’ˆ๋ช…", "type": "text" }, + { "field": "plan_date", "label": "๊ณ„ํš๊ธฐ๊ฐ„", "type": "daterange" }, + { + "field": "status", + "label": "์ƒํƒœ", + "type": "select", + "options": [ + { "value": "", "label": "์ „์ฒด" }, + { "value": "๊ณ„ํš", "label": "๊ณ„ํš" }, + { "value": "์ง€์‹œ", "label": "์ง€์‹œ" }, + { "value": "์ง„ํ–‰", "label": "์ง„ํ–‰" }, + { "value": "์™„๋ฃŒ", "label": "์™„๋ฃŒ" } + ] + } + ], + "targetTableId": "comp_table" + }, + "displayOrder": 0 + }, + { + "id": "comp_table", + "url": "@/lib/registry/components/v2-table-list", + "size": { "width": 1920, "height": 700 }, + "position": { "x": 0, "y": 120, "z": 1 }, + "overrides": { + "type": "v2-table-list", + "label": "์ƒ์‚ฐ๊ณ„ํš ๋ชฉ๋ก", + "columns": [ + { "columnName": "plan_no", "displayName": "๊ณ„ํš๋ฒˆํ˜ธ", "order": 0, "visible": true, "sortable": true, "format": "text", "align": "left", "width": 120 }, + { "columnName": "plan_date", "displayName": "๊ณ„ํš์ผ์ž", "order": 1, "visible": true, "sortable": true, "format": "date", "align": "center", "width": 100 }, + { "columnName": "item_code", "displayName": "ํ’ˆ๋ชฉ์ฝ”๋“œ", "order": 2, "visible": true, "sortable": true, "format": "text", "align": "left", "width": 100 }, + { "columnName": "item_name", "displayName": "ํ’ˆ๋ช…", "order": 3, "visible": true, "sortable": true, "format": "text", "align": "left", "width": 150 }, + { "columnName": "plan_qty", "displayName": "๊ณ„ํš์ˆ˜๋Ÿ‰", "order": 4, "visible": true, "sortable": true, "format": "number", "align": "right", "width": 100 }, + { "columnName": "start_date", "displayName": "์‹œ์ž‘์ผ", "order": 5, "visible": true, "sortable": true, "format": "date", "align": "center", "width": 100 }, + { "columnName": "end_date", "displayName": "์ข…๋ฃŒ์ผ", "order": 6, "visible": true, "sortable": true, "format": "date", "align": "center", "width": 100 }, + { "columnName": "due_date", "displayName": "๋‚ฉ๊ธฐ์ผ", "order": 7, "visible": true, "sortable": true, "format": "date", "align": "center", "width": 100 }, + { "columnName": "equipment_name", "displayName": "์„ค๋น„", "order": 8, "visible": true, "sortable": true, "format": "text", "align": "left", "width": 120 }, + { + "columnName": "status", + "displayName": "์ƒํƒœ", + "order": 9, + "visible": true, + "sortable": true, + "format": "badge", + "align": "center", + "width": 80, + "badgeConfig": { + "๊ณ„ํš": { "background": "#dbeafe", "color": "#1e40af" }, + "์ง€์‹œ": { "background": "#fef3c7", "color": "#92400e" }, + "์ง„ํ–‰": { "background": "#d1fae5", "color": "#065f46" }, + "์™„๋ฃŒ": { "background": "#f3f4f6", "color": "#4b5563" } + } + }, + { "columnName": "product_type", "displayName": "์ œํ’ˆ์œ ํ˜•", "order": 10, "visible": true, "sortable": true, "format": "text", "align": "center", "width": 80 }, + { "columnName": "manager_name", "displayName": "๋‹ด๋‹น์ž", "order": 11, "visible": true, "sortable": true, "format": "text", "align": "left", "width": 100 } + ], + "selectedTable": "production_plan_mng", + "pagination": { "enabled": true, "pageSize": 20, "pageSizeOptions": [10, 20, 50, 100] }, + "checkbox": { "enabled": true, "multiple": true }, + "horizontalScroll": { "enabled": true, "minColumnWidth": 80, "maxColumnWidth": 200 }, + "defaultSort": { "field": "plan_date", "order": "desc" } + }, + "displayOrder": 1 + }, + { + "id": "comp_btn_register", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 100, "height": 40 }, + "position": { "x": 1480, "y": 70, "z": 1 }, + "overrides": { + "text": "์‹ ๊ทœ ๋“ฑ๋ก", + "type": "v2-button-primary", + "action": { "type": "modal", "modalTitle": "์ƒ์‚ฐ๊ณ„ํš ๋“ฑ๋ก", "targetScreenId": null }, + "variant": "success" + }, + "displayOrder": 2 + }, + { + "id": "comp_btn_edit", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1590, "y": 70, "z": 1 }, + "overrides": { + "text": "์ˆ˜์ •", + "type": "v2-button-primary", + "action": { "type": "edit", "modalTitle": "์ƒ์‚ฐ๊ณ„ํš ์ˆ˜์ •", "targetScreenId": null }, + "variant": "secondary", + "requireSelection": true + }, + "displayOrder": 3 + }, + { + "id": "comp_btn_delete", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1680, "y": 70, "z": 1 }, + "overrides": { + "text": "์‚ญ์ œ", + "type": "v2-button-primary", + "action": { "type": "delete" }, + "variant": "danger", + "requireSelection": true, + "enableDataflowControl": true, + "dataflowDiagramId": null + }, + "displayOrder": 4 + }, + { + "id": "comp_btn_work_order", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 120, "height": 40 }, + "position": { "x": 1770, "y": 70, "z": 1 }, + "overrides": { + "text": "์ž‘์—…์ง€์‹œ ์ƒ์„ฑ", + "type": "v2-button-primary", + "action": { "type": "custom" }, + "variant": "primary", + "requireSelection": true, + "enableDataflowControl": true, + "dataflowDiagramId": null, + "confirmMessage": "์„ ํƒํ•œ ๊ณ„ํš์— ๋Œ€ํ•ด ์ž‘์—…์ง€์‹œ๋ฅผ ์ƒ์„ฑํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + }, + "displayOrder": 5 + } + ] +} +``` + +--- + +## 10. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์š”์ฒญ์„œ (๊ฐ„์†Œํ™” ๋ฒ„์ „) + +> **์ค‘์š”**: ์ด ์„น์…˜์€ ํ‘œ์ค€ ์–‘์‹์— ๋”ฐ๋ผ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + +``` +=== ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์š”์ฒญ์„œ === + +ใ€ํ™”๋ฉด ์ •๋ณดใ€‘ +- ํ™”๋ฉด๋ช…: ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ (๊ฐ„์†Œํ™” ๋ฒ„์ „) +- ํšŒ์‚ฌ์ฝ”๋“œ: {COMPANY_CODE} +- ๋ฉ”๋‰ดID: {TBD} + +ใ€ํ…Œ์ด๋ธ” ์ •๋ณดใ€‘ +- ๋ฉ”์ธ ํ…Œ์ด๋ธ”: production_plan_mng +- ๋””ํ…Œ์ผ ํ…Œ์ด๋ธ”: ์—†์Œ (๊ฐ„์†Œํ™” ๋ฒ„์ „) +- ๊ด€๊ณ„ FK: ์—†์Œ + +ใ€๋ฒ„ํŠผ ๋ชฉ๋กใ€‘ +๋ฒ„ํŠผ1: + - ๋ฒ„ํŠผ๋ช…: ์‹ ๊ทœ ๋“ฑ๋ก + - ๋™์ž‘ ์œ ํ˜•: ๋ชจ๋‹ฌ ์—ด๊ธฐ (insert) + - ์กฐ๊ฑด: ์—†์Œ + - ๋Œ€์ƒ ํ…Œ์ด๋ธ”: production_plan_mng + - ์ถ”๊ฐ€ ๋™์ž‘: + 1. plan_no ์ž๋™์ฑ„๋ฒˆ (PP-YYYYMMDD-0001) + 2. plan_date ๊ธฐ๋ณธ๊ฐ’ = ์˜ค๋Š˜ + 3. status ๊ธฐ๋ณธ๊ฐ’ = '๊ณ„ํš' + +๋ฒ„ํŠผ2: + - ๋ฒ„ํŠผ๋ช…: ์ˆ˜์ • + - ๋™์ž‘ ์œ ํ˜•: ๋ชจ๋‹ฌ ์—ด๊ธฐ (update) + - ์กฐ๊ฑด: ํ–‰ ์„ ํƒ ํ•„์ˆ˜ + - ๋Œ€์ƒ ํ…Œ์ด๋ธ”: production_plan_mng + - ์ถ”๊ฐ€ ๋™์ž‘: ์—†์Œ + +๋ฒ„ํŠผ3: + - ๋ฒ„ํŠผ๋ช…: ์‚ญ์ œ + - ๋™์ž‘ ์œ ํ˜•: ์‚ญ์ œ (delete) + - ์กฐ๊ฑด: + 1. ํ–‰ ์„ ํƒ ํ•„์ˆ˜ + 2. status != '์ง„ํ–‰' AND status != '์™„๋ฃŒ' + - ๋Œ€์ƒ ํ…Œ์ด๋ธ”: production_plan_mng + - ์ถ”๊ฐ€ ๋™์ž‘: ์‚ญ์ œ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ + +๋ฒ„ํŠผ4: + - ๋ฒ„ํŠผ๋ช…: ์ž‘์—…์ง€์‹œ ์ƒ์„ฑ + - ๋™์ž‘ ์œ ํ˜•: ์ˆ˜์ • (update) + - ์กฐ๊ฑด: + 1. ํ–‰ ์„ ํƒ ํ•„์ˆ˜ (๋‹ค์ค‘ ์„ ํƒ ๊ฐ€๋Šฅ) + 2. status = '๊ณ„ํš' + - ๋Œ€์ƒ ํ…Œ์ด๋ธ”: production_plan_mng + - ์ถ”๊ฐ€ ๋™์ž‘: + 1. status๋ฅผ '์ง€์‹œ'๋กœ ๋ณ€๊ฒฝ + 2. work_order_no ์ž๋™์ฑ„๋ฒˆ (WO-YYYYMMDD-0001) + 3. work_order_mng์— INSERT (์ž‘์—…์ง€์‹œ ํ…Œ์ด๋ธ”) + +ใ€์ถ”๊ฐ€ ์š”๊ตฌ์‚ฌํ•ญใ€‘ +- ์ง„ํ–‰/์™„๋ฃŒ ์ƒํƒœ์˜ ๊ณ„ํš์€ ์ˆ˜์ •/์‚ญ์ œ ๋ถˆ๊ฐ€ +- ๊ณ„ํš๋ฒˆํ˜ธ(plan_no) ์ž๋™์ฑ„๋ฒˆ: PP-YYYYMMDD-0001 +- ํ’ˆ๋ชฉ์ฝ”๋“œ ์„ ํƒ ์‹œ ํ’ˆ๋ช… ์ž๋™ ์ž…๋ ฅ +- ์„ค๋น„ ์„ ํƒ ์‹œ ์‹œ๊ฐ„๋‹น ์ƒ์‚ฐ๋Šฅ๋ ฅ ์ž๋™ ์ž…๋ ฅ +- ์‹œ์ž‘์ผ/์ข…๋ฃŒ์ผ ๊ธฐ๋ฐ˜ ์ƒ์‚ฐ๊ธฐ๊ฐ„ ์ž๋™ ๊ณ„์‚ฐ +``` + +--- + +## 11. ์ œ์–ด๊ด€๋ฆฌ ์„ค์ • (dataflow_diagrams) + +### 11.1 ์‹ ๊ทœ ๋“ฑ๋ก ๋ฒ„ํŠผ (๊ธฐ๋ณธ ์ €์žฅ) + +> ๊ธฐ๋ณธ INSERT ์•ก์…˜์€ ํ™”๋ฉด ๋””์ž์ด๋„ˆ์—์„œ ์ฒ˜๋ฆฌ๋˜๋ฏ€๋กœ ๋ณ„๋„ dataflow ๋ถˆํ•„์š” + +### 11.2 ์‚ญ์ œ ๋ฒ„ํŠผ (์กฐ๊ฑด๋ถ€ ์‚ญ์ œ) + +```json +{ + "diagram_name": "์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ_์‚ญ์ œ", + "company_code": "{COMPANY_CODE}", + "control": { + "actionType": "delete", + "triggerType": "before", + "conditions": [ + { + "id": "cond-status-not-in-progress", + "type": "condition", + "field": "status", + "operator": "!=", + "value": "์ง„ํ–‰", + "dataType": "string" + }, + { + "id": "cond-status-not-completed", + "type": "condition", + "field": "status", + "operator": "!=", + "value": "์™„๋ฃŒ", + "dataType": "string" + } + ], + "conditionLogic": "AND", + "errorMessage": "์ง„ํ–‰ ์ค‘์ด๊ฑฐ๋‚˜ ์™„๋ฃŒ๋œ ๊ณ„ํš์€ ์‚ญ์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." + }, + "plan": { + "actions": [ + { + "id": "action-delete", + "actionType": "delete", + "targetTable": "production_plan_mng" + } + ] + } +} +``` + +### 11.3 ์ž‘์—…์ง€์‹œ ์ƒ์„ฑ ๋ฒ„ํŠผ + +```json +{ + "diagram_name": "์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ_์ž‘์—…์ง€์‹œ์ƒ์„ฑ", + "company_code": "{COMPANY_CODE}", + "control": { + "actionType": "update", + "triggerType": "after", + "conditions": [ + { + "id": "cond-status-plan", + "type": "condition", + "field": "status", + "operator": "=", + "value": "๊ณ„ํš", + "dataType": "string" + } + ], + "errorMessage": "๊ณ„ํš ์ƒํƒœ์ธ ํ•ญ๋ชฉ๋งŒ ์ž‘์—…์ง€์‹œ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." + }, + "plan": { + "actions": [ + { + "id": "action-update-status", + "actionType": "update", + "targetTable": "production_plan_mng", + "fieldMappings": [ + { "targetField": "status", "defaultValue": "์ง€์‹œ" }, + { "targetField": "updated_date", "defaultValue": "#NOW" }, + { "targetField": "updated_by", "defaultValue": "#USER" } + ] + }, + { + "id": "action-insert-workorder", + "actionType": "insert", + "targetTable": "work_order_mng", + "fieldMappings": [ + { "sourceField": "plan_no", "targetField": "plan_no" }, + { "sourceField": "item_code", "targetField": "item_code" }, + { "sourceField": "item_name", "targetField": "item_name" }, + { "sourceField": "plan_qty", "targetField": "order_qty" }, + { "sourceField": "start_date", "targetField": "work_start_date" }, + { "sourceField": "end_date", "targetField": "work_end_date" }, + { "sourceField": "equipment_id", "targetField": "equipment_id" }, + { "sourceField": "equipment_name", "targetField": "equipment_name" }, + { "targetField": "status", "defaultValue": "๋Œ€๊ธฐ" }, + { "targetField": "company_code", "defaultValue": "#COMPANY" }, + { "targetField": "created_date", "defaultValue": "#NOW" }, + { "targetField": "created_by", "defaultValue": "#USER" } + ] + } + ] + } +} +``` + +--- + +## 12. ์ปดํฌ๋„ŒํŠธ ์—ฐ๋™ ์„ค์ • (๊ฐ„์†Œํ™” ๋ฒ„์ „) + +### 12.1 ์ด๋ฒคํŠธ ํ๋ฆ„ + +``` +[ํ™”๋ฉด ์ง„์ž…] + โ”‚ + โ–ผ +v2-table-search-widget (๊ฒ€์ƒ‰ ํ•„ํ„ฐ) + โ”‚ onFilterChange + โ–ผ +v2-table-list (์ƒ์‚ฐ๊ณ„ํš ๋ชฉ๋ก) + โ”‚ onRowSelect + โ–ผ +[๋ฒ„ํŠผ ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™”] + โ”‚ + โ”œโ”€โ”€ [์‹ ๊ทœ ๋“ฑ๋ก] ํด๋ฆญ โ†’ ๋“ฑ๋ก ๋ชจ๋‹ฌ ์˜คํ”ˆ + โ”‚ โ”‚ onSave + โ”‚ โ–ผ + โ”‚ INSERT โ†’ ํ…Œ์ด๋ธ” Refresh + โ”‚ + โ”œโ”€โ”€ [์ˆ˜์ •] ํด๋ฆญ โ†’ ์ˆ˜์ • ๋ชจ๋‹ฌ ์˜คํ”ˆ + โ”‚ โ”‚ onSave + โ”‚ โ–ผ + โ”‚ UPDATE โ†’ ํ…Œ์ด๋ธ” Refresh + โ”‚ + โ”œโ”€โ”€ [์‚ญ์ œ] ํด๋ฆญ โ†’ ์กฐ๊ฑด ์ฒดํฌ + โ”‚ โ”‚ ํ†ต๊ณผ ์‹œ + โ”‚ โ–ผ + โ”‚ DELETE โ†’ ํ…Œ์ด๋ธ” Refresh + โ”‚ + โ””โ”€โ”€ [์ž‘์—…์ง€์‹œ ์ƒ์„ฑ] ํด๋ฆญ โ†’ ์กฐ๊ฑด ์ฒดํฌ + โ”‚ ํ†ต๊ณผ ์‹œ + โ–ผ + UPDATE + INSERT โ†’ ํ…Œ์ด๋ธ” Refresh +``` + +### 12.2 ์—ฐ๋™ ์„ค์ • ํ…Œ์ด๋ธ” + +| ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ | ์ด๋ฒคํŠธ | ๋Œ€์ƒ ์ปดํฌ๋„ŒํŠธ | ๋™์ž‘ | +|---------------|--------|---------------|------| +| search-widget | onFilterChange | main-table | ํ•„ํ„ฐ ์ ์šฉ ํ›„ ์žฌ์กฐํšŒ | +| main-table | onRowSelect | btn-edit, btn-delete | ๋ฒ„ํŠผ ํ™œ์„ฑํ™” | +| main-table | onRowDoubleClick | modal-edit | ์ˆ˜์ • ๋ชจ๋‹ฌ ์˜คํ”ˆ | +| btn-register | onClick | modal-register | ๋“ฑ๋ก ๋ชจ๋‹ฌ ์˜คํ”ˆ | +| btn-edit | onClick | modal-edit | ์ˆ˜์ • ๋ชจ๋‹ฌ ์˜คํ”ˆ | +| btn-delete | onClick | dataflow | ์‚ญ์ œ ์‹คํ–‰ | +| btn-work-order | onClick | dataflow | ์ž‘์—…์ง€์‹œ ์ƒ์„ฑ ์‹คํ–‰ | +| modal-* | onSave | main-table | ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ | + +--- + +## 13. ์‚ฌ์šฉ์ž ์‚ฌ์šฉ ์˜ˆ์‹œ ์‹œ๋‚˜๋ฆฌ์˜ค (๊ฐ„์†Œํ™” ๋ฒ„์ „) + +### ์‹œ๋‚˜๋ฆฌ์˜ค 1: ์ƒ์‚ฐ๊ณ„ํš ์กฐํšŒ + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|:----:|-------------|-----------| +| 1 | ํ™”๋ฉด ์ง„์ž… | ์ „์ฒด ์ƒ์‚ฐ๊ณ„ํš ๋ชฉ๋ก ํ‘œ์‹œ (์ตœ๊ทผ์ˆœ) | +| 2 | ํ’ˆ๋ชฉ์ฝ”๋“œ ์ž…๋ ฅ ํ›„ [์กฐํšŒ] ํด๋ฆญ | ํ•ด๋‹น ํ’ˆ๋ชฉ์˜ ์ƒ์‚ฐ๊ณ„ํš๋งŒ ํ•„ํ„ฐ๋ง | +| 3 | ์ƒํƒœ ๋“œ๋กญ๋‹ค์šด์—์„œ '๊ณ„ํš' ์„ ํƒ | ๊ณ„ํš ์ƒํƒœ์ธ ํ•ญ๋ชฉ๋งŒ ํ‘œ์‹œ | +| 4 | ์ปฌ๋Ÿผ ํ—ค๋” ํด๋ฆญ | ํ•ด๋‹น ์ปฌ๋Ÿผ ๊ธฐ์ค€ ์ •๋ ฌ | + +### ์‹œ๋‚˜๋ฆฌ์˜ค 2: ์ƒ์‚ฐ๊ณ„ํš ๋“ฑ๋ก + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|:----:|-------------|-----------| +| 1 | [์‹ ๊ทœ ๋“ฑ๋ก] ๋ฒ„ํŠผ ํด๋ฆญ | ๋“ฑ๋ก ๋ชจ๋‹ฌ ์˜คํ”ˆ | +| 2 | ํ’ˆ๋ชฉ์ฝ”๋“œ ๊ฒ€์ƒ‰/์„ ํƒ | ํ’ˆ๋ช… ์ž๋™ ์ž…๋ ฅ | +| 3 | ๊ณ„ํš์ˆ˜๋Ÿ‰, ์‹œ์ž‘์ผ, ์ข…๋ฃŒ์ผ ์ž…๋ ฅ | ์ž…๋ ฅ ํ•„๋“œ ์ฑ„์›€ | +| 4 | ์„ค๋น„ ์„ ํƒ | ์‹œ๊ฐ„๋‹น ์ƒ์‚ฐ๋Šฅ๋ ฅ ํ‘œ์‹œ | +| 5 | [์ €์žฅ] ๋ฒ„ํŠผ ํด๋ฆญ | ์ €์žฅ ์™„๋ฃŒ, ๋ชฉ๋ก์— ์‹ ๊ทœ ํ–‰ ํ‘œ์‹œ | + +### ์‹œ๋‚˜๋ฆฌ์˜ค 3: ์ƒ์‚ฐ๊ณ„ํš ์ˆ˜์ • + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|:----:|-------------|-----------| +| 1 | ๋ชฉ๋ก์—์„œ ํ–‰ ์„ ํƒ | ํ–‰ ํ•˜์ด๋ผ์ดํŠธ | +| 2 | [์ˆ˜์ •] ๋ฒ„ํŠผ ํด๋ฆญ (๋˜๋Š” ๋”๋ธ”ํด๋ฆญ) | ์ˆ˜์ • ๋ชจ๋‹ฌ ์˜คํ”ˆ (๊ธฐ์กด ๊ฐ’ ๋กœ๋“œ) | +| 3 | ํ•„์š”ํ•œ ํ•„๋“œ ์ˆ˜์ • | ๊ฐ’ ๋ณ€๊ฒฝ | +| 4 | [์ €์žฅ] ๋ฒ„ํŠผ ํด๋ฆญ | ์ €์žฅ ์™„๋ฃŒ, ๋ชฉ๋ก ๊ฐฑ์‹  | + +### ์‹œ๋‚˜๋ฆฌ์˜ค 4: ์ƒ์‚ฐ๊ณ„ํš ์‚ญ์ œ + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|:----:|-------------|-----------| +| 1 | ์‚ญ์ œํ•  ํ–‰ ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ | ์„ ํƒ ํ‘œ์‹œ | +| 2 | [์‚ญ์ œ] ๋ฒ„ํŠผ ํด๋ฆญ | ์‚ญ์ œ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ | +| 3-a | (์ƒํƒœ๊ฐ€ ๊ณ„ํš/์ง€์‹œ) ํ™•์ธ ํด๋ฆญ | ์‚ญ์ œ ์™„๋ฃŒ, ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ | +| 3-b | (์ƒํƒœ๊ฐ€ ์ง„ํ–‰/์™„๋ฃŒ) ํ™•์ธ ํด๋ฆญ | "์‚ญ์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ | + +### ์‹œ๋‚˜๋ฆฌ์˜ค 5: ์ž‘์—…์ง€์‹œ ์ƒ์„ฑ + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|:----:|-------------|-----------| +| 1 | ๊ณ„ํš ์ƒํƒœ์ธ ํ–‰ ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ (๋‹ค์ค‘ ๊ฐ€๋Šฅ) | ์„ ํƒ ํ‘œ์‹œ | +| 2 | [์ž‘์—…์ง€์‹œ ์ƒ์„ฑ] ๋ฒ„ํŠผ ํด๋ฆญ | ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ | +| 3-a | (์ƒํƒœ๊ฐ€ ๊ณ„ํš) ํ™•์ธ ํด๋ฆญ | ์ƒํƒœ๊ฐ€ '์ง€์‹œ'๋กœ ๋ณ€๊ฒฝ, ์ž‘์—…์ง€์‹œ๋ฒˆํ˜ธ ์ƒ์„ฑ | +| 3-b | (์ƒํƒœ๊ฐ€ ๊ณ„ํš ์•„๋‹˜) ํ™•์ธ ํด๋ฆญ | "๊ณ„ํš ์ƒํƒœ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ | + +--- + +## 14. ๊ฒ€์ฆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### 14.1 ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ + +- [ ] ๋ฐ์ดํ„ฐ ์กฐํšŒ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ๊ฒ€์ƒ‰ ํ•„ํ„ฐ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์‹ ๊ทœ ๋“ฑ๋ก์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ˆ˜์ •์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์‚ญ์ œ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? (์กฐ๊ฑด ํฌํ•จ) +- [ ] ์ž‘์—…์ง€์‹œ ์ƒ์„ฑ์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? (์กฐ๊ฑด ํฌํ•จ) +- [ ] ํŽ˜์ด์ง€๋„ค์ด์…˜์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ •๋ ฌ์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? + +### 14.2 ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ + +- [ ] ๊ณ„ํš๋ฒˆํ˜ธ(plan_no) ์ž๋™์ฑ„๋ฒˆ์ด ์ค‘๋ณต ์—†์ด ์ƒ์„ฑ๋˜๋Š”๊ฐ€? +- [ ] ํ’ˆ๋ชฉ์ฝ”๋“œ ์„ ํƒ ์‹œ ํ’ˆ๋ช…์ด ์ž๋™ ์ž…๋ ฅ๋˜๋Š”๊ฐ€? +- [ ] ์„ค๋น„ ์„ ํƒ ์‹œ ์‹œ๊ฐ„๋‹น ์ƒ์‚ฐ๋Šฅ๋ ฅ์ด ํ‘œ์‹œ๋˜๋Š”๊ฐ€? +- [ ] company_code ํ•„ํ„ฐ๋ง์ด ์ ์šฉ๋˜๋Š”๊ฐ€? (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ) + +### 14.3 ๋น„์ฆˆ๋‹ˆ์Šค ๋ฃฐ ๊ฒ€์ฆ + +- [ ] ์ง„ํ–‰/์™„๋ฃŒ ์ƒํƒœ๋Š” ์‚ญ์ œ ๋ถˆ๊ฐ€ํ•œ๊ฐ€? +- [ ] ๊ณ„ํš ์ƒํƒœ๋งŒ ์ž‘์—…์ง€์‹œ ์ƒ์„ฑ ๊ฐ€๋Šฅํ•œ๊ฐ€? +- [ ] ์ž‘์—…์ง€์‹œ ์ƒ์„ฑ ์‹œ work_order_mng์— ๋ฐ์ดํ„ฐ๊ฐ€ INSERT๋˜๋Š”๊ฐ€? + +--- + +## 15. ๊ตฌํ˜„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### 15.1 ๊ฐ„์†Œํ™” ๋ฒ„์ „ (ํ˜„์žฌ ๊ตฌํ˜„ ๊ฐ€๋Šฅ) + +| ์ฒดํฌ | ํ•ญ๋ชฉ | ์„ค๋ช… | +|:----:|------|------| +| โ˜ | **ํ…Œ์ด๋ธ” ์ƒ์„ฑ** | `production_plan_mng` ํ…Œ์ด๋ธ” ์ƒ์„ฑ | +| โ˜ | **ํ™”๋ฉด ์ •์˜** | `screen_definitions` INSERT | +| โ˜ | **๋ชจ๋‹ฌ ํ™”๋ฉด ์ƒ์„ฑ** | ๋“ฑ๋ก/์ˆ˜์ • ๋ชจ๋‹ฌ ํ™”๋ฉด ์ƒ์„ฑ | +| โ˜ | **๋ฉ”์ธ ํ™”๋ฉด ์ƒ์„ฑ** | `screen_layouts_v2` INSERT | +| โ˜ | **์ œ์–ด๊ด€๋ฆฌ ์„ค์ •** | `dataflow_diagrams` INSERT (์‚ญ์ œ, ์ž‘์—…์ง€์‹œ ์ƒ์„ฑ) | +| โ˜ | **๋ฉ”๋‰ด ์—ฐ๊ฒฐ** | ๋Œ€์ƒ ๋ฉ”๋‰ด์— ํ™”๋ฉด ํ• ๋‹น | +| โ˜ | **๊ธฐ๋ณธ CRUD ํ…Œ์ŠคํŠธ** | ๋“ฑ๋ก/์ˆ˜์ •/์‚ญ์ œ/์กฐํšŒ ํ…Œ์ŠคํŠธ | +| โ˜ | **๋น„์ฆˆ๋‹ˆ์Šค ๋ฃฐ ํ…Œ์ŠคํŠธ** | ์กฐ๊ฑด๋ถ€ ์‚ญ์ œ, ์ž‘์—…์ง€์‹œ ์ƒ์„ฑ ํ…Œ์ŠคํŠธ | + +### 15.2 ์ „์ฒด ๋ฒ„์ „ (ํ–ฅํ›„ ๊ตฌํ˜„) + +| ์ฒดํฌ | ํ•ญ๋ชฉ | ์„ค๋ช… | +|:----:|------|------| +| โœ… | ~~**v2-split-panel-layout ํ™•์žฅ**~~ | `displayMode: "custom"` + `components[]` ์ถ”๊ฐ€ **์™„๋ฃŒ** | +| โœ… | ~~**v2-table-grouped ๊ฐœ๋ฐœ**~~ | ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ **์™„๋ฃŒ** | +| โ˜ | **v2-timeline-scheduler ๊ฐœ๋ฐœ** | ํƒ€์ž„๋ผ์ธ/๊ฐ„ํŠธ์ฐจํŠธ ์Šค์ผ€์ค„๋Ÿฌ | +| โ˜ | **๋“œ๋ž˜๊ทธ&๋“œ๋กญ ๊ธฐ๋Šฅ** | ํƒ€์ž„๋ผ์ธ ๋ฐ” ๋“œ๋ž˜๊ทธ ์ด๋™/ํฌ๊ธฐ ์กฐ์ • | +| โ˜ | **์ž๋™ ์Šค์ผ€์ค„ ๋กœ์ง** | ์ž๋™ ์Šค์ผ€์ค„ ์ƒ์„ฑ ๋ฐฑ์—”๋“œ API | +| โ˜ | **๋ฐ˜์ œํ’ˆ ์—ฐ๋™** | BOM ๊ธฐ๋ฐ˜ ๋ฐ˜์ œํ’ˆ ๊ณ„ํš ์ƒ์„ฑ | +| โ˜ | **์ •์‹ ๋ฒ„์ „ ํ™”๋ฉด ๊ตฌ์„ฑ** | ์ปค์Šคํ…€ ๋ชจ๋“œ๋กœ ํƒญ+ํ…Œ์ด๋ธ”+ํƒ€์ž„๋ผ์ธ ์กฐํ•ฉ | + +--- + +## 16. ์ž๋™ ์Šค์ผ€์ค„ ์ƒ์„ฑ ๊ธฐ๋Šฅ + +> ์ƒ์„ธ ๊ฐ€์ด๋“œ: [์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ](../00_analysis/schedule-auto-generation-guide.md) + +### 16.1 ๊ฐœ์š” + +์ขŒ์ธก ์ˆ˜์ฃผ ํ…Œ์ด๋ธ”์—์„œ ํ’ˆ๋ชฉ์„ ์„ ํƒํ•˜๊ณ  "์ž๋™ ์Šค์ผ€์ค„ ์ƒ์„ฑ" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด, ์„ ํƒ๋œ ํ’ˆ๋ชฉ๋“ค์— ๋Œ€ํ•œ ์ƒ์‚ฐ ์Šค์ผ€์ค„์ด ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜์–ด ์šฐ์ธก ํƒ€์ž„๋ผ์ธ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. + +### 16.2 ๋ฐ์ดํ„ฐ ํ๋ฆ„ + +``` +1. ์ขŒ์ธก v2-table-grouped์—์„œ ํ’ˆ๋ชฉ ์„ ํƒ (๊ทธ๋ฃน ์„ ํƒ ์‹œ ์ž์‹ ํฌํ•จ) +2. "์ž๋™ ์Šค์ผ€์ค„ ์ƒ์„ฑ" ๋ฒ„ํŠผ ํด๋ฆญ +3. ๋ฐฑ์—”๋“œ API์—์„œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ƒ์„ฑ (์ƒ์„ฑ/์‚ญ์ œ/์ˆ˜์ •๋  ์Šค์ผ€์ค„) +4. ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ +5. ํ™•์ธ ์‹œ ์Šค์ผ€์ค„ ์ ์šฉ ๋ฐ ํƒ€์ž„๋ผ์ธ ์ƒˆ๋กœ๊ณ ์นจ +6. ๋‹ค์Œ ๋ฐฉ๋ฌธ ์‹œ: ์ขŒ์ธก ์„ ํƒ โ†’ linkedFilter๋กœ ์šฐ์ธก ์ž๋™ ํ•„ํ„ฐ๋ง +``` + +### 16.3 ์Šค์ผ€์ค„ ์ƒ์„ฑ ์„ค์ • + +```json +{ + "scheduleType": "PRODUCTION", + "source": { + "componentId": "order_table", + "tableName": "sales_order_mng", + "groupByField": "part_code", + "quantityField": "balance_qty", + "dueDateField": "due_date" + }, + "resource": { + "type": "ITEM", + "idField": "part_code", + "nameField": "part_name" + }, + "rules": { + "leadTimeDays": 3, + "dailyCapacity": 100, + "workingDays": [1, 2, 3, 4, 5], + "considerStock": true, + "stockTableName": "inventory_mng", + "stockQtyField": "current_qty" + }, + "target": { + "tableName": "production_plan_mng", + "timelineComponentId": "production_timeline" + } +} +``` + +### 16.4 ๋ฒ„ํŠผ ์„ค์ • + +```json +{ + "componentType": "v2-button-primary", + "componentId": "btn_auto_schedule", + "componentConfig": { + "label": "์ž๋™ ์Šค์ผ€์ค„ ์ƒ์„ฑ", + "variant": "default", + "icon": "Calendar", + "action": { + "type": "custom", + "customAction": "autoGenerateSchedule", + "scheduleConfig": { /* ์œ„ ์„ค์ • */ } + } + } +} +``` + +### 16.5 ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์„ค์ • (linkedFilters) + +์ขŒ์ธก ํ…Œ์ด๋ธ” ์„ ํƒ ์‹œ ์šฐ์ธก ํƒ€์ž„๋ผ์ธ ์ž๋™ ํ•„ํ„ฐ๋ง: + +```json +{ + "linkedFilters": [ + { + "sourceComponentId": "order_table", + "sourceField": "part_code", + "targetColumn": "resource_id" + } + ] +} +``` + +### 16.6 ๊ตฌํ˜„ ์ƒํƒœ + +| ํ•ญ๋ชฉ | ์ƒํƒœ | ๋น„๊ณ  | +|------|:----:|------| +| schedule_mng ํ…Œ์ด๋ธ” | โณ ๋Œ€๊ธฐ | ๋ฒ”์šฉ ์Šค์ผ€์ค„ ํ…Œ์ด๋ธ” | +| /api/schedule/preview API | โณ ๋Œ€๊ธฐ | ๋ฏธ๋ฆฌ๋ณด๊ธฐ | +| /api/schedule/apply API | โณ ๋Œ€๊ธฐ | ์ ์šฉ | +| autoGenerateSchedule ๋ฒ„ํŠผ ์•ก์…˜ | โณ ๋Œ€๊ธฐ | buttonActions.ts | +| ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ | โณ ๋Œ€๊ธฐ | ๊ธฐ์กด AlertDialog ํ™œ์šฉ | +| linkedFilter ์—ฐ๋™ | โณ ๋Œ€๊ธฐ | ํƒ€์ž„๋ผ์ธ ํ•„ํ„ฐ๋ง | + +--- + +## 17. ๊ด€๋ จ ๋ฌธ์„œ + +- [์ˆ˜์ฃผ๊ด€๋ฆฌ](../02_sales/order.md) +- [ํ’ˆ๋ชฉ์ •๋ณด](../01_master-data/item-info.md) +- [์„ค๋น„๊ด€๋ฆฌ](../05_equipment/equipment-info.md) +- [BOM๊ด€๋ฆฌ](../01_master-data/bom.md) +- [์ž‘์—…์ง€์‹œ](./work-order.md) +- **[์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ ๊ธฐ๋Šฅ ๊ฐ€์ด๋“œ](../00_analysis/schedule-auto-generation-guide.md)** + +--- + +## 18. ์ฐธ๊ณ : ํ‘œ์ค€ ๊ฐ€์ด๋“œ + +- [ํ™”๋ฉด๊ฐœ๋ฐœ ํ‘œ์ค€ ๊ฐ€์ด๋“œ](../ํ™”๋ฉด๊ฐœ๋ฐœ_ํ‘œ์ค€_๊ฐ€์ด๋“œ.md) +- [V2 ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ](../00_analysis/v2-component-usage-guide.md) +- [์ „์ฒด ํ™”๋ฉด ๋ถ„์„ ๋ณด๊ณ ์„œ](../00_analysis/full-screen-analysis.md) diff --git a/docs/screen-implementation-guide/03_production/work-order.md b/docs/screen-implementation-guide/03_production/work-order.md new file mode 100644 index 00000000..47f3cd05 --- /dev/null +++ b/docs/screen-implementation-guide/03_production/work-order.md @@ -0,0 +1,194 @@ +# ์ž‘์—…์ง€์‹œ ํ™”๋ฉด ๊ตฌํ˜„ ๊ฐ€์ด๋“œ + +> **ํ™”๋ฉด๋ช…**: ์ž‘์—…์ง€์‹œ +> **ํŒŒ์ผ**: ์ž‘์—…์ง€์‹œ.html +> **๋ถ„๋ฅ˜**: ์ƒ์‚ฐ๊ด€๋ฆฌ +> **๊ตฌํ˜„ ๊ฐ€๋Šฅ**: โš ๏ธ ๋ถ€๋ถ„ (๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” ํ•„์š”) + +--- + +## 1. ํ™”๋ฉด ๊ฐœ์š” + +์ƒ์‚ฐ๊ณ„ํš์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž‘์—…์ง€์‹œ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ํ™”๋ฉด์ž…๋‹ˆ๋‹ค. + +### ํ•ต์‹ฌ ๊ธฐ๋Šฅ +- ์ž‘์—…์ง€์‹œ ๋ชฉ๋ก ์กฐํšŒ (ํƒญ๋ณ„ ๊ตฌ๋ถ„) +- ๊ทธ๋ฃนํ™” ๊ธฐ๋Šฅ (์ž‘์—…์ผ์ž, ๊ณต์ •๋ณ„) +- ์ž‘์—…์ง€์‹œ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ +- ์ž‘์—…์ง€์‹œ์„œ ์ธ์‡„ +- ์‹ค์  ์—ฐ๊ณ„ + +--- + +## 2. ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๊ธฐ๊ฐ„] [ํ’ˆ๋ชฉ] [๊ณต์ •] [์ž‘์—…์ƒํƒœโ–ผ] [์ดˆ๊ธฐํ™”][์กฐํšŒ] [์‚ฌ์šฉ์ž์˜ต์…˜][์—‘์…€] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [์ „์ฒด] [๋Œ€๊ธฐ] [์ง„ํ–‰์ค‘] [์™„๋ฃŒ] [์ง€์—ฐ] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ๐Ÿ“‹ ์ž‘์—…์ง€์‹œ ๋ชฉ๋ก โ”‚ ๐Ÿ“„ ์ž‘์—…์ง€์‹œ ์ƒ์„ธ โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ [์ธ์‡„] [์‹œ์ž‘] [์™„๋ฃŒ] [์ˆ˜์ •] [์‚ญ์ œ] โ”‚ +โ”‚ Group by: [์ž‘์—…์ผ์žโ–ผ] โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ์ง€์‹œ๋ฒˆํ˜ธ: WO-2026-0001 โ”‚ +โ”‚ โ”‚โ–ผ 2026-01-30 (5) โ”‚ โ”‚ ํ’ˆ๋ชฉ๋ช…: ์ œํ’ˆA โ”‚ +โ”‚ โ”‚ WO-001|์ œํ’ˆA|๋Œ€๊ธฐโ”‚ โ”‚ ์ง€์‹œ์ˆ˜๋Ÿ‰: 100 EA โ”‚ +โ”‚ โ”‚ WO-002|์ œํ’ˆB|์ง„ํ–‰โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚โ–ผ 2026-01-31 (3) โ”‚ โ”‚ [์ž์žฌํˆฌ์ž…] [๊ณต์ •ํ˜„ํ™ฉ] [์‹ค์ ํ˜„ํ™ฉ] โ”‚ +โ”‚ โ”‚ WO-003|์ œํ’ˆC|๋Œ€๊ธฐโ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ [ํˆฌ์ž…์ž์žฌ ํ…Œ์ด๋ธ”] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 3. V2 ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +| HTML ์˜์—ญ | V2 ์ปดํฌ๋„ŒํŠธ | ์ƒํƒœ | +|-----------|-------------|------| +| ๊ฒ€์ƒ‰ ์„น์…˜ | `v2-table-search-widget` | โœ… ๊ฐ€๋Šฅ | +| ์ƒํƒœ ํƒญ | `v2-tabs-widget` | โœ… ๊ฐ€๋Šฅ | +| ์ž‘์—…์ง€์‹œ ๋ชฉ๋ก (๊ทธ๋ฃนํ™”) | `v2-table-list` | โš ๏ธ ๊ทธ๋ฃนํ™” ๋ฏธ์ง€์› | +| ๋ถ„ํ•  ํŒจ๋„ | `v2-split-panel-layout` | โœ… ๊ฐ€๋Šฅ | +| ์ƒ์„ธ ํƒญ | `v2-tabs-widget` | โœ… ๊ฐ€๋Šฅ | + +--- + +## 4. ํ…Œ์ด๋ธ” ์ •์˜ + +### 4.1 ์ž‘์—…์ง€์‹œ ๋ชฉ๋ก + +```typescript +columns: [ + { id: 'checkbox', type: 'checkbox', width: 50 }, + { id: 'work_order_no', label: '์ง€์‹œ๋ฒˆํ˜ธ', width: 120 }, + { id: 'work_date', label: '์ž‘์—…์ผ', width: 100 }, + { id: 'item_code', label: 'ํ’ˆ๋ชฉ์ฝ”๋“œ', width: 100 }, + { id: 'item_name', label: 'ํ’ˆ๋ชฉ๋ช…', width: 200 }, + { id: 'order_qty', label: '์ง€์‹œ์ˆ˜๋Ÿ‰', width: 100, align: 'right' }, + { id: 'prod_qty', label: '์ƒ์‚ฐ์ˆ˜๋Ÿ‰', width: 100, align: 'right' }, + { id: 'process_name', label: '๊ณต์ •', width: 100 }, + { id: 'status', label: '์ƒํƒœ', width: 80 }, + { id: 'worker', label: '์ž‘์—…์ž', width: 100 } +] +``` + +### 4.2 ์ž์žฌํˆฌ์ž… ํƒญ + +```typescript +materialColumns: [ + { id: 'item_code', label: 'ํ’ˆ๋ชฉ์ฝ”๋“œ', width: 100 }, + { id: 'item_name', label: 'ํ’ˆ๋ชฉ๋ช…', width: 200 }, + { id: 'required_qty', label: '์†Œ์š”๋Ÿ‰', width: 100, align: 'right' }, + { id: 'issued_qty', label: 'ํˆฌ์ž…๋Ÿ‰', width: 100, align: 'right' }, + { id: 'unit', label: '๋‹จ์œ„', width: 60 }, + { id: 'warehouse', label: '์ถœ๊ณ ์ฐฝ๊ณ ', width: 100 } +] +``` + +--- + +## 5. ์ƒํƒœ ํƒญ + +```typescript +statusTabs: [ + { id: 'all', label: '์ „์ฒด', count: 25 }, + { id: 'waiting', label: '๋Œ€๊ธฐ', count: 10 }, + { id: 'progress', label: '์ง„ํ–‰์ค‘', count: 8 }, + { id: 'completed', label: '์™„๋ฃŒ', count: 5 }, + { id: 'delayed', label: '์ง€์—ฐ', count: 2 } +] +``` + +--- + +## 6. ๊ทธ๋ฃนํ™” ๊ธฐ๋Šฅ (v2-grouped-table ํ•„์š”) + +```typescript +groupByOptions: [ + { id: 'work_date', label: '์ž‘์—…์ผ์ž' }, + { id: 'process_name', label: '๊ณต์ •' }, + { id: 'item_type', label: 'ํ’ˆ๋ชฉ๊ตฌ๋ถ„' } +] +``` + +--- + +## 7. ํ˜„์žฌ ๊ตฌํ˜„ ๊ฐ€๋Šฅ ๋ฒ”์œ„ + +### โœ… ๊ฐ€๋Šฅ +- ๊ฒ€์ƒ‰ ์˜์—ญ +- ์ƒํƒœ ํƒญ ์ „ํ™˜ +- ๋ถ„ํ•  ํŒจ๋„ +- ์ƒ์„ธ ํƒญ +- ์ž์žฌํˆฌ์ž…/๊ณต์ •ํ˜„ํ™ฉ/์‹ค์ ํ˜„ํ™ฉ ํ…Œ์ด๋ธ” + +### โš ๏ธ ๋ถ€๋ถ„ ๊ฐ€๋Šฅ +- ์ž‘์—…์ง€์‹œ ๋ชฉ๋ก: ๊ทธ๋ฃนํ™” ์—†์ด ์ผ๋ฐ˜ ํ…Œ์ด๋ธ” + +### โŒ ๋ถˆ๊ฐ€๋Šฅ +- ๋™์  ๊ทธ๋ฃนํ™” + +--- + +## 8. ๊ตฌํ˜„ JSON + +```json +{ + "screen_code": "WORK_ORDER_MAIN", + "screen_name": "์ž‘์—…์ง€์‹œ", + "components": [ + { + "type": "v2-table-search-widget", + "position": { "x": 0, "y": 0, "w": 12, "h": 1 }, + "config": { + "searchFields": [ + { "type": "date", "id": "date_range", "placeholder": "๊ธฐ๊ฐ„", "dateRange": true }, + { "type": "input", "id": "item_name", "placeholder": "ํ’ˆ๋ชฉ๋ช…" }, + { "type": "select", "id": "process", "placeholder": "๊ณต์ •" }, + { "type": "select", "id": "status", "placeholder": "์ƒํƒœ" } + ] + } + }, + { + "type": "v2-tabs-widget", + "position": { "x": 0, "y": 1, "w": 12, "h": 11 }, + "config": { + "tabs": [ + { "id": "all", "label": "์ „์ฒด" }, + { "id": "waiting", "label": "๋Œ€๊ธฐ" }, + { "id": "progress", "label": "์ง„ํ–‰์ค‘" }, + { "id": "completed", "label": "์™„๋ฃŒ" }, + { "id": "delayed", "label": "์ง€์—ฐ" } + ], + "tabContent": { + "type": "v2-split-panel-layout", + "config": { + "masterPanel": { + "title": "์ž‘์—…์ง€์‹œ ๋ชฉ๋ก", + "entityId": "work_order", + "columns": [ + { "id": "work_order_no", "label": "์ง€์‹œ๋ฒˆํ˜ธ" }, + { "id": "work_date", "label": "์ž‘์—…์ผ" }, + { "id": "item_name", "label": "ํ’ˆ๋ชฉ๋ช…" }, + { "id": "order_qty", "label": "์ง€์‹œ์ˆ˜๋Ÿ‰" }, + { "id": "status", "label": "์ƒํƒœ" } + ] + }, + "detailPanel": { + "tabs": [ + { "id": "material", "label": "์ž์žฌํˆฌ์ž…", "entityId": "work_order_material" }, + { "id": "process", "label": "๊ณต์ •ํ˜„ํ™ฉ", "entityId": "work_order_process" }, + { "id": "result", "label": "์‹ค์ ํ˜„ํ™ฉ", "entityId": "work_order_result" } + ] + } + } + } + } + } + ] +} +``` + +**v2-grouped-table ๊ฐœ๋ฐœ ์‹œ ์žฌํ™œ์šฉ ๊ฐ€๋Šฅ** diff --git a/docs/screen-implementation-guide/04_purchase/.gitkeep b/docs/screen-implementation-guide/04_purchase/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docs/screen-implementation-guide/04_purchase/purchase-order.md b/docs/screen-implementation-guide/04_purchase/purchase-order.md new file mode 100644 index 00000000..225a331b --- /dev/null +++ b/docs/screen-implementation-guide/04_purchase/purchase-order.md @@ -0,0 +1,172 @@ +# ๋ฐœ์ฃผ๊ด€๋ฆฌ ํ™”๋ฉด ๊ตฌํ˜„ ๊ฐ€์ด๋“œ + +> **ํ™”๋ฉด๋ช…**: ๋ฐœ์ฃผ๊ด€๋ฆฌ +> **ํŒŒ์ผ**: ๋ฐœ์ฃผ๊ด€๋ฆฌ.html +> **๋ถ„๋ฅ˜**: ๊ตฌ๋งค๊ด€๋ฆฌ +> **๊ตฌํ˜„ ๊ฐ€๋Šฅ**: โœ… ์™„์ „ (ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ) + +--- + +## 1. ํ™”๋ฉด ๊ฐœ์š” + +์ž์žฌ/์›์ž์žฌ ๋ฐœ์ฃผ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ํ™”๋ฉด์ž…๋‹ˆ๋‹ค. + +### ํ•ต์‹ฌ ๊ธฐ๋Šฅ +- ๋ฐœ์ฃผ ๋ชฉ๋ก ์กฐํšŒ/๊ฒ€์ƒ‰ +- ๋ฐœ์ฃผ ๋“ฑ๋ก/์ˆ˜์ •/์‚ญ์ œ +- ๋ฐœ์ฃผ์„œ ์ธ์‡„ +- ์ž…๊ณ  ์—ฐ๊ณ„ + +--- + +## 2. ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๊ธฐ๊ฐ„] [๊ณต๊ธ‰์—…์ฒด] [๋ฐœ์ฃผ๋ฒˆํ˜ธ] [ํ’ˆ๋ชฉ๋ช…] [์ƒํƒœโ–ผ] [์ดˆ๊ธฐํ™”][์กฐํšŒ] โ”‚ +โ”‚ [์‚ฌ์šฉ์ž์˜ต์…˜][OCR][์—‘์…€] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ๐Ÿ“‹ ๋ฐœ์ฃผ ๋ชฉ๋ก [์‹ ๊ทœ๋“ฑ๋ก] โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ โ”‚โ–ก|๋ฐœ์ฃผ๋ฒˆํ˜ธ |๋ฐœ์ฃผ์ผ |๊ณต๊ธ‰์—…์ฒด |๋ฐœ์ฃผ๊ธˆ์•ก |์ƒํƒœ |๋‹ด๋‹น์žโ”‚ โ”‚ +โ”‚ โ”‚โ–ก|PO-2026..|2026-01-30|(์ฃผ)์›์ž์žฌ|5,000,000 |์ง„ํ–‰์ค‘|ํ™๊ธธ๋™โ”‚ โ”‚ +โ”‚ โ”‚โ–ก|PO-2026..|2026-01-29|(์ฃผ)๋ถ€ํ’ˆ์‚ฌ|3,200,000 |์™„๋ฃŒ |๊น€์ฒ ์ˆ˜โ”‚ โ”‚ +โ”‚ โ”‚โ–ก|PO-2026..|2026-01-28|(์ฃผ)์ž์žฌ์‚ฌ|1,800,000 |์ง„ํ–‰์ค‘|์ด์˜ํฌโ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 3. V2 ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +| HTML ์˜์—ญ | V2 ์ปดํฌ๋„ŒํŠธ | ์ƒํƒœ | +|-----------|-------------|------| +| ๊ฒ€์ƒ‰ ์„น์…˜ | `v2-table-search-widget` | โœ… ๊ฐ€๋Šฅ | +| ๋ฐœ์ฃผ ๋ชฉ๋ก | `v2-table-list` | โœ… ๊ฐ€๋Šฅ | +| ๋ฐœ์ฃผ ๋“ฑ๋ก ๋ชจ๋‹ฌ | `v2-modal-form` (ํ•„์š”) | โš ๏ธ ๋Œ€์ฒด ๊ฐ€๋Šฅ | + +--- + +## 4. ํ…Œ์ด๋ธ” ์ •์˜ + +```typescript +columns: [ + { id: 'checkbox', type: 'checkbox', width: 50 }, + { id: 'po_no', label: '๋ฐœ์ฃผ๋ฒˆํ˜ธ', width: 120 }, + { id: 'po_date', label: '๋ฐœ์ฃผ์ผ', width: 100 }, + { id: 'supplier_name', label: '๊ณต๊ธ‰์—…์ฒด', width: 200 }, + { id: 'total_amount', label: '๋ฐœ์ฃผ๊ธˆ์•ก', width: 120, align: 'right', format: 'currency' }, + { id: 'delivery_date', label: '๋‚ฉ๊ธฐ์ผ', width: 100 }, + { id: 'status', label: '์ƒํƒœ', width: 80 }, + { id: 'receive_status', label: '์ž…๊ณ ์ƒํƒœ', width: 100 }, + { id: 'manager', label: '๋‹ด๋‹น์ž', width: 100 } +] +``` + +--- + +## 5. ๊ฒ€์ƒ‰ ์กฐ๊ฑด + +| ํ•„๋“œ๋ช… | ์ปดํฌ๋„ŒํŠธ | ์„ค์ • | +|--------|----------|------| +| ๊ธฐ๊ฐ„ | `v2-date` | dateRange: true | +| ๊ณต๊ธ‰์—…์ฒด | `v2-input` | placeholder: "๊ณต๊ธ‰์—…์ฒด" | +| ๋ฐœ์ฃผ๋ฒˆํ˜ธ | `v2-input` | placeholder: "๋ฐœ์ฃผ๋ฒˆํ˜ธ" | +| ํ’ˆ๋ชฉ๋ช… | `v2-input` | placeholder: "ํ’ˆ๋ชฉ๋ช…" | +| ์ƒํƒœ | `v2-select` | ์ž‘์„ฑ์ค‘, ๋ฐœ์ฃผ, ๋ถ€๋ถ„์ž…๊ณ , ์ž…๊ณ ์™„๋ฃŒ | + +--- + +## 6. ๊ตฌํ˜„ JSON + +```json +{ + "screen_code": "PO_MAIN", + "screen_name": "๋ฐœ์ฃผ๊ด€๋ฆฌ", + "components": [ + { + "type": "v2-table-search-widget", + "position": { "x": 0, "y": 0, "w": 12, "h": 2 }, + "config": { + "searchFields": [ + { "type": "date", "id": "date_range", "placeholder": "๋ฐœ์ฃผ๊ธฐ๊ฐ„", "dateRange": true }, + { "type": "input", "id": "supplier_name", "placeholder": "๊ณต๊ธ‰์—…์ฒด" }, + { "type": "input", "id": "po_no", "placeholder": "๋ฐœ์ฃผ๋ฒˆํ˜ธ" }, + { "type": "input", "id": "item_name", "placeholder": "ํ’ˆ๋ชฉ๋ช…" }, + { "type": "select", "id": "status", "placeholder": "์ƒํƒœ" } + ], + "buttons": [ + { "label": "์ดˆ๊ธฐํ™”", "action": "reset", "variant": "outline" }, + { "label": "์กฐํšŒ", "action": "search", "variant": "primary" } + ], + "rightButtons": [ + { "label": "์‚ฌ์šฉ์ž์˜ต์…˜", "action": "userOptions" }, + { "label": "OCR์ž…๋ ฅ", "action": "ocr" }, + { "label": "์—‘์…€๋‹ค์šด๋กœ๋“œ", "action": "excelDownload" } + ] + } + }, + { + "type": "v2-table-list", + "position": { "x": 0, "y": 2, "w": 12, "h": 10 }, + "config": { + "title": "๋ฐœ์ฃผ ๋ชฉ๋ก", + "entityId": "purchase_order", + "buttons": [ + { "label": "์‹ ๊ทœ๋“ฑ๋ก", "action": "create", "variant": "primary" } + ], + "columns": [ + { "id": "po_no", "label": "๋ฐœ์ฃผ๋ฒˆํ˜ธ", "width": 120 }, + { "id": "po_date", "label": "๋ฐœ์ฃผ์ผ", "width": 100 }, + { "id": "supplier_name", "label": "๊ณต๊ธ‰์—…์ฒด", "width": 200 }, + { "id": "total_amount", "label": "๋ฐœ์ฃผ๊ธˆ์•ก", "width": 120, "align": "right" }, + { "id": "delivery_date", "label": "๋‚ฉ๊ธฐ์ผ", "width": 100 }, + { "id": "status", "label": "์ƒํƒœ", "width": 80 }, + { "id": "manager", "label": "๋‹ด๋‹น์ž", "width": 100 } + ], + "rowActions": [ + { "label": "์ƒ์„ธ", "action": "view" }, + { "label": "์ˆ˜์ •", "action": "edit" }, + { "label": "์‚ญ์ œ", "action": "delete" } + ] + } + } + ] +} +``` + +--- + +## 7. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ” + +### purchase_order (๋ฐœ์ฃผ ํ—ค๋”) +```sql +CREATE TABLE purchase_order ( + id SERIAL PRIMARY KEY, + company_code VARCHAR(20) NOT NULL, + po_no VARCHAR(50) NOT NULL, + po_date DATE NOT NULL, + supplier_code VARCHAR(50), + supplier_name VARCHAR(200), + total_amount NUMERIC(15,2), + tax_amount NUMERIC(15,2), + status VARCHAR(20) DEFAULT 'draft', + delivery_date DATE, + manager VARCHAR(100), + remark TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +--- + +## 8. ๊ตฌํ˜„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +- [x] ๊ฒ€์ƒ‰ ์˜์—ญ: v2-table-search-widget +- [x] ๋ฐœ์ฃผ ๋ชฉ๋ก ํ…Œ์ด๋ธ”: v2-table-list +- [x] ์ปฌ๋Ÿผ ์ •๋ ฌ/ํ•„ํ„ฐ +- [ ] ๋ฐœ์ฃผ ๋“ฑ๋ก ๋ชจ๋‹ฌ +- [ ] OCR ์ž…๋ ฅ ๊ธฐ๋Šฅ (๋ณ„๋„) +- [ ] ์ธ์‡„ ๊ธฐ๋Šฅ + +**ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ๋กœ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๊ฐ€๋Šฅ** diff --git a/docs/screen-implementation-guide/05_equipment/.gitkeep b/docs/screen-implementation-guide/05_equipment/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docs/screen-implementation-guide/05_equipment/equipment-info.md b/docs/screen-implementation-guide/05_equipment/equipment-info.md new file mode 100644 index 00000000..70405df8 --- /dev/null +++ b/docs/screen-implementation-guide/05_equipment/equipment-info.md @@ -0,0 +1,244 @@ +# ์„ค๋น„์ •๋ณด ํ™”๋ฉด ๊ตฌํ˜„ ๊ฐ€์ด๋“œ + +> **ํ™”๋ฉด๋ช…**: ์„ค๋น„์ •๋ณด +> **ํŒŒ์ผ**: ์„ค๋น„์ •๋ณด.html +> **๋ถ„๋ฅ˜**: ์„ค๋น„๊ด€๋ฆฌ +> **๊ตฌํ˜„ ๊ฐ€๋Šฅ**: โœ… ์™„์ „ (v2-card-display ํ™œ์šฉ) + +--- + +## 1. ํ™”๋ฉด ๊ฐœ์š” + +์ƒ์‚ฐ ์„ค๋น„์˜ ๊ธฐ๋ณธ์ •๋ณด ๋ฐ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ํ™”๋ฉด์ž…๋‹ˆ๋‹ค. + +### ํ•ต์‹ฌ ๊ธฐ๋Šฅ +- ์„ค๋น„ ๋ชฉ๋ก ์กฐํšŒ (์นด๋“œ ํ˜•ํƒœ) +- ์„ค๋น„ ๋“ฑ๋ก/์ˆ˜์ •/์‚ญ์ œ +- ์„ค๋น„ ์ƒ์„ธ ์ •๋ณด ํƒญ ๊ด€๋ฆฌ +- ์„ค๋น„ ์ด๋ฏธ์ง€ ๊ด€๋ฆฌ + +--- + +## 2. ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [์„ค๋น„์ฝ”๋“œ] [์„ค๋น„๋ช…] [์„ค๋น„์œ ํ˜•โ–ผ] [์ƒํƒœโ–ผ] [์ดˆ๊ธฐํ™”][์กฐํšŒ] โ”‚ +โ”‚ [์‚ฌ์šฉ์ž์˜ต์…˜][์—…๋กœ๋“œ][๋‹ค์šด๋กœ๋“œ]โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ๐Ÿญ ์„ค๋น„ ๋ชฉ๋ก โ”‚ [๊ธฐ๋ณธ์ •๋ณด][๋ณด์ „์ด๋ ฅ][์ ๊ฒ€์ด๋ ฅ][๊ฐ€๋™ํ˜„ํ™ฉ] โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ [์‹ ๊ทœ๋“ฑ๋ก] โ”‚ ์„ค๋น„์ฝ”๋“œ: EQ-001 โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ์„ค๋น„๋ช…: CNC ๋ฐ€๋ง๋จธ์‹  1ํ˜ธ๊ธฐ โ”‚ +โ”‚ โ”‚ [์ด๋ฏธ์ง€] EQ-001 โ”‚ โ”‚ ์„ค๋น„์œ ํ˜•: ๊ฐ€๊ณต์„ค๋น„ โ”‚ +โ”‚ โ”‚ CNC ๋ฐ€๋ง [๊ฐ€๋™์ค‘] โ”‚ โ”‚ ์ƒํƒœ: ๊ฐ€๋™์ค‘ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ ์ œ์กฐ์‚ฌ: ํ˜„๋Œ€๊ณต์ž‘๊ธฐ๊ณ„ โ”‚ +โ”‚ โ”‚ [์ด๋ฏธ์ง€] EQ-002 โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ ์„ ๋ฐ˜ 1ํ˜ธ [์ ๊ฒ€์ค‘] โ”‚ โ”‚ [๋ณด์ „์ด๋ ฅ ํ…Œ์ด๋ธ”] โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚์ผ์ž |์œ ํ˜• |๋‚ด์šฉ |๋‹ด๋‹น์žโ”‚ โ”‚ +โ”‚ โ”‚ [์ด๋ฏธ์ง€] EQ-003 โ”‚ โ”‚ โ”‚2026-01|์ •๊ธฐ |์˜ค์ผ ๊ตํ™˜ |๊น€์ฒ ์ˆ˜โ”‚ โ”‚ +โ”‚ โ”‚ ํ”„๋ ˆ์Šค [๊ณ ์žฅ] โ”‚ โ”‚ โ”‚2026-01|์ˆ˜๋ฆฌ |๋ฒ ์–ด๋ง ๊ต์ฒด |์ด์˜ํฌโ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 3. V2 ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +| HTML ์˜์—ญ | V2 ์ปดํฌ๋„ŒํŠธ | ์ƒํƒœ | +|-----------|-------------|------| +| ๊ฒ€์ƒ‰ ์„น์…˜ | `v2-table-search-widget` | โœ… ๊ฐ€๋Šฅ | +| ์„ค๋น„ ์นด๋“œ ๋ชฉ๋ก | `v2-card-display` | โœ… ๊ฐ€๋Šฅ | +| ๋ถ„ํ•  ํŒจ๋„ | `v2-split-panel-layout` | โœ… ๊ฐ€๋Šฅ | +| ์ƒ์„ธ ํƒญ | `v2-tabs-widget` | โœ… ๊ฐ€๋Šฅ | + +--- + +## 4. ์„ค๋น„ ์นด๋“œ ๊ตฌ์กฐ + +```typescript +interface EquipmentCard { + id: string; + image: string; // ์„ค๋น„ ์ด๋ฏธ์ง€ URL + code: string; // ์„ค๋น„์ฝ”๋“œ + name: string; // ์„ค๋น„๋ช… + type: string; // ์„ค๋น„์œ ํ˜• + status: 'running' | 'idle' | 'maintenance' | 'broken'; + location: string; +} + +// ์ƒํƒœ๋ณ„ ์Šคํƒ€์ผ +statusStyles: { + running: { bg: '#d1fae5', color: '#065f46', label: '๊ฐ€๋™์ค‘' }, + idle: { bg: '#e5e7eb', color: '#374151', label: '๋Œ€๊ธฐ์ค‘' }, + maintenance: { bg: '#fef3c7', color: '#92400e', label: '์ ๊ฒ€์ค‘' }, + broken: { bg: '#fee2e2', color: '#991b1b', label: '๊ณ ์žฅ' } +} +``` + +--- + +## 5. ์ƒ์„ธ ํƒญ ๊ตฌ์„ฑ + +```typescript +tabs: [ + { + id: 'basic', + label: '๊ธฐ๋ณธ์ •๋ณด', + fields: [ + { id: 'eq_code', label: '์„ค๋น„์ฝ”๋“œ' }, + { id: 'eq_name', label: '์„ค๋น„๋ช…' }, + { id: 'eq_type', label: '์„ค๋น„์œ ํ˜•' }, + { id: 'status', label: '์ƒํƒœ' }, + { id: 'manufacturer', label: '์ œ์กฐ์‚ฌ' }, + { id: 'model', label: '๋ชจ๋ธ๋ช…' }, + { id: 'serial_no', label: '์‹œ๋ฆฌ์–ผ๋ฒˆํ˜ธ' }, + { id: 'install_date', label: '์„ค์น˜์ผ' }, + { id: 'location', label: '์„ค์น˜์œ„์น˜' }, + { id: 'manager', label: '๋‹ด๋‹น์ž' } + ] + }, + { + id: 'maintenance', + label: '๋ณด์ „์ด๋ ฅ', + type: 'table', + entityId: 'equipment_maintenance', + columns: [ + { id: 'date', label: '์ผ์ž' }, + { id: 'type', label: '์œ ํ˜•' }, + { id: 'content', label: '๋‚ด์šฉ' }, + { id: 'worker', label: '๋‹ด๋‹น์ž' }, + { id: 'cost', label: '๋น„์šฉ' } + ] + }, + { + id: 'inspection', + label: '์ ๊ฒ€์ด๋ ฅ', + type: 'table', + entityId: 'equipment_inspection' + }, + { + id: 'operation', + label: '๊ฐ€๋™ํ˜„ํ™ฉ', + type: 'chart' // ํ–ฅํ›„ ํ™•์žฅ + } +] +``` + +--- + +## 6. ๊ฒ€์ƒ‰ ์กฐ๊ฑด + +| ํ•„๋“œ๋ช… | ์ปดํฌ๋„ŒํŠธ | ์˜ต์…˜ | +|--------|----------|------| +| ์„ค๋น„์ฝ”๋“œ | `v2-input` | placeholder: "์„ค๋น„์ฝ”๋“œ" | +| ์„ค๋น„๋ช… | `v2-input` | placeholder: "์„ค๋น„๋ช…" | +| ์„ค๋น„์œ ํ˜• | `v2-select` | ๊ฐ€๊ณต์„ค๋น„, ์กฐ๋ฆฝ์„ค๋น„, ๊ฒ€์‚ฌ์„ค๋น„ ๋“ฑ | +| ์ƒํƒœ | `v2-select` | ๊ฐ€๋™์ค‘, ๋Œ€๊ธฐ์ค‘, ์ ๊ฒ€์ค‘, ๊ณ ์žฅ | + +--- + +## 7. ํ˜„์žฌ ๊ตฌํ˜„ ๊ฐ€๋Šฅ ๋ฒ”์œ„ + +### โœ… ๊ฐ€๋Šฅ +- ๊ฒ€์ƒ‰ ์˜์—ญ: `v2-table-search-widget` +- ์„ค๋น„ ์นด๋“œ ๋ชฉ๋ก: `v2-card-display` (์ด๋ฏธ์ง€+์ •๋ณด ์กฐํ•ฉ ์ง€์›) +- ๋ถ„ํ•  ํŒจ๋„ ๋ ˆ์ด์•„์›ƒ: `v2-split-panel-layout` +- ์ƒ์„ธ ํƒญ: `v2-tabs-widget` +- ๋ณด์ „์ด๋ ฅ/์ ๊ฒ€์ด๋ ฅ ํ…Œ์ด๋ธ”: `v2-table-list` + +### โš ๏ธ ๋ถ€๋ถ„ ๊ฐ€๋Šฅ +- ๊ฐ€๋™ํ˜„ํ™ฉ ์ฐจํŠธ: ๋ณ„๋„ ์ฐจํŠธ ์ปดํฌ๋„ŒํŠธ ํ•„์š” + +--- + +## 8. ํ…Œ์ด๋ธ” ๋Œ€์ฒด ๊ตฌํ˜„ JSON + +```json +{ + "screen_code": "EQUIPMENT_MAIN", + "screen_name": "์„ค๋น„์ •๋ณด", + "components": [ + { + "type": "v2-table-search-widget", + "position": { "x": 0, "y": 0, "w": 12, "h": 2 }, + "config": { + "searchFields": [ + { "type": "input", "id": "eq_code", "placeholder": "์„ค๋น„์ฝ”๋“œ" }, + { "type": "input", "id": "eq_name", "placeholder": "์„ค๋น„๋ช…" }, + { "type": "select", "id": "eq_type", "placeholder": "์„ค๋น„์œ ํ˜•" }, + { "type": "select", "id": "status", "placeholder": "์ƒํƒœ", + "options": [ + { "value": "running", "label": "๊ฐ€๋™์ค‘" }, + { "value": "idle", "label": "๋Œ€๊ธฐ์ค‘" }, + { "value": "maintenance", "label": "์ ๊ฒ€์ค‘" }, + { "value": "broken", "label": "๊ณ ์žฅ" } + ] + } + ] + } + }, + { + "type": "v2-split-panel-layout", + "position": { "x": 0, "y": 2, "w": 12, "h": 10 }, + "config": { + "masterPanel": { + "title": "์„ค๋น„ ๋ชฉ๋ก", + "entityId": "equipment", + "buttons": [ + { "label": "์‹ ๊ทœ๋“ฑ๋ก", "action": "create", "variant": "primary" } + ], + "columns": [ + { "id": "eq_code", "label": "์„ค๋น„์ฝ”๋“œ", "width": 100 }, + { "id": "eq_name", "label": "์„ค๋น„๋ช…", "width": 200 }, + { "id": "eq_type", "label": "์„ค๋น„์œ ํ˜•", "width": 100 }, + { "id": "status", "label": "์ƒํƒœ", "width": 80 }, + { "id": "location", "label": "์„ค์น˜์œ„์น˜", "width": 150 } + ] + }, + "detailPanel": { + "tabs": [ + { "id": "basic", "label": "๊ธฐ๋ณธ์ •๋ณด", "type": "form" }, + { "id": "maintenance", "label": "๋ณด์ „์ด๋ ฅ", "type": "table", "entityId": "eq_maintenance" }, + { "id": "inspection", "label": "์ ๊ฒ€์ด๋ ฅ", "type": "table", "entityId": "eq_inspection" }, + { "id": "operation", "label": "๊ฐ€๋™ํ˜„ํ™ฉ", "type": "custom" } + ] + } + } + } + ] +} +``` + +--- + +## 9. v2-card-display ์„ค์ • ์˜ˆ์‹œ + +`v2-card-display`๋Š” ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค. + +```typescript +// v2-card-display ์„ค์ • +cardDisplayConfig: { + cardsPerRow: 3, + cardSpacing: 16, + cardStyle: { + showTitle: true, // eq_name ํ‘œ์‹œ + showSubtitle: true, // eq_code ํ‘œ์‹œ + showDescription: true, + showImage: true, // ์„ค๋น„ ์ด๋ฏธ์ง€ ํ‘œ์‹œ + showActions: true, + imagePosition: "top", + imageSize: "medium", + }, + columnMapping: { + title: "eq_name", + subtitle: "eq_code", + image: "image_url", + status: "status" + }, + dataSource: "table" +} +``` + +**ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ๋กœ ์™„์ „ ๊ตฌํ˜„ ๊ฐ€๋Šฅ** diff --git a/docs/screen-implementation-guide/06_logistics/.gitkeep b/docs/screen-implementation-guide/06_logistics/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docs/screen-implementation-guide/06_logistics/inout.md b/docs/screen-implementation-guide/06_logistics/inout.md new file mode 100644 index 00000000..943e07a4 --- /dev/null +++ b/docs/screen-implementation-guide/06_logistics/inout.md @@ -0,0 +1,179 @@ +# ์ž…์ถœ๊ณ ๊ด€๋ฆฌ ํ™”๋ฉด ๊ตฌํ˜„ ๊ฐ€์ด๋“œ + +> **ํ™”๋ฉด๋ช…**: ์ž…์ถœ๊ณ ๊ด€๋ฆฌ +> **ํŒŒ์ผ**: ์ž…์ถœ๊ณ ๊ด€๋ฆฌ.html +> **๋ถ„๋ฅ˜**: ๋ฌผ๋ฅ˜๊ด€๋ฆฌ +> **๊ตฌํ˜„ ๊ฐ€๋Šฅ**: โš ๏ธ ๋ถ€๋ถ„ (๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” ํ•„์š”) + +--- + +## 1. ํ™”๋ฉด ๊ฐœ์š” + +์ž์žฌ/์ œํ’ˆ์˜ ์ž…๊ณ  ๋ฐ ์ถœ๊ณ  ๋‚ด์—ญ์„ ํ†ตํ•ฉ ๊ด€๋ฆฌํ•˜๋Š” ํ™”๋ฉด์ž…๋‹ˆ๋‹ค. + +### ํ•ต์‹ฌ ๊ธฐ๋Šฅ +- ์ž…์ถœ๊ณ  ๋‚ด์—ญ ์กฐํšŒ/๊ฒ€์ƒ‰ +- ๊ทธ๋ฃนํ™” ๊ธฐ๋Šฅ (์ž…์ถœ๊ณ ๊ตฌ๋ถ„, ์ฐฝ๊ณ , ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„) +- ์—‘์…€ ์—…๋กœ๋“œ/๋‹ค์šด๋กœ๋“œ + +--- + +## 2. ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [์ž…์ถœ๊ณ ๊ตฌ๋ถ„โ–ผ][์นดํ…Œ๊ณ ๋ฆฌโ–ผ][์ฐฝ๊ณ โ–ผ][ํ’ˆ๋ชฉ์ฝ”๋“œ][ํ’ˆ๋ชฉ๋ช…][๊ธฐ๊ฐ„][์ดˆ๊ธฐํ™”][๊ฒ€์ƒ‰]โ”‚ +โ”‚ [์‚ฌ์šฉ์ž์˜ต์…˜][์—…๋กœ๋“œ][๋‹ค์šด๋กœ๋“œ]โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ๐Ÿ“‹ ์ž…์ถœ๊ณ  ๋‚ด์—ญ ์ „์ฒด 150๊ฑด โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ Group by: [์ž…์ถœ๊ณ ๊ตฌ๋ถ„โ–ผ] โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ โ”‚โ–ผ ์ž…๊ณ  (80) โ”‚ +โ”‚ โ”‚ โ”‚IN-001|๊ตฌ๋งค์ž…๊ณ |2026-01-30|๋ณธ์‚ฌ์ฐฝ๊ณ |P-001|์›์ž์žฌA|100|KG โ”‚ +โ”‚ โ”‚ โ”‚IN-002|์ƒ์‚ฐ์ž…๊ณ |2026-01-30|์ œ1์ฐฝ๊ณ |P-002|์ œํ’ˆA |50 |EA โ”‚ +โ”‚ โ”‚โ–ผ ์ถœ๊ณ  (70) โ”‚ +โ”‚ โ”‚ โ”‚OUT-001|ํŒ๋งค์ถœ๊ณ |2026-01-30|๋ณธ์‚ฌ์ฐฝ๊ณ |P-003|์ œํ’ˆB|30|EA โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 3. V2 ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +| HTML ์˜์—ญ | V2 ์ปดํฌ๋„ŒํŠธ | ์ƒํƒœ | +|-----------|-------------|------| +| ๊ฒ€์ƒ‰ ์„น์…˜ | `v2-table-search-widget` | โœ… ๊ฐ€๋Šฅ | +| ์ž…์ถœ๊ณ  ๋ชฉ๋ก (๊ทธ๋ฃนํ™”) | `v2-table-list` | โš ๏ธ ๊ทธ๋ฃนํ™” ๋ฏธ์ง€์› | + +--- + +## 4. ํ…Œ์ด๋ธ” ์ •์˜ + +```typescript +columns: [ + { id: 'checkbox', type: 'checkbox', width: 50 }, + { id: 'inout_type', label: '์ž…์ถœ๊ณ ๊ตฌ๋ถ„', width: 100 }, + { id: 'category', label: '์นดํ…Œ๊ณ ๋ฆฌ', width: 120 }, + { id: 'doc_no', label: '์ „ํ‘œ๋ฒˆํ˜ธ', width: 120 }, + { id: 'process_date', label: '์ฒ˜๋ฆฌ์ผ์ž', width: 100 }, + { id: 'warehouse', label: '์ฐฝ๊ณ ', width: 120 }, + { id: 'location', label: '์œ„์น˜', width: 100 }, + { id: 'item_code', label: 'ํ’ˆ๋ชฉ์ฝ”๋“œ', width: 120 }, + { id: 'item_name', label: 'ํ’ˆ๋ชฉ๋ช…', width: 200 }, + { id: 'quantity', label: '์ˆ˜๋Ÿ‰', width: 100, align: 'right' }, + { id: 'unit', label: '๋‹จ์œ„', width: 60 }, + { id: 'lot_no', label: '๋กœํŠธ๋ฒˆํ˜ธ', width: 120 }, + { id: 'customer', label: '๊ฑฐ๋ž˜์ฒ˜', width: 120 }, + { id: 'manager', label: '๋‹ด๋‹น์ž', width: 100 }, + { id: 'remark', label: '๋น„๊ณ ', width: 200 } +] +``` + +--- + +## 5. ๊ฒ€์ƒ‰ ์กฐ๊ฑด + +| ํ•„๋“œ๋ช… | ์ปดํฌ๋„ŒํŠธ | ์˜ต์…˜ | +|--------|----------|------| +| ์ž…์ถœ๊ณ ๊ตฌ๋ถ„ | `v2-select` | ์ž…๊ณ , ์ถœ๊ณ  | +| ์นดํ…Œ๊ณ ๋ฆฌ | `v2-select` | ๊ตฌ๋งค์ž…๊ณ , ์ƒ์‚ฐ์ž…๊ณ , ๋ฐ˜ํ’ˆ์ž…๊ณ , ํŒ๋งค์ถœ๊ณ , ์ƒ์‚ฐ์ถœ๊ณ  ๋“ฑ | +| ์ฐฝ๊ณ  | `v2-select` | ๋ณธ์‚ฌ์ฐฝ๊ณ , ์ œ1์ฐฝ๊ณ , ์ œ2์ฐฝ๊ณ  | +| ํ’ˆ๋ชฉ์ฝ”๋“œ | `v2-input` | - | +| ํ’ˆ๋ชฉ๋ช… | `v2-input` | - | +| ๊ธฐ๊ฐ„ | `v2-date` | dateRange: true | + +--- + +## 6. ๊ทธ๋ฃนํ™” ๊ธฐ๋Šฅ (v2-grouped-table ํ•„์š”) + +```typescript +groupByOptions: [ + { id: 'inout_type', label: '์ž…์ถœ๊ณ ๊ตฌ๋ถ„' }, + { id: 'category', label: '์นดํ…Œ๊ณ ๋ฆฌ' }, + { id: 'warehouse', label: '์ฐฝ๊ณ ' }, + { id: 'item_code', label: 'ํ’ˆ๋ชฉ์ฝ”๋“œ' }, + { id: 'process_date', label: '์ฒ˜๋ฆฌ์ผ์ž' }, + { id: 'customer', label: '๊ฑฐ๋ž˜์ฒ˜' } +] +``` + +--- + +## 7. ํ˜„์žฌ ๊ตฌํ˜„ ๊ฐ€๋Šฅ ๋ฒ”์œ„ + +### โœ… ๊ฐ€๋Šฅ +- ๊ฒ€์ƒ‰ ์˜์—ญ +- ์ผ๋ฐ˜ ํ…Œ์ด๋ธ” ๋ชฉ๋ก + +### โš ๏ธ ๋ถ€๋ถ„ ๊ฐ€๋Šฅ +- ๊ทธ๋ฃนํ™” ์—†์ด ํ•„ํ„ฐ๋กœ ๋Œ€์ฒด + +### โŒ ๋ถˆ๊ฐ€๋Šฅ +- ๋™์  ๊ทธ๋ฃนํ™” + +--- + +## 8. ๊ฐ„์†Œํ™” ๊ตฌํ˜„ JSON + +```json +{ + "screen_code": "INOUT_MAIN", + "screen_name": "์ž…์ถœ๊ณ ๊ด€๋ฆฌ", + "components": [ + { + "type": "v2-table-search-widget", + "position": { "x": 0, "y": 0, "w": 12, "h": 2 }, + "config": { + "searchFields": [ + { "type": "select", "id": "inout_type", "placeholder": "์ž…์ถœ๊ณ ๊ตฌ๋ถ„", + "options": [ + { "value": "IN", "label": "์ž…๊ณ " }, + { "value": "OUT", "label": "์ถœ๊ณ " } + ] + }, + { "type": "select", "id": "category", "placeholder": "์นดํ…Œ๊ณ ๋ฆฌ", + "options": [ + { "value": "purchase", "label": "๊ตฌ๋งค์ž…๊ณ " }, + { "value": "production_in", "label": "์ƒ์‚ฐ์ž…๊ณ " }, + { "value": "return_in", "label": "๋ฐ˜ํ’ˆ์ž…๊ณ " }, + { "value": "sales", "label": "ํŒ๋งค์ถœ๊ณ " }, + { "value": "production_out", "label": "์ƒ์‚ฐ์ถœ๊ณ " } + ] + }, + { "type": "select", "id": "warehouse", "placeholder": "์ฐฝ๊ณ " }, + { "type": "input", "id": "item_code", "placeholder": "ํ’ˆ๋ชฉ์ฝ”๋“œ" }, + { "type": "input", "id": "item_name", "placeholder": "ํ’ˆ๋ชฉ๋ช…" }, + { "type": "date", "id": "date_range", "placeholder": "์ฒ˜๋ฆฌ์ผ์ž", "dateRange": true } + ], + "buttons": [ + { "label": "์ดˆ๊ธฐํ™”", "action": "reset" }, + { "label": "๊ฒ€์ƒ‰", "action": "search", "variant": "primary" } + ] + } + }, + { + "type": "v2-table-list", + "position": { "x": 0, "y": 2, "w": 12, "h": 10 }, + "config": { + "title": "์ž…์ถœ๊ณ  ๋‚ด์—ญ", + "entityId": "inventory_transaction", + "showTotalCount": true, + "columns": [ + { "id": "inout_type", "label": "์ž…์ถœ๊ณ ๊ตฌ๋ถ„", "width": 100 }, + { "id": "category", "label": "์นดํ…Œ๊ณ ๋ฆฌ", "width": 120 }, + { "id": "doc_no", "label": "์ „ํ‘œ๋ฒˆํ˜ธ", "width": 120 }, + { "id": "process_date", "label": "์ฒ˜๋ฆฌ์ผ์ž", "width": 100 }, + { "id": "warehouse", "label": "์ฐฝ๊ณ ", "width": 120 }, + { "id": "item_code", "label": "ํ’ˆ๋ชฉ์ฝ”๋“œ", "width": 120 }, + { "id": "item_name", "label": "ํ’ˆ๋ชฉ๋ช…", "width": 200 }, + { "id": "quantity", "label": "์ˆ˜๋Ÿ‰", "width": 100, "align": "right" }, + { "id": "unit", "label": "๋‹จ์œ„", "width": 60 } + ] + } + } + ] +} +``` + +**v2-grouped-table ๊ฐœ๋ฐœ ์‹œ ๊ทธ๋ฃนํ™” ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ** diff --git a/docs/screen-implementation-guide/07_quality/.gitkeep b/docs/screen-implementation-guide/07_quality/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docs/screen-implementation-guide/07_quality/inspection.md b/docs/screen-implementation-guide/07_quality/inspection.md new file mode 100644 index 00000000..a2fdc95d --- /dev/null +++ b/docs/screen-implementation-guide/07_quality/inspection.md @@ -0,0 +1,169 @@ +# ๊ฒ€์‚ฌ์ •๋ณด๊ด€๋ฆฌ ํ™”๋ฉด ๊ตฌํ˜„ ๊ฐ€์ด๋“œ + +> **ํ™”๋ฉด๋ช…**: ๊ฒ€์‚ฌ์ •๋ณด๊ด€๋ฆฌ +> **ํŒŒ์ผ**: ๊ฒ€์‚ฌ์ •๋ณด๊ด€๋ฆฌ.html +> **๋ถ„๋ฅ˜**: ํ’ˆ์งˆ๊ด€๋ฆฌ +> **๊ตฌํ˜„ ๊ฐ€๋Šฅ**: โœ… ์™„์ „ (ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ) + +--- + +## 1. ํ™”๋ฉด ๊ฐœ์š” + +ํ’ˆ์งˆ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๋ฅผ ๋“ฑ๋กํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ํ™”๋ฉด์ž…๋‹ˆ๋‹ค. + +### ํ•ต์‹ฌ ๊ธฐ๋Šฅ +- ๊ฒ€์‚ฌ ์œ ํ˜•๋ณ„ ํƒญ (์ˆ˜์ž…๊ฒ€์‚ฌ, ๊ณต์ •๊ฒ€์‚ฌ, ์ถœํ•˜๊ฒ€์‚ฌ) +- ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ๋“ฑ๋ก/์ˆ˜์ • +- ๋ถˆ๋Ÿ‰ ์ฒ˜๋ฆฌ ์—ฐ๊ณ„ +- ๊ฒ€์‚ฌ ์ด๋ ฅ ๊ด€๋ฆฌ + +--- + +## 2. ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๊ธฐ๊ฐ„] [ํ’ˆ๋ชฉ] [๊ฑฐ๋ž˜์ฒ˜] [๊ฒ€์‚ฌ๊ฒฐ๊ณผโ–ผ] [์ดˆ๊ธฐํ™”][์กฐํšŒ] [์‚ฌ์šฉ์ž์˜ต์…˜][์—‘์…€]โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [๐Ÿ”์ˆ˜์ž…๊ฒ€์‚ฌ(25)][โš™๏ธ๊ณต์ •๊ฒ€์‚ฌ(18)][๐Ÿ“ฆ์ถœํ•˜๊ฒ€์‚ฌ(12)] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ๐Ÿ“‹ ์ˆ˜์ž…๊ฒ€์‚ฌ ๋ชฉ๋ก [์‹ ๊ทœ๋“ฑ๋ก] โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ โ”‚โ–ก|๊ฒ€์‚ฌ๋ฒˆํ˜ธ |๊ฒ€์‚ฌ์ผ |ํ’ˆ๋ชฉ๋ช… |๊ฒ€์‚ฌ์ˆ˜๋Ÿ‰|ํ•ฉ๊ฒฉ์ˆ˜๋Ÿ‰|๋ถˆ๋Ÿ‰์ˆ˜๋Ÿ‰|๊ฒฐ๊ณผโ”‚ +โ”‚ โ”‚โ–ก|IQC-001 |2026-01-30|์›์ž์žฌA |100 |98 |2 |ํ•ฉ๊ฒฉโ”‚ +โ”‚ โ”‚โ–ก|IQC-002 |2026-01-30|์›์ž์žฌB |200 |195 |5 |ํ•ฉ๊ฒฉโ”‚ +โ”‚ โ”‚โ–ก|IQC-003 |2026-01-29|๋ถ€ํ’ˆC |50 |30 |20 |๋ถˆํ•ฉ๊ฒฉโ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 3. V2 ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +| HTML ์˜์—ญ | V2 ์ปดํฌ๋„ŒํŠธ | ์ƒํƒœ | +|-----------|-------------|------| +| ๊ฒ€์ƒ‰ ์„น์…˜ | `v2-table-search-widget` | โœ… ๊ฐ€๋Šฅ | +| ๊ฒ€์‚ฌ์œ ํ˜• ํƒญ | `v2-tabs-widget` | โœ… ๊ฐ€๋Šฅ | +| ๊ฒ€์‚ฌ ๋ชฉ๋ก | `v2-table-list` | โœ… ๊ฐ€๋Šฅ | + +--- + +## 4. ํƒญ ๊ตฌ์„ฑ + +```typescript +tabs: [ + { id: 'incoming', label: '์ˆ˜์ž…๊ฒ€์‚ฌ', icon: '๐Ÿ”', count: 25 }, + { id: 'process', label: '๊ณต์ •๊ฒ€์‚ฌ', icon: 'โš™๏ธ', count: 18 }, + { id: 'shipping', label: '์ถœํ•˜๊ฒ€์‚ฌ', icon: '๐Ÿ“ฆ', count: 12 } +] +``` + +--- + +## 5. ํ…Œ์ด๋ธ” ์ •์˜ + +```typescript +columns: [ + { id: 'checkbox', type: 'checkbox', width: 50 }, + { id: 'inspect_no', label: '๊ฒ€์‚ฌ๋ฒˆํ˜ธ', width: 120 }, + { id: 'inspect_date', label: '๊ฒ€์‚ฌ์ผ', width: 100 }, + { id: 'item_code', label: 'ํ’ˆ๋ชฉ์ฝ”๋“œ', width: 100 }, + { id: 'item_name', label: 'ํ’ˆ๋ชฉ๋ช…', width: 200 }, + { id: 'lot_no', label: '๋กœํŠธ๋ฒˆํ˜ธ', width: 120 }, + { id: 'inspect_qty', label: '๊ฒ€์‚ฌ์ˆ˜๋Ÿ‰', width: 100, align: 'right' }, + { id: 'pass_qty', label: 'ํ•ฉ๊ฒฉ์ˆ˜๋Ÿ‰', width: 100, align: 'right' }, + { id: 'fail_qty', label: '๋ถˆ๋Ÿ‰์ˆ˜๋Ÿ‰', width: 100, align: 'right' }, + { id: 'result', label: '๊ฒฐ๊ณผ', width: 80 }, + { id: 'inspector', label: '๊ฒ€์‚ฌ์ž', width: 100 } +] +``` + +--- + +## 6. ๊ฒ€์ƒ‰ ์กฐ๊ฑด + +| ํ•„๋“œ๋ช… | ์ปดํฌ๋„ŒํŠธ | ์„ค์ • | +|--------|----------|------| +| ๊ธฐ๊ฐ„ | `v2-date` | dateRange: true | +| ํ’ˆ๋ชฉ | `v2-input` | placeholder: "ํ’ˆ๋ชฉ" | +| ๊ฑฐ๋ž˜์ฒ˜ | `v2-input` | placeholder: "๊ฑฐ๋ž˜์ฒ˜" | +| ๊ฒ€์‚ฌ๊ฒฐ๊ณผ | `v2-select` | ์ „์ฒด, ํ•ฉ๊ฒฉ, ๋ถˆํ•ฉ๊ฒฉ, ์กฐ๊ฑด๋ถ€ํ•ฉ๊ฒฉ | + +--- + +## 7. ๊ตฌํ˜„ JSON + +```json +{ + "screen_code": "INSPECTION_MAIN", + "screen_name": "๊ฒ€์‚ฌ์ •๋ณด๊ด€๋ฆฌ", + "components": [ + { + "type": "v2-table-search-widget", + "position": { "x": 0, "y": 0, "w": 12, "h": 2 }, + "config": { + "searchFields": [ + { "type": "date", "id": "date_range", "placeholder": "๊ฒ€์‚ฌ๊ธฐ๊ฐ„", "dateRange": true }, + { "type": "input", "id": "item_name", "placeholder": "ํ’ˆ๋ชฉ" }, + { "type": "input", "id": "supplier", "placeholder": "๊ฑฐ๋ž˜์ฒ˜" }, + { "type": "select", "id": "result", "placeholder": "๊ฒ€์‚ฌ๊ฒฐ๊ณผ", + "options": [ + { "value": "pass", "label": "ํ•ฉ๊ฒฉ" }, + { "value": "fail", "label": "๋ถˆํ•ฉ๊ฒฉ" }, + { "value": "conditional", "label": "์กฐ๊ฑด๋ถ€ํ•ฉ๊ฒฉ" } + ] + } + ], + "buttons": [ + { "label": "์ดˆ๊ธฐํ™”", "action": "reset" }, + { "label": "์กฐํšŒ", "action": "search", "variant": "primary" } + ] + } + }, + { + "type": "v2-tabs-widget", + "position": { "x": 0, "y": 2, "w": 12, "h": 10 }, + "config": { + "tabs": [ + { "id": "incoming", "label": "์ˆ˜์ž…๊ฒ€์‚ฌ" }, + { "id": "process", "label": "๊ณต์ •๊ฒ€์‚ฌ" }, + { "id": "shipping", "label": "์ถœํ•˜๊ฒ€์‚ฌ" } + ], + "tabContent": { + "type": "v2-table-list", + "config": { + "entityId": "inspection", + "filterByTab": true, + "tabFilterField": "inspect_type", + "buttons": [ + { "label": "์‹ ๊ทœ๋“ฑ๋ก", "action": "create", "variant": "primary" } + ], + "columns": [ + { "id": "inspect_no", "label": "๊ฒ€์‚ฌ๋ฒˆํ˜ธ", "width": 120 }, + { "id": "inspect_date", "label": "๊ฒ€์‚ฌ์ผ", "width": 100 }, + { "id": "item_name", "label": "ํ’ˆ๋ชฉ๋ช…", "width": 200 }, + { "id": "lot_no", "label": "๋กœํŠธ๋ฒˆํ˜ธ", "width": 120 }, + { "id": "inspect_qty", "label": "๊ฒ€์‚ฌ์ˆ˜๋Ÿ‰", "width": 100 }, + { "id": "pass_qty", "label": "ํ•ฉ๊ฒฉ์ˆ˜๋Ÿ‰", "width": 100 }, + { "id": "fail_qty", "label": "๋ถˆ๋Ÿ‰์ˆ˜๋Ÿ‰", "width": 100 }, + { "id": "result", "label": "๊ฒฐ๊ณผ", "width": 80 } + ] + } + } + } + } + ] +} +``` + +--- + +## 8. ๊ตฌํ˜„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +- [x] ๊ฒ€์ƒ‰ ์˜์—ญ: v2-table-search-widget +- [x] ๊ฒ€์‚ฌ์œ ํ˜• ํƒญ: v2-tabs-widget +- [x] ๊ฒ€์‚ฌ ๋ชฉ๋ก ํ…Œ์ด๋ธ”: v2-table-list +- [ ] ๊ฒ€์‚ฌ ๋“ฑ๋ก ๋ชจ๋‹ฌ +- [ ] ๋ถˆ๋Ÿ‰ ์ฒ˜๋ฆฌ ์—ฐ๊ณ„ + +**ํ˜„์žฌ V2 ์ปดํฌ๋„ŒํŠธ๋กœ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๊ฐ€๋Šฅ** diff --git a/docs/screen-implementation-guide/README.md b/docs/screen-implementation-guide/README.md new file mode 100644 index 00000000..f82a1fda --- /dev/null +++ b/docs/screen-implementation-guide/README.md @@ -0,0 +1,159 @@ +# ํ™”๋ฉด ๊ตฌํ˜„ ๊ฐ€์ด๋“œ + +V2 ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ™œ์šฉํ•œ ERP ํ™”๋ฉด ๊ตฌํ˜„ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค. + +--- + +## ์ „์ฒด ํ™”๋ฉด ๋ถ„์„ ์š”์•ฝ (2026-01-30) + +### ์ปดํฌ๋„ŒํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ + +| ๊ตฌ๋ถ„ | ํ™”๋ฉด ์ˆ˜ | ๋น„์œจ | +|------|--------|------| +| ํ˜„์žฌ ์ฆ‰์‹œ ๊ตฌํ˜„ ๊ฐ€๋Šฅ | 17๊ฐœ | 65% | +| v2-grouped-table ์ถ”๊ฐ€ ์‹œ | 22๊ฐœ | 85% | +| v2-tree-view ์ถ”๊ฐ€ ์‹œ | 24๊ฐœ | 92% | +| ๋ณ„๋„ ๊ฐœ๋ฐœ ํ•„์š” | 2๊ฐœ | 8% | + +### ์‹ ๊ทœ ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ ์šฐ์„ ์ˆœ์œ„ + +| ์ˆœ์œ„ | ์ปดํฌ๋„ŒํŠธ | ์žฌํ™œ์šฉ ํ™”๋ฉด ์ˆ˜ | ROI | +|------|----------|--------------|-----| +| 1 | v2-grouped-table | 5+ | ๋†’์Œ | +| 2 | v2-tree-view | 3 | ์ค‘๊ฐ„ | +| 3 | v2-timeline-scheduler | 1~2 | ๋‚ฎ์Œ | + +> **์ฐธ๊ณ **: ํ™”๋ฉด ๋””์ž์ด๋„ˆ์—์„œ ํผ ๋ฐฐ์น˜๊ฐ€ ์ž์ฒด ๊ทœ๊ฒฉ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๋ฏ€๋กœ ๋ณ„๋„ ๋ชจ๋‹ฌ/ํผ ์ปดํฌ๋„ŒํŠธ ๋ถˆํ•„์š”. +> `v2-card-display`๋Š” ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. + +> ์ƒ์„ธ ๋ถ„์„: [full-screen-analysis.md](./00_analysis/full-screen-analysis.md) + +--- + +## ํด๋” ๊ตฌ์กฐ + +``` +screen-implementation-guide/ +โ”œโ”€โ”€ 00_analysis/ # ์ „์ฒด ๋ถ„์„ +โ”‚ โ””โ”€โ”€ full-screen-analysis.md # ํ™”๋ฉด ์ „์ฒด ๋ถ„์„ ๋ณด๊ณ ์„œ +โ”‚ +โ”œโ”€โ”€ 01_master-data/ # ๊ธฐ์ค€์ •๋ณด +โ”‚ โ”œโ”€โ”€ item-info.md # ํ’ˆ๋ชฉ์ •๋ณด โœ… +โ”‚ โ”œโ”€โ”€ bom.md # BOM๊ด€๋ฆฌ โš ๏ธ +โ”‚ โ”œโ”€โ”€ company-info.md # ํšŒ์‚ฌ์ •๋ณด +โ”‚ โ”œโ”€โ”€ department.md # ๋ถ€์„œ๊ด€๋ฆฌ +โ”‚ โ””โ”€โ”€ options.md # ์˜ต์…˜์„ค์ • +โ”‚ +โ”œโ”€โ”€ 02_sales/ # ์˜์—…๊ด€๋ฆฌ +โ”‚ โ”œโ”€โ”€ order.md # ์ˆ˜์ฃผ๊ด€๋ฆฌ โœ… +โ”‚ โ”œโ”€โ”€ quote.md # ๊ฒฌ์ ๊ด€๋ฆฌ โœ… +โ”‚ โ”œโ”€โ”€ customer.md # ๊ฑฐ๋ž˜์ฒ˜๊ด€๋ฆฌ โš ๏ธ +โ”‚ โ”œโ”€โ”€ sales-item.md # ํŒ๋งคํ’ˆ๋ชฉ์ •๋ณด +โ”‚ โ””โ”€โ”€ options.md # ์˜์—…์˜ต์…˜์„ค์ • +โ”‚ +โ”œโ”€โ”€ 03_production/ # ์ƒ์‚ฐ๊ด€๋ฆฌ +โ”‚ โ”œโ”€โ”€ production-plan.md # ์ƒ์‚ฐ๊ณ„ํš๊ด€๋ฆฌ โŒ +โ”‚ โ”œโ”€โ”€ work-order.md # ์ž‘์—…์ง€์‹œ โš ๏ธ +โ”‚ โ”œโ”€โ”€ production-result.md # ์ƒ์‚ฐ์‹ค์  +โ”‚ โ”œโ”€โ”€ process-info.md # ๊ณต์ •์ •๋ณด๊ด€๋ฆฌ +โ”‚ โ””โ”€โ”€ options.md # ์ƒ์‚ฐ์˜ต์…˜์„ค์ • +โ”‚ +โ”œโ”€โ”€ 04_purchase/ # ๊ตฌ๋งค๊ด€๋ฆฌ +โ”‚ โ”œโ”€โ”€ purchase-order.md # ๋ฐœ์ฃผ๊ด€๋ฆฌ โœ… +โ”‚ โ”œโ”€โ”€ purchase-item.md # ๊ตฌ๋งคํ’ˆ๋ชฉ์ •๋ณด +โ”‚ โ”œโ”€โ”€ supplier.md # ๊ณต๊ธ‰์—…์ฒด๊ด€๋ฆฌ +โ”‚ โ”œโ”€โ”€ receiving.md # ์ž…๊ณ ๊ด€๋ฆฌ +โ”‚ โ””โ”€โ”€ options.md # ๊ตฌ๋งค์˜ต์…˜์„ค์ • +โ”‚ +โ”œโ”€โ”€ 05_equipment/ # ์„ค๋น„๊ด€๋ฆฌ +โ”‚ โ”œโ”€โ”€ equipment-info.md # ์„ค๋น„์ •๋ณด โœ… +โ”‚ โ””โ”€โ”€ options.md # ์„ค๋น„์˜ต์…˜์„ค์ • +โ”‚ +โ”œโ”€โ”€ 06_logistics/ # ๋ฌผ๋ฅ˜๊ด€๋ฆฌ +โ”‚ โ”œโ”€โ”€ inout.md # ์ž…์ถœ๊ณ ๊ด€๋ฆฌ โš ๏ธ +โ”‚ โ”œโ”€โ”€ logistics-info.md # ๋ฌผ๋ฅ˜์ •๋ณด๊ด€๋ฆฌ +โ”‚ โ”œโ”€โ”€ inventory.md # ์žฌ๊ณ ํ˜„ํ™ฉ +โ”‚ โ”œโ”€โ”€ warehouse.md # ์ฐฝ๊ณ ์ •๋ณด๊ด€๋ฆฌ +โ”‚ โ”œโ”€โ”€ shipping.md # ์ถœ๊ณ ๊ด€๋ฆฌ +โ”‚ โ””โ”€โ”€ options.md # ๋ฌผ๋ฅ˜์˜ต์…˜์„ค์ • +โ”‚ +โ”œโ”€โ”€ 07_quality/ # ํ’ˆ์งˆ๊ด€๋ฆฌ +โ”‚ โ”œโ”€โ”€ inspection.md # ๊ฒ€์‚ฌ์ •๋ณด๊ด€๋ฆฌ โœ… +โ”‚ โ”œโ”€โ”€ item-inspection.md # ํ’ˆ๋ชฉ๊ฒ€์‚ฌ์ •๋ณด +โ”‚ โ””โ”€โ”€ options.md # ํ’ˆ์งˆ์˜ต์…˜์„ค์ • +โ”‚ +โ””โ”€โ”€ README.md + +# ๋ฒ”๋ก€: โœ… ์™„์ „๊ตฌํ˜„ | โš ๏ธ ๋ถ€๋ถ„๊ตฌํ˜„ | โŒ ์‹ ๊ทœ๊ฐœ๋ฐœํ•„์š” +``` + +## ๋ฌธ์„œ ์ž‘์„ฑ ํ˜•์‹ + +๊ฐ ํ™”๋ฉด๋ณ„ ๋ฌธ์„œ๋Š” ๋‹ค์Œ ๊ตฌ์กฐ๋กœ ์ž‘์„ฑ๋ฉ๋‹ˆ๋‹ค: + +### 1. ํ…Œ์ด๋ธ” ์„ ํƒ ๋ฐ ํ™”๋ฉด ๊ตฌ์กฐ +- ์‚ฌ์šฉํ•  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ” +- ํ…Œ์ด๋ธ” ๊ฐ„ ๊ด€๊ณ„ (FK, ์กฐ์ธ) +- ํ™”๋ฉด ์ „์ฒด ๋ ˆ์ด์•„์›ƒ + +### 2. ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜๋„ +- ASCII ๋‹ค์ด์–ด๊ทธ๋žจ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ +- ๊ฐ ์˜์—ญ๋ณ„ ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ ๋ช…์‹œ + +### 3. ๊ฐ ์ปดํฌ๋„ŒํŠธ๋ณ„ ์„ค์ • +- ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… +- ์ƒ์„ธ ์„ค์ • (config) +- ์—ฐ๋™ ์„ค์ • + +### 4. ์‚ฌ์šฉ์ž ์‚ฌ์šฉ ์˜ˆ์‹œ ์‹œ๋‚˜๋ฆฌ์˜ค +- ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค +- ๊ธฐ๋Œ€ ๋™์ž‘ +- ๊ฒ€์ฆ ํฌ์ธํŠธ + +## ๋ฉ”๋‰ด๋ณ„ Screen ID ๋งคํ•‘ + +| ๋ฉ”๋‰ด | Screen ID | ์ƒํƒœ | +|------|-----------|------| +| **๊ธฐ์ค€์ •๋ณด** | | | +| ํšŒ์‚ฌ์ •๋ณด | /screens/138 | ํ™œ์„ฑํ™” | +| ๋ถ€์„œ๊ด€๋ฆฌ | /screens/1487 | ํ™œ์„ฑํ™” | +| ํ’ˆ๋ชฉ์ •๋ณด | /screens/140 | ํ™œ์„ฑํ™” | +| ์˜ต์…˜์„ค์ • | /screens/1421 | ํ™œ์„ฑํ™” | +| **์˜์—…๊ด€๋ฆฌ** | | | +| ๊ฒฌ์ ๊ด€๋ฆฌ | - | ํ™œ์„ฑํ™” | +| ์ˆ˜์ฃผ๊ด€๋ฆฌ | /screens/156 | ํ™œ์„ฑํ™” | +| ๊ฑฐ๋ž˜์ฒ˜๊ด€๋ฆฌ | - | ํ™œ์„ฑํ™” | +| ํŒ๋งคํ’ˆ๋ชฉ์ •๋ณด | - | ํ™œ์„ฑํ™” | +| ์˜์—…์˜ต์…˜์„ค์ • | /screens/1552 | ํ™œ์„ฑํ™” | +| **์ƒ์‚ฐ๊ด€๋ฆฌ** | | | +| ์ƒ์‚ฐ๊ณ„ํš | - | ํ™œ์„ฑํ™” | +| ์ž‘์—…์ง€์‹œ | - | ํ™œ์„ฑํ™” | +| ์ƒ์‚ฐ์‹ค์  | - | ํ™œ์„ฑํ™” | +| ๊ณต์ •์ •๋ณด๊ด€๋ฆฌ | /screens/1599 | ํ™œ์„ฑํ™” | +| BOM๊ด€๋ฆฌ | - | ํ™œ์„ฑํ™” | +| ์ƒ์‚ฐ์˜ต์…˜์„ค์ • | /screens/1606 | ํ™œ์„ฑํ™” | +| **๊ตฌ๋งค๊ด€๋ฆฌ** | | | +| ๋ฐœ์ฃผ๊ด€๋ฆฌ | /screens/1244 | ํ™œ์„ฑํ™” | +| ๊ตฌ๋งคํ’ˆ๋ชฉ์ •๋ณด | /screens/1061 | ํ™œ์„ฑํ™” | +| ๊ณต๊ธ‰์—…์ฒด๊ด€๋ฆฌ | /screens/1053 | ํ™œ์„ฑํ™” | +| ์ž…๊ณ ๊ด€๋ฆฌ | /screens/1064 | ํ™œ์„ฑํ™” | +| ๊ตฌ๋งค์˜ต์…˜์„ค์ • | /screens/1057 | ํ™œ์„ฑํ™” | +| **์„ค๋น„๊ด€๋ฆฌ** | | | +| ์„ค๋น„์ •๋ณด | /screens/1253 | ํ™œ์„ฑํ™” | +| ์„ค๋น„์˜ต์…˜์„ค์ • | /screens/1264 | ํ™œ์„ฑํ™” | +| **๋ฌผ๋ฅ˜๊ด€๋ฆฌ** | | | +| ๋ฌผ๋ฅ˜์ •๋ณด๊ด€๋ฆฌ | /screens/1556 | ํ™œ์„ฑํ™” | +| ์ž…์ถœ๊ณ ๊ด€๋ฆฌ | - | ํ™œ์„ฑํ™” | +| ์žฌ๊ณ ํ˜„ํ™ฉ | /screens/1587 | ํ™œ์„ฑํ™” | +| ์ฐฝ๊ณ ์ •๋ณด๊ด€๋ฆฌ | /screens/1562 | ํ™œ์„ฑํ™” | +| ์ถœ๊ณ ๊ด€๋ฆฌ | /screens/2296 | ํ™œ์„ฑํ™” | +| ๋ฌผ๋ฅ˜์˜ต์…˜์„ค์ • | /screens/1559 | ํ™œ์„ฑํ™” | +| **ํ’ˆ์งˆ๊ด€๋ฆฌ** | | | +| ๊ฒ€์‚ฌ์ •๋ณด๊ด€๋ฆฌ | /screens/1616 | ํ™œ์„ฑํ™” | +| ํ’ˆ๋ชฉ๊ฒ€์‚ฌ์ •๋ณด | /screens/2089 | ํ™œ์„ฑํ™” | +| ํ’ˆ์งˆ์˜ต์…˜์„ค์ • | /screens/1622 | ํ™œ์„ฑํ™” | + +## ์ฐธ๊ณ  ๋ฌธ์„œ + +- [V2 ์ปดํฌ๋„ŒํŠธ ๋ถ„์„ ๊ฐ€์ด๋“œ](../V2_์ปดํฌ๋„ŒํŠธ_๋ถ„์„_๊ฐ€์ด๋“œ.md) +- [V2 ์ปดํฌ๋„ŒํŠธ ์—ฐ๋™ ๊ฐ€์ด๋“œ](../V2_์ปดํฌ๋„ŒํŠธ_์—ฐ๋™_๊ฐ€์ด๋“œ.md) diff --git a/docs/screen-implementation-guide/SCREEN_DEVELOPMENT_STANDARD.md b/docs/screen-implementation-guide/SCREEN_DEVELOPMENT_STANDARD.md new file mode 100644 index 00000000..606caa03 --- /dev/null +++ b/docs/screen-implementation-guide/SCREEN_DEVELOPMENT_STANDARD.md @@ -0,0 +1,572 @@ +# Screen Development Standard Guide (AI Agent Reference) + +> **Purpose**: Ensure consistent screen development output regardless of who develops it +> **Target**: AI Agents (Cursor, etc.), Developers +> **Version**: 1.0.0 + +--- + +## CRITICAL RULES + +1. **ONLY use V2 components** (components with `v2-` prefix) +2. **SEPARATE UI and Logic**: UI in `screen_layouts_v2`, Logic in `dataflow_diagrams` +3. **ALWAYS apply company_code filtering** (multi-tenancy) + +--- + +## AVAILABLE V2 COMPONENTS (23 total) + +### Input Components +| ID | Name | Purpose | +|----|------|---------| +| `v2-input` | Input | text, number, password, email, tel, url, textarea | +| `v2-select` | Select | dropdown, combobox, radio, checkbox | +| `v2-date` | Date | date, time, datetime, daterange, month, year | + +### Display Components +| ID | Name | Purpose | +|----|------|---------| +| `v2-text-display` | Text Display | labels, titles | +| `v2-card-display` | Card Display | table data as cards | +| `v2-aggregation-widget` | Aggregation Widget | sum, avg, count, min, max | + +### Table/Data Components +| ID | Name | Purpose | +|----|------|---------| +| `v2-table-list` | Table List | data grid with CRUD | +| `v2-table-search-widget` | Search Widget | table search/filter | +| `v2-pivot-grid` | Pivot Grid | multi-dimensional analysis | + +### Layout Components +| ID | Name | Purpose | +|----|------|---------| +| `v2-split-panel-layout` | Split Panel | master-detail layout | +| `v2-tabs-widget` | Tabs Widget | tab navigation | +| `v2-section-card` | Section Card | titled grouping container | +| `v2-section-paper` | Section Paper | background grouping | +| `v2-divider-line` | Divider | area separator | +| `v2-repeat-container` | Repeat Container | data-driven repeat | +| `v2-repeater` | Repeater | repeat control | +| `v2-repeat-screen-modal` | Repeat Screen Modal | modal repeat | + +### Action/Special Components +| ID | Name | Purpose | +|----|------|---------| +| `v2-button-primary` | Primary Button | save, delete, etc. | +| `v2-numbering-rule` | Numbering Rule | auto code generation | +| `v2-category-manager` | Category Manager | category management | +| `v2-location-swap-selector` | Location Swap | location selection | +| `v2-rack-structure` | Rack Structure | warehouse rack visualization | +| `v2-media` | Media | image/video display | + +--- + +## SCREEN PATTERNS (5 types) + +### Pattern A: Basic Master Screen +**When**: Single table CRUD +**Components**: +``` +v2-table-search-widget +v2-table-list +v2-button-primary +``` + +### Pattern B: Master-Detail Screen +**When**: Master selection โ†’ Detail display +**Components**: +``` +v2-split-panel-layout + โ”œโ”€ left: v2-table-list (master) + โ””โ”€ right: v2-table-list (detail) +``` +**Required Config**: +```json +{ + "leftPanel": { "tableName": "master_table" }, + "rightPanel": { + "tableName": "detail_table", + "relation": { "type": "detail", "foreignKey": "master_id" } + }, + "splitRatio": 30 +} +``` + +### Pattern C: Master-Detail + Tabs +**When**: Master selection โ†’ Multiple tabs +**Components**: +``` +v2-split-panel-layout + โ”œโ”€ left: v2-table-list (master) + โ””โ”€ right: v2-tabs-widget +``` + +### Pattern D: Card View +**When**: Image + info card display +**Components**: +``` +v2-table-search-widget +v2-card-display +``` +**Required Config**: +```json +{ + "cardsPerRow": 3, + "columnMapping": { + "title": "name", + "subtitle": "code", + "image": "image_url" + } +} +``` + +### Pattern E: Pivot Analysis +**When**: Multi-dimensional aggregation +**Components**: +``` +v2-pivot-grid +``` + +--- + +## DATABASE TABLES + +### Screen Definition +```sql +-- screen_definitions: Screen basic info +INSERT INTO screen_definitions ( + screen_name, screen_code, description, table_name, company_code +) VALUES (...) RETURNING screen_id; + +-- screen_layouts_v2: UI layout (JSON) +INSERT INTO screen_layouts_v2 ( + screen_id, company_code, layout_data +) VALUES (...); + +-- screen_menu_assignments: Menu connection +INSERT INTO screen_menu_assignments ( + screen_id, menu_objid, company_code +) VALUES (...); +``` + +### Control Management (Business Logic) +```sql +-- dataflow_diagrams: Business logic +INSERT INTO dataflow_diagrams ( + diagram_name, company_code, control, plan +) VALUES (...); +``` + +--- + +## UI SETTING vs BUSINESS LOGIC + +### UI Setting (Screen Designer) +| Item | Storage | +|------|---------| +| Component placement | screen_layouts_v2.layout_data | +| Table name | layout_data.tableName | +| Column visibility | layout_data.columns | +| Search fields | layout_data.searchFields | +| Basic save/delete | button config.action.type | + +### Business Logic (Control Management) +| Item | Storage | +|------|---------| +| Conditional execution | dataflow_diagrams.control | +| Multi-table save | dataflow_diagrams.plan | +| Before/after trigger | control.triggerType | +| Field mapping | plan.mappings | + +--- + +## BUSINESS LOGIC JSON STRUCTURE + +### Control (Conditions) +```json +{ + "control": { + "actionType": "update|insert|delete", + "triggerType": "before|after", + "conditions": [ + { + "id": "unique-id", + "type": "condition", + "field": "column_name", + "operator": "=|!=|>|<|>=|<=|LIKE|IN|IS NULL", + "value": "compare_value", + "dataType": "string|number|date|boolean" + } + ] + } +} +``` + +### Plan (Actions) +```json +{ + "plan": { + "actions": [ + { + "id": "action-id", + "actionType": "update|insert|delete", + "targetTable": "table_name", + "fieldMappings": [ + { + "sourceField": "source_column", + "targetField": "target_column", + "defaultValue": "static_value", + "valueType": "field|static" + } + ] + } + ] + } +} +``` + +### Special Values +| Value | Meaning | +|-------|---------| +| `#NOW` | Current timestamp | +| `#USER` | Current user ID | +| `#COMPANY` | Current company code | + +--- + +## DEVELOPMENT STEPS + +### Step 1: Analyze Requirements +``` +1. Which tables? (table names) +2. Table relationships? (FK) +3. Which pattern? (A/B/C/D/E) +4. Which buttons? +5. Business logic per button? +``` + +### Step 2: INSERT screen_definitions +```sql +INSERT INTO screen_definitions ( + screen_name, screen_code, description, table_name, company_code, created_at +) VALUES ( + 'ํ™”๋ฉด๋ช…', 'SCREEN_CODE', '์„ค๋ช…', 'main_table', 'COMPANY_CODE', NOW() +) RETURNING screen_id; +``` + +### Step 3: INSERT screen_layouts_v2 +```sql +INSERT INTO screen_layouts_v2 ( + screen_id, company_code, layout_data +) VALUES ( + {screen_id}, 'COMPANY_CODE', '{layout_json}'::jsonb +); +``` + +### Step 4: INSERT dataflow_diagrams (if complex logic) +```sql +INSERT INTO dataflow_diagrams ( + diagram_name, company_code, control, plan +) VALUES ( + 'ํ™”๋ฉด๋ช…_์ œ์–ด', 'COMPANY_CODE', '{control_json}'::jsonb, '{plan_json}'::jsonb +) RETURNING diagram_id; +``` + +### Step 5: Link button to dataflow +In layout_data, set button config: +```json +{ + "id": "btn-action", + "componentType": "v2-button-primary", + "componentConfig": { + "text": "ํ™•์ •", + "enableDataflowControl": true, + "dataflowDiagramId": {diagram_id} + } +} +``` + +### Step 6: INSERT screen_menu_assignments +```sql +INSERT INTO screen_menu_assignments ( + screen_id, menu_objid, company_code +) VALUES ( + {screen_id}, {menu_objid}, 'COMPANY_CODE' +); +``` + +--- + +## EXAMPLE: Order Management + +### Requirements +``` +Screen: ์ˆ˜์ฃผ๊ด€๋ฆฌ (Order Management) +Pattern: B (Master-Detail) +Tables: + - Master: order_master + - Detail: order_detail +Buttons: + - [์ €์žฅ]: Save to order_master + - [ํ™•์ •]: + - Condition: status = '๋Œ€๊ธฐ' + - Action: Update status to 'ํ™•์ •' + - Additional: Insert to order_history +``` + +### layout_data JSON +```json +{ + "components": [ + { + "id": "search-1", + "componentType": "v2-table-search-widget", + "position": {"x": 0, "y": 0}, + "size": {"width": 1920, "height": 80} + }, + { + "id": "split-1", + "componentType": "v2-split-panel-layout", + "position": {"x": 0, "y": 80}, + "size": {"width": 1920, "height": 800}, + "componentConfig": { + "leftPanel": {"tableName": "order_master"}, + "rightPanel": { + "tableName": "order_detail", + "relation": {"type": "detail", "foreignKey": "order_id"} + }, + "splitRatio": 30 + } + }, + { + "id": "btn-save", + "componentType": "v2-button-primary", + "componentConfig": { + "text": "์ €์žฅ", + "action": {"type": "save"} + } + }, + { + "id": "btn-confirm", + "componentType": "v2-button-primary", + "componentConfig": { + "text": "ํ™•์ •", + "enableDataflowControl": true, + "dataflowDiagramId": 123 + } + } + ] +} +``` + +### dataflow_diagrams JSON (for ํ™•์ • button) +```json +{ + "control": { + "actionType": "update", + "triggerType": "after", + "conditions": [ + { + "id": "cond-1", + "type": "condition", + "field": "status", + "operator": "=", + "value": "๋Œ€๊ธฐ", + "dataType": "string" + } + ] + }, + "plan": { + "actions": [ + { + "id": "action-1", + "actionType": "update", + "targetTable": "order_master", + "fieldMappings": [ + {"targetField": "status", "defaultValue": "ํ™•์ •"} + ] + }, + { + "id": "action-2", + "actionType": "insert", + "targetTable": "order_history", + "fieldMappings": [ + {"sourceField": "order_no", "targetField": "order_no"}, + {"sourceField": "customer_name", "targetField": "customer_name"}, + {"defaultValue": "#NOW", "targetField": "confirmed_at"} + ] + } + ] + } +} +``` + +--- + +## NOT SUPPORTED (Requires Custom Development) + +| UI Type | Status | Alternative | +|---------|--------|-------------| +| Tree View | โŒ | Develop `v2-tree-view` | +| Grouped Table | โŒ | Develop `v2-grouped-table` | +| Gantt Chart | โŒ | Separate development | +| Drag & Drop | โŒ | Use order column | + +--- + +## CHECKLIST + +### Screen Creation +``` +โ–ก screen_definitions INSERT completed +โ–ก screen_layouts_v2 INSERT completed +โ–ก screen_menu_assignments INSERT completed (if needed) +โ–ก company_code filtering applied +โ–ก All components have v2- prefix +``` + +### Business Logic +``` +โ–ก Basic actions (save/delete) โ†’ Screen designer setting +โ–ก Conditional/Multi-table โ†’ dataflow_diagrams INSERT +โ–ก Button config has dataflowDiagramId +โ–ก control.conditions configured +โ–ก plan.actions or plan.mappings configured +``` + +--- + +## BUSINESS LOGIC REQUEST FORMAT (MANDATORY) + +> **WARNING**: No format = No processing. Write it properly, idiot. +> Vague input = vague output. No input = no output. + +### Request Template + +``` +=== BUSINESS LOGIC REQUEST === + +ใ€SCREEN INFOใ€‘ +- Screen Name: +- Company Code: +- Menu ID (if any): + +ใ€TABLE INFOใ€‘ +- Main Table: +- Detail Table (if any): +- FK Relation (if any): + +ใ€BUTTON LISTใ€‘ +Button 1: + - Name: + - Action Type: (save/delete/update/query/other) + - Condition (if any): + - Target Table: + - Additional Actions (if any): + +Button 2: + - Name: + - ... + +ใ€ADDITIONAL REQUIREMENTSใ€‘ +- +``` + +### Valid Example + +``` +=== BUSINESS LOGIC REQUEST === + +ใ€SCREEN INFOใ€‘ +- Screen Name: ์ˆ˜์ฃผ๊ด€๋ฆฌ (Order Management) +- Company Code: ssalmeog +- Menu ID: 55566 + +ใ€TABLE INFOใ€‘ +- Main Table: order_master +- Detail Table: order_detail +- FK Relation: order_id + +ใ€BUTTON LISTใ€‘ +Button 1: + - Name: ์ €์žฅ (Save) + - Action Type: save + - Condition: none + - Target Table: order_master, order_detail + - Additional Actions: none + +Button 2: + - Name: ํ™•์ • (Confirm) + - Action Type: update + - Condition: status = '๋Œ€๊ธฐ' + - Target Table: order_master + - Additional Actions: + 1. Change status to 'ํ™•์ •' + 2. INSERT to order_history (order_no, customer_name, confirmed_at=NOW) + +Button 3: + - Name: ์‚ญ์ œ (Delete) + - Action Type: delete + - Condition: status != 'ํ™•์ •' + - Target Table: order_master, order_detail (cascade) + - Additional Actions: none + +ใ€ADDITIONAL REQUIREMENTSใ€‘ +- Confirmed orders cannot be modified/deleted +- Auto-numbering for order_no (ORDER-YYYYMMDD-0001) +``` + +### Invalid Examples (DO NOT DO THIS) + +``` +โŒ "Make an order management screen" + โ†’ Which table? Buttons? Logic? + +โŒ "Save button should save" + โ†’ To which table? Conditions? + +โŒ "Handle inventory when confirmed" + โ†’ Which table? Increase? Decrease? By how much? + +โŒ "Similar to the previous screen" + โ†’ What previous screen? +``` + +### Complex Logic Format + +For multiple conditions or complex workflows: + +``` +ใ€COMPLEX BUTTON LOGICใ€‘ +Button Name: ์ถœ๊ณ ํ™•์ • (Shipment Confirm) + +Execution Conditions: + Cond1: status = '์ถœ๊ณ ๋Œ€๊ธฐ' AND + Cond2: qty > 0 AND + Cond3: warehouse_id IS NOT NULL + +Execution Steps (in order): + 1. shipment_master.status โ†’ '์ถœ๊ณ ์™„๋ฃŒ' + 2. Decrease qty in inventory (WHERE item_code = current_row.item_code) + 3. INSERT to shipment_history: + - shipment_no โ† current_row.shipment_no + - shipped_qty โ† current_row.qty + - shipped_at โ† #NOW + - shipped_by โ† #USER + +On Failure: + - Insufficient stock: Show "์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค" + - Condition not met: Show "์ถœ๊ณ ๋Œ€๊ธฐ ์ƒํƒœ๋งŒ ํ™•์ • ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" +``` + +--- + +## REFERENCE PATHS + +| Item | Path/Table | +|------|------------| +| Control Management Page | `/admin/systemMng/dataflow` | +| Screen Definition Table | `screen_definitions` | +| Layout Table | `screen_layouts_v2` | +| Control Table | `dataflow_diagrams` | +| Menu Assignment Table | `screen_menu_assignments` | diff --git a/docs/screen-implementation-guide/_TEMPLATE.md b/docs/screen-implementation-guide/_TEMPLATE.md new file mode 100644 index 00000000..e9efd0ba --- /dev/null +++ b/docs/screen-implementation-guide/_TEMPLATE.md @@ -0,0 +1,212 @@ +# [ํ™”๋ฉด๋ช…] + +> Screen ID: /screens/XXX +> ๋ฉ”๋‰ด ๊ฒฝ๋กœ: [L2 ๋ฉ”๋‰ด] > [L3 ๋ฉ”๋‰ด] + +## 1. ํ…Œ์ด๋ธ” ์„ ํƒ ๋ฐ ํ™”๋ฉด ๊ตฌ์กฐ + +### 1.1 ์‚ฌ์šฉ ํ…Œ์ด๋ธ” + +| ํ…Œ์ด๋ธ”๋ช… | ์šฉ๋„ | ๋น„๊ณ  | +|----------|------|------| +| `table_name` | ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ | ์ฃผ ํ…Œ์ด๋ธ” | +| `detail_table` | ๋””ํ…Œ์ผ ๋ฐ์ดํ„ฐ | FK: master_id | + +### 1.2 ํ…Œ์ด๋ธ” ๊ด€๊ณ„ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ master_table โ”‚ โ”‚ detail_table โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ id (PK) โ”‚โ”€โ”€1:Nโ”€โ”€โ”‚ master_id (FK) โ”‚ +โ”‚ name โ”‚ โ”‚ id (PK) โ”‚ +โ”‚ ... โ”‚ โ”‚ ... โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 1.3 ํ™”๋ฉด ๊ตฌ์กฐ ๊ฐœ์š” + +- **ํ™”๋ฉด ์œ ํ˜•**: [๋ชฉ๋กํ˜• / ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ / ๋‹จ์ผ ํผ / ๋ณตํ•ฉ] +- **์ฃผ์š” ๊ธฐ๋Šฅ**: [CRUD / ์กฐํšŒ / ์ง‘๊ณ„ ๋“ฑ] + +--- + +## 2. ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜๋„ + +### 2.1 ์ „์ฒด ๋ ˆ์ด์•„์›ƒ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๊ฒ€์ƒ‰ ์˜์—ญ] v2-table-search-widget โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ [๋ฉ”์ธ ํ…Œ์ด๋ธ”] v2-table-list โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [๋ฒ„ํŠผ ์˜์—ญ] v2-button-primary (์‹ ๊ทœ, ์ €์žฅ, ์‚ญ์ œ) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2.2 ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก + +| ์ปดํฌ๋„ŒํŠธ ID | ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… | ์—ญํ•  | +|-------------|---------------|------| +| `search-widget` | v2-table-search-widget | ๊ฒ€์ƒ‰ ํ•„ํ„ฐ | +| `main-table` | v2-table-list | ๋ฐ์ดํ„ฐ ๋ชฉ๋ก | +| `btn-new` | v2-button-primary | ์‹ ๊ทœ ๋“ฑ๋ก | +| `btn-save` | v2-button-primary | ์ €์žฅ | +| `btn-delete` | v2-button-primary | ์‚ญ์ œ | + +--- + +## 3. ๊ฐ ์ปดํฌ๋„ŒํŠธ๋ณ„ ์„ค์ • + +### 3.1 v2-table-search-widget + +```json +{ + "targetTableId": "main-table", + "searchFields": [ + { + "field": "name", + "label": "์ด๋ฆ„", + "type": "text" + }, + { + "field": "status", + "label": "์ƒํƒœ", + "type": "select", + "options": [ + { "value": "active", "label": "ํ™œ์„ฑ" }, + { "value": "inactive", "label": "๋น„ํ™œ์„ฑ" } + ] + } + ] +} +``` + +### 3.2 v2-table-list + +```json +{ + "tableName": "master_table", + "columns": [ + { + "field": "id", + "headerName": "ID", + "width": 80, + "visible": false + }, + { + "field": "name", + "headerName": "์ด๋ฆ„", + "width": 150 + }, + { + "field": "status", + "headerName": "์ƒํƒœ", + "width": 100 + } + ], + "features": { + "checkbox": true, + "pagination": true, + "sorting": true + }, + "pagination": { + "pageSize": 20 + } +} +``` + +### 3.3 v2-button-primary (์ €์žฅ) + +```json +{ + "label": "์ €์žฅ", + "actionType": "save", + "variant": "default", + "afterSaveActions": ["refreshTable"] +} +``` + +--- + +## 4. ์ปดํฌ๋„ŒํŠธ ์—ฐ๋™ ์„ค์ • + +### 4.1 ์ด๋ฒคํŠธ ํ๋ฆ„ + +``` +[๊ฒ€์ƒ‰ ์ž…๋ ฅ] + โ”‚ + โ–ผ +v2-table-search-widget + โ”‚ onFilterChange + โ–ผ +v2-table-list (์ž๋™ ์žฌ์กฐํšŒ) + โ”‚ + โ–ผ +[๋ฐ์ดํ„ฐ ํ‘œ์‹œ] +``` + +### 4.2 ์—ฐ๋™ ์„ค์ • + +| ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ | ์ด๋ฒคํŠธ/์•ก์…˜ | ๋Œ€์ƒ ์ปดํฌ๋„ŒํŠธ | ๋™์ž‘ | +|---------------|-------------|---------------|------| +| search-widget | onFilterChange | main-table | ํ•„ํ„ฐ ์ ์šฉ | +| btn-save | click | main-table | refreshTable | + +--- + +## 5. ์‚ฌ์šฉ์ž ์‚ฌ์šฉ ์˜ˆ์‹œ ์‹œ๋‚˜๋ฆฌ์˜ค + +### ์‹œ๋‚˜๋ฆฌ์˜ค 1: ๋ฐ์ดํ„ฐ ์กฐํšŒ + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|------|-------------|-----------| +| 1 | ํ™”๋ฉด ์ง„์ž… | ์ „์ฒด ๋ชฉ๋ก ํ‘œ์‹œ | +| 2 | ๊ฒ€์ƒ‰์–ด ์ž…๋ ฅ | ํ•„ํ„ฐ๋ง๋œ ๊ฒฐ๊ณผ ํ‘œ์‹œ | +| 3 | ์ •๋ ฌ ํด๋ฆญ | ์ •๋ ฌ ์ˆœ์„œ ๋ณ€๊ฒฝ | + +### ์‹œ๋‚˜๋ฆฌ์˜ค 2: ๋ฐ์ดํ„ฐ ๋“ฑ๋ก + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|------|-------------|-----------| +| 1 | [์‹ ๊ทœ] ๋ฒ„ํŠผ ํด๋ฆญ | ๋“ฑ๋ก ๋ชจ๋‹ฌ/ํผ ํ‘œ์‹œ | +| 2 | ๋ฐ์ดํ„ฐ ์ž…๋ ฅ | ์ž…๋ ฅ ํ•„๋“œ ์ฑ„์›€ | +| 3 | [์ €์žฅ] ๋ฒ„ํŠผ ํด๋ฆญ | ์ €์žฅ ์™„๋ฃŒ, ๋ชฉ๋ก ๊ฐฑ์‹  | + +### ์‹œ๋‚˜๋ฆฌ์˜ค 3: ๋ฐ์ดํ„ฐ ์ˆ˜์ • + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|------|-------------|-----------| +| 1 | ํ–‰ ๋”๋ธ”ํด๋ฆญ | ์ˆ˜์ • ๋ชจ๋‹ฌ/ํผ ํ‘œ์‹œ | +| 2 | ๋ฐ์ดํ„ฐ ์ˆ˜์ • | ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ | +| 3 | [์ €์žฅ] ๋ฒ„ํŠผ ํด๋ฆญ | ์ €์žฅ ์™„๋ฃŒ, ๋ชฉ๋ก ๊ฐฑ์‹  | + +### ์‹œ๋‚˜๋ฆฌ์˜ค 4: ๋ฐ์ดํ„ฐ ์‚ญ์ œ + +| ๋‹จ๊ณ„ | ์‚ฌ์šฉ์ž ๋™์ž‘ | ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ | +|------|-------------|-----------| +| 1 | ํ–‰ ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ | ์„ ํƒ ํ‘œ์‹œ | +| 2 | [์‚ญ์ œ] ๋ฒ„ํŠผ ํด๋ฆญ | ์‚ญ์ œ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ | +| 3 | ํ™•์ธ | ์‚ญ์ œ ์™„๋ฃŒ, ๋ชฉ๋ก ๊ฐฑ์‹  | + +--- + +## 6. ๊ฒ€์ฆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +- [ ] ๋ฐ์ดํ„ฐ ์กฐํšŒ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ๊ฒ€์ƒ‰ ํ•„ํ„ฐ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์‹ ๊ทœ ๋“ฑ๋ก์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ˆ˜์ •์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์‚ญ์ œ๊ฐ€ ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ํŽ˜์ด์ง€๋„ค์ด์…˜์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? +- [ ] ์ •๋ ฌ์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”๊ฐ€? + +--- + +## 7. ์ฐธ๊ณ  ์‚ฌํ•ญ + +- ๊ด€๋ จ ํ™”๋ฉด: [๊ด€๋ จ ํ™”๋ฉด๋ช…](./related-screen.md) +- ํŠน์ด ์‚ฌํ•ญ: ์—†์Œ diff --git a/docs/screen-implementation-guide/ํ™”๋ฉด๊ฐœ๋ฐœ_ํ‘œ์ค€_๊ฐ€์ด๋“œ.md b/docs/screen-implementation-guide/ํ™”๋ฉด๊ฐœ๋ฐœ_ํ‘œ์ค€_๊ฐ€์ด๋“œ.md new file mode 100644 index 00000000..83774f38 --- /dev/null +++ b/docs/screen-implementation-guide/ํ™”๋ฉด๊ฐœ๋ฐœ_ํ‘œ์ค€_๊ฐ€์ด๋“œ.md @@ -0,0 +1,706 @@ +# ํ™”๋ฉด ๊ฐœ๋ฐœ ํ‘œ์ค€ ๊ฐ€์ด๋“œ + +> **๋ชฉ์ **: ์–ด๋–ค ๊ฐœ๋ฐœ์ž/AI๊ฐ€ ํ™”๋ฉด์„ ๊ฐœ๋ฐœํ•˜๋“  ๋™์ผํ•œ ๊ฒฐ๊ณผ๋ฌผ์ด ๋‚˜์˜ค๋„๋ก ํ•˜๋Š” ํ‘œ์ค€ ๊ฐ€์ด๋“œ +> **๋Œ€์ƒ**: ๊ฐœ๋ฐœ์ž, AI ์—์ด์ „ํŠธ (Cursor ๋“ฑ) +> **๋ฒ„์ „**: 1.0.0 + +--- + +## 1. ๊ฐœ์š” + +์ด ๋ฌธ์„œ๋Š” WACE ์†”๋ฃจ์…˜์—์„œ ํ™”๋ฉด์„ ๊ฐœ๋ฐœํ•  ๋•Œ ๋ฐ˜๋“œ์‹œ ๋”ฐ๋ผ์•ผ ํ•˜๋Š” ํ‘œ์ค€์ž…๋‹ˆ๋‹ค. +๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์–ด๋–ป๊ฒŒ ์„ค๋ช…ํ•˜๋“ , ์ตœ์ข… ๊ฒฐ๊ณผ๋ฌผ์€ ์ด ๊ฐ€์ด๋“œ๋Œ€๋กœ ์ƒ์„ฑ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + +### ํ•ต์‹ฌ ์›์น™ + +1. **V2 ์ปดํฌ๋„ŒํŠธ๋งŒ ์‚ฌ์šฉ**: `v2-` ์ ‘๋‘์‚ฌ๊ฐ€ ๋ถ™์€ ์ปดํฌ๋„ŒํŠธ๋งŒ ์‚ฌ์šฉ +2. **UI์™€ ๋กœ์ง ๋ถ„๋ฆฌ**: UI๋Š” `screen_layouts_v2`, ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ `dataflow_diagrams` +3. **๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„์ˆ˜**: ๋ชจ๋“  ์ฟผ๋ฆฌ์— `company_code` ํ•„ํ„ฐ๋ง + +--- + +## 2. ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ V2 ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก (23๊ฐœ) + +### ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| `v2-input` | ์ž…๋ ฅ | ํ…์ŠคํŠธ, ์ˆซ์ž, ๋น„๋ฐ€๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ ๋“ฑ | +| `v2-select` | ์„ ํƒ | ๋“œ๋กญ๋‹ค์šด, ์ฝค๋ณด๋ฐ•์Šค, ๋ผ๋””์˜ค, ์ฒดํฌ๋ฐ•์Šค | +| `v2-date` | ๋‚ ์งœ | ๋‚ ์งœ, ์‹œ๊ฐ„, ๋‚ ์งœ๋ฒ”์œ„ | + +### ํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| `v2-text-display` | ํ…์ŠคํŠธ ํ‘œ์‹œ | ๋ผ๋ฒจ, ์ œ๋ชฉ | +| `v2-card-display` | ์นด๋“œ ๋””์Šคํ”Œ๋ ˆ์ด | ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ๋ฅผ ์นด๋“œ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ | +| `v2-aggregation-widget` | ์ง‘๊ณ„ ์œ„์ ฏ | ํ•ฉ๊ณ„, ํ‰๊ท , ๊ฐœ์ˆ˜ ๋“ฑ | + +### ํ…Œ์ด๋ธ”/๋ฐ์ดํ„ฐ ์ปดํฌ๋„ŒํŠธ + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| `v2-table-list` | ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | ๋ฐ์ดํ„ฐ ์กฐํšŒ/ํŽธ์ง‘ ํ…Œ์ด๋ธ” | +| `v2-table-search-widget` | ๊ฒ€์ƒ‰ ํ•„ํ„ฐ | ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰/ํ•„ํ„ฐ | +| `v2-pivot-grid` | ํ”ผ๋ฒ— ๊ทธ๋ฆฌ๋“œ | ๋‹ค์ฐจ์› ๋ถ„์„ | + +### ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| `v2-split-panel-layout` | ๋ถ„ํ•  ํŒจ๋„ | ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ์ขŒ์šฐ ๋ถ„ํ•  | +| `v2-tabs-widget` | ํƒญ ์œ„์ ฏ | ํƒญ ์ „ํ™˜ | +| `v2-section-card` | ์„น์…˜ ์นด๋“œ | ์ œ๋ชฉ+ํ…Œ๋‘๋ฆฌ ๊ทธ๋ฃนํ™” | +| `v2-section-paper` | ์„น์…˜ ํŽ˜์ดํผ | ๋ฐฐ๊ฒฝ์ƒ‰ ๊ทธ๋ฃนํ™” | +| `v2-divider-line` | ๊ตฌ๋ถ„์„  | ์˜์—ญ ๊ตฌ๋ถ„ | +| `v2-repeat-container` | ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ | ๋ฐ์ดํ„ฐ ๋ฐ˜๋ณต ๋ Œ๋”๋ง | +| `v2-repeater` | ๋ฆฌํ”ผํ„ฐ | ๋ฐ˜๋ณต ์ปจํŠธ๋กค | +| `v2-repeat-screen-modal` | ๋ฐ˜๋ณต ํ™”๋ฉด ๋ชจ๋‹ฌ | ๋ชจ๋‹ฌ ๋ฐ˜๋ณต | + +### ์•ก์…˜/ํŠน์ˆ˜ ์ปดํฌ๋„ŒํŠธ + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| `v2-button-primary` | ๊ธฐ๋ณธ ๋ฒ„ํŠผ | ์ €์žฅ, ์‚ญ์ œ ๋“ฑ ์•ก์…˜ | +| `v2-numbering-rule` | ์ฑ„๋ฒˆ๊ทœ์น™ | ์ž๋™ ์ฝ”๋“œ ์ƒ์„ฑ | +| `v2-category-manager` | ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ์ž | ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ | +| `v2-location-swap-selector` | ์œ„์น˜ ๊ตํ™˜ | ์œ„์น˜ ์„ ํƒ/๊ตํ™˜ | +| `v2-rack-structure` | ๋ž™ ๊ตฌ์กฐ | ์ฐฝ๊ณ  ๋ž™ ์‹œ๊ฐํ™” | +| `v2-media` | ๋ฏธ๋””์–ด | ์ด๋ฏธ์ง€/๋™์˜์ƒ ํ‘œ์‹œ | + +--- + +## 3. ํ™”๋ฉด ํŒจํ„ด (5๊ฐ€์ง€) + +### ํŒจํ„ด A: ๊ธฐ๋ณธ ๋งˆ์Šคํ„ฐ ํ™”๋ฉด + +**์‚ฌ์šฉ ์กฐ๊ฑด**: ๋‹จ์ผ ํ…Œ์ด๋ธ” CRUD + +**์ปดํฌ๋„ŒํŠธ ๊ตฌ์„ฑ**: +``` +v2-table-search-widget (๊ฒ€์ƒ‰) +v2-table-list (ํ…Œ์ด๋ธ”) +v2-button-primary (์ €์žฅ/์‚ญ์ œ) +``` + +**๋ ˆ์ด์•„์›ƒ**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๊ฒ€์ƒ‰ํ•„๋“œ๋“ค] [์กฐํšŒ] [์—‘์…€] โ”‚ โ† v2-table-search-widget +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ์ œ๋ชฉ [์‹ ๊ทœ] [์‚ญ์ œ] โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ โ–ก | ์ฝ”๋“œ | ์ด๋ฆ„ | ์ƒํƒœ | ๋“ฑ๋ก์ผ | โ”‚ โ† v2-table-list +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +### ํŒจํ„ด B: ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ํ™”๋ฉด + +**์‚ฌ์šฉ ์กฐ๊ฑด**: ๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ” ์„ ํƒ โ†’ ๋””ํ…Œ์ผ ํ…Œ์ด๋ธ” ํ‘œ์‹œ + +**์ปดํฌ๋„ŒํŠธ ๊ตฌ์„ฑ**: +``` +v2-split-panel-layout (๋ถ„ํ• ) + โ”œโ”€ ์ขŒ์ธก: v2-table-list (๋งˆ์Šคํ„ฐ) + โ””โ”€ ์šฐ์ธก: v2-table-list (๋””ํ…Œ์ผ) +``` + +**๋ ˆ์ด์•„์›ƒ**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๋งˆ์Šคํ„ฐ ๋ฆฌ์ŠคํŠธ โ”‚ ๋””ํ…Œ์ผ ๋ฆฌ์ŠคํŠธ โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”‚ +โ”‚ โ–ก A001 ํ•ญ๋ชฉ1 โ”‚ [๋””ํ…Œ์ผ ํ…Œ์ด๋ธ”] โ”‚ +โ”‚ โ–ก A002 ํ•ญ๋ชฉ2 โ† โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + v2-split-panel-layout +``` + +**ํ•„์ˆ˜ ์„ค์ •**: +```json +{ + "leftPanel": { + "tableName": "๋งˆ์Šคํ„ฐ_ํ…Œ์ด๋ธ”๋ช…" + }, + "rightPanel": { + "tableName": "๋””ํ…Œ์ผ_ํ…Œ์ด๋ธ”๋ช…", + "relation": { + "type": "detail", + "foreignKey": "master_id" + } + }, + "splitRatio": 30 +} +``` + +--- + +### ํŒจํ„ด C: ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ + ํƒญ + +**์‚ฌ์šฉ ์กฐ๊ฑด**: ๋งˆ์Šคํ„ฐ ์„ ํƒ โ†’ ์—ฌ๋Ÿฌ ํƒญ์œผ๋กœ ์ƒ์„ธ ์ •๋ณด ํ‘œ์‹œ + +**์ปดํฌ๋„ŒํŠธ ๊ตฌ์„ฑ**: +``` +v2-split-panel-layout (๋ถ„ํ• ) + โ”œโ”€ ์ขŒ์ธก: v2-table-list (๋งˆ์Šคํ„ฐ) + โ””โ”€ ์šฐ์ธก: v2-tabs-widget (ํƒญ) + โ”œโ”€ ํƒญ1: v2-table-list + โ”œโ”€ ํƒญ2: v2-table-list + โ””โ”€ ํƒญ3: ํผ ์ปดํฌ๋„ŒํŠธ๋“ค +``` + +--- + +### ํŒจํ„ด D: ์นด๋“œ ๋ทฐ + +**์‚ฌ์šฉ ์กฐ๊ฑด**: ์ด๋ฏธ์ง€+์ •๋ณด ์นด๋“œ ํ˜•ํƒœ ํ‘œ์‹œ + +**์ปดํฌ๋„ŒํŠธ ๊ตฌ์„ฑ**: +``` +v2-table-search-widget (๊ฒ€์ƒ‰) +v2-card-display (์นด๋“œ) +``` + +**ํ•„์ˆ˜ ์„ค์ •**: +```json +{ + "cardsPerRow": 3, + "columnMapping": { + "title": "name", + "subtitle": "code", + "image": "image_url", + "status": "status" + } +} +``` + +--- + +### ํŒจํ„ด E: ํ”ผ๋ฒ— ๋ถ„์„ + +**์‚ฌ์šฉ ์กฐ๊ฑด**: ๋‹ค์ฐจ์› ์ง‘๊ณ„/๋ถ„์„ + +**์ปดํฌ๋„ŒํŠธ ๊ตฌ์„ฑ**: +``` +v2-pivot-grid (ํ”ผ๋ฒ—) +``` + +**ํ•„์ˆ˜ ์„ค์ •**: +```json +{ + "fields": [ + { "field": "region", "area": "row" }, + { "field": "year", "area": "column" }, + { "field": "amount", "area": "data", "summaryType": "sum" } + ] +} +``` + +--- + +## 4. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ตฌ์กฐ + +### ํ™”๋ฉด ์ •์˜ ํ…Œ์ด๋ธ” + +```sql +-- screen_definitions: ํ™”๋ฉด ๊ธฐ๋ณธ ์ •๋ณด +INSERT INTO screen_definitions ( + screen_id, + screen_name, + screen_code, + description, + table_name, + company_code +) VALUES (...); + +-- screen_layouts_v2: UI ๋ ˆ์ด์•„์›ƒ (JSON) +INSERT INTO screen_layouts_v2 ( + screen_id, + company_code, + layout_data -- JSON: ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ ์ •๋ณด +) VALUES (...); + +-- screen_menu_assignments: ๋ฉ”๋‰ด ์—ฐ๊ฒฐ +INSERT INTO screen_menu_assignments ( + screen_id, + menu_objid, + company_code +) VALUES (...); +``` + +### ์ œ์–ด๊ด€๋ฆฌ ํ…Œ์ด๋ธ” + +```sql +-- dataflow_diagrams: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง +INSERT INTO dataflow_diagrams ( + diagram_name, + company_code, + control, -- JSON: ์กฐ๊ฑด ์„ค์ • + plan -- JSON: ์‹คํ–‰ ๊ณ„ํš +) VALUES (...); +``` + +--- + +## 5. UI ์„ค์ • vs ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์„ค์ • + +### UI ์„ค์ • (ํ™”๋ฉด ๋””์ž์ด๋„ˆ์—์„œ ์ฒ˜๋ฆฌ) + +| ํ•ญ๋ชฉ | ์ €์žฅ ์œ„์น˜ | +|------|----------| +| ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ | screen_layouts_v2.layout_data | +| ํ…Œ์ด๋ธ”๋ช… | layout_data ๋‚ด tableName | +| ์ปฌ๋Ÿผ ํ‘œ์‹œ/์ˆจ๊น€ | layout_data ๋‚ด columns | +| ๊ฒ€์ƒ‰ ํ•„๋“œ | layout_data ๋‚ด searchFields | +| ๊ธฐ๋ณธ ์ €์žฅ/์‚ญ์ œ | ๋ฒ„ํŠผ config.action.type | + +### ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง (์ œ์–ด๊ด€๋ฆฌ์—์„œ ์ฒ˜๋ฆฌ) + +| ํ•ญ๋ชฉ | ์ €์žฅ ์œ„์น˜ | +|------|----------| +| ์กฐ๊ฑด๋ถ€ ์‹คํ–‰ | dataflow_diagrams.control | +| ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ €์žฅ | dataflow_diagrams.plan | +| ๋ฒ„ํŠผ ์ „/ํ›„ ํŠธ๋ฆฌ๊ฑฐ | dataflow_diagrams.control.triggerType | +| ํ•„๋“œ ๋งคํ•‘ | dataflow_diagrams.plan.mappings | + +--- + +## 6. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์„ค์ • ํ‘œ์ค€ ํ˜•์‹ + +### ๊ธฐ๋ณธ ๊ตฌ์กฐ + +```json +{ + "control": { + "actionType": "update", + "triggerType": "after", + "conditions": [ + { + "id": "์กฐ๊ฑดID", + "type": "condition", + "field": "status", + "operator": "=", + "value": "๋Œ€๊ธฐ", + "dataType": "string" + } + ] + }, + "plan": { + "mappings": [ + { + "id": "๋งคํ•‘ID", + "sourceField": "์†Œ์Šคํ•„๋“œ", + "targetField": "ํƒ€๊ฒŸํ•„๋“œ", + "targetTable": "ํƒ€๊ฒŸํ…Œ์ด๋ธ”", + "valueType": "field" + } + ] + } +} +``` + +### ์กฐ๊ฑด ์—ฐ์‚ฐ์ž + +| ์—ฐ์‚ฐ์ž | ์„ค๋ช… | +|--------|------| +| `=` | ๊ฐ™์Œ | +| `!=` | ๋‹ค๋ฆ„ | +| `>` | ํผ | +| `<` | ์ž‘์Œ | +| `>=` | ํฌ๊ฑฐ๋‚˜ ๊ฐ™์Œ | +| `<=` | ์ž‘๊ฑฐ๋‚˜ ๊ฐ™์Œ | +| `LIKE` | ํฌํ•จ | +| `IN` | ๋ชฉ๋ก์— ํฌํ•จ | +| `IS NULL` | NULL ๊ฐ’ | + +### ์•ก์…˜ ํƒ€์ž… + +| ํƒ€์ž… | ์„ค๋ช… | +|------|------| +| `insert` | ์ƒˆ ๋ฐ์ดํ„ฐ ์‚ฝ์ž… | +| `update` | ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์ˆ˜์ • | +| `delete` | ๋ฐ์ดํ„ฐ ์‚ญ์ œ | + +### ํŠธ๋ฆฌ๊ฑฐ ํƒ€์ž… + +| ํƒ€์ž… | ์„ค๋ช… | +|------|------| +| `before` | ๋ฒ„ํŠผ ํด๋ฆญ ์ „ ์‹คํ–‰ | +| `after` | ๋ฒ„ํŠผ ํด๋ฆญ ํ›„ ์‹คํ–‰ | + +--- + +## 7. ํ™”๋ฉด ๊ฐœ๋ฐœ ์ˆœ์„œ + +### Step 1: ์š”๊ตฌ์‚ฌํ•ญ ๋ถ„์„ + +``` +1. ์–ด๋–ค ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•˜๋Š”๊ฐ€? +2. ํ…Œ์ด๋ธ” ๊ฐ„ ๊ด€๊ณ„๋Š”? (FK) +3. ์–ด๋–ค ํŒจํ„ด์ธ๊ฐ€? (A/B/C/D/E) +4. ์–ด๋–ค ๋ฒ„ํŠผ์ด ํ•„์š”ํ•œ๊ฐ€? +5. ๊ฐ ๋ฒ„ํŠผ์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€? +``` + +### Step 2: screen_definitions INSERT + +```sql +INSERT INTO screen_definitions ( + screen_name, + screen_code, + description, + table_name, + company_code, + created_at +) VALUES ( + 'ํ™”๋ฉด๋ช…', + 'SCREEN_CODE', + 'ํ™”๋ฉด ์„ค๋ช…', + '๋ฉ”์ธํ…Œ์ด๋ธ”๋ช…', + 'ํšŒ์‚ฌ์ฝ”๋“œ', + NOW() +) RETURNING screen_id; +``` + +### Step 3: screen_layouts_v2 INSERT + +```sql +INSERT INTO screen_layouts_v2 ( + screen_id, + company_code, + layout_data +) VALUES ( + ์œ„์—์„œ_๋ฐ›์€_screen_id, + 'ํšŒ์‚ฌ์ฝ”๋“œ', + '{"components": [...], "layout": {...}}'::jsonb +); +``` + +### Step 4: dataflow_diagrams INSERT (๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์žˆ๋Š” ๊ฒฝ์šฐ) + +```sql +INSERT INTO dataflow_diagrams ( + diagram_name, + company_code, + control, + plan +) VALUES ( + 'ํ™”๋ฉด๋ช…_์ œ์–ด', + 'ํšŒ์‚ฌ์ฝ”๋“œ', + '{"์กฐ๊ฑด์„ค์ •"}'::jsonb, + '{"์‹คํ–‰๊ณ„ํš"}'::jsonb +); +``` + +### Step 5: screen_menu_assignments INSERT + +```sql +INSERT INTO screen_menu_assignments ( + screen_id, + menu_objid, + company_code +) VALUES ( + screen_id, + ๋ฉ”๋‰ดID, + 'ํšŒ์‚ฌ์ฝ”๋“œ' +); +``` + +--- + +## 8. ์˜ˆ์‹œ: ์ˆ˜์ฃผ๊ด€๋ฆฌ ํ™”๋ฉด + +### ์š”๊ตฌ์‚ฌํ•ญ + +``` +ํ™”๋ฉด๋ช…: ์ˆ˜์ฃผ๊ด€๋ฆฌ +ํŒจํ„ด: B (๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ) +ํ…Œ์ด๋ธ”: + - ๋งˆ์Šคํ„ฐ: order_master + - ๋””ํ…Œ์ผ: order_detail +๋ฒ„ํŠผ: + - [์ €์žฅ]: order_master์— ์ €์žฅ + - [ํ™•์ •]: + - ์กฐ๊ฑด: status = '๋Œ€๊ธฐ' + - ๋™์ž‘: status๋ฅผ 'ํ™•์ •'์œผ๋กœ ๋ณ€๊ฒฝ + - ์ถ”๊ฐ€: order_history์— ์ด๋ ฅ ์ €์žฅ +``` + +### screen_definitions + +```sql +INSERT INTO screen_definitions ( + screen_name, screen_code, description, table_name, company_code +) VALUES ( + '์ˆ˜์ฃผ๊ด€๋ฆฌ', 'ORDER_MNG', '์ˆ˜์ฃผ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ํ™”๋ฉด', 'order_master', 'COMPANY_A' +); +``` + +### screen_layouts_v2 (layout_data) + +```json +{ + "components": [ + { + "id": "search-1", + "componentType": "v2-table-search-widget", + "position": {"x": 0, "y": 0}, + "size": {"width": 1920, "height": 80} + }, + { + "id": "split-1", + "componentType": "v2-split-panel-layout", + "position": {"x": 0, "y": 80}, + "size": {"width": 1920, "height": 800}, + "componentConfig": { + "leftPanel": { + "tableName": "order_master" + }, + "rightPanel": { + "tableName": "order_detail", + "relation": { + "type": "detail", + "foreignKey": "order_id" + } + }, + "splitRatio": 30 + } + }, + { + "id": "btn-save", + "componentType": "v2-button-primary", + "componentConfig": { + "text": "์ €์žฅ", + "action": {"type": "save"} + } + }, + { + "id": "btn-confirm", + "componentType": "v2-button-primary", + "componentConfig": { + "text": "ํ™•์ •", + "enableDataflowControl": true, + "dataflowDiagramId": 123 + } + } + ] +} +``` + +### dataflow_diagrams (ํ™•์ • ๋ฒ„ํŠผ ๋กœ์ง) + +```json +{ + "control": { + "actionType": "update", + "triggerType": "after", + "conditions": [ + { + "id": "cond-1", + "type": "condition", + "field": "status", + "operator": "=", + "value": "๋Œ€๊ธฐ", + "dataType": "string" + } + ] + }, + "plan": { + "actions": [ + { + "id": "action-1", + "actionType": "update", + "targetTable": "order_master", + "fieldMappings": [ + {"targetField": "status", "defaultValue": "ํ™•์ •"} + ] + }, + { + "id": "action-2", + "actionType": "insert", + "targetTable": "order_history", + "fieldMappings": [ + {"sourceField": "order_no", "targetField": "order_no"}, + {"sourceField": "customer_name", "targetField": "customer_name"}, + {"defaultValue": "#NOW", "targetField": "confirmed_at"} + ] + } + ] + } +} +``` + +--- + +## 9. ์ง€์›ํ•˜์ง€ ์•Š๋Š” UI (๋ณ„๋„ ๊ฐœ๋ฐœ ํ•„์š”) + +| UI ์œ ํ˜• | ์ƒํƒœ | ๋Œ€์•ˆ | +|---------|------|------| +| ํŠธ๋ฆฌ ๋ทฐ | โŒ ๋ฏธ์ง€์› | ํ…Œ์ด๋ธ”๋กœ ๋Œ€์ฒด or `v2-tree-view` ๊ฐœ๋ฐœ ํ•„์š” | +| ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” | โŒ ๋ฏธ์ง€์› | ์ผ๋ฐ˜ ํ…Œ์ด๋ธ”๋กœ ๋Œ€์ฒด or `v2-grouped-table` ๊ฐœ๋ฐœ ํ•„์š” | +| ๊ฐ„ํŠธ ์ฐจํŠธ | โŒ ๋ฏธ์ง€์› | ๋ณ„๋„ ๊ฐœ๋ฐœ ํ•„์š” | +| ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ | โŒ ๋ฏธ์ง€์› | ์ˆœ์„œ ์ปฌ๋Ÿผ์œผ๋กœ ๋Œ€์ฒด | + +--- + +## 10. ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### ํ™”๋ฉด ์ƒ์„ฑ ์‹œ + +``` +โ–ก screen_definitions INSERT ์™„๋ฃŒ +โ–ก screen_layouts_v2 INSERT ์™„๋ฃŒ +โ–ก screen_menu_assignments INSERT ์™„๋ฃŒ (๋ฉ”๋‰ด ์—ฐ๊ฒฐ ํ•„์š” ์‹œ) +โ–ก company_code ํ•„ํ„ฐ๋ง ์ ์šฉ +โ–ก ์‚ฌ์šฉํ•œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ชจ๋‘ v2- ์ ‘๋‘์‚ฌ์ธ์ง€ ํ™•์ธ +``` + +### ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์„ค์ • ์‹œ + +``` +โ–ก ๊ธฐ๋ณธ ์•ก์…˜ (์ €์žฅ/์‚ญ์ œ)๋งŒ โ†’ ํ™”๋ฉด ๋””์ž์ด๋„ˆ์—์„œ ์„ค์ • +โ–ก ์กฐ๊ฑด๋ถ€/๋‹ค์ค‘ํ…Œ์ด๋ธ” โ†’ dataflow_diagrams INSERT +โ–ก ๋ฒ„ํŠผ config์— dataflowDiagramId ์—ฐ๊ฒฐ +โ–ก control.conditions ์„ค์ • +โ–ก plan.actions ๋˜๋Š” plan.mappings ์„ค์ • +``` + +--- + +## 11. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์š”์ฒญ ์–‘์‹ (ํ•„์ˆ˜) + +> **๊ฒฝ๊ณ **: ์–‘์‹๋Œ€๋กœ ์•ˆ ์“ฐ๋ฉด ์ฒ˜๋ฆฌ ์•ˆ ํ•จ. ๋ณ‘์‹ ์•„ ์ œ๋Œ€๋กœ ์จ. +> ๋Œ€์ถฉ ์“ฐ๋ฉด ๋Œ€์ถฉ ๋งŒ๋“ค์–ด์ง€๊ณ , ์•ˆ ์“ฐ๋ฉด ์•ˆ ๋งŒ๋“ค์–ด์คŒ. + +### 11.1 ์–‘์‹ ํ…œํ”Œ๋ฆฟ + +``` +=== ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์š”์ฒญ์„œ === + +ใ€ํ™”๋ฉด ์ •๋ณดใ€‘ +- ํ™”๋ฉด๋ช…: +- ํšŒ์‚ฌ์ฝ”๋“œ: +- ๋ฉ”๋‰ดID (์žˆ์œผ๋ฉด): + +ใ€ํ…Œ์ด๋ธ” ์ •๋ณดใ€‘ +- ๋ฉ”์ธ ํ…Œ์ด๋ธ”: +- ๋””ํ…Œ์ผ ํ…Œ์ด๋ธ” (์žˆ์œผ๋ฉด): +- ๊ด€๊ณ„ FK (์žˆ์œผ๋ฉด): + +ใ€๋ฒ„ํŠผ ๋ชฉ๋กใ€‘ +๋ฒ„ํŠผ1: + - ๋ฒ„ํŠผ๋ช…: + - ๋™์ž‘ ์œ ํ˜•: (์ €์žฅ/์‚ญ์ œ/์ˆ˜์ •/์กฐํšŒ/๊ธฐํƒ€) + - ์กฐ๊ฑด (์žˆ์œผ๋ฉด): + - ๋Œ€์ƒ ํ…Œ์ด๋ธ”: + - ์ถ”๊ฐ€ ๋™์ž‘ (์žˆ์œผ๋ฉด): + +๋ฒ„ํŠผ2: + - ๋ฒ„ํŠผ๋ช…: + - ... + +ใ€์ถ”๊ฐ€ ์š”๊ตฌ์‚ฌํ•ญใ€‘ +- +``` + +### 11.2 ์ž‘์„ฑ ์˜ˆ์‹œ (์˜ฌ๋ฐ”๋ฅธ ์˜ˆ์‹œ) + +``` +=== ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์š”์ฒญ์„œ === + +ใ€ํ™”๋ฉด ์ •๋ณดใ€‘ +- ํ™”๋ฉด๋ช…: ์ˆ˜์ฃผ๊ด€๋ฆฌ +- ํšŒ์‚ฌ์ฝ”๋“œ: ssalmeog +- ๋ฉ”๋‰ดID: 55566 + +ใ€ํ…Œ์ด๋ธ” ์ •๋ณดใ€‘ +- ๋ฉ”์ธ ํ…Œ์ด๋ธ”: order_master +- ๋””ํ…Œ์ผ ํ…Œ์ด๋ธ”: order_detail +- ๊ด€๊ณ„ FK: order_id + +ใ€๋ฒ„ํŠผ ๋ชฉ๋กใ€‘ +๋ฒ„ํŠผ1: + - ๋ฒ„ํŠผ๋ช…: ์ €์žฅ + - ๋™์ž‘ ์œ ํ˜•: ์ €์žฅ + - ์กฐ๊ฑด: ์—†์Œ + - ๋Œ€์ƒ ํ…Œ์ด๋ธ”: order_master, order_detail + - ์ถ”๊ฐ€ ๋™์ž‘: ์—†์Œ + +๋ฒ„ํŠผ2: + - ๋ฒ„ํŠผ๋ช…: ํ™•์ • + - ๋™์ž‘ ์œ ํ˜•: ์ˆ˜์ • + - ์กฐ๊ฑด: status = '๋Œ€๊ธฐ' + - ๋Œ€์ƒ ํ…Œ์ด๋ธ”: order_master + - ์ถ”๊ฐ€ ๋™์ž‘: + 1. status๋ฅผ 'ํ™•์ •'์œผ๋กœ ๋ณ€๊ฒฝ + 2. order_history์— ์ด๋ ฅ INSERT (order_no, customer_name, confirmed_at=ํ˜„์žฌ์‹œ๊ฐ„) + +๋ฒ„ํŠผ3: + - ๋ฒ„ํŠผ๋ช…: ์‚ญ์ œ + - ๋™์ž‘ ์œ ํ˜•: ์‚ญ์ œ + - ์กฐ๊ฑด: status != 'ํ™•์ •' + - ๋Œ€์ƒ ํ…Œ์ด๋ธ”: order_master, order_detail (cascade) + - ์ถ”๊ฐ€ ๋™์ž‘: ์—†์Œ + +ใ€์ถ”๊ฐ€ ์š”๊ตฌ์‚ฌํ•ญใ€‘ +- ํ™•์ •๋œ ์ˆ˜์ฃผ๋Š” ์ˆ˜์ •/์‚ญ์ œ ๋ถˆ๊ฐ€ +- ์ˆ˜์ฃผ๋ฒˆํ˜ธ ์ž๋™์ฑ„๋ฒˆ (ORDER-YYYYMMDD-0001) +``` + +### 11.3 ์ž˜๋ชป๋œ ์˜ˆ์‹œ (์ด๋ ‡๊ฒŒ ์“ฐ๋ฉด ์•ˆ ๋จ) + +``` +โŒ "์ˆ˜์ฃผ๊ด€๋ฆฌ ํ™”๋ฉด ๋งŒ๋“ค์–ด์ค˜" + โ†’ ํ…Œ์ด๋ธ”์ด ๋ญ”๋ฐ? ๋ฒ„ํŠผ์€? ๋กœ์ง์€? + +โŒ "์ €์žฅ ๋ฒ„ํŠผ ๋ˆ„๋ฅด๋ฉด ์ €์žฅํ•ด์ค˜" + โ†’ ์–ด๋–ค ํ…Œ์ด๋ธ”์—? ์กฐ๊ฑด์€? + +โŒ "ํ™•์ •ํ•˜๋ฉด ์žฌ๊ณ  ์ฒ˜๋ฆฌํ•ด์ค˜" + โ†’ ์–ด๋–ค ํ…Œ์ด๋ธ”? ์ฆ๊ฐ€? ๊ฐ์†Œ? ์–ผ๋งˆ๋‚˜? + +โŒ "์ด์ „ ํ™”๋ฉด์ด๋ž‘ ๋น„์Šทํ•˜๊ฒŒ" + โ†’ ์ด์ „ ํ™”๋ฉด์ด ๋ญ”๋ฐ? +``` + +### 11.4 ๋ณต์žกํ•œ ๋กœ์ง ์ถ”๊ฐ€ ์–‘์‹ + +์กฐ๊ฑด์ด ์—ฌ๋Ÿฌ ๊ฐœ์ด๊ฑฐ๋‚˜ ๋ณต์žกํ•œ ๊ฒฝ์šฐ: + +``` +ใ€๋ณต์žกํ•œ ๋ฒ„ํŠผ ๋กœ์งใ€‘ +๋ฒ„ํŠผ๋ช…: ์ถœ๊ณ ํ™•์ • + +์‹คํ–‰ ์กฐ๊ฑด: + ์กฐ๊ฑด1: status = '์ถœ๊ณ ๋Œ€๊ธฐ' AND + ์กฐ๊ฑด2: qty > 0 AND + ์กฐ๊ฑด3: warehouse_id IS NOT NULL + +์‹คํ–‰ ๋™์ž‘ (์ˆœ์„œ๋Œ€๋กœ): + 1. shipment_master.status โ†’ '์ถœ๊ณ ์™„๋ฃŒ' + 2. inventory์—์„œ qty๋งŒํผ ๊ฐ์†Œ (WHERE item_code = ํ˜„์žฌํ–‰.item_code) + 3. shipment_history์— INSERT: + - shipment_no โ† ํ˜„์žฌํ–‰.shipment_no + - shipped_qty โ† ํ˜„์žฌํ–‰.qty + - shipped_at โ† ํ˜„์žฌ์‹œ๊ฐ„ + - shipped_by โ† ํ˜„์žฌ์‚ฌ์šฉ์ž + +์‹คํŒจ ์‹œ: + - ์žฌ๊ณ  ๋ถ€์กฑ: "์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ + - ์กฐ๊ฑด ๋ถˆ์ถฉ์กฑ: "์ถœ๊ณ ๋Œ€๊ธฐ ์ƒํƒœ๋งŒ ํ™•์ • ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ +``` + +--- + +## 12. ์ฐธ๊ณ  ๊ฒฝ๋กœ + +| ํ•ญ๋ชฉ | ๊ฒฝ๋กœ/ํ…Œ์ด๋ธ” | +|------|------------| +| ์ œ์–ด๊ด€๋ฆฌ ํŽ˜์ด์ง€ | `/admin/systemMng/dataflow` | +| ํ™”๋ฉด ์ •์˜ ํ…Œ์ด๋ธ” | `screen_definitions` | +| ๋ ˆ์ด์•„์›ƒ ํ…Œ์ด๋ธ” | `screen_layouts_v2` | +| ์ œ์–ด๊ด€๋ฆฌ ํ…Œ์ด๋ธ” | `dataflow_diagrams` | +| ๋ฉ”๋‰ด ์—ฐ๊ฒฐ ํ…Œ์ด๋ธ” | `screen_menu_assignments` | diff --git a/docs/v2-sales-order-modal-layout.json b/docs/v2-sales-order-modal-layout.json new file mode 100644 index 00000000..13e929e0 --- /dev/null +++ b/docs/v2-sales-order-modal-layout.json @@ -0,0 +1,573 @@ +{ + "version": "2.0", + "screenResolution": { + "width": 1400, + "height": 900, + "name": "์ˆ˜์ฃผ๋“ฑ๋ก ๋ชจ๋‹ฌ", + "category": "modal" + }, + "components": [ + { + "id": "section-options", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 20, "z": 1 }, + "size": { "width": 1360, "height": 80 }, + "overrides": { + "componentConfig": { + "title": "", + "showHeader": false, + "padding": "md", + "borderStyle": "solid" + } + }, + "displayOrder": 0 + }, + { + "id": "select-input-method", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 40, "y": 35, "z": 2 }, + "size": { "width": 300, "height": 40 }, + "overrides": { + "label": "์ž…๋ ฅ ๋ฐฉ์‹", + "columnName": "input_method", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "customer_first", "label": "๊ฑฐ๋ž˜์ฒ˜ ์šฐ์„ " }, + { "value": "item_first", "label": "ํ’ˆ๋ชฉ ์šฐ์„ " } + ], + "placeholder": "์ž…๋ ฅ ๋ฐฉ์‹ ์„ ํƒ" + }, + "displayOrder": 1 + }, + { + "id": "select-sales-type", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 360, "y": 35, "z": 2 }, + "size": { "width": 300, "height": 40 }, + "overrides": { + "label": "ํŒ๋งค ์œ ํ˜•", + "columnName": "sales_type", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "domestic", "label": "๊ตญ๋‚ด ํŒ๋งค" }, + { "value": "overseas", "label": "ํ•ด์™ธ ํŒ๋งค" } + ], + "placeholder": "ํŒ๋งค ์œ ํ˜• ์„ ํƒ" + }, + "displayOrder": 2 + }, + { + "id": "select-price-method", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 680, "y": 35, "z": 2 }, + "size": { "width": 250, "height": 40 }, + "overrides": { + "label": "๋‹จ๊ฐ€ ๋ฐฉ์‹", + "columnName": "price_method", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "standard", "label": "๊ธฐ์ค€ ๋‹จ๊ฐ€" }, + { "value": "contract", "label": "๊ณ„์•ฝ ๋‹จ๊ฐ€" }, + { "value": "custom", "label": "๊ฐœ๋ณ„ ์ž…๋ ฅ" } + ], + "placeholder": "๋‹จ๊ฐ€ ๋ฐฉ์‹" + }, + "displayOrder": 3 + }, + { + "id": "checkbox-price-edit", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 950, "y": 35, "z": 2 }, + "size": { "width": 150, "height": 40 }, + "overrides": { + "label": "๋‹จ๊ฐ€ ์ˆ˜์ • ํ—ˆ์šฉ", + "columnName": "allow_price_edit", + "mode": "check", + "source": "static", + "options": [{ "value": "Y", "label": "ํ—ˆ์šฉ" }] + }, + "displayOrder": 4 + }, + + { + "id": "section-customer-info", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 110, "z": 1 }, + "size": { "width": 1360, "height": 120 }, + "overrides": { + "componentConfig": { + "title": "๊ฑฐ๋ž˜์ฒ˜ ์ •๋ณด", + "showHeader": true, + "padding": "md", + "borderStyle": "solid" + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 5 + }, + { + "id": "select-customer", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 40, "y": 155, "z": 3 }, + "size": { "width": 320, "height": 40 }, + "overrides": { + "label": "๊ฑฐ๋ž˜์ฒ˜ *", + "columnName": "partner_id", + "mode": "dropdown", + "source": "entity", + "entityTable": "customer_mng", + "entityValueColumn": "customer_code", + "entityLabelColumn": "customer_name", + "searchable": true, + "placeholder": "๊ฑฐ๋ž˜์ฒ˜๋ช… ์ž…๋ ฅํ•˜์—ฌ ๊ฒ€์ƒ‰", + "required": true, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 6 + }, + { + "id": "input-manager", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 380, "y": 155, "z": 3 }, + "size": { "width": 240, "height": 40 }, + "overrides": { + "label": "๋‹ด๋‹น์ž", + "columnName": "manager_name", + "placeholder": "๋‹ด๋‹น์ž", + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 7 + }, + { + "id": "input-delivery-partner", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 640, "y": 155, "z": 3 }, + "size": { "width": 240, "height": 40 }, + "overrides": { + "label": "๋‚ฉํ’ˆ์ฒ˜", + "columnName": "delivery_partner_id", + "placeholder": "๋‚ฉํ’ˆ์ฒ˜", + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 8 + }, + { + "id": "input-delivery-address", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 900, "y": 155, "z": 3 }, + "size": { "width": 460, "height": 40 }, + "overrides": { + "label": "๋‚ฉํ’ˆ์žฅ์†Œ", + "columnName": "delivery_address", + "placeholder": "๋‚ฉํ’ˆ์žฅ์†Œ", + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 9 + }, + + { + "id": "section-item-first", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 110, "z": 1 }, + "size": { "width": 1360, "height": 200 }, + "overrides": { + "componentConfig": { + "title": "ํ’ˆ๋ชฉ ๋ฐ ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ์ˆ˜์ฃผ", + "showHeader": true, + "padding": "md", + "borderStyle": "solid" + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "item_first", + "action": "show" + } + }, + "displayOrder": 10 + }, + + { + "id": "section-items", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 240, "z": 1 }, + "size": { "width": 1360, "height": 280 }, + "overrides": { + "componentConfig": { + "title": "์ถ”๊ฐ€๋œ ํ’ˆ๋ชฉ", + "showHeader": true, + "padding": "md", + "borderStyle": "solid" + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 11 + }, + { + "id": "btn-item-search", + "url": "@/lib/registry/components/v2-button-primary", + "position": { "x": 1140, "y": 245, "z": 5 }, + "size": { "width": 100, "height": 36 }, + "overrides": { + "label": "ํ’ˆ๋ชฉ ๊ฒ€์ƒ‰", + "action": { + "type": "openModal", + "modalType": "itemSelection" + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 12 + }, + { + "id": "btn-shipping-plan", + "url": "@/lib/registry/components/v2-button-primary", + "position": { "x": 1250, "y": 245, "z": 5 }, + "size": { "width": 100, "height": 36 }, + "overrides": { + "label": "์ถœํ•˜๊ณ„ํš", + "webTypeConfig": { + "variant": "destructive" + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 13 + }, + { + "id": "repeater-items", + "url": "@/lib/registry/components/v2-repeater", + "position": { "x": 40, "y": 290, "z": 3 }, + "size": { "width": 1320, "height": 200 }, + "overrides": { + "renderMode": "modal", + "dataSource": { + "tableName": "sales_order_detail", + "foreignKey": "order_no", + "referenceKey": "order_no" + }, + "columns": [ + { "field": "part_code", "header": "ํ’ˆ๋ฒˆ", "width": 100 }, + { "field": "part_name", "header": "ํ’ˆ๋ช…", "width": 150 }, + { "field": "spec", "header": "๊ทœ๊ฒฉ", "width": 100 }, + { "field": "unit", "header": "๋‹จ์œ„", "width": 80 }, + { "field": "qty", "header": "์ˆ˜๋Ÿ‰", "width": 100, "editable": true }, + { + "field": "unit_price", + "header": "๋‹จ๊ฐ€", + "width": 100, + "editable": true + }, + { "field": "amount", "header": "๊ธˆ์•ก", "width": 100 }, + { + "field": "due_date", + "header": "๋‚ฉ๊ธฐ์ผ", + "width": 120, + "editable": true + } + ], + "modal": { + "sourceTable": "item_info", + "sourceColumns": [ + "part_code", + "part_name", + "spec", + "material", + "unit_price" + ], + "filterCondition": {} + }, + "features": { + "showAddButton": false, + "showDeleteButton": true, + "inlineEdit": true + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 14 + }, + + { + "id": "section-trade-info", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 530, "z": 1 }, + "size": { "width": 1360, "height": 150 }, + "overrides": { + "componentConfig": { + "title": "๋ฌด์—ญ ์ •๋ณด", + "showHeader": true, + "padding": "md", + "borderStyle": "solid" + }, + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 15 + }, + { + "id": "select-incoterms", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 40, "y": 575, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "์ธ์ฝ”ํ…€์ฆˆ", + "columnName": "incoterms", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "FOB", "label": "FOB" }, + { "value": "CIF", "label": "CIF" }, + { "value": "EXW", "label": "EXW" }, + { "value": "DDP", "label": "DDP" } + ], + "placeholder": "์„ ํƒ", + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 16 + }, + { + "id": "select-payment-term", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 260, "y": 575, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "๊ฒฐ์ œ ์กฐ๊ฑด", + "columnName": "payment_term", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "TT", "label": "T/T" }, + { "value": "LC", "label": "L/C" }, + { "value": "DA", "label": "D/A" }, + { "value": "DP", "label": "D/P" } + ], + "placeholder": "์„ ํƒ", + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 17 + }, + { + "id": "select-currency", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 480, "y": 575, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "ํ†ตํ™”", + "columnName": "currency", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "KRW", "label": "KRW (์›)" }, + { "value": "USD", "label": "USD (๋‹ฌ๋Ÿฌ)" }, + { "value": "EUR", "label": "EUR (์œ ๋กœ)" }, + { "value": "JPY", "label": "JPY (์—”)" }, + { "value": "CNY", "label": "CNY (์œ„์•ˆ)" } + ], + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 18 + }, + { + "id": "input-port-loading", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 40, "y": 625, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "์„ ์ ํ•ญ", + "columnName": "port_of_loading", + "placeholder": "์„ ์ ํ•ญ", + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 19 + }, + { + "id": "input-port-discharge", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 260, "y": 625, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "๋„์ฐฉํ•ญ", + "columnName": "port_of_discharge", + "placeholder": "๋„์ฐฉํ•ญ", + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 20 + }, + { + "id": "input-hs-code", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 480, "y": 625, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "HS Code", + "columnName": "hs_code", + "placeholder": "HS Code", + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 21 + }, + + { + "id": "section-additional", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 690, "z": 1 }, + "size": { "width": 1360, "height": 130 }, + "overrides": { + "componentConfig": { + "title": "์ถ”๊ฐ€ ์ •๋ณด", + "showHeader": true, + "padding": "md", + "borderStyle": "solid" + } + }, + "displayOrder": 22 + }, + { + "id": "input-memo", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 40, "y": 735, "z": 3 }, + "size": { "width": 1320, "height": 70 }, + "overrides": { + "label": "๋ฉ”๋ชจ", + "columnName": "memo", + "type": "textarea", + "placeholder": "๋ฉ”๋ชจ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 23 + }, + + { + "id": "btn-cancel", + "url": "@/lib/registry/components/v2-button-primary", + "position": { "x": 1180, "y": 840, "z": 5 }, + "size": { "width": 90, "height": 40 }, + "overrides": { + "label": "์ทจ์†Œ", + "webTypeConfig": { + "variant": "outline" + }, + "action": { + "type": "close" + } + }, + "displayOrder": 24 + }, + { + "id": "btn-save", + "url": "@/lib/registry/components/v2-button-primary", + "position": { "x": 1280, "y": 840, "z": 5 }, + "size": { "width": 90, "height": 40 }, + "overrides": { + "label": "์ €์žฅ", + "action": { + "type": "save" + } + }, + "displayOrder": 25 + } + ], + "gridSettings": { + "columns": 12, + "gap": 16, + "padding": 20, + "snapToGrid": true, + "showGrid": false + } +} diff --git a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx index 73e5d282..17c52897 100644 --- a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx @@ -773,18 +773,81 @@ export default function TableManagementPage() { // 2. ๋ชจ๋“  ์ปฌ๋Ÿผ ์„ค์ • ์ €์žฅ if (columns.length > 0) { - const columnSettings = columns.map((column) => ({ - columnName: column.columnName, // ์‹ค์ œ DB ์ปฌ๋Ÿผ๋ช… (๋ณ€๊ฒฝ ๋ถˆ๊ฐ€) - columnLabel: column.displayName, // ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ํ‘œ์‹œ๋ช… - inputType: column.inputType || "text", - detailSettings: column.detailSettings || "", - description: column.description || "", - codeCategory: column.codeCategory || "", - codeValue: column.codeValue || "", - referenceTable: column.referenceTable || "", - referenceColumn: column.referenceColumn || "", - displayColumn: column.displayColumn || "", // ๐ŸŽฏ Entity ์กฐ์ธ์—์„œ ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ๋ช… - })); + const columnSettings = columns.map((column) => { + // detailSettings ๊ณ„์‚ฐ + let finalDetailSettings = column.detailSettings || ""; + + // ๐Ÿ†• Numbering ํƒ€์ž…์ธ ๊ฒฝ์šฐ numberingRuleId๋ฅผ detailSettings์— ํฌํ•จ + if (column.inputType === "numbering" && column.numberingRuleId) { + let existingSettings: Record = {}; + if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) { + try { + existingSettings = JSON.parse(finalDetailSettings); + } catch { + existingSettings = {}; + } + } + const numberingSettings = { + ...existingSettings, + numberingRuleId: column.numberingRuleId, + }; + finalDetailSettings = JSON.stringify(numberingSettings); + console.log("๐Ÿ”ง ์ „์ฒด์ €์žฅ - Numbering ์„ค์ • JSON ์ƒ์„ฑ:", { + columnName: column.columnName, + numberingRuleId: column.numberingRuleId, + finalDetailSettings, + }); + } + + // ๐Ÿ†• Entity ํƒ€์ž…์ธ ๊ฒฝ์šฐ detailSettings์— ์—”ํ‹ฐํ‹ฐ ์„ค์ • ํฌํ•จ + if (column.inputType === "entity" && column.referenceTable) { + let existingSettings: Record = {}; + if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) { + try { + existingSettings = JSON.parse(finalDetailSettings); + } catch { + existingSettings = {}; + } + } + const entitySettings = { + ...existingSettings, + entityTable: column.referenceTable, + entityCodeColumn: column.referenceColumn || "id", + entityLabelColumn: column.displayColumn || "name", + }; + finalDetailSettings = JSON.stringify(entitySettings); + } + + // ๐Ÿ†• Code ํƒ€์ž…์ธ ๊ฒฝ์šฐ hierarchyRole์„ detailSettings์— ํฌํ•จ + if (column.inputType === "code" && column.hierarchyRole) { + let existingSettings: Record = {}; + if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) { + try { + existingSettings = JSON.parse(finalDetailSettings); + } catch { + existingSettings = {}; + } + } + const codeSettings = { + ...existingSettings, + hierarchyRole: column.hierarchyRole, + }; + finalDetailSettings = JSON.stringify(codeSettings); + } + + return { + columnName: column.columnName, // ์‹ค์ œ DB ์ปฌ๋Ÿผ๋ช… (๋ณ€๊ฒฝ ๋ถˆ๊ฐ€) + columnLabel: column.displayName, // ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ํ‘œ์‹œ๋ช… + inputType: column.inputType || "text", + detailSettings: finalDetailSettings, + description: column.description || "", + codeCategory: column.codeCategory || "", + codeValue: column.codeValue || "", + referenceTable: column.referenceTable || "", + referenceColumn: column.referenceColumn || "", + displayColumn: column.displayColumn || "", // ๐ŸŽฏ Entity ์กฐ์ธ์—์„œ ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ๋ช… + }; + }); // console.log("์ €์žฅํ•  ์ „์ฒด ์„ค์ •:", { tableLabel, tableDescription, columnSettings }); diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index eb7ecce5..9f043adf 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -26,8 +26,11 @@ import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; // ํ™œ์„ฑ ํƒญ import { evaluateConditional } from "@/lib/utils/conditionalEvaluator"; // ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ํ‰๊ฐ€ import { ScreenMultiLangProvider } from "@/contexts/ScreenMultiLangContext"; // ํ™”๋ฉด ๋‹ค๊ตญ์–ด import { convertV2ToLegacy, isValidV2Layout } from "@/lib/utils/layoutV2Converter"; // V2 Zod ๊ธฐ๋ฐ˜ ๋ณ€ํ™˜ +import { useScheduleGenerator, ScheduleConfirmDialog } from "@/lib/v2-core/services/ScheduleGeneratorService"; // ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ function ScreenViewPage() { + // ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค ํ™œ์„ฑํ™” + const { showConfirmDialog, previewResult, handleConfirm, closeDialog, isLoading: scheduleLoading } = useScheduleGenerator(); const params = useParams(); const searchParams = useSearchParams(); const router = useRouter(); @@ -158,7 +161,6 @@ function ScreenViewPage() { // V2 ๋ ˆ์ด์•„์›ƒ: Zod ๊ธฐ๋ฐ˜ ๋ณ€ํ™˜ (๊ธฐ๋ณธ๊ฐ’ ๋ณ‘ํ•ฉ) const convertedLayout = convertV2ToLegacy(v2Response); if (convertedLayout) { - console.log("๐Ÿ“ฆ V2 ๋ ˆ์ด์•„์›ƒ ๋กœ๋“œ (Zod ๊ธฐ๋ฐ˜):", v2Response.components?.length || 0, "๊ฐœ ์ปดํฌ๋„ŒํŠธ"); setLayout({ ...convertedLayout, screenResolution: v2Response.screenResolution || convertedLayout.screenResolution, @@ -202,7 +204,90 @@ function ScreenViewPage() { } }, [screenId]); - // ๐Ÿ†• autoFill ์ž๋™ ์ž…๋ ฅ ์ดˆ๊ธฐํ™” + // ๐Ÿ†• ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ž๋™ ๋กœ๋“œ (๋‹จ์ผ ๋ ˆ์ฝ”๋“œ ํผ) + // ํ™”๋ฉด์˜ ๋ฉ”์ธ ํ…Œ์ด๋ธ”์—์„œ ์‚ฌ์šฉ์ž ํšŒ์‚ฌ ์ฝ”๋“œ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜์—ฌ ํผ์— ์ž๋™ ์ฑ„์›€ + useEffect(() => { + const loadMainTableData = async () => { + if (!screen || !layout || !layout.components || !companyCode) { + return; + } + + const mainTableName = screen.tableName; + if (!mainTableName) { + return; + } + + // ํ…Œ์ด๋ธ” ์œ„์ ฏ์ด ์—†๋Š” ๊ฒฝ์šฐ์—๋งŒ ์ž๋™ ๋กœ๋“œ (ํ…Œ์ด๋ธ”์ด ์žˆ์œผ๋ฉด ํ–‰ ์„ ํƒ์œผ๋กœ ๋ฐ์ดํ„ฐ ๋กœ๋“œ) + const hasTableWidget = layout.components.some( + (comp: any) => + comp.componentType === "table-list" || + comp.componentType === "v2-table-list" || + comp.widgetType === "table" + ); + + if (hasTableWidget) { + return; + } + + // ์ธํ’‹ ์ปดํฌ๋„ŒํŠธ๋“ค ์ค‘ ๋ฉ”์ธ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋“ค ์ฐพ๊ธฐ + const inputComponents = layout.components.filter((comp: any) => { + const compType = comp.componentType || comp.widgetType; + const isInputType = compType?.includes("input") || + compType?.includes("select") || + compType?.includes("textarea") || + compType?.includes("v2-input") || + compType?.includes("v2-select") || + compType?.includes("v2-media") || + compType?.includes("file-upload"); // ๐Ÿ†• ๋ ˆ๊ฑฐ์‹œ ํŒŒ์ผ ์—…๋กœ๋“œ ํฌํ•จ + const hasColumnName = !!(comp as any).columnName; + return isInputType && hasColumnName; + }); + + if (inputComponents.length === 0) { + return; + } + + // ๋ฉ”์ธ ํ…Œ์ด๋ธ”์—์„œ ํ˜„์žฌ ํšŒ์‚ฌ์˜ ๋ฐ์ดํ„ฐ ์กฐํšŒ + try { + const { tableTypeApi } = await import("@/lib/api/screen"); + + // company_code๋กœ ํ•„ํ„ฐ๋งํ•˜์—ฌ ๋‹จ์ผ ๋ ˆ์ฝ”๋“œ ์กฐํšŒ + const result = await tableTypeApi.getTableRecord( + mainTableName, + "company_code", + companyCode, + "*" // ๋ชจ๋“  ์ปฌ๋Ÿผ + ); + + if (result && result.record) { + console.log("๐Ÿ“ฆ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ž๋™ ๋กœ๋“œ:", mainTableName, result.record); + + // ๊ฐ ์ธํ’‹ ์ปดํฌ๋„ŒํŠธ์— ํ•ด๋‹นํ•˜๋Š” ๋ฐ์ดํ„ฐ ์ฑ„์šฐ๊ธฐ + const newFormData: Record = {}; + inputComponents.forEach((comp: any) => { + const columnName = comp.columnName; + if (columnName && result.record[columnName] !== undefined) { + newFormData[columnName] = result.record[columnName]; + } + }); + + if (Object.keys(newFormData).length > 0) { + setFormData((prev) => ({ + ...prev, + ...newFormData, + })); + } + } + } catch (error) { + console.log("๋ฉ”์ธ ํ…Œ์ด๋ธ” ์ž๋™ ๋กœ๋“œ ์‹คํŒจ (์ •์ƒ์ผ ์ˆ˜ ์žˆ์Œ):", error); + // ์—๋Ÿฌ๋Š” ๋ฌด์‹œ - ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๊ถŒํ•œ์ด ์—†์„ ์ˆ˜ ์žˆ์Œ + } + }; + + loadMainTableData(); + }, [screen, layout, companyCode]); + + // ๐Ÿ†• ๊ฐœ๋ณ„ autoFill ์ฒ˜๋ฆฌ (๋ฉ”์ธ ํ…Œ์ด๋ธ”๊ณผ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์—์„œ ์กฐํšŒํ•˜๋Š” ๊ฒฝ์šฐ) useEffect(() => { const initAutoFill = async () => { if (!layout || !layout.components || !user) { @@ -215,7 +300,7 @@ function ScreenViewPage() { const widget = comp as any; const fieldName = widget.columnName || widget.id; - // autoFill ์ฒ˜๋ฆฌ + // autoFill ์ฒ˜๋ฆฌ (๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •๋œ ๊ฒฝ์šฐ๋งŒ) if (widget.autoFill?.enabled || (comp as any).autoFill?.enabled) { const autoFillConfig = widget.autoFill || (comp as any).autoFill; const currentValue = formData[fieldName]; @@ -909,6 +994,16 @@ function ScreenViewPage() { }); }} /> + + {/* ์Šค์ผ€์ค„ ์ƒ์„ฑ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ */} + !open && closeDialog()} + preview={previewResult} + onConfirm={() => handleConfirm(true)} + onCancel={closeDialog} + isLoading={scheduleLoading} + />
diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 68fa0cb1..49fb3355 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -13,6 +13,7 @@ import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext"; import { useSplitPanelContext } from "@/contexts/SplitPanelContext"; import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; +import { convertV2ToLegacy, isValidV2Layout } from "@/lib/utils/layoutV2Converter"; interface ScreenModalState { isOpen: boolean; @@ -126,6 +127,24 @@ export const ScreenModal: React.FC = ({ className }) => { // ๋ชจ๋‹ฌ์ด ์—ด๋ฆฐ ์‹œ๊ฐ„ ์ถ”์  (์ €์žฅ ์„ฑ๊ณต ์ด๋ฒคํŠธ ๋ฌด์‹œ์šฉ) const modalOpenedAtRef = React.useRef(0); + // ๐Ÿ†• ์ฑ„๋ฒˆ ํ•„๋“œ ์ˆ˜๋™ ์ž…๋ ฅ ๊ฐ’ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ + useEffect(() => { + const handleNumberingValueChanged = (event: CustomEvent) => { + const { columnName, value } = event.detail; + if (columnName && modalState.isOpen) { + setFormData((prev) => ({ + ...prev, + [columnName]: value, + })); + } + }; + + window.addEventListener("numberingValueChanged", handleNumberingValueChanged as EventListener); + return () => { + window.removeEventListener("numberingValueChanged", handleNumberingValueChanged as EventListener); + }; + }, [modalState.isOpen]); + // ์ „์—ญ ๋ชจ๋‹ฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ useEffect(() => { const handleOpenModal = (event: CustomEvent) => { @@ -139,6 +158,7 @@ export const ScreenModal: React.FC = ({ className }) => { splitPanelParentData, selectedData: eventSelectedData, selectedIds, + isCreateMode, // ๐Ÿ†• ๋ณต์‚ฌ ๋ชจ๋“œ ํ”Œ๋ž˜๊ทธ (true๋ฉด editData๊ฐ€ ์žˆ์–ด๋„ originalData ์„ค์ • ์•ˆ ํ•จ) } = event.detail; // ๐Ÿ†• ๋ชจ๋‹ฌ ์—ด๋ฆฐ ์‹œ๊ฐ„ ๊ธฐ๋ก @@ -162,7 +182,8 @@ export const ScreenModal: React.FC = ({ className }) => { } // ๐Ÿ†• editData๊ฐ€ ์žˆ์œผ๋ฉด formData์™€ originalData๋กœ ์„ค์ • (์ˆ˜์ • ๋ชจ๋“œ) - if (editData) { + // ๐Ÿ”ง ๋‹จ, isCreateMode๊ฐ€ true์ด๋ฉด (๋ณต์‚ฌ ๋ชจ๋“œ) originalData๋ฅผ ์„ค์ •ํ•˜์ง€ ์•Š์Œ โ†’ ์ฑ„๋ฒˆ ์ƒ์„ฑ ๊ฐ€๋Šฅ + if (editData && !isCreateMode) { // ๐Ÿ†• ๋ฐฐ์—ด์ธ ๊ฒฝ์šฐ ๋‘ ๊ฐ€์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ์„ค์ •: // 1. formData: ์ฒซ ๋ฒˆ์งธ ์š”์†Œ(๊ฐ์ฒด) - ์ผ๋ฐ˜ ์ž…๋ ฅ ํ•„๋“œ์šฉ (TextInput ๋“ฑ) // 2. selectedData: ์ „์ฒด ๋ฐฐ์—ด - ๋‹ค์ค‘ ํ•ญ๋ชฉ ์ปดํฌ๋„ŒํŠธ์šฉ (SelectedItemsDetailInput ๋“ฑ) @@ -176,6 +197,17 @@ export const ScreenModal: React.FC = ({ className }) => { setSelectedData([editData]); // ๐Ÿ”ง ๋‹จ์ผ ๊ฐ์ฒด๋„ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅ setOriginalData(editData); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ €์žฅ (UPDATE ํŒ๋‹จ์šฉ) } + } else if (editData && isCreateMode) { + // ๐Ÿ†• ๋ณต์‚ฌ ๋ชจ๋“œ: formData๋งŒ ์„ค์ •ํ•˜๊ณ  originalData๋Š” null๋กœ ์œ ์ง€ (์ฑ„๋ฒˆ ์ƒ์„ฑ ๊ฐ€๋Šฅ) + if (Array.isArray(editData)) { + const firstRecord = editData[0] || {}; + setFormData(firstRecord); + setSelectedData(editData); + } else { + setFormData(editData); + setSelectedData([editData]); + } + setOriginalData(null); // ๐Ÿ”ง ๋ณต์‚ฌ ๋ชจ๋“œ์—์„œ๋Š” originalData๋ฅผ null๋กœ ์„ค์ • } else { // ๐Ÿ†• ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ: ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฏธ๋ฆฌ ์„ค์ • // ๐Ÿ”ง ์ค‘์š”: ์‹ ๊ทœ ๋“ฑ๋ก ์‹œ์—๋Š” ์—ฐ๊ฒฐ ํ•„๋“œ(equipment_code ๋“ฑ)๋งŒ ์ „๋‹ฌํ•ด์•ผ ํ•จ @@ -322,12 +354,27 @@ export const ScreenModal: React.FC = ({ className }) => { try { setLoading(true); - // ํ™”๋ฉด ์ •๋ณด์™€ ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ - const [screenInfo, layoutData] = await Promise.all([ + // ํ™”๋ฉด ์ •๋ณด์™€ ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ (V2 API ์‚ฌ์šฉ์œผ๋กœ ๊ธฐ๋ณธ๊ฐ’ ๋ณ‘ํ•ฉ) + const [screenInfo, v2LayoutData] = await Promise.all([ screenApi.getScreen(screenId), - screenApi.getLayout(screenId), + screenApi.getLayoutV2(screenId), ]); + // V2 โ†’ Legacy ๋ณ€ํ™˜ (๊ธฐ๋ณธ๊ฐ’ ๋ณ‘ํ•ฉ ํฌํ•จ) + let layoutData: any = null; + if (v2LayoutData && isValidV2Layout(v2LayoutData)) { + layoutData = convertV2ToLegacy(v2LayoutData); + if (layoutData) { + // screenResolution์€ V2 ๋ ˆ์ด์•„์›ƒ์—์„œ ์ง์ ‘ ๊ฐ€์ ธ์˜ค๊ธฐ + layoutData.screenResolution = v2LayoutData.screenResolution || layoutData.screenResolution; + } + } + + // V2 ๋ ˆ์ด์•„์›ƒ์ด ์—†์œผ๋ฉด ๊ธฐ์กด API๋กœ fallback + if (!layoutData) { + layoutData = await screenApi.getLayout(screenId); + } + // ๐Ÿ†• URL ํŒŒ๋ผ๋ฏธํ„ฐ ํ™•์ธ (์ˆ˜์ • ๋ชจ๋“œ) if (typeof window !== "undefined") { const urlParams = new URLSearchParams(window.location.search); @@ -337,8 +384,6 @@ export const ScreenModal: React.FC = ({ className }) => { const groupByColumnsParam = urlParams.get("groupByColumns"); const primaryKeyColumn = urlParams.get("primaryKeyColumn"); // ๐Ÿ†• Primary Key ์ปฌ๋Ÿผ๋ช… - console.log("๐Ÿ“‹ URL ํŒŒ๋ผ๋ฏธํ„ฐ ํ™•์ธ:", { mode, editId, tableName, groupByColumnsParam, primaryKeyColumn }); - // ์ˆ˜์ • ๋ชจ๋“œ์ด๊ณ  editId๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ๋ ˆ์ฝ”๋“œ ์กฐํšŒ if (mode === "edit" && editId && tableName) { try { @@ -363,14 +408,8 @@ export const ScreenModal: React.FC = ({ className }) => { // ๐Ÿ†• Primary Key ์ปฌ๋Ÿผ๋ช… ์ „๋‹ฌ (๋ฐฑ์—”๋“œ ์ž๋™ ๊ฐ์ง€ ์‹คํŒจ ์‹œ ์‚ฌ์šฉ) if (primaryKeyColumn) { params.primaryKeyColumn = primaryKeyColumn; - console.log("โœ… [ScreenModal] primaryKeyColumn์„ params์— ์ถ”๊ฐ€:", primaryKeyColumn); } - console.log("๐Ÿ“ก [ScreenModal] ์‹ค์ œ API ์š”์ฒญ:", { - url: `/data/${tableName}/${editId}`, - params, - }); - const apiResponse = await apiClient.get(`/data/${tableName}/${editId}`, { params }); const response = apiResponse.data; @@ -483,26 +522,34 @@ export const ScreenModal: React.FC = ({ className }) => { return { className: "w-fit min-w-[400px] max-w-4xl max-h-[90vh] overflow-hidden p-0", style: undefined, // undefined๋กœ ๋ณ€๊ฒฝ - defaultWidth/defaultHeight ์‚ฌ์šฉ + needsScroll: false, }; } // ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ์„ค์ •ํ•œ ํฌ๊ธฐ = ์ปจํ…์ธ  ์˜์—ญ ํฌ๊ธฐ - // ์‹ค์ œ ๋ชจ๋‹ฌ ํฌ๊ธฐ = ์ปจํ…์ธ  + ํ—ค๋” + ์—ฐ์†๋“ฑ๋ก ์ฒดํฌ๋ฐ•์Šค + gap + padding - const headerHeight = 52; // DialogHeader (ํƒ€์ดํ‹€ + border-b + py-3) - const footerHeight = 52; // ์—ฐ์† ๋“ฑ๋ก ๋ชจ๋“œ ์ฒดํฌ๋ฐ•์Šค ์˜์—ญ - const dialogGap = 16; // DialogContent gap-4 - const extraPadding = 24; // ์ถ”๊ฐ€ ์—ฌ๋ฐฑ (์•ˆ์ „ ๋งˆ์ง„) + // ์‹ค์ œ ๋ชจ๋‹ฌ ํฌ๊ธฐ = ์ปจํ…์ธ  + ํ—ค๋” + ์—ฐ์†๋“ฑ๋ก ์ฒดํฌ๋ฐ•์Šค + gap + ํŒจ๋”ฉ + // ๐Ÿ”ง DialogContent์˜ gap-4 (16px ร— 2) + ์ปจํ…์ธ  pt-6 (24px) ํฌํ•จ + const headerHeight = 48; // DialogHeader (ํƒ€์ดํ‹€ + border-b + py-3) + const footerHeight = 44; // ์—ฐ์† ๋“ฑ๋ก ๋ชจ๋“œ ์ฒดํฌ๋ฐ•์Šค ์˜์—ญ + const dialogGap = 32; // gap-4 ร— 2 (header-content, content-footer ์‚ฌ์ด) + const contentTopPadding = 24; // pt-6 (์ปจํ…์ธ  ์˜์—ญ ์ƒ๋‹จ ํŒจ๋”ฉ) + const horizontalPadding = 16; // ์ขŒ์šฐ ํŒจ๋”ฉ ์ตœ์†Œํ™” - const totalHeight = screenDimensions.height + headerHeight + footerHeight + dialogGap + extraPadding; + const totalHeight = screenDimensions.height + headerHeight + footerHeight + dialogGap + contentTopPadding; + const maxAvailableHeight = window.innerHeight * 0.95; + + // ์ฝ˜ํ…์ธ ๊ฐ€ ํ™”๋ฉด ๋†’์ด๋ฅผ ์ดˆ๊ณผํ•  ๋•Œ๋งŒ ์Šคํฌ๋กค ํ•„์š” + const needsScroll = totalHeight > maxAvailableHeight; return { className: "overflow-hidden p-0", style: { - width: `${Math.min(screenDimensions.width + 48, window.innerWidth * 0.98)}px`, // ์ขŒ์šฐ ํŒจ๋”ฉ ์ถ”๊ฐ€ - height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`, + width: `${Math.min(screenDimensions.width + horizontalPadding, window.innerWidth * 0.98)}px`, + // ๐Ÿ”ง height ๋Œ€์‹  max-height๋งŒ ์„ค์ • - ์ฝ˜ํ…์ธ ๊ฐ€ ์ž‘์œผ๋ฉด ์ž๋™์œผ๋กœ ์ค„์–ด๋“ฆ + maxHeight: `${maxAvailableHeight}px`, maxWidth: "98vw", - maxHeight: "95vh", }, + needsScroll, }; }; @@ -570,7 +617,7 @@ export const ScreenModal: React.FC = ({ className }) => { return ( @@ -585,7 +632,9 @@ export const ScreenModal: React.FC = ({ className }) => {
-
+
{loading ? (
@@ -597,30 +646,137 @@ export const ScreenModal: React.FC = ({ className }) => {
- {screenData.components.map((component) => { + {(() => { + // ๐Ÿ†• ๋™์  y ์ขŒํ‘œ ์กฐ์ •์„ ์œ„ํ•ด ๋จผ์ € ์ˆจ๊ฒจ์ง€๋Š” ์ปดํฌ๋„ŒํŠธ๋“ค ํŒŒ์•… + const isComponentHidden = (comp: any) => { + const cc = comp.componentConfig?.conditionalConfig || comp.conditionalConfig; + if (!cc?.enabled || !formData) return false; + + const { field, operator, value, action } = cc; + const fieldValue = formData[field]; + + let conditionMet = false; + switch (operator) { + case "=": + case "==": + case "===": + conditionMet = fieldValue === value; + break; + case "!=": + case "!==": + conditionMet = fieldValue !== value; + break; + default: + conditionMet = fieldValue === value; + } + + return (action === "show" && !conditionMet) || (action === "hide" && conditionMet); + }; + + // ํ‘œ์‹œ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ๋“ค์˜ y ๋ฒ”์œ„ ์ˆ˜์ง‘ + const visibleRanges: { y: number; bottom: number }[] = []; + screenData.components.forEach((comp: any) => { + if (!isComponentHidden(comp)) { + const y = parseFloat(comp.position?.y?.toString() || "0"); + const height = parseFloat(comp.size?.height?.toString() || "0"); + visibleRanges.push({ y, bottom: y + height }); + } + }); + + // ์ˆจ๊ฒจ์ง€๋Š” ์ปดํฌ๋„ŒํŠธ์˜ "์‹ค์ œ ๋นˆ ๊ณต๊ฐ„" ๊ณ„์‚ฐ (ํ‘œ์‹œ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์™€ ๊ฒน์น˜์ง€ ์•Š๋Š” ์˜์—ญ) + const getActualGap = (hiddenY: number, hiddenBottom: number): number => { + // ์ˆจ๊ฒจ์ง€๋Š” ์˜์—ญ ์ค‘ ํ‘œ์‹œ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์™€ ๊ฒน์น˜๋Š” ๋ถ€๋ถ„์„ ์ œ์™ธ + let gapStart = hiddenY; + let gapEnd = hiddenBottom; + + for (const visible of visibleRanges) { + // ๊ฒน์น˜๋Š” ์˜์—ญ ํ™•์ธ + if (visible.y < gapEnd && visible.bottom > gapStart) { + // ๊ฒน์น˜๋Š” ๋ถ€๋ถ„์„ ์ œ์™ธ + if (visible.y <= gapStart && visible.bottom >= gapEnd) { + // ์™„์ „ํžˆ ๋ฎํž˜ - ๋นˆ ๊ณต๊ฐ„ ์—†์Œ + return 0; + } else if (visible.y <= gapStart) { + // ์œ„์ชฝ์ด ๋ฎํž˜ + gapStart = visible.bottom; + } else if (visible.bottom >= gapEnd) { + // ์•„๋ž˜์ชฝ์ด ๋ฎํž˜ + gapEnd = visible.y; + } + } + } + + return Math.max(0, gapEnd - gapStart); + }; + + // ์ˆจ๊ฒจ์ง€๋Š” ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ์‹ค์ œ ๋นˆ ๊ณต๊ฐ„ ์ˆ˜์ง‘ + const hiddenGaps: { bottom: number; gap: number }[] = []; + screenData.components.forEach((comp: any) => { + if (isComponentHidden(comp)) { + const y = parseFloat(comp.position?.y?.toString() || "0"); + const height = parseFloat(comp.size?.height?.toString() || "0"); + const bottom = y + height; + const gap = getActualGap(y, bottom); + if (gap > 0) { + hiddenGaps.push({ bottom, gap }); + } + } + }); + + // bottom ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ ๋ฐ ์ค‘๋ณต ์ œ๊ฑฐ (๊ฐ™์€ bottom์€ ๊ฐ€์žฅ ํฐ gap๋งŒ ์œ ์ง€) + const mergedGaps = new Map(); + hiddenGaps.forEach(({ bottom, gap }) => { + const existing = mergedGaps.get(bottom) || 0; + mergedGaps.set(bottom, Math.max(existing, gap)); + }); + + const sortedGaps = Array.from(mergedGaps.entries()) + .map(([bottom, gap]) => ({ bottom, gap })) + .sort((a, b) => a.bottom - b.bottom); + + // ๊ฐ ์ปดํฌ๋„ŒํŠธ์˜ y ์กฐ์ •๊ฐ’ ๊ณ„์‚ฐ ํ•จ์ˆ˜ + const getYOffset = (compY: number, compId?: string) => { + let offset = 0; + for (const { bottom, gap } of sortedGaps) { + // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ˆจ๊ฒจ์ง„ ์˜์—ญ ์•„๋ž˜์— ์žˆ์œผ๋ฉด ๊ทธ ๋นˆ ๊ณต๊ฐ„๋งŒํผ ์œ„๋กœ ์ด๋™ + if (compY > bottom) { + offset += gap; + } + } + return offset; + }; + + return screenData.components.map((component: any) => { + // ์ˆจ๊ฒจ์ง€๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” ๋ Œ๋”๋ง ์•ˆํ•จ + if (isComponentHidden(component)) { + return null; + } + // ํ™”๋ฉด ๊ด€๋ฆฌ ํ•ด์ƒ๋„๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ offset ์กฐ์ • ๋ถˆํ•„์š” const offsetX = screenDimensions?.offsetX || 0; const offsetY = screenDimensions?.offsetY || 0; + + // ๐Ÿ†• ๋™์  y ์ขŒํ‘œ ์กฐ์ • (์ˆจ๊ฒจ์ง„ ์ปดํฌ๋„ŒํŠธ ๋†’์ด๋งŒํผ ์œ„๋กœ ์ด๋™) + const compY = parseFloat(component.position?.y?.toString() || "0"); + const yAdjustment = getYOffset(compY, component.id); // offset์ด 0์ด๋ฉด ์›๋ณธ ์œ„์น˜ ์‚ฌ์šฉ (ํ™”๋ฉด ๊ด€๋ฆฌ ํ•ด์ƒ๋„ ์‚ฌ์šฉ ์‹œ) - const adjustedComponent = - offsetX === 0 && offsetY === 0 - ? component - : { - ...component, - position: { - ...component.position, - x: parseFloat(component.position?.x?.toString() || "0") - offsetX, - y: parseFloat(component.position?.y?.toString() || "0") - offsetY, - }, - }; + const adjustedComponent = { + ...component, + position: { + ...component.position, + x: parseFloat(component.position?.x?.toString() || "0") - offsetX, + y: compY - offsetY - yAdjustment, // ๐Ÿ†• ๋™์  ์กฐ์ • ์ ์šฉ + }, + }; return ( = ({ className }) => { companyCode={user?.companyCode} /> ); - })} + }); + })()}
diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx index 57e4896b..9320f00e 100644 --- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx +++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx @@ -108,7 +108,7 @@ export const NumberingRuleDesigner: React.FC = ({ // ์ „์ฒด ์นดํ…Œ๊ณ ๋ฆฌ ์˜ต์…˜ ๋กœ๋“œ (๋ชจ๋“  ํ…Œ์ด๋ธ”์˜ category ํƒ€์ž… ์ปฌ๋Ÿผ) const loadAllCategoryOptions = async () => { try { - // category_values_test ํ…Œ์ด๋ธ”์—์„œ ๊ณ ์œ ํ•œ ํ…Œ์ด๋ธ”.์ปฌ๋Ÿผ ์กฐํ•ฉ ์กฐํšŒ + // category_values ํ…Œ์ด๋ธ”์—์„œ ๊ณ ์œ ํ•œ ํ…Œ์ด๋ธ”.์ปฌ๋Ÿผ ์กฐํ•ฉ ์กฐํšŒ const response = await getAllCategoryKeys(); if (response.success && response.data) { const options: CategoryOption[] = response.data.map((item) => ({ @@ -260,22 +260,24 @@ export const NumberingRuleDesigner: React.FC = ({ toast.success(`๊ทœ์น™ ${newPart.order}๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค`); }, [currentRule, maxRules]); - const handleUpdatePart = useCallback((partId: string, updates: Partial) => { + // partOrder ๊ธฐ๋ฐ˜์œผ๋กœ ํŒŒํŠธ ์—…๋ฐ์ดํŠธ (id๊ฐ€ null์ผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ order ์‚ฌ์šฉ) + const handleUpdatePart = useCallback((partOrder: number, updates: Partial) => { setCurrentRule((prev) => { if (!prev) return null; return { ...prev, - parts: prev.parts.map((part) => (part.id === partId ? { ...part, ...updates } : part)), + parts: prev.parts.map((part) => (part.order === partOrder ? { ...part, ...updates } : part)), }; }); }, []); - const handleDeletePart = useCallback((partId: string) => { + // partOrder ๊ธฐ๋ฐ˜์œผ๋กœ ํŒŒํŠธ ์‚ญ์ œ (id๊ฐ€ null์ผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ order ์‚ฌ์šฉ) + const handleDeletePart = useCallback((partOrder: number) => { setCurrentRule((prev) => { if (!prev) return null; return { ...prev, - parts: prev.parts.filter((part) => part.id !== partId).map((part, index) => ({ ...part, order: index + 1 })), + parts: prev.parts.filter((part) => part.order !== partOrder).map((part, index) => ({ ...part, order: index + 1 })), }; }); @@ -295,8 +297,6 @@ export const NumberingRuleDesigner: React.FC = ({ setLoading(true); try { - const existing = savedRules.find((r) => r.ruleId === currentRule.ruleId); - // ํŒŒํŠธ๋ณ„ ๊ธฐ๋ณธ autoConfig ์ •์˜ const defaultAutoConfigs: Record = { sequence: { sequenceLength: 3, startFrom: 1 }, @@ -341,19 +341,34 @@ export const NumberingRuleDesigner: React.FC = ({ ruleToSave, }); - // ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ”์— ์ €์žฅ (numbering_rules_test) + // ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ”์— ์ €์žฅ (numbering_rules) const response = await saveNumberingRuleToTest(ruleToSave); if (response.success && response.data) { + // ๊นŠ์€ ๋ณต์‚ฌํ•˜์—ฌ savedRules์™€ currentRule์ด ๋‹ค๋ฅธ ๊ฐ์ฒด๋ฅผ ์ฐธ์กฐํ•˜๋„๋ก ํ•จ + const currentData = JSON.parse(JSON.stringify(response.data)) as NumberingRuleConfig; + + // setSavedRules ๋‚ด๋ถ€์—์„œ prev๋ฅผ ์‚ฌ์šฉํ•ด์„œ existing ํ™•์ธ (ํด๋กœ์ € ๋ฌธ์ œ ๋ฐฉ์ง€) setSavedRules((prev) => { - if (existing) { - return prev.map((r) => (r.ruleId === ruleToSave.ruleId ? response.data! : r)); + const savedData = JSON.parse(JSON.stringify(response.data)) as NumberingRuleConfig; + const existsInPrev = prev.some((r) => r.ruleId === ruleToSave.ruleId); + + console.log("๐Ÿ” [handleSave] setSavedRules:", { + ruleId: ruleToSave.ruleId, + existsInPrev, + prevCount: prev.length, + }); + + if (existsInPrev) { + // ๊ธฐ์กด ๊ทœ์น™ ์—…๋ฐ์ดํŠธ + return prev.map((r) => (r.ruleId === ruleToSave.ruleId ? savedData : r)); } else { - return [...prev, response.data!]; + // ์ƒˆ ๊ทœ์น™ ์ถ”๊ฐ€ + return [...prev, savedData]; } }); - setCurrentRule(response.data); + setCurrentRule(currentData); setSelectedRuleId(response.data.ruleId); await onSave?.(response.data); @@ -366,11 +381,27 @@ export const NumberingRuleDesigner: React.FC = ({ } finally { setLoading(false); } - }, [currentRule, savedRules, onSave, currentTableName]); + }, [currentRule, onSave, currentTableName, menuObjid]); const handleSelectRule = useCallback((rule: NumberingRuleConfig) => { + console.log("๐Ÿ” [handleSelectRule] ๊ทœ์น™ ์„ ํƒ:", { + ruleId: rule.ruleId, + ruleName: rule.ruleName, + partsCount: rule.parts?.length || 0, + parts: rule.parts?.map(p => ({ id: p.id, order: p.order, partType: p.partType })), + }); + setSelectedRuleId(rule.ruleId); - setCurrentRule(rule); + // ๊นŠ์€ ๋ณต์‚ฌํ•˜์—ฌ ๊ฐ์ฒด ์ฐธ์กฐ ๋ถ„๋ฆฌ (์ขŒ์ธก ๋ชฉ๋ก๊ณผ ํŽธ์ง‘ ์˜์—ญ์˜ ๊ฐ์ฒด๊ฐ€ ๊ณต์œ ๋˜์ง€ ์•Š๋„๋ก) + const ruleCopy = JSON.parse(JSON.stringify(rule)) as NumberingRuleConfig; + + console.log("๐Ÿ” [handleSelectRule] ๊นŠ์€ ๋ณต์‚ฌ ํ›„:", { + ruleId: ruleCopy.ruleId, + partsCount: ruleCopy.parts?.length || 0, + parts: ruleCopy.parts?.map(p => ({ id: p.id, order: p.order, partType: p.partType })), + }); + + setCurrentRule(ruleCopy); toast.info(`"${rule.ruleName}" ๊ทœ์น™์„ ๋ถˆ๋Ÿฌ์™”์Šต๋‹ˆ๋‹ค`); }, []); @@ -595,12 +626,12 @@ export const NumberingRuleDesigner: React.FC = ({
) : (
- {currentRule.parts.map((part) => ( + {currentRule.parts.map((part, index) => ( handleUpdatePart(part.id, updates)} - onDelete={() => handleDeletePart(part.id)} + onUpdate={(updates) => handleUpdatePart(part.order, updates)} + onDelete={() => handleDeletePart(part.order)} isPreview={isPreview} /> ))} diff --git a/frontend/components/screen/CopyScreenModal.tsx b/frontend/components/screen/CopyScreenModal.tsx index f1e49f9c..070a0ce6 100644 --- a/frontend/components/screen/CopyScreenModal.tsx +++ b/frontend/components/screen/CopyScreenModal.tsx @@ -253,6 +253,24 @@ export default function CopyScreenModal({ } }, [useBulkRename, removeText, addPrefix]); + // ์›๋ณธ ํšŒ์‚ฌ๊ฐ€ ์„ ํƒ๋œ ๊ฒฝ์šฐ ๋‹ค๋ฅธ ํšŒ์‚ฌ๋กœ ์ž๋™ ๋ณ€๊ฒฝ + useEffect(() => { + if (!companies.length || !isOpen) return; + + const sourceCompanyCode = mode === "group" + ? sourceGroup?.company_code + : sourceScreen?.companyCode; + + // ์›๋ณธ ํšŒ์‚ฌ์™€ ๊ฐ™์€ ํšŒ์‚ฌ๊ฐ€ ์„ ํƒ๋˜์–ด ์žˆ์œผ๋ฉด ๋‹ค๋ฅธ ํšŒ์‚ฌ๋กœ ๋ณ€๊ฒฝ + if (sourceCompanyCode && targetCompanyCode === sourceCompanyCode) { + const otherCompany = companies.find(c => c.companyCode !== sourceCompanyCode); + if (otherCompany) { + console.log("๐Ÿ”„ ์›๋ณธ ํšŒ์‚ฌ ์„ ํƒ๋จ โ†’ ๋‹ค๋ฅธ ํšŒ์‚ฌ๋กœ ์ž๋™ ๋ณ€๊ฒฝ:", otherCompany.companyCode); + setTargetCompanyCode(otherCompany.companyCode); + } + } + }, [companies, isOpen, mode, sourceGroup, sourceScreen, targetCompanyCode]); + // ๋Œ€์ƒ ํšŒ์‚ฌ ๋ณ€๊ฒฝ ์‹œ ๊ธฐ์กด ์ฝ”๋“œ ์ดˆ๊ธฐํ™” useEffect(() => { if (targetCompanyCode) { @@ -597,7 +615,7 @@ export default function CopyScreenModal({ screen_id: result.mainScreen.screenId, screen_role: "MAIN", display_order: 1, - target_company_code: finalCompanyCode, // ๋Œ€์ƒ ํšŒ์‚ฌ ์ฝ”๋“œ ์ „๋‹ฌ + target_company_code: targetCompanyCode || sourceScreen.companyCode, // ๋Œ€์ƒ ํšŒ์‚ฌ ์ฝ”๋“œ ์ „๋‹ฌ }); console.log(`โœ… ๋ณต์ œ๋œ ํ™”๋ฉด์„ ๊ทธ๋ฃน(${selectedTargetGroupId})์— ์ถ”๊ฐ€ ์™„๋ฃŒ`); } catch (groupError) { @@ -606,8 +624,68 @@ export default function CopyScreenModal({ } } + // ์ถ”๊ฐ€ ๋ณต์‚ฌ ์˜ต์…˜ ์ฒ˜๋ฆฌ (๋‹จ์ผ ํ™”๋ฉด ๋ณต์ œ์šฉ) + const sourceCompanyCode = sourceScreen.companyCode; + const copyTargetCompanyCode = targetCompanyCode || sourceCompanyCode; + let additionalCopyMessages: string[] = []; + + // ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ + if (copyNumberingRules && sourceCompanyCode !== copyTargetCompanyCode) { + try { + console.log("๐Ÿ“‹ ๋‹จ์ผ ํ™”๋ฉด: ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์‹œ์ž‘..."); + const numberingResult = await apiClient.post("/api/screen-management/copy-numbering-rules", { + sourceCompanyCode, + targetCompanyCode: copyTargetCompanyCode + }); + if (numberingResult.data.success) { + additionalCopyMessages.push(`์ฑ„๋ฒˆ๊ทœ์น™ ${numberingResult.data.copiedCount || 0}๊ฐœ`); + console.log("โœ… ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์™„๋ฃŒ:", numberingResult.data); + } + } catch (err: any) { + console.error("์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์‹คํŒจ:", err); + } + } + + // ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ณต์ œ + if (copyCategoryValues && sourceCompanyCode !== copyTargetCompanyCode) { + try { + console.log("๐Ÿ“‹ ๋‹จ์ผ ํ™”๋ฉด: ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ณต์ œ ์‹œ์ž‘..."); + const categoryResult = await apiClient.post("/api/screen-management/copy-category-mapping", { + sourceCompanyCode, + targetCompanyCode: copyTargetCompanyCode + }); + if (categoryResult.data.success) { + additionalCopyMessages.push(`์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ${categoryResult.data.copiedValues || 0}๊ฐœ`); + console.log("โœ… ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ณต์ œ ์™„๋ฃŒ:", categoryResult.data); + } + } catch (err: any) { + console.error("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ณต์ œ ์‹คํŒจ:", err); + } + } + + // ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ + if (copyTableTypeColumns && sourceCompanyCode !== copyTargetCompanyCode) { + try { + console.log("๐Ÿ“‹ ๋‹จ์ผ ํ™”๋ฉด: ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ ์‹œ์ž‘..."); + const tableTypeResult = await apiClient.post("/api/screen-management/copy-table-type-columns", { + sourceCompanyCode, + targetCompanyCode: copyTargetCompanyCode + }); + if (tableTypeResult.data.success) { + additionalCopyMessages.push(`ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ${tableTypeResult.data.copiedCount || 0}๊ฐœ`); + console.log("โœ… ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ ์™„๋ฃŒ:", tableTypeResult.data); + } + } catch (err: any) { + console.error("ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ ์‹คํŒจ:", err); + } + } + + const additionalInfo = additionalCopyMessages.length > 0 + ? ` + ์ถ”๊ฐ€: ${additionalCopyMessages.join(", ")}` + : ""; + toast.success( - `ํ™”๋ฉด ๋ณต์‚ฌ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! (๋ฉ”์ธ 1๊ฐœ + ๋ชจ๋‹ฌ ${result.modalScreens.length}๊ฐœ)` + `ํ™”๋ฉด ๋ณต์‚ฌ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! (๋ฉ”์ธ 1๊ฐœ + ๋ชจ๋‹ฌ ${result.modalScreens.length}๊ฐœ${additionalInfo})` ); // ์ƒˆ๋กœ๊ณ ์นจ ์™„๋ฃŒ ํ›„ ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ @@ -1122,31 +1200,36 @@ export default function CopyScreenModal({ // ๊ทธ๋ฃน ๋ณต์ œ ๋ชจ๋“œ ๋ Œ๋”๋ง if (mode === "group") { return ( - - - {/* ๋กœ๋”ฉ ์˜ค๋ฒ„๋ ˆ์ด */} - {isCopying && ( -
- -

{copyProgress.message}

+ <> + {/* ๋กœ๋”ฉ ์˜ค๋ฒ„๋ ˆ์ด - Dialog ๋ฐ”๊นฅ์—์„œ ํ™”๋ฉด ์ „์ฒด ๊ณ ์ • */} + {isCopying && ( +
+
+ +

{copyProgress.message}

{copyProgress.total > 0 && ( <> -
+
-

- {copyProgress.current} / {copyProgress.total} ํ™”๋ฉด +

+ {copyProgress.current} / {copyProgress.total} ํ™”๋ฉด ๋ณต์ œ ์ค‘...

)} +

+ ๋ณต์ œ๊ฐ€ ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ์ž ์‹œ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š” +

- )} - - - +
+ )} + + + + ๊ทธ๋ฃน ๋ณต์ œ @@ -1426,15 +1509,22 @@ export default function CopyScreenModal({ onChange={(e) => setTargetCompanyCode(e.target.value)} className="mt-1 flex h-8 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-xs ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 sm:h-10 sm:text-sm" > - {companies.map((company) => ( - - ))} + {companies + .filter((company) => company.companyCode !== sourceGroup?.company_code) + .map((company) => ( + + ))}

๋ณต์ œ๋œ ๊ทธ๋ฃน๊ณผ ํ™”๋ฉด์ด ์ด ํšŒ์‚ฌ์— ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค

+ {sourceGroup && ( +

+ * ์›๋ณธ ํšŒ์‚ฌ({sourceGroup.company_code})๋กœ๋Š” ๋ณต์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค +

+ )}
)} @@ -1530,14 +1620,25 @@ export default function CopyScreenModal({
+ ); } // ํ™”๋ฉด ๋ณต์ œ ๋ชจ๋“œ ๋ Œ๋”๋ง return ( - - - + <> + {/* ๋กœ๋”ฉ ์˜ค๋ฒ„๋ ˆ์ด - Dialog ๋ฐ”๊นฅ์—์„œ ํ™”๋ฉด ์ „์ฒด ๊ณ ์ • */} + {isCopying && ( +
+
+ +

๋ณต์ œ๊ฐ€ ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ์ž ์‹œ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”

+
+
+ )} + + + ํ™”๋ฉด ๋ณต์ œ "{sourceScreen?.screenName}" ํ™”๋ฉด์„ ๋ณต์ œํ•ฉ๋‹ˆ๋‹ค. @@ -1634,13 +1735,20 @@ export default function CopyScreenModal({ - {companies.map((company) => ( - - {company.companyName} - - ))} + {companies + .filter((company) => company.companyCode !== sourceScreen?.companyCode) + .map((company) => ( + + {company.companyName} + + ))} + {sourceScreen && ( +

+ * ์›๋ณธ ํšŒ์‚ฌ({sourceScreen.companyCode})๋กœ๋Š” ๋ณต์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค +

+ )}
)} @@ -1678,6 +1786,50 @@ export default function CopyScreenModal({
)} + {/* ์ถ”๊ฐ€ ๋ณต์‚ฌ ์˜ต์…˜ (๋‹จ์ผ ํ™”๋ฉด ๋ณต์ œ์šฉ) */} +
+ + + {/* ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ */} +
+ setCopyNumberingRules(checked === true)} + /> + +
+ + {/* ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ณต์‚ฌ */} +
+ setCopyCategoryValues(checked === true)} + /> +
+ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ณต์‚ฌ + + + + {/* ํ…Œ์ด๋ธ” ํƒ€์ž…๊ด€๋ฆฌ ์ž…๋ ฅํƒ€์ž… ์„ค์ • ๋ณต์‚ฌ */} +
+ setCopyTableTypeColumns(checked === true)} + /> + +
+ + {/* ํ™”๋ฉด๋ช… ์ผ๊ด„ ์ˆ˜์ • (์ ‘ํžˆ๋Š” ์˜ต์…˜) */}
@@ -1736,6 +1888,7 @@ export default function CopyScreenModal({ + ); } diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index db722991..b95ee973 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -618,7 +618,36 @@ export const EditModal: React.FC = ({ className }) => { if (currentValue !== originalValue) { console.log(`๐Ÿ” [ํ’ˆ๋ชฉ ์ˆ˜์ • ๊ฐ์ง€] ${key}: ${originalValue} โ†’ ${currentValue}`); // ๋‚ ์งœ ํ•„๋“œ๋Š” ์ •๊ทœํ™”๋œ ๊ฐ’ ์‚ฌ์šฉ, ๋‚˜๋จธ์ง€๋Š” ์›๋ณธ ๊ฐ’ ์‚ฌ์šฉ - changedData[key] = dateFields.includes(key) ? currentValue : currentData[key]; + let finalValue = dateFields.includes(key) ? currentValue : currentData[key]; + + // ๐Ÿ”ง ๋ฐฐ์—ด์ด๋ฉด ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ (๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ) + if (Array.isArray(finalValue)) { + const isRepeaterData = finalValue.length > 0 && + typeof finalValue[0] === "object" && + finalValue[0] !== null && + ("_targetTable" in finalValue[0] || "_isNewItem" in finalValue[0] || "_existingRecord" in finalValue[0]); + + if (!isRepeaterData) { + // ๐Ÿ”ง ์†์ƒ๋œ ๊ฐ’ ํ•„ํ„ฐ๋ง ํ—ฌํผ + const isValidValue = (v: any): boolean => { + if (typeof v === "number" && !isNaN(v)) return true; + if (typeof v !== "string") return false; + if (!v || v.trim() === "") return false; + if (v.includes("{") || v.includes("}") || v.includes('"') || v.includes("\\")) return false; + return true; + }; + + const validValues = finalValue + .map((v: any) => typeof v === "number" ? String(v) : v) + .filter(isValidValue); + + const stringValue = validValues.join(","); + console.log(`๐Ÿ”ง [EditModal ๊ทธ๋ฃนUPDATE] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜: ${key}`, { original: finalValue.length, valid: validValues.length, converted: stringValue }); + finalValue = stringValue; + } + } + + changedData[key] = finalValue; } }); @@ -704,7 +733,12 @@ export const EditModal: React.FC = ({ className }) => { controlConfig, }); - if (controlConfig?.enableDataflowControl && controlConfig?.dataflowTiming === "after") { + // ๐Ÿ”ง executionTiming ์ฒดํฌ: dataflowTiming ๋˜๋Š” flowConfig.executionTiming ๋˜๋Š” flowControls ํ™•์ธ + const flowTiming = controlConfig?.dataflowTiming + || controlConfig?.dataflowConfig?.flowConfig?.executionTiming + || (controlConfig?.dataflowConfig?.flowControls?.length > 0 ? "after" : null); + + if (controlConfig?.enableDataflowControl && flowTiming === "after") { console.log("๐ŸŽฏ [EditModal] ์ €์žฅ ํ›„ ์ œ์–ด๋กœ์ง ๋ฐœ๊ฒฌ:", controlConfig.dataflowConfig); // buttonActions์˜ executeAfterSaveControl ๋™์  import @@ -762,41 +796,51 @@ export const EditModal: React.FC = ({ className }) => { } } - // ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์žˆ๋Š” ํ•„๋“œ์— ๋Œ€ํ•ด allocateCode ํ˜ธ์ถœ + // ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์žˆ๋Š” ํ•„๋“œ์— ๋Œ€ํ•ด allocateCode ํ˜ธ์ถœ (๐Ÿš€ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ์ตœ์ ํ™”) if (Object.keys(fieldsWithNumbering).length > 0) { - console.log("๐ŸŽฏ [EditModal] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์‹œ์ž‘"); + console.log("๐ŸŽฏ [EditModal] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์‹œ์ž‘, formData:", { + material: formData.material, + allKeys: Object.keys(formData), + }); const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); - let hasAllocationFailure = false; - const failedFields: string[] = []; - - for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) { - try { + // ๐Ÿš€ Promise.all๋กœ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ (์—ฌ๋Ÿฌ ์ฑ„๋ฒˆ ํ•„๋“œ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ์„ฑ๋Šฅ ํ–ฅ์ƒ) + const allocationPromises = Object.entries(fieldsWithNumbering).map( + async ([fieldName, ruleId]) => { + const userInputCode = dataToSave[fieldName] as string; console.log(`๐Ÿ”„ [EditModal] ${fieldName} ํ•„๋“œ์— ๋Œ€ํ•ด allocateCode ํ˜ธ์ถœ: ${ruleId}`); - const allocateResult = await allocateNumberingCode(ruleId); - - if (allocateResult.success && allocateResult.data?.generatedCode) { - const newCode = allocateResult.data.generatedCode; - console.log(`โœ… [EditModal] ${fieldName} ์ƒˆ ์ฝ”๋“œ ํ• ๋‹น: ${dataToSave[fieldName]} โ†’ ${newCode}`); - dataToSave[fieldName] = newCode; - } else { - console.warn(`โš ๏ธ [EditModal] ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์‹คํŒจ:`, allocateResult.error); - if (!dataToSave[fieldName] || dataToSave[fieldName] === "") { - hasAllocationFailure = true; - failedFields.push(fieldName); + + try { + const allocateResult = await allocateNumberingCode(ruleId, userInputCode, formData); + + if (allocateResult.success && allocateResult.data?.generatedCode) { + return { fieldName, success: true, code: allocateResult.data.generatedCode }; + } else { + console.warn(`โš ๏ธ [EditModal] ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์‹คํŒจ:`, allocateResult.error); + return { fieldName, success: false, hasExistingValue: !!(dataToSave[fieldName]) }; } + } catch (allocateError) { + console.error(`โŒ [EditModal] ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์˜ค๋ฅ˜:`, allocateError); + return { fieldName, success: false, hasExistingValue: !!(dataToSave[fieldName]) }; } - } catch (allocateError) { - console.error(`โŒ [EditModal] ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์˜ค๋ฅ˜:`, allocateError); - if (!dataToSave[fieldName] || dataToSave[fieldName] === "") { - hasAllocationFailure = true; - failedFields.push(fieldName); - } + } + ); + + const allocationResults = await Promise.all(allocationPromises); + + // ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ + const failedFields: string[] = []; + for (const result of allocationResults) { + if (result.success && result.code) { + console.log(`โœ… [EditModal] ${result.fieldName} ์ƒˆ ์ฝ”๋“œ ํ• ๋‹น: ${result.code}`); + dataToSave[result.fieldName] = result.code; + } else if (!result.hasExistingValue) { + failedFields.push(result.fieldName); } } // ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์‹คํŒจ ์‹œ ์ €์žฅ ์ค‘๋‹จ - if (hasAllocationFailure) { + if (failedFields.length > 0) { const fieldNames = failedFields.join(", "); toast.error(`์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค (${fieldNames}). ํ™”๋ฉด ์„ค์ •์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.`); console.error(`โŒ [EditModal] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์‹คํŒจ๋กœ ์ €์žฅ ์ค‘๋‹จ. ์‹คํŒจ ํ•„๋“œ: ${fieldNames}`); @@ -812,12 +856,39 @@ export const EditModal: React.FC = ({ className }) => { } // ๐Ÿ†• ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ(๋ฐฐ์—ด)๋ฅผ ๋งˆ์Šคํ„ฐ ์ €์žฅ์—์„œ ์ œ์™ธ (V2Repeater๊ฐ€ ๋ณ„๋„๋กœ ์ €์žฅ) + // ๐Ÿ”ง ๋‹จ, ๋‹ค์ค‘ ์„ ํƒ ๋ฐฐ์—ด์€ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅ const masterDataToSave: Record = {}; Object.entries(dataToSave).forEach(([key, value]) => { if (!Array.isArray(value)) { masterDataToSave[key] = value; } else { - console.log(`๐Ÿ”„ [EditModal] ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ (๋ณ„๋„ ์ €์žฅ): ${key}, ${value.length}๊ฐœ ํ•ญ๋ชฉ`); + // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ์ธ์ง€ ํ™•์ธ (๊ฐ์ฒด ๋ฐฐ์—ด์ด๊ณ  _targetTable ๋˜๋Š” _isNewItem์ด ์žˆ์œผ๋ฉด ๋ฆฌํ”ผํ„ฐ) + const isRepeaterData = value.length > 0 && + typeof value[0] === "object" && + value[0] !== null && + ("_targetTable" in value[0] || "_isNewItem" in value[0] || "_existingRecord" in value[0]); + + if (isRepeaterData) { + console.log(`๐Ÿ”„ [EditModal] ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ (๋ณ„๋„ ์ €์žฅ): ${key}, ${value.length}๊ฐœ ํ•ญ๋ชฉ`); + } else { + // ๐Ÿ”ง ์†์ƒ๋œ ๊ฐ’ ํ•„ํ„ฐ๋ง ํ—ฌํผ (์ค‘๊ด„ํ˜ธ, ๋”ฐ์˜ดํ‘œ, ๋ฐฑ์Šฌ๋ž˜์‹œ ํฌํ•จ ์‹œ ๋ฌดํšจ) + const isValidValue = (v: any): boolean => { + if (typeof v === "number" && !isNaN(v)) return true; + if (typeof v !== "string") return false; + if (!v || v.trim() === "") return false; + if (v.includes("{") || v.includes("}") || v.includes('"') || v.includes("\\")) return false; + return true; + }; + + // ๐Ÿ”ง ๋‹ค์ค‘ ์„ ํƒ ๋ฐฐ์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ (์†์ƒ๋œ ๊ฐ’ ํ•„ํ„ฐ๋ง) + const validValues = value + .map((v: any) => typeof v === "number" ? String(v) : v) + .filter(isValidValue); + + const stringValue = validValues.join(","); + console.log(`๐Ÿ”ง [EditModal CREATE] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜: ${key}`, { original: value.length, valid: validValues.length, converted: stringValue }); + masterDataToSave[key] = stringValue; + } } }); @@ -863,7 +934,12 @@ export const EditModal: React.FC = ({ className }) => { console.log("[EditModal] INSERT ์™„๋ฃŒ ํ›„ ์ œ์–ด๋กœ์ง ์‹คํ–‰ ์‹œ๋„", { controlConfig }); - if (controlConfig?.enableDataflowControl && controlConfig?.dataflowTiming === "after") { + // ๐Ÿ”ง executionTiming ์ฒดํฌ: dataflowTiming ๋˜๋Š” flowConfig.executionTiming ๋˜๋Š” flowControls ํ™•์ธ + const flowTimingInsert = controlConfig?.dataflowTiming + || controlConfig?.dataflowConfig?.flowConfig?.executionTiming + || (controlConfig?.dataflowConfig?.flowControls?.length > 0 ? "after" : null); + + if (controlConfig?.enableDataflowControl && flowTimingInsert === "after") { console.log("๐ŸŽฏ [EditModal] ์ €์žฅ ํ›„ ์ œ์–ด๋กœ์ง ๋ฐœ๊ฒฌ:", controlConfig.dataflowConfig); const { ButtonActionExecutor } = await import("@/lib/utils/buttonActions"); @@ -896,7 +972,47 @@ export const EditModal: React.FC = ({ className }) => { const changedData: Record = {}; Object.keys(formData).forEach((key) => { if (formData[key] !== originalData[key]) { - changedData[key] = formData[key]; + let value = formData[key]; + + // ๐Ÿ”ง ๋ฐฐ์—ด์ด๋ฉด ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ (๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ) + if (Array.isArray(value)) { + // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ์ธ์ง€ ํ™•์ธ (๊ฐ์ฒด ๋ฐฐ์—ด์ด๊ณ  _targetTable ๋˜๋Š” _isNewItem์ด ์žˆ์œผ๋ฉด ๋ฆฌํ”ผํ„ฐ) + const isRepeaterData = value.length > 0 && + typeof value[0] === "object" && + value[0] !== null && + ("_targetTable" in value[0] || "_isNewItem" in value[0] || "_existingRecord" in value[0]); + + if (!isRepeaterData) { + // ๐Ÿ”ง ์†์ƒ๋œ ๊ฐ’ ํ•„ํ„ฐ๋ง ํ—ฌํผ (์ค‘๊ด„ํ˜ธ, ๋”ฐ์˜ดํ‘œ, ๋ฐฑ์Šฌ๋ž˜์‹œ ํฌํ•จ ์‹œ ๋ฌดํšจ) + const isValidValue = (v: any): boolean => { + if (typeof v === "number" && !isNaN(v)) return true; + if (typeof v !== "string") return false; + if (!v || v.trim() === "") return false; + // ์†์ƒ๋œ PostgreSQL ๋ฐฐ์—ด ํ˜•์‹ ๊ฐ์ง€ + if (v.includes("{") || v.includes("}") || v.includes('"') || v.includes("\\")) return false; + return true; + }; + + // ๐Ÿ”ง ๋‹ค์ค‘ ์„ ํƒ ๋ฐฐ์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ (์†์ƒ๋œ ๊ฐ’ ํ•„ํ„ฐ๋ง) + const validValues = value + .map((v: any) => typeof v === "number" ? String(v) : v) + .filter(isValidValue); + + if (validValues.length !== value.length) { + console.warn(`โš ๏ธ [EditModal UPDATE] ์†์ƒ๋œ ๊ฐ’ ํ•„ํ„ฐ๋ง: ${key}`, { + before: value.length, + after: validValues.length, + removed: value.filter((v: any) => !isValidValue(v)) + }); + } + + const stringValue = validValues.join(","); + console.log(`๐Ÿ”ง [EditModal UPDATE] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜: ${key}`, { original: value.length, valid: validValues.length, converted: stringValue }); + value = stringValue; + } + } + + changedData[key] = value; } }); @@ -936,7 +1052,12 @@ export const EditModal: React.FC = ({ className }) => { console.log("[EditModal] UPDATE ์™„๋ฃŒ ํ›„ ์ œ์–ด๋กœ์ง ์‹คํ–‰ ์‹œ๋„", { controlConfig }); - if (controlConfig?.enableDataflowControl && controlConfig?.dataflowTiming === "after") { + // ๐Ÿ”ง executionTiming ์ฒดํฌ: dataflowTiming ๋˜๋Š” flowConfig.executionTiming ๋˜๋Š” flowControls ํ™•์ธ + const flowTimingUpdate = controlConfig?.dataflowTiming + || controlConfig?.dataflowConfig?.flowConfig?.executionTiming + || (controlConfig?.dataflowConfig?.flowControls?.length > 0 ? "after" : null); + + if (controlConfig?.enableDataflowControl && flowTimingUpdate === "after") { console.log("๐ŸŽฏ [EditModal] ์ €์žฅ ํ›„ ์ œ์–ด๋กœ์ง ๋ฐœ๊ฒฌ:", controlConfig.dataflowConfig); const { ButtonActionExecutor } = await import("@/lib/utils/buttonActions"); diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index 2c400df5..8efde578 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -43,7 +43,7 @@ import { } from "lucide-react"; import { tableTypeApi } from "@/lib/api/screen"; import { commonCodeApi } from "@/lib/api/commonCode"; -import { apiClient, getCurrentUser, UserInfo } from "@/lib/api/client"; +import { apiClient, getCurrentUser, UserInfo, getFullImageUrl } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; import { DataTableComponent, DataTableColumn, DataTableFilter } from "@/types/screen-legacy-backup"; import { cn } from "@/lib/utils"; @@ -2224,6 +2224,38 @@ export const InteractiveDataTable: React.FC = ({ // ํŒŒ์ผ ํƒ€์ž… ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ (๊ฐ€์ƒ ํŒŒ์ผ ์ปฌ๋Ÿผ ํฌํ•จ) const isFileColumn = actualWebType === "file" || column.columnName === "file_path" || column.isVirtualFileColumn; + // ๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€ ํƒ€์ž… ์ปฌ๋Ÿผ: ์ธ๋„ค์ผ๋กœ ํ‘œ์‹œ + const isImageColumn = actualWebType === "image" || actualWebType === "img"; + if (isImageColumn && value) { + // value๊ฐ€ objid (์ˆซ์ž ๋˜๋Š” ์ˆซ์ž ๋ฌธ์ž์—ด)์ธ ๊ฒฝ์šฐ ํŒŒ์ผ API URL ์‚ฌ์šฉ + // ๐Ÿ”‘ download ๋Œ€์‹  preview ์‚ฌ์šฉ (๊ณต๊ฐœ ์ ‘๊ทผ ํ—ˆ์šฉ) + const isObjid = /^\d+$/.test(String(value)); + // ๐Ÿ”‘ ์ƒ๋Œ€ ๊ฒฝ๋กœ(/api/...) ๋Œ€์‹  ์ „์ฒด URL ์‚ฌ์šฉ (Docker ํ™˜๊ฒฝ์—์„œ Next.js rewrite ์˜์กด ๋ฐฉ์ง€) + const imageUrl = isObjid + ? getFilePreviewUrl(String(value)) + : getFullImageUrl(String(value)); + + return ( +
+ ์ด๋ฏธ์ง€ { + e.stopPropagation(); + // ์ด๋ฏธ์ง€ ํด๋ฆญ ์‹œ ํฌ๊ฒŒ ๋ณด๊ธฐ (์ƒˆ ํƒญ์—์„œ ์—ด๊ธฐ) + window.open(imageUrl, "_blank"); + }} + onError={(e) => { + // ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ ์•„์ด์ฝ˜ ํ‘œ์‹œ + (e.target as HTMLImageElement).style.display = "none"; + }} + /> +
+ ); + } + // ํŒŒ์ผ ํƒ€์ž… ์ปฌ๋Ÿผ์€ ํŒŒ์ผ ์•„์ด์ฝ˜์œผ๋กœ ํ‘œ์‹œ (์ปฌ๋Ÿผ๋ณ„ ํŒŒ์ผ ๊ด€๋ฆฌ) if (isFileColumn && rowData) { // ํ˜„์žฌ ํ–‰์˜ ๊ธฐ๋ณธํ‚ค ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index fbcf8243..6b9a092b 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useCallback } from "react"; +import React, { useState, useCallback, useEffect, useMemo } from "react"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; @@ -16,7 +16,7 @@ import { useAuth } from "@/hooks/useAuth"; import { uploadFilesAndCreateData } from "@/lib/api/file"; import { toast } from "sonner"; import { useCascadingDropdown } from "@/hooks/useCascadingDropdown"; -import { CascadingDropdownConfig } from "@/types/screen-management"; +import { CascadingDropdownConfig, LayerDefinition } from "@/types/screen-management"; import { ComponentData, WidgetComponent, @@ -164,6 +164,8 @@ interface InteractiveScreenViewerProps { enableAutoSave?: boolean; showToastMessages?: boolean; }; + // ๐Ÿ†• ๋ ˆ์ด์–ด ์‹œ์Šคํ…œ ์ง€์› + layers?: LayerDefinition[]; } export const InteractiveScreenViewer: React.FC = ({ @@ -178,6 +180,7 @@ export const InteractiveScreenViewer: React.FC = ( tableColumns = [], showValidationPanel = false, validationOptions = {}, + layers = [], // ๐Ÿ†• ๋ ˆ์ด์–ด ๋ชฉ๋ก }) => { // component๊ฐ€ ์—†์œผ๋ฉด ๋นˆ div ๋ฐ˜ํ™˜ if (!component) { @@ -206,9 +209,81 @@ export const InteractiveScreenViewer: React.FC = ( // ํŒ์—… ์ „์šฉ formData ์ƒํƒœ const [popupFormData, setPopupFormData] = useState>({}); + // ๐Ÿ†• ๋ ˆ์ด์–ด ์ƒํƒœ ๊ด€๋ฆฌ (๋Ÿฐํƒ€์ž„์šฉ) + const [activeLayerIds, setActiveLayerIds] = useState([]); + + // ๐Ÿ†• ์ดˆ๊ธฐ ๋ ˆ์ด์–ด ์„ค์ • (visible์ธ ๋ ˆ์ด์–ด๋“ค) + useEffect(() => { + if (layers.length > 0) { + const initialActiveLayers = layers.filter((l) => l.isVisible).map((l) => l.id); + setActiveLayerIds(initialActiveLayers); + } + }, [layers]); + + // ๐Ÿ†• ๋ ˆ์ด์–ด ์ œ์–ด ์•ก์…˜ ํ•ธ๋“ค๋Ÿฌ + const handleLayerAction = useCallback((action: string, layerId: string) => { + setActiveLayerIds((prev) => { + switch (action) { + case "show": + return [...new Set([...prev, layerId])]; + case "hide": + return prev.filter((id) => id !== layerId); + case "toggle": + return prev.includes(layerId) + ? prev.filter((id) => id !== layerId) + : [...prev, layerId]; + case "exclusive": + // ํ•ด๋‹น ๋ ˆ์ด์–ด๋งŒ ํ‘œ์‹œ (๋ชจ๋‹ฌ/๋“œ๋กœ์–ด ๊ฐ™์€ ํŠน์ˆ˜ ๋ ˆ์ด์–ด ์ฒ˜๋ฆฌ์— ํ™œ์šฉ) + return [...prev, layerId]; + default: + return prev; + } + }); + }, []); + // ํ†ตํ•ฉ๋œ ํผ ๋ฐ์ดํ„ฐ const finalFormData = { ...localFormData, ...externalFormData }; + // ๐Ÿ†• ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด ๋กœ์ง (formData ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ํ‰๊ฐ€) + useEffect(() => { + layers.forEach((layer) => { + if (layer.type === "conditional" && layer.condition) { + const { targetComponentId, operator, value } = layer.condition; + + // 1. ์ปดํฌ๋„ŒํŠธ ID๋กœ ๋Œ€์ƒ ์ปดํฌ๋„ŒํŠธ ์ฐพ๊ธฐ + const targetComponent = allComponents.find((c) => c.id === targetComponentId); + + // 2. ์ปดํฌ๋„ŒํŠธ์˜ columnName์œผ๋กœ formData์—์„œ ๊ฐ’ ์กฐํšŒ + // columnName์ด ์—†์œผ๋ฉด ์ปดํฌ๋„ŒํŠธ ID๋กœ ํด๋ฐฑ + const fieldKey = + (targetComponent as any)?.columnName || + (targetComponent as any)?.componentConfig?.columnName || + targetComponentId; + + const targetValue = finalFormData[fieldKey]; + + let isMatch = false; + switch (operator) { + case "eq": + isMatch = targetValue == value; + break; + case "neq": + isMatch = targetValue != value; + break; + case "in": + isMatch = Array.isArray(value) && value.includes(targetValue); + break; + } + + if (isMatch) { + handleLayerAction("show", layer.id); + } else { + handleLayerAction("hide", layer.id); + } + } + }); + }, [finalFormData, layers, allComponents, handleLayerAction]); + // ๊ฐœ์„ ๋œ ๊ฒ€์ฆ ์‹œ์Šคํ…œ (์„ ํƒ์  ํ™œ์„ฑํ™”) const enhancedValidation = enableEnhancedValidation && screenInfo && tableColumns.length > 0 ? useFormValidation( @@ -1395,7 +1470,6 @@ export const InteractiveScreenViewer: React.FC = ( > = ( ))} - , + ); } @@ -2124,6 +2198,159 @@ export const InteractiveScreenViewer: React.FC = ( } : component; + // ๐Ÿ†• ๋ ˆ์ด์–ด๋ณ„ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ํ•จ์ˆ˜ + const renderLayerComponents = useCallback((layer: LayerDefinition) => { + // ํ™œ์„ฑํ™”๋˜์ง€ ์•Š์€ ๋ ˆ์ด์–ด๋Š” ๋ Œ๋”๋งํ•˜์ง€ ์•Š์Œ + if (!activeLayerIds.includes(layer.id)) return null; + + // ๋ชจ๋‹ฌ ๋ ˆ์ด์–ด ์ฒ˜๋ฆฌ + if (layer.type === "modal") { + const modalStyle: React.CSSProperties = { + ...(layer.overlayConfig?.backgroundColor && { backgroundColor: layer.overlayConfig.backgroundColor }), + ...(layer.overlayConfig?.backdropBlur && { backdropFilter: `blur(${layer.overlayConfig.backdropBlur}px)` }), + }; + + return ( + handleLayerAction("hide", layer.id)}> + + + {layer.name} + +
+ {layer.components.map((comp) => ( +
+ +
+ ))} +
+
+
+ ); + } + + // ๋“œ๋กœ์–ด ๋ ˆ์ด์–ด ์ฒ˜๋ฆฌ + if (layer.type === "drawer") { + const drawerPosition = layer.overlayConfig?.position || "right"; + const drawerWidth = layer.overlayConfig?.width || "400px"; + const drawerHeight = layer.overlayConfig?.height || "100%"; + + const drawerPositionStyles: Record = { + right: { right: 0, top: 0, width: drawerWidth, height: "100%" }, + left: { left: 0, top: 0, width: drawerWidth, height: "100%" }, + bottom: { bottom: 0, left: 0, width: "100%", height: drawerHeight }, + top: { top: 0, left: 0, width: "100%", height: drawerHeight }, + }; + + return ( +
handleLayerAction("hide", layer.id)} + > + {/* ๋ฐฑ๋“œ๋กญ */} +
+ {/* ๋“œ๋กœ์–ด ํŒจ๋„ */} +
e.stopPropagation()} + > +
+

{layer.name}

+ +
+
+ {layer.components.map((comp) => ( +
+ +
+ ))} +
+
+
+ ); + } + + // ์ผ๋ฐ˜/์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด (base, conditional) + return ( +
+ {layer.components.map((comp) => ( +
+ +
+ ))} +
+ ); + }, [activeLayerIds, handleLayerAction, externalFormData, onFormDataChange, screenInfo]); + return ( @@ -2147,6 +2374,9 @@ export const InteractiveScreenViewer: React.FC = (
+ {/* ๐Ÿ†• ๋ ˆ์ด์–ด ๋ Œ๋”๋ง */} + {layers.length > 0 && layers.map(renderLayerComponents)} + {/* ๊ฐœ์„ ๋œ ๊ฒ€์ฆ ํŒจ๋„ (์„ ํƒ์  ํ‘œ์‹œ) */} {showValidationPanel && enhancedValidation && (
diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 735fb53c..73e6819d 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -335,13 +335,42 @@ export const InteractiveScreenViewerDynamic: React.FC { - // ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ํ‰๊ฐ€ + // ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ํ‰๊ฐ€ (๊ธฐ์กด conditional ์‹œ์Šคํ…œ) const conditionalResult = evaluateConditional(comp.conditional, formData, allComponents); // ์กฐ๊ฑด์— ๋”ฐ๋ผ ์ˆจ๊น€ ์ฒ˜๋ฆฌ if (!conditionalResult.visible) { return null; } + + // ๐Ÿ†• conditionalConfig ์‹œ์Šคํ…œ ์ฒดํฌ (V2 ๋ ˆ์ด์•„์›ƒ์šฉ) + const conditionalConfig = (comp as any).componentConfig?.conditionalConfig; + if (conditionalConfig?.enabled && formData) { + const { field, operator, value, action } = conditionalConfig; + const fieldValue = formData[field]; + + let conditionMet = false; + switch (operator) { + case "=": + case "==": + case "===": + conditionMet = fieldValue === value; + break; + case "!=": + case "!==": + conditionMet = fieldValue !== value; + break; + default: + conditionMet = fieldValue === value; + } + + if (action === "show" && !conditionMet) { + return null; + } + if (action === "hide" && conditionMet) { + return null; + } + } // ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ ์ฒ˜๋ฆฌ if (isDataTableComponent(comp)) { @@ -533,11 +562,31 @@ export const InteractiveScreenViewerDynamic: React.FC = {}; + + // ํŒŒ์ผ ์—…๋กœ๋“œ ์ปดํฌ๋„ŒํŠธ์˜ columnName ๋ชฉ๋ก ์ˆ˜์ง‘ (v2-media, file-upload ๋ชจ๋‘ ํฌํ•จ) + const mediaColumnNames = new Set( + allComponents + .filter((c: any) => + c.componentType === "v2-media" || + c.componentType === "file-upload" || + c.url?.includes("v2-media") || + c.url?.includes("file-upload") + ) + .map((c: any) => c.columnName || c.componentConfig?.columnName) + .filter(Boolean) + ); + Object.entries(formData).forEach(([key, value]) => { - // ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ๋Š” ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ์ด๋ฏ€๋กœ ์ œ์™ธ if (!Array.isArray(value)) { + // ๋ฐฐ์—ด์ด ์•„๋‹Œ ๊ฐ’์€ ๊ทธ๋Œ€๋กœ ์ €์žฅ masterFormData[key] = value; + } else if (mediaColumnNames.has(key)) { + // v2-media ์ปดํฌ๋„ŒํŠธ์˜ ๋ฐฐ์—ด์€ ์ฒซ ๋ฒˆ์งธ ๊ฐ’๋งŒ ์ €์žฅ (๋‹จ์ผ ํŒŒ์ผ ์ปฌ๋Ÿผ ๋Œ€์‘) + // ๋˜๋Š” JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜๋ ค๋ฉด JSON.stringify(value) ์‚ฌ์šฉ + masterFormData[key] = value.length > 0 ? value[0] : null; + console.log(`๐Ÿ“ท ๋ฏธ๋””์–ด ๋ฐ์ดํ„ฐ ์ €์žฅ: ${key}, objid: ${masterFormData[key]}`); } else { console.log(`๐Ÿ”„ ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ (๋ณ„๋„ ์ €์žฅ): ${key}, ${value.length}๊ฐœ ํ•ญ๋ชฉ`); } @@ -1018,22 +1067,35 @@ export const InteractiveScreenViewerDynamic: React.FC 0 ? "visible" : undefined, }; return ( <>
- {/* ๋ผ๋ฒจ ์ˆจ๊น€ - ๋ชจ๋‹ฌ์—์„œ๋Š” ๋ผ๋ฒจ์„ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ */} - {/* ์œ„์ ฏ ๋ Œ๋”๋ง */} + {/* ์œ„์ ฏ ๋ Œ๋”๋ง (๋ผ๋ฒจ์€ V2Input ๋‚ด๋ถ€์—์„œ absolute๋กœ ํ‘œ์‹œ๋จ) */} {renderInteractiveWidget(component)}
diff --git a/frontend/components/screen/LayerConditionPanel.tsx b/frontend/components/screen/LayerConditionPanel.tsx new file mode 100644 index 00000000..4304aa55 --- /dev/null +++ b/frontend/components/screen/LayerConditionPanel.tsx @@ -0,0 +1,371 @@ +import React, { useState, useEffect, useMemo, useCallback } from "react"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Loader2, AlertCircle, Check, X } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { ComponentData, LayerCondition, LayerDefinition } from "@/types/screen-management"; +import { getCodesByCategory, CodeItem } from "@/lib/api/codeManagement"; + +interface LayerConditionPanelProps { + layer: LayerDefinition; + components: ComponentData[]; // ํ™”๋ฉด์˜ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ + onUpdateCondition: (condition: LayerCondition | undefined) => void; + onClose?: () => void; +} + +// ์กฐ๊ฑด ์—ฐ์‚ฐ์ž ์˜ต์…˜ +const OPERATORS = [ + { value: "eq", label: "๊ฐ™์Œ (=)" }, + { value: "neq", label: "๊ฐ™์ง€ ์•Š์Œ (โ‰ )" }, + { value: "in", label: "ํฌํ•จ (in)" }, +] as const; + +type OperatorType = "eq" | "neq" | "in"; + +export const LayerConditionPanel: React.FC = ({ + layer, + components, + onUpdateCondition, + onClose, +}) => { + // ์กฐ๊ฑด ์„ค์ • ์ƒํƒœ + const [targetComponentId, setTargetComponentId] = useState( + layer.condition?.targetComponentId || "" + ); + const [operator, setOperator] = useState( + (layer.condition?.operator as OperatorType) || "eq" + ); + const [value, setValue] = useState( + layer.condition?.value?.toString() || "" + ); + const [multiValues, setMultiValues] = useState( + Array.isArray(layer.condition?.value) ? layer.condition.value : [] + ); + + // ์ฝ”๋“œ ๋ชฉ๋ก ๋กœ๋”ฉ ์ƒํƒœ + const [codeOptions, setCodeOptions] = useState([]); + const [isLoadingCodes, setIsLoadingCodes] = useState(false); + const [codeLoadError, setCodeLoadError] = useState(null); + + // ํŠธ๋ฆฌ๊ฑฐ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ ํ•„ํ„ฐ๋ง (์…€๋ ‰ํŠธ, ๋ผ๋””์˜ค, ์ฝ”๋“œ ํƒ€์ž… ๋“ฑ) + const triggerableComponents = useMemo(() => { + return components.filter((comp) => { + const componentType = (comp.componentType || "").toLowerCase(); + const widgetType = ((comp as any).widgetType || "").toLowerCase(); + const webType = ((comp as any).webType || "").toLowerCase(); + const inputType = ((comp as any).componentConfig?.inputType || "").toLowerCase(); + + // ์…€๋ ‰ํŠธ, ๋ผ๋””์˜ค, ์ฝ”๋“œ ํƒ€์ž… ์ปดํฌ๋„ŒํŠธ๋งŒ ํ—ˆ์šฉ + const triggerTypes = ["select", "radio", "code", "checkbox", "toggle"]; + const isTriggerType = triggerTypes.some((type) => + componentType.includes(type) || + widgetType.includes(type) || + webType.includes(type) || + inputType.includes(type) + ); + + return isTriggerType; + }); + }, [components]); + + // ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด + const selectedComponent = useMemo(() => { + return components.find((c) => c.id === targetComponentId); + }, [components, targetComponentId]); + + // ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ์˜ ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ + const codeCategory = useMemo(() => { + if (!selectedComponent) return null; + + // codeCategory ํ™•์ธ (๋‹ค์–‘ํ•œ ์œ„์น˜์— ์žˆ์„ ์ˆ˜ ์žˆ์Œ) + const category = + (selectedComponent as any).codeCategory || + (selectedComponent as any).componentConfig?.codeCategory || + (selectedComponent as any).webTypeConfig?.codeCategory; + + return category || null; + }, [selectedComponent]); + + // ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์‹œ ์ฝ”๋“œ ๋ชฉ๋ก ๋กœ๋“œ + useEffect(() => { + if (!codeCategory) { + setCodeOptions([]); + return; + } + + const loadCodes = async () => { + setIsLoadingCodes(true); + setCodeLoadError(null); + + try { + const codes = await getCodesByCategory(codeCategory); + setCodeOptions(codes); + } catch (error: any) { + console.error("์ฝ”๋“œ ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:", error); + setCodeLoadError(error.message || "์ฝ”๋“œ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + setCodeOptions([]); + } finally { + setIsLoadingCodes(false); + } + }; + + loadCodes(); + }, [codeCategory]); + + // ์กฐ๊ฑด ์ €์žฅ + const handleSave = useCallback(() => { + if (!targetComponentId) { + return; + } + + const condition: LayerCondition = { + targetComponentId, + operator, + value: operator === "in" ? multiValues : value, + }; + + onUpdateCondition(condition); + onClose?.(); + }, [targetComponentId, operator, value, multiValues, onUpdateCondition, onClose]); + + // ์กฐ๊ฑด ์‚ญ์ œ + const handleClear = useCallback(() => { + onUpdateCondition(undefined); + setTargetComponentId(""); + setOperator("eq"); + setValue(""); + setMultiValues([]); + onClose?.(); + }, [onUpdateCondition, onClose]); + + // in ์—ฐ์‚ฐ์ž์šฉ ๋‹ค์ค‘ ๊ฐ’ ํ† ๊ธ€ + const toggleMultiValue = useCallback((val: string) => { + setMultiValues((prev) => + prev.includes(val) + ? prev.filter((v) => v !== val) + : [...prev, val] + ); + }, []); + + // ์ปดํฌ๋„ŒํŠธ ๋ผ๋ฒจ ๊ฐ€์ ธ์˜ค๊ธฐ + const getComponentLabel = (comp: ComponentData) => { + return comp.label || (comp as any).columnName || comp.id; + }; + + return ( +
+
+

์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ์„ค์ •

+ {layer.condition && ( + + ์„ค์ •๋จ + + )} +
+ + {/* ํŠธ๋ฆฌ๊ฑฐ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ */} +
+ + + + {/* ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ ํ‘œ์‹œ */} + {codeCategory && ( +
+ ์นดํ…Œ๊ณ ๋ฆฌ: + + {codeCategory} + +
+ )} +
+ + {/* ์—ฐ์‚ฐ์ž ์„ ํƒ */} + {targetComponentId && ( +
+ + +
+ )} + + {/* ์กฐ๊ฑด ๊ฐ’ ์„ ํƒ */} + {targetComponentId && ( +
+ + + {isLoadingCodes ? ( +
+ + ์ฝ”๋“œ ๋ชฉ๋ก ๋กœ๋”ฉ ์ค‘... +
+ ) : codeLoadError ? ( +
+ + {codeLoadError} +
+ ) : codeOptions.length > 0 ? ( + // ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ - ์„ ํƒ UI + operator === "in" ? ( + // ๋‹ค์ค‘ ์„ ํƒ (in ์—ฐ์‚ฐ์ž) +
+ {codeOptions.map((code) => ( +
toggleMultiValue(code.codeValue)} + > +
+ {multiValues.includes(code.codeValue) && ( + + )} +
+ {code.codeName} + ({code.codeValue}) +
+ ))} +
+ ) : ( + // ๋‹จ์ผ ์„ ํƒ (eq, neq ์—ฐ์‚ฐ์ž) + + ) + ) : ( + // ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ - ์ง์ ‘ ์ž…๋ ฅ + setValue(e.target.value)} + placeholder="์กฐ๊ฑด ๊ฐ’ ์ž…๋ ฅ..." + className="h-8 text-xs" + /> + )} + + {/* ์„ ํƒ๋œ ๊ฐ’ ํ‘œ์‹œ (in ์—ฐ์‚ฐ์ž) */} + {operator === "in" && multiValues.length > 0 && ( +
+ {multiValues.map((val) => { + const code = codeOptions.find((c) => c.codeValue === val); + return ( + + {code?.codeName || val} + toggleMultiValue(val)} + /> + + ); + })} +
+ )} +
+ )} + + {/* ํ˜„์žฌ ์กฐ๊ฑด ์š”์•ฝ */} + {targetComponentId && (value || multiValues.length > 0) && ( +
+ ์š”์•ฝ: + + "{getComponentLabel(selectedComponent!)}" ๊ฐ’์ด{" "} + {operator === "eq" && `"${codeOptions.find(c => c.codeValue === value)?.codeName || value}"์™€ ๊ฐ™์œผ๋ฉด`} + {operator === "neq" && `"${codeOptions.find(c => c.codeValue === value)?.codeName || value}"์™€ ๋‹ค๋ฅด๋ฉด`} + {operator === "in" && `[${multiValues.map(v => codeOptions.find(c => c.codeValue === v)?.codeName || v).join(", ")}] ์ค‘ ํ•˜๋‚˜์ด๋ฉด`} + {" "}์ด ๋ ˆ์ด์–ด ํ‘œ์‹œ + +
+ )} + + {/* ๋ฒ„ํŠผ */} +
+ + +
+
+ ); +}; diff --git a/frontend/components/screen/LayerManagerPanel.tsx b/frontend/components/screen/LayerManagerPanel.tsx new file mode 100644 index 00000000..05fb36f3 --- /dev/null +++ b/frontend/components/screen/LayerManagerPanel.tsx @@ -0,0 +1,405 @@ +import React, { useState, useMemo, useCallback } from "react"; +import { useLayer } from "@/contexts/LayerContext"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu"; +import { Badge } from "@/components/ui/badge"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + Eye, + EyeOff, + Lock, + Unlock, + Plus, + Trash2, + GripVertical, + Layers, + SplitSquareVertical, + PanelRight, + ChevronDown, + ChevronRight, + Settings2, + Zap, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { LayerType, LayerDefinition, ComponentData, LayerCondition } from "@/types/screen-management"; +import { LayerConditionPanel } from "./LayerConditionPanel"; + +// ๋ ˆ์ด์–ด ํƒ€์ž…๋ณ„ ์•„์ด์ฝ˜ +const getLayerTypeIcon = (type: LayerType) => { + switch (type) { + case "base": + return ; + case "conditional": + return ; + case "modal": + return ; + case "drawer": + return ; + default: + return ; + } +}; + +// ๋ ˆ์ด์–ด ํƒ€์ž…๋ณ„ ๋ผ๋ฒจ +function getLayerTypeLabel(type: LayerType): string { + switch (type) { + case "base": + return "๊ธฐ๋ณธ"; + case "conditional": + return "์กฐ๊ฑด๋ถ€"; + case "modal": + return "๋ชจ๋‹ฌ"; + case "drawer": + return "๋“œ๋กœ์–ด"; + default: + return type; + } +} + +// ๋ ˆ์ด์–ด ํƒ€์ž…๋ณ„ ์ƒ‰์ƒ +function getLayerTypeColor(type: LayerType): string { + switch (type) { + case "base": + return "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300"; + case "conditional": + return "bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300"; + case "modal": + return "bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300"; + case "drawer": + return "bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300"; + default: + return "bg-gray-100 text-gray-700 dark:bg-gray-900 dark:text-gray-300"; + } +} + +interface LayerItemProps { + layer: LayerDefinition; + isActive: boolean; + componentCount: number; // ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ์ˆ˜ (layout.components ๊ธฐ๋ฐ˜) + allComponents: ComponentData[]; // ์กฐ๊ฑด ์„ค์ •์— ํ•„์š”ํ•œ ์ „์ฒด ์ปดํฌ๋„ŒํŠธ + onSelect: () => void; + onToggleVisibility: () => void; + onToggleLock: () => void; + onRemove: () => void; + onUpdateName: (name: string) => void; + onUpdateCondition: (condition: LayerCondition | undefined) => void; +} + +const LayerItem: React.FC = ({ + layer, + isActive, + componentCount, + allComponents, + onSelect, + onToggleVisibility, + onToggleLock, + onRemove, + onUpdateName, + onUpdateCondition, +}) => { + const [isEditing, setIsEditing] = useState(false); + const [isConditionOpen, setIsConditionOpen] = useState(false); + + // ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด์ธ์ง€ ํ™•์ธ + const isConditionalLayer = layer.type === "conditional"; + // ์กฐ๊ฑด ์„ค์ • ์—ฌ๋ถ€ + const hasCondition = !!layer.condition; + + return ( +
+ {/* ๋ ˆ์ด์–ด ๋ฉ”์ธ ์˜์—ญ */} +
+ {/* ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค */} + + + {/* ๋ ˆ์ด์–ด ์ •๋ณด */} +
+
+ {/* ๋ ˆ์ด์–ด ํƒ€์ž… ์•„์ด์ฝ˜ */} + + {getLayerTypeIcon(layer.type)} + + + {/* ๋ ˆ์ด์–ด ์ด๋ฆ„ */} + {isEditing ? ( + onUpdateName(e.target.value)} + onBlur={() => setIsEditing(false)} + onKeyDown={(e) => { + if (e.key === "Enter") setIsEditing(false); + }} + className="flex-1 bg-transparent outline-none border-b border-primary text-sm" + autoFocus + onClick={(e) => e.stopPropagation()} + /> + ) : ( + { + e.stopPropagation(); + setIsEditing(true); + }} + > + {layer.name} + + )} +
+ + {/* ๋ ˆ์ด์–ด ๋ฉ”ํƒ€ ์ •๋ณด */} +
+ + {getLayerTypeLabel(layer.type)} + + + {componentCount}๊ฐœ ์ปดํฌ๋„ŒํŠธ + + {/* ์กฐ๊ฑด ์„ค์ •๋จ ํ‘œ์‹œ */} + {hasCondition && ( + + + ์กฐ๊ฑด + + )} +
+
+ + {/* ์•ก์…˜ ๋ฒ„ํŠผ๋“ค */} +
+ {/* ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด์ผ ๋•Œ ์กฐ๊ฑด ์„ค์ • ๋ฒ„ํŠผ */} + {isConditionalLayer && ( + + )} + + + + + + {layer.type !== "base" && ( + + )} +
+
+ + {/* ์กฐ๊ฑด ์„ค์ • ํŒจ๋„ (์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด๋งŒ) */} + {isConditionalLayer && isConditionOpen && ( +
+ setIsConditionOpen(false)} + /> +
+ )} +
+ ); +}; + +interface LayerManagerPanelProps { + components?: ComponentData[]; // layout.components๋ฅผ ์ „๋‹ฌ๋ฐ›์Œ +} + +export const LayerManagerPanel: React.FC = ({ components = [] }) => { + const { + layers, + activeLayerId, + setActiveLayerId, + addLayer, + removeLayer, + toggleLayerVisibility, + toggleLayerLock, + updateLayer, + } = useLayer(); + + // ๋ ˆ์ด์–ด ์กฐ๊ฑด ์—…๋ฐ์ดํŠธ ํ•ธ๋“ค๋Ÿฌ + const handleUpdateCondition = useCallback((layerId: string, condition: LayerCondition | undefined) => { + updateLayer(layerId, { condition }); + }, [updateLayer]); + + // ๐Ÿ†• ๊ฐ ๋ ˆ์ด์–ด๋ณ„ ์ปดํฌ๋„ŒํŠธ ์ˆ˜ ๊ณ„์‚ฐ (layout.components ๊ธฐ๋ฐ˜) + const componentCountByLayer = useMemo(() => { + const counts: Record = {}; + + // ๋ชจ๋“  ๋ ˆ์ด์–ด๋ฅผ 0์œผ๋กœ ์ดˆ๊ธฐํ™” + layers.forEach(layer => { + counts[layer.id] = 0; + }); + + // layout.components์—์„œ layerId๋ณ„๋กœ ์นด์šดํŠธ + components.forEach(comp => { + const layerId = comp.layerId || "default-layer"; + if (counts[layerId] !== undefined) { + counts[layerId]++; + } else { + // layerId๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ ˆ์ด์–ด์ธ ๊ฒฝ์šฐ default-layer๋กœ ์นด์šดํŠธ + if (counts["default-layer"] !== undefined) { + counts["default-layer"]++; + } + } + }); + + return counts; + }, [components, layers]); + + return ( +
+ {/* ํ—ค๋” */} +
+
+ +

๋ ˆ์ด์–ด

+ + {layers.length} + +
+ + {/* ๋ ˆ์ด์–ด ์ถ”๊ฐ€ ๋“œ๋กญ๋‹ค์šด */} + + + + + + addLayer("conditional", "์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด")}> + + ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด + + + addLayer("modal", "๋ชจ๋‹ฌ ๋ ˆ์ด์–ด")}> + + ๋ชจ๋‹ฌ ๋ ˆ์ด์–ด + + addLayer("drawer", "๋“œ๋กœ์–ด ๋ ˆ์ด์–ด")}> + + ๋“œ๋กœ์–ด ๋ ˆ์ด์–ด + + + +
+ + {/* ๋ ˆ์ด์–ด ๋ชฉ๋ก */} + +
+ {layers.length === 0 ? ( +
+ ๋ ˆ์ด์–ด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. +
+ ์œ„์˜ + ๋ฒ„ํŠผ์œผ๋กœ ์ถ”๊ฐ€ํ•˜์„ธ์š”. +
+ ) : ( + layers + .slice() + .reverse() // ์ƒ์œ„ ๋ ˆ์ด์–ด๊ฐ€ ์œ„์— ํ‘œ์‹œ + .map((layer) => ( + setActiveLayerId(layer.id)} + onToggleVisibility={() => toggleLayerVisibility(layer.id)} + onToggleLock={() => toggleLayerLock(layer.id)} + onRemove={() => removeLayer(layer.id)} + onUpdateName={(name) => updateLayer(layer.id, { name })} + onUpdateCondition={(condition) => handleUpdateCondition(layer.id, condition)} + /> + )) + )} +
+
+ + {/* ๋„์›€๋ง */} +
+

๋”๋ธ”ํด๋ฆญ: ์ด๋ฆ„ ํŽธ์ง‘ | ๋“œ๋ž˜๊ทธ: ์ˆœ์„œ ๋ณ€๊ฒฝ

+
+
+ ); +}; diff --git a/frontend/components/screen/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index b58a6a1f..5a786616 100644 --- a/frontend/components/screen/RealtimePreview.tsx +++ b/frontend/components/screen/RealtimePreview.tsx @@ -119,6 +119,9 @@ const WidgetRenderer: React.FC<{ tableDisplayData?: any[]; [key: string]: any; }> = ({ component, isDesignMode = false, sortBy, sortOrder, tableDisplayData, ...restProps }) => { + // ๐Ÿ”ง ๋ฌด์กฐ๊ฑด ๋กœ๊ทธ (๋ Œ๋”๋ง ํ™•์ธ์šฉ) + console.log("๐Ÿ“ฆ WidgetRenderer ๋ Œ๋”๋ง:", component.id, "labelDisplay:", component.style?.labelDisplay); + // ์œ„์ ฏ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ ๋นˆ div ๋ฐ˜ํ™˜ if (!isWidgetComponent(component)) { return
์œ„์ ฏ์ด ์•„๋‹™๋‹ˆ๋‹ค
; @@ -127,9 +130,6 @@ const WidgetRenderer: React.FC<{ const widget = component; const { widgetType, label, placeholder, required, readonly, columnName, style } = widget; - // ๋””๋ฒ„๊น…: ์‹ค์ œ widgetType ๊ฐ’ ํ™•์ธ - // console.log("RealtimePreviewDynamic - widgetType:", widgetType, "columnName:", columnName); - // ์‚ฌ์šฉ์ž๊ฐ€ ํ…Œ๋‘๋ฆฌ๋ฅผ ์„ค์ •ํ–ˆ๋Š”์ง€ ํ™•์ธ const hasCustomBorder = style && (style.borderWidth || style.borderStyle || style.borderColor || style.border); @@ -246,8 +246,17 @@ export const RealtimePreviewDynamic: React.FC = ({ tableDisplayData, // ๐Ÿ†• ํ™”๋ฉด ํ‘œ์‹œ ๋ฐ์ดํ„ฐ ...restProps }) => { + // ๐Ÿ”ง ๋ฌด์กฐ๊ฑด ๋กœ๊ทธ - ํŒŒ์ผ ๋ฐ˜์˜ ํ…Œ์ŠคํŠธ์šฉ (2024-TEST) + console.log("๐Ÿ”ท๐Ÿ”ท๐Ÿ”ท RealtimePreview 2024:", component.id); + const { user } = useAuth(); const { type, id, position, size, style = {} } = component; + + // ๐Ÿ”ง v2 ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ์ถ”์  + if (id?.includes("v2-")) { + console.log("๐Ÿ”ท RealtimePreview ๋ Œ๋”:", id, "type:", type, "labelDisplay:", style?.labelDisplay); + } + const [fileUpdateTrigger, setFileUpdateTrigger] = useState(0); const [actualHeight, setActualHeight] = useState(null); const contentRef = React.useRef(null); @@ -741,6 +750,7 @@ export const RealtimePreviewDynamic: React.FC = ({ {/* ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… - ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๊ธฐ๋ฐ˜ ๋ Œ๋”๋ง (Section Paper, Section Card ๋“ฑ) */} {type === "component" && (() => { + console.log("๐Ÿ“ฆ DynamicComponentRenderer ๋ Œ๋”๋ง:", component.id, "labelDisplay:", component.style?.labelDisplay); const { DynamicComponentRenderer } = require("@/lib/registry/DynamicComponentRenderer"); return ( void; // ๐Ÿ†• ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ ์ฝœ๋ฐฑ onSelectTabComponent?: (tabId: string, compId: string, comp: any) => void; // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ฝœ๋ฐฑ selectedTabComponentId?: string; // ๐Ÿ†• ์„ ํƒ๋œ ํƒญ ์ปดํฌ๋„ŒํŠธ ID + onSelectPanelComponent?: (panelSide: "left" | "right", compId: string, comp: any) => void; // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ฝœ๋ฐฑ + selectedPanelComponentId?: string; // ๐Ÿ†• ์„ ํƒ๋œ ๋ถ„ํ•  ํŒจ๋„ ์ปดํฌ๋„ŒํŠธ ID onResize?: (componentId: string, newSize: { width: number; height: number }) => void; // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ฝœ๋ฐฑ // ๋ฒ„ํŠผ ์•ก์…˜์„ ์œ„ํ•œ props @@ -140,6 +142,8 @@ const RealtimePreviewDynamicComponent: React.FC = ({ onUpdateComponent, // ๐Ÿ†• ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ ์ฝœ๋ฐฑ onSelectTabComponent, // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ฝœ๋ฐฑ selectedTabComponentId, // ๐Ÿ†• ์„ ํƒ๋œ ํƒญ ์ปดํฌ๋„ŒํŠธ ID + onSelectPanelComponent, // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ฝœ๋ฐฑ + selectedPanelComponentId, // ๐Ÿ†• ์„ ํƒ๋œ ๋ถ„ํ•  ํŒจ๋„ ์ปดํฌ๋„ŒํŠธ ID onResize, // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ฝœ๋ฐฑ }) => { // ๐Ÿ†• ํ™”๋ฉด ๋‹ค๊ตญ์–ด ์ปจํ…์ŠคํŠธ @@ -594,12 +598,7 @@ const RealtimePreviewDynamicComponent: React.FC = ({ (contentRef as any).current = node; } }} - className={`${ - (component.type === "component" && (component as any).componentType === "flow-widget") || - ((component as any).componentType === "conditional-container" && !isDesignMode) - ? "h-auto" - : "h-full" - } overflow-visible`} + className="h-full overflow-visible" style={{ width: "100%", maxWidth: "100%" }} > = ({ onUpdateComponent={onUpdateComponent} onSelectTabComponent={onSelectTabComponent} selectedTabComponentId={selectedTabComponentId} + onSelectPanelComponent={onSelectPanelComponent} + selectedPanelComponentId={selectedPanelComponentId} />
- {/* ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด ํ‘œ์‹œ */} + {/* ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด ํ‘œ์‹œ - ๐Ÿ”ง ์˜ค๋ฅธ์ชฝ์œผ๋กœ ์ด๋™ (๋ผ๋ฒจ๊ณผ ๊ฒน์น˜์ง€ ์•Š๋„๋ก) */} {isSelected && ( -
+
{type === "widget" && (
{getWidgetIcon((component as WidgetComponent).widgetType)} @@ -684,7 +685,8 @@ const RealtimePreviewDynamicComponent: React.FC = ({ ); }; -// React.memo๋กœ ๋ž˜ํ•‘ํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง ๋ฐฉ์ง€ +// ๐Ÿ”ง arePropsEqual ์ œ๊ฑฐ - ๊ธฐ๋ณธ React.memo ์‚ฌ์šฉ (๋””๋ฒ„๊น…์šฉ) +// component ๊ฐ์ฒด๊ฐ€ ์ƒˆ๋กœ ์ƒ์„ฑ๋˜๋ฉด ์ž๋™์œผ๋กœ ๋ฆฌ๋ Œ๋”๋ง๋จ export const RealtimePreviewDynamic = React.memo(RealtimePreviewDynamicComponent); // displayName ์„ค์ • (๋””๋ฒ„๊น…์šฉ) diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 743b1cf9..5a96a959 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -17,7 +17,11 @@ import { SCREEN_RESOLUTIONS, } from "@/types/screen"; import { generateComponentId } from "@/lib/utils/generateId"; -import { getComponentIdFromWebType, createV2ConfigFromColumn, getV2ConfigFromWebType } from "@/lib/utils/webTypeMapping"; +import { + getComponentIdFromWebType, + createV2ConfigFromColumn, + getV2ConfigFromWebType, +} from "@/lib/utils/webTypeMapping"; import { createGroupComponent, calculateBoundingBox, @@ -31,6 +35,17 @@ import { snapSizeToGrid, snapToGrid, } from "@/lib/utils/gridUtils"; +import { + alignComponents, + distributeComponents, + matchComponentSize, + toggleAllLabels, + nudgeComponents, + AlignMode, + DistributeDirection, + MatchSizeMode, +} from "@/lib/utils/alignmentUtils"; +import { KeyboardShortcutsModal } from "./modals/KeyboardShortcutsModal"; // 10px ๋‹จ์œ„ ์Šค๋ƒ… ํ•จ์ˆ˜ const snapTo10px = (value: number): number => { @@ -122,9 +137,12 @@ interface ScreenDesignerProps { defaultDevicePreview?: "mobile" | "tablet"; } -// ํŒจ๋„ ์„ค์ • (ํ†ตํ•ฉ ํŒจ๋„ 1๊ฐœ) +import { useLayerOptional, LayerProvider, createDefaultLayer } from "@/contexts/LayerContext"; +import { LayerManagerPanel } from "./LayerManagerPanel"; +import { LayerType, LayerDefinition } from "@/types/screen-management"; + +// ํŒจ๋„ ์„ค์ • ์—…๋ฐ์ดํŠธ const panelConfigs: PanelConfig[] = [ - // ํ†ตํ•ฉ ํŒจ๋„ (์ปดํฌ๋„ŒํŠธ + ํŽธ์ง‘ ํƒญ) { id: "v2", title: "ํŒจ๋„", @@ -133,6 +151,14 @@ const panelConfigs: PanelConfig[] = [ defaultHeight: 700, shortcutKey: "p", }, + { + id: "layer", + title: "๋ ˆ์ด์–ด", + defaultPosition: "right", + defaultWidth: 240, + defaultHeight: 500, + shortcutKey: "l", + }, ]; export default function ScreenDesigner({ @@ -144,9 +170,6 @@ export default function ScreenDesigner({ }: ScreenDesignerProps) { // POP ๋ชจ๋“œ ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ API ๋ถ„๊ธฐ const USE_POP_API = isPop; - // ํŒจ๋„ ์ƒํƒœ ๊ด€๋ฆฌ - const { panelStates, togglePanel, openPanel, closePanel } = usePanelState(panelConfigs); - const [layout, setLayout] = useState({ components: [], gridSettings: { @@ -169,6 +192,9 @@ export default function ScreenDesigner({ // ๋ฉ”๋‰ด ํ• ๋‹น ๋ชจ๋‹ฌ ์ƒํƒœ const [showMenuAssignmentModal, setShowMenuAssignmentModal] = useState(false); + // ๋‹จ์ถ•ํ‚ค ๋„์›€๋ง ๋ชจ๋‹ฌ ์ƒํƒœ + const [showShortcutsModal, setShowShortcutsModal] = useState(false); + // ํŒŒ์ผ์ฒจ๋ถ€ ์ƒ์„ธ ๋ชจ๋‹ฌ ์ƒํƒœ const [showFileAttachmentModal, setShowFileAttachmentModal] = useState(false); const [selectedFileComponent, setSelectedFileComponent] = useState(null); @@ -178,23 +204,39 @@ export default function ScreenDesigner({ SCREEN_RESOLUTIONS[0], // ๊ธฐ๋ณธ๊ฐ’: Full HD ); + // ๐Ÿ†• ํŒจ๋„ ์ƒํƒœ ๊ด€๋ฆฌ (usePanelState ํ›…) + const { panelStates, togglePanel, openPanel, closePanel, closeAllPanels, updatePanelPosition, updatePanelSize } = + usePanelState(panelConfigs); + const [selectedComponent, setSelectedComponent] = useState(null); - // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ƒํƒœ + // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ƒํƒœ (์ค‘์ฒฉ ๊ตฌ์กฐ ์ง€์›) const [selectedTabComponentInfo, setSelectedTabComponentInfo] = useState<{ tabsComponentId: string; // ํƒญ ์ปดํฌ๋„ŒํŠธ ID tabId: string; // ํƒญ ID componentId: string; // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ID component: any; // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ๋ฐ์ดํ„ฐ + // ๐Ÿ†• ์ค‘์ฒฉ ๊ตฌ์กฐ์šฉ: ๋ถ€๋ชจ ๋ถ„ํ•  ํŒจ๋„ ์ •๋ณด + parentSplitPanelId?: string | null; + parentPanelSide?: "left" | "right" | null; + } | null>(null); + + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ƒํƒœ + const [selectedPanelComponentInfo, setSelectedPanelComponentInfo] = useState<{ + splitPanelId: string; // ๋ถ„ํ•  ํŒจ๋„ ์ปดํฌ๋„ŒํŠธ ID + panelSide: "left" | "right"; // ์ขŒ์ธก/์šฐ์ธก ํŒจ๋„ + componentId: string; // ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ID + component: any; // ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ๋ฐ์ดํ„ฐ } | null>(null); // ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์‹œ ํ†ตํ•ฉ ํŒจ๋„ ์ž๋™ ์—ด๊ธฐ const handleComponentSelect = useCallback( (component: ComponentData | null) => { setSelectedComponent(component); - // ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์‹œ ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ํ•ด์ œ + // ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์‹œ ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ/๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ํ•ด์ œ if (component) { setSelectedTabComponentInfo(null); + setSelectedPanelComponentInfo(null); } // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์„ ํƒ๋˜๋ฉด ํ†ตํ•ฉ ํŒจ๋„ ์ž๋™ ์—ด๊ธฐ @@ -205,28 +247,102 @@ export default function ScreenDesigner({ [openPanel], ); - // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ + // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ (์ค‘์ฒฉ ๊ตฌ์กฐ ์ง€์›) const handleSelectTabComponent = useCallback( - (tabsComponentId: string, tabId: string, compId: string, comp: any) => { + ( + tabsComponentId: string, + tabId: string, + compId: string, + comp: any, + // ๐Ÿ†• ์ค‘์ฒฉ ๊ตฌ์กฐ์šฉ: ๋ถ€๋ชจ ๋ถ„ํ•  ํŒจ๋„ ์ •๋ณด (์„ ํƒ์ ) + parentSplitPanelId?: string | null, + parentPanelSide?: "left" | "right" | null, + ) => { if (!compId) { // ํƒญ ์˜์—ญ ๋นˆ ๊ณต๊ฐ„ ํด๋ฆญ ์‹œ ์„ ํƒ ํ•ด์ œ setSelectedTabComponentInfo(null); return; } - + setSelectedTabComponentInfo({ tabsComponentId, tabId, componentId: compId, component: comp, + parentSplitPanelId: parentSplitPanelId || null, + parentPanelSide: parentPanelSide || null, }); - // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์‹œ ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ํ•ด์ œ + // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์‹œ ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ/๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ํ•ด์ œ setSelectedComponent(null); + setSelectedPanelComponentInfo(null); openPanel("v2"); }, [openPanel], ); + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ + const handleSelectPanelComponent = useCallback( + (splitPanelId: string, panelSide: "left" | "right", compId: string, comp: any) => { + // ๐Ÿ› ๋””๋ฒ„๊น…: ์ „๋‹ฌ๋ฐ›์€ comp ํ™•์ธ + console.log("๐Ÿ› [handleSelectPanelComponent] comp:", { + compId, + componentType: comp?.componentType, + selectedTable: comp?.componentConfig?.selectedTable, + fieldMapping: comp?.componentConfig?.fieldMapping, + fieldMappingKeys: comp?.componentConfig?.fieldMapping ? Object.keys(comp.componentConfig.fieldMapping) : [], + }); + + if (!compId) { + // ํŒจ๋„ ์˜์—ญ ๋นˆ ๊ณต๊ฐ„ ํด๋ฆญ ์‹œ ์„ ํƒ ํ•ด์ œ + setSelectedPanelComponentInfo(null); + return; + } + + setSelectedPanelComponentInfo({ + splitPanelId, + panelSide, + componentId: compId, + component: comp, + }); + // ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์‹œ ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ/ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ํ•ด์ œ + setSelectedComponent(null); + setSelectedTabComponentInfo(null); + openPanel("v2"); + }, + [openPanel], + ); + + // ๐Ÿ†• ์ค‘์ฒฉ๋œ ํƒญ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ (๋ถ„ํ•  ํŒจ๋„ ์•ˆ์˜ ํƒญ ์•ˆ์˜ ์ปดํฌ๋„ŒํŠธ) + useEffect(() => { + const handleNestedTabComponentSelect = (event: CustomEvent) => { + const { tabsComponentId, tabId, componentId, component, parentSplitPanelId, parentPanelSide } = event.detail; + + if (!componentId) { + setSelectedTabComponentInfo(null); + return; + } + + console.log("๐ŸŽฏ ์ค‘์ฒฉ๋œ ํƒญ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ:", event.detail); + + setSelectedTabComponentInfo({ + tabsComponentId, + tabId, + componentId, + component, + parentSplitPanelId, + parentPanelSide, + }); + setSelectedComponent(null); + setSelectedPanelComponentInfo(null); + openPanel("v2"); + }; + + window.addEventListener("nested-tab-component-select", handleNestedTabComponentSelect as EventListener); + + return () => { + window.removeEventListener("nested-tab-component-select", handleNestedTabComponentSelect as EventListener); + }; + }, [openPanel]); // ํด๋ฆฝ๋ณด๋“œ ์ƒํƒœ const [clipboard, setClipboard] = useState([]); @@ -269,6 +385,7 @@ export default function ScreenDesigner({ const [zoomLevel, setZoomLevel] = useState(1); // 1 = 100% const MIN_ZOOM = 0.1; // 10% const MAX_ZOOM = 3; // 300% + const zoomRafRef = useRef(null); // ์คŒ RAF throttle์šฉ // ์ „์—ญ ํŒŒ์ผ ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ ๊ฐ•์ œ ๋ฆฌ๋ Œ๋”๋ง์„ ์œ„ํ•œ ์ƒํƒœ const [forceRenderTrigger, setForceRenderTrigger] = useState(0); @@ -359,6 +476,17 @@ export default function ScreenDesigner({ const [tables, setTables] = useState([]); const [searchTerm, setSearchTerm] = useState(""); + // ๐Ÿ†• ๊ฒ€์ƒ‰์–ด๋กœ ํ•„ํ„ฐ๋ง๋œ ํ…Œ์ด๋ธ” ๋ชฉ๋ก + const filteredTables = useMemo(() => { + if (!searchTerm.trim()) return tables; + const term = searchTerm.toLowerCase(); + return tables.filter( + (table) => + table.tableName.toLowerCase().includes(term) || + table.columns?.some((col) => col.columnName.toLowerCase().includes(term)), + ); + }, [tables, searchTerm]); + // ๊ทธ๋ฃน ์ƒ์„ฑ ๋‹ค์ด์–ผ๋กœ๊ทธ const [showGroupCreateDialog, setShowGroupCreateDialog] = useState(false); @@ -383,27 +511,43 @@ export default function ScreenDesigner({ return lines; }, [layout.gridSettings?.showGrid, screenResolution.width, screenResolution.height]); - // ํ•„ํ„ฐ๋œ ํ…Œ์ด๋ธ” ๋ชฉ๋ก - const filteredTables = useMemo(() => { - if (!searchTerm) return tables; - return tables.filter( - (table) => - table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) || - table.columns.some((col) => col.columnName.toLowerCase().includes(searchTerm.toLowerCase())), - ); - }, [tables, searchTerm]); + // ๐Ÿ†• ๋ ˆ์ด์–ด ํ™œ์„ฑ ์ƒํƒœ ๊ด€๋ฆฌ (LayerProvider ์™ธ๋ถ€์—์„œ ๊ด€๋ฆฌ) + const [activeLayerId, setActiveLayerIdLocal] = useState("default-layer"); + + // ์บ”๋ฒ„์Šค์— ๋ Œ๋”๋งํ•  ์ปดํฌ๋„ŒํŠธ ํ•„ํ„ฐ๋ง (๋ ˆ์ด์–ด ๊ธฐ๋ฐ˜) + // ํ™œ์„ฑ ๋ ˆ์ด์–ด๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ๋งŒ ํ‘œ์‹œ + // layerId๊ฐ€ ์—†๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” ๊ธฐ๋ณธ ๋ ˆ์ด์–ด("default-layer")์— ์†ํ•œ ๊ฒƒ์œผ๋กœ ์ฒ˜๋ฆฌ + const visibleComponents = useMemo(() => { + // ๋ ˆ์ด์–ด ์‹œ์Šคํ…œ์ด ํ™œ์„ฑํ™”๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ํ™œ์„ฑ ๋ ˆ์ด์–ด๊ฐ€ ์—†์œผ๋ฉด ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ ํ‘œ์‹œ + if (!activeLayerId) { + return layout.components; + } + + // ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ์†ํ•œ ์ปดํฌ๋„ŒํŠธ๋งŒ ํ•„ํ„ฐ๋ง + return layout.components.filter((comp) => { + // layerId๊ฐ€ ์—†๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” ๊ธฐ๋ณธ ๋ ˆ์ด์–ด("default-layer")์— ์†ํ•œ ๊ฒƒ์œผ๋กœ ์ฒ˜๋ฆฌ + const compLayerId = comp.layerId || "default-layer"; + return compLayerId === activeLayerId; + }); + }, [layout.components, activeLayerId]); // ์ด๋ฏธ ๋ฐฐ์น˜๋œ ์ปฌ๋Ÿผ ๋ชฉ๋ก ๊ณ„์‚ฐ const placedColumns = useMemo(() => { const placed = new Set(); + // ๐Ÿ”ง ํ™”๋ฉด์˜ ๋ฉ”์ธ ํ…Œ์ด๋ธ”๋ช…์„ fallback์œผ๋กœ ์‚ฌ์šฉ + const screenTableName = selectedScreen?.tableName; const collectColumns = (components: ComponentData[]) => { components.forEach((comp) => { const anyComp = comp as any; - // widget ํƒ€์ž… ๋˜๋Š” component ํƒ€์ž… (์ƒˆ๋กœ์šด ์‹œ์Šคํ…œ)์—์„œ tableName๊ณผ columnName ํ™•์ธ - if ((comp.type === "widget" || comp.type === "component") && anyComp.tableName && anyComp.columnName) { - const key = `${anyComp.tableName}.${anyComp.columnName}`; + // ๐Ÿ”ง tableName๊ณผ columnName์„ ์—ฌ๋Ÿฌ ์œ„์น˜์—์„œ ์ฐพ๊ธฐ (์ตœ์ƒ์œ„, componentConfig, ๋˜๋Š” ํ™”๋ฉด ํ…Œ์ด๋ธ”๋ช…) + const tableName = anyComp.tableName || anyComp.componentConfig?.tableName || screenTableName; + const columnName = anyComp.columnName || anyComp.componentConfig?.columnName; + + // widget ํƒ€์ž… ๋˜๋Š” component ํƒ€์ž…์—์„œ columnName ํ™•์ธ (tableName์€ ํ™”๋ฉด ํ…Œ์ด๋ธ”๋ช…์œผ๋กœ fallback) + if ((comp.type === "widget" || comp.type === "component") && tableName && columnName) { + const key = `${tableName}.${columnName}`; placed.add(key); } @@ -416,7 +560,7 @@ export default function ScreenDesigner({ collectColumns(layout.components); return placed; - }, [layout.components]); + }, [layout.components, selectedScreen?.tableName]); // ํžˆ์Šคํ† ๋ฆฌ์— ์ €์žฅ const saveToHistory = useCallback( @@ -431,18 +575,16 @@ export default function ScreenDesigner({ [historyIndex], ); - // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ค์ • ์—…๋ฐ์ดํŠธ ํ•ธ๋“ค๋Ÿฌ + // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ค์ • ์—…๋ฐ์ดํŠธ ํ•ธ๋“ค๋Ÿฌ (์ค‘์ฒฉ ๊ตฌ์กฐ ์ง€์›) const handleUpdateTabComponentConfig = useCallback( (path: string, value: any) => { if (!selectedTabComponentInfo) return; - - const { tabsComponentId, tabId, componentId } = selectedTabComponentInfo; - - setLayout((prevLayout) => { - const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); - if (!tabsComponent) return prevLayout; - const currentConfig = (tabsComponent as any).componentConfig || {}; + const { tabsComponentId, tabId, componentId, parentSplitPanelId, parentPanelSide } = selectedTabComponentInfo; + + // ํƒญ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ (์žฌ์‚ฌ์šฉ) + const updateTabsComponent = (tabsComponent: any) => { + const currentConfig = tabsComponent.componentConfig || {}; const tabs = currentConfig.tabs || []; const updatedTabs = tabs.map((tab: any) => { @@ -451,34 +593,18 @@ export default function ScreenDesigner({ ...tab, components: (tab.components || []).map((comp: any) => { if (comp.id === componentId) { - // path์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ์†์„ฑ ์—…๋ฐ์ดํŠธ if (path.startsWith("componentConfig.")) { const configPath = path.replace("componentConfig.", ""); return { ...comp, - componentConfig: { - ...comp.componentConfig, - [configPath]: value, - }, + componentConfig: { ...comp.componentConfig, [configPath]: value }, }; } else if (path.startsWith("style.")) { const stylePath = path.replace("style.", ""); - return { - ...comp, - style: { - ...comp.style, - [stylePath]: value, - }, - }; + return { ...comp, style: { ...comp.style, [stylePath]: value } }; } else if (path.startsWith("size.")) { const sizePath = path.replace("size.", ""); - return { - ...comp, - size: { - ...comp.size, - [sizePath]: value, - }, - }; + return { ...comp, size: { ...comp.size, [sizePath]: value } }; } else { return { ...comp, [path]: value }; } @@ -490,29 +616,68 @@ export default function ScreenDesigner({ return tab; }); - const updatedComponent = { - ...tabsComponent, - componentConfig: { - ...currentConfig, - tabs: updatedTabs, - }, - }; + return { ...tabsComponent, componentConfig: { ...currentConfig, tabs: updatedTabs } }; + }; - const newLayout = { - ...prevLayout, - components: prevLayout.components.map((c) => - c.id === tabsComponentId ? updatedComponent : c - ), - }; + setLayout((prevLayout) => { + let newLayout; + let updatedTabs; + + if (parentSplitPanelId && parentPanelSide) { + // ๐Ÿ†• ์ค‘์ฒฉ ๊ตฌ์กฐ: ๋ถ„ํ•  ํŒจ๋„ ์•ˆ์˜ ํƒญ ์—…๋ฐ์ดํŠธ + newLayout = { + ...prevLayout, + components: prevLayout.components.map((c) => { + if (c.id === parentSplitPanelId) { + const splitConfig = (c as any).componentConfig || {}; + const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; + const panelConfig = splitConfig[panelKey] || {}; + const panelComponents = panelConfig.components || []; + + const tabsComponent = panelComponents.find((pc: any) => pc.id === tabsComponentId); + if (!tabsComponent) return c; + + const updatedTabsComponent = updateTabsComponent(tabsComponent); + updatedTabs = updatedTabsComponent.componentConfig.tabs; + + return { + ...c, + componentConfig: { + ...splitConfig, + [panelKey]: { + ...panelConfig, + components: panelComponents.map((pc: any) => + pc.id === tabsComponentId ? updatedTabsComponent : pc, + ), + }, + }, + }; + } + return c; + }), + }; + } else { + // ์ผ๋ฐ˜ ๊ตฌ์กฐ: ์ตœ์ƒ์œ„ ํƒญ ์—…๋ฐ์ดํŠธ + const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); + if (!tabsComponent) return prevLayout; + + const updatedTabsComponent = updateTabsComponent(tabsComponent); + updatedTabs = updatedTabsComponent.componentConfig.tabs; + + newLayout = { + ...prevLayout, + components: prevLayout.components.map((c) => (c.id === tabsComponentId ? updatedTabsComponent : c)), + }; + } // ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด๋„ ์—…๋ฐ์ดํŠธ - const updatedComp = updatedTabs - .find((t: any) => t.id === tabId) - ?.components?.find((c: any) => c.id === componentId); - if (updatedComp) { - setSelectedTabComponentInfo((prev) => - prev ? { ...prev, component: updatedComp } : null - ); + if (updatedTabs) { + const updatedComp = updatedTabs + .find((t: any) => t.id === tabId) + ?.components?.find((c: any) => c.id === componentId); + if (updatedComp) { + setSelectedTabComponentInfo((prev) => (prev ? { ...prev, component: updatedComp } : null)); + } } return newLayout; @@ -669,11 +834,27 @@ export default function ScreenDesigner({ const finalKey = pathParts[pathParts.length - 1]; current[finalKey] = value; + // ๐Ÿ”ง style ๊ด€๋ จ ์—…๋ฐ์ดํŠธ ๋””๋ฒ„๊ทธ ๋กœ๊ทธ + if (path.includes("style") || path.includes("labelDisplay")) { + console.log("๐ŸŽจ style ์—…๋ฐ์ดํŠธ ์ œ๋Œ€๋กœ ๋ Œ๋”๋ง๋œ๊ฑฐ๋‹ค ๋‚ด๊ฐ€๋ฐ”๊ฟˆ:", { + componentId: comp.id, + path, + value, + updatedStyle: newComp.style, + pathIncludesLabelDisplay: path.includes("labelDisplay"), + }); + } + + // ๐Ÿ†• labelDisplay ๋ณ€๊ฒฝ ์‹œ ๊ฐ•์ œ ๋ฆฌ๋ Œ๋”๋ง ํŠธ๋ฆฌ๊ฑฐ (์กฐ๊ฑด๋ฌธ ๋ฐ–์œผ๋กœ ์ด๋™) + if (path === "style.labelDisplay") { + console.log("โฐโฐโฐ labelDisplay ๋ณ€๊ฒฝ ๊ฐ์ง€! forceRenderTrigger ์‹คํ–‰ ์˜ˆ์ •"); + } + // ๐Ÿ†• size ๋ณ€๊ฒฝ ์‹œ style๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ (ํŒŒ๋ž€ ํ…Œ๋‘๋ฆฌ์™€ ์‹ค์ œ ํฌ๊ธฐ ๋™๊ธฐํ™”) if (path === "size.width" || path === "size.height" || path === "size") { // ๐Ÿ”ง style ๊ฐ์ฒด๋ฅผ ์ƒˆ๋กœ ๋ณต์‚ฌํ•˜์—ฌ ๋ถˆ๋ณ€์„ฑ ์œ ์ง€ newComp.style = { ...(newComp.style || {}) }; - + if (path === "size.width") { newComp.style.width = `${value}px`; } else if (path === "size.height") { @@ -687,7 +868,7 @@ export default function ScreenDesigner({ newComp.style.height = `${value.height}px`; } } - + console.log("๐Ÿ”„ size ๋ณ€๊ฒฝ โ†’ style ๋™๊ธฐํ™”:", { componentId: newComp.id, path, @@ -992,18 +1173,19 @@ export default function ScreenDesigner({ // ์ „์ฒด selectedScreen ๊ฐ์ฒด๋„ ์ถœ๋ ฅ fullScreen: selectedScreen, }); - + // REST API ๋ฐ์ดํ„ฐ ์†Œ์Šค์ธ ๊ฒฝ์šฐ // 1. dataSourceType์ด "restapi"์ธ ๊ฒฝ์šฐ // 2. tableName์ด restapi_ ๋˜๋Š” _restapi_๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒฝ์šฐ // 3. restApiConnectionId๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ - const isRestApi = selectedScreen?.dataSourceType === "restapi" || - selectedScreen?.tableName?.startsWith("restapi_") || - selectedScreen?.tableName?.startsWith("_restapi_") || - !!selectedScreen?.restApiConnectionId; - + const isRestApi = + selectedScreen?.dataSourceType === "restapi" || + selectedScreen?.tableName?.startsWith("restapi_") || + selectedScreen?.tableName?.startsWith("_restapi_") || + !!selectedScreen?.restApiConnectionId; + console.log("๐Ÿ” [ScreenDesigner] REST API ์—ฌ๋ถ€:", { isRestApi }); - + if (isRestApi && (selectedScreen?.restApiConnectionId || selectedScreen?.tableName)) { try { // ์—ฐ๊ฒฐ ID ์ถ”์ถœ (restApiConnectionId๊ฐ€ ์—†์œผ๋ฉด tableName์—์„œ ์ถ”์ถœ) @@ -1012,13 +1194,13 @@ export default function ScreenDesigner({ const match = selectedScreen.tableName.match(/restapi_(\d+)/); connectionId = match ? parseInt(match[1]) : undefined; } - + if (!connectionId) { throw new Error("REST API ์—ฐ๊ฒฐ ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } - + console.log("๐ŸŒ [ScreenDesigner] REST API ๋ฐ์ดํ„ฐ ๋กœ๋“œ:", { connectionId }); - + const restApiData = await ExternalRestApiConnectionAPI.fetchData( connectionId, selectedScreen?.restApiEndpoint, @@ -1043,12 +1225,12 @@ export default function ScreenDesigner({ tableLabel: restApiData.connectionInfo.connectionName || "REST API ๋ฐ์ดํ„ฐ", columns, }; - + console.log("โœ… [ScreenDesigner] REST API ์ปฌ๋Ÿผ ๋กœ๋“œ ์™„๋ฃŒ:", { tableName: tableInfo.tableName, tableLabel: tableInfo.tableLabel, columnsCount: columns.length, - columns: columns.map(c => c.columnName), + columns: columns.map((c) => c.columnName), }); setTables([tableInfo]); @@ -1155,95 +1337,107 @@ export default function ScreenDesigner({ }; loadScreenDataSource(); - }, [selectedScreen?.tableName, selectedScreen?.screenName, selectedScreen?.dataSourceType, selectedScreen?.restApiConnectionId, selectedScreen?.restApiEndpoint, selectedScreen?.restApiJsonPath]); + }, [ + selectedScreen?.tableName, + selectedScreen?.screenName, + selectedScreen?.dataSourceType, + selectedScreen?.restApiConnectionId, + selectedScreen?.restApiEndpoint, + selectedScreen?.restApiJsonPath, + ]); // ํ…Œ์ด๋ธ” ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ - ์‚ฌ์ด๋“œ๋ฐ”์—์„œ ํ…Œ์ด๋ธ” ์„ ํƒ ์‹œ ํ˜ธ์ถœ - const handleTableSelect = useCallback(async (tableName: string) => { - console.log("๐Ÿ“Š ํ…Œ์ด๋ธ” ์„ ํƒ:", tableName); - - try { - // ํ…Œ์ด๋ธ” ๋ผ๋ฒจ ์กฐํšŒ - const tableListResponse = await tableManagementApi.getTableList(); - const currentTable = - tableListResponse.success && tableListResponse.data - ? tableListResponse.data.find((t: any) => (t.tableName || t.table_name) === tableName) - : null; - const tableLabel = currentTable?.displayName || currentTable?.table_label || tableName; + const handleTableSelect = useCallback( + async (tableName: string) => { + console.log("๐Ÿ“Š ํ…Œ์ด๋ธ” ์„ ํƒ:", tableName); - // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด ์กฐํšŒ - const columnsResponse = await tableTypeApi.getColumns(tableName, true); + try { + // ํ…Œ์ด๋ธ” ๋ผ๋ฒจ ์กฐํšŒ + const tableListResponse = await tableManagementApi.getTableList(); + const currentTable = + tableListResponse.success && tableListResponse.data + ? tableListResponse.data.find((t: any) => (t.tableName || t.table_name) === tableName) + : null; + const tableLabel = currentTable?.displayName || currentTable?.table_label || tableName; - const columns: ColumnInfo[] = (columnsResponse || []).map((col: any) => { - const inputType = col.inputType || col.input_type; - const widgetType = inputType || col.widgetType || col.widget_type || col.webType || col.web_type; + // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด ์กฐํšŒ + const columnsResponse = await tableTypeApi.getColumns(tableName, true); - let detailSettings = col.detailSettings || col.detail_settings; - if (typeof detailSettings === "string") { - try { - detailSettings = JSON.parse(detailSettings); - } catch (e) { - detailSettings = {}; - } - } + const columns: ColumnInfo[] = (columnsResponse || []).map((col: any) => { + const inputType = col.inputType || col.input_type; + const widgetType = inputType || col.widgetType || col.widget_type || col.webType || col.web_type; - return { - tableName: col.tableName || tableName, - columnName: col.columnName || col.column_name, - columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, - dataType: col.dataType || col.data_type || col.dbType, - webType: col.webType || col.web_type, - input_type: inputType, - inputType: inputType, - widgetType, - isNullable: col.isNullable || col.is_nullable, - required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO", - columnDefault: col.columnDefault || col.column_default, - characterMaximumLength: col.characterMaximumLength || col.character_maximum_length, - codeCategory: col.codeCategory || col.code_category, - codeValue: col.codeValue || col.code_value, - referenceTable: detailSettings?.referenceTable || col.referenceTable || col.reference_table, - referenceColumn: detailSettings?.referenceColumn || col.referenceColumn || col.reference_column, - displayColumn: detailSettings?.displayColumn || col.displayColumn || col.display_column, - detailSettings, - }; - }); - - const tableInfo: TableInfo = { - tableName, - tableLabel, - columns, - }; - - setTables([tableInfo]); - toast.success(`ํ…Œ์ด๋ธ” "${tableLabel}" ์„ ํƒ๋จ`); - - // ๊ธฐ์กด ํ…Œ์ด๋ธ”๊ณผ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ์„ ํƒ ์‹œ, ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ ์ค‘ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์€ ์ œ๊ฑฐ - if (tables.length > 0 && tables[0].tableName !== tableName) { - setLayout((prev) => { - const newComponents = prev.components.filter((comp) => { - // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ์ธ์ง€ ํ™•์ธ - if (comp.tableName && comp.tableName !== tableName) { - console.log("๐Ÿ—‘๏ธ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ ์ œ๊ฑฐ:", comp.tableName, comp.columnName); - return false; + let detailSettings = col.detailSettings || col.detail_settings; + if (typeof detailSettings === "string") { + try { + detailSettings = JSON.parse(detailSettings); + } catch (e) { + detailSettings = {}; } - return true; - }); - - if (newComponents.length < prev.components.length) { - toast.info(`์ด์ „ ํ…Œ์ด๋ธ”(${tables[0].tableName})์˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ${prev.components.length - newComponents.length}๊ฐœ ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); } return { - ...prev, - components: newComponents, + tableName: col.tableName || tableName, + columnName: col.columnName || col.column_name, + columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, + dataType: col.dataType || col.data_type || col.dbType, + webType: col.webType || col.web_type, + input_type: inputType, + inputType: inputType, + widgetType, + isNullable: col.isNullable || col.is_nullable, + required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO", + columnDefault: col.columnDefault || col.column_default, + characterMaximumLength: col.characterMaximumLength || col.character_maximum_length, + codeCategory: col.codeCategory || col.code_category, + codeValue: col.codeValue || col.code_value, + referenceTable: detailSettings?.referenceTable || col.referenceTable || col.reference_table, + referenceColumn: detailSettings?.referenceColumn || col.referenceColumn || col.reference_column, + displayColumn: detailSettings?.displayColumn || col.displayColumn || col.display_column, + detailSettings, }; }); + + const tableInfo: TableInfo = { + tableName, + tableLabel, + columns, + }; + + setTables([tableInfo]); + toast.success(`ํ…Œ์ด๋ธ” "${tableLabel}" ์„ ํƒ๋จ`); + + // ๊ธฐ์กด ํ…Œ์ด๋ธ”๊ณผ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ์„ ํƒ ์‹œ, ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ ์ค‘ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์€ ์ œ๊ฑฐ + if (tables.length > 0 && tables[0].tableName !== tableName) { + setLayout((prev) => { + const newComponents = prev.components.filter((comp) => { + // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ์ธ์ง€ ํ™•์ธ + if (comp.tableName && comp.tableName !== tableName) { + console.log("๐Ÿ—‘๏ธ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ ์ œ๊ฑฐ:", comp.tableName, comp.columnName); + return false; + } + return true; + }); + + if (newComponents.length < prev.components.length) { + toast.info( + `์ด์ „ ํ…Œ์ด๋ธ”(${tables[0].tableName})์˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ${prev.components.length - newComponents.length}๊ฐœ ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, + ); + } + + return { + ...prev, + components: newComponents, + }; + }); + } + } catch (error) { + console.error("ํ…Œ์ด๋ธ” ์ •๋ณด ๋กœ๋“œ ์‹คํŒจ:", error); + toast.error("ํ…Œ์ด๋ธ” ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } - } catch (error) { - console.error("ํ…Œ์ด๋ธ” ์ •๋ณด ๋กœ๋“œ ์‹คํŒจ:", error); - toast.error("ํ…Œ์ด๋ธ” ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); - } - }, [tables]); + }, + [tables], + ); // ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ๋กœ๋“œ useEffect(() => { @@ -1274,12 +1468,27 @@ export default function ScreenDesigner({ } else if (USE_V2_API) { // ๋ฐ์Šคํฌํ†ฑ V2 ๋ชจ๋“œ: screen_layouts_v2 ํ…Œ์ด๋ธ” ์‚ฌ์šฉ const v2Response = await screenApi.getLayoutV2(selectedScreen.screenId); + + // ๐Ÿ› ๋””๋ฒ„๊น…: API ์‘๋‹ต์—์„œ fieldMapping.id ํ™•์ธ + const splitPanelInV2 = v2Response?.components?.find((c: any) => c.url?.includes("v2-split-panel-layout")); + const finishedTimelineInV2 = splitPanelInV2?.overrides?.rightPanel?.components?.find( + (c: any) => c.id === "finished_timeline", + ); + console.log("๐Ÿ› [API ์‘๋‹ต RAW] finished_timeline:", JSON.stringify(finishedTimelineInV2, null, 2)); + console.log("๐Ÿ› [API ์‘๋‹ต] finished_timeline fieldMapping:", { + fieldMapping: JSON.stringify(finishedTimelineInV2?.componentConfig?.fieldMapping), + fieldMappingKeys: finishedTimelineInV2?.componentConfig?.fieldMapping + ? Object.keys(finishedTimelineInV2?.componentConfig?.fieldMapping) + : [], + hasId: !!finishedTimelineInV2?.componentConfig?.fieldMapping?.id, + idValue: finishedTimelineInV2?.componentConfig?.fieldMapping?.id, + }); + response = v2Response ? convertV2ToLegacy(v2Response) : null; - console.log("๐Ÿ“ฆ V2 ๋ ˆ์ด์•„์›ƒ ๋กœ๋“œ:", v2Response?.components?.length || 0, "๊ฐœ ์ปดํฌ๋„ŒํŠธ"); } else { response = await screenApi.getLayout(selectedScreen.screenId); } - + if (response) { // ๐Ÿ”„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•„์š” ์—ฌ๋ถ€ ํ™•์ธ (V2๋Š” ์Šคํ‚ต) let layoutToUse = response; @@ -1321,15 +1530,18 @@ export default function ScreenDesigner({ } // ๐Ÿ” ๋””๋ฒ„๊น…: ๋กœ๋“œ๋œ ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ์˜ action ํ™•์ธ - const buttonComponents = layoutWithDefaultGrid.components.filter( - (c: any) => c.componentType?.startsWith("button") + const buttonComponents = layoutWithDefaultGrid.components.filter((c: any) => + c.componentType?.startsWith("button"), + ); + console.log( + "๐Ÿ” [๋กœ๋“œ] ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ action ํ™•์ธ:", + buttonComponents.map((c: any) => ({ + id: c.id, + type: c.componentType, + actionType: c.componentConfig?.action?.type, + fullAction: c.componentConfig?.action, + })), ); - console.log("๐Ÿ” [๋กœ๋“œ] ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ action ํ™•์ธ:", buttonComponents.map((c: any) => ({ - id: c.id, - type: c.componentType, - actionType: c.componentConfig?.action?.type, - fullAction: c.componentConfig?.action, - }))); setLayout(layoutWithDefaultGrid); setHistory([layoutWithDefaultGrid]); @@ -1355,8 +1567,8 @@ export default function ScreenDesigner({ if ( activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement || - activeElement?.getAttribute('contenteditable') === 'true' || - activeElement?.getAttribute('role') === 'textbox' + activeElement?.getAttribute("contenteditable") === "true" || + activeElement?.getAttribute("role") === "textbox" ) { return; } @@ -1382,8 +1594,8 @@ export default function ScreenDesigner({ if ( activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement || - activeElement?.getAttribute('contenteditable') === 'true' || - activeElement?.getAttribute('role') === 'textbox' + activeElement?.getAttribute("contenteditable") === "true" || + activeElement?.getAttribute("role") === "textbox" ) { return; } @@ -1467,7 +1679,7 @@ export default function ScreenDesigner({ panState.innerScrollTop, ]); - // ๋งˆ์šฐ์Šค ํœ ๋กœ ์คŒ ์ œ์–ด + // ๋งˆ์šฐ์Šค ํœ ๋กœ ์คŒ ์ œ์–ด (RAF throttle ์ ์šฉ์œผ๋กœ ๊นœ๋นก์ž„ ๋ฐฉ์ง€) useEffect(() => { const handleWheel = (e: WheelEvent) => { // ์บ”๋ฒ„์Šค ์ปจํ…Œ์ด๋„ˆ ๋‚ด์—์„œ๋งŒ ๋™์ž‘ @@ -1480,9 +1692,16 @@ export default function ScreenDesigner({ const delta = e.deltaY; const zoomFactor = 0.001; // ์คŒ ์†๋„ ์กฐ์ ˆ - setZoomLevel((prevZoom) => { - const newZoom = prevZoom - delta * zoomFactor; - return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoom)); + // RAF throttle: ํ”„๋ ˆ์ž„๋‹น ํ•œ ๋ฒˆ๋งŒ ์ƒํƒœ ์—…๋ฐ์ดํŠธ + if (zoomRafRef.current !== null) { + cancelAnimationFrame(zoomRafRef.current); + } + zoomRafRef.current = requestAnimationFrame(() => { + setZoomLevel((prevZoom) => { + const newZoom = prevZoom - delta * zoomFactor; + return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoom)); + }); + zoomRafRef.current = null; }); } } @@ -1494,58 +1713,22 @@ export default function ScreenDesigner({ const containerRef = canvasContainerRef.current; return () => { containerRef?.removeEventListener("wheel", handleWheel); + if (zoomRafRef.current !== null) { + cancelAnimationFrame(zoomRafRef.current); + } }; }, [MIN_ZOOM, MAX_ZOOM]); - // ๊ฒฉ์ž ์„ค์ • ์—…๋ฐ์ดํŠธ ๋ฐ ์ปดํฌ๋„ŒํŠธ ์ž๋™ ์Šค๋ƒ… + // ๊ฒฉ์ž ์„ค์ • ์—…๋ฐ์ดํŠธ (์ปดํฌ๋„ŒํŠธ ์ž๋™ ์กฐ์ • ์ œ๊ฑฐ๋จ) const updateGridSettings = useCallback( (newGridSettings: GridSettings) => { const newLayout = { ...layout, gridSettings: newGridSettings }; - - // ๊ฒฉ์ž ์Šค๋ƒ…์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ, ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒˆ๋กœ์šด ๊ฒฉ์ž์— ๋งž๊ฒŒ ์กฐ์ • - if (newGridSettings.snapToGrid && screenResolution.width > 0) { - // ์ƒˆ๋กœ์šด ๊ฒฉ์ž ์„ค์ •์œผ๋กœ ๊ฒฉ์ž ์ •๋ณด ์žฌ๊ณ„์‚ฐ (ํ•ด์ƒ๋„ ๊ธฐ์ค€) - const newGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, { - columns: newGridSettings.columns, - gap: newGridSettings.gap, - padding: newGridSettings.padding, - snapToGrid: newGridSettings.snapToGrid || false, - }); - - const gridUtilSettings = { - columns: newGridSettings.columns, - gap: newGridSettings.gap, - padding: newGridSettings.padding, - snapToGrid: true, // ํ•ญ์ƒ 10px ์Šค๋ƒ… ํ™œ์„ฑํ™” - }; - - const adjustedComponents = layout.components.map((comp) => { - const snappedPosition = snapToGrid(comp.position, newGridInfo, gridUtilSettings); - const snappedSize = snapSizeToGrid(comp.size, newGridInfo, gridUtilSettings); - - // gridColumns๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด ์ž๋™ ์กฐ์ • - let adjustedGridColumns = comp.gridColumns; - if (!adjustedGridColumns || adjustedGridColumns < 1 || adjustedGridColumns > newGridSettings.columns) { - adjustedGridColumns = adjustGridColumnsFromSize({ size: snappedSize }, newGridInfo, gridUtilSettings); - } - - return { - ...comp, - position: snappedPosition, - size: snappedSize, - gridColumns: adjustedGridColumns, // gridColumns ์†์„ฑ ์ถ”๊ฐ€/์กฐ์ • - }; - }); - - newLayout.components = adjustedComponents; - // console.log("๊ฒฉ์ž ์„ค์ • ๋ณ€๊ฒฝ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ ์œ„์น˜ ๋ฐ ํฌ๊ธฐ ์ž๋™ ์กฐ์ •:", adjustedComponents.length, "๊ฐœ"); - // console.log("์ƒˆ๋กœ์šด ๊ฒฉ์ž ์ •๋ณด:", newGridInfo); - } - + // ๐Ÿ†• ๊ฒฉ์ž ์„ค์ • ๋ณ€๊ฒฝ ์‹œ ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ/์œ„์น˜ ์ž๋™ ์กฐ์ • ๋กœ์ง ์ œ๊ฑฐ๋จ + // ์‚ฌ์šฉ์ž๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ "๊ฒฉ์ž ์žฌ์กฐ์ •" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ด์•ผ๋งŒ ์กฐ์ •๋จ setLayout(newLayout); saveToHistory(newLayout); }, - [layout, screenResolution, saveToHistory], + [layout, saveToHistory], ); // ํ•ด์ƒ๋„ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ (์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ/์œ„์น˜ ์œ ์ง€) @@ -1573,7 +1756,7 @@ export default function ScreenDesigner({ setLayout(updatedLayout); saveToHistory(updatedLayout); - toast.success(`ํ•ด์ƒ๋„๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, { + toast.success("ํ•ด์ƒ๋„๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", { description: `${oldWidth}ร—${oldHeight} โ†’ ${newWidth}ร—${newHeight}`, }); @@ -1644,6 +1827,103 @@ export default function ScreenDesigner({ toast.success(`${adjustedComponents.length}๊ฐœ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ฒฉ์ž์— ๋งž๊ฒŒ ์žฌ์ •๋ ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); }, [layout, screenResolution, saveToHistory]); + // === ์ •๋ ฌ/๋ฐฐ๋ถ„/๋™์ผํฌ๊ธฐ/๋ผ๋ฒจํ† ๊ธ€/Nudge ํ•ธ๋“ค๋Ÿฌ === + + // ์ปดํฌ๋„ŒํŠธ ์ •๋ ฌ + const handleGroupAlign = useCallback( + (mode: AlignMode) => { + if (groupState.selectedComponents.length < 2) { + toast.warning("2๊ฐœ ์ด์ƒ์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); + return; + } + saveToHistory(layout); + const newComponents = alignComponents(layout.components, groupState.selectedComponents, mode); + setLayout((prev) => ({ ...prev, components: newComponents })); + + const modeNames: Record = { + left: "์ขŒ์ธก", right: "์šฐ์ธก", centerX: "๊ฐ€๋กœ ์ค‘์•™", + top: "์ƒ๋‹จ", bottom: "ํ•˜๋‹จ", centerY: "์„ธ๋กœ ์ค‘์•™", + }; + toast.success(`${modeNames[mode]} ์ •๋ ฌ ์™„๋ฃŒ`); + }, + [groupState.selectedComponents, layout, saveToHistory] + ); + + // ์ปดํฌ๋„ŒํŠธ ๊ท ๋“ฑ ๋ฐฐ๋ถ„ + const handleGroupDistribute = useCallback( + (direction: DistributeDirection) => { + if (groupState.selectedComponents.length < 3) { + toast.warning("3๊ฐœ ์ด์ƒ์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); + return; + } + saveToHistory(layout); + const newComponents = distributeComponents(layout.components, groupState.selectedComponents, direction); + setLayout((prev) => ({ ...prev, components: newComponents })); + toast.success(`${direction === "horizontal" ? "๊ฐ€๋กœ" : "์„ธ๋กœ"} ๊ท ๋“ฑ ๋ฐฐ๋ถ„ ์™„๋ฃŒ`); + }, + [groupState.selectedComponents, layout, saveToHistory] + ); + + // ๋™์ผ ํฌ๊ธฐ ๋งž์ถ”๊ธฐ + const handleMatchSize = useCallback( + (mode: MatchSizeMode) => { + if (groupState.selectedComponents.length < 2) { + toast.warning("2๊ฐœ ์ด์ƒ์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); + return; + } + saveToHistory(layout); + const newComponents = matchComponentSize( + layout.components, + groupState.selectedComponents, + mode, + selectedComponent?.id + ); + setLayout((prev) => ({ ...prev, components: newComponents })); + + const modeNames: Record = { + width: "๋„ˆ๋น„", height: "๋†’์ด", both: "ํฌ๊ธฐ", + }; + toast.success(`${modeNames[mode]} ๋งž์ถ”๊ธฐ ์™„๋ฃŒ`); + }, + [groupState.selectedComponents, layout, selectedComponent?.id, saveToHistory] + ); + + // ๋ผ๋ฒจ ์ผ๊ด„ ํ† ๊ธ€ + const handleToggleAllLabels = useCallback(() => { + saveToHistory(layout); + const newComponents = toggleAllLabels(layout.components); + setLayout((prev) => ({ ...prev, components: newComponents })); + + const hasHidden = layout.components.some( + (c) => c.type === "widget" && (c.style as any)?.labelDisplay === false + ); + toast.success(hasHidden ? "๋ชจ๋“  ๋ผ๋ฒจ ํ‘œ์‹œ" : "๋ชจ๋“  ๋ผ๋ฒจ ์ˆจ๊ธฐ๊ธฐ"); + }, [layout, saveToHistory]); + + // Nudge (ํ™”์‚ดํ‘œ ํ‚ค ์ด๋™) + const handleNudge = useCallback( + (direction: "up" | "down" | "left" | "right", distance: number) => { + const targetIds = + groupState.selectedComponents.length > 0 + ? groupState.selectedComponents + : selectedComponent + ? [selectedComponent.id] + : []; + + if (targetIds.length === 0) return; + + const newComponents = nudgeComponents(layout.components, targetIds, direction, distance); + setLayout((prev) => ({ ...prev, components: newComponents })); + + // ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ + if (selectedComponent && targetIds.includes(selectedComponent.id)) { + const updated = newComponents.find((c) => c.id === selectedComponent.id); + if (updated) setSelectedComponent(updated); + } + }, + [groupState.selectedComponents, selectedComponent, layout.components] + ); + // ์ €์žฅ const handleSave = useCallback(async () => { if (!selectedScreen?.screenId) { @@ -1689,10 +1969,21 @@ export default function ScreenDesigner({ // ํ•ด์ƒ๋„ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ // ํ˜„์žฌ ์„ ํƒ๋œ ํ…Œ์ด๋ธ”์„ ํ™”๋ฉด์˜ ๊ธฐ๋ณธ ํ…Œ์ด๋ธ”๋กœ ์ €์žฅ const currentMainTableName = tables.length > 0 ? tables[0].tableName : null; - + + // ๐Ÿ†• ๋ ˆ์ด์–ด ์ •๋ณด๋„ ํ•จ๊ป˜ ์ €์žฅ (๋ ˆ์ด์–ด๊ฐ€ ์žˆ์œผ๋ฉด ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ๋กœ ์—…๋ฐ์ดํŠธ) + const updatedLayers = layout.layers?.map((layer) => ({ + ...layer, + components: layer.components.map((comp) => { + // ๋ถ„ํ•  ํŒจ๋„ ์—…๋ฐ์ดํŠธ ๋กœ์ง ์ ์šฉ + const updatedComp = updatedComponents.find((uc) => uc.id === comp.id); + return updatedComp || comp; + }), + })); + const layoutWithResolution = { ...layout, components: updatedComponents, + layers: updatedLayers, // ๐Ÿ†• ๋ ˆ์ด์–ด ์ •๋ณด ํฌํ•จ screenResolution: screenResolution, mainTableName: currentMainTableName, // ํ™”๋ฉด์˜ ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” }; @@ -1700,36 +1991,24 @@ export default function ScreenDesigner({ const buttonComponents = layoutWithResolution.components.filter( (c: any) => c.componentType?.startsWith("button") || c.type === "button" || c.type === "button-primary", ); - console.log("๐Ÿ’พ ์ €์žฅ ์‹œ์ž‘:", { - screenId: selectedScreen.screenId, - componentsCount: layoutWithResolution.components.length, - gridSettings: layoutWithResolution.gridSettings, - screenResolution: layoutWithResolution.screenResolution, - buttonComponents: buttonComponents.map((c: any) => ({ - id: c.id, - type: c.type, - componentType: c.componentType, - text: c.componentConfig?.text, - actionType: c.componentConfig?.action?.type, - fullAction: c.componentConfig?.action, - })), - }); + // ๐Ÿ’พ ์ €์žฅ ๋กœ๊ทธ (๋””๋ฒ„๊ทธ ์™„๋ฃŒ - ๊ฐ„์†Œํ™”) + // console.log("๐Ÿ’พ ์ €์žฅ ์‹œ์ž‘:", { screenId: selectedScreen.screenId, componentsCount: layoutWithResolution.components.length }); + // ๋ถ„ํ•  ํŒจ๋„ ๋””๋ฒ„๊ทธ ๋กœ๊ทธ (์ฃผ์„ ์ฒ˜๋ฆฌ) // V2/POP API ์‚ฌ์šฉ ์—ฌ๋ถ€์— ๋”ฐ๋ผ ๋ถ„๊ธฐ const v2Layout = convertLegacyToV2(layoutWithResolution); if (USE_POP_API) { // POP ๋ชจ๋“œ: screen_layouts_pop ํ…Œ์ด๋ธ”์— ์ €์žฅ await screenApi.saveLayoutPop(selectedScreen.screenId, v2Layout); - console.log("๐Ÿ“ฑ POP ๋ ˆ์ด์•„์›ƒ ์ €์žฅ:", v2Layout.components.length, "๊ฐœ ์ปดํฌ๋„ŒํŠธ"); } else if (USE_V2_API) { // ๋ฐ์Šคํฌํ†ฑ V2 ๋ชจ๋“œ: screen_layouts_v2 ํ…Œ์ด๋ธ”์— ์ €์žฅ await screenApi.saveLayoutV2(selectedScreen.screenId, v2Layout); - console.log("๐Ÿ“ฆ V2 ๋ ˆ์ด์•„์›ƒ ์ €์žฅ:", v2Layout.components.length, "๊ฐœ ์ปดํฌ๋„ŒํŠธ"); + // console.log("๐Ÿ“ฆ V2 ๋ ˆ์ด์•„์›ƒ ์ €์žฅ:", v2Layout.components.length, "๊ฐœ ์ปดํฌ๋„ŒํŠธ"); } else { await screenApi.saveLayout(selectedScreen.screenId, layoutWithResolution); } - console.log("โœ… ์ €์žฅ ์„ฑ๊ณต! ๋ฉ”๋‰ด ํ• ๋‹น ๋ชจ๋‹ฌ ์—ด๊ธฐ"); + // console.log("โœ… ์ €์žฅ ์„ฑ๊ณต!"); toast.success("ํ™”๋ฉด์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); // ์ €์žฅ ์„ฑ๊ณต ํ›„ ๋ถ€๋ชจ์—๊ฒŒ ํ™”๋ฉด ์ •๋ณด ์—…๋ฐ์ดํŠธ ์•Œ๋ฆผ (ํ…Œ์ด๋ธ”๋ช… ์ฆ‰์‹œ ๋ฐ˜์˜) @@ -1825,14 +2104,14 @@ export default function ScreenDesigner({ if (response.success && response.data) { // ์ž๋™ ๋งคํ•‘ ์ ์šฉ const updatedComponents = applyMultilangMappings(layout.components, response.data); - + // ๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ const updatedLayout = { ...layout, components: updatedComponents, screenResolution: screenResolution, }; - + setLayout(updatedLayout); // ์ž๋™ ์ €์žฅ (๋งคํ•‘ ์ •๋ณด๊ฐ€ ์†์‹ค๋˜์ง€ ์•Š๋„๋ก) @@ -2260,23 +2539,29 @@ export default function ScreenDesigner({ } }); + // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ + const componentsWithLayerId = newComponents.map((comp) => ({ + ...comp, + layerId: activeLayerId || "default-layer", + })); + // ๋ ˆ์ด์•„์›ƒ์— ์ƒˆ ์ปดํฌ๋„ŒํŠธ๋“ค ์ถ”๊ฐ€ const newLayout = { ...layout, - components: [...layout.components, ...newComponents], + components: [...layout.components, ...componentsWithLayerId], }; setLayout(newLayout); saveToHistory(newLayout); // ์ฒซ ๋ฒˆ์งธ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ - if (newComponents.length > 0) { - setSelectedComponent(newComponents[0]); + if (componentsWithLayerId.length > 0) { + setSelectedComponent(componentsWithLayerId[0]); } toast.success(`${template.name} ํ…œํ”Œ๋ฆฟ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); }, - [layout, selectedScreen, saveToHistory], + [layout, selectedScreen, saveToHistory, activeLayerId], ); // ๋ ˆ์ด์•„์›ƒ ๋“œ๋ž˜๊ทธ ์ฒ˜๋ฆฌ @@ -2330,6 +2615,7 @@ export default function ScreenDesigner({ label: layoutData.label, allowedComponentTypes: layoutData.allowedComponentTypes, dropZoneConfig: layoutData.dropZoneConfig, + layerId: activeLayerId || "default-layer", // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ์ถ”๊ฐ€ } as ComponentData; // ๋ ˆ์ด์•„์›ƒ์— ์ƒˆ ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ @@ -2346,7 +2632,7 @@ export default function ScreenDesigner({ toast.success(`${layoutData.label} ๋ ˆ์ด์•„์›ƒ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); }, - [layout, screenResolution, saveToHistory, zoomLevel], + [layout, screenResolution, saveToHistory, zoomLevel, activeLayerId], ); // handleZoneComponentDrop์€ handleComponentDrop์œผ๋กœ ๋Œ€์ฒด๋จ @@ -2438,7 +2724,7 @@ export default function ScreenDesigner({ if (targetComponent && (compType === "repeat-container" || compType === "v2-repeat-container")) { const currentConfig = (targetComponent as any).componentConfig || {}; const currentChildren = currentConfig.children || []; - + // ์ƒˆ ์ž์‹ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ const newChild = { id: `slot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, @@ -2449,7 +2735,7 @@ export default function ScreenDesigner({ size: component.defaultSize || { width: 200, height: 32 }, componentConfig: component.defaultConfig || {}, }; - + // ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ const updatedComponent = { ...targetComponent, @@ -2458,14 +2744,12 @@ export default function ScreenDesigner({ children: [...currentChildren, newChild], }, }; - + const newLayout = { ...layout, - components: layout.components.map((c) => - c.id === containerId ? updatedComponent : c - ), + components: layout.components.map((c) => (c.id === containerId ? updatedComponent : c)), }; - + setLayout(newLayout); saveToHistory(newLayout); return; // ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ์ฒ˜๋ฆฌ ์™„๋ฃŒ @@ -2473,35 +2757,75 @@ export default function ScreenDesigner({ } } - // ๐ŸŽฏ ํƒญ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€ ๋“œ๋กญ ์ฒ˜๋ฆฌ + // ๐ŸŽฏ ํƒญ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€ ๋“œ๋กญ ์ฒ˜๋ฆฌ (์ค‘์ฒฉ ๊ตฌ์กฐ ์ง€์›) const tabsContainer = dropTarget.closest('[data-tabs-container="true"]'); if (tabsContainer) { const containerId = tabsContainer.getAttribute("data-component-id"); const activeTabId = tabsContainer.getAttribute("data-active-tab-id"); if (containerId && activeTabId) { - const targetComponent = layout.components.find((c) => c.id === containerId); + // 1. ๋จผ์ € ์ตœ์ƒ์œ„ ๋ ˆ์ด์•„์›ƒ์—์„œ ํƒญ ์ปดํฌ๋„ŒํŠธ ์ฐพ๊ธฐ + let targetComponent = layout.components.find((c) => c.id === containerId); + let parentSplitPanelId: string | null = null; + let parentPanelSide: "left" | "right" | null = null; + + // 2. ์ตœ์ƒ์œ„์— ์—†์œผ๋ฉด ๋ถ„ํ•  ํŒจ๋„ ์•ˆ์—์„œ ์ค‘์ฒฉ๋œ ํƒญ ์ฐพ๊ธฐ + if (!targetComponent) { + for (const comp of layout.components) { + const compType = (comp as any)?.componentType; + if (compType === "split-panel-layout" || compType === "v2-split-panel-layout") { + const config = (comp as any).componentConfig || {}; + + // ์ขŒ์ธก ํŒจ๋„์—์„œ ์ฐพ๊ธฐ + const leftComponents = config.leftPanel?.components || []; + const foundInLeft = leftComponents.find((c: any) => c.id === containerId); + if (foundInLeft) { + targetComponent = foundInLeft; + parentSplitPanelId = comp.id; + parentPanelSide = "left"; + break; + } + + // ์šฐ์ธก ํŒจ๋„์—์„œ ์ฐพ๊ธฐ + const rightComponents = config.rightPanel?.components || []; + const foundInRight = rightComponents.find((c: any) => c.id === containerId); + if (foundInRight) { + targetComponent = foundInRight; + parentSplitPanelId = comp.id; + parentPanelSide = "right"; + break; + } + } + } + } + const compType = (targetComponent as any)?.componentType; if (targetComponent && (compType === "tabs-widget" || compType === "v2-tabs-widget")) { const currentConfig = (targetComponent as any).componentConfig || {}; const tabs = currentConfig.tabs || []; - + // ํ™œ์„ฑ ํƒญ์˜ ๋“œ๋กญ ์œ„์น˜ ๊ณ„์‚ฐ const tabContentRect = tabsContainer.getBoundingClientRect(); const dropX = (e.clientX - tabContentRect.left) / zoomLevel; const dropY = (e.clientY - tabContentRect.top) / zoomLevel; - - // ์ƒˆ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ - ๋“œ๋กญ๋œ ์ปดํฌ๋„ŒํŠธ์˜ id๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ - // component.id๋Š” ComponentDefinition์˜ id (์˜ˆ: "v2-table-list", "v2-button-primary") + + // ์ƒˆ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ const componentType = component.id || component.componentType || "v2-text-display"; - + console.log("๐ŸŽฏ ํƒญ์— ์ปดํฌ๋„ŒํŠธ ๋“œ๋กญ:", { componentId: component.id, componentType: componentType, componentName: component.name, - defaultConfig: component.defaultConfig, - defaultSize: component.defaultSize, + isNested: !!parentSplitPanelId, + parentSplitPanelId, + parentPanelSide, + // ๐Ÿ†• ์œ„์น˜ ๋””๋ฒ„๊น… + clientX: e.clientX, + clientY: e.clientY, + tabContentRect: { left: tabContentRect.left, top: tabContentRect.top }, + zoomLevel, + calculatedPosition: { x: dropX, y: dropY }, }); - + const newTabComponent = { id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, componentType: componentType, @@ -2510,7 +2834,7 @@ export default function ScreenDesigner({ size: component.defaultSize || { width: 200, height: 100 }, componentConfig: component.defaultConfig || {}, }; - + // ํ•ด๋‹น ํƒญ์— ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ const updatedTabs = tabs.map((tab: any) => { if (tab.id === activeTabId) { @@ -2521,30 +2845,125 @@ export default function ScreenDesigner({ } return tab; }); - - const updatedComponent = { + + const updatedTabsComponent = { ...targetComponent, componentConfig: { ...currentConfig, tabs: updatedTabs, }, }; - - const newLayout = { - ...layout, - components: layout.components.map((c) => - c.id === containerId ? updatedComponent : c - ), - }; - + + let newLayout; + + if (parentSplitPanelId && parentPanelSide) { + // ๐Ÿ†• ์ค‘์ฒฉ ๊ตฌ์กฐ: ๋ถ„ํ•  ํŒจ๋„ ์•ˆ์˜ ํƒญ ์—…๋ฐ์ดํŠธ + newLayout = { + ...layout, + components: layout.components.map((c) => { + if (c.id === parentSplitPanelId) { + const splitConfig = (c as any).componentConfig || {}; + const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; + const panelConfig = splitConfig[panelKey] || {}; + const panelComponents = panelConfig.components || []; + + return { + ...c, + componentConfig: { + ...splitConfig, + [panelKey]: { + ...panelConfig, + components: panelComponents.map((pc: any) => + pc.id === containerId ? updatedTabsComponent : pc, + ), + }, + }, + }; + } + return c; + }), + }; + toast.success("์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ค‘์ฒฉ๋œ ํƒญ์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); + } else { + // ์ผ๋ฐ˜ ๊ตฌ์กฐ: ์ตœ์ƒ์œ„ ํƒญ ์—…๋ฐ์ดํŠธ + newLayout = { + ...layout, + components: layout.components.map((c) => (c.id === containerId ? updatedTabsComponent : c)), + }; + toast.success("์ปดํฌ๋„ŒํŠธ๊ฐ€ ํƒญ์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); + } + setLayout(newLayout); saveToHistory(newLayout); - toast.success("์ปดํฌ๋„ŒํŠธ๊ฐ€ ํƒญ์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); return; // ํƒญ ์ปจํ…Œ์ด๋„ˆ ์ฒ˜๋ฆฌ ์™„๋ฃŒ } } } + // ๐ŸŽฏ ๋ถ„ํ•  ํŒจ๋„ ์ปค์Šคํ…€ ๋ชจ๋“œ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€ ๋“œ๋กญ ์ฒ˜๋ฆฌ + const splitPanelContainer = dropTarget.closest('[data-split-panel-container="true"]'); + if (splitPanelContainer) { + const containerId = splitPanelContainer.getAttribute("data-component-id"); + const panelSide = splitPanelContainer.getAttribute("data-panel-side"); // "left" or "right" + if (containerId && panelSide) { + const targetComponent = layout.components.find((c) => c.id === containerId); + const compType = (targetComponent as any)?.componentType; + if (targetComponent && (compType === "split-panel-layout" || compType === "v2-split-panel-layout")) { + const currentConfig = (targetComponent as any).componentConfig || {}; + const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel"; + const panelConfig = currentConfig[panelKey] || {}; + const currentComponents = panelConfig.components || []; + + // ๋“œ๋กญ ์œ„์น˜ ๊ณ„์‚ฐ + const panelRect = splitPanelContainer.getBoundingClientRect(); + const dropX = (e.clientX - panelRect.left) / zoomLevel; + const dropY = (e.clientY - panelRect.top) / zoomLevel; + + // ์ƒˆ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ + const componentType = component.id || component.componentType || "v2-text-display"; + + console.log("๐ŸŽฏ ๋ถ„ํ•  ํŒจ๋„์— ์ปดํฌ๋„ŒํŠธ ๋“œ๋กญ:", { + componentId: component.id, + componentType: componentType, + panelSide: panelSide, + dropPosition: { x: dropX, y: dropY }, + }); + + const newPanelComponent = { + id: `panel_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + componentType: componentType, + label: component.name || component.label || "์ƒˆ ์ปดํฌ๋„ŒํŠธ", + position: { x: Math.max(0, dropX), y: Math.max(0, dropY) }, + size: component.defaultSize || { width: 200, height: 100 }, + componentConfig: component.defaultConfig || {}, + }; + + const updatedPanelConfig = { + ...panelConfig, + components: [...currentComponents, newPanelComponent], + }; + + const updatedComponent = { + ...targetComponent, + componentConfig: { + ...currentConfig, + [panelKey]: updatedPanelConfig, + }, + }; + + const newLayout = { + ...layout, + components: layout.components.map((c) => (c.id === containerId ? updatedComponent : c)), + }; + + setLayout(newLayout); + saveToHistory(newLayout); + toast.success(`์ปดํฌ๋„ŒํŠธ๊ฐ€ ${panelSide === "left" ? "์ขŒ์ธก" : "์šฐ์ธก"} ํŒจ๋„์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค`); + return; // ๋ถ„ํ•  ํŒจ๋„ ์ฒ˜๋ฆฌ ์™„๋ฃŒ + } + } + } + const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) return; @@ -2804,6 +3223,7 @@ export default function ScreenDesigner({ position: snappedPosition, size: componentSize, gridColumns: gridColumns, // ์ปดํฌ๋„ŒํŠธ๋ณ„ ๊ทธ๋ฆฌ๋“œ ์ปฌ๋Ÿผ ์ˆ˜ ์ ์šฉ + layerId: activeLayerId || "default-layer", // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ์ถ”๊ฐ€ componentConfig: { type: component.id, // ์ƒˆ ์ปดํฌ๋„ŒํŠธ ์‹œ์Šคํ…œ์˜ ID ์‚ฌ์šฉ webType: component.webType, // ์›นํƒ€์ž… ์ •๋ณด ์ถ”๊ฐ€ @@ -2811,7 +3231,7 @@ export default function ScreenDesigner({ }, webTypeConfig: getDefaultWebTypeConfig(component.webType), style: { - labelDisplay: false, // ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์˜ ๊ธฐ๋ณธ ๋ผ๋ฒจ ํ‘œ์‹œ๋ฅผ false๋กœ ์„ค์ • + labelDisplay: true, // ๐Ÿ†• ๋ผ๋ฒจ ๊ธฐ๋ณธ ํ‘œ์‹œ (์‚ฌ์šฉ์ž๊ฐ€ ๋„๊ณ  ์‹ถ์œผ๋ฉด ์ฒดํฌ ํ•ด์ œ) labelFontSize: "14px", labelColor: "#212121", labelFontWeight: "500", @@ -2837,7 +3257,7 @@ export default function ScreenDesigner({ toast.success(`${component.name} ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); }, - [layout, selectedScreen, saveToHistory], + [layout, selectedScreen, saveToHistory, activeLayerId], ); // ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์ฒ˜๋ฆฌ @@ -2895,7 +3315,7 @@ export default function ScreenDesigner({ if (targetComponent && (rcType === "repeat-container" || rcType === "v2-repeat-container")) { const currentConfig = (targetComponent as any).componentConfig || {}; const currentChildren = currentConfig.children || []; - + // ์ƒˆ ์ž์‹ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ (์ปฌ๋Ÿผ ๊ธฐ๋ฐ˜) const newChild = { id: `slot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, @@ -2906,7 +3326,7 @@ export default function ScreenDesigner({ size: { width: 200, height: 32 }, componentConfig: {}, }; - + const updatedComponent = { ...targetComponent, componentConfig: { @@ -2914,14 +3334,12 @@ export default function ScreenDesigner({ children: [...currentChildren, newChild], }, }; - + const newLayout = { ...layout, - components: layout.components.map((c) => - c.id === containerId ? updatedComponent : c - ), + components: layout.components.map((c) => (c.id === containerId ? updatedComponent : c)), }; - + setLayout(newLayout); saveToHistory(newLayout); return; @@ -2929,23 +3347,57 @@ export default function ScreenDesigner({ } } - // ๐ŸŽฏ ํƒญ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์— ์ปฌ๋Ÿผ ๋“œ๋กญ ์‹œ ์ฒ˜๋ฆฌ + // ๐ŸŽฏ ํƒญ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์— ์ปฌ๋Ÿผ ๋“œ๋กญ ์‹œ ์ฒ˜๋ฆฌ (์ค‘์ฒฉ ๊ตฌ์กฐ ์ง€์›) const tabsContainer = dropTarget.closest('[data-tabs-container="true"]'); if (tabsContainer && type === "column" && column) { const containerId = tabsContainer.getAttribute("data-component-id"); const activeTabId = tabsContainer.getAttribute("data-active-tab-id"); if (containerId && activeTabId) { - const targetComponent = layout.components.find((c) => c.id === containerId); + // 1. ๋จผ์ € ์ตœ์ƒ์œ„ ๋ ˆ์ด์•„์›ƒ์—์„œ ํƒญ ์ปดํฌ๋„ŒํŠธ ์ฐพ๊ธฐ + let targetComponent = layout.components.find((c) => c.id === containerId); + let parentSplitPanelId: string | null = null; + let parentPanelSide: "left" | "right" | null = null; + + // 2. ์ตœ์ƒ์œ„์— ์—†์œผ๋ฉด ๋ถ„ํ•  ํŒจ๋„ ์•ˆ์—์„œ ์ค‘์ฒฉ๋œ ํƒญ ์ฐพ๊ธฐ + if (!targetComponent) { + for (const comp of layout.components) { + const compType = (comp as any)?.componentType; + if (compType === "split-panel-layout" || compType === "v2-split-panel-layout") { + const config = (comp as any).componentConfig || {}; + + // ์ขŒ์ธก ํŒจ๋„์—์„œ ์ฐพ๊ธฐ + const leftComponents = config.leftPanel?.components || []; + const foundInLeft = leftComponents.find((c: any) => c.id === containerId); + if (foundInLeft) { + targetComponent = foundInLeft; + parentSplitPanelId = comp.id; + parentPanelSide = "left"; + break; + } + + // ์šฐ์ธก ํŒจ๋„์—์„œ ์ฐพ๊ธฐ + const rightComponents = config.rightPanel?.components || []; + const foundInRight = rightComponents.find((c: any) => c.id === containerId); + if (foundInRight) { + targetComponent = foundInRight; + parentSplitPanelId = comp.id; + parentPanelSide = "right"; + break; + } + } + } + } + const compType = (targetComponent as any)?.componentType; if (targetComponent && (compType === "tabs-widget" || compType === "v2-tabs-widget")) { const currentConfig = (targetComponent as any).componentConfig || {}; const tabs = currentConfig.tabs || []; - + // ๋“œ๋กญ ์œ„์น˜ ๊ณ„์‚ฐ const tabContentRect = tabsContainer.getBoundingClientRect(); const dropX = (e.clientX - tabContentRect.left) / zoomLevel; const dropY = (e.clientY - tabContentRect.top) / zoomLevel; - + // ๐Ÿ†• V2 ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ ์‚ฌ์šฉ (์ผ๋ฐ˜ ์บ”๋ฒ„์Šค์™€ ๋™์ผ) const v2Mapping = createV2ConfigFromColumn({ widgetType: column.widgetType, @@ -2959,7 +3411,7 @@ export default function ScreenDesigner({ referenceColumn: column.referenceColumn, displayColumn: column.displayColumn, }); - + // ์›นํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ const getTabComponentSize = (widgetType: string) => { const sizeMap: Record = { @@ -2979,25 +3431,25 @@ export default function ScreenDesigner({ }; return sizeMap[widgetType] || { width: 200, height: 36 }; }; - + const componentSize = getTabComponentSize(column.widgetType); - + const newTabComponent = { id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - componentType: v2Mapping.componentType, // v2-input, v2-select ๋“ฑ + componentType: v2Mapping.componentType, label: column.columnLabel || column.columnName, position: { x: Math.max(0, dropX), y: Math.max(0, dropY) }, size: componentSize, - inputType: column.inputType || column.widgetType, // ๐Ÿ†• inputType ์ €์žฅ (์„ค์ • ํŒจ๋„์šฉ) - widgetType: column.widgetType, // ๐Ÿ†• widgetType ์ €์žฅ + inputType: column.inputType || column.widgetType, + widgetType: column.widgetType, componentConfig: { - ...v2Mapping.componentConfig, // V2 ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ณธ ์„ค์ • + ...v2Mapping.componentConfig, columnName: column.columnName, tableName: column.tableName, - inputType: column.inputType || column.widgetType, // ๐Ÿ†• componentConfig์—๋„ ์ €์žฅ + inputType: column.inputType || column.widgetType, }, }; - + // ํ•ด๋‹น ํƒญ์— ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ const updatedTabs = tabs.map((tab: any) => { if (tab.id === activeTabId) { @@ -3008,25 +3460,153 @@ export default function ScreenDesigner({ } return tab; }); - - const updatedComponent = { + + const updatedTabsComponent = { ...targetComponent, componentConfig: { ...currentConfig, tabs: updatedTabs, }, }; - - const newLayout = { - ...layout, - components: layout.components.map((c) => - c.id === containerId ? updatedComponent : c - ), - }; - + + let newLayout; + + if (parentSplitPanelId && parentPanelSide) { + // ๐Ÿ†• ์ค‘์ฒฉ ๊ตฌ์กฐ: ๋ถ„ํ•  ํŒจ๋„ ์•ˆ์˜ ํƒญ ์—…๋ฐ์ดํŠธ + newLayout = { + ...layout, + components: layout.components.map((c) => { + if (c.id === parentSplitPanelId) { + const splitConfig = (c as any).componentConfig || {}; + const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; + const panelConfig = splitConfig[panelKey] || {}; + const panelComponents = panelConfig.components || []; + + return { + ...c, + componentConfig: { + ...splitConfig, + [panelKey]: { + ...panelConfig, + components: panelComponents.map((pc: any) => + pc.id === containerId ? updatedTabsComponent : pc, + ), + }, + }, + }; + } + return c; + }), + }; + toast.success("์ปฌ๋Ÿผ์ด ์ค‘์ฒฉ๋œ ํƒญ์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); + } else { + // ์ผ๋ฐ˜ ๊ตฌ์กฐ: ์ตœ์ƒ์œ„ ํƒญ ์—…๋ฐ์ดํŠธ + newLayout = { + ...layout, + components: layout.components.map((c) => (c.id === containerId ? updatedTabsComponent : c)), + }; + toast.success("์ปฌ๋Ÿผ์ด ํƒญ์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); + } + setLayout(newLayout); saveToHistory(newLayout); - toast.success("์ปฌ๋Ÿผ์ด ํƒญ์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); + return; + } + } + } + + // ๐ŸŽฏ ๋ถ„ํ•  ํŒจ๋„ ์ปค์Šคํ…€ ๋ชจ๋“œ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์— ์ปฌ๋Ÿผ ๋“œ๋กญ ์‹œ ์ฒ˜๋ฆฌ + const splitPanelContainer = dropTarget.closest('[data-split-panel-container="true"]'); + if (splitPanelContainer && type === "column" && column) { + const containerId = splitPanelContainer.getAttribute("data-component-id"); + const panelSide = splitPanelContainer.getAttribute("data-panel-side"); // "left" or "right" + if (containerId && panelSide) { + const targetComponent = layout.components.find((c) => c.id === containerId); + const compType = (targetComponent as any)?.componentType; + if (targetComponent && (compType === "split-panel-layout" || compType === "v2-split-panel-layout")) { + const currentConfig = (targetComponent as any).componentConfig || {}; + const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel"; + const panelConfig = currentConfig[panelKey] || {}; + const currentComponents = panelConfig.components || []; + + // ๋“œ๋กญ ์œ„์น˜ ๊ณ„์‚ฐ + const panelRect = splitPanelContainer.getBoundingClientRect(); + const dropX = (e.clientX - panelRect.left) / zoomLevel; + const dropY = (e.clientY - panelRect.top) / zoomLevel; + + // V2 ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ ์‚ฌ์šฉ + const v2Mapping = createV2ConfigFromColumn({ + widgetType: column.widgetType, + columnName: column.columnName, + columnLabel: column.columnLabel, + codeCategory: column.codeCategory, + inputType: column.inputType, + required: column.required, + detailSettings: column.detailSettings, + referenceTable: column.referenceTable, + referenceColumn: column.referenceColumn, + displayColumn: column.displayColumn, + }); + + // ์›นํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ + const getPanelComponentSize = (widgetType: string) => { + const sizeMap: Record = { + text: { width: 200, height: 36 }, + number: { width: 150, height: 36 }, + decimal: { width: 150, height: 36 }, + date: { width: 180, height: 36 }, + datetime: { width: 200, height: 36 }, + select: { width: 200, height: 36 }, + category: { width: 200, height: 36 }, + code: { width: 200, height: 36 }, + entity: { width: 220, height: 36 }, + boolean: { width: 120, height: 36 }, + checkbox: { width: 120, height: 36 }, + textarea: { width: 300, height: 100 }, + file: { width: 250, height: 80 }, + }; + return sizeMap[widgetType] || { width: 200, height: 36 }; + }; + + const componentSize = getPanelComponentSize(column.widgetType); + + const newPanelComponent = { + id: `panel_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + componentType: v2Mapping.componentType, + label: column.columnLabel || column.columnName, + position: { x: Math.max(0, dropX), y: Math.max(0, dropY) }, + size: componentSize, + inputType: column.inputType || column.widgetType, + widgetType: column.widgetType, + componentConfig: { + ...v2Mapping.componentConfig, + columnName: column.columnName, + tableName: column.tableName, + inputType: column.inputType || column.widgetType, + }, + }; + + const updatedPanelConfig = { + ...panelConfig, + components: [...currentComponents, newPanelComponent], + }; + + const updatedComponent = { + ...targetComponent, + componentConfig: { + ...currentConfig, + [panelKey]: updatedPanelConfig, + }, + }; + + const newLayout = { + ...layout, + components: layout.components.map((c) => (c.id === containerId ? updatedComponent : c)), + }; + + setLayout(newLayout); + saveToHistory(newLayout); + toast.success(`์ปฌ๋Ÿผ์ด ${panelSide === "left" ? "์ขŒ์ธก" : "์šฐ์ธก"} ํŒจ๋„์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค`); return; } } @@ -3049,6 +3629,7 @@ export default function ScreenDesigner({ tableName: table.tableName, position: { x, y, z: 1 } as Position, size: { width: 300, height: 200 }, + layerId: activeLayerId || "default-layer", // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ์ถ”๊ฐ€ style: { labelDisplay: true, labelFontSize: "14px", @@ -3299,6 +3880,7 @@ export default function ScreenDesigner({ componentType: v2Mapping.componentType, // v2-input, v2-select ๋“ฑ position: { x: relativeX, y: relativeY, z: 1 } as Position, size: { width: componentWidth, height: getDefaultHeight(column.widgetType) }, + layerId: activeLayerId || "default-layer", // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ์ถ”๊ฐ€ // ์ฝ”๋“œ ํƒ€์ž…์ธ ๊ฒฝ์šฐ ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด ์ถ”๊ฐ€ ...(column.widgetType === "code" && column.codeCategory && { @@ -3309,9 +3891,9 @@ export default function ScreenDesigner({ isEntityJoin: true, entityJoinTable: column.entityJoinTable, entityJoinColumn: column.entityJoinColumn, - }), + }), style: { - labelDisplay: false, // ๋ผ๋ฒจ ์ˆจ๊น€ + labelDisplay: true, // ๐Ÿ†• ๋ผ๋ฒจ ๊ธฐ๋ณธ ํ‘œ์‹œ labelFontSize: "12px", labelColor: "#212121", labelFontWeight: "500", @@ -3365,6 +3947,7 @@ export default function ScreenDesigner({ componentType: v2Mapping.componentType, // v2-input, v2-select ๋“ฑ position: { x, y, z: 1 } as Position, size: { width: componentWidth, height: getDefaultHeight(column.widgetType) }, + layerId: activeLayerId || "default-layer", // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ์ถ”๊ฐ€ // ์ฝ”๋“œ ํƒ€์ž…์ธ ๊ฒฝ์šฐ ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด ์ถ”๊ฐ€ ...(column.widgetType === "code" && column.codeCategory && { @@ -3375,9 +3958,9 @@ export default function ScreenDesigner({ isEntityJoin: true, entityJoinTable: column.entityJoinTable, entityJoinColumn: column.entityJoinColumn, - }), + }), style: { - labelDisplay: false, // ๋ผ๋ฒจ ์ˆจ๊น€ + labelDisplay: true, // ๐Ÿ†• ๋ผ๋ฒจ ๊ธฐ๋ณธ ํ‘œ์‹œ labelFontSize: "14px", labelColor: "#000000", // ์ˆœ์ˆ˜ํ•œ ๊ฒ€์ • labelFontWeight: "500", @@ -3645,252 +4228,325 @@ export default function ScreenDesigner({ ); // ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ - const endDrag = useCallback((mouseEvent?: MouseEvent) => { - if (dragState.isDragging && dragState.draggedComponent) { - // ๐ŸŽฏ ํƒญ ์ปจํ…Œ์ด๋„ˆ๋กœ์˜ ๋“œ๋กญ ์ฒ˜๋ฆฌ (๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ ์ด๋™) - if (mouseEvent) { - const dropTarget = document.elementFromPoint(mouseEvent.clientX, mouseEvent.clientY) as HTMLElement; - const tabsContainer = dropTarget?.closest('[data-tabs-container="true"]'); - - if (tabsContainer) { - const containerId = tabsContainer.getAttribute("data-component-id"); - const activeTabId = tabsContainer.getAttribute("data-active-tab-id"); - - if (containerId && activeTabId) { - const targetComponent = layout.components.find((c) => c.id === containerId); - const compType = (targetComponent as any)?.componentType; - - // ์ž๊ธฐ ์ž์‹ ์„ ์ž์‹ ์—๊ฒŒ ๋“œ๋กญํ•˜๋Š” ๊ฒƒ ๋ฐฉ์ง€ - if (targetComponent && - (compType === "tabs-widget" || compType === "v2-tabs-widget") && - dragState.draggedComponent !== containerId) { - - const draggedComponent = layout.components.find((c) => c.id === dragState.draggedComponent); - if (draggedComponent) { - const currentConfig = (targetComponent as any).componentConfig || {}; - const tabs = currentConfig.tabs || []; - - // ํƒญ ์ปจํ…์ธ  ์˜์—ญ ๊ธฐ์ค€ ๋“œ๋กญ ์œ„์น˜ ๊ณ„์‚ฐ - const tabContentRect = tabsContainer.getBoundingClientRect(); - const dropX = (mouseEvent.clientX - tabContentRect.left) / zoomLevel; - const dropY = (mouseEvent.clientY - tabContentRect.top) / zoomLevel; - - // ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ๋ฅผ ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ณ€ํ™˜ - const newTabComponent = { - id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - componentType: (draggedComponent as any).componentType || draggedComponent.type, - label: (draggedComponent as any).label || "์ปดํฌ๋„ŒํŠธ", - position: { x: Math.max(0, dropX), y: Math.max(0, dropY) }, - size: draggedComponent.size || { width: 200, height: 100 }, - componentConfig: (draggedComponent as any).componentConfig || {}, - style: (draggedComponent as any).style || {}, - }; - - // ํ•ด๋‹น ํƒญ์— ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ - const updatedTabs = tabs.map((tab: any) => { - if (tab.id === activeTabId) { - return { - ...tab, - components: [...(tab.components || []), newTabComponent], - }; + const endDrag = useCallback( + (mouseEvent?: MouseEvent) => { + if (dragState.isDragging && dragState.draggedComponent) { + // ๐ŸŽฏ ํƒญ ์ปจํ…Œ์ด๋„ˆ๋กœ์˜ ๋“œ๋กญ ์ฒ˜๋ฆฌ (๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ ์ด๋™, ์ค‘์ฒฉ ๊ตฌ์กฐ ์ง€์›) + if (mouseEvent) { + const dropTarget = document.elementFromPoint(mouseEvent.clientX, mouseEvent.clientY) as HTMLElement; + const tabsContainer = dropTarget?.closest('[data-tabs-container="true"]'); + + if (tabsContainer) { + const containerId = tabsContainer.getAttribute("data-component-id"); + const activeTabId = tabsContainer.getAttribute("data-active-tab-id"); + + if (containerId && activeTabId) { + // 1. ๋จผ์ € ์ตœ์ƒ์œ„ ๋ ˆ์ด์•„์›ƒ์—์„œ ํƒญ ์ปดํฌ๋„ŒํŠธ ์ฐพ๊ธฐ + let targetComponent = layout.components.find((c) => c.id === containerId); + let parentSplitPanelId: string | null = null; + let parentPanelSide: "left" | "right" | null = null; + + // 2. ์ตœ์ƒ์œ„์— ์—†์œผ๋ฉด ๋ถ„ํ•  ํŒจ๋„ ์•ˆ์—์„œ ์ค‘์ฒฉ๋œ ํƒญ ์ฐพ๊ธฐ + if (!targetComponent) { + for (const comp of layout.components) { + const compType = (comp as any)?.componentType; + if (compType === "split-panel-layout" || compType === "v2-split-panel-layout") { + const config = (comp as any).componentConfig || {}; + + // ์ขŒ์ธก ํŒจ๋„์—์„œ ์ฐพ๊ธฐ + const leftComponents = config.leftPanel?.components || []; + const foundInLeft = leftComponents.find((c: any) => c.id === containerId); + if (foundInLeft) { + targetComponent = foundInLeft; + parentSplitPanelId = comp.id; + parentPanelSide = "left"; + break; + } + + // ์šฐ์ธก ํŒจ๋„์—์„œ ์ฐพ๊ธฐ + const rightComponents = config.rightPanel?.components || []; + const foundInRight = rightComponents.find((c: any) => c.id === containerId); + if (foundInRight) { + targetComponent = foundInRight; + parentSplitPanelId = comp.id; + parentPanelSide = "right"; + break; + } } - return tab; - }); - - // ํƒญ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ + ์›๋ž˜ ์ปดํฌ๋„ŒํŠธ ์บ”๋ฒ„์Šค์—์„œ ์ œ๊ฑฐ - const newLayout = { - ...layout, - components: layout.components - .filter((c) => c.id !== dragState.draggedComponent) // ์บ”๋ฒ„์Šค์—์„œ ์ œ๊ฑฐ - .map((c) => { - if (c.id === containerId) { - return { - ...c, - componentConfig: { - ...currentConfig, - tabs: updatedTabs, - }, - }; - } - return c; - }), - }; - - setLayout(newLayout); - saveToHistory(newLayout); - setSelectedComponent(null); - toast.success("์ปดํฌ๋„ŒํŠธ๊ฐ€ ํƒญ์œผ๋กœ ์ด๋™๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); - - // ๋“œ๋ž˜๊ทธ ์ƒํƒœ ์ดˆ๊ธฐํ™” ํ›„ ์ข…๋ฃŒ - setDragState({ - isDragging: false, - draggedComponent: null, - draggedComponents: [], - originalPosition: { x: 0, y: 0, z: 1 }, - currentPosition: { x: 0, y: 0, z: 1 }, - grabOffset: { x: 0, y: 0 }, - justFinishedDrag: true, - }); - - setTimeout(() => { - setDragState((prev) => ({ ...prev, justFinishedDrag: false })); - }, 100); - - return; // ํƒญ์œผ๋กœ ์ด๋™ ์™„๋ฃŒ, ์ผ๋ฐ˜ ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ ๋กœ์ง ์Šคํ‚ต + } + } + + const compType = (targetComponent as any)?.componentType; + + // ์ž๊ธฐ ์ž์‹ ์„ ์ž์‹ ์—๊ฒŒ ๋“œ๋กญํ•˜๋Š” ๊ฒƒ ๋ฐฉ์ง€ + if ( + targetComponent && + (compType === "tabs-widget" || compType === "v2-tabs-widget") && + dragState.draggedComponent !== containerId + ) { + const draggedComponent = layout.components.find((c) => c.id === dragState.draggedComponent); + if (draggedComponent) { + const currentConfig = (targetComponent as any).componentConfig || {}; + const tabs = currentConfig.tabs || []; + + // ํƒญ ์ปจํ…์ธ  ์˜์—ญ ๊ธฐ์ค€ ๋“œ๋กญ ์œ„์น˜ ๊ณ„์‚ฐ + const tabContentRect = tabsContainer.getBoundingClientRect(); + const dropX = (mouseEvent.clientX - tabContentRect.left) / zoomLevel; + const dropY = (mouseEvent.clientY - tabContentRect.top) / zoomLevel; + + // ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ๋ฅผ ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ณ€ํ™˜ + const newTabComponent = { + id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + componentType: (draggedComponent as any).componentType || draggedComponent.type, + label: (draggedComponent as any).label || "์ปดํฌ๋„ŒํŠธ", + position: { x: Math.max(0, dropX), y: Math.max(0, dropY) }, + size: draggedComponent.size || { width: 200, height: 100 }, + componentConfig: (draggedComponent as any).componentConfig || {}, + style: (draggedComponent as any).style || {}, + }; + + // ํ•ด๋‹น ํƒญ์— ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ + const updatedTabs = tabs.map((tab: any) => { + if (tab.id === activeTabId) { + return { + ...tab, + components: [...(tab.components || []), newTabComponent], + }; + } + return tab; + }); + + const updatedTabsComponent = { + ...targetComponent, + componentConfig: { + ...currentConfig, + tabs: updatedTabs, + }, + }; + + let newLayout; + + if (parentSplitPanelId && parentPanelSide) { + // ๐Ÿ†• ์ค‘์ฒฉ ๊ตฌ์กฐ: ๋ถ„ํ•  ํŒจ๋„ ์•ˆ์˜ ํƒญ ์—…๋ฐ์ดํŠธ + newLayout = { + ...layout, + components: layout.components + .filter((c) => c.id !== dragState.draggedComponent) + .map((c) => { + if (c.id === parentSplitPanelId) { + const splitConfig = (c as any).componentConfig || {}; + const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; + const panelConfig = splitConfig[panelKey] || {}; + const panelComponents = panelConfig.components || []; + + return { + ...c, + componentConfig: { + ...splitConfig, + [panelKey]: { + ...panelConfig, + components: panelComponents.map((pc: any) => + pc.id === containerId ? updatedTabsComponent : pc, + ), + }, + }, + }; + } + return c; + }), + }; + toast.success("์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ค‘์ฒฉ๋œ ํƒญ์œผ๋กœ ์ด๋™๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); + } else { + // ์ผ๋ฐ˜ ๊ตฌ์กฐ: ์ตœ์ƒ์œ„ ํƒญ ์—…๋ฐ์ดํŠธ + newLayout = { + ...layout, + components: layout.components + .filter((c) => c.id !== dragState.draggedComponent) + .map((c) => { + if (c.id === containerId) { + return updatedTabsComponent; + } + return c; + }), + }; + toast.success("์ปดํฌ๋„ŒํŠธ๊ฐ€ ํƒญ์œผ๋กœ ์ด๋™๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); + } + + setLayout(newLayout); + saveToHistory(newLayout); + setSelectedComponent(null); + + // ๋“œ๋ž˜๊ทธ ์ƒํƒœ ์ดˆ๊ธฐํ™” ํ›„ ์ข…๋ฃŒ + setDragState({ + isDragging: false, + draggedComponent: null, + draggedComponents: [], + originalPosition: { x: 0, y: 0, z: 1 }, + currentPosition: { x: 0, y: 0, z: 1 }, + grabOffset: { x: 0, y: 0 }, + justFinishedDrag: true, + }); + + setTimeout(() => { + setDragState((prev) => ({ ...prev, justFinishedDrag: false })); + }, 100); + + return; // ํƒญ์œผ๋กœ ์ด๋™ ์™„๋ฃŒ, ์ผ๋ฐ˜ ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ ๋กœ์ง ์Šคํ‚ต + } } } } } - } - - // ์ฃผ ๋“œ๋ž˜๊ทธ ์ปดํฌ๋„ŒํŠธ์˜ ์ตœ์ข… ์œ„์น˜ ๊ณ„์‚ฐ - const draggedComponent = layout.components.find((c) => c.id === dragState.draggedComponent); - let finalPosition = dragState.currentPosition; - // ํ˜„์žฌ ํ•ด์ƒ๋„์— ๋งž๋Š” ๊ฒฉ์ž ์ •๋ณด ๊ณ„์‚ฐ - const currentGridInfo = layout.gridSettings - ? calculateGridInfo(screenResolution.width, screenResolution.height, { - columns: layout.gridSettings.columns, - gap: layout.gridSettings.gap, - padding: layout.gridSettings.padding, - snapToGrid: layout.gridSettings.snapToGrid || false, - }) - : null; + // ์ฃผ ๋“œ๋ž˜๊ทธ ์ปดํฌ๋„ŒํŠธ์˜ ์ตœ์ข… ์œ„์น˜ ๊ณ„์‚ฐ + const draggedComponent = layout.components.find((c) => c.id === dragState.draggedComponent); + let finalPosition = dragState.currentPosition; - // ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ ๋ฐ ํ”Œ๋กœ์šฐ ๋ฒ„ํŠผ ๊ทธ๋ฃน์— ๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ (์ผ๋ฐ˜ ๊ทธ๋ฃน ์ œ์™ธ) - if (draggedComponent?.type !== "group" && layout.gridSettings?.snapToGrid && currentGridInfo) { - finalPosition = snapPositionTo10px( - { - x: dragState.currentPosition.x, - y: dragState.currentPosition.y, - z: dragState.currentPosition.z ?? 1, - }, - currentGridInfo, - { - columns: layout.gridSettings.columns, - gap: layout.gridSettings.gap, - padding: layout.gridSettings.padding, - snapToGrid: layout.gridSettings.snapToGrid || false, - }, - ); + // ํ˜„์žฌ ํ•ด์ƒ๋„์— ๋งž๋Š” ๊ฒฉ์ž ์ •๋ณด ๊ณ„์‚ฐ + const currentGridInfo = layout.gridSettings + ? calculateGridInfo(screenResolution.width, screenResolution.height, { + columns: layout.gridSettings.columns, + gap: layout.gridSettings.gap, + padding: layout.gridSettings.padding, + snapToGrid: layout.gridSettings.snapToGrid || false, + }) + : null; - } + // ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ ๋ฐ ํ”Œ๋กœ์šฐ ๋ฒ„ํŠผ ๊ทธ๋ฃน์— ๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ (์ผ๋ฐ˜ ๊ทธ๋ฃน ์ œ์™ธ) + if (draggedComponent?.type !== "group" && layout.gridSettings?.snapToGrid && currentGridInfo) { + finalPosition = snapPositionTo10px( + { + x: dragState.currentPosition.x, + y: dragState.currentPosition.y, + z: dragState.currentPosition.z ?? 1, + }, + currentGridInfo, + { + columns: layout.gridSettings.columns, + gap: layout.gridSettings.gap, + padding: layout.gridSettings.padding, + snapToGrid: layout.gridSettings.snapToGrid || false, + }, + ); + } - // ์Šค๋ƒ…์œผ๋กœ ์ธํ•œ ์ถ”๊ฐ€ ์ด๋™ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ - const snapDeltaX = finalPosition.x - dragState.currentPosition.x; - const snapDeltaY = finalPosition.y - dragState.currentPosition.y; + // ์Šค๋ƒ…์œผ๋กœ ์ธํ•œ ์ถ”๊ฐ€ ์ด๋™ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ + const snapDeltaX = finalPosition.x - dragState.currentPosition.x; + const snapDeltaY = finalPosition.y - dragState.currentPosition.y; - // ์›๋ž˜ ์ด๋™ ๊ฑฐ๋ฆฌ + ์Šค๋ƒ… ์กฐ์ • ๊ฑฐ๋ฆฌ - const totalDeltaX = dragState.currentPosition.x - dragState.originalPosition.x + snapDeltaX; - const totalDeltaY = dragState.currentPosition.y - dragState.originalPosition.y + snapDeltaY; + // ์›๋ž˜ ์ด๋™ ๊ฑฐ๋ฆฌ + ์Šค๋ƒ… ์กฐ์ • ๊ฑฐ๋ฆฌ + const totalDeltaX = dragState.currentPosition.x - dragState.originalPosition.x + snapDeltaX; + const totalDeltaY = dragState.currentPosition.y - dragState.originalPosition.y + snapDeltaY; - // ๋‹ค์ค‘ ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ์ตœ์ข… ์œ„์น˜ ์—…๋ฐ์ดํŠธ - const updatedComponents = layout.components.map((comp) => { - const isDraggedComponent = dragState.draggedComponents.some((dragComp) => dragComp.id === comp.id); - if (isDraggedComponent) { - const originalComponent = dragState.draggedComponents.find((dragComp) => dragComp.id === comp.id)!; - let newPosition = { - x: originalComponent.position.x + totalDeltaX, - y: originalComponent.position.y + totalDeltaY, - z: originalComponent.position.z || 1, - }; - - // ์บ”๋ฒ„์Šค ๊ฒฝ๊ณ„ ์ œํ•œ (์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ™”๋ฉด ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€์ง€ ์•Š๋„๋ก) - const componentWidth = comp.size?.width || 100; - const componentHeight = comp.size?.height || 40; - - // ์ตœ์†Œ ์œ„์น˜: 0, ์ตœ๋Œ€ ์œ„์น˜: ์บ”๋ฒ„์Šค ํฌ๊ธฐ - ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ - newPosition.x = Math.max(0, Math.min(newPosition.x, screenResolution.width - componentWidth)); - newPosition.y = Math.max(0, Math.min(newPosition.y, screenResolution.height - componentHeight)); - - // ๊ทธ๋ฃน ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ์ธ ๊ฒฝ์šฐ ํŒจ๋”ฉ์„ ๊ณ ๋ คํ•œ ๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ - if (comp.parentId && layout.gridSettings?.snapToGrid && gridInfo) { - const { columnWidth } = gridInfo; - const { gap } = layout.gridSettings; - - // ๊ทธ๋ฃน ๋‚ด๋ถ€ ํŒจ๋”ฉ ๊ณ ๋ คํ•œ ๊ฒฉ์ž ์ •๋ ฌ - const padding = 16; - const effectiveX = newPosition.x - padding; - const columnIndex = Math.round(effectiveX / (columnWidth + (gap || 16))); - const snappedX = padding + columnIndex * (columnWidth + (gap || 16)); - - // Y ์ขŒํ‘œ๋Š” 20px ๋‹จ์œ„๋กœ ์Šค๋ƒ… - const effectiveY = newPosition.y - padding; - const rowIndex = Math.round(effectiveY / 20); - const snappedY = padding + rowIndex * 20; - - // ํฌ๊ธฐ๋„ ์™ธ๋ถ€ ๊ฒฉ์ž์™€ ๋™์ผํ•˜๊ฒŒ ์Šค๋ƒ… - const fullColumnWidth = columnWidth + (gap || 16); // ์™ธ๋ถ€ ๊ฒฉ์ž์™€ ๋™์ผํ•œ ํฌ๊ธฐ - const widthInColumns = Math.max(1, Math.round(comp.size.width / fullColumnWidth)); - const snappedWidth = widthInColumns * fullColumnWidth - (gap || 16); // gap ์ œ๊ฑฐํ•˜์—ฌ ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ - // ๋†’์ด๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๊ฐ’ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ (์Šค๋ƒ… ์ œ๊ฑฐ) - const snappedHeight = Math.max(40, comp.size.height); - - newPosition = { - x: Math.max(padding, snappedX), // ํŒจ๋”ฉ๋งŒํผ ์ตœ์†Œ ์—ฌ๋ฐฑ ํ™•๋ณด - y: Math.max(padding, snappedY), - z: newPosition.z, + // ๋‹ค์ค‘ ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ์ตœ์ข… ์œ„์น˜ ์—…๋ฐ์ดํŠธ + const updatedComponents = layout.components.map((comp) => { + const isDraggedComponent = dragState.draggedComponents.some((dragComp) => dragComp.id === comp.id); + if (isDraggedComponent) { + const originalComponent = dragState.draggedComponents.find((dragComp) => dragComp.id === comp.id)!; + let newPosition = { + x: originalComponent.position.x + totalDeltaX, + y: originalComponent.position.y + totalDeltaY, + z: originalComponent.position.z || 1, }; - // ํฌ๊ธฐ๋„ ์—…๋ฐ์ดํŠธ - const newSize = { - width: snappedWidth, - height: snappedHeight, - }; + // ์บ”๋ฒ„์Šค ๊ฒฝ๊ณ„ ์ œํ•œ (์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ™”๋ฉด ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€์ง€ ์•Š๋„๋ก) + const componentWidth = comp.size?.width || 100; + const componentHeight = comp.size?.height || 40; + + // ์ตœ์†Œ ์œ„์น˜: 0, ์ตœ๋Œ€ ์œ„์น˜: ์บ”๋ฒ„์Šค ํฌ๊ธฐ - ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ + newPosition.x = Math.max(0, Math.min(newPosition.x, screenResolution.width - componentWidth)); + newPosition.y = Math.max(0, Math.min(newPosition.y, screenResolution.height - componentHeight)); + + // ๊ทธ๋ฃน ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ์ธ ๊ฒฝ์šฐ ํŒจ๋”ฉ์„ ๊ณ ๋ คํ•œ ๊ฒฉ์ž ์Šค๋ƒ… ์ ์šฉ + if (comp.parentId && layout.gridSettings?.snapToGrid && gridInfo) { + const { columnWidth } = gridInfo; + const { gap } = layout.gridSettings; + + // ๊ทธ๋ฃน ๋‚ด๋ถ€ ํŒจ๋”ฉ ๊ณ ๋ คํ•œ ๊ฒฉ์ž ์ •๋ ฌ + const padding = 16; + const effectiveX = newPosition.x - padding; + const columnIndex = Math.round(effectiveX / (columnWidth + (gap || 16))); + const snappedX = padding + columnIndex * (columnWidth + (gap || 16)); + + // Y ์ขŒํ‘œ๋Š” 20px ๋‹จ์œ„๋กœ ์Šค๋ƒ… + const effectiveY = newPosition.y - padding; + const rowIndex = Math.round(effectiveY / 20); + const snappedY = padding + rowIndex * 20; + + // ํฌ๊ธฐ๋„ ์™ธ๋ถ€ ๊ฒฉ์ž์™€ ๋™์ผํ•˜๊ฒŒ ์Šค๋ƒ… + const fullColumnWidth = columnWidth + (gap || 16); // ์™ธ๋ถ€ ๊ฒฉ์ž์™€ ๋™์ผํ•œ ํฌ๊ธฐ + const widthInColumns = Math.max(1, Math.round(comp.size.width / fullColumnWidth)); + const snappedWidth = widthInColumns * fullColumnWidth - (gap || 16); // gap ์ œ๊ฑฐํ•˜์—ฌ ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ + // ๋†’์ด๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๊ฐ’ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ (์Šค๋ƒ… ์ œ๊ฑฐ) + const snappedHeight = Math.max(40, comp.size.height); + + newPosition = { + x: Math.max(padding, snappedX), // ํŒจ๋”ฉ๋งŒํผ ์ตœ์†Œ ์—ฌ๋ฐฑ ํ™•๋ณด + y: Math.max(padding, snappedY), + z: newPosition.z, + }; + + // ํฌ๊ธฐ๋„ ์—…๋ฐ์ดํŠธ + const newSize = { + width: snappedWidth, + height: snappedHeight, + }; + + return { + ...comp, + position: newPosition as Position, + size: newSize, + }; + } return { ...comp, position: newPosition as Position, - size: newSize, }; } + return comp; + }); - return { - ...comp, - position: newPosition as Position, - }; + const newLayout = { ...layout, components: updatedComponents }; + setLayout(newLayout); + + // ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ๋„ ์—…๋ฐ์ดํŠธ (PropertiesPanel ๋™๊ธฐํ™”์šฉ) + if (selectedComponent && dragState.draggedComponents.some((c) => c.id === selectedComponent.id)) { + const updatedSelectedComponent = updatedComponents.find((c) => c.id === selectedComponent.id); + if (updatedSelectedComponent) { + console.log("๐Ÿ”„ ScreenDesigner: ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์œ„์น˜ ์—…๋ฐ์ดํŠธ", { + componentId: selectedComponent.id, + oldPosition: selectedComponent.position, + newPosition: updatedSelectedComponent.position, + }); + setSelectedComponent(updatedSelectedComponent); + } } - return comp; - }); - const newLayout = { ...layout, components: updatedComponents }; - setLayout(newLayout); - - // ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ๋„ ์—…๋ฐ์ดํŠธ (PropertiesPanel ๋™๊ธฐํ™”์šฉ) - if (selectedComponent && dragState.draggedComponents.some((c) => c.id === selectedComponent.id)) { - const updatedSelectedComponent = updatedComponents.find((c) => c.id === selectedComponent.id); - if (updatedSelectedComponent) { - console.log("๐Ÿ”„ ScreenDesigner: ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์œ„์น˜ ์—…๋ฐ์ดํŠธ", { - componentId: selectedComponent.id, - oldPosition: selectedComponent.position, - newPosition: updatedSelectedComponent.position, - }); - setSelectedComponent(updatedSelectedComponent); - } + // ํžˆ์Šคํ† ๋ฆฌ์— ์ €์žฅ + saveToHistory(newLayout); } - // ํžˆ์Šคํ† ๋ฆฌ์— ์ €์žฅ - saveToHistory(newLayout); - } + setDragState({ + isDragging: false, + draggedComponent: null, + draggedComponents: [], + originalPosition: { x: 0, y: 0, z: 1 }, + currentPosition: { x: 0, y: 0, z: 1 }, + grabOffset: { x: 0, y: 0 }, + justFinishedDrag: true, + }); - setDragState({ - isDragging: false, - draggedComponent: null, - draggedComponents: [], - originalPosition: { x: 0, y: 0, z: 1 }, - currentPosition: { x: 0, y: 0, z: 1 }, - grabOffset: { x: 0, y: 0 }, - justFinishedDrag: true, - }); - - // ์งง์€ ์‹œ๊ฐ„ ํ›„ justFinishedDrag ํ”Œ๋ž˜๊ทธ ํ•ด์ œ - setTimeout(() => { - setDragState((prev) => ({ - ...prev, - justFinishedDrag: false, - })); - }, 100); - }, [dragState, layout, saveToHistory]); + // ์งง์€ ์‹œ๊ฐ„ ํ›„ justFinishedDrag ํ”Œ๋ž˜๊ทธ ํ•ด์ œ + setTimeout(() => { + setDragState((prev) => ({ + ...prev, + justFinishedDrag: false, + })); + }, 100); + }, + [dragState, layout, saveToHistory], + ); // ๋“œ๋ž˜๊ทธ ์„ ํƒ ์‹œ์ž‘ const startSelectionDrag = useCallback( @@ -3943,7 +4599,8 @@ export default function ScreenDesigner({ bottom: Math.max(selectionDrag.startPoint.y, currentPoint.y), }; - const selectedIds = layout.components + // ๐Ÿ†• visibleComponents๋งŒ ์„ ํƒ ๋Œ€์ƒ์œผ๋กœ (ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ๋งŒ) + const selectedIds = visibleComponents .filter((comp) => { const compRect = { left: comp.position.x, @@ -3966,7 +4623,7 @@ export default function ScreenDesigner({ selectedComponents: selectedIds, })); }, - [selectionDrag.isSelecting, selectionDrag.startPoint, layout.components, zoomLevel], + [selectionDrag.isSelecting, selectionDrag.startPoint, visibleComponents, zoomLevel], ); // ๋“œ๋ž˜๊ทธ ์„ ํƒ ์ข…๋ฃŒ @@ -4113,6 +4770,7 @@ export default function ScreenDesigner({ z: clipComponent.position.z || 1, } as Position, parentId: undefined, // ๋ถ™์—ฌ๋„ฃ๊ธฐ ์‹œ ๋ถ€๋ชจ ๊ด€๊ณ„ ํ•ด์ œ + layerId: activeLayerId || "default-layer", // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์— ๋ถ™์—ฌ๋„ฃ๊ธฐ }; newComponents.push(newComponent); }); @@ -4133,7 +4791,7 @@ export default function ScreenDesigner({ // console.log("์ปดํฌ๋„ŒํŠธ ๋ถ™์—ฌ๋„ฃ๊ธฐ ์™„๋ฃŒ:", newComponents.length, "๊ฐœ"); toast.success(`${newComponents.length}๊ฐœ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ถ™์—ฌ๋„ฃ์–ด์กŒ์Šต๋‹ˆ๋‹ค.`); - }, [clipboard, layout, saveToHistory]); + }, [clipboard, layout, saveToHistory, activeLayerId]); // ๐Ÿ†• ํ”Œ๋กœ์šฐ ๋ฒ„ํŠผ ๊ทธ๋ฃน ์ƒ์„ฑ (๋‹ค์ค‘ ์„ ํƒ๋œ ๋ฒ„ํŠผ๋“ค์„ ํ•œ ๋ฒˆ์— ๊ทธ๋ฃน์œผ๋กœ) // ๐Ÿ†• ํ”Œ๋กœ์šฐ ๋ฒ„ํŠผ ๊ทธ๋ฃน ๋‹ค์ด์–ผ๋กœ๊ทธ ์ƒํƒœ @@ -4859,6 +5517,105 @@ export default function ScreenDesigner({ } return false; } + + // === 9. ํ™”์‚ดํ‘œ ํ‚ค Nudge (์ปดํฌ๋„ŒํŠธ ๋ฏธ์„ธ ์ด๋™) === + if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) { + // ์ž…๋ ฅ ํ•„๋“œ์—์„œ๋Š” ๋ฌด์‹œ + const active = document.activeElement; + if ( + active instanceof HTMLInputElement || + active instanceof HTMLTextAreaElement || + active?.getAttribute("contenteditable") === "true" + ) { + return; + } + + if (selectedComponent || groupState.selectedComponents.length > 0) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + const distance = e.shiftKey ? 10 : 1; // Shift ๋ˆ„๋ฅด๋ฉด 10px + const dirMap: Record = { + ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right", + }; + handleNudge(dirMap[e.key], distance); + return false; + } + } + + // === 10. ์ •๋ ฌ ๋‹จ์ถ•ํ‚ค (Alt + ํ‚ค) - ๋‹ค์ค‘ ์„ ํƒ ์‹œ === + if (e.altKey && !e.ctrlKey && !e.metaKey) { + const alignKey = e.key?.toLowerCase(); + const alignMap: Record = { + l: "left", r: "right", c: "centerX", + t: "top", b: "bottom", m: "centerY", + }; + + if (alignMap[alignKey] && groupState.selectedComponents.length >= 2) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + handleGroupAlign(alignMap[alignKey]); + return false; + } + + // ๊ท ๋“ฑ ๋ฐฐ๋ถ„ (Alt+H: ๊ฐ€๋กœ, Alt+V: ์„ธ๋กœ) + if (alignKey === "h" && groupState.selectedComponents.length >= 3) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + handleGroupDistribute("horizontal"); + return false; + } + if (alignKey === "v" && groupState.selectedComponents.length >= 3) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + handleGroupDistribute("vertical"); + return false; + } + + // ๋™์ผ ํฌ๊ธฐ ๋งž์ถ”๊ธฐ (Alt+W: ๋„ˆ๋น„, Alt+E: ๋†’์ด) + if (alignKey === "w" && groupState.selectedComponents.length >= 2) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + handleMatchSize("width"); + return false; + } + if (alignKey === "e" && groupState.selectedComponents.length >= 2) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + handleMatchSize("height"); + return false; + } + } + + // === 11. ๋ผ๋ฒจ ์ผ๊ด„ ํ† ๊ธ€ (Alt+Shift+L) === + if (e.altKey && e.shiftKey && e.key?.toLowerCase() === "l") { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + handleToggleAllLabels(); + return false; + } + + // === 12. ๋‹จ์ถ•ํ‚ค ๋„์›€๋ง (? ํ‚ค) === + if (e.key === "?" && !e.ctrlKey && !e.metaKey && !e.altKey) { + // ์ž…๋ ฅ ํ•„๋“œ์—์„œ๋Š” ๋ฌด์‹œ + const active = document.activeElement; + if ( + active instanceof HTMLInputElement || + active instanceof HTMLTextAreaElement || + active?.getAttribute("contenteditable") === "true" + ) { + return; + } + e.preventDefault(); + setShowShortcutsModal(true); + return false; + } }; // window ๋ ˆ๋ฒจ์—์„œ ์บก์ฒ˜ ๋‹จ๊ณ„์—์„œ ๊ฐ€์žฅ ๋จผ์ € ์ฒ˜๋ฆฌ @@ -4876,6 +5633,11 @@ export default function ScreenDesigner({ groupState.selectedComponents, layout, selectedScreen, + handleNudge, + handleGroupAlign, + handleGroupDistribute, + handleMatchSize, + handleToggleAllLabels, ]); // ํ”Œ๋กœ์šฐ ์œ„์ ฏ ๋†’์ด ์ž๋™ ์—…๋ฐ์ดํŠธ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ @@ -4931,6 +5693,36 @@ export default function ScreenDesigner({ }; }, [layout, selectedComponent]); + // ๐Ÿ†• ๋ ˆ์ด์–ด ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ - ๋ ˆ์ด์–ด ์ปจํ…์ŠคํŠธ์—์„œ ๋ ˆ์ด์–ด๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด layout์—๋„ ๋ฐ˜์˜ + // ์ฃผ์˜: layout.components๋Š” layerId ์†์„ฑ์œผ๋กœ ๋ ˆ์ด์–ด๋ฅผ ๊ตฌ๋ถ„ํ•˜๋ฏ€๋กœ, ์—ฌ๊ธฐ์„œ ๋ฎ์–ด์“ฐ์ง€ ์•Š์Œ + const handleLayersChange = useCallback((newLayers: LayerDefinition[]) => { + setLayout((prevLayout) => ({ + ...prevLayout, + layers: newLayers, + // components๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€ - layerId ์†์„ฑ์œผ๋กœ ๋ ˆ์ด์–ด ๊ตฌ๋ถ„ + // components: prevLayout.components (๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์œ ์ง€๋จ) + })); + }, []); + + // ๐Ÿ†• ํ™œ์„ฑ ๋ ˆ์ด์–ด ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ + const handleActiveLayerChange = useCallback((newActiveLayerId: string | null) => { + setActiveLayerIdLocal(newActiveLayerId); + }, []); + + // ๐Ÿ†• ์ดˆ๊ธฐ ๋ ˆ์ด์–ด ๊ณ„์‚ฐ - layout์—์„œ layers๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ, ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋ ˆ์ด์–ด ์ƒ์„ฑ + // ์ฃผ์˜: components๋Š” layout.components์— layerId ์†์„ฑ์œผ๋กœ ์ €์žฅ๋˜๋ฏ€๋กœ, layer.components๋Š” ๋น„์›Œ๋‘  + const initialLayers = useMemo(() => { + if (layout.layers && layout.layers.length > 0) { + // ๊ธฐ์กด ๋ ˆ์ด์–ด ๊ตฌ์กฐ ์‚ฌ์šฉ (layer.components๋Š” ๋ฌด์‹œํ•˜๊ณ  ๋นˆ ๋ฐฐ์—ด๋กœ ์„ค์ •) + return layout.layers.map(layer => ({ + ...layer, + components: [], // layout.components + layerId ๋ฐฉ์‹ ์‚ฌ์šฉ + })); + } + // layers๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋ ˆ์ด์–ด ์ƒ์„ฑ (components๋Š” ๋นˆ ๋ฐฐ์—ด) + return [createDefaultLayer()]; + }, [layout.layers]); + if (!selectedScreen) { return (
@@ -4945,9 +5737,17 @@ export default function ScreenDesigner({ ); } + // ๐Ÿ”ง ScreenDesigner ๋ Œ๋”๋ง ํ™•์ธ (๋””๋ฒ„๊ทธ ์™„๋ฃŒ - ์ฃผ์„ ์ฒ˜๋ฆฌ) + // console.log("๐Ÿ  ScreenDesigner ๋ Œ๋”!", Date.now()); + return ( - + +
{/* ์ƒ๋‹จ ์Šฌ๋ฆผ ํˆด๋ฐ” */} setShowMultilangSettingsModal(true)} isPanelOpen={panelStates.v2?.isOpen || false} onTogglePanel={() => togglePanel("v2")} + selectedCount={groupState.selectedComponents.length} + onAlign={handleGroupAlign} + onDistribute={handleGroupDistribute} + onMatchSize={handleMatchSize} + onToggleLabels={handleToggleAllLabels} + onShowShortcuts={() => setShowShortcutsModal(true)} /> {/* ๋ฉ”์ธ ์ปจํ…Œ์ด๋„ˆ (ํŒจ๋„๋“ค + ์บ”๋ฒ„์Šค) */}
{/* ํ†ตํ•ฉ ํŒจ๋„ - ์ขŒ์ธก ์‚ฌ์ด๋“œ๋ฐ” ์ œ๊ฑฐ ํ›„ ๋„ˆ๋น„ 300px๋กœ ํ™•์žฅ */} {panelStates.v2?.isOpen && ( -
-
+
+

ํŒจ๋„

- + ์ปดํฌ๋„ŒํŠธ + + ๋ ˆ์ด์–ด + ํŽธ์ง‘ @@ -5012,12 +5821,17 @@ export default function ScreenDesigner({ /> + {/* ๐Ÿ†• ๋ ˆ์ด์–ด ๊ด€๋ฆฌ ํƒญ */} + + + + {/* ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์‹œ์—๋„ V2PropertiesPanel ์‚ฌ์šฉ */} {selectedTabComponentInfo ? ( (() => { const tabComp = selectedTabComponentInfo.component; - + // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ComponentData ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ const tabComponentAsComponentData: ComponentData = { id: tabComp.id, @@ -5030,15 +5844,40 @@ export default function ScreenDesigner({ style: tabComp.style || {}, } as ComponentData; - // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ์šฉ ์†์„ฑ ์—…๋ฐ์ดํŠธ ํ•ธ๋“ค๋Ÿฌ + // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ์šฉ ์†์„ฑ ์—…๋ฐ์ดํŠธ ํ•ธ๋“ค๋Ÿฌ (์ค‘์ฒฉ ๊ตฌ์กฐ ์ง€์›) const updateTabComponentProperty = (componentId: string, path: string, value: any) => { - const { tabsComponentId, tabId } = selectedTabComponentInfo; - - setLayout((prevLayout) => { - const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); - if (!tabsComponent) return prevLayout; + const { tabsComponentId, tabId, parentSplitPanelId, parentPanelSide } = + selectedTabComponentInfo; - const currentConfig = (tabsComponent as any).componentConfig || {}; + console.log("๐Ÿ”ง updateTabComponentProperty ํ˜ธ์ถœ:", { + componentId, + path, + value, + parentSplitPanelId, + parentPanelSide, + }); + + // ๐Ÿ†• ์•ˆ์ „ํ•œ ๊นŠ์€ ๊ฒฝ๋กœ ์—…๋ฐ์ดํŠธ ํ—ฌํผ ํ•จ์ˆ˜ + const setNestedValue = (obj: any, pathStr: string, val: any): any => { + // ๊นŠ์€ ๋ณต์‚ฌ๋กœ ์‹œ์ž‘ + const result = JSON.parse(JSON.stringify(obj)); + const parts = pathStr.split("."); + let current = result; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (!current[part] || typeof current[part] !== "object") { + current[part] = {}; + } + current = current[part]; + } + current[parts[parts.length - 1]] = val; + return result; + }; + + // ํƒญ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ + const updateTabsComponent = (tabsComponent: any) => { + const currentConfig = JSON.parse(JSON.stringify(tabsComponent.componentConfig || {})); const tabs = currentConfig.tabs || []; const updatedTabs = tabs.map((tab: any) => { @@ -5047,63 +5886,102 @@ export default function ScreenDesigner({ ...tab, components: (tab.components || []).map((comp: any) => { if (comp.id !== componentId) return comp; - - // path๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ ์ค‘์ฒฉ ์†์„ฑ ์—…๋ฐ์ดํŠธ - const pathParts = path.split("."); - const newComp = { ...comp }; - let current: any = newComp; - - for (let i = 0; i < pathParts.length - 1; i++) { - const part = pathParts[i]; - if (!current[part]) { - current[part] = {}; - } else { - current[part] = { ...current[part] }; - } - current = current[part]; - } - current[pathParts[pathParts.length - 1]] = value; - - return newComp; + + // ๐Ÿ†• ์•ˆ์ „ํ•œ ๊นŠ์€ ๊ฒฝ๋กœ ์—…๋ฐ์ดํŠธ ์‚ฌ์šฉ + const updatedComp = setNestedValue(comp, path, value); + console.log("๐Ÿ”ง ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ ๊ฒฐ๊ณผ:", updatedComp); + return updatedComp; }), }; } return tab; }); - const updatedComponent = { + return { ...tabsComponent, componentConfig: { ...currentConfig, tabs: updatedTabs }, }; + }; - // ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด ์—…๋ฐ์ดํŠธ - const updatedComp = updatedTabs - .find((t: any) => t.id === tabId) - ?.components?.find((c: any) => c.id === componentId); - if (updatedComp) { - setSelectedTabComponentInfo((prev) => - prev ? { ...prev, component: updatedComp } : null - ); + setLayout((prevLayout) => { + let newLayout; + let updatedTabs; + + if (parentSplitPanelId && parentPanelSide) { + // ๐Ÿ†• ์ค‘์ฒฉ ๊ตฌ์กฐ: ๋ถ„ํ•  ํŒจ๋„ ์•ˆ์˜ ํƒญ ์—…๋ฐ์ดํŠธ + newLayout = { + ...prevLayout, + components: prevLayout.components.map((c) => { + if (c.id === parentSplitPanelId) { + const splitConfig = (c as any).componentConfig || {}; + const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; + const panelConfig = splitConfig[panelKey] || {}; + const panelComponents = panelConfig.components || []; + + const tabsComponent = panelComponents.find( + (pc: any) => pc.id === tabsComponentId, + ); + if (!tabsComponent) return c; + + const updatedTabsComponent = updateTabsComponent(tabsComponent); + updatedTabs = updatedTabsComponent.componentConfig.tabs; + + return { + ...c, + componentConfig: { + ...splitConfig, + [panelKey]: { + ...panelConfig, + components: panelComponents.map((pc: any) => + pc.id === tabsComponentId ? updatedTabsComponent : pc, + ), + }, + }, + }; + } + return c; + }), + }; + } else { + // ์ผ๋ฐ˜ ๊ตฌ์กฐ: ์ตœ์ƒ์œ„ ํƒญ ์—…๋ฐ์ดํŠธ + const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); + if (!tabsComponent) return prevLayout; + + const updatedTabsComponent = updateTabsComponent(tabsComponent); + updatedTabs = updatedTabsComponent.componentConfig.tabs; + + newLayout = { + ...prevLayout, + components: prevLayout.components.map((c) => + c.id === tabsComponentId ? updatedTabsComponent : c, + ), + }; } - return { - ...prevLayout, - components: prevLayout.components.map((c) => - c.id === tabsComponentId ? updatedComponent : c - ), - }; + // ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด ์—…๋ฐ์ดํŠธ + if (updatedTabs) { + const updatedComp = updatedTabs + .find((t: any) => t.id === tabId) + ?.components?.find((c: any) => c.id === componentId); + if (updatedComp) { + setSelectedTabComponentInfo((prev) => + prev ? { ...prev, component: updatedComp } : null, + ); + } + } + + return newLayout; }); }; - // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ + // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ (์ค‘์ฒฉ ๊ตฌ์กฐ ์ง€์›) const deleteTabComponent = (componentId: string) => { - const { tabsComponentId, tabId } = selectedTabComponentInfo; - - setLayout((prevLayout) => { - const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); - if (!tabsComponent) return prevLayout; + const { tabsComponentId, tabId, parentSplitPanelId, parentPanelSide } = + selectedTabComponentInfo; - const currentConfig = (tabsComponent as any).componentConfig || {}; + // ํƒญ ์ปดํฌ๋„ŒํŠธ์—์„œ ํŠน์ • ์ปดํฌ๋„ŒํŠธ ์‚ญ์ œ + const updateTabsComponentForDelete = (tabsComponent: any) => { + const currentConfig = tabsComponent.componentConfig || {}; const tabs = currentConfig.tabs || []; const updatedTabs = tabs.map((tab: any) => { @@ -5116,28 +5994,75 @@ export default function ScreenDesigner({ return tab; }); - const updatedComponent = { + return { ...tabsComponent, componentConfig: { ...currentConfig, tabs: updatedTabs }, }; + }; + + setLayout((prevLayout) => { + let newLayout; + + if (parentSplitPanelId && parentPanelSide) { + // ๐Ÿ†• ์ค‘์ฒฉ ๊ตฌ์กฐ: ๋ถ„ํ•  ํŒจ๋„ ์•ˆ์˜ ํƒญ์—์„œ ์‚ญ์ œ + newLayout = { + ...prevLayout, + components: prevLayout.components.map((c) => { + if (c.id === parentSplitPanelId) { + const splitConfig = (c as any).componentConfig || {}; + const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; + const panelConfig = splitConfig[panelKey] || {}; + const panelComponents = panelConfig.components || []; + + const tabsComponent = panelComponents.find( + (pc: any) => pc.id === tabsComponentId, + ); + if (!tabsComponent) return c; + + const updatedTabsComponent = updateTabsComponentForDelete(tabsComponent); + + return { + ...c, + componentConfig: { + ...splitConfig, + [panelKey]: { + ...panelConfig, + components: panelComponents.map((pc: any) => + pc.id === tabsComponentId ? updatedTabsComponent : pc, + ), + }, + }, + }; + } + return c; + }), + }; + } else { + // ์ผ๋ฐ˜ ๊ตฌ์กฐ: ์ตœ์ƒ์œ„ ํƒญ์—์„œ ์‚ญ์ œ + const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); + if (!tabsComponent) return prevLayout; + + const updatedTabsComponent = updateTabsComponentForDelete(tabsComponent); + + newLayout = { + ...prevLayout, + components: prevLayout.components.map((c) => + c.id === tabsComponentId ? updatedTabsComponent : c, + ), + }; + } setSelectedTabComponentInfo(null); - - return { - ...prevLayout, - components: prevLayout.components.map((c) => - c.id === tabsComponentId ? updatedComponent : c - ), - }; + return newLayout; }); }; return (
- ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ + ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ
); })() + ) : selectedPanelComponentInfo ? ( + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์‹œ V2PropertiesPanel ์‚ฌ์šฉ + (() => { + const panelComp = selectedPanelComponentInfo.component; + + // ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ComponentData ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ + const panelComponentAsComponentData: ComponentData = { + id: panelComp.id, + type: "component", + componentType: panelComp.componentType, + label: panelComp.label, + position: panelComp.position || { x: 0, y: 0 }, + size: panelComp.size || { width: 200, height: 100 }, + componentConfig: panelComp.componentConfig || {}, + style: panelComp.style || {}, + } as ComponentData; + + // ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ์šฉ ์†์„ฑ ์—…๋ฐ์ดํŠธ ํ•ธ๋“ค๋Ÿฌ + const updatePanelComponentProperty = (componentId: string, path: string, value: any) => { + const { splitPanelId, panelSide } = selectedPanelComponentInfo; + const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel"; + + console.log("๐Ÿ”ง updatePanelComponentProperty ํ˜ธ์ถœ:", { + componentId, + path, + value, + splitPanelId, + panelSide, + }); + + // ๐Ÿ†• ์•ˆ์ „ํ•œ ๊นŠ์€ ๊ฒฝ๋กœ ์—…๋ฐ์ดํŠธ ํ—ฌํผ ํ•จ์ˆ˜ + const setNestedValue = (obj: any, pathStr: string, val: any): any => { + const result = JSON.parse(JSON.stringify(obj)); + const parts = pathStr.split("."); + let current = result; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (!current[part] || typeof current[part] !== "object") { + current[part] = {}; + } + current = current[part]; + } + current[parts[parts.length - 1]] = val; + return result; + }; + + setLayout((prevLayout) => { + const splitPanelComponent = prevLayout.components.find((c) => c.id === splitPanelId); + if (!splitPanelComponent) return prevLayout; + + const currentConfig = (splitPanelComponent as any).componentConfig || {}; + const panelConfig = currentConfig[panelKey] || {}; + const components = panelConfig.components || []; + + // ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ ์ฐพ๊ธฐ + const targetCompIndex = components.findIndex((c: any) => c.id === componentId); + if (targetCompIndex === -1) return prevLayout; + + // ๐Ÿ†• ์•ˆ์ „ํ•œ ๊นŠ์€ ๊ฒฝ๋กœ ์—…๋ฐ์ดํŠธ ์‚ฌ์šฉ + const targetComp = components[targetCompIndex]; + const updatedComp = + path === "style" + ? { ...targetComp, style: value } + : setNestedValue(targetComp, path, value); + + console.log("๐Ÿ”ง ๋ถ„ํ•  ํŒจ๋„ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ ๊ฒฐ๊ณผ:", updatedComp); + + const updatedComponents = [ + ...components.slice(0, targetCompIndex), + updatedComp, + ...components.slice(targetCompIndex + 1), + ]; + + const updatedComponent = { + ...splitPanelComponent, + componentConfig: { + ...currentConfig, + [panelKey]: { + ...panelConfig, + components: updatedComponents, + }, + }, + }; + + // selectedPanelComponentInfo ์—…๋ฐ์ดํŠธ + setSelectedPanelComponentInfo((prev) => + prev ? { ...prev, component: updatedComp } : null, + ); + + return { + ...prevLayout, + components: prevLayout.components.map((c) => + c.id === splitPanelId ? updatedComponent : c, + ), + }; + }); + }; + + // ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ + const deletePanelComponent = (componentId: string) => { + const { splitPanelId, panelSide } = selectedPanelComponentInfo; + const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel"; + + setLayout((prevLayout) => { + const splitPanelComponent = prevLayout.components.find((c) => c.id === splitPanelId); + if (!splitPanelComponent) return prevLayout; + + const currentConfig = (splitPanelComponent as any).componentConfig || {}; + const panelConfig = currentConfig[panelKey] || {}; + const components = panelConfig.components || []; + + const updatedComponents = components.filter((c: any) => c.id !== componentId); + + const updatedComponent = { + ...splitPanelComponent, + componentConfig: { + ...currentConfig, + [panelKey]: { + ...panelConfig, + components: updatedComponents, + }, + }, + }; + + setSelectedPanelComponentInfo(null); + + return { + ...prevLayout, + components: prevLayout.components.map((c) => + c.id === splitPanelId ? updatedComponent : c, + ), + }; + }); + }; + + return ( +
+
+ + ๋ถ„ํ•  ํŒจ๋„ ({selectedPanelComponentInfo.panelSide === "left" ? "์ขŒ์ธก" : "์šฐ์ธก"}) + ์ปดํฌ๋„ŒํŠธ + + +
+
+ 0 ? tables[0] : undefined} + currentTableName={selectedScreen?.tableName} + currentScreenCompanyCode={selectedScreen?.companyCode} + onStyleChange={(style) => { + updatePanelComponentProperty(panelComp.id, "style", style); + }} + allComponents={layout.components} + menuObjid={menuObjid} + /> +
+
+ ); + })() ) : ( )} - {/* ๋ฉ”์ธ ์บ”๋ฒ„์Šค ์˜์—ญ (์Šคํฌ๋กค ๊ฐ€๋Šฅํ•œ ์ปจํ…Œ์ด๋„ˆ) - ์ขŒ์šฐ ์ตœ์†Œํ™”, ์œ„์•„๋ž˜ ๋„‰๋„‰ํ•œ ์—ฌ์œ  */} -
+ {/* ๋ฉ”์ธ ์บ”๋ฒ„์Šค ์˜์—ญ (์Šคํฌ๋กค ๊ฐ€๋Šฅํ•œ ์ปจํ…Œ์ด๋„ˆ) - GPU ๊ฐ€์† ์Šคํฌ๋กค ์ ์šฉ */} +
{/* Pan ๋ชจ๋“œ ์•ˆ๋‚ด - ์ œ๊ฑฐ๋จ */} {/* ์คŒ ๋ ˆ๋ฒจ ํ‘œ์‹œ */}
@@ -5298,12 +6396,13 @@ export default function ScreenDesigner({
); })()} - {/* ๐Ÿ”ฅ ์คŒ ์ ์šฉ ์‹œ ์Šคํฌ๋กค ์˜์—ญ ํ™•๋ณด๋ฅผ ์œ„ํ•œ ๋ž˜ํผ - ์ค‘์•™ ์ •๋ ฌ๋กœ ๋ณ€๊ฒฝ */} + {/* ์คŒ ์ ์šฉ ์‹œ ์Šคํฌ๋กค ์˜์—ญ ํ™•๋ณด๋ฅผ ์œ„ํ•œ ๋ž˜ํผ - ์ค‘์•™ ์ •๋ ฌ + contain ์ตœ์ ํ™” */}
{/* ์‹ค์ œ ์ž‘์—… ์บ”๋ฒ„์Šค (ํ•ด์ƒ๋„ ํฌ๊ธฐ) - ๊ณ ์ • ํฌ๊ธฐ + ์คŒ ์ ์šฉ */} @@ -5316,8 +6415,10 @@ export default function ScreenDesigner({ maxWidth: `${screenResolution.width}px`, minHeight: `${screenResolution.height}px`, flexShrink: 0, - transform: `scale(${zoomLevel})`, + transform: `scale3d(${zoomLevel}, ${zoomLevel}, 1)`, transformOrigin: "top center", // ์ค‘์•™ ๊ธฐ์ค€์œผ๋กœ ์Šค์ผ€์ผ + willChange: "transform", // GPU ๊ฐ€์† ๋ ˆ์ด์–ด ์ƒ์„ฑ + backfaceVisibility: "hidden" as const, // ๋ฆฌํŽ˜์ธํŠธ ์ตœ์ ํ™” }} >
{ // ๐Ÿ†• ํ”Œ๋กœ์šฐ ๋ฒ„ํŠผ ๊ทธ๋ฃน ๊ฐ์ง€ ๋ฐ ์ฒ˜๋ฆฌ - const topLevelComponents = layout.components.filter((component) => !component.parentId); + // visibleComponents๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ™œ์„ฑ ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ๋งŒ ํ‘œ์‹œ + const topLevelComponents = visibleComponents.filter((component) => !component.parentId); // auto-compact ๋ชจ๋“œ์˜ ๋ฒ„ํŠผ๋“ค์„ ๊ทธ๋ฃน๋ณ„๋กœ ๋ฌถ๊ธฐ const buttonGroups: Record = {}; @@ -5397,6 +6499,9 @@ export default function ScreenDesigner({ // ๊ทธ๋ฃน์— ์†ํ•˜์ง€ ์•Š์€ ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ๋“ค const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id)); + // ๐Ÿ”ง ๋ Œ๋”๋ง ํ™•์ธ ๋กœ๊ทธ (๋””๋ฒ„๊ทธ ์™„๋ฃŒ - ์ฃผ์„ ์ฒ˜๋ฆฌ) + // console.log("๐Ÿ”„ ScreenDesigner ๋ Œ๋”๋ง:", { componentsCount: regularComponents.length, forceRenderTrigger, timestamp: Date.now() }); + return ( <> {/* ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ๋“ค */} @@ -5462,11 +6567,26 @@ export default function ScreenDesigner({ const globalFiles = globalFileState[component.id] || []; const componentFiles = (component as any).uploadedFiles || []; const fileStateKey = `${globalFiles.length}-${JSON.stringify(globalFiles.map((f: any) => f.objid) || [])}-${componentFiles.length}`; + // ๐Ÿ†• style ๋ณ€๊ฒฝ ์‹œ ๋ฆฌ๋ Œ๋”๋ง์„ ์œ„ํ•œ key ์ถ”๊ฐ€ + const styleKey = + component.style?.labelDisplay !== undefined + ? `label-${component.style.labelDisplay}` + : ""; + const fullKey = `${component.id}-${fileStateKey}-${styleKey}-${(component as any).lastFileUpdate || 0}-${forceRenderTrigger}`; + + // ๐Ÿ”ง v2-input ๊ณ„์—ด ์ปดํฌ๋„ŒํŠธ key ๋ณ€๊ฒฝ ๋กœ๊ทธ (๋””๋ฒ„๊ทธ ์™„๋ฃŒ - ์ฃผ์„ ์ฒ˜๋ฆฌ) + // if (component.id.includes("v2-") || component.widgetType?.includes("v2-")) { console.log("๐Ÿ”‘ RealtimePreview key:", { id: component.id, styleKey, labelDisplay: component.style?.labelDisplay, forceRenderTrigger, fullKey }); } + + // ๐Ÿ†• labelDisplay ๋ณ€๊ฒฝ ์‹œ ์ƒˆ ๊ฐ์ฒด๋กœ ๊ฐ•์ œ ๋ณ€๊ฒฝ ๊ฐ์ง€ + const componentWithLabel = { + ...displayComponent, + _labelDisplayKey: component.style?.labelDisplay, + }; return ( startComponentDrag(component, e)} onDragEnd={endDrag} selectedScreen={selectedScreen} + tableName={selectedScreen?.tableName} // ๐Ÿ†• ๋””์ž์ธ ๋ชจ๋“œ์—์„œ๋„ ์˜ต์…˜ ๋กœ๋”ฉ์„ ์œ„ํ•ด ์ „๋‹ฌ menuObjid={menuObjid} // ๐Ÿ†• ๋ฉ”๋‰ด OBJID ์ „๋‹ฌ // onZoneComponentDrop ์ œ๊ฑฐ onZoneClick={handleZoneClick} @@ -5514,7 +6635,7 @@ export default function ScreenDesigner({ // ๐Ÿ†• ์ปดํฌ๋„ŒํŠธ ์ „์ฒด ์—…๋ฐ์ดํŠธ ํ•ธ๋“ค๋Ÿฌ (ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์œ„์น˜ ์กฐ์ • ๋“ฑ) onUpdateComponent={(updatedComponent) => { const updatedComponents = layout.components.map((comp) => - comp.id === updatedComponent.id ? updatedComponent : comp + comp.id === updatedComponent.id ? updatedComponent : comp, ); const newLayout = { @@ -5529,9 +6650,7 @@ export default function ScreenDesigner({ onResize={(componentId, newSize) => { setLayout((prevLayout) => { const updatedComponents = prevLayout.components.map((comp) => - comp.id === componentId - ? { ...comp, size: newSize } - : comp + comp.id === componentId ? { ...comp, size: newSize } : comp, ); const newLayout = { @@ -5545,7 +6664,7 @@ export default function ScreenDesigner({ }); }} // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ - onSelectTabComponent={(tabId, compId, comp) => + onSelectTabComponent={(tabId, compId, comp) => handleSelectTabComponent(component.id, tabId, compId, comp) } selectedTabComponentId={ @@ -5553,6 +6672,15 @@ export default function ScreenDesigner({ ? selectedTabComponentInfo.componentId : undefined } + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ + onSelectPanelComponent={(panelSide, compId, comp) => + handleSelectPanelComponent(component.id, panelSide, compId, comp) + } + selectedPanelComponentId={ + selectedPanelComponentInfo?.splitPanelId === component.id + ? selectedPanelComponentInfo.componentId + : undefined + } > {/* ์ปจํ…Œ์ด๋„ˆ, ๊ทธ๋ฃน, ์˜์—ญ, ์ปดํฌ๋„ŒํŠธ์˜ ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋“ค ๋ Œ๋”๋ง (๋ ˆ์ด์•„์›ƒ์€ ๋…๋ฆฝ์ ์œผ๋กœ ๋ Œ๋”๋ง) */} {(component.type === "group" || @@ -5636,6 +6764,7 @@ export default function ScreenDesigner({ onDragStart={(e) => startComponentDrag(child, e)} onDragEnd={endDrag} selectedScreen={selectedScreen} + tableName={selectedScreen?.tableName} // ๐Ÿ†• ๋””์ž์ธ ๋ชจ๋“œ์—์„œ๋„ ์˜ต์…˜ ๋กœ๋”ฉ์„ ์œ„ํ•ด ์ „๋‹ฌ // onZoneComponentDrop ์ œ๊ฑฐ onZoneClick={handleZoneClick} // ์„ค์ • ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ (์ž์‹ ์ปดํฌ๋„ŒํŠธ์šฉ) @@ -5647,9 +6776,7 @@ export default function ScreenDesigner({ onResize={(componentId, newSize) => { setLayout((prevLayout) => { const updatedComponents = prevLayout.components.map((comp) => - comp.id === componentId - ? { ...comp, size: newSize } - : comp + comp.id === componentId ? { ...comp, size: newSize } : comp, ); const newLayout = { @@ -5858,6 +6985,7 @@ export default function ScreenDesigner({ component={relativeButton} isDesignMode={true} formData={{}} + tableName={selectedScreen?.tableName} onDataflowComplete={() => {}} />
@@ -5990,8 +7118,14 @@ export default function ScreenDesigner({ } }} /> + {/* ๋‹จ์ถ•ํ‚ค ๋„์›€๋ง ๋ชจ๋‹ฌ */} + setShowShortcutsModal(false)} + />
+ ); } diff --git a/frontend/components/screen/ScreenGroupTreeView.tsx b/frontend/components/screen/ScreenGroupTreeView.tsx index ef82467c..1aa47f0d 100644 --- a/frontend/components/screen/ScreenGroupTreeView.tsx +++ b/frontend/components/screen/ScreenGroupTreeView.tsx @@ -18,6 +18,7 @@ import { Loader2, RefreshCw, Building2, + AlertTriangle, } from "lucide-react"; import { ScreenDefinition } from "@/types/screen"; import { @@ -175,7 +176,7 @@ export function ScreenGroupTreeView({ const [syncProgress, setSyncProgress] = useState<{ message: string; detail?: string } | null>(null); // ํšŒ์‚ฌ ์„ ํƒ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž์šฉ) - const { user, switchCompany } = useAuth(); + const { user } = useAuth(); const [companies, setCompanies] = useState([]); const [selectedCompanyCode, setSelectedCompanyCode] = useState(""); const [isSyncCompanySelectOpen, setIsSyncCompanySelectOpen] = useState(false); @@ -301,23 +302,18 @@ export function ScreenGroupTreeView({ } }; - // ํšŒ์‚ฌ ์„ ํƒ ์‹œ ํšŒ์‚ฌ ์ „ํ™˜ + ์ƒํƒœ ์กฐํšŒ + // ํšŒ์‚ฌ ์„ ํƒ ์‹œ ์ƒํƒœ๋งŒ ๋ณ€๊ฒฝ (ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ ์—†์ด) const handleCompanySelect = async (companyCode: string) => { setSelectedCompanyCode(companyCode); setIsSyncCompanySelectOpen(false); setSyncStatus(null); if (companyCode) { - // ๐Ÿ”ง ํšŒ์‚ฌ ์ „ํ™˜ (JWT ํ† ํฐ ๋ณ€๊ฒฝ) - ๋ชจ๋“  API๊ฐ€ ์„ ํƒํ•œ ํšŒ์‚ฌ๋กœ ๋™์ž‘ํ•˜๋„๋ก - const switchResult = await switchCompany(companyCode); - if (!switchResult.success) { - toast.error(switchResult.message || "ํšŒ์‚ฌ ์ „ํ™˜ ์‹คํŒจ"); - return; + // ๋™๊ธฐํ™” ์ƒํƒœ ์กฐํšŒ (์„ ํƒํ•œ ํšŒ์‚ฌ ์ฝ”๋“œ๋กœ) + const response = await getMenuScreenSyncStatus(companyCode); + if (response.success && response.data) { + setSyncStatus(response.data); } - toast.success(`${companyCode} ํšŒ์‚ฌ๋กœ ์ „ํ™˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•ฉ๋‹ˆ๋‹ค.`); - - // ๐Ÿ”ง ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ์œผ๋กœ ์ƒˆ JWT ํ™•์‹คํ•˜๊ฒŒ ์ ์šฉ - window.location.reload(); } }; @@ -447,17 +443,24 @@ export function ScreenGroupTreeView({ }; // ๊ทธ๋ฃน๊ณผ ๋ชจ๋“  ํ•˜์œ„ ๊ทธ๋ฃน์˜ ํ™”๋ฉด์„ ์žฌ๊ท€์ ์œผ๋กœ ์ˆ˜์ง‘ - const getAllScreensInGroupRecursively = (groupId: number): ScreenDefinition[] => { + // ๊ฐ™์€ ํšŒ์‚ฌ์˜ ๊ทธ๋ฃน๋งŒ ํ•„ํ„ฐ๋งํ•˜์—ฌ ๋‹ค๋ฅธ ํšŒ์‚ฌ ํ™”๋ฉด์ด ์ž˜๋ชป ์ˆ˜์ง‘๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ + const getAllScreensInGroupRecursively = (groupId: number, targetCompanyCode?: string): ScreenDefinition[] => { const result: ScreenDefinition[] = []; + // ๋ถ€๋ชจ ๊ทธ๋ฃน์˜ company_code ํ™•์ธ + const parentGroup = groups.find(g => g.id === groupId); + const companyCode = targetCompanyCode || parentGroup?.company_code; // ํ˜„์žฌ ๊ทธ๋ฃน์˜ ํ™”๋ฉด๋“ค const currentGroupScreens = getScreensInGroup(groupId); result.push(...currentGroupScreens); - // ํ•˜์œ„ ๊ทธ๋ฃน๋“ค ์ฐพ๊ธฐ - const childGroups = groups.filter((g) => (g as any).parent_group_id === groupId); + // ๊ฐ™์€ ํšŒ์‚ฌ + ๊ฐ™์€ ๋ถ€๋ชจ๋ฅผ ๊ฐ€์ง„ ํ•˜์œ„ ๊ทธ๋ฃน๋“ค ์ฐพ๊ธฐ + const childGroups = groups.filter((g) => + (g as any).parent_group_id === groupId && + (!companyCode || g.company_code === companyCode) + ); for (const childGroup of childGroups) { - const childScreens = getAllScreensInGroupRecursively(childGroup.id); + const childScreens = getAllScreensInGroupRecursively(childGroup.id, companyCode); result.push(...childScreens); } @@ -465,13 +468,22 @@ export function ScreenGroupTreeView({ }; // ๋ชจ๋“  ํ•˜์œ„ ๊ทธ๋ฃน ID๋ฅผ ์žฌ๊ท€์ ์œผ๋กœ ์ˆ˜์ง‘ (์‚ญ์ œ ์ˆœ์„œ: ์ž์‹ โ†’ ๋ถ€๋ชจ) - const getAllChildGroupIds = (groupId: number): number[] => { + // ๊ฐ™์€ ํšŒ์‚ฌ์˜ ๊ทธ๋ฃน๋งŒ ํ•„ํ„ฐ๋งํ•˜์—ฌ ๋‹ค๋ฅธ ํšŒ์‚ฌ ๊ทธ๋ฃน์ด ์ž˜๋ชป ์‚ญ์ œ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ + const getAllChildGroupIds = (groupId: number, targetCompanyCode?: string): number[] => { const result: number[] = []; - const childGroups = groups.filter((g) => (g as any).parent_group_id === groupId); + // ๋ถ€๋ชจ ๊ทธ๋ฃน์˜ company_code ํ™•์ธ + const parentGroup = groups.find(g => g.id === groupId); + const companyCode = targetCompanyCode || parentGroup?.company_code; + + // ๊ฐ™์€ ํšŒ์‚ฌ + ๊ฐ™์€ ๋ถ€๋ชจ๋ฅผ ๊ฐ€์ง„ ๊ทธ๋ฃน๋งŒ ํ•„ํ„ฐ๋ง + const childGroups = groups.filter((g) => + (g as any).parent_group_id === groupId && + (!companyCode || g.company_code === companyCode) + ); for (const childGroup of childGroups) { // ์ž์‹์˜ ์ž์‹๋“ค์„ ๋จผ์ € ์ˆ˜์ง‘ (๊นŠ์€ ๊ณณ๋ถ€ํ„ฐ) - const grandChildIds = getAllChildGroupIds(childGroup.id); + const grandChildIds = getAllChildGroupIds(childGroup.id, companyCode); result.push(...grandChildIds); result.push(childGroup.id); } @@ -483,10 +495,35 @@ export function ScreenGroupTreeView({ const confirmDeleteGroup = async () => { if (!deletingGroup) return; + // ๐Ÿ” ๋””๋ฒ„๊น…: ์‚ญ์ œ ๋Œ€์ƒ ๊ทธ๋ฃน ์ •๋ณด + console.log("========== ๊ทธ๋ฃน ์‚ญ์ œ ๋””๋ฒ„๊น… =========="); + console.log("์‚ญ์ œ ๋Œ€์ƒ ๊ทธ๋ฃน:", { + id: deletingGroup.id, + name: deletingGroup.group_name, + company_code: deletingGroup.company_code, + parent_group_id: (deletingGroup as any).parent_group_id + }); + + // ๐Ÿ” ๋””๋ฒ„๊น…: ์ „์ฒด groups ๋ฐฐ์—ด์—์„œ ๊ฐ™์€ ํšŒ์‚ฌ ๊ทธ๋ฃน ์ถœ๋ ฅ + const sameCompanyGroups = groups.filter(g => g.company_code === deletingGroup.company_code); + console.log("๊ฐ™์€ ํšŒ์‚ฌ ๊ทธ๋ฃน๋“ค:", sameCompanyGroups.map(g => ({ + id: g.id, + name: g.group_name, + parent_group_id: (g as any).parent_group_id + }))); + // ์‚ญ์ œ ์ „ ํ†ต๊ณ„ ์ˆ˜์ง‘ (ํ™”๋ฉด ์ˆ˜๋Š” ์‚ญ์ œ ์ „์— ๊ณ„์‚ฐ) const totalScreensToDelete = getAllScreensInGroupRecursively(deletingGroup.id).length; const childGroupIds = getAllChildGroupIds(deletingGroup.id); + // ๐Ÿ” ๋””๋ฒ„๊น…: ์ˆ˜์ง‘๋œ ํ•˜์œ„ ๊ทธ๋ฃน ID๋“ค + console.log("์ˆ˜์ง‘๋œ ํ•˜์œ„ ๊ทธ๋ฃน ID๋“ค:", childGroupIds); + console.log("ํ•˜์œ„ ๊ทธ๋ฃน ์ƒ์„ธ:", childGroupIds.map(id => { + const g = groups.find(grp => grp.id === id); + return g ? { id: g.id, name: g.group_name, parent_group_id: (g as any).parent_group_id } : { id, name: "NOT_FOUND" }; + })); + console.log("=========================================="); + // ์ด ์ž‘์—… ์ˆ˜ ๊ณ„์‚ฐ (ํ™”๋ฉด + ํ•˜์œ„ ๊ทธ๋ฃน + ํ˜„์žฌ ๊ทธ๋ฃน) const totalSteps = totalScreensToDelete + childGroupIds.length + 1; let currentStep = 0; @@ -511,7 +548,7 @@ export function ScreenGroupTreeView({ total: totalSteps, message: `ํ™”๋ฉด ์‚ญ์ œ ์ค‘: ${screen.screenName}` }); - await screenApi.deleteScreen(screen.screenId, "๊ทธ๋ฃน ์‚ญ์ œ์™€ ํ•จ๊ป˜ ์‚ญ์ œ"); + await screenApi.deleteScreen(screen.screenId, "๊ทธ๋ฃน ์‚ญ์ œ์™€ ํ•จ๊ป˜ ์‚ญ์ œ", true); // force: true๋กœ ์˜์กด์„ฑ ๋ฌด์‹œ } console.log(`โœ… ๊ทธ๋ฃน ๋ฐ ํ•˜์œ„ ๊ทธ๋ฃน ๋‚ด ํ™”๋ฉด ${allScreens.length}๊ฐœ ์‚ญ์ œ ์™„๋ฃŒ`); } @@ -1427,16 +1464,26 @@ export function ScreenGroupTreeView({ {/* ๊ทธ๋ฃน ์‚ญ์ œ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ */} - + - ๊ทธ๋ฃน ์‚ญ์ œ - - "{deletingGroup?.group_name}" ๊ทธ๋ฃน์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? -
- {deleteScreensWithGroup - ? ๊ทธ๋ฃน์— ์†ํ•œ ํ™”๋ฉด๋“ค๋„ ํ•จ๊ป˜ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค. - : "๊ทธ๋ฃน์— ์†ํ•œ ํ™”๋ฉด๋“ค์€ ๋ฏธ๋ถ„๋ฅ˜๋กœ ์ด๋™๋ฉ๋‹ˆ๋‹ค." - } + + + ๊ทธ๋ฃน ์‚ญ์ œ ๊ฒฝ๊ณ  + + +
+
+

+ "{deletingGroup?.group_name}" ๊ทธ๋ฃน์„ ์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? +

+

+ {deleteScreensWithGroup + ? "โš ๏ธ ๊ทธ๋ฃน์— ์†ํ•œ ๋ชจ๋“  ํ™”๋ฉด, ํ”Œ๋กœ์šฐ, ๊ด€๋ จ ๋ฐ์ดํ„ฐ๊ฐ€ ์˜๊ตฌ์ ์œผ๋กœ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค. ์ด ์ž‘์—…์€ ๋˜๋Œ๋ฆด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." + : "๊ทธ๋ฃน์— ์†ํ•œ ํ™”๋ฉด๋“ค์€ ๋ฏธ๋ถ„๋ฅ˜๋กœ ์ด๋™๋ฉ๋‹ˆ๋‹ค." + } +

+
+
@@ -1534,11 +1581,21 @@ export function ScreenGroupTreeView({ )} - ํ™”๋ฉด ์‚ญ์ œ - - "{deletingScreen?.screenName}" ํ™”๋ฉด์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? -
- ์‚ญ์ œ๋œ ํ™”๋ฉด์€ ํœด์ง€ํ†ต์œผ๋กœ ์ด๋™๋ฉ๋‹ˆ๋‹ค. + + + ํ™”๋ฉด ์‚ญ์ œ ๊ฒฝ๊ณ  + + +
+
+

+ "{deletingScreen?.screenName}" ํ™”๋ฉด์„ ์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? +

+

+ โš ๏ธ ํ™”๋ฉด๊ณผ ์—ฐ๊ฒฐ๋œ ํ”Œ๋กœ์šฐ, ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ชจ๋‘ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค. ์‚ญ์ œ๋œ ํ™”๋ฉด์€ ํœด์ง€ํ†ต์œผ๋กœ ์ด๋™๋ฉ๋‹ˆ๋‹ค. +

+
+
diff --git a/frontend/components/screen/ScreenNode.tsx b/frontend/components/screen/ScreenNode.tsx index e49bf6d8..bcaaf054 100644 --- a/frontend/components/screen/ScreenNode.tsx +++ b/frontend/components/screen/ScreenNode.tsx @@ -58,6 +58,7 @@ export interface TableNodeData { label: string; subLabel?: string; isMain?: boolean; + isFilterTable?: boolean; // ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ์˜ ๋””ํ…Œ์ผ ํ…Œ์ด๋ธ”์ธ์ง€ (๋ณด๋ผ์ƒ‰ ํ…Œ๋‘๋ฆฌ) isFocused?: boolean; // ํฌ์ปค์Šค๋œ ํ…Œ์ด๋ธ”์ธ์ง€ isFaded?: boolean; // ํ‘๋ฐฑ ์ฒ˜๋ฆฌํ• ์ง€ columns?: Array<{ @@ -448,7 +449,7 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType: // ========== ํ…Œ์ด๋ธ” ๋…ธ๋“œ (ํ•˜๋‹จ) - ์ปฌ๋Ÿผ ๋ชฉ๋ก ํ‘œ์‹œ (์ปดํŒฉํŠธ) ========== export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { - const { label, subLabel, isMain, isFocused, isFaded, columns, highlightedColumns, joinColumns, joinColumnRefs, filterColumns, fieldMappings, referencedBy, saveInfos } = data; + const { label, subLabel, isMain, isFilterTable, isFocused, isFaded, columns, highlightedColumns, joinColumns, joinColumnRefs, filterColumns, fieldMappings, referencedBy, saveInfos } = data; // ๊ฐ•์กฐํ•  ์ปฌ๋Ÿผ ์„ธํŠธ (์˜๋ฌธ ์ปฌ๋Ÿผ๋ช… ๊ธฐ์ค€) const highlightSet = new Set(highlightedColumns || []); @@ -574,16 +575,19 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { return (
{ + const handleScreenListRefresh = () => { + // refreshKey ์ฆ๊ฐ€๋กœ ๋ฐ์ดํ„ฐ ์žฌ๋กœ๋“œ ํŠธ๋ฆฌ๊ฑฐ + setRefreshKey(prev => prev + 1); + }; + + window.addEventListener("screen-list-refresh", handleScreenListRefresh); + return () => { + window.removeEventListener("screen-list-refresh", handleScreenListRefresh); + }; + }, []); + // ๊ทธ๋ฃน ๋˜๋Š” ํ™”๋ฉด์ด ๋ณ€๊ฒฝ๋  ๋•Œ ํฌ์ปค์Šค ์ดˆ๊ธฐํ™” useEffect(() => { setFocusedScreenId(null); @@ -171,6 +184,10 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId // ํ™”๋ฉด๋ณ„ ์‚ฌ์šฉ ์ปฌ๋Ÿผ ๋งคํ•‘ (ํ™”๋ฉด ID -> ํ…Œ์ด๋ธ”๋ช… -> ์‚ฌ์šฉ ์ปฌ๋Ÿผ๋“ค) const [screenUsedColumnsMap, setScreenUsedColumnsMap] = useState>>({}); + + // ์ „์—ญ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ชฉ๋ก (์šฐ์„ ์ˆœ์œ„: ๋ฉ”์ธ > ์„œ๋ธŒ) + // ์ด ๋ชฉ๋ก์— ์žˆ๋Š” ํ…Œ์ด๋ธ”์€ ์„œ๋ธŒ ํ…Œ์ด๋ธ”๋กœ ๋ถ„๋ฅ˜๋˜์ง€ ์•Š์Œ + const [globalMainTables, setGlobalMainTables] = useState>(new Set()); // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด ๋กœ๋“œ const loadTableColumns = useCallback( @@ -267,24 +284,26 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId const flows = flowsRes.success ? flowsRes.data || [] : []; const relations = relationsRes.success ? relationsRes.data || [] : []; - // ๋ฐ์ดํ„ฐ ํ๋ฆ„์—์„œ ์—ฐ๊ฒฐ๋œ ํ™”๋ฉด๋“ค ์ถ”๊ฐ€ - flows.forEach((flow: any) => { - if (flow.source_screen_id === screen.screenId && flow.target_screen_id) { - const exists = screenList.some((s) => s.screenId === flow.target_screen_id); - if (!exists) { - screenList.push({ - screenId: flow.target_screen_id, - screenName: flow.target_screen_name || `ํ™”๋ฉด ${flow.target_screen_id}`, - screenCode: "", - tableName: "", - companyCode: screen.companyCode, - isActive: "Y", - createdDate: new Date(), - updatedDate: new Date(), - } as ScreenDefinition); + // ๋ฐ์ดํ„ฐ ํ๋ฆ„์—์„œ ์—ฐ๊ฒฐ๋œ ํ™”๋ฉด๋“ค ์ถ”๊ฐ€ (๊ฐœ๋ณ„ ํ™”๋ฉด ๋ชจ๋“œ์—์„œ๋งŒ - ๊ทธ๋ฃน ๋ชจ๋“œ์—์„œ๋Š” ๊ทธ๋ฃน ๋‚ด ํ™”๋ฉด๋งŒ ํ‘œ์‹œ) + if (!selectedGroup && screen) { + flows.forEach((flow: any) => { + if (flow.source_screen_id === screen.screenId && flow.target_screen_id) { + const exists = screenList.some((s) => s.screenId === flow.target_screen_id); + if (!exists) { + screenList.push({ + screenId: flow.target_screen_id, + screenName: flow.target_screen_name || `ํ™”๋ฉด ${flow.target_screen_id}`, + screenCode: "", + tableName: "", + companyCode: screen.companyCode, + isActive: "Y", + createdDate: new Date(), + updatedDate: new Date(), + } as ScreenDefinition); + } } - } - }); + }); + } // ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์š”์•ฝ ์ •๋ณด ๋กœ๋“œ const screenIds = screenList.map((s) => s.screenId); @@ -306,6 +325,13 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId subTablesData = subTablesRes.data as Record; // ์„œ๋ธŒ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ €์žฅ (์กฐ์ธ ์ปฌ๋Ÿผ ์ •๋ณด ํฌํ•จ) setSubTablesDataMap(subTablesData); + + // ์ „์—ญ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์ €์žฅ (์šฐ์„ ์ˆœ์œ„ ์ ์šฉ์šฉ) + // ์ด ๋ชฉ๋ก์— ์žˆ๋Š” ํ…Œ์ด๋ธ”์€ ์„œ๋ธŒ ํ…Œ์ด๋ธ”๋กœ ๋ถ„๋ฅ˜๋˜์ง€ ์•Š์Œ + const globalMainTablesArr = (subTablesRes as any).globalMainTables as string[] | undefined; + if (globalMainTablesArr && Array.isArray(globalMainTablesArr)) { + setGlobalMainTables(new Set(globalMainTablesArr)); + } } } catch (e) { console.error("๋ ˆ์ด์•„์›ƒ ์š”์•ฝ/์„œ๋ธŒ ํ…Œ์ด๋ธ” ๋กœ๋“œ ์‹คํŒจ:", e); @@ -435,9 +461,27 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId if (rel.table_name) mainTableSet.add(rel.table_name); }); - // ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์ˆ˜์ง‘ (componentConfig์—์„œ ์ถ”์ถœ๋œ ํ…Œ์ด๋ธ”๋“ค) - // ์„œ๋ธŒ ํ…Œ์ด๋ธ”์€ ๋ฉ”์ธ ํ…Œ์ด๋ธ”๊ณผ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”๋“ค - // ํ™”๋ฉด๋ณ„ ์„œ๋ธŒ ํ…Œ์ด๋ธ” ๋งคํ•‘๋„ ํ•จ๊ป˜ ๊ตฌ์ถ• + // ============================================================ + // ํ…Œ์ด๋ธ” ๋ถ„๋ฅ˜ (์šฐ์„ ์ˆœ์œ„: ๋ฉ”์ธ > ์„œ๋ธŒ) + // ============================================================ + // ๋ฉ”์ธ ํ…Œ์ด๋ธ” ์กฐ๊ฑด: + // 1. screen_definitions.table_name (์ปดํฌ๋„ŒํŠธ ์ง์ ‘ ์—ฐ๊ฒฐ) - ์ด๋ฏธ mainTableSet์— ์ถ”๊ฐ€๋จ + // 2. globalMainTables (WHERE ์กฐ๊ฑด ๋Œ€์ƒ, ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ์˜ ๋””ํ…Œ์ผ ํ…Œ์ด๋ธ”) + // + // ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์กฐ๊ฑด: + // - ์กฐ์ธ(JOIN)์œผ๋กœ๋งŒ ์—ฐ๊ฒฐ๋œ ํ…Œ์ด๋ธ” (autocomplete ๋“ฑ์—์„œ ์ฐธ์กฐ) + // - ๋‹จ, mainTableSet์— ์žˆ์œผ๋ฉด ์ œ์™ธ (์šฐ์„ ์ˆœ์œ„: ๋ฉ”์ธ > ์„œ๋ธŒ) + + // 1. globalMainTables๋ฅผ mainTableSet์— ๋จผ์ € ์ถ”๊ฐ€ (์šฐ์„ ์ˆœ์œ„ ์ ์šฉ) + const filterTableSet = new Set(); // ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ์˜ ๋””ํ…Œ์ผ ํ…Œ์ด๋ธ”๋“ค + globalMainTables.forEach((tableName) => { + if (!mainTableSet.has(tableName)) { + mainTableSet.add(tableName); + filterTableSet.add(tableName); // ํ•„ํ„ฐ ํ…Œ์ด๋ธ”๋กœ ๋ถ„๋ฅ˜ (๋ณด๋ผ์ƒ‰ ํ…Œ๋‘๋ฆฌ) + } + }); + + // 2. ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์ˆ˜์ง‘ (mainTableSet์— ์—†๋Š” ๊ฒƒ๋งŒ) const newScreenSubTableMap: Record = {}; Object.entries(subTablesData).forEach(([screenIdStr, screenSubData]) => { @@ -445,11 +489,14 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId const subTableNames: string[] = []; screenSubData.subTables.forEach((subTable) => { - // ๋ฉ”์ธ ํ…Œ์ด๋ธ”์— ์—†๋Š” ๊ฒƒ๋งŒ ์„œ๋ธŒ ํ…Œ์ด๋ธ”๋กœ ์ถ”๊ฐ€ - if (!mainTableSet.has(subTable.tableName)) { - subTableSet.add(subTable.tableName); - subTableNames.push(subTable.tableName); + // mainTableSet์— ์žˆ์œผ๋ฉด ์„œ๋ธŒ ํ…Œ์ด๋ธ”์—์„œ ์ œ์™ธ (์šฐ์„ ์ˆœ์œ„: ๋ฉ”์ธ > ์„œ๋ธŒ) + if (mainTableSet.has(subTable.tableName)) { + return; } + + // ์กฐ์ธ์œผ๋กœ๋งŒ ์—ฐ๊ฒฐ๋œ ํ…Œ์ด๋ธ” โ†’ ์„œ๋ธŒ ํ…Œ์ด๋ธ” + subTableSet.add(subTable.tableName); + subTableNames.push(subTable.tableName); }); if (subTableNames.length > 0) { @@ -540,10 +587,19 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId isForeignKey: !!col.referenceTable || (col.columnName?.includes("_id") && col.columnName !== "id"), })); - // ์—ฌ๋Ÿฌ ํ™”๋ฉด์ด ๊ฐ™์€ ํ…Œ์ด๋ธ” ์‚ฌ์šฉํ•˜๋ฉด "๊ณตํ†ต ๋ฉ”์ธ ํ…Œ์ด๋ธ”", ์•„๋‹ˆ๋ฉด "๋ฉ”์ธ ํ…Œ์ด๋ธ”" - const subLabel = linkedScreens.length > 1 - ? `๋ฉ”์ธ ํ…Œ์ด๋ธ” (${linkedScreens.length}๊ฐœ ํ™”๋ฉด)` - : "๋ฉ”์ธ ํ…Œ์ด๋ธ”"; + // ํ…Œ์ด๋ธ” ๋ถ„๋ฅ˜์— ๋”ฐ๋ฅธ ๋ผ๋ฒจ ๊ฒฐ์ • + // 1. ํ•„ํ„ฐ ํ…Œ์ด๋ธ” (๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ์˜ ๋””ํ…Œ์ผ): "ํ•„ํ„ฐ ๋Œ€์ƒ ํ…Œ์ด๋ธ”" + // 2. ์—ฌ๋Ÿฌ ํ™”๋ฉด์ด ๊ฐ™์€ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ: "๊ณตํ†ต ๋ฉ”์ธ ํ…Œ์ด๋ธ” (N๊ฐœ ํ™”๋ฉด)" + // 3. ์ผ๋ฐ˜ ๋ฉ”์ธ ํ…Œ์ด๋ธ”: "๋ฉ”์ธ ํ…Œ์ด๋ธ”" + const isFilterTable = filterTableSet.has(tableName); + let subLabel: string; + if (isFilterTable) { + subLabel = "ํ•„ํ„ฐ ๋Œ€์ƒ ํ…Œ์ด๋ธ” (๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ)"; + } else if (linkedScreens.length > 1) { + subLabel = `๋ฉ”์ธ ํ…Œ์ด๋ธ” (${linkedScreens.length}๊ฐœ ํ™”๋ฉด)`; + } else { + subLabel = "๋ฉ”์ธ ํ…Œ์ด๋ธ”"; + } // ์ด ํ…Œ์ด๋ธ”์„ ์ฐธ์กฐํ•˜๋Š” ๊ด€๊ณ„๋“ค tableNodes.push({ @@ -553,7 +609,8 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId data: { label: tableName, subLabel: subLabel, - isMain: true, // mainTableSet์˜ ๋ชจ๋“  ํ…Œ์ด๋ธ”์€ ๋ฉ”์ธ + isMain: !isFilterTable, // ํ•„ํ„ฐ ํ…Œ์ด๋ธ”์€ isMain: false๋กœ ์„ค์ • (๋ณด๋ผ์ƒ‰ ํ…Œ๋‘๋ฆฌ ํ‘œ์‹œ์šฉ) + isFilterTable: isFilterTable, // ํ•„ํ„ฐ ํ…Œ์ด๋ธ” ์—ฌ๋ถ€ ํ‘œ์‹œ columns: formattedColumns, // referencedBy, filterColumns, saveInfos๋Š” styledNodes์—์„œ ํฌ์ปค์Šค ์ƒํƒœ์— ๋”ฐ๋ผ ๋™์ ์œผ๋กœ ์„ค์ • }, diff --git a/frontend/components/screen/ScreenSettingModal.tsx b/frontend/components/screen/ScreenSettingModal.tsx index 54da7776..22c7af89 100644 --- a/frontend/components/screen/ScreenSettingModal.tsx +++ b/frontend/components/screen/ScreenSettingModal.tsx @@ -367,7 +367,7 @@ export function ScreenSettingModal({ return ( <> - + @@ -528,34 +528,30 @@ export function ScreenSettingModal({ - {/* ScreenDesigner ์ „์ฒด ํ™”๋ฉด ๋ชจ๋‹ฌ */} - - - ํ™”๋ฉด ๋””์ž์ด๋„ˆ -
- { - setShowDesignerModal(false); - // ๋””์ž์ด๋„ˆ์—์„œ ์ €์žฅ ํ›„ ๋ชจ๋‹ฌ ๋‹ซ์œผ๋ฉด ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ - await loadData(); - // ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์™„๋ฃŒ ํ›„ iframe ๊ฐฑ์‹  - setIframeKey(prev => prev + 1); - }} - /> -
-
-
+ {/* ScreenDesigner ์ „์ฒด ํ™”๋ฉด - Dialog ์ค‘์ฒฉ ์‹œ ์˜ค๋ฒ„๋ ˆ์ด ์ปดํฌ์ง€ํŒ…์œผ๋กœ ๊นœ๋นก์ž„ ๋ฐœ์ƒํ•˜์—ฌ ์ง์ ‘ ๋ Œ๋”๋ง */} + {/* ScreenDesigner ์ž์ฒด SlimToolbar์— "๋ชฉ๋ก์œผ๋กœ" ๋‹ซ๊ธฐ ๋ฒ„ํŠผ์ด ์žˆ์œผ๋ฏ€๋กœ ๋ณ„๋„ X ๋ฒ„ํŠผ ๋ถˆํ•„์š” */} + {showDesignerModal && ( +
+ { + setShowDesignerModal(false); + await loadData(); + setIframeKey(prev => prev + 1); + }} + /> +
+ )} {/* TableSettingModal */} {tableSettingTarget && ( @@ -736,7 +732,12 @@ function TableColumnAccordion({ if (allTables.length === 0) { const tablesResult = await tableManagementApi.getTableList(); if (tablesResult.success && tablesResult.data) { - setAllTables(tablesResult.data); + // ์ค‘๋ณต ํ…Œ์ด๋ธ” ์ œ๊ฑฐ (tableName ๊ธฐ์ค€) + const uniqueTables = tablesResult.data.filter( + (table, index, self) => + index === self.findIndex((t) => t.tableName === table.tableName) + ); + setAllTables(uniqueTables); } } } catch (error) { @@ -1351,9 +1352,9 @@ function JoinSettingEditor({ ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - {allTables.map(t => ( + {allTables.map((t, idx) => ( { setEditingJoin({ ...editingJoin, referenceTable: t.tableName, referenceColumn: "", displayColumn: "" }); diff --git a/frontend/components/screen/TableTypeSelector.tsx b/frontend/components/screen/TableTypeSelector.tsx index 5da49312..c6757d92 100644 --- a/frontend/components/screen/TableTypeSelector.tsx +++ b/frontend/components/screen/TableTypeSelector.tsx @@ -174,30 +174,10 @@ export default function TableTypeSelector({ } }; - // ์ž…๋ ฅ ํƒ€์ž… ๋ณ€๊ฒฝ - const handleInputTypeChange = async (columnName: string, inputType: "direct" | "auto") => { - try { - // ํ˜„์žฌ ์ปฌ๋Ÿผ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ - const currentColumn = columns.find((col) => col.columnName === columnName); - if (!currentColumn) return; - - // ์›น ํƒ€์ž…๊ณผ ํ•จ๊ป˜ ์ž…๋ ฅ ํƒ€์ž… ์—…๋ฐ์ดํŠธ - await tableTypeApi.setColumnWebType( - selectedTable, - columnName, - currentColumn.webType || "text", - undefined, // detailSettings - inputType, - ); - - // ๋กœ์ปฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ - setColumns((prev) => prev.map((col) => (col.columnName === columnName ? { ...col, inputType } : col))); - - // console.log(`์ปฌ๋Ÿผ ${columnName}์˜ ์ž…๋ ฅ ํƒ€์ž…์„ ${inputType}๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.`); - } catch (error) { - // console.error("์ž…๋ ฅ ํƒ€์ž… ๋ณ€๊ฒฝ ์‹คํŒจ:", error); - alert("์ž…๋ ฅ ํƒ€์ž… ์„ค์ •์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."); - } + // ์ž…๋ ฅ ํƒ€์ž… ๋ณ€๊ฒฝ (๋กœ์ปฌ ์ƒํƒœ๋งŒ - DB์— ์ €์žฅํ•˜์ง€ ์•Š์Œ) + const handleInputTypeChange = (columnName: string, inputType: "direct" | "auto") => { + // ๋กœ์ปฌ ์ƒํƒœ๋งŒ ์—…๋ฐ์ดํŠธ (DB์—๋Š” ์ €์žฅํ•˜์ง€ ์•Š์Œ - inputType์€ ํ™”๋ฉด ๋ Œ๋”๋ง์šฉ) + setColumns((prev) => prev.map((col) => (col.columnName === columnName ? { ...col, inputType } : col))); }; const filteredTables = tables.filter((table) => table.displayName.toLowerCase().includes(searchTerm.toLowerCase())); diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index bade282b..8d6df989 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -51,13 +51,9 @@ export const ButtonConfigPanel: React.FC = ({ }) => { // ๐Ÿ”ง component๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ๋ฐฉ์–ด ์ฒ˜๋ฆฌ if (!component) { - return ( -
- ์ปดํฌ๋„ŒํŠธ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. -
- ); + return
์ปดํฌ๋„ŒํŠธ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
; } - + // ๐Ÿ”ง component์—์„œ ์ง์ ‘ ์ฝ๊ธฐ (useMemo ์ œ๊ฑฐ) const config = component.componentConfig || {}; const currentAction = component.componentConfig?.action || {}; @@ -122,7 +118,9 @@ export const ButtonConfigPanel: React.FC = ({ const [modalActionTargetTable, setModalActionTargetTable] = useState(null); const [modalActionSourceColumns, setModalActionSourceColumns] = useState>([]); const [modalActionTargetColumns, setModalActionTargetColumns] = useState>([]); - const [modalActionFieldMappings, setModalActionFieldMappings] = useState>([]); + const [modalActionFieldMappings, setModalActionFieldMappings] = useState< + Array<{ sourceField: string; targetField: string }> + >([]); const [modalFieldMappingSourceOpen, setModalFieldMappingSourceOpen] = useState>({}); const [modalFieldMappingTargetOpen, setModalFieldMappingTargetOpen] = useState>({}); const [modalFieldMappingSourceSearch, setModalFieldMappingSourceSearch] = useState>({}); @@ -353,7 +351,7 @@ export const ButtonConfigPanel: React.FC = ({ useEffect(() => { const actionType = config.action?.type; if (actionType !== "modal") return; - + const autoDetect = config.action?.autoDetectDataSource; if (!autoDetect) { // ๋ฐ์ดํ„ฐ ์ „๋‹ฌ์ด ๋น„ํ™œ์„ฑํ™”๋˜๋ฉด ์ƒํƒœ ์ดˆ๊ธฐํ™” @@ -363,19 +361,19 @@ export const ButtonConfigPanel: React.FC = ({ setModalActionTargetColumns([]); return; } - + const targetScreenId = config.action?.targetScreenId; if (!targetScreenId) return; - + const loadModalActionMappingData = async () => { // 1. ์†Œ์Šค ํ…Œ์ด๋ธ” ๊ฐ์ง€ (ํ˜„์žฌ ํ™”๋ฉด) let sourceTableName: string | null = currentTableName || null; - + // allComponents์—์„œ ๋ถ„ํ• ํŒจ๋„/ํ…Œ์ด๋ธ”๋ฆฌ์ŠคํŠธ/ํ†ตํ•ฉ๋ชฉ๋ก ๊ฐ์ง€ for (const comp of allComponents) { const compType = comp.componentType || (comp as any).componentConfig?.type; const compConfig = (comp as any).componentConfig || {}; - + if (compType === "split-panel-layout" || compType === "screen-split-panel") { sourceTableName = compConfig.leftPanel?.tableName || compConfig.tableName || null; if (sourceTableName) break; @@ -389,9 +387,9 @@ export const ButtonConfigPanel: React.FC = ({ if (sourceTableName) break; } } - + setModalActionSourceTable(sourceTableName); - + // 2. ๋Œ€์ƒ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ” ์กฐํšŒ let targetTableName: string | null = null; try { @@ -405,9 +403,9 @@ export const ButtonConfigPanel: React.FC = ({ } catch (error) { console.error("๋Œ€์ƒ ํ™”๋ฉด ์ •๋ณด ๋กœ๋“œ ์‹คํŒจ:", error); } - + setModalActionTargetTable(targetTableName); - + // 3. ์†Œ์Šค ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ if (sourceTableName) { try { @@ -416,7 +414,7 @@ export const ButtonConfigPanel: React.FC = ({ let columnData = response.data.data; if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns; if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data; - + if (Array.isArray(columnData)) { const columns = columnData.map((col: any) => ({ name: col.name || col.columnName, @@ -429,7 +427,7 @@ export const ButtonConfigPanel: React.FC = ({ console.error("์†Œ์Šค ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ ์‹คํŒจ:", error); } } - + // 4. ๋Œ€์ƒ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ if (targetTableName) { try { @@ -438,7 +436,7 @@ export const ButtonConfigPanel: React.FC = ({ let columnData = response.data.data; if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns; if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data; - + if (Array.isArray(columnData)) { const columns = columnData.map((col: any) => ({ name: col.name || col.columnName, @@ -451,7 +449,7 @@ export const ButtonConfigPanel: React.FC = ({ console.error("๋Œ€์ƒ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ ์‹คํŒจ:", error); } } - + // 5. ๊ธฐ์กด ํ•„๋“œ ๋งคํ•‘ ๋กœ๋“œ ๋˜๋Š” ์ž๋™ ๋งคํ•‘ ์ƒ์„ฑ const existingMappings = config.action?.fieldMappings || []; if (existingMappings.length > 0) { @@ -461,10 +459,16 @@ export const ButtonConfigPanel: React.FC = ({ setModalActionFieldMappings([]); // ๋นˆ ๋ฐฐ์—ด = ์ž๋™ ๋งคํ•‘ } }; - + loadModalActionMappingData(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [config.action?.type, config.action?.autoDetectDataSource, config.action?.targetScreenId, currentTableName, allComponents]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + config.action?.type, + config.action?.autoDetectDataSource, + config.action?.targetScreenId, + currentTableName, + allComponents, + ]); // ๐Ÿ†• ํ˜„์žฌ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ (๊ทธ๋ฃนํ™” ์ปฌ๋Ÿผ ์„ ํƒ์šฉ) useEffect(() => { @@ -818,21 +822,26 @@ export const ButtonConfigPanel: React.FC = ({ ํŽ˜์ด์ง€ ์ด๋™ ๋ชจ๋‹ฌ ์—ด๊ธฐ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ - + {/* ์—‘์…€ ๊ด€๋ จ */} ์—‘์…€ ๋‹ค์šด๋กœ๋“œ ์—‘์…€ ์—…๋กœ๋“œ - + {/* ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ */} ์ฆ‰์‹œ ์ €์žฅ ์ œ์–ด ํ๋ฆ„ - + {/* ํŠน์ˆ˜ ๊ธฐ๋Šฅ (ํ•„์š” ์‹œ ์‚ฌ์šฉ) */} ๋ฐ”์ฝ”๋“œ ์Šค์บ” ์šดํ–‰์•Œ๋ฆผ ๋ฐ ์ข…๋ฃŒ - - {/* ๐Ÿ”’ ์ˆจ๊น€ ์ฒ˜๋ฆฌ - ๊ธฐ์กด ์‹œ์Šคํ…œ ํ˜ธํ™˜์„ฑ ์œ ์ง€, UI์—์„œ๋งŒ ์ˆจ๊น€ + + {/* ์ด๋ฒคํŠธ ๋ฒ„์Šค */} + ์ด๋ฒคํŠธ ๋ฐœ์†ก + + {/* ๋ณต์‚ฌ */} ๋ณต์‚ฌ (ํ’ˆ๋ชฉ์ฝ”๋“œ ์ดˆ๊ธฐํ™”) + + {/* ๐Ÿ”’ ์ˆจ๊น€ ์ฒ˜๋ฆฌ - ๊ธฐ์กด ์‹œ์Šคํ…œ ํ˜ธํ™˜์„ฑ ์œ ์ง€, UI์—์„œ๋งŒ ์ˆจ๊น€ ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ ๋ฒ„ํŠผ ๋ชจ๋‹ฌ ์—ด๊ธฐ (deprecated) ๋ฐ์ดํ„ฐ ์ „๋‹ฌ + ๋ชจ๋‹ฌ ์—ด๊ธฐ ํ…Œ์ด๋ธ” ์ด๋ ฅ ๋ณด๊ธฐ @@ -980,10 +989,10 @@ export const ButtonConfigPanel: React.FC = ({ }} />
-
@@ -991,11 +1000,11 @@ export const ButtonConfigPanel: React.FC = ({ {/* ๐Ÿ†• ํ•„๋“œ ๋งคํ•‘ UI (๋ฐ์ดํ„ฐ ์ „๋‹ฌ ํ™œ์„ฑํ™” + ํ…Œ์ด๋ธ”์ด ๋‹ค๋ฅธ ๊ฒฝ์šฐ) */} {component.componentConfig?.action?.autoDetectDataSource === true && ( -
+
{/* ํ…Œ์ด๋ธ” ์ •๋ณด ํ‘œ์‹œ */}
- + ์†Œ์Šค: {modalActionSourceTable || "๊ฐ์ง€ ์ค‘..."}
@@ -1007,171 +1016,210 @@ export const ButtonConfigPanel: React.FC = ({
{/* ํ…Œ์ด๋ธ”์ด ๊ฐ™์œผ๋ฉด ์ž๋™ ๋งคํ•‘ ์•ˆ๋‚ด */} - {modalActionSourceTable && modalActionTargetTable && modalActionSourceTable === modalActionTargetTable && ( -
- ๋™์ผํ•œ ํ…Œ์ด๋ธ”์ž…๋‹ˆ๋‹ค. ์ปฌ๋Ÿผ๋ช…์ด ๊ฐ™์€ ํ•„๋“œ๋Š” ์ž๋™์œผ๋กœ ๋งคํ•‘๋ฉ๋‹ˆ๋‹ค. -
- )} + {modalActionSourceTable && + modalActionTargetTable && + modalActionSourceTable === modalActionTargetTable && ( +
+ ๋™์ผํ•œ ํ…Œ์ด๋ธ”์ž…๋‹ˆ๋‹ค. ์ปฌ๋Ÿผ๋ช…์ด ๊ฐ™์€ ํ•„๋“œ๋Š” ์ž๋™์œผ๋กœ ๋งคํ•‘๋ฉ๋‹ˆ๋‹ค. +
+ )} {/* ํ…Œ์ด๋ธ”์ด ๋‹ค๋ฅด๋ฉด ํ•„๋“œ ๋งคํ•‘ UI ํ‘œ์‹œ */} - {modalActionSourceTable && modalActionTargetTable && modalActionSourceTable !== modalActionTargetTable && ( -
-
- - -
- - {(component.componentConfig?.action?.fieldMappings || []).length === 0 && ( -

- ์ปฌ๋Ÿผ๋ช…์ด ๋‹ค๋ฅธ ๊ฒฝ์šฐ ๋งคํ•‘์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”. ๋งคํ•‘์ด ์—†์œผ๋ฉด ๋™์ผ ์ปฌ๋Ÿผ๋ช…๋งŒ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. -

- )} - - {(component.componentConfig?.action?.fieldMappings || []).map((mapping: any, index: number) => ( -
- {/* ์†Œ์Šค ํ•„๋“œ ์„ ํƒ */} - setModalFieldMappingSourceOpen((prev) => ({ ...prev, [index]: open }))} - > - - - - - - setModalFieldMappingSourceSearch((prev) => ({ ...prev, [index]: val }))} - /> - - ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - - {modalActionSourceColumns - .filter((col) => - col.name.toLowerCase().includes((modalFieldMappingSourceSearch[index] || "").toLowerCase()) || - col.label.toLowerCase().includes((modalFieldMappingSourceSearch[index] || "").toLowerCase()) - ) - .map((col) => ( - { - const newMappings = [...(component.componentConfig?.action?.fieldMappings || [])]; - newMappings[index] = { ...newMappings[index], sourceField: col.name }; - setModalActionFieldMappings(newMappings); - onUpdateProperty("componentConfig.action.fieldMappings", newMappings); - setModalFieldMappingSourceOpen((prev) => ({ ...prev, [index]: false })); - }} - > - -
- {col.label} - {col.name} -
-
- ))} -
-
-
-
-
- - โ†’ - - {/* ๋Œ€์ƒ ํ•„๋“œ ์„ ํƒ */} - setModalFieldMappingTargetOpen((prev) => ({ ...prev, [index]: open }))} - > - - - - - - setModalFieldMappingTargetSearch((prev) => ({ ...prev, [index]: val }))} - /> - - ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - - {modalActionTargetColumns - .filter((col) => - col.name.toLowerCase().includes((modalFieldMappingTargetSearch[index] || "").toLowerCase()) || - col.label.toLowerCase().includes((modalFieldMappingTargetSearch[index] || "").toLowerCase()) - ) - .map((col) => ( - { - const newMappings = [...(component.componentConfig?.action?.fieldMappings || [])]; - newMappings[index] = { ...newMappings[index], targetField: col.name }; - setModalActionFieldMappings(newMappings); - onUpdateProperty("componentConfig.action.fieldMappings", newMappings); - setModalFieldMappingTargetOpen((prev) => ({ ...prev, [index]: false })); - }} - > - -
- {col.label} - {col.name} -
-
- ))} -
-
-
-
-
- - {/* ์‚ญ์ œ ๋ฒ„ํŠผ */} + {modalActionSourceTable && + modalActionTargetTable && + modalActionSourceTable !== modalActionTargetTable && ( +
+
+
- ))} -
- )} + + {(component.componentConfig?.action?.fieldMappings || []).length === 0 && ( +

+ ์ปฌ๋Ÿผ๋ช…์ด ๋‹ค๋ฅธ ๊ฒฝ์šฐ ๋งคํ•‘์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”. ๋งคํ•‘์ด ์—†์œผ๋ฉด ๋™์ผ ์ปฌ๋Ÿผ๋ช…๋งŒ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. +

+ )} + + {(component.componentConfig?.action?.fieldMappings || []).map((mapping: any, index: number) => ( +
+ {/* ์†Œ์Šค ํ•„๋“œ ์„ ํƒ */} + + setModalFieldMappingSourceOpen((prev) => ({ ...prev, [index]: open })) + } + > + + + + + + + setModalFieldMappingSourceSearch((prev) => ({ ...prev, [index]: val })) + } + /> + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {modalActionSourceColumns + .filter( + (col) => + col.name + .toLowerCase() + .includes((modalFieldMappingSourceSearch[index] || "").toLowerCase()) || + col.label + .toLowerCase() + .includes((modalFieldMappingSourceSearch[index] || "").toLowerCase()), + ) + .map((col) => ( + { + const newMappings = [ + ...(component.componentConfig?.action?.fieldMappings || []), + ]; + newMappings[index] = { ...newMappings[index], sourceField: col.name }; + setModalActionFieldMappings(newMappings); + onUpdateProperty("componentConfig.action.fieldMappings", newMappings); + setModalFieldMappingSourceOpen((prev) => ({ ...prev, [index]: false })); + }} + > + +
+ {col.label} + {col.name} +
+
+ ))} +
+
+
+
+
+ + โ†’ + + {/* ๋Œ€์ƒ ํ•„๋“œ ์„ ํƒ */} + + setModalFieldMappingTargetOpen((prev) => ({ ...prev, [index]: open })) + } + > + + + + + + + setModalFieldMappingTargetSearch((prev) => ({ ...prev, [index]: val })) + } + /> + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {modalActionTargetColumns + .filter( + (col) => + col.name + .toLowerCase() + .includes((modalFieldMappingTargetSearch[index] || "").toLowerCase()) || + col.label + .toLowerCase() + .includes((modalFieldMappingTargetSearch[index] || "").toLowerCase()), + ) + .map((col) => ( + { + const newMappings = [ + ...(component.componentConfig?.action?.fieldMappings || []), + ]; + newMappings[index] = { ...newMappings[index], targetField: col.name }; + setModalActionFieldMappings(newMappings); + onUpdateProperty("componentConfig.action.fieldMappings", newMappings); + setModalFieldMappingTargetOpen((prev) => ({ ...prev, [index]: false })); + }} + > + +
+ {col.label} + {col.name} +
+
+ ))} +
+
+
+
+
+ + {/* ์‚ญ์ œ ๋ฒ„ํŠผ */} + +
+ ))} +
+ )}
)}
@@ -1180,9 +1228,10 @@ export const ButtonConfigPanel: React.FC = ({ {/* ๐Ÿ†• ๋ฐ์ดํ„ฐ ์ „๋‹ฌ + ๋ชจ๋‹ฌ ์—ด๊ธฐ ์•ก์…˜ ์„ค์ • (deprecated - ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€) */} {component.componentConfig?.action?.type === "openModalWithData" && (
-

๋ฐ์ดํ„ฐ ์ „๋‹ฌ + ๋ชจ๋‹ฌ ์„ค์ •

+

๋ฐ์ดํ„ฐ ์ „๋‹ฌ + ๋ชจ๋‹ฌ ์„ค์ •

- ์ด ์˜ต์…˜์€ "๋ชจ๋‹ฌ ์—ด๊ธฐ" ์•ก์…˜์œผ๋กœ ํ†ตํ•ฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ ๊ฐœ๋ฐœ์—์„œ๋Š” "๋ชจ๋‹ฌ ์—ด๊ธฐ" + "์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ"์„ ์‚ฌ์šฉํ•˜์„ธ์š”. + ์ด ์˜ต์…˜์€ "๋ชจ๋‹ฌ ์—ด๊ธฐ" ์•ก์…˜์œผ๋กœ ํ†ตํ•ฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ ๊ฐœ๋ฐœ์—์„œ๋Š” "๋ชจ๋‹ฌ ์—ด๊ธฐ" + "์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ"์„ + ์‚ฌ์šฉํ•˜์„ธ์š”.

{/* ๐Ÿ†• ๋ธ”๋ก ๊ธฐ๋ฐ˜ ์ œ๋ชฉ ๋นŒ๋” */} @@ -3536,6 +3585,104 @@ export const ButtonConfigPanel: React.FC = ({ /> )} + {/* ๐Ÿ†• ์ด๋ฒคํŠธ ๋ฐœ์†ก ์•ก์…˜ ์„ค์ • */} + {localInputs.actionType === "event" && ( +
+

์ด๋ฒคํŠธ ๋ฐœ์†ก ์„ค์ •

+

+ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ V2 ์ด๋ฒคํŠธ ๋ฒ„์Šค๋ฅผ ํ†ตํ•ด ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์†กํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋‚˜ ์„œ๋น„์Šค์—์„œ ์ด ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜์—ฌ + ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +

+ +
+ + +
+ + {component.componentConfig?.action?.eventConfig?.eventName === "SCHEDULE_GENERATE_REQUEST" && ( +
+
+ + +
+ +
+ + { + onUpdateProperty( + "componentConfig.action.eventConfig.eventPayload.config.scheduling.leadTimeDays", + parseInt(e.target.value) || 3, + ); + }} + /> +
+ +
+ + { + onUpdateProperty( + "componentConfig.action.eventConfig.eventPayload.config.scheduling.maxDailyCapacity", + parseInt(e.target.value) || 100, + ); + }} + /> +
+ +
+

+ ๋™์ž‘ ๋ฐฉ์‹: ํ…Œ์ด๋ธ”์—์„œ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์Šค์ผ€์ค„์„ ์ž๋™ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ƒ์„ฑ ์ „ + ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. +

+
+
+ )} +
+ )} + {/* ๐Ÿ†• ํ–‰ ์„ ํƒ ์‹œ์—๋งŒ ํ™œ์„ฑํ™” ์„ค์ • */}

ํ–‰ ์„ ํƒ ํ™œ์„ฑํ™” ์กฐ๊ฑด

diff --git a/frontend/components/screen/config-panels/ImprovedButtonControlConfigPanel.tsx b/frontend/components/screen/config-panels/ImprovedButtonControlConfigPanel.tsx index 197b9759..1cbad525 100644 --- a/frontend/components/screen/config-panels/ImprovedButtonControlConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ImprovedButtonControlConfigPanel.tsx @@ -173,6 +173,8 @@ export const ImprovedButtonControlConfigPanel: React.FC void; +} + +export const KeyboardShortcutsModal: React.FC = ({ + isOpen, + onClose, +}) => { + return ( + + + + + ํ‚ค๋ณด๋“œ ๋‹จ์ถ•ํ‚ค + + + ํ™”๋ฉด ๋””์ž์ด๋„ˆ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋‹จ์ถ•ํ‚ค ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. Mac์—์„œ๋Š” Ctrl ๋Œ€์‹  Cmd๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + + + +
+ {shortcutGroups.map((group) => ( +
+

+ {group.title} +

+
+ {group.shortcuts.map((shortcut, idx) => ( +
+ + {shortcut.description} + +
+ {shortcut.keys.map((key, kidx) => ( + + {kidx > 0 && ( + + + )} + + {key} + + + ))} +
+
+ ))} +
+
+ ))} +
+
+
+ ); +}; diff --git a/frontend/components/screen/panels/ComponentsPanel.tsx b/frontend/components/screen/panels/ComponentsPanel.tsx index 8f055bc3..9464a204 100644 --- a/frontend/components/screen/panels/ComponentsPanel.tsx +++ b/frontend/components/screen/panels/ComponentsPanel.tsx @@ -80,7 +80,7 @@ export function ComponentsPanel({ "textarea-basic", // V2 ์ปดํฌ๋„ŒํŠธ๋กœ ๋Œ€์ฒด๋จ "image-widget", // โ†’ V2Media (image) - "file-upload", // โ†’ V2Media (file) + // "file-upload", // ๐Ÿ†• ๋ ˆ๊ฑฐ์‹œ ์ปดํฌ๋„ŒํŠธ ๋…ธ์ถœ (์•ˆ์ •์ ์ธ ํŒŒ์ผ ์—…๋กœ๋“œ) "entity-search-input", // โ†’ V2Select (entity ๋ชจ๋“œ) "autocomplete-search-input", // โ†’ V2Select (autocomplete ๋ชจ๋“œ) // DataFlow ์ „์šฉ (์ผ๋ฐ˜ ํ™”๋ฉด์—์„œ ๋ถˆํ•„์š”) diff --git a/frontend/components/screen/panels/TablesPanel.tsx b/frontend/components/screen/panels/TablesPanel.tsx index 480222a6..12dcc19a 100644 --- a/frontend/components/screen/panels/TablesPanel.tsx +++ b/frontend/components/screen/panels/TablesPanel.tsx @@ -431,7 +431,8 @@ export const TablesPanel: React.FC = ({ } return ( -
+ // ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ํ…Œ์ด๋ธ”์— ๊ณ ์œ  ์ ‘๋‘์‚ฌ ์ถ”๊ฐ€ (๋ฉ”์ธ ํ…Œ์ด๋ธ”๊ณผ ํ‚ค ์ค‘๋ณต ๋ฐฉ์ง€) +
{/* ์กฐ์ธ ํ…Œ์ด๋ธ” ํ—ค๋” */}
= ({ if (componentId?.startsWith("v2-")) { const v2ConfigPanels: Record void }>> = { "v2-input": require("@/components/v2/config-panels/V2InputConfigPanel").V2InputConfigPanel, - "v2-select": require("@/components/v2/config-panels/V2SelectConfigPanel") - .V2SelectConfigPanel, + "v2-select": require("@/components/v2/config-panels/V2SelectConfigPanel").V2SelectConfigPanel, "v2-date": require("@/components/v2/config-panels/V2DateConfigPanel").V2DateConfigPanel, "v2-list": require("@/components/v2/config-panels/V2ListConfigPanel").V2ListConfigPanel, - "v2-layout": require("@/components/v2/config-panels/V2LayoutConfigPanel") - .V2LayoutConfigPanel, + "v2-layout": require("@/components/v2/config-panels/V2LayoutConfigPanel").V2LayoutConfigPanel, "v2-group": require("@/components/v2/config-panels/V2GroupConfigPanel").V2GroupConfigPanel, "v2-media": require("@/components/v2/config-panels/V2MediaConfigPanel").V2MediaConfigPanel, "v2-biz": require("@/components/v2/config-panels/V2BizConfigPanel").V2BizConfigPanel, - "v2-hierarchy": require("@/components/v2/config-panels/V2HierarchyConfigPanel") - .V2HierarchyConfigPanel, + "v2-hierarchy": require("@/components/v2/config-panels/V2HierarchyConfigPanel").V2HierarchyConfigPanel, }; const V2ConfigPanel = v2ConfigPanels[componentId]; @@ -263,6 +260,7 @@ export const V2PropertiesPanel: React.FC = ({ definitionName: definition.name, hasConfigPanel: !!definition.configPanel, currentConfig, + defaultSort: currentConfig?.defaultSort, // ๐Ÿ” defaultSort ํ™•์ธ }); // ๐Ÿ”ง ConfigPanelWrapper๋ฅผ ์ธ๋ผ์ธ ํ•จ์ˆ˜ ๋Œ€์‹  ์ง์ ‘ JSX ๋ฐ˜ํ™˜ (๋ฆฌ๋งˆ์šดํŠธ ๋ฐฉ์ง€) @@ -822,8 +820,16 @@ export const V2PropertiesPanel: React.FC = ({
handleUpdate("style.labelText", e.target.value)} + value={ + selectedComponent.style?.labelText !== undefined + ? selectedComponent.style.labelText + : selectedComponent.label || selectedComponent.componentConfig?.label || "" + } + onChange={(e) => { + handleUpdate("style.labelText", e.target.value); + handleUpdate("label", e.target.value); // label๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ + }} + placeholder="๋ผ๋ฒจ์„ ์ž…๋ ฅํ•˜์„ธ์š” (๋น„์šฐ๋ฉด ๋ผ๋ฒจ ์—†์Œ)" className="h-6 w-full px-2 py-0 text-xs" />
@@ -857,8 +863,20 @@ export const V2PropertiesPanel: React.FC = ({
handleUpdate("style.labelDisplay", checked)} + checked={selectedComponent.style?.labelDisplay === true || selectedComponent.labelDisplay === true} + onCheckedChange={(checked) => { + const boolValue = checked === true; + // ๐Ÿ”ง "ํ•„์ˆ˜"์ฒ˜๋Ÿผ ์ง์ ‘ ๊ฒฝ๋กœ๋กœ ์—…๋ฐ์ดํŠธ! (style ๊ฐ์ฒด ์ „์ฒด ๋ฎ์–ด์“ฐ๊ธฐ ๋ฐฉ์ง€) + handleUpdate("style.labelDisplay", boolValue); + handleUpdate("labelDisplay", boolValue); + // labelText๋„ ์„ค์ • (์ฒ˜์Œ ์ผค ๋•Œ ๋ผ๋ฒจ ํ…์ŠคํŠธ๊ฐ€ ์—†์„ ์ˆ˜ ์žˆ์Œ) + if (boolValue && !selectedComponent.style?.labelText) { + const labelValue = selectedComponent.label || selectedComponent.componentConfig?.label || ""; + if (labelValue) { + handleUpdate("style.labelText", labelValue); + } + } + }} className="h-4 w-4" /> @@ -868,9 +886,9 @@ export const V2PropertiesPanel: React.FC = ({ )} - {/* ์˜ต์…˜ */} + {/* ์˜ต์…˜ - ์ž…๋ ฅ ํ•„๋“œ์—์„œ๋Š” ํ•ญ์ƒ ํ‘œ์‹œ, ๊ธฐํƒ€ ์ปดํฌ๋„ŒํŠธ๋Š” ์†์„ฑ์ด ์ •์˜๋œ ๊ฒฝ์šฐ๋งŒ ํ‘œ์‹œ */}
- {widget.required !== undefined && ( + {(isInputField || widget.required !== undefined) && (
= ({
)} - {widget.readonly !== undefined && ( + {(isInputField || widget.readonly !== undefined) && (
= ({
)} - {/* ์ˆจ๊น€ ์˜ต์…˜ */} + {/* ์ˆจ๊น€ ์˜ต์…˜ - ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์—์„œ ํ‘œ์‹œ */}
= ({ } // ๐Ÿ†• 3.5. V2 ์ปดํฌ๋„ŒํŠธ - ๋ฐ˜๋“œ์‹œ ๋‹ค๋ฅธ ์ฒดํฌ๋ณด๋‹ค ๋จผ์ € ์ฒ˜๋ฆฌ - const v2ComponentType = - (selectedComponent as any).componentType || selectedComponent.componentConfig?.type || ""; + const v2ComponentType = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type || ""; if (v2ComponentType.startsWith("v2-")) { const configPanel = renderComponentConfigPanel(); if (configPanel) { @@ -1055,8 +1072,15 @@ export const V2PropertiesPanel: React.FC = ({ allComponents={allComponents} // ๐Ÿ†• ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด ๋ถ€๋ชจ ๊ฐ์ง€์šฉ currentComponent={selectedComponent} // ๐Ÿ†• ํ˜„์žฌ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด onChange={(newConfig) => { + console.log("๐Ÿ”ง [V2PropertiesPanel] DynamicConfigPanel onChange:", { + componentId: selectedComponent.id, + newConfigKeys: Object.keys(newConfig), + defaultSort: newConfig.defaultSort, + newConfig, + }); // ๊ฐœ๋ณ„ ์†์„ฑ๋ณ„๋กœ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ๋‹ค๋ฅธ ์†์„ฑ๊ณผ์˜ ์ถฉ๋Œ ๋ฐฉ์ง€ Object.entries(newConfig).forEach(([key, value]) => { + console.log(` -> handleUpdate: componentConfig.${key} =`, value); handleUpdate(`componentConfig.${key}`, value); }); }} diff --git a/frontend/components/screen/panels/webtype-configs/SelectTypeConfigPanel.tsx b/frontend/components/screen/panels/webtype-configs/SelectTypeConfigPanel.tsx index f59171d1..e83c17f6 100644 --- a/frontend/components/screen/panels/webtype-configs/SelectTypeConfigPanel.tsx +++ b/frontend/components/screen/panels/webtype-configs/SelectTypeConfigPanel.tsx @@ -5,6 +5,7 @@ import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Plus, X } from "lucide-react"; import { SelectTypeConfig } from "@/types/screen"; @@ -22,6 +23,7 @@ export const SelectTypeConfigPanel: React.FC = ({ co placeholder: "", allowClear: false, maxSelections: undefined, + defaultValue: "", ...config, }; @@ -32,6 +34,7 @@ export const SelectTypeConfigPanel: React.FC = ({ co placeholder: safeConfig.placeholder, allowClear: safeConfig.allowClear, maxSelections: safeConfig.maxSelections?.toString() || "", + defaultValue: safeConfig.defaultValue || "", }); const [newOption, setNewOption] = useState({ label: "", value: "" }); @@ -53,6 +56,7 @@ export const SelectTypeConfigPanel: React.FC = ({ co placeholder: safeConfig.placeholder, allowClear: safeConfig.allowClear, maxSelections: safeConfig.maxSelections?.toString() || "", + defaultValue: safeConfig.defaultValue || "", }); setLocalOptions( @@ -68,6 +72,7 @@ export const SelectTypeConfigPanel: React.FC = ({ co safeConfig.placeholder, safeConfig.allowClear, safeConfig.maxSelections, + safeConfig.defaultValue, JSON.stringify(safeConfig.options), // ์˜ต์…˜ ๋ฐฐ์—ด์˜ ์ „์ฒด ๋‚ด์šฉ ๋ณ€ํ™” ๊ฐ์ง€ ]); @@ -174,6 +179,30 @@ export const SelectTypeConfigPanel: React.FC = ({ co />
+ {/* ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • */} +
+ + +

ํ™”๋ฉด ๋กœ๋“œ ์‹œ ์ž๋™์œผ๋กœ ์„ ํƒ๋  ๊ฐ’

+
+ {/* ๋‹ค์ค‘ ์„ ํƒ */}
+ {/* ์ค‘์•™: ์ •๋ ฌ/๋ฐฐ๋ถ„ ๋„๊ตฌ (๋‹ค์ค‘ ์„ ํƒ ์‹œ ํ‘œ์‹œ) */} + {selectedCount >= 2 && (onAlign || onDistribute || onMatchSize) && ( +
+ {/* ์ •๋ ฌ */} + {onAlign && ( + <> + ์ •๋ ฌ + + + +
+ + + + + )} + + {/* ๋ฐฐ๋ถ„ (3๊ฐœ ์ด์ƒ ์„ ํƒ ์‹œ) */} + {onDistribute && selectedCount >= 3 && ( + <> +
+ ๋ฐฐ๋ถ„ + + + + )} + + {/* ํฌ๊ธฐ ๋งž์ถ”๊ธฐ */} + {onMatchSize && ( + <> +
+ ํฌ๊ธฐ + + + + + )} + +
+ {selectedCount}๊ฐœ ์„ ํƒ +
+ )} + {/* ์šฐ์ธก: ๋ฒ„ํŠผ๋“ค */}
+ {/* ๋ผ๋ฒจ ํ† ๊ธ€ ๋ฒ„ํŠผ */} + {onToggleLabels && ( + + )} + + {/* ๋‹จ์ถ•ํ‚ค ๋„์›€๋ง */} + {onShowShortcuts && ( + + )} + {onPreview && ( diff --git a/frontend/components/screen/widgets/types/ImageWidget.tsx b/frontend/components/screen/widgets/types/ImageWidget.tsx index 5c81ca9c..a9bd62d3 100644 --- a/frontend/components/screen/widgets/types/ImageWidget.tsx +++ b/frontend/components/screen/widgets/types/ImageWidget.tsx @@ -9,15 +9,19 @@ import { WidgetComponent } from "@/types/screen"; import { toast } from "sonner"; import { apiClient, getFullImageUrl } from "@/lib/api/client"; -export const ImageWidget: React.FC = ({ - component, - value, - onChange, +export const ImageWidget: React.FC< + WebTypeComponentProps & { size?: { width?: number; height?: number }; style?: React.CSSProperties } +> = ({ + component, + value, + onChange, readonly = false, - isDesignMode = false // ๋””์ž์ธ ๋ชจ๋“œ ์—ฌ๋ถ€ + isDesignMode = false, // ๋””์ž์ธ ๋ชจ๋“œ ์—ฌ๋ถ€ + size, // props๋กœ ์ „๋‹ฌ๋œ size + style: propStyle, // props๋กœ ์ „๋‹ฌ๋œ style }) => { const widget = component as WidgetComponent; - const { required, style } = widget; + const { required, style: widgetStyle } = widget; const fileInputRef = useRef(null); const [uploading, setUploading] = useState(false); @@ -25,8 +29,16 @@ export const ImageWidget: React.FC = ({ const rawImageUrl = value || widget.value || ""; const imageUrl = rawImageUrl ? getFullImageUrl(rawImageUrl) : ""; - // style์—์„œ width, height ์ œ๊ฑฐ (๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ ํฌ๊ธฐ ์‚ฌ์šฉ) - const filteredStyle = style ? { ...style, width: undefined, height: undefined } : {}; + // ๐Ÿ”ง ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ ์šฉ (props.size ์šฐ์„ , ์—†์œผ๋ฉด style์—์„œ ๊ฐ€์ ธ์˜ด) + const effectiveSize = size || (widget as any).size || {}; + const effectiveStyle = propStyle || widgetStyle || {}; + const containerStyle: React.CSSProperties = { + width: effectiveSize.width ? `${effectiveSize.width}px` : effectiveStyle?.width || "100%", + height: effectiveSize.height ? `${effectiveSize.height}px` : effectiveStyle?.height || "100%", + }; + + // style์—์„œ width, height ์ œ๊ฑฐ (๋‚ด๋ถ€ ์š”์†Œ์šฉ) + const filteredStyle = effectiveStyle ? { ...effectiveStyle, width: undefined, height: undefined } : {}; // ํŒŒ์ผ ์„ ํƒ ์ฒ˜๋ฆฌ const handleFileSelect = () => { @@ -120,31 +132,27 @@ export const ImageWidget: React.FC = ({ }; return ( -
+
{imageUrl ? ( // ์ด๋ฏธ์ง€ ํ‘œ์‹œ ๋ชจ๋“œ
- ์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€ { - e.currentTarget.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Crect width='100' height='100' fill='%23f3f4f6'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' font-family='sans-serif' font-size='14' fill='%239ca3af'%3E์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ%3C/text%3E%3C/svg%3E"; - }} - /> - + ์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€ { + e.currentTarget.src = + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Crect width='100' height='100' fill='%23f3f4f6'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' font-family='sans-serif' font-size='14' fill='%239ca3af'%3E์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ%3C/text%3E%3C/svg%3E"; + }} + /> + {/* ํ˜ธ๋ฒ„ ์‹œ ์ œ๊ฑฐ ๋ฒ„ํŠผ */} {!readonly && !isDesignMode && (
- @@ -154,9 +162,9 @@ export const ImageWidget: React.FC = ({ ) : ( // ์—…๋กœ๋“œ ์˜์—ญ
= ({ /> {/* ํ•„์ˆ˜ ํ•„๋“œ ๊ฒฝ๊ณ  */} - {required && !imageUrl && ( -
* ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค
- )} + {required && !imageUrl &&
* ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค
}
); }; diff --git a/frontend/components/ui/select.tsx b/frontend/components/ui/select.tsx index 81e90fd3..64fef9c4 100644 --- a/frontend/components/ui/select.tsx +++ b/frontend/components/ui/select.tsx @@ -22,18 +22,25 @@ function SelectTrigger({ className, size = "xs", children, + style, ...props }: React.ComponentProps & { size?: "xs" | "sm" | "default"; }) { + // className์— h-full/h-[ ๋˜๋Š” style.height๊ฐ€ ์žˆ์œผ๋ฉด data-size ๋†’์ด๋ฅผ ๋ฌด์‹œ + const hasCustomHeight = className?.includes("h-full") || className?.includes("h-[") || !!style?.height; + return ( {children} diff --git a/frontend/components/unified/UnifiedRepeater.tsx b/frontend/components/unified/UnifiedRepeater.tsx index c604e465..2f521665 100644 --- a/frontend/components/unified/UnifiedRepeater.tsx +++ b/frontend/components/unified/UnifiedRepeater.tsx @@ -8,6 +8,11 @@ * - modal: ์—”ํ‹ฐํ‹ฐ ์„ ํƒ (FK ์ €์žฅ) + ์ถ”๊ฐ€ ์ž…๋ ฅ ์ปฌ๋Ÿผ * * RepeaterTable ๋ฐ ItemSelectionModal ์žฌ์‚ฌ์šฉ + * + * ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ธํ„ฐํŽ˜์ด์Šค: + * - DataProvidable: ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ œ๊ณต + * - DataReceivable: ์™ธ๋ถ€์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์‹  + * - repeaterDataChange ์ด๋ฒคํŠธ ๋ฐœํ–‰ */ import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"; @@ -29,6 +34,13 @@ import { RepeaterTable } from "@/lib/registry/components/modal-repeater-table/Re import { ItemSelectionModal } from "@/lib/registry/components/modal-repeater-table/ItemSelectionModal"; import { RepeaterColumnConfig } from "@/lib/registry/components/modal-repeater-table/types"; +// ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ธํ„ฐํŽ˜์ด์Šค +import type { DataProvidable, DataReceivable, DataReceiverConfig } from "@/types/data-transfer"; +import { useScreenContextOptional } from "@/contexts/ScreenContext"; + +// V2 ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ +import { V2_EVENTS, dispatchV2Event } from "@/types/component-events"; + // ์ „์—ญ UnifiedRepeater ๋“ฑ๋ก (buttonActions์—์„œ ์‚ฌ์šฉ) declare global { interface Window { @@ -56,6 +68,9 @@ export const UnifiedRepeater: React.FC = ({ [propConfig], ); + // ScreenContext (๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ธํ„ฐํŽ˜์ด์Šค ๋“ฑ๋ก์šฉ) + const screenContext = useScreenContextOptional(); + // ์ƒํƒœ const [data, setData] = useState(initialData || []); const [selectedRows, setSelectedRows] = useState>(new Set()); @@ -105,6 +120,133 @@ export const UnifiedRepeater: React.FC = ({ }; }, [config.useCustomTable, config.mainTableName, config.dataSource?.tableName]); + // ============================================================ + // DataProvidable ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ + // ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ด ๋ฆฌํ”ผํ„ฐ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ๊ฐˆ ์ˆ˜ ์žˆ๊ฒŒ ํ•จ + // ============================================================ + const dataProvider: DataProvidable = useMemo( + () => ({ + componentId: parentId || config.fieldName || "unified-repeater", + componentType: "unified-repeater", + + // ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + getSelectedData: () => { + return Array.from(selectedRows) + .map((idx) => data[idx]) + .filter(Boolean); + }, + + // ์ „์ฒด ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + getAllData: () => { + return [...data]; + }, + + // ์„ ํƒ ์ดˆ๊ธฐํ™” + clearSelection: () => { + setSelectedRows(new Set()); + }, + }), + [parentId, config.fieldName, data, selectedRows], + ); + + // ============================================================ + // DataReceivable ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ + // ์™ธ๋ถ€์—์„œ ์ด ๋ฆฌํ”ผํ„ฐ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌ๋ฐ›์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•จ + // ============================================================ + const dataReceiver: DataReceivable = useMemo( + () => ({ + componentId: parentId || config.fieldName || "unified-repeater", + componentType: "repeater", + + // ๋ฐ์ดํ„ฐ ์ˆ˜์‹  (append, replace, merge ๋ชจ๋“œ ์ง€์›) + receiveData: async (incomingData: any[], receiverConfig: DataReceiverConfig) => { + if (!incomingData || incomingData.length === 0) return; + + // ๋งคํ•‘ ๊ทœ์น™ ์ ์šฉ + const mappedData = incomingData.map((item, index) => { + const newRow: any = { _id: `received_${Date.now()}_${index}` }; + + if (receiverConfig.mappingRules && receiverConfig.mappingRules.length > 0) { + receiverConfig.mappingRules.forEach((rule) => { + const sourceValue = item[rule.sourceField]; + newRow[rule.targetField] = sourceValue !== undefined ? sourceValue : rule.defaultValue; + }); + } else { + // ๋งคํ•‘ ๊ทœ์น™ ์—†์œผ๋ฉด ๊ทธ๋Œ€๋กœ ๋ณต์‚ฌ + Object.assign(newRow, item); + } + + return newRow; + }); + + // ๋ชจ๋“œ์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ + switch (receiverConfig.mode) { + case "replace": + setData(mappedData); + onDataChange?.(mappedData); + break; + case "merge": + // ์ค‘๋ณต ์ œ๊ฑฐ ํ›„ ๋ณ‘ํ•ฉ (id ๋˜๋Š” _id ๊ธฐ์ค€) + const existingIds = new Set(data.map((row) => row.id || row._id)); + const newItems = mappedData.filter((row) => !existingIds.has(row.id || row._id)); + const mergedData = [...data, ...newItems]; + setData(mergedData); + onDataChange?.(mergedData); + break; + case "append": + default: + const appendedData = [...data, ...mappedData]; + setData(appendedData); + onDataChange?.(appendedData); + break; + } + }, + + // ํ˜„์žฌ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + getData: () => { + return [...data]; + }, + }), + [parentId, config.fieldName, data, onDataChange], + ); + + // ============================================================ + // ScreenContext์— DataProvider/DataReceiver ๋“ฑ๋ก + // ============================================================ + useEffect(() => { + if (screenContext && (parentId || config.fieldName)) { + const componentId = parentId || config.fieldName || "unified-repeater"; + + screenContext.registerDataProvider(componentId, dataProvider); + screenContext.registerDataReceiver(componentId, dataReceiver); + + return () => { + screenContext.unregisterDataProvider(componentId); + screenContext.unregisterDataReceiver(componentId); + }; + } + }, [screenContext, parentId, config.fieldName, dataProvider, dataReceiver]); + + // ============================================================ + // repeaterDataChange ์ด๋ฒคํŠธ ๋ฐœํ–‰ + // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ(aggregation-widget ๋“ฑ)์— ์•Œ๋ฆผ + // ============================================================ + const prevDataLengthRef = useRef(data.length); + useEffect(() => { + // ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ๋งŒ ์ด๋ฒคํŠธ ๋ฐœํ–‰ + if (typeof window !== "undefined" && data.length !== prevDataLengthRef.current) { + dispatchV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, { + componentId: parentId || config.fieldName || "unified-repeater", + tableName: config.dataSource?.tableName || "", + data: data, + selectedData: Array.from(selectedRows) + .map((idx) => data[idx]) + .filter(Boolean), + }); + prevDataLengthRef.current = data.length; + } + }, [data, selectedRows, parentId, config.fieldName, config.dataSource?.tableName]); + // ์ €์žฅ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ useEffect(() => { const handleSaveEvent = async (event: CustomEvent) => { @@ -568,19 +710,23 @@ export const UnifiedRepeater: React.FC = ({ ); // ๐Ÿ†• ์ฑ„๋ฒˆ API ํ˜ธ์ถœ (๋น„๋™๊ธฐ) - const generateNumberingCode = useCallback(async (ruleId: string): Promise => { - try { - const result = await allocateNumberingCode(ruleId); - if (result.success && result.data?.generatedCode) { - return result.data.generatedCode; + // ๐Ÿ†• ์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ง€์›์„ ์œ„ํ•ด userInputCode ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ + const generateNumberingCode = useCallback( + async (ruleId: string, userInputCode?: string, formData?: Record): Promise => { + try { + const result = await allocateNumberingCode(ruleId, userInputCode, formData); + if (result.success && result.data?.generatedCode) { + return result.data.generatedCode; + } + console.error("์ฑ„๋ฒˆ ์‹คํŒจ:", result.error); + return ""; + } catch (error) { + console.error("์ฑ„๋ฒˆ API ํ˜ธ์ถœ ์‹คํŒจ:", error); + return ""; } - console.error("์ฑ„๋ฒˆ ์‹คํŒจ:", result.error); - return ""; - } catch (error) { - console.error("์ฑ„๋ฒˆ API ํ˜ธ์ถœ ์‹คํŒจ:", error); - return ""; - } - }, []); + }, + [], + ); // ๐Ÿ†• ํ–‰ ์ถ”๊ฐ€ (inline ๋ชจ๋“œ ๋˜๋Š” ๋ชจ๋‹ฌ ์—ด๊ธฐ) - ๋น„๋™๊ธฐ๋กœ ๋ณ€๊ฒฝ const handleAddRow = useCallback(async () => { @@ -699,7 +845,8 @@ export const UnifiedRepeater: React.FC = ({ if (match) { const ruleId = match[1]; try { - const result = await allocateNumberingCode(ruleId); + // ๐Ÿ†• ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘ํ•œ ๊ฐ’์„ ์ „๋‹ฌ (์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ถ”์ถœ์šฉ) + const result = await allocateNumberingCode(ruleId, undefined, newRow); if (result.success && result.data?.generatedCode) { newRow[key] = result.data.generatedCode; } else { diff --git a/frontend/components/unified/UnifiedSelect.tsx b/frontend/components/unified/UnifiedSelect.tsx index 99a82e17..9560fa38 100644 --- a/frontend/components/unified/UnifiedSelect.tsx +++ b/frontend/components/unified/UnifiedSelect.tsx @@ -492,7 +492,7 @@ export const UnifiedSelect = forwardRef((pro const categoryTable = (config as any).categoryTable; const categoryColumn = (config as any).categoryColumn; - // category ์†Œ์Šค ์œ ์ง€ (category_values_test ํ…Œ์ด๋ธ”์—์„œ ๋กœ๋“œ) + // category ์†Œ์Šค ์œ ์ง€ (category_values ํ…Œ์ด๋ธ”์—์„œ ๋กœ๋“œ) const source = rawSource; const codeGroup = config.codeGroup; @@ -612,7 +612,7 @@ export const UnifiedSelect = forwardRef((pro fetchedOptions = data; } } else if (source === "category") { - // ์นดํ…Œ๊ณ ๋ฆฌ์—์„œ ๋กœ๋“œ (category_values_test ํ…Œ์ด๋ธ”) + // ์นดํ…Œ๊ณ ๋ฆฌ์—์„œ ๋กœ๋“œ (category_values ํ…Œ์ด๋ธ”) // tableName, columnName์€ props์—์„œ ๊ฐ€์ ธ์˜ด const catTable = categoryTable || tableName; const catColumn = categoryColumn || columnName; diff --git a/frontend/components/v2/V2Biz.tsx b/frontend/components/v2/V2Biz.tsx index 2ce97050..4f4bec36 100644 --- a/frontend/components/v2/V2Biz.tsx +++ b/frontend/components/v2/V2Biz.tsx @@ -311,31 +311,39 @@ export const V2Biz = forwardRef( const componentWidth = size?.width || style?.width; const componentHeight = size?.height || style?.height; + // ๋ผ๋ฒจ ๋†’์ด ๊ณ„์‚ฐ (๊ธฐ๋ณธ 20px, ์‚ฌ์šฉ์ž ์„ค์ •์— ๋”ฐ๋ผ ์กฐ์ •) + const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14; + const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4; + const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2; + return (
+ {/* ๐Ÿ”ง ๋ผ๋ฒจ์„ absolute๋กœ ์ปดํฌ๋„ŒํŠธ ์œ„์— ๋ฐฐ์น˜ */} {showLabel && ( )} -
+
{renderBiz()}
diff --git a/frontend/components/v2/V2Date.tsx b/frontend/components/v2/V2Date.tsx index c825b1bf..91a4cf5d 100644 --- a/frontend/components/v2/V2Date.tsx +++ b/frontend/components/v2/V2Date.tsx @@ -75,10 +75,11 @@ const SingleDatePicker = forwardRef< disabled?: boolean; readonly?: boolean; className?: string; + placeholder?: string; } >( ( - { value, onChange, dateFormat = "YYYY-MM-DD", showToday = true, minDate, maxDate, disabled, readonly, className }, + { value, onChange, dateFormat = "YYYY-MM-DD", showToday = true, minDate, maxDate, disabled, readonly, className, placeholder = "๋‚ ์งœ ์„ ํƒ" }, ref, ) => { const [open, setOpen] = useState(false); @@ -87,6 +88,16 @@ const SingleDatePicker = forwardRef< const minDateObj = useMemo(() => parseDate(minDate, dateFormat), [minDate, dateFormat]); const maxDateObj = useMemo(() => parseDate(maxDate, dateFormat), [maxDate, dateFormat]); + // ํ‘œ์‹œํ•  ๋‚ ์งœ ํ…์ŠคํŠธ ๊ณ„์‚ฐ (ISO ํ˜•์‹์ด๋ฉด ํฌ๋งทํŒ…, ์•„๋‹ˆ๋ฉด ๊ทธ๋Œ€๋กœ) + const displayText = useMemo(() => { + if (!value) return ""; + // Date ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ ํ›„ ํฌ๋งทํŒ… + if (date && isValid(date)) { + return formatDate(date, dateFormat); + } + return value; + }, [value, date, dateFormat]); + const handleSelect = useCallback( (selectedDate: Date | undefined) => { if (selectedDate) { @@ -115,13 +126,13 @@ const SingleDatePicker = forwardRef< variant="outline" disabled={disabled || readonly} className={cn( - "h-10 w-full justify-start text-left font-normal", - !value && "text-muted-foreground", + "h-full w-full justify-start text-left font-normal", + !displayText && "text-muted-foreground", className, )} > - - {value || "๋‚ ์งœ ์„ ํƒ"} + + {displayText || placeholder} @@ -211,14 +222,14 @@ const RangeDatePicker = forwardRef< ); return ( -
+
{/* ์‹œ์ž‘ ๋‚ ์งœ */} -
- ))} -
- )} -
- ); -}); -FileUploader.displayName = "FileUploader"; - -/** - * ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ/ํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ - */ -const ImageUploader = forwardRef void; - multiple?: boolean; - accept?: string; - maxSize?: number; - preview?: boolean; - disabled?: boolean; - uploadEndpoint?: string; - className?: string; -}>(({ - value, - onChange, - multiple = false, - accept = "image/*", - maxSize = 10485760, - preview = true, - disabled, - uploadEndpoint = "/api/upload", - className -}, ref) => { - const inputRef = useRef(null); - const [isDragging, setIsDragging] = useState(false); - const [isUploading, setIsUploading] = useState(false); - const [previewUrl, setPreviewUrl] = useState(null); - - const images = Array.isArray(value) ? value : value ? [value] : []; - - // ํŒŒ์ผ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ - const handleFileSelect = useCallback(async (selectedFiles: FileList | null) => { - if (!selectedFiles || selectedFiles.length === 0) return; - - setIsUploading(true); - - try { - const fileArray = Array.from(selectedFiles); - const uploadedUrls: string[] = []; - - for (const file of fileArray) { - // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ƒ์„ฑ - if (preview) { - const reader = new FileReader(); - reader.onload = () => setPreviewUrl(reader.result as string); - reader.readAsDataURL(file); - } - - const formData = new FormData(); - formData.append("file", file); - - const response = await fetch(uploadEndpoint, { - method: "POST", - body: formData, - }); - - if (response.ok) { - const data = await response.json(); - if (data.success && data.url) { - uploadedUrls.push(data.url); - } else if (data.filePath) { - uploadedUrls.push(data.filePath); - } - } - } - - if (multiple) { - onChange?.([...images, ...uploadedUrls]); - } else { - onChange?.(uploadedUrls[0] || ""); - } - } finally { - setIsUploading(false); - setPreviewUrl(null); - } - }, [images, multiple, preview, uploadEndpoint, onChange]); - - // ์ด๋ฏธ์ง€ ์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ - const handleRemove = useCallback((index: number) => { - const newImages = images.filter((_, i) => i !== index); - onChange?.(multiple ? newImages : ""); - }, [images, multiple, onChange]); - - return ( -
- {/* ์ด๋ฏธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ */} - {preview && images.length > 0 && ( -
- {images.map((src, index) => ( -
- {`์ด๋ฏธ์ง€ -
- - -
-
- ))} -
- )} - - {/* ์—…๋กœ๋“œ ๋ฒ„ํŠผ */} - {(!images.length || multiple) && ( -
!disabled && inputRef.current?.click()} - onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }} - onDragLeave={(e) => { e.preventDefault(); setIsDragging(false); }} - onDrop={(e) => { e.preventDefault(); setIsDragging(false); handleFileSelect(e.dataTransfer.files); }} - > - handleFileSelect(e.target.files)} - className="hidden" - /> - - {isUploading ? ( -
-
- ์—…๋กœ๋“œ ์ค‘... -
- ) : ( -
- - - ์ด๋ฏธ์ง€ {multiple ? "์ถ”๊ฐ€" : "์„ ํƒ"} - -
- )} -
- )} -
- ); -}); -ImageUploader.displayName = "ImageUploader"; - -/** - * ๋น„๋””์˜ค ์ปดํฌ๋„ŒํŠธ - */ -const VideoPlayer = forwardRef(({ value, className }, ref) => { - if (!value) { - return ( -
-
- ); + if (["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(ext)) { + return ; + } + if (["mp4", "avi", "mov", "wmv", "flv", "webm"].includes(ext)) { + return ; + } + if (["mp3", "wav", "flac", "aac", "ogg"].includes(ext)) { + return ; + } + if (["pdf"].includes(ext)) { + return ; + } + if (["doc", "docx", "hwp", "hwpx", "pages"].includes(ext)) { + return ; + } + if (["xls", "xlsx", "hcell", "numbers"].includes(ext)) { + return ; + } + if (["ppt", "pptx", "hanshow", "keynote"].includes(ext)) { + return ; + } + if (["zip", "rar", "7z", "tar", "gz"].includes(ext)) { + return ; } - return ( -
-
- ); -}); -VideoPlayer.displayName = "VideoPlayer"; + return ; +}; /** - * ์˜ค๋””์˜ค ์ปดํฌ๋„ŒํŠธ - */ -const AudioPlayer = forwardRef(({ value, className }, ref) => { - if (!value) { - return ( -
- -
- ); - } - - return ( -
-
- ); -}); -AudioPlayer.displayName = "AudioPlayer"; - -/** - * ๋ฉ”์ธ V2Media ์ปดํฌ๋„ŒํŠธ + * V2 ๋ฏธ๋””์–ด ์ปดํฌ๋„ŒํŠธ (๋ ˆ๊ฑฐ์‹œ ๊ธฐ๋Šฅ ํ†ตํ•ฉ) */ export const V2Media = forwardRef( (props, ref) => { @@ -469,67 +90,676 @@ export const V2Media = forwardRef( config: configProp, value, onChange, + formData, + columnName, + tableName, + onFormDataChange, + isDesignMode = false, + isInteractive = true, + onUpdate, + ...restProps } = props; - // config๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ - const config = configProp || { type: "image" as const }; + // ์ธ์ฆ ์ •๋ณด + const { user } = useAuth(); - // ํƒ€์ž…๋ณ„ ๋ฏธ๋””์–ด ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง - const renderMedia = () => { - const isDisabled = disabled || readonly; - const mediaType = config.type || "image"; + // config ๊ธฐ๋ณธ๊ฐ’ + const config = configProp || { type: "file" as const }; + const mediaType = config.type || "file"; - switch (mediaType) { - case "file": - return ( - - ); + // ํŒŒ์ผ ์ƒํƒœ + const [uploadedFiles, setUploadedFiles] = useState([]); + const [uploadStatus, setUploadStatus] = useState<"idle" | "uploading" | "success" | "error">("idle"); + const [dragOver, setDragOver] = useState(false); + const [representativeImageUrl, setRepresentativeImageUrl] = useState(null); + + // ๋ชจ๋‹ฌ ์ƒํƒœ + const [viewerFile, setViewerFile] = useState(null); + const [isViewerOpen, setIsViewerOpen] = useState(false); + const [isFileManagerOpen, setIsFileManagerOpen] = useState(false); + + const fileInputRef = useRef(null); - case "image": - return ( - - ); + // ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ ํŒ๋‹จ + const isRecordMode = !!(formData?.id && !String(formData.id).startsWith('temp_')); + const recordTableName = formData?.tableName || tableName; + const recordId = formData?.id; + // ๐Ÿ”‘ columnName ์šฐ์„  ์‚ฌ์šฉ (์‹ค์ œ DB ์ปฌ๋Ÿผ๋ช…), ์—†์œผ๋ฉด id, ์ตœํ›„์— attachments + const effectiveColumnName = columnName || id || 'attachments'; - case "video": - return ( - - ); - - case "audio": - return ( - - ); - - default: - return ( - - ); + // ๋ ˆ์ฝ”๋“œ์šฉ targetObjid ์ƒ์„ฑ + const getRecordTargetObjid = useCallback(() => { + if (isRecordMode && recordTableName && recordId) { + return `${recordTableName}:${recordId}:${effectiveColumnName}`; } + return null; + }, [isRecordMode, recordTableName, recordId, effectiveColumnName]); + + // ๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์ƒ์„ฑ + const getUniqueKey = useCallback(() => { + if (isRecordMode && recordTableName && recordId) { + return `v2media_${recordTableName}_${recordId}_${id}`; + } + return `v2media_${id}`; + }, [isRecordMode, recordTableName, recordId, id]); + + // ๋ ˆ์ฝ”๋“œ ID ๋ณ€๊ฒฝ ์‹œ ํŒŒ์ผ ๋ชฉ๋ก ์ดˆ๊ธฐํ™” + const prevRecordIdRef = useRef(null); + useEffect(() => { + if (prevRecordIdRef.current !== recordId) { + prevRecordIdRef.current = recordId; + if (isRecordMode) { + setUploadedFiles([]); + } + } + }, [recordId, isRecordMode]); + + // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ localStorage์—์„œ ํŒŒ์ผ ๋ณต์› + useEffect(() => { + if (!id) return; + + try { + const backupKey = getUniqueKey(); + const backupFiles = localStorage.getItem(backupKey); + if (backupFiles) { + const parsedFiles = JSON.parse(backupFiles); + if (parsedFiles.length > 0) { + setUploadedFiles(parsedFiles); + + if (typeof window !== "undefined") { + (window as any).globalFileState = { + ...(window as any).globalFileState, + [backupKey]: parsedFiles, + }; + } + } + } + } catch (e) { + console.warn("ํŒŒ์ผ ๋ณต์› ์‹คํŒจ:", e); + } + }, [id, getUniqueKey, recordId]); + + // DB์—์„œ ํŒŒ์ผ ๋ชฉ๋ก ๋กœ๋“œ + const loadComponentFiles = useCallback(async () => { + if (!id) return false; + + try { + let screenId = formData?.screenId; + + if (!screenId && typeof window !== "undefined") { + const pathname = window.location.pathname; + const screenMatch = pathname.match(/\/screens\/(\d+)/); + if (screenMatch) { + screenId = parseInt(screenMatch[1]); + } + } + + if (!screenId && isDesignMode) { + screenId = 999999; + } + + if (!screenId) { + screenId = 0; + } + + const params = { + screenId, + componentId: id, + tableName: recordTableName || formData?.tableName || tableName, + recordId: recordId || formData?.id, + columnName: effectiveColumnName, + }; + + const response = await getComponentFiles(params); + + if (response.success) { + const formattedFiles = response.totalFiles.map((file: any) => ({ + objid: file.objid || file.id, + savedFileName: file.savedFileName || file.saved_file_name, + realFileName: file.realFileName || file.real_file_name, + fileSize: file.fileSize || file.file_size, + fileExt: file.fileExt || file.file_ext, + regdate: file.regdate, + status: file.status || "ACTIVE", + uploadedAt: file.uploadedAt || new Date().toISOString(), + targetObjid: file.targetObjid || file.target_objid, + filePath: file.filePath || file.file_path, + ...file, + })); + + // localStorage์™€ ๋ณ‘ํ•ฉ + let finalFiles = formattedFiles; + const uniqueKey = getUniqueKey(); + try { + const backupFiles = localStorage.getItem(uniqueKey); + if (backupFiles) { + const parsedBackupFiles = JSON.parse(backupFiles); + const serverObjIds = new Set(formattedFiles.map((f: any) => f.objid)); + const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid)); + finalFiles = [...formattedFiles, ...additionalFiles]; + } + } catch (e) { + console.warn("ํŒŒ์ผ ๋ณ‘ํ•ฉ ์˜ค๋ฅ˜:", e); + } + + setUploadedFiles(finalFiles); + + if (typeof window !== "undefined") { + (window as any).globalFileState = { + ...(window as any).globalFileState, + [uniqueKey]: finalFiles, + }; + + GlobalFileManager.registerFiles(finalFiles, { + uploadPage: window.location.pathname, + componentId: id, + screenId: formData?.screenId, + recordId: recordId, + }); + + try { + localStorage.setItem(uniqueKey, JSON.stringify(finalFiles)); + } catch (e) { + console.warn("localStorage ๋ฐฑ์—… ์‹คํŒจ:", e); + } + } + return true; + } + } catch (error) { + console.error("ํŒŒ์ผ ์กฐํšŒ ์˜ค๋ฅ˜:", error); + } + return false; + }, [id, tableName, columnName, formData?.screenId, formData?.tableName, formData?.id, getUniqueKey, recordId, isRecordMode, recordTableName, effectiveColumnName, isDesignMode]); + + // ํŒŒ์ผ ๋™๊ธฐํ™” + useEffect(() => { + loadComponentFiles(); + }, [loadComponentFiles]); + + // ์ „์—ญ ์ƒํƒœ ๋ณ€๊ฒฝ ๊ฐ์ง€ + useEffect(() => { + const handleGlobalFileStateChange = (event: CustomEvent) => { + const { componentId, files, isRestore } = event.detail; + + if (componentId === id) { + setUploadedFiles(files); + + try { + const backupKey = getUniqueKey(); + localStorage.setItem(backupKey, JSON.stringify(files)); + } catch (e) { + console.warn("localStorage ๋ฐฑ์—… ์‹คํŒจ:", e); + } + } + }; + + if (typeof window !== "undefined") { + window.addEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener); + return () => { + window.removeEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener); + }; + } + }, [id, getUniqueKey]); + + // ํŒŒ์ผ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ + const handleFileUpload = useCallback( + async (files: File[]) => { + if (!files.length) return; + + // ์ค‘๋ณต ์ฒดํฌ + const existingFileNames = uploadedFiles.map((f) => f.realFileName.toLowerCase()); + const duplicates: string[] = []; + const uniqueFiles: File[] = []; + + files.forEach((file) => { + const fileName = file.name.toLowerCase(); + if (existingFileNames.includes(fileName)) { + duplicates.push(file.name); + } else { + uniqueFiles.push(file); + } + }); + + if (duplicates.length > 0) { + toast.error(`์ค‘๋ณต๋œ ํŒŒ์ผ: ${duplicates.join(", ")}`); + if (uniqueFiles.length === 0) return; + toast.info(`${uniqueFiles.length}๊ฐœ์˜ ์ƒˆ๋กœ์šด ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.`); + } + + const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : files; + setUploadStatus("uploading"); + toast.loading("ํŒŒ์ผ ์—…๋กœ๋“œ ์ค‘...", { id: "file-upload" }); + + try { + const effectiveTableName = recordTableName || formData?.tableName || tableName || "default_table"; + const effectiveRecordId = recordId || formData?.id; + + let screenId = formData?.screenId; + if (!screenId && typeof window !== "undefined") { + const pathname = window.location.pathname; + const screenMatch = pathname.match(/\/screens\/(\d+)/); + if (screenMatch) { + screenId = parseInt(screenMatch[1]); + } + } + + let targetObjid; + const effectiveIsRecordMode = isRecordMode || (effectiveRecordId && !String(effectiveRecordId).startsWith('temp_')); + + if (effectiveIsRecordMode && effectiveTableName && effectiveRecordId) { + targetObjid = `${effectiveTableName}:${effectiveRecordId}:${effectiveColumnName}`; + } else if (screenId) { + targetObjid = `screen_files:${screenId}:${id}:${effectiveColumnName}`; + } else { + targetObjid = `temp_${id}`; + } + + const userCompanyCode = user?.companyCode || (window as any).__user__?.companyCode; + + const finalLinkedTable = effectiveIsRecordMode + ? effectiveTableName + : (formData?.linkedTable || effectiveTableName); + + const uploadData = { + autoLink: formData?.autoLink || true, + linkedTable: finalLinkedTable, + recordId: effectiveRecordId || `temp_${id}`, + columnName: effectiveColumnName, + isVirtualFileColumn: formData?.isVirtualFileColumn || true, + docType: config?.docType || "DOCUMENT", + docTypeName: config?.docTypeName || "์ผ๋ฐ˜ ๋ฌธ์„œ", + companyCode: userCompanyCode, + tableName: effectiveTableName, + fieldName: effectiveColumnName, + targetObjid: targetObjid, + isRecordMode: effectiveIsRecordMode, + }; + + const response = await uploadFiles({ + files: filesToUpload, + ...uploadData, + }); + + if (response.success) { + const fileData = response.files || (response as any).data || []; + + if (fileData.length === 0) { + throw new Error("์—…๋กœ๋“œ๋œ ํŒŒ์ผ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + + const newFiles = fileData.map((file: any) => ({ + objid: file.objid || file.id, + savedFileName: file.saved_file_name || file.savedFileName, + realFileName: file.real_file_name || file.realFileName || file.name, + fileSize: file.file_size || file.fileSize || file.size, + fileExt: file.file_ext || file.fileExt || file.extension, + filePath: file.file_path || file.filePath || file.path, + docType: file.doc_type || file.docType, + docTypeName: file.doc_type_name || file.docTypeName, + targetObjid: file.target_objid || file.targetObjid, + parentTargetObjid: file.parent_target_objid || file.parentTargetObjid, + companyCode: file.company_code || file.companyCode, + writer: file.writer, + regdate: file.regdate, + status: file.status || "ACTIVE", + uploadedAt: new Date().toISOString(), + ...file, + })); + + const updatedFiles = [...uploadedFiles, ...newFiles]; + setUploadedFiles(updatedFiles); + setUploadStatus("success"); + + // localStorage ๋ฐฑ์—… + try { + const backupKey = getUniqueKey(); + localStorage.setItem(backupKey, JSON.stringify(updatedFiles)); + } catch (e) { + console.warn("localStorage ๋ฐฑ์—… ์‹คํŒจ:", e); + } + + // ์ „์—ญ ์ƒํƒœ ์—…๋ฐ์ดํŠธ + if (typeof window !== "undefined") { + const globalFileState = (window as any).globalFileState || {}; + const uniqueKey = getUniqueKey(); + globalFileState[uniqueKey] = updatedFiles; + (window as any).globalFileState = globalFileState; + + GlobalFileManager.registerFiles(newFiles, { + uploadPage: window.location.pathname, + componentId: id, + screenId: formData?.screenId, + recordId: recordId, + }); + + const syncEvent = new CustomEvent("globalFileStateChanged", { + detail: { + componentId: id, + uniqueKey: uniqueKey, + recordId: recordId, + files: updatedFiles, + fileCount: updatedFiles.length, + timestamp: Date.now(), + }, + }); + window.dispatchEvent(syncEvent); + } + + // ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ + if (onUpdate) { + onUpdate({ + uploadedFiles: updatedFiles, + lastFileUpdate: Date.now(), + }); + } + + // onChange ์ฝœ๋ฐฑ (objid ๋ฐฐ์—ด ๋˜๋Š” ๋‹จ์ผ ๊ฐ’) + const fileIds = updatedFiles.map((f) => f.objid); + const finalValue = config.multiple ? fileIds : fileIds[0] || ""; + const targetColumn = columnName || effectiveColumnName; + + console.log("๐Ÿ“ค [V2Media] ํŒŒ์ผ ์—…๋กœ๋“œ ์™„๋ฃŒ - ๊ฐ’ ์ „๋‹ฌ:", { + columnName: targetColumn, + fileIds, + finalValue, + hasOnChange: !!onChange, + hasOnFormDataChange: !!onFormDataChange, + }); + + if (onChange) { + onChange(finalValue); + } + + // ํผ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ - ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ ์‹œ๊ทธ๋‹ˆ์ฒ˜์— ๋งž๊ฒŒ (fieldName, value) ํ˜•์‹ + if (onFormDataChange && targetColumn) { + // ๐Ÿ”‘ ๋‹จ์ผ ํŒŒ์ผ: ์ฒซ ๋ฒˆ์งธ objid๋งŒ ์ „๋‹ฌ (DB ์ปฌ๋Ÿผ์— ์ €์žฅ๋  ๊ฐ’) + // ๋ณต์ˆ˜ ํŒŒ์ผ: ์ฝค๋งˆ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ์ „๋‹ฌ + const formValue = config.multiple + ? fileIds.join(',') + : (fileIds[0] || ''); + + console.log("๐Ÿ“ [V2Media] formData ์—…๋ฐ์ดํŠธ:", { + columnName: targetColumn, + fileIds, + formValue, + isMultiple: config.multiple, + isRecordMode: effectiveIsRecordMode, + }); + // (fieldName: string, value: any) ํ˜•์‹์œผ๋กœ ํ˜ธ์ถœ + onFormDataChange(targetColumn, formValue); + } + + // ๊ทธ๋ฆฌ๋“œ ํŒŒ์ผ ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + if (typeof window !== "undefined") { + const refreshEvent = new CustomEvent("refreshFileStatus", { + detail: { + tableName: effectiveTableName, + recordId: effectiveRecordId, + columnName: targetColumn, + targetObjid: targetObjid, + fileCount: updatedFiles.length, + }, + }); + window.dispatchEvent(refreshEvent); + } + + toast.dismiss("file-upload"); + toast.success(`${newFiles.length}๊ฐœ ํŒŒ์ผ ์—…๋กœ๋“œ ์™„๋ฃŒ`); + } else { + throw new Error(response.message || (response as any).error || "ํŒŒ์ผ ์—…๋กœ๋“œ ์‹คํŒจ"); + } + } catch (error) { + console.error("ํŒŒ์ผ ์—…๋กœ๋“œ ์˜ค๋ฅ˜:", error); + setUploadStatus("error"); + toast.dismiss("file-upload"); + toast.error(`์—…๋กœ๋“œ ์˜ค๋ฅ˜: ${error instanceof Error ? error.message : "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"}`); + } + }, + [config, uploadedFiles, onChange, id, getUniqueKey, recordId, isRecordMode, recordTableName, effectiveColumnName, tableName, onUpdate, onFormDataChange, user, columnName], + ); + + // ํŒŒ์ผ ๋ทฐ์–ด ์—ด๊ธฐ/๋‹ซ๊ธฐ + const handleFileView = useCallback((file: FileInfo) => { + setViewerFile(file); + setIsViewerOpen(true); + }, []); + + const handleViewerClose = useCallback(() => { + setIsViewerOpen(false); + setViewerFile(null); + }, []); + + // ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ + const handleFileDownload = useCallback(async (file: FileInfo) => { + try { + await downloadFile({ + fileId: file.objid, + serverFilename: file.savedFileName, + originalName: file.realFileName, + }); + toast.success(`${file.realFileName} ๋‹ค์šด๋กœ๋“œ ์™„๋ฃŒ`); + } catch (error) { + console.error("ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์˜ค๋ฅ˜:", error); + toast.error("ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์‹คํŒจ"); + } + }, []); + + // ํŒŒ์ผ ์‚ญ์ œ + const handleFileDelete = useCallback( + async (file: FileInfo | string) => { + try { + const fileId = typeof file === "string" ? file : file.objid; + const fileName = typeof file === "string" ? "ํŒŒ์ผ" : file.realFileName; + const serverFilename = typeof file === "string" ? "temp_file" : file.savedFileName; + + await deleteFile(fileId, serverFilename); + + const updatedFiles = uploadedFiles.filter((f) => f.objid !== fileId); + setUploadedFiles(updatedFiles); + + // localStorage ๋ฐฑ์—… + try { + const backupKey = getUniqueKey(); + localStorage.setItem(backupKey, JSON.stringify(updatedFiles)); + } catch (e) { + console.warn("localStorage ๋ฐฑ์—… ์‹คํŒจ:", e); + } + + // ์ „์—ญ ์ƒํƒœ ์—…๋ฐ์ดํŠธ + if (typeof window !== "undefined") { + const globalFileState = (window as any).globalFileState || {}; + const uniqueKey = getUniqueKey(); + globalFileState[uniqueKey] = updatedFiles; + (window as any).globalFileState = globalFileState; + + const syncEvent = new CustomEvent("globalFileStateChanged", { + detail: { + componentId: id, + uniqueKey: uniqueKey, + recordId: recordId, + files: updatedFiles, + fileCount: updatedFiles.length, + timestamp: Date.now(), + action: "delete", + }, + }); + window.dispatchEvent(syncEvent); + } + + if (onUpdate) { + onUpdate({ + uploadedFiles: updatedFiles, + lastFileUpdate: Date.now(), + }); + } + + // onChange ์ฝœ๋ฐฑ + const fileIds = updatedFiles.map((f) => f.objid); + const finalValue = config.multiple ? fileIds : fileIds[0] || ""; + const targetColumn = columnName || effectiveColumnName; + + console.log("๐Ÿ—‘๏ธ [V2Media] ํŒŒ์ผ ์‚ญ์ œ ์™„๋ฃŒ - ๊ฐ’ ์ „๋‹ฌ:", { + columnName: targetColumn, + fileIds, + finalValue, + }); + + if (onChange) { + onChange(finalValue); + } + + // ํผ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ - ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ ์‹œ๊ทธ๋‹ˆ์ฒ˜์— ๋งž๊ฒŒ (fieldName, value) ํ˜•์‹ + if (onFormDataChange && targetColumn) { + // ๐Ÿ”‘ ๋‹จ์ผ ํŒŒ์ผ: ์ฒซ ๋ฒˆ์งธ objid๋งŒ ์ „๋‹ฌ (DB ์ปฌ๋Ÿผ์— ์ €์žฅ๋  ๊ฐ’) + // ๋ณต์ˆ˜ ํŒŒ์ผ: ์ฝค๋งˆ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ์ „๋‹ฌ + const formValue = config.multiple + ? fileIds.join(',') + : (fileIds[0] || ''); + + console.log("๐Ÿ—‘๏ธ [V2Media] ์‚ญ์ œ ํ›„ formData ์—…๋ฐ์ดํŠธ:", { + columnName: targetColumn, + fileIds, + formValue, + }); + // (fieldName: string, value: any) ํ˜•์‹์œผ๋กœ ํ˜ธ์ถœ + onFormDataChange(targetColumn, formValue); + } + + toast.success(`${fileName} ์‚ญ์ œ ์™„๋ฃŒ`); + } catch (error) { + console.error("ํŒŒ์ผ ์‚ญ์ œ ์˜ค๋ฅ˜:", error); + toast.error("ํŒŒ์ผ ์‚ญ์ œ ์‹คํŒจ"); + } + }, + [uploadedFiles, onUpdate, id, isRecordMode, onFormDataChange, recordTableName, recordId, effectiveColumnName, getUniqueKey, onChange, config.multiple, columnName], + ); + + // ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ๋กœ๋“œ + const loadRepresentativeImage = useCallback( + async (file: FileInfo) => { + try { + const isImage = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes( + file.fileExt.toLowerCase().replace(".", "") + ); + + if (!isImage) { + setRepresentativeImageUrl(null); + return; + } + + if (!file.objid || file.objid === "0" || file.objid === "") { + setRepresentativeImageUrl(null); + return; + } + + const response = await apiClient.get(`/files/download/${file.objid}`, { + params: { serverFilename: file.savedFileName }, + responseType: "blob", + }); + + const blob = new Blob([response.data]); + const url = window.URL.createObjectURL(blob); + + if (representativeImageUrl) { + window.URL.revokeObjectURL(representativeImageUrl); + } + + setRepresentativeImageUrl(url); + } catch (error) { + console.error("๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ:", error); + setRepresentativeImageUrl(null); + } + }, + [representativeImageUrl], + ); + + // ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ์„ค์ • + const handleSetRepresentative = useCallback( + async (file: FileInfo) => { + try { + const { setRepresentativeFile } = await import("@/lib/api/file"); + await setRepresentativeFile(file.objid); + + const updatedFiles = uploadedFiles.map((f) => ({ + ...f, + isRepresentative: f.objid === file.objid, + })); + + setUploadedFiles(updatedFiles); + loadRepresentativeImage(file); + } catch (e) { + console.error("๋Œ€ํ‘œ ํŒŒ์ผ ์„ค์ • ์‹คํŒจ:", e); + } + }, + [uploadedFiles, loadRepresentativeImage] + ); + + // uploadedFiles ๋ณ€๊ฒฝ ์‹œ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ๋กœ๋“œ + useEffect(() => { + const representativeFile = uploadedFiles.find(f => f.isRepresentative) || uploadedFiles[0]; + if (representativeFile) { + loadRepresentativeImage(representativeFile); + } else { + setRepresentativeImageUrl(null); + } + + return () => { + if (representativeImageUrl) { + window.URL.revokeObjectURL(representativeImageUrl); + } + }; + }, [uploadedFiles]); + + // ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํ•ธ๋“ค๋Ÿฌ + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (!readonly && !disabled) { + setDragOver(true); + } + }, [readonly, disabled]); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDragOver(false); + }, []); + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDragOver(false); + + if (!readonly && !disabled) { + const files = Array.from(e.dataTransfer.files); + if (files.length > 0) { + handleFileUpload(files); + } + } + }, [readonly, disabled, handleFileUpload]); + + // ํŒŒ์ผ ์„ ํƒ + const handleFileSelect = useCallback(() => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }, []); + + const handleInputChange = useCallback((e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []); + if (files.length > 0) { + handleFileUpload(files); + } + e.target.value = ''; + }, [handleFileUpload]); + + // ํŒŒ์ผ ์„ค์ • + const fileConfig: FileUploadConfig = { + accept: config.accept || "*/*", + multiple: config.multiple || false, + maxSize: config.maxSize || 10 * 1024 * 1024, + disabled: disabled, + readonly: readonly, }; const showLabel = label && style?.labelDisplay !== false; @@ -540,12 +770,10 @@ export const V2Media = forwardRef(
+ {/* ๋ผ๋ฒจ */} {showLabel && ( )} -
- {renderMedia()} + + {/* ๋ฉ”์ธ ์ปจํ…Œ์ด๋„ˆ */} +
+
+ {/* ์ˆจ๊ฒจ์ง„ ํŒŒ์ผ ์ž…๋ ฅ */} + + + {/* ํŒŒ์ผ์ด ์žˆ๋Š” ๊ฒฝ์šฐ: ๋Œ€ํ‘œ ์ด๋ฏธ์ง€/ํŒŒ์ผ ํ‘œ์‹œ */} + {uploadedFiles.length > 0 ? (() => { + const representativeFile = uploadedFiles.find(f => f.isRepresentative) || uploadedFiles[0]; + const isImage = representativeFile && ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes( + representativeFile.fileExt.toLowerCase().replace(".", "") + ); + + return ( + <> + {isImage && representativeImageUrl ? ( +
+ {representativeFile.realFileName} +
+ ) : isImage && !representativeImageUrl ? ( +
+
+

์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ค‘...

+
+ ) : ( +
+ {getFileIcon(representativeFile.fileExt)} +

+ {representativeFile.realFileName} +

+ + ๋Œ€ํ‘œ ํŒŒ์ผ + +
+ )} + + {/* ์šฐ์ธก ํ•˜๋‹จ ์ž์„ธํžˆ๋ณด๊ธฐ ๋ฒ„ํŠผ */} +
+ +
+ + ); + })() : ( + // ํŒŒ์ผ์ด ์—†๋Š” ๊ฒฝ์šฐ: ์—…๋กœ๋“œ ์•ˆ๋‚ด +
!disabled && !readonly && handleFileSelect()} + > + +

ํŒŒ์ผ์„ ๋“œ๋ž˜๊ทธํ•˜๊ฑฐ๋‚˜ ํด๋ฆญํ•˜์„ธ์š”

+

+ ์ตœ๋Œ€ {formatFileSize(config.maxSize || 10 * 1024 * 1024)} + {config.accept && config.accept !== "*/*" && ` (${config.accept})`} +

+ +
+ )} +
+ + {/* ํŒŒ์ผ ๋ทฐ์–ด ๋ชจ๋‹ฌ */} + + + {/* ํŒŒ์ผ ๊ด€๋ฆฌ ๋ชจ๋‹ฌ */} + setIsFileManagerOpen(false)} + uploadedFiles={uploadedFiles} + onFileUpload={handleFileUpload} + onFileDownload={handleFileDownload} + onFileDelete={handleFileDelete} + onFileView={handleFileView} + onSetRepresentative={handleSetRepresentative} + config={fileConfig} + isDesignMode={isDesignMode} + />
); } @@ -572,4 +922,3 @@ export const V2Media = forwardRef( V2Media.displayName = "V2Media"; export default V2Media; - diff --git a/frontend/components/v2/V2Repeater.tsx b/frontend/components/v2/V2Repeater.tsx index ee80d0d7..eda9e5b2 100644 --- a/frontend/components/v2/V2Repeater.tsx +++ b/frontend/components/v2/V2Repeater.tsx @@ -6,7 +6,7 @@ * ๋ Œ๋”๋ง ๋ชจ๋“œ: * - inline: ํ˜„์žฌ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ง์ ‘ ์ž…๋ ฅ * - modal: ์—”ํ‹ฐํ‹ฐ ์„ ํƒ (FK ์ €์žฅ) + ์ถ”๊ฐ€ ์ž…๋ ฅ ์ปฌ๋Ÿผ - * + * * RepeaterTable ๋ฐ ItemSelectionModal ์žฌ์‚ฌ์šฉ */ @@ -63,7 +63,7 @@ export const V2Repeater: React.FC = ({ // ๐Ÿ†• ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ž๋™์œผ๋กœ ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์กฐ์ • ํŠธ๋ฆฌ๊ฑฐ const [autoWidthTrigger, setAutoWidthTrigger] = useState(0); - + // ์†Œ์Šค ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋ผ๋ฒจ ๋งคํ•‘ const [sourceColumnLabels, setSourceColumnLabels] = useState>({}); @@ -72,10 +72,10 @@ export const V2Repeater: React.FC = ({ // ๐Ÿ†• ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ โ†’ ๋ผ๋ฒจ ๋งคํ•‘ (RepeaterTable ํ‘œ์‹œ์šฉ) const [categoryLabelMap, setCategoryLabelMap] = useState>({}); - + // ํ˜„์žฌ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด (inputType ๋งคํ•‘์šฉ) const [currentTableColumnInfo, setCurrentTableColumnInfo] = useState>({}); - + // ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ƒํƒœ const [activeDataSources, setActiveDataSources] = useState>({}); @@ -88,10 +88,9 @@ export const V2Repeater: React.FC = ({ // ์ „์—ญ ๋ฆฌํ”ผํ„ฐ ๋“ฑ๋ก // ๐Ÿ†• useCustomTable์ด ์„ค์ •๋œ ๊ฒฝ์šฐ mainTableName ์‚ฌ์šฉ (์‹ค์ œ ์ €์žฅ๋  ํ…Œ์ด๋ธ”) useEffect(() => { - const targetTableName = config.useCustomTable && config.mainTableName - ? config.mainTableName - : config.dataSource?.tableName; - + const targetTableName = + config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; + if (targetTableName) { if (!window.__v2RepeaterInstances) { window.__v2RepeaterInstances = new Set(); @@ -110,22 +109,21 @@ export const V2Repeater: React.FC = ({ useEffect(() => { const handleSaveEvent = async (event: CustomEvent) => { // ๐Ÿ†• mainTableName์ด ์„ค์ •๋œ ๊ฒฝ์šฐ ์šฐ์„  ์‚ฌ์šฉ, ์—†์œผ๋ฉด dataSource.tableName ์‚ฌ์šฉ - const tableName = config.useCustomTable && config.mainTableName - ? config.mainTableName - : config.dataSource?.tableName; + const tableName = + config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; const eventParentId = event.detail?.parentId; const mainFormData = event.detail?.mainFormData; - + // ๐Ÿ†• ๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ”์—์„œ ์ƒ์„ฑ๋œ ID (FK ์—ฐ๊ฒฐ์šฉ) const masterRecordId = event.detail?.masterRecordId || mainFormData?.id; - + if (!tableName || data.length === 0) { return; } // V2Repeater ์ €์žฅ ์‹œ์ž‘ - const saveInfo = { - tableName, + const saveInfo = { + tableName, useCustomTable: config.useCustomTable, mainTableName: config.mainTableName, foreignKeyColumn: config.foreignKeyColumn, @@ -145,10 +143,10 @@ export const V2Repeater: React.FC = ({ } catch { console.warn("ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด ์กฐํšŒ ์‹คํŒจ"); } - + for (let i = 0; i < data.length; i++) { const row = data[i]; - + // ๋‚ด๋ถ€ ํ•„๋“œ ์ œ๊ฑฐ const cleanRow = Object.fromEntries(Object.entries(row).filter(([key]) => !key.startsWith("_"))); @@ -157,14 +155,14 @@ export const V2Repeater: React.FC = ({ if (config.useCustomTable && config.mainTableName) { // ์ปค์Šคํ…€ ํ…Œ์ด๋ธ”: ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ๋งŒ ์ €์žฅ mergedData = { ...cleanRow }; - + // ๐Ÿ†• FK ์ž๋™ ์—ฐ๊ฒฐ - foreignKeySourceColumn์ด ์„ค์ •๋œ ๊ฒฝ์šฐ ํ•ด๋‹น ์ปฌ๋Ÿผ ๊ฐ’ ์‚ฌ์šฉ if (config.foreignKeyColumn) { // foreignKeySourceColumn์ด ์žˆ์œผ๋ฉด mainFormData์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ ๊ฐ’ ์‚ฌ์šฉ // ์—†์œผ๋ฉด ๋งˆ์Šคํ„ฐ ๋ ˆ์ฝ”๋“œ ID ์‚ฌ์šฉ (๊ธฐ์กด ๋™์ž‘) const sourceColumn = config.foreignKeySourceColumn; let fkValue: any; - + if (sourceColumn && mainFormData && mainFormData[sourceColumn] !== undefined) { // mainFormData์—์„œ ์ฐธ์กฐ ์ปฌ๋Ÿผ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ fkValue = mainFormData[sourceColumn]; @@ -172,18 +170,18 @@ export const V2Repeater: React.FC = ({ // ๊ธฐ๋ณธ: ๋งˆ์Šคํ„ฐ ๋ ˆ์ฝ”๋“œ ID ์‚ฌ์šฉ fkValue = masterRecordId; } - + if (fkValue !== undefined && fkValue !== null) { mergedData[config.foreignKeyColumn] = fkValue; } } } else { // ๊ธฐ์กด ๋ฐฉ์‹: ๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ - const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {}; + const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {}; mergedData = { - ...mainFormDataWithoutId, - ...cleanRow, - }; + ...mainFormDataWithoutId, + ...cleanRow, + }; } // ์œ ํšจํ•˜์ง€ ์•Š์€ ์ปฌ๋Ÿผ ์ œ๊ฑฐ @@ -193,10 +191,9 @@ export const V2Repeater: React.FC = ({ filteredData[key] = value; } } - + await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData); } - } catch (error) { console.error("โŒ V2Repeater ์ €์žฅ ์‹คํŒจ:", error); throw error; @@ -207,14 +204,13 @@ export const V2Repeater: React.FC = ({ const unsubscribe = v2EventBus.subscribe( V2_EVENTS.REPEATER_SAVE, async (payload) => { - const tableName = config.useCustomTable && config.mainTableName - ? config.mainTableName - : config.dataSource?.tableName; + const tableName = + config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; if (payload.tableName === tableName) { await handleSaveEvent({ detail: payload } as CustomEvent); } }, - { componentId: `v2-repeater-${config.dataSource?.tableName}` } + { componentId: `v2-repeater-${config.dataSource?.tableName}` }, ); // ๋ ˆ๊ฑฐ์‹œ ์ด๋ฒคํŠธ๋„ ๊ณ„์† ์ง€์› (์ ์ง„์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜) @@ -223,7 +219,14 @@ export const V2Repeater: React.FC = ({ unsubscribe(); window.removeEventListener("repeaterSave" as any, handleSaveEvent); }; - }, [data, config.dataSource?.tableName, config.useCustomTable, config.mainTableName, config.foreignKeyColumn, parentId]); + }, [ + data, + config.dataSource?.tableName, + config.useCustomTable, + config.mainTableName, + config.foreignKeyColumn, + parentId, + ]); // ํ˜„์žฌ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด ๋กœ๋“œ useEffect(() => { @@ -234,7 +237,7 @@ export const V2Repeater: React.FC = ({ try { const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); const columns = response.data?.data?.columns || response.data?.columns || response.data || []; - + const columnMap: Record = {}; columns.forEach((col: any) => { const name = col.columnName || col.column_name || col.name; @@ -320,7 +323,7 @@ export const V2Repeater: React.FC = ({ try { const response = await apiClient.get(`/table-management/tables/${resolvedSourceTable}/columns`); const columns = response.data?.data?.columns || response.data?.columns || response.data || []; - + const labels: Record = {}; const categoryCols: string[] = []; @@ -364,13 +367,13 @@ export const V2Repeater: React.FC = ({ calculated: true, width: col.width === "auto" ? undefined : col.width, }; - } - + } + // ์ผ๋ฐ˜ ์ž…๋ ฅ ์ปฌ๋Ÿผ let type: "text" | "number" | "date" | "select" | "category" = "text"; - if (inputType === "number" || inputType === "decimal") type = "number"; - else if (inputType === "date" || inputType === "datetime") type = "date"; - else if (inputType === "code") type = "select"; + if (inputType === "number" || inputType === "decimal") type = "number"; + else if (inputType === "date" || inputType === "datetime") type = "date"; + else if (inputType === "code") type = "select"; else if (inputType === "category") type = "category"; // ๐Ÿ†• ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… // ๐Ÿ†• ์นดํ…Œ๊ณ ๋ฆฌ ์ฐธ์กฐ ID ๊ฐ€์ ธ์˜ค๊ธฐ (tableName.columnName ํ˜•์‹) @@ -383,19 +386,19 @@ export const V2Repeater: React.FC = ({ categoryRef = `${tableName}.${col.key}`; } } - - return { - field: col.key, - label: col.title || colInfo?.displayName || col.key, - type, - editable: col.editable !== false, - width: col.width === "auto" ? undefined : col.width, - required: false, + + return { + field: col.key, + label: col.title || colInfo?.displayName || col.key, + type, + editable: col.editable !== false, + width: col.width === "auto" ? undefined : col.width, + required: false, categoryRef, // ๐Ÿ†• ์นดํ…Œ๊ณ ๋ฆฌ ์ฐธ์กฐ ID ์ „๋‹ฌ hidden: col.hidden, // ๐Ÿ†• ํžˆ๋“  ์ฒ˜๋ฆฌ autoFill: col.autoFill, // ๐Ÿ†• ์ž๋™ ์ž…๋ ฅ ์„ค์ • - }; - }); + }; + }); }, [config.columns, sourceColumnLabels, currentTableColumnInfo, resolvedSourceTable, config.dataSource?.tableName]); // ๐Ÿ†• ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์นดํ…Œ๊ณ ๋ฆฌ ๋ผ๋ฒจ ๋กœ๋“œ (RepeaterTable ํ‘œ์‹œ์šฉ) @@ -451,26 +454,25 @@ export const V2Repeater: React.FC = ({ // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ const handleDataChange = useCallback( (newData: any[]) => { - setData(newData); - - // ๐Ÿ†• _targetTable ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จํ•˜์—ฌ ์ „๋‹ฌ (๋ฐฑ์—”๋“œ์—์„œ ํ…Œ์ด๋ธ” ๋ถ„๋ฆฌ์šฉ) - if (onDataChange) { - const targetTable = config.useCustomTable && config.mainTableName - ? config.mainTableName - : config.dataSource?.tableName; - - if (targetTable) { - // ๊ฐ ํ–‰์— _targetTable ์ถ”๊ฐ€ - const dataWithTarget = newData.map(row => ({ - ...row, - _targetTable: targetTable, - })); - onDataChange(dataWithTarget); - } else { - onDataChange(newData); + setData(newData); + + // ๐Ÿ†• _targetTable ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จํ•˜์—ฌ ์ „๋‹ฌ (๋ฐฑ์—”๋“œ์—์„œ ํ…Œ์ด๋ธ” ๋ถ„๋ฆฌ์šฉ) + if (onDataChange) { + const targetTable = + config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; + + if (targetTable) { + // ๊ฐ ํ–‰์— _targetTable ์ถ”๊ฐ€ + const dataWithTarget = newData.map((row) => ({ + ...row, + _targetTable: targetTable, + })); + onDataChange(dataWithTarget); + } else { + onDataChange(newData); + } } - } - + // ๐Ÿ†• ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ž๋™์œผ๋กœ ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์กฐ์ • setAutoWidthTrigger((prev) => prev + 1); }, @@ -480,26 +482,25 @@ export const V2Repeater: React.FC = ({ // ํ–‰ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ const handleRowChange = useCallback( (index: number, newRow: any) => { - const newData = [...data]; - newData[index] = newRow; - setData(newData); - - // ๐Ÿ†• _targetTable ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จ - if (onDataChange) { - const targetTable = config.useCustomTable && config.mainTableName - ? config.mainTableName - : config.dataSource?.tableName; - - if (targetTable) { - const dataWithTarget = newData.map(row => ({ - ...row, - _targetTable: targetTable, - })); - onDataChange(dataWithTarget); - } else { - onDataChange(newData); + const newData = [...data]; + newData[index] = newRow; + setData(newData); + + // ๐Ÿ†• _targetTable ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จ + if (onDataChange) { + const targetTable = + config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; + + if (targetTable) { + const dataWithTarget = newData.map((row) => ({ + ...row, + _targetTable: targetTable, + })); + onDataChange(dataWithTarget); + } else { + onDataChange(newData); + } } - } }, [data, onDataChange, config.useCustomTable, config.mainTableName, config.dataSource?.tableName], ); @@ -507,16 +508,16 @@ export const V2Repeater: React.FC = ({ // ํ–‰ ์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ const handleRowDelete = useCallback( (index: number) => { - const newData = data.filter((_, i) => i !== index); + const newData = data.filter((_, i) => i !== index); handleDataChange(newData); // ๐Ÿ†• handleDataChange ์‚ฌ์šฉ - - // ์„ ํƒ ์ƒํƒœ ์—…๋ฐ์ดํŠธ - const newSelected = new Set(); - selectedRows.forEach((i) => { - if (i < index) newSelected.add(i); - else if (i > index) newSelected.add(i - 1); - }); - setSelectedRows(newSelected); + + // ์„ ํƒ ์ƒํƒœ ์—…๋ฐ์ดํŠธ + const newSelected = new Set(); + selectedRows.forEach((i) => { + if (i < index) newSelected.add(i); + else if (i > index) newSelected.add(i - 1); + }); + setSelectedRows(newSelected); }, [data, selectedRows, handleDataChange], ); @@ -535,30 +536,30 @@ export const V2Repeater: React.FC = ({ if (!col.autoFill || col.autoFill.type === "none") return undefined; const now = new Date(); - + switch (col.autoFill.type) { case "currentDate": return now.toISOString().split("T")[0]; // YYYY-MM-DD - + case "currentDateTime": return now.toISOString().slice(0, 19).replace("T", " "); // YYYY-MM-DD HH:mm:ss - + case "sequence": return rowIndex + 1; // 1๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ์ˆœ๋ฒˆ - + case "numbering": // ์ฑ„๋ฒˆ์€ ๋ณ„๋„ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ํ•„์š” return null; // null ๋ฐ˜ํ™˜ํ•˜์—ฌ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ํ•„์š”ํ•จ์„ ํ‘œ์‹œ - + case "fromMainForm": if (col.autoFill.sourceField && mainFormData) { return mainFormData[col.autoFill.sourceField]; } return ""; - + case "fixed": return col.autoFill.fixedValue ?? ""; - + default: return undefined; } @@ -567,19 +568,23 @@ export const V2Repeater: React.FC = ({ ); // ๐Ÿ†• ์ฑ„๋ฒˆ API ํ˜ธ์ถœ (๋น„๋™๊ธฐ) - const generateNumberingCode = useCallback(async (ruleId: string): Promise => { - try { - const result = await allocateNumberingCode(ruleId); - if (result.success && result.data?.generatedCode) { - return result.data.generatedCode; + // ๐Ÿ†• ์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ง€์›์„ ์œ„ํ•ด userInputCode ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ + const generateNumberingCode = useCallback( + async (ruleId: string, userInputCode?: string, formData?: Record): Promise => { + try { + const result = await allocateNumberingCode(ruleId, userInputCode, formData); + if (result.success && result.data?.generatedCode) { + return result.data.generatedCode; + } + console.error("์ฑ„๋ฒˆ ์‹คํŒจ:", result.error); + return ""; + } catch (error) { + console.error("์ฑ„๋ฒˆ API ํ˜ธ์ถœ ์‹คํŒจ:", error); + return ""; } - console.error("์ฑ„๋ฒˆ ์‹คํŒจ:", result.error); - return ""; - } catch (error) { - console.error("์ฑ„๋ฒˆ API ํ˜ธ์ถœ ์‹คํŒจ:", error); - return ""; - } - }, []); + }, + [], + ); // ๐Ÿ†• ํ–‰ ์ถ”๊ฐ€ (inline ๋ชจ๋“œ ๋˜๋Š” ๋ชจ๋‹ฌ ์—ด๊ธฐ) - ๋น„๋™๊ธฐ๋กœ ๋ณ€๊ฒฝ const handleAddRow = useCallback(async () => { @@ -588,7 +593,7 @@ export const V2Repeater: React.FC = ({ } else { const newRow: any = { _id: `new_${Date.now()}` }; const currentRowCount = data.length; - + // ๋จผ์ € ๋™๊ธฐ์  ์ž๋™ ์ž…๋ ฅ ๊ฐ’ ์ ์šฉ for (const col of config.columns) { const autoValue = generateAutoFillValueSync(col, currentRowCount); @@ -598,10 +603,10 @@ export const V2Repeater: React.FC = ({ } else if (autoValue !== undefined) { newRow[col.key] = autoValue; } else { - newRow[col.key] = ""; + newRow[col.key] = ""; } } - + const newData = [...data, newRow]; handleDataChange(newData); } @@ -610,23 +615,23 @@ export const V2Repeater: React.FC = ({ // ๋ชจ๋‹ฌ์—์„œ ํ•ญ๋ชฉ ์„ ํƒ - ๋น„๋™๊ธฐ๋กœ ๋ณ€๊ฒฝ const handleSelectItems = useCallback( async (items: Record[]) => { - const fkColumn = config.dataSource?.foreignKey; + const fkColumn = config.dataSource?.foreignKey; const currentRowCount = data.length; // ์ฑ„๋ฒˆ์ด ํ•„์š”ํ•œ ์ปฌ๋Ÿผ ์ฐพ๊ธฐ const numberingColumns = config.columns.filter( - (col) => !col.isSourceDisplay && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId + (col) => !col.isSourceDisplay && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId, ); - + const newRows = await Promise.all( items.map(async (item, index) => { - const row: any = { _id: `new_${Date.now()}_${Math.random()}` }; - + const row: any = { _id: `new_${Date.now()}_${Math.random()}` }; + // FK ๊ฐ’ ์ €์žฅ (resolvedReferenceKey ์‚ฌ์šฉ) if (fkColumn && item[resolvedReferenceKey]) { row[fkColumn] = item[resolvedReferenceKey]; - } - + } + // ๋ชจ๋“  ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ (์ˆœ์„œ๋Œ€๋กœ) for (const col of config.columns) { if (col.isSourceDisplay) { @@ -642,20 +647,28 @@ export const V2Repeater: React.FC = ({ row[col.key] = autoValue; } else if (row[col.key] === undefined) { // ์ž…๋ ฅ ์ปฌ๋Ÿผ: ๋นˆ ๊ฐ’์œผ๋กœ ์ดˆ๊ธฐํ™” - row[col.key] = ""; - } + row[col.key] = ""; + } } } - - return row; - }) + + return row; + }), ); - - const newData = [...data, ...newRows]; + + const newData = [...data, ...newRows]; handleDataChange(newData); - setModalOpen(false); + setModalOpen(false); }, - [config.dataSource?.foreignKey, resolvedReferenceKey, config.columns, data, handleDataChange, generateAutoFillValueSync, generateNumberingCode], + [ + config.dataSource?.foreignKey, + resolvedReferenceKey, + config.columns, + data, + handleDataChange, + generateAutoFillValueSync, + generateNumberingCode, + ], ); // ์†Œ์Šค ์ปฌ๋Ÿผ ๋ชฉ๋ก (๋ชจ๋‹ฌ์šฉ) - ๐Ÿ†• columns ๋ฐฐ์—ด์—์„œ isSourceDisplay์ธ ๊ฒƒ๋งŒ ํ•„ํ„ฐ๋ง @@ -669,19 +682,19 @@ export const V2Repeater: React.FC = ({ // ๐Ÿ†• beforeFormSave ์ด๋ฒคํŠธ์—์„œ ์ฑ„๋ฒˆ placeholder๋ฅผ ์‹ค์ œ ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ const dataRef = useRef(data); dataRef.current = data; - + useEffect(() => { const handleBeforeFormSave = async (event: Event) => { const customEvent = event as CustomEvent; const formData = customEvent.detail?.formData; - + if (!formData || !dataRef.current.length) return; // ์ฑ„๋ฒˆ placeholder๊ฐ€ ์žˆ๋Š” ํ–‰๋“ค์„ ์ฐพ์•„์„œ ์‹ค์ œ ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ const processedData = await Promise.all( dataRef.current.map(async (row) => { const newRow = { ...row }; - + for (const key of Object.keys(newRow)) { const value = newRow[key]; if (typeof value === "string" && value.startsWith("__NUMBERING_RULE__")) { @@ -690,7 +703,8 @@ export const V2Repeater: React.FC = ({ if (match) { const ruleId = match[1]; try { - const result = await allocateNumberingCode(ruleId); + // ๐Ÿ†• ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘ํ•œ ๊ฐ’์„ ์ „๋‹ฌ (์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ถ”์ถœ์šฉ) + const result = await allocateNumberingCode(ruleId, undefined, newRow); if (result.success && result.data?.generatedCode) { newRow[key] = result.data.generatedCode; } else { @@ -704,16 +718,16 @@ export const V2Repeater: React.FC = ({ } } } - + return newRow; }), ); - + // ์ฒ˜๋ฆฌ๋œ ๋ฐ์ดํ„ฐ๋ฅผ formData์— ์ถ”๊ฐ€ const fieldName = config.fieldName || "repeaterData"; formData[fieldName] = processedData; }; - + // V2 EventBus ๊ตฌ๋… const unsubscribe = v2EventBus.subscribe( V2_EVENTS.FORM_SAVE_COLLECT, @@ -724,12 +738,12 @@ export const V2Repeater: React.FC = ({ } as CustomEvent; await handleBeforeFormSave(fakeEvent); }, - { componentId: `v2-repeater-${config.dataSource?.tableName}` } + { componentId: `v2-repeater-${config.dataSource?.tableName}` }, ); // ๋ ˆ๊ฑฐ์‹œ ์ด๋ฒคํŠธ๋„ ๊ณ„์† ์ง€์› (์ ์ง„์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜) window.addEventListener("beforeFormSave", handleBeforeFormSave); - + return () => { unsubscribe(); window.removeEventListener("beforeFormSave", handleBeforeFormSave); @@ -742,20 +756,20 @@ export const V2Repeater: React.FC = ({ const handleComponentDataTransfer = async (event: Event) => { const customEvent = event as CustomEvent; const { targetComponentId, data: transferData, mappingRules, mode } = customEvent.detail || {}; - + // ์ด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋Œ€์ƒ์ธ์ง€ ํ™•์ธ if (targetComponentId !== parentId && targetComponentId !== config.fieldName) { return; } - + if (!transferData || transferData.length === 0) { return; } - + // ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ const mappedData = transferData.map((item: any, index: number) => { const newRow: any = { _id: `transfer_${Date.now()}_${index}` }; - + if (mappingRules && mappingRules.length > 0) { // ๋งคํ•‘ ๊ทœ์น™์ด ์žˆ์œผ๋ฉด ์ ์šฉ mappingRules.forEach((rule: any) => { @@ -765,10 +779,10 @@ export const V2Repeater: React.FC = ({ // ๋งคํ•‘ ๊ทœ์น™ ์—†์œผ๋ฉด ๊ทธ๋Œ€๋กœ ๋ณต์‚ฌ Object.assign(newRow, item); } - + return newRow; }); - + // mode์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ if (mode === "replace") { handleDataChange(mappedData); @@ -782,20 +796,20 @@ export const V2Repeater: React.FC = ({ handleDataChange([...data, ...mappedData]); } }; - + // splitPanelDataTransfer: ๋ถ„ํ•  ํŒจ๋„์—์„œ ์ „์—ญ ์ด๋ฒคํŠธ๋กœ ์ „๋‹ฌ const handleSplitPanelDataTransfer = async (event: Event) => { const customEvent = event as CustomEvent; const { data: transferData, mappingRules, mode, sourcePosition } = customEvent.detail || {}; - + if (!transferData || transferData.length === 0) { return; } - + // ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ const mappedData = transferData.map((item: any, index: number) => { const newRow: any = { _id: `transfer_${Date.now()}_${index}` }; - + if (mappingRules && mappingRules.length > 0) { mappingRules.forEach((rule: any) => { newRow[rule.targetField] = item[rule.sourceField]; @@ -803,10 +817,10 @@ export const V2Repeater: React.FC = ({ } else { Object.assign(newRow, item); } - + return newRow; }); - + // mode์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ if (mode === "replace") { handleDataChange(mappedData); @@ -814,7 +828,7 @@ export const V2Repeater: React.FC = ({ handleDataChange([...data, ...mappedData]); } }; - + // V2 EventBus ๊ตฌ๋… const unsubscribeComponent = v2EventBus.subscribe( V2_EVENTS.COMPONENT_DATA_TRANSFER, @@ -829,7 +843,7 @@ export const V2Repeater: React.FC = ({ } as CustomEvent; handleComponentDataTransfer(fakeEvent); }, - { componentId: `v2-repeater-${config.dataSource?.tableName}` } + { componentId: `v2-repeater-${config.dataSource?.tableName}` }, ); const unsubscribeSplitPanel = v2EventBus.subscribe( @@ -844,13 +858,13 @@ export const V2Repeater: React.FC = ({ } as CustomEvent; handleSplitPanelDataTransfer(fakeEvent); }, - { componentId: `v2-repeater-${config.dataSource?.tableName}` } + { componentId: `v2-repeater-${config.dataSource?.tableName}` }, ); // ๋ ˆ๊ฑฐ์‹œ ์ด๋ฒคํŠธ๋„ ๊ณ„์† ์ง€์› (์ ์ง„์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜) window.addEventListener("componentDataTransfer", handleComponentDataTransfer as EventListener); window.addEventListener("splitPanelDataTransfer", handleSplitPanelDataTransfer as EventListener); - + return () => { unsubscribeComponent(); unsubscribeSplitPanel(); @@ -926,11 +940,7 @@ V2Repeater.displayName = "V2Repeater"; // V2ErrorBoundary๋กœ ๋ž˜ํ•‘๋œ ์•ˆ์ „ํ•œ ๋ฒ„์ „ export export const SafeV2Repeater: React.FC = (props) => { return ( - + ); diff --git a/frontend/components/v2/V2Select.tsx b/frontend/components/v2/V2Select.tsx index 31c5ba2a..832f2ddb 100644 --- a/frontend/components/v2/V2Select.tsx +++ b/frontend/components/v2/V2Select.tsx @@ -42,6 +42,7 @@ const DropdownSelect = forwardRef(({ options, value, @@ -52,7 +53,8 @@ const DropdownSelect = forwardRef { const [open, setOpen] = useState(false); @@ -64,7 +66,8 @@ const DropdownSelect = forwardRef onChange?.(v)} disabled={disabled} > - + {/* SelectTrigger์— style๋กœ ์ง์ ‘ height ์ „๋‹ฌ (Radix Select.Root๋Š” DOM ์—†์–ด์„œ h-full ์ฒด์ธ ๋Š๊น€) */} + @@ -112,13 +115,21 @@ const DropdownSelect = forwardRef + {/* Button์— style๋กœ ์ง์ ‘ height ์ „๋‹ฌ (Popover๋„ DOM ์ฒด์ธ ๋Š๊น€) */}
-
+
{options.map((option: any, index: number) => (
updateOption(index, "value", e.target.value)} placeholder="๊ฐ’" - className="h-7 text-xs flex-1" + className="h-7 flex-1 text-xs" /> updateOption(index, "label", e.target.value)} placeholder="ํ‘œ์‹œ ํ…์ŠคํŠธ" - className="h-7 text-xs flex-1" + className="h-7 flex-1 text-xs" />
))} {options.length === 0 && ( -

- ์˜ต์…˜์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š” -

+

์˜ต์…˜์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”

)}
+ + {/* ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • */} + {options.length > 0 && ( +
+ + +

ํ™”๋ฉด ๋กœ๋“œ ์‹œ ์ž๋™ ์„ ํƒ๋  ๊ฐ’

+
+ )}
)} @@ -202,16 +210,13 @@ export const V2SelectConfigPanel: React.FC = ({
{config.codeGroup ? ( -

{config.codeGroup}

+

{config.codeGroup}

) : ( -

- ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ์—์„œ ์ฝ”๋“œ ๊ทธ๋ฃน์„ ์„ค์ •ํ•ด์ฃผ์„ธ์š” -

+

ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ์—์„œ ์ฝ”๋“œ ๊ทธ๋ฃน์„ ์„ค์ •ํ•ด์ฃผ์„ธ์š”

)}
)} - {/* ์—”ํ‹ฐํ‹ฐ(์ฐธ์กฐ ํ…Œ์ด๋ธ”) ์„ค์ • */} {config.source === "entity" && (
@@ -222,16 +227,16 @@ export const V2SelectConfigPanel: React.FC = ({ readOnly disabled placeholder="ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ์—์„œ ์„ค์ •" - className="h-8 text-xs bg-muted" + className="bg-muted h-8 text-xs" /> -

+

์กฐ์ธํ•  ํ…Œ์ด๋ธ”๋ช… (ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ์—์„œ ์„ค์ •๋œ ๊ฒฝ์šฐ ์ž๋™ ์ž…๋ ฅ๋จ)

{/* ์ปฌ๋Ÿผ ๋กœ๋”ฉ ์ค‘ ํ‘œ์‹œ */} {loadingColumns && ( -
+
์ปฌ๋Ÿผ ๋ชฉ๋ก ๋กœ๋”ฉ ์ค‘...
@@ -265,7 +270,7 @@ export const V2SelectConfigPanel: React.FC = ({ className="h-8 text-xs" /> )} -

์ €์žฅ๋  ๊ฐ’

+

์ €์žฅ๋  ๊ฐ’

@@ -293,7 +298,7 @@ export const V2SelectConfigPanel: React.FC = ({ className="h-8 text-xs" /> )} -

ํ™”๋ฉด์— ํ‘œ์‹œ๋  ๊ฐ’

+

ํ™”๋ฉด์— ํ‘œ์‹œ๋  ๊ฐ’

@@ -311,14 +316,16 @@ export const V2SelectConfigPanel: React.FC = ({ {/* ์ถ”๊ฐ€ ์˜ต์…˜ */}
- +
updateConfig("multiple", checked)} /> - +
@@ -327,7 +334,9 @@ export const V2SelectConfigPanel: React.FC = ({ checked={config.searchable || false} onCheckedChange={(checked) => updateConfig("searchable", checked)} /> - +
@@ -336,7 +345,9 @@ export const V2SelectConfigPanel: React.FC = ({ checked={config.allowClear !== false} onCheckedChange={(checked) => updateConfig("allowClear", checked)} /> - +
diff --git a/frontend/contexts/LayerContext.tsx b/frontend/contexts/LayerContext.tsx new file mode 100644 index 00000000..6e0f67cd --- /dev/null +++ b/frontend/contexts/LayerContext.tsx @@ -0,0 +1,337 @@ +import React, { createContext, useContext, useState, useCallback, ReactNode, useMemo } from "react"; +import { LayerDefinition, LayerType, ComponentData } from "@/types/screen-management"; +import { v4 as uuidv4 } from "uuid"; + +interface LayerContextType { + // ๋ ˆ์ด์–ด ์ƒํƒœ + layers: LayerDefinition[]; + activeLayerId: string | null; + activeLayer: LayerDefinition | null; + + // ๋ ˆ์ด์–ด ๊ด€๋ฆฌ + setLayers: (layers: LayerDefinition[]) => void; + setActiveLayerId: (id: string | null) => void; + addLayer: (type: LayerType, name?: string) => void; + removeLayer: (id: string) => void; + updateLayer: (id: string, updates: Partial) => void; + moveLayer: (dragIndex: number, hoverIndex: number) => void; + toggleLayerVisibility: (id: string) => void; + toggleLayerLock: (id: string) => void; + getLayerById: (id: string) => LayerDefinition | undefined; + + // ์ปดํฌ๋„ŒํŠธ ๊ด€๋ฆฌ (๋ ˆ์ด์–ด๋ณ„) + addComponentToLayer: (layerId: string, component: ComponentData) => void; + removeComponentFromLayer: (layerId: string, componentId: string) => void; + updateComponentInLayer: (layerId: string, componentId: string, updates: Partial) => void; + moveComponentToLayer: (componentId: string, fromLayerId: string, toLayerId: string) => void; + + // ์ปดํฌ๋„ŒํŠธ ์กฐํšŒ + getAllComponents: () => ComponentData[]; + getComponentById: (componentId: string) => { component: ComponentData; layerId: string } | null; + getComponentsInActiveLayer: () => ComponentData[]; + + // ๋ ˆ์ด์–ด ๊ฐ€์‹œ์„ฑ (๋Ÿฐํƒ€์ž„์šฉ) + runtimeVisibleLayers: string[]; + setRuntimeVisibleLayers: React.Dispatch>; + showLayer: (layerId: string) => void; + hideLayer: (layerId: string) => void; + toggleLayerRuntime: (layerId: string) => void; +} + +const LayerContext = createContext(undefined); + +export const useLayer = () => { + const context = useContext(LayerContext); + if (!context) { + throw new Error("useLayer must be used within a LayerProvider"); + } + return context; +}; + +// LayerProvider๊ฐ€ ์—†์„ ๋•Œ ์‚ฌ์šฉํ•  ๊ธฐ๋ณธ ์ปจํ…์ŠคํŠธ (์„ ํƒ์  ์‚ฌ์šฉ) +export const useLayerOptional = () => { + return useContext(LayerContext); +}; + +interface LayerProviderProps { + children: ReactNode; + initialLayers?: LayerDefinition[]; + onLayersChange?: (layers: LayerDefinition[]) => void; + onActiveLayerChange?: (activeLayerId: string | null) => void; // ๐Ÿ†• ํ™œ์„ฑ ๋ ˆ์ด์–ด ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ +} + +// ๊ธฐ๋ณธ ๋ ˆ์ด์–ด ์ƒ์„ฑ ํ—ฌํผ +export const createDefaultLayer = (components?: ComponentData[]): LayerDefinition => ({ + id: "default-layer", + name: "๊ธฐ๋ณธ ๋ ˆ์ด์–ด", + type: "base", + zIndex: 0, + isVisible: true, + isLocked: false, + components: components || [], +}); + +export const LayerProvider: React.FC = ({ + children, + initialLayers = [], + onLayersChange, + onActiveLayerChange, +}) => { + // ์ดˆ๊ธฐ ๋ ˆ์ด์–ด๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๋ ˆ์ด์–ด ์ƒ์„ฑ + const effectiveInitialLayers = initialLayers.length > 0 + ? initialLayers + : [createDefaultLayer()]; + + const [layers, setLayersState] = useState(effectiveInitialLayers); + const [activeLayerIdState, setActiveLayerIdState] = useState( + effectiveInitialLayers.length > 0 ? effectiveInitialLayers[0].id : null, + ); + + // ๐Ÿ†• ํ™œ์„ฑ ๋ ˆ์ด์–ด ๋ณ€๊ฒฝ ์‹œ ์ฝœ๋ฐฑ ํ˜ธ์ถœ + const setActiveLayerId = useCallback((id: string | null) => { + setActiveLayerIdState(id); + onActiveLayerChange?.(id); + }, [onActiveLayerChange]); + + // ํ™œ์„ฑ ๋ ˆ์ด์–ด ID (๋‚ด๋ถ€ ์ƒํƒœ ์‚ฌ์šฉ) + const activeLayerId = activeLayerIdState; + + // ๋Ÿฐํƒ€์ž„ ๊ฐ€์‹œ์„ฑ ์ƒํƒœ (ํŽธ์ง‘๊ธฐ์—์„œ์˜ isVisible๊ณผ ๋ณ„๊ฐœ) + const [runtimeVisibleLayers, setRuntimeVisibleLayers] = useState( + effectiveInitialLayers.filter(l => l.isVisible).map(l => l.id) + ); + + // ๋ ˆ์ด์–ด ๋ณ€๊ฒฝ ์‹œ ์ฝœ๋ฐฑ ํ˜ธ์ถœ + const setLayers = useCallback((newLayers: LayerDefinition[]) => { + setLayersState(newLayers); + onLayersChange?.(newLayers); + }, [onLayersChange]); + + // ํ™œ์„ฑ ๋ ˆ์ด์–ด ๊ณ„์‚ฐ + const activeLayer = useMemo(() => { + return layers.find(l => l.id === activeLayerId) || null; + }, [layers, activeLayerId]); + + const addLayer = useCallback( + (type: LayerType, name?: string) => { + const newLayer: LayerDefinition = { + id: uuidv4(), + name: name || `์ƒˆ ๋ ˆ์ด์–ด ${layers.length + 1}`, + type, + zIndex: layers.length, + isVisible: true, + isLocked: false, + components: [], + // ๋ชจ๋‹ฌ/๋“œ๋กœ์–ด ๊ธฐ๋ณธ ์„ค์ • + ...(type === "modal" || type === "drawer" ? { + overlayConfig: { + backdrop: true, + closeOnBackdropClick: true, + width: type === "drawer" ? "320px" : "600px", + height: type === "drawer" ? "100%" : "auto", + }, + } : {}), + }; + + setLayers([...layers, newLayer]); + setActiveLayerId(newLayer.id); + // ์ƒˆ ๋ ˆ์ด์–ด๋Š” ๋Ÿฐํƒ€์ž„์—์„œ๋„ ๊ธฐ๋ณธ์ ์œผ๋กœ ํ‘œ์‹œ + setRuntimeVisibleLayers(prev => [...prev, newLayer.id]); + }, + [layers, setLayers], + ); + + const removeLayer = useCallback( + (id: string) => { + // ๊ธฐ๋ณธ ๋ ˆ์ด์–ด๋Š” ์‚ญ์ œ ๋ถˆ๊ฐ€ + const layer = layers.find(l => l.id === id); + if (layer?.type === "base") { + console.warn("๊ธฐ๋ณธ ๋ ˆ์ด์–ด๋Š” ์‚ญ์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + return; + } + + const filtered = layers.filter((layer) => layer.id !== id); + setLayers(filtered); + + if (activeLayerId === id) { + setActiveLayerId(filtered.length > 0 ? filtered[0].id : null); + } + + setRuntimeVisibleLayers(prev => prev.filter(lid => lid !== id)); + }, + [layers, activeLayerId, setLayers], + ); + + const updateLayer = useCallback((id: string, updates: Partial) => { + setLayers(layers.map((layer) => (layer.id === id ? { ...layer, ...updates } : layer))); + }, [layers, setLayers]); + + const moveLayer = useCallback((dragIndex: number, hoverIndex: number) => { + const newLayers = [...layers]; + const [removed] = newLayers.splice(dragIndex, 1); + newLayers.splice(hoverIndex, 0, removed); + // Update zIndex based on new order + setLayers(newLayers.map((layer, index) => ({ ...layer, zIndex: index }))); + }, [layers, setLayers]); + + const toggleLayerVisibility = useCallback((id: string) => { + setLayers(layers.map((layer) => (layer.id === id ? { ...layer, isVisible: !layer.isVisible } : layer))); + }, [layers, setLayers]); + + const toggleLayerLock = useCallback((id: string) => { + setLayers(layers.map((layer) => (layer.id === id ? { ...layer, isLocked: !layer.isLocked } : layer))); + }, [layers, setLayers]); + + const getLayerById = useCallback( + (id: string) => { + return layers.find((layer) => layer.id === id); + }, + [layers], + ); + + // ===== ์ปดํฌ๋„ŒํŠธ ๊ด€๋ฆฌ ํ•จ์ˆ˜ ===== + + const addComponentToLayer = useCallback((layerId: string, component: ComponentData) => { + setLayers(layers.map(layer => { + if (layer.id === layerId) { + return { + ...layer, + components: [...layer.components, component], + }; + } + return layer; + })); + }, [layers, setLayers]); + + const removeComponentFromLayer = useCallback((layerId: string, componentId: string) => { + setLayers(layers.map(layer => { + if (layer.id === layerId) { + return { + ...layer, + components: layer.components.filter(c => c.id !== componentId), + }; + } + return layer; + })); + }, [layers, setLayers]); + + const updateComponentInLayer = useCallback((layerId: string, componentId: string, updates: Partial) => { + setLayers(layers.map(layer => { + if (layer.id === layerId) { + return { + ...layer, + components: layer.components.map(c => + c.id === componentId ? { ...c, ...updates } as ComponentData : c + ), + }; + } + return layer; + })); + }, [layers, setLayers]); + + const moveComponentToLayer = useCallback((componentId: string, fromLayerId: string, toLayerId: string) => { + if (fromLayerId === toLayerId) return; + + const fromLayer = layers.find(l => l.id === fromLayerId); + const component = fromLayer?.components.find(c => c.id === componentId); + + if (!component) return; + + setLayers(layers.map(layer => { + if (layer.id === fromLayerId) { + return { + ...layer, + components: layer.components.filter(c => c.id !== componentId), + }; + } + if (layer.id === toLayerId) { + return { + ...layer, + components: [...layer.components, component], + }; + } + return layer; + })); + }, [layers, setLayers]); + + // ===== ์ปดํฌ๋„ŒํŠธ ์กฐํšŒ ํ•จ์ˆ˜ ===== + + const getAllComponents = useCallback((): ComponentData[] => { + return layers.flatMap(layer => layer.components); + }, [layers]); + + const getComponentById = useCallback((componentId: string): { component: ComponentData; layerId: string } | null => { + for (const layer of layers) { + const component = layer.components.find(c => c.id === componentId); + if (component) { + return { component, layerId: layer.id }; + } + } + return null; + }, [layers]); + + const getComponentsInActiveLayer = useCallback((): ComponentData[] => { + const layer = layers.find(l => l.id === activeLayerId); + return layer?.components || []; + }, [layers, activeLayerId]); + + // ===== ๋Ÿฐํƒ€์ž„ ๋ ˆ์ด์–ด ๊ฐ€์‹œ์„ฑ ๊ด€๋ฆฌ ===== + + const showLayer = useCallback((layerId: string) => { + setRuntimeVisibleLayers(prev => [...new Set([...prev, layerId])]); + }, []); + + const hideLayer = useCallback((layerId: string) => { + setRuntimeVisibleLayers(prev => prev.filter(id => id !== layerId)); + }, []); + + const toggleLayerRuntime = useCallback((layerId: string) => { + setRuntimeVisibleLayers(prev => + prev.includes(layerId) + ? prev.filter(id => id !== layerId) + : [...prev, layerId] + ); + }, []); + + return ( + + {children} + + ); +}; diff --git a/frontend/lib/api/categoryTree.ts b/frontend/lib/api/categoryTree.ts index 720cb99d..ac429607 100644 --- a/frontend/lib/api/categoryTree.ts +++ b/frontend/lib/api/categoryTree.ts @@ -41,6 +41,7 @@ export interface CreateCategoryValueInput { icon?: string; isActive?: boolean; isDefault?: boolean; + targetCompanyCode?: string; // ์ €์žฅํ•  ํšŒ์‚ฌ ์ฝ”๋“œ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ํšŒ์‚ฌ ์„ ํƒ ์‹œ) } // ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ˆ˜์ • ์ž…๋ ฅ diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts index 9b5b7aea..8867f96f 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -62,7 +62,10 @@ export const getFullImageUrl = (imagePath: string): string => { } // SSR ๋˜๋Š” ์•Œ ์ˆ˜ ์—†๋Š” ํ™˜๊ฒฝ์—์„œ๋Š” API_BASE_URL ์‚ฌ์šฉ (fallback) - const baseUrl = API_BASE_URL.replace("/api", ""); + // ์ฃผ์˜: ํ”„๋กœ๋•์…˜ URL์ด https://api.vexplor.com/api ์ด๋ฏ€๋กœ + // ๋‹จ์ˆœ .replace("/api", "")๋Š” ํ˜ธ์ŠคํŠธ๋ช…์˜ /api๊นŒ์ง€ ์ œ๊ฑฐํ•˜๋Š” ๋ฒ„๊ทธ ๋ฐœ์ƒ + // ๋ฐ˜๋“œ์‹œ ๋ฌธ์ž์—ด ๋์˜ /api๋งŒ ์ œ๊ฑฐํ•ด์•ผ ํ•จ + const baseUrl = API_BASE_URL.replace(/\/api$/, ""); if (baseUrl.startsWith("http://") || baseUrl.startsWith("https://")) { return `${baseUrl}${imagePath}`; } diff --git a/frontend/lib/api/file.ts b/frontend/lib/api/file.ts index e6cab8ae..0eaf5579 100644 --- a/frontend/lib/api/file.ts +++ b/frontend/lib/api/file.ts @@ -1,4 +1,4 @@ -import { apiClient } from "./client"; +import { apiClient, API_BASE_URL } from "./client"; export interface FileInfo { id: string; @@ -231,10 +231,10 @@ export const getLinkedFiles = async ( /** * ํŒŒ์ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ URL ์ƒ์„ฑ + * ๐Ÿ”‘ ์ƒ๋Œ€ ๊ฒฝ๋กœ(/api) ๋Œ€์‹  API_BASE_URL ์‚ฌ์šฉ (Docker ํ™˜๊ฒฝ์—์„œ Next.js rewrite ์˜์กด ๋ฐฉ์ง€) */ export const getFilePreviewUrl = (fileId: string): string => { - const baseUrl = process.env.NEXT_PUBLIC_API_URL || "/api"; - return `${baseUrl}/files/preview/${fileId}`; + return `${API_BASE_URL}/files/preview/${fileId}`; }; /** @@ -274,7 +274,9 @@ export const getDirectFileUrl = (filePath: string): string => { } // SSR ๋˜๋Š” ์•Œ ์ˆ˜ ์—†๋Š” ํ™˜๊ฒฝ์—์„œ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์‚ฌ์šฉ (fallback) - const baseUrl = process.env.NEXT_PUBLIC_API_URL?.replace("/api", "") || ""; + // ์ฃผ์˜: ํ”„๋กœ๋•์…˜ URL์ด https://api.vexplor.com/api ์ด๋ฏ€๋กœ + // ๋‹จ์ˆœ .replace("/api", "")๋Š” ํ˜ธ์ŠคํŠธ๋ช…์˜ /api๊นŒ์ง€ ์ œ๊ฑฐํ•˜๋Š” ๋ฒ„๊ทธ ๋ฐœ์ƒ + const baseUrl = process.env.NEXT_PUBLIC_API_URL?.replace(/\/api$/, "") || ""; if (baseUrl.startsWith("http://") || baseUrl.startsWith("https://")) { return `${baseUrl}${filePath}`; } @@ -298,3 +300,31 @@ export const setRepresentativeFile = async (objid: string): Promise<{ throw new Error("๋Œ€ํ‘œ ํŒŒ์ผ ์„ค์ •์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } }; + +/** + * ํŒŒ์ผ ์ •๋ณด ์กฐํšŒ (๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋งŒ, objid๋กœ ์กฐํšŒ) + */ +export const getFileInfoByObjid = async (objid: string): Promise<{ + success: boolean; + data?: { + objid: string; + realFileName: string; + fileSize: number; + fileExt: string; + filePath: string; + regdate: string; + isRepresentative: boolean; + }; + message?: string; +}> => { + try { + const response = await apiClient.get(`/files/info/${objid}`); + return response.data; + } catch (error) { + console.error("ํŒŒ์ผ ์ •๋ณด ์กฐํšŒ ์˜ค๋ฅ˜:", error); + return { + success: false, + message: "ํŒŒ์ผ ์ •๋ณด ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + }; + } +}; diff --git a/frontend/lib/api/numberingRule.ts b/frontend/lib/api/numberingRule.ts index 8b7f47bd..b0ec38e2 100644 --- a/frontend/lib/api/numberingRule.ts +++ b/frontend/lib/api/numberingRule.ts @@ -26,13 +26,9 @@ export async function getNumberingRules(): Promise> { +export async function getAvailableNumberingRules(menuObjid?: number): Promise> { try { - const url = menuObjid - ? `/numbering-rules/available/${menuObjid}` - : "/numbering-rules/available"; + const url = menuObjid ? `/numbering-rules/available/${menuObjid}` : "/numbering-rules/available"; const response = await apiClient.get(url); return response.data; } catch (error: any) { @@ -46,7 +42,7 @@ export async function getAvailableNumberingRules( * @returns ํ•ด๋‹น ํ…Œ์ด๋ธ”์˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก */ export async function getAvailableNumberingRulesForScreen( - tableName: string + tableName: string, ): Promise> { try { const response = await apiClient.get("/numbering-rules/available-for-screen", { @@ -70,9 +66,7 @@ export async function getNumberingRuleById(ruleId: string): Promise> { +export async function createNumberingRule(config: NumberingRuleConfig): Promise> { try { const response = await apiClient.post("/numbering-rules", config); return response.data; @@ -83,7 +77,7 @@ export async function createNumberingRule( export async function updateNumberingRule( ruleId: string, - config: Partial + config: Partial, ): Promise> { try { const response = await apiClient.put(`/numbering-rules/${ruleId}`, config); @@ -110,7 +104,7 @@ export async function deleteNumberingRule(ruleId: string): Promise + formData?: Record, ): Promise> { // ruleId ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ if (!ruleId || ruleId === "undefined" || ruleId === "null") { @@ -127,11 +121,8 @@ export async function previewNumberingCode( return response.data; } catch (error: unknown) { const err = error as { response?: { data?: { error?: string; message?: string } }; message?: string }; - const errorMessage = - err.response?.data?.error || - err.response?.data?.message || - err.message || - "์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์‹คํŒจ"; + const errorMessage = + err.response?.data?.error || err.response?.data?.message || err.message || "์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์‹คํŒจ"; return { success: false, error: errorMessage }; } } @@ -139,12 +130,20 @@ export async function previewNumberingCode( /** * ์ฝ”๋“œ ํ• ๋‹น (์ €์žฅ ์‹œ์ ์— ์‹ค์ œ ์ˆœ๋ฒˆ ์ฆ๊ฐ€) * ์‹ค์ œ ์ €์žฅํ•  ๋•Œ๋งŒ ํ˜ธ์ถœ + * @param ruleId ์ฑ„๋ฒˆ ๊ทœ์น™ ID + * @param userInputCode ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘ํ•œ ์ตœ์ข… ์ฝ”๋“œ (์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ถ”์ถœ์šฉ) + * @param formData ํผ ๋ฐ์ดํ„ฐ (์นดํ…Œ๊ณ ๋ฆฌ/๋‚ ์งœ ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ์šฉ) */ export async function allocateNumberingCode( - ruleId: string + ruleId: string, + userInputCode?: string, + formData?: Record, ): Promise> { try { - const response = await apiClient.post(`/numbering-rules/${ruleId}/allocate`); + const response = await apiClient.post(`/numbering-rules/${ruleId}/allocate`, { + userInputCode, + formData, + }); return response.data; } catch (error: any) { return { success: false, error: error.message || "์ฝ”๋“œ ํ• ๋‹น ์‹คํŒจ" }; @@ -154,9 +153,7 @@ export async function allocateNumberingCode( /** * @deprecated ๊ธฐ์กด generateNumberingCode๋Š” previewNumberingCode๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š” */ -export async function generateNumberingCode( - ruleId: string -): Promise> { +export async function generateNumberingCode(ruleId: string): Promise> { console.warn("generateNumberingCode๋Š” deprecated. previewNumberingCode ์‚ฌ์šฉ ๊ถŒ์žฅ"); return previewNumberingCode(ruleId); } @@ -173,20 +170,16 @@ export async function resetSequence(ruleId: string): Promise> } } -// ====== ํ…Œ์ŠคํŠธ์šฉ API (numbering_rules_test ํ…Œ์ด๋ธ” ์‚ฌ์šฉ) ====== +// ====== ํ…Œ์ŠคํŠธ์šฉ API (numbering_rules ํ…Œ์ด๋ธ” ์‚ฌ์šฉ) ====== /** * [ํ…Œ์ŠคํŠธ] ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ”์—์„œ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ - * numbering_rules_test ํ…Œ์ด๋ธ” ์‚ฌ์šฉ + * numbering_rules ํ…Œ์ด๋ธ” ์‚ฌ์šฉ * @param menuObjid ๋ฉ”๋‰ด OBJID (์„ ํƒ) - ํ•„ํ„ฐ๋ง์šฉ */ -export async function getNumberingRulesFromTest( - menuObjid?: number -): Promise> { +export async function getNumberingRulesFromTest(menuObjid?: number): Promise> { try { - const url = menuObjid - ? `/numbering-rules/test/list/${menuObjid}` - : "/numbering-rules/test/list"; + const url = menuObjid ? `/numbering-rules/test/list/${menuObjid}` : "/numbering-rules/test/list"; const response = await apiClient.get(url); return response.data; } catch (error: any) { @@ -199,11 +192,11 @@ export async function getNumberingRulesFromTest( /** * [ํ…Œ์ŠคํŠธ] ํ…Œ์ด๋ธ”+์ปฌ๋Ÿผ ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ๊ทœ์น™ ์กฐํšŒ - * numbering_rules_test ํ…Œ์ด๋ธ” ์‚ฌ์šฉ + * numbering_rules ํ…Œ์ด๋ธ” ์‚ฌ์šฉ */ export async function getNumberingRuleByColumn( tableName: string, - columnName: string + columnName: string, ): Promise> { try { const response = await apiClient.get("/numbering-rules/test/by-column", { @@ -220,11 +213,9 @@ export async function getNumberingRuleByColumn( /** * [ํ…Œ์ŠคํŠธ] ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ”์— ์ฑ„๋ฒˆ๊ทœ์น™ ์ €์žฅ - * numbering_rules_test ํ…Œ์ด๋ธ” ์‚ฌ์šฉ + * numbering_rules ํ…Œ์ด๋ธ” ์‚ฌ์šฉ */ -export async function saveNumberingRuleToTest( - config: NumberingRuleConfig -): Promise> { +export async function saveNumberingRuleToTest(config: NumberingRuleConfig): Promise> { try { const response = await apiClient.post("/numbering-rules/test/save", config); return response.data; @@ -238,11 +229,9 @@ export async function saveNumberingRuleToTest( /** * [ํ…Œ์ŠคํŠธ] ํ…Œ์ŠคํŠธ ํ…Œ์ด๋ธ”์—์„œ ์ฑ„๋ฒˆ๊ทœ์น™ ์‚ญ์ œ - * numbering_rules_test ํ…Œ์ด๋ธ” ์‚ฌ์šฉ + * numbering_rules ํ…Œ์ด๋ธ” ์‚ฌ์šฉ */ -export async function deleteNumberingRuleFromTest( - ruleId: string -): Promise> { +export async function deleteNumberingRuleFromTest(ruleId: string): Promise> { try { const response = await apiClient.delete(`/numbering-rules/test/${ruleId}`); return response.data; @@ -262,7 +251,7 @@ export async function getNumberingRuleByColumnWithCategory( tableName: string, columnName: string, categoryColumn?: string, - categoryValueId?: number + categoryValueId?: number, ): Promise> { try { const response = await apiClient.get("/numbering-rules/test/by-column-with-category", { @@ -282,7 +271,7 @@ export async function getNumberingRuleByColumnWithCategory( */ export async function getRulesByTableColumn( tableName: string, - columnName: string + columnName: string, ): Promise> { try { const response = await apiClient.get("/numbering-rules/test/rules-by-table-column", { @@ -296,4 +285,3 @@ export async function getRulesByTableColumn( }; } } - diff --git a/frontend/lib/api/screen.ts b/frontend/lib/api/screen.ts index c610da30..597e9795 100644 --- a/frontend/lib/api/screen.ts +++ b/frontend/lib/api/screen.ts @@ -373,12 +373,10 @@ export const tableTypeApi = { columnName: string, webType: string, detailSettings?: Record, - inputType?: "direct" | "auto", ): Promise => { await apiClient.put(`/table-management/tables/${tableName}/columns/${columnName}/web-type`, { webType, detailSettings, - inputType, }); }, diff --git a/frontend/lib/registry/ComponentRegistry.ts b/frontend/lib/registry/ComponentRegistry.ts index 00866c68..f6065ff5 100644 --- a/frontend/lib/registry/ComponentRegistry.ts +++ b/frontend/lib/registry/ComponentRegistry.ts @@ -31,10 +31,7 @@ export class ComponentRegistry { throw new Error(`์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก ์‹คํŒจ (${definition.id}): ${validation.errors.join(", ")}`); } - // ์ค‘๋ณต ๋“ฑ๋ก ์ฒดํฌ - if (this.components.has(definition.id)) { - console.warn(`โš ๏ธ ์ปดํฌ๋„ŒํŠธ ์ค‘๋ณต ๋“ฑ๋ก: ${definition.id} - ๊ธฐ์กด ์ •์˜๋ฅผ ๋ฎ์–ด์”๋‹ˆ๋‹ค.`); - } + // ์ค‘๋ณต ๋“ฑ๋ก ์ฒดํฌ (๊ธฐ์กด ์ •์˜๋ฅผ ๋ฎ์–ด์”€) // ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”๊ฐ€ const enhancedDefinition = { @@ -64,7 +61,6 @@ export class ComponentRegistry { static unregisterComponent(id: string): void { const definition = this.components.get(id); if (!definition) { - console.warn(`โš ๏ธ ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์ปดํฌ๋„ŒํŠธ ํ•ด์ œ ์‹œ๋„: ${id}`); return; } @@ -76,8 +72,6 @@ export class ComponentRegistry { data: definition, timestamp: new Date(), }); - - console.log(`๐Ÿ—‘๏ธ ์ปดํฌ๋„ŒํŠธ ํ•ด์ œ: ${id}`); } /** @@ -355,7 +349,6 @@ export class ComponentRegistry { }, force: async () => { // hotReload ๊ธฐ๋Šฅ ๋น„ํ™œ์„ฑํ™” (๋ถˆํ•„์š”) - console.log("โš ๏ธ ๊ฐ•์ œ Hot Reload๋Š” ๋” ์ด์ƒ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"); }, }, diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 23b684ac..e6b13067 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -143,6 +143,9 @@ export interface DynamicComponentRendererProps { // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ฝœ๋ฐฑ onSelectTabComponent?: (tabId: string, compId: string, comp: any) => void; selectedTabComponentId?: string; + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ฝœ๋ฐฑ + onSelectPanelComponent?: (panelSide: "left" | "right", compId: string, comp: any) => void; + selectedPanelComponentId?: string; flowSelectedStepId?: number | null; onFlowSelectedDataChange?: (selectedData: any[], stepId: number | null) => void; // ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ ํ‚ค @@ -173,7 +176,15 @@ export const DynamicComponentRenderer: React.FC = ...props }) => { // ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ์ถ”์ถœ - ์ƒˆ ์‹œ์Šคํ…œ์—์„œ๋Š” componentType ์†์„ฑ ์‚ฌ์šฉ, ๋ ˆ๊ฑฐ์‹œ๋Š” type ์‚ฌ์šฉ - const rawComponentType = (component as any).componentType || component.type; + // ๐Ÿ†• V2 ๋ ˆ์ด์•„์›ƒ์˜ ๊ฒฝ์šฐ url์—์„œ ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ์ถ”์ถœ (์˜ˆ: "@/lib/registry/components/v2-input" โ†’ "v2-input") + const extractTypeFromUrl = (url: string | undefined): string | undefined => { + if (!url) return undefined; + // url์˜ ๋งˆ์ง€๋ง‰ ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ปดํฌ๋„ŒํŠธ ํƒ€์ž…์œผ๋กœ ์‚ฌ์šฉ + const segments = url.split("/"); + return segments[segments.length - 1]; + }; + + const rawComponentType = (component as any).componentType || component.type || extractTypeFromUrl((component as any).url); // ๋ ˆ๊ฑฐ์‹œ ํƒ€์ž…์„ v2 ์ปดํฌ๋„ŒํŠธ๋กœ ๋งคํ•‘ (v2 ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์—†์œผ๋ฉด ์›๋ณธ ์œ ์ง€) const mapToV2ComponentType = (type: string | undefined): string | undefined => { @@ -196,6 +207,62 @@ export const DynamicComponentRenderer: React.FC = // ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ๋ณ€ํ™˜ ์™„๋ฃŒ + // ๐Ÿ†• ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง ์ฒดํฌ (conditionalConfig) + // componentConfig ๋˜๋Š” overrides์—์„œ conditionalConfig๋ฅผ ๊ฐ€์ ธ์™€์„œ formData์™€ ๋น„๊ต + const conditionalConfig = (component as any).componentConfig?.conditionalConfig || (component as any).overrides?.conditionalConfig; + + // ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง ์ฒ˜๋ฆฌ + if (conditionalConfig?.enabled && props.formData) { + const { field, operator, value, action } = conditionalConfig; + const fieldValue = props.formData[field]; + + // ์กฐ๊ฑด ํ‰๊ฐ€ + let conditionMet = false; + switch (operator) { + case "=": + case "==": + case "===": + conditionMet = fieldValue === value; + break; + case "!=": + case "!==": + conditionMet = fieldValue !== value; + break; + case ">": + conditionMet = Number(fieldValue) > Number(value); + break; + case "<": + conditionMet = Number(fieldValue) < Number(value); + break; + case ">=": + conditionMet = Number(fieldValue) >= Number(value); + break; + case "<=": + conditionMet = Number(fieldValue) <= Number(value); + break; + case "contains": + conditionMet = String(fieldValue || "").includes(String(value)); + break; + case "empty": + conditionMet = !fieldValue || fieldValue === ""; + break; + case "notEmpty": + conditionMet = !!fieldValue && fieldValue !== ""; + break; + default: + conditionMet = fieldValue === value; + } + + // ์•ก์…˜์— ๋”ฐ๋ผ ๋ Œ๋”๋ง ๊ฒฐ์ • + if (action === "show" && !conditionMet) { + return null; + } + if (action === "hide" && conditionMet) { + return null; + } + // "enable"/"disable" ์•ก์…˜์€ conditionalDisabled props๋กœ ์ „๋‹ฌ + } + // ๐Ÿ†• ๋ชจ๋“  v2- ์ปดํฌ๋„ŒํŠธ๋Š” ComponentRegistry์—์„œ ํ†ตํ•ฉ ์ฒ˜๋ฆฌ // (v2-input, v2-select, v2-repeat-container ๋“ฑ ๋ชจ๋‘ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌ) @@ -204,16 +271,66 @@ export const DynamicComponentRenderer: React.FC = const webType = (component as any).componentConfig?.webType; const tableName = (component as any).tableName; const columnName = (component as any).columnName; - + // ์นดํ…Œ๊ณ ๋ฆฌ ์…€๋ ‰ํŠธ: webType์ด "category"์ด๊ณ  tableName๊ณผ columnName์ด ์žˆ๋Š” ๊ฒฝ์šฐ๋งŒ - // โš ๏ธ ๋‹จ, componentType์ด "select-basic"์ธ ๊ฒฝ์šฐ๋Š” ComponentRegistry๋กœ ์ฒ˜๋ฆฌ (๋‹ค์ค‘์„ ํƒ ๋“ฑ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ์ง€์›) + // โš ๏ธ ๋‹จ, ๋‹ค์Œ ๊ฒฝ์šฐ๋Š” V2SelectRenderer๋กœ ์ง์ ‘ ์ฒ˜๋ฆฌ (๊ณ ๊ธ‰ ๋ชจ๋“œ ์ง€์›): + // 1. componentType์ด "select-basic" ๋˜๋Š” "v2-select"์ธ ๊ฒฝ์šฐ + // 2. config.mode๊ฐ€ dropdown์ด ์•„๋‹Œ ๊ฒฝ์šฐ (radio, check, tagbox ๋“ฑ) + const componentMode = (component as any).componentConfig?.mode || (component as any).config?.mode; + const nonDropdownModes = ["radio", "check", "checkbox", "tag", "tagbox", "toggle", "swap", "combobox"]; + const isNonDropdownMode = componentMode && nonDropdownModes.includes(componentMode); + const shouldUseV2Select = componentType === "select-basic" || componentType === "v2-select" || isNonDropdownMode; + if ( (inputType === "category" || webType === "category") && tableName && columnName && - componentType === "select-basic" + shouldUseV2Select ) { - // select-basic์€ ComponentRegistry์—์„œ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์•„๋ž˜๋กœ ํ†ต๊ณผ + // V2SelectRenderer๋กœ ์ง์ ‘ ๋ Œ๋”๋ง (์นดํ…Œ๊ณ ๋ฆฌ + ๊ณ ๊ธ‰ ๋ชจ๋“œ) + try { + const { V2SelectRenderer } = require("@/lib/registry/components/v2-select/V2SelectRenderer"); + const fieldName = columnName || component.id; + const currentValue = props.formData?.[fieldName] || ""; + + const handleChange = (value: any) => { + if (props.onFormDataChange) { + props.onFormDataChange(fieldName, value); + } + }; + + // V2SelectRenderer์šฉ ์ปดํฌ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ + const selectComponent = { + ...component, + componentConfig: { + ...component.componentConfig, + mode: componentMode || "dropdown", + source: "category", + categoryTable: tableName, + categoryColumn: columnName, + }, + tableName, + columnName, + inputType: "category", + webType: "category", + }; + + const rendererProps = { + component: selectComponent, + formData: props.formData, + onFormDataChange: props.onFormDataChange, + isDesignMode: props.isDesignMode, + isInteractive: props.isInteractive ?? !props.isDesignMode, + tableName, + style: (component as any).style, + size: (component as any).size, + }; + + const rendererInstance = new V2SelectRenderer(rendererProps); + return rendererInstance.render(); + } catch (error) { + console.error("โŒ V2SelectRenderer ๋กœ๋“œ ์‹คํŒจ:", error); + } } else if ((inputType === "category" || webType === "category") && tableName && columnName) { try { const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent"); @@ -230,6 +347,12 @@ export const DynamicComponentRenderer: React.FC = const isFieldDisabled = props.disabledFields?.includes(columnName) || (component as any).disabled; const isFieldReadonly = (component as any).readonly || (component as any).componentConfig?.readonly; + // ๐Ÿ”ง ๋†’์ด ๊ณ„์‚ฐ: component.size์—์„œ height ์ถ”์ถœ + const categorySize = (component as any).size; + const categoryStyle = (component as any).style; + const categoryLabel = (component as any).label; + const categoryId = component.id; + return ( = disabled={isFieldDisabled} readonly={isFieldReadonly} className="w-full" + size={categorySize} + style={categoryStyle} + label={categoryLabel} + id={categoryId} + isDesignMode={props.isDesignMode} /> ); } catch (error) { @@ -332,7 +460,7 @@ export const DynamicComponentRenderer: React.FC = const safeProps = filterDOMProps(restProps); // ์ปดํฌ๋„ŒํŠธ์˜ columnName์— ํ•ด๋‹นํ•˜๋Š” formData ๊ฐ’ ์ถ”์ถœ - const fieldName = (component as any).columnName || component.id; + const fieldName = (component as any).columnName || (component as any).componentConfig?.columnName || component.id; // ๋‹ค์ค‘ ๋ ˆ์ฝ”๋“œ๋ฅผ ๋‹ค๋ฃจ๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ๋กœ ์ดˆ๊ธฐํ™” let currentValue; @@ -401,10 +529,26 @@ export const DynamicComponentRenderer: React.FC = }; // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰ ์ปดํฌ๋„ŒํŠธ๋Š” componentConfig.tableName์„ ์‚ฌ์šฉํ•ด์•ผ ํ•จ (ํ™”๋ฉด ํ…Œ์ด๋ธ”์ด ์•„๋‹Œ ๊ฒ€์ƒ‰ ๋Œ€์ƒ ํ…Œ์ด๋ธ”) + // ๐Ÿ†• v2-input๋„ ํฌํ•จ (์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹œ tableName ํ•„์š”) const useConfigTableName = componentType === "entity-search-input" || componentType === "autocomplete-search-input" || - componentType === "modal-repeater-table"; + componentType === "modal-repeater-table" || + componentType === "v2-input"; + + // ๐Ÿ†• v2-input ๋“ฑ์˜ ๋ผ๋ฒจ ํ‘œ์‹œ ๋กœ์ง (labelDisplay๊ฐ€ true์ผ ๋•Œ๋งŒ ๋ผ๋ฒจ ํ‘œ์‹œ) + const labelDisplay = component.style?.labelDisplay ?? (component as any).labelDisplay; + const effectiveLabel = labelDisplay === true + ? (component.style?.labelText || (component as any).label || component.componentConfig?.label) + : undefined; + + // ๐Ÿ”ง ์ˆœ์„œ ์ค‘์š”! component.style ๋จผ์ €, CSS ํฌ๊ธฐ ์†์„ฑ์€ size ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฎ์–ด์”€ + const mergedStyle = { + ...component.style, // ์›๋ณธ style (labelDisplay, labelText ๋“ฑ) - ๋จผ์ €! + // CSS ํฌ๊ธฐ ์†์„ฑ์€ size์—์„œ ๊ณ„์‚ฐํ•œ ๊ฐ’์œผ๋กœ ๋ช…์‹œ์  ๋ฎ์–ด์“ฐ๊ธฐ (์šฐ์„ ์ˆœ์œ„ ์ตœ๊ณ ) + width: finalStyle.width, + height: finalStyle.height, + }; const rendererProps = { component, @@ -414,14 +558,29 @@ export const DynamicComponentRenderer: React.FC = onDragEnd, size: component.size || newComponent.defaultSize, position: component.position, - style: finalStyle, // size๋ฅผ ํฌํ•จํ•œ ์ตœ์ข… style config: component.componentConfig, componentConfig: component.componentConfig, // componentConfig์˜ ๋ชจ๋“  ์†์„ฑ์„ props๋กœ spread (tableName, displayField ๋“ฑ) ...(component.componentConfig || {}), + // ๐Ÿ”ง style์€ ๋งจ ๋งˆ์ง€๋ง‰์—! (componentConfig.style์ด ์žˆ์–ด๋„ mergedStyle์ด ์šฐ์„ ) + style: mergedStyle, + // ๐Ÿ†• ๋ผ๋ฒจ ํ‘œ์‹œ (labelDisplay๊ฐ€ true์ผ ๋•Œ๋งŒ) + label: effectiveLabel, + // ๐Ÿ†• V2 ๋ ˆ์ด์•„์›ƒ์—์„œ overrides์—์„œ ๋ณต์›๋œ ์ƒ์œ„ ๋ ˆ๋ฒจ ์†์„ฑ๋“ค๋„ ์ „๋‹ฌ + inputType: (component as any).inputType || component.componentConfig?.inputType, + columnName: (component as any).columnName || component.componentConfig?.columnName, value: currentValue, // formData์—์„œ ์ถ”์ถœํ•œ ํ˜„์žฌ ๊ฐ’ ์ „๋‹ฌ // ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ๋“ค ์ „๋‹ฌ - autoGeneration: component.autoGeneration || component.componentConfig?.autoGeneration, + // ๐Ÿ†• webTypeConfig.numberingRuleId๊ฐ€ ์žˆ์œผ๋ฉด autoGeneration์œผ๋กœ ๋ณ€ํ™˜ + autoGeneration: component.autoGeneration || + component.componentConfig?.autoGeneration || + ((component as any).webTypeConfig?.numberingRuleId ? { + type: "numbering_rule" as const, + enabled: true, + options: { + numberingRuleId: (component as any).webTypeConfig.numberingRuleId, + }, + } : undefined), hidden: hiddenValue, // React ์ „์šฉ props๋“ค์€ ์ง์ ‘ ์ „๋‹ฌ (DOM์— ์ „๋‹ฌ๋˜์ง€ ์•Š์Œ) isInteractive, @@ -429,7 +588,10 @@ export const DynamicComponentRenderer: React.FC = onFormDataChange, onChange: handleChange, // ๊ฐœ์„ ๋œ onChange ํ•ธ๋“ค๋Ÿฌ ์ „๋‹ฌ // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰ ์ปดํฌ๋„ŒํŠธ๋Š” componentConfig.tableName ์œ ์ง€, ๊ทธ ์™ธ๋Š” ํ™”๋ฉด ํ…Œ์ด๋ธ”๋ช… ์‚ฌ์šฉ - tableName: useConfigTableName ? component.componentConfig?.tableName || tableName : tableName, + // ๐Ÿ†• component.tableName๋„ ํ™•์ธ (V2 ๋ ˆ์ด์•„์›ƒ์—์„œ overrides.tableName์ด ๋ณต์›๋จ) + tableName: useConfigTableName + ? component.componentConfig?.tableName || (component as any).tableName || tableName + : tableName, menuId, // ๐Ÿ†• ๋ฉ”๋‰ด ID menuObjid, // ๐Ÿ†• ๋ฉ”๋‰ด OBJID (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) selectedScreen, // ๐Ÿ†• ํ™”๋ฉด ์ •๋ณด @@ -494,6 +656,9 @@ export const DynamicComponentRenderer: React.FC = // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ฝœ๋ฐฑ onSelectTabComponent: props.onSelectTabComponent, selectedTabComponentId: props.selectedTabComponentId, + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ฝœ๋ฐฑ + onSelectPanelComponent: props.onSelectPanelComponent, + selectedPanelComponentId: props.selectedPanelComponentId, }; // ๋ Œ๋”๋Ÿฌ๊ฐ€ ํด๋ž˜์Šค์ธ์ง€ ํ•จ์ˆ˜์ธ์ง€ ํ™•์ธ diff --git a/frontend/lib/registry/components/category-select/CategorySelectComponent.tsx b/frontend/lib/registry/components/category-select/CategorySelectComponent.tsx index 001b68e3..ba5d752d 100644 --- a/frontend/lib/registry/components/category-select/CategorySelectComponent.tsx +++ b/frontend/lib/registry/components/category-select/CategorySelectComponent.tsx @@ -8,6 +8,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Label } from "@/components/ui/label"; import { getCategoryValues } from "@/lib/api/tableCategoryValue"; import { TableCategoryValue } from "@/types/tableCategoryValue"; import { Loader2 } from "lucide-react"; @@ -23,6 +24,20 @@ interface CategorySelectComponentProps { readonly?: boolean; tableName?: string; columnName?: string; + // ๐Ÿ”ง ๋†’์ด ์กฐ์ ˆ์„ ์œ„ํ•œ props ์ถ”๊ฐ€ + style?: React.CSSProperties & { + labelDisplay?: boolean; + labelFontSize?: string | number; + labelColor?: string; + labelFontWeight?: string | number; + labelMarginBottom?: string | number; + }; + size?: { width?: number | string; height?: number | string }; + // ๐Ÿ”ง ๋ผ๋ฒจ ํ‘œ์‹œ๋ฅผ ์œ„ํ•œ props ์ถ”๊ฐ€ + label?: string; + id?: string; + // ๐Ÿ”ง ๋””์ž์ธ ๋ชจ๋“œ ์ฒ˜๋ฆฌ + isDesignMode?: boolean; } /** @@ -43,7 +58,27 @@ export const CategorySelectComponent: React.FC< readonly = false, tableName: propTableName, columnName: propColumnName, + style, + size, + label: propLabel, + id: propId, + isDesignMode = false, }) => { + // ๐Ÿ”ง ๋†’์ด ๊ณ„์‚ฐ: size.height > style.height > ๊ธฐ๋ณธ๊ฐ’(40px) + const componentHeight = size?.height || style?.height; + const heightStyle: React.CSSProperties = componentHeight + ? { height: componentHeight } + : {}; + + // ๐Ÿ”ง ๋ผ๋ฒจ ๊ด€๋ จ ๊ณ„์‚ฐ + const label = propLabel || component?.label; + const id = propId || component?.id; + const showLabel = label && style?.labelDisplay !== false; + + // ๋ผ๋ฒจ ๋†’์ด ๊ณ„์‚ฐ (๊ธฐ๋ณธ 20px) + const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14; + const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4; + const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2; const [categoryValues, setCategoryValues] = useState( [] ); @@ -56,11 +91,6 @@ export const CategorySelectComponent: React.FC< useEffect(() => { if (!tableName || !columnName) { - console.warn("CategorySelectComponent: tableName ๋˜๋Š” columnName์ด ์—†์Šต๋‹ˆ๋‹ค", { - tableName, - columnName, - component, - }); return; } @@ -93,16 +123,52 @@ export const CategorySelectComponent: React.FC< }; const handleValueChange = (newValue: string) => { - console.log("๐Ÿ”„ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ณ€๊ฒฝ:", { oldValue: value, newValue }); onChange?.(newValue); }; + // ๐Ÿ”ง ๊ณตํ†ต ๋ผ๋ฒจ ๋ Œ๋”๋ง ํ•จ์ˆ˜ + const renderLabel = () => { + if (!showLabel) return null; + return ( + + ); + }; + + // ๐Ÿ”ง ๊ณตํ†ต wrapper ์Šคํƒ€์ผ + const wrapperStyle: React.CSSProperties = { + width: size?.width || style?.width, + height: componentHeight, + }; + + // ๐Ÿ”ง ๋””์ž์ธ ๋ชจ๋“œ์ผ ๋•Œ ํด๋ฆญ ๋ฐฉ์ง€ + const designModeClass = isDesignMode ? "pointer-events-none" : ""; + // ๋กœ๋”ฉ ์ค‘ if (isLoading) { return ( -
- - ๋กœ๋”ฉ ์ค‘... +
+ {renderLabel()} +
+ + ๋กœ๋”ฉ ์ค‘... +
); } @@ -110,8 +176,14 @@ export const CategorySelectComponent: React.FC< // ์—๋Ÿฌ if (error) { return ( -
- โš ๏ธ {error} +
+ {renderLabel()} +
+ โš ๏ธ {error} +
); } @@ -119,33 +191,44 @@ export const CategorySelectComponent: React.FC< // ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์ด ์—†์Œ if (categoryValues.length === 0) { return ( -
- ์„ค์ •๋œ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์ด ์—†์Šต๋‹ˆ๋‹ค +
+ {renderLabel()} +
+ ์„ค์ •๋œ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์ด ์—†์Šต๋‹ˆ๋‹ค +
); } return ( - +
+ {renderLabel()} +
+ +
+
); }; diff --git a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx index dd4f4c6b..f655ebe3 100644 --- a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx +++ b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx @@ -177,10 +177,18 @@ const FileUploadComponent: React.FC = ({ // ๐Ÿ”‘ ๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์‚ฌ์šฉ const backupKey = getUniqueKey(); const backupFiles = localStorage.getItem(backupKey); + console.log("๐Ÿ”Ž [DEBUG-MOUNT] localStorage ํ™•์ธ:", { + backupKey, + hasBackup: !!backupFiles, + componentId: component.id, + recordId: recordId, + formDataId: formData?.id, + stackTrace: new Error().stack?.split('\n').slice(1, 4).join(' <- '), + }); if (backupFiles) { const parsedFiles = JSON.parse(backupFiles); if (parsedFiles.length > 0) { - console.log("๐Ÿš€ ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ํŒŒ์ผ ์ฆ‰์‹œ ๋ณต์›:", { + console.log("๐Ÿš€ [DEBUG-MOUNT] ํŒŒ์ผ ์ฆ‰์‹œ ๋ณต์›:", { uniqueKey: backupKey, componentId: component.id, recordId: recordId, @@ -203,6 +211,50 @@ const FileUploadComponent: React.FC = ({ } }, [component.id, getUniqueKey, recordId]); // ๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ๋ณ€๊ฒฝ ์‹œ ์žฌ์‹คํ–‰ + // ๐Ÿ†• ๋ชจ๋‹ฌ ๋‹ซํž˜/์ €์žฅ ์„ฑ๊ณต ์‹œ localStorage ํŒŒ์ผ ์บ์‹œ ์ •๋ฆฌ (๋“ฑ๋ก ํ›„ ์žฌ๋“ฑ๋ก ์‹œ ์ด์ „ ํŒŒ์ผ ์ž”์กด ๋ฐฉ์ง€) + useEffect(() => { + const handleClearFileCache = (event: Event) => { + const backupKey = getUniqueKey(); + const eventType = event.type; + console.log("๐Ÿงน [DEBUG-CLEAR] ํŒŒ์ผ ์บ์‹œ ์ •๋ฆฌ ์ด๋ฒคํŠธ ์ˆ˜์‹ :", { + eventType, + backupKey, + componentId: component.id, + currentFiles: uploadedFiles.length, + hasLocalStorage: !!localStorage.getItem(backupKey), + }); + try { + localStorage.removeItem(backupKey); + setUploadedFiles([]); + setRepresentativeImageUrl(null); + if (typeof window !== "undefined") { + const globalFileState = (window as any).globalFileState || {}; + delete globalFileState[backupKey]; + (window as any).globalFileState = globalFileState; + } + console.log("๐Ÿงน [DEBUG-CLEAR] ์ •๋ฆฌ ์™„๋ฃŒ:", backupKey); + } catch (e) { + console.warn("ํŒŒ์ผ ์บ์‹œ ์ •๋ฆฌ ์‹คํŒจ:", e); + } + }; + + // EditModal ๋‹ซํž˜, ScreenModal ์—ฐ์† ๋“ฑ๋ก ์ €์žฅ ์„ฑ๊ณต, ์ผ๋ฐ˜ ์ €์žฅ ์„ฑ๊ณต ๋ชจ๋‘ ์ฒ˜๋ฆฌ + window.addEventListener("closeEditModal", handleClearFileCache); + window.addEventListener("saveSuccess", handleClearFileCache); + window.addEventListener("saveSuccessInModal", handleClearFileCache); + + console.log("๐Ÿ”Ž [DEBUG-CLEAR] ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก ์™„๋ฃŒ:", { + componentId: component.id, + backupKey: getUniqueKey(), + }); + + return () => { + window.removeEventListener("closeEditModal", handleClearFileCache); + window.removeEventListener("saveSuccess", handleClearFileCache); + window.removeEventListener("saveSuccessInModal", handleClearFileCache); + }; + }, [getUniqueKey]); + // ๐ŸŽฏ ํ™”๋ฉด์„ค๊ณ„ ๋ชจ๋“œ์—์„œ ์‹ค์ œ ํ™”๋ฉด์œผ๋กœ์˜ ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™” ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ useEffect(() => { const handleDesignModeFileChange = (event: CustomEvent) => { @@ -363,6 +415,12 @@ const FileUploadComponent: React.FC = ({ console.warn("ํŒŒ์ผ ๋ณ‘ํ•ฉ ์ค‘ ์˜ค๋ฅ˜:", e); } + console.log("๐Ÿ”Ž [DEBUG-LOAD] API ์‘๋‹ต ํ›„ ํŒŒ์ผ ์„ค์ •:", { + componentId: component.id, + serverFiles: formattedFiles.length, + finalFiles: finalFiles.length, + files: finalFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })), + }); setUploadedFiles(finalFiles); // ์ „์—ญ ์ƒํƒœ์—๋„ ์ €์žฅ (๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์‚ฌ์šฉ) diff --git a/frontend/lib/registry/components/file-upload/FileViewerModal.tsx b/frontend/lib/registry/components/file-upload/FileViewerModal.tsx index 9eb0edeb..36e37044 100644 --- a/frontend/lib/registry/components/file-upload/FileViewerModal.tsx +++ b/frontend/lib/registry/components/file-upload/FileViewerModal.tsx @@ -284,7 +284,9 @@ export const FileViewerModal: React.FC = ({ file, isOpen, } } else { // ๊ธฐํƒ€ ํŒŒ์ผ์€ ๋‹ค์šด๋กœ๋“œ URL ์‚ฌ์šฉ - const url = `${API_BASE_URL.replace("/api", "")}/api/files/download/${file.objid}`; + // ์ฃผ์˜: ํ”„๋กœ๋•์…˜ URL์ด https://api.vexplor.com/api ์ด๋ฏ€๋กœ + // ๋์˜ /api๋งŒ ์ œ๊ฑฐํ•ด์•ผ ํ˜ธ์ŠคํŠธ๋ช…์ด ๊นจ์ง€์ง€ ์•Š์Œ + const url = `${API_BASE_URL.replace(/\/api$/, "")}/api/files/download/${file.objid}`; setPreviewUrl(url); } } else { diff --git a/frontend/lib/registry/components/file-upload/index.ts b/frontend/lib/registry/components/file-upload/index.ts index fcca65cc..3f059ae1 100644 --- a/frontend/lib/registry/components/file-upload/index.ts +++ b/frontend/lib/registry/components/file-upload/index.ts @@ -14,22 +14,23 @@ import { FileUploadConfig } from "./types"; */ export const FileUploadDefinition = createComponentDefinition({ id: "file-upload", - name: "ํŒŒ์ผ ์—…๋กœ๋“œ", - nameEng: "FileUpload Component", - description: "ํŒŒ์ผ ์—…๋กœ๋“œ๋ฅผ ์œ„ํ•œ ํŒŒ์ผ ์„ ํƒ ์ปดํฌ๋„ŒํŠธ", + name: "ํŒŒ์ผ ์—…๋กœ๋“œ (๋ ˆ๊ฑฐ์‹œ)", + nameEng: "FileUpload Component (Legacy)", + description: "ํŒŒ์ผ ์—…๋กœ๋“œ๋ฅผ ์œ„ํ•œ ํŒŒ์ผ ์„ ํƒ ์ปดํฌ๋„ŒํŠธ (๋ ˆ๊ฑฐ์‹œ)", category: ComponentCategory.INPUT, webType: "file", component: FileUploadComponent, defaultConfig: { placeholder: "์ž…๋ ฅํ•˜์„ธ์š”", }, - defaultSize: { width: 350, height: 240 }, // 40 * 6 (ํŒŒ์ผ ์„ ํƒ + ๋ชฉ๋ก ํ‘œ์‹œ) + defaultSize: { width: 350, height: 240 }, configPanel: FileUploadConfigPanel, icon: "Edit", tags: [], version: "1.0.0", author: "๊ฐœ๋ฐœํŒ€", documentation: "https://docs.example.com/components/file-upload", + hidden: true, // v2-file-upload ์‚ฌ์šฉ์œผ๋กœ ํŒจ๋„์—์„œ ์ˆจ๊น€ }); // ํƒ€์ž… ๋‚ด๋ณด๋‚ด๊ธฐ diff --git a/frontend/lib/registry/components/image-widget/index.ts b/frontend/lib/registry/components/image-widget/index.ts index 67abbc80..aee663e8 100644 --- a/frontend/lib/registry/components/image-widget/index.ts +++ b/frontend/lib/registry/components/image-widget/index.ts @@ -13,9 +13,9 @@ import { ImageWidgetConfigPanel } from "./ImageWidgetConfigPanel"; */ export const ImageWidgetDefinition = createComponentDefinition({ id: "image-widget", - name: "์ด๋ฏธ์ง€ ์œ„์ ฏ", - nameEng: "Image Widget", - description: "์ด๋ฏธ์ง€ ํ‘œ์‹œ ๋ฐ ์—…๋กœ๋“œ", + name: "์ด๋ฏธ์ง€ ์œ„์ ฏ (๋ ˆ๊ฑฐ์‹œ)", + nameEng: "Image Widget (Legacy)", + description: "์ด๋ฏธ์ง€ ํ‘œ์‹œ ๋ฐ ์—…๋กœ๋“œ (๋ ˆ๊ฑฐ์‹œ)", category: ComponentCategory.INPUT, webType: "image", component: ImageWidget, @@ -32,6 +32,7 @@ export const ImageWidgetDefinition = createComponentDefinition({ version: "1.0.0", author: "๊ฐœ๋ฐœํŒ€", documentation: "https://docs.example.com/components/image-widget", + hidden: true, // v2-file-upload ์‚ฌ์šฉ์œผ๋กœ ํŒจ๋„์—์„œ ์ˆจ๊น€ }); // ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ณด๋‚ด๊ธฐ diff --git a/frontend/lib/registry/components/index.ts b/frontend/lib/registry/components/index.ts index 8a2ac932..172f0067 100644 --- a/frontend/lib/registry/components/index.ts +++ b/frontend/lib/registry/components/index.ts @@ -105,7 +105,13 @@ import "./v2-location-swap-selector/LocationSwapSelectorRenderer"; import "./v2-table-search-widget"; import "./v2-tabs-widget/tabs-component"; import "./v2-category-manager/V2CategoryManagerRenderer"; -import "./v2-media"; // ํ†ตํ•ฉ ๋ฏธ๋””์–ด ์ปดํฌ๋„ŒํŠธ +import "./v2-media/V2MediaRenderer"; // V2 ํ†ตํ•ฉ ๋ฏธ๋””์–ด ์ปดํฌ๋„ŒํŠธ +import "./v2-table-grouped/TableGroupedRenderer"; // ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” +import "./v2-timeline-scheduler/TimelineSchedulerRenderer"; // ํƒ€์ž„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ +import "./v2-input/V2InputRenderer"; // V2 ํ†ตํ•ฉ ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ +import "./v2-select/V2SelectRenderer"; // V2 ํ†ตํ•ฉ ์„ ํƒ ์ปดํฌ๋„ŒํŠธ +import "./v2-date/V2DateRenderer"; // V2 ํ†ตํ•ฉ ๋‚ ์งœ ์ปดํฌ๋„ŒํŠธ +import "./v2-file-upload/V2FileUploadRenderer"; // V2 ํŒŒ์ผ ์—…๋กœ๋“œ ์ปดํฌ๋„ŒํŠธ /** * ์ปดํฌ๋„ŒํŠธ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx index 4aa4d930..570a82a7 100644 --- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx @@ -220,8 +220,8 @@ export function RepeaterTable({ columns .filter((col) => !col.hidden) .forEach((col) => { - widths[col.field] = col.width ? parseInt(col.width) : 120; - }); + widths[col.field] = col.width ? parseInt(col.width) : 120; + }); return widths; }); @@ -404,10 +404,10 @@ export function RepeaterTable({ // ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์ž๋™ ๋งž์ถค, ์—†์œผ๋ฉด ๊ท ๋“ฑ ๋ถ„๋ฐฐ const timer = setTimeout(() => { if (data.length > 0) { - applyAutoFitWidths(); - } else { - applyEqualizeWidths(); - } + applyAutoFitWidths(); + } else { + applyEqualizeWidths(); + } }, 50); return () => clearTimeout(timer); @@ -654,11 +654,17 @@ export function RepeaterTable({
{/* ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค ํ—ค๋” - ์ขŒ์ธก ๊ณ ์ • */} - {/* ์ฒดํฌ๋ฐ•์Šค ํ—ค๋” - ์ขŒ์ธก ๊ณ ์ • */} - - {visibleColumns.map((col) => { + {visibleColumns.map((col, colIndex) => { const hasDynamicSource = col.dynamicDataSource?.enabled && col.dynamicDataSource.options.length > 0; const activeOptionId = activeDataSources[col.field] || col.dynamicDataSource?.defaultOptionId; const activeOption = hasDynamicSource @@ -677,7 +683,7 @@ export function RepeaterTable({ return ( {data.length === 0 ? ( - + {/* ์ฒดํฌ๋ฐ•์Šค - ์ขŒ์ธก ๊ณ ์ • */} {/* ๋ฐ์ดํ„ฐ ์ปฌ๋Ÿผ๋“ค */} - {visibleColumns.map((col) => ( + {visibleColumns.map((col, colIndex) => (
+ ์ˆœ์„œ + handleDoubleClick(col.field)} @@ -765,8 +771,9 @@ export function RepeaterTable({
@@ -787,8 +794,9 @@ export function RepeaterTable({ <> {/* ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค - ์ขŒ์ธก ๊ณ ์ • */} @@ -806,8 +814,9 @@ export function RepeaterTable({ @@ -818,9 +827,9 @@ export function RepeaterTable({ /> (null); const [isSaving, setIsSaving] = useState(false); - + // ๐Ÿ†• v3.1: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ (ํ…Œ์ด๋ธ” ํ–‰๋ณ„๋กœ ๊ด€๋ฆฌ) const [externalTableData, setExternalTableData] = useState>({}); // ๐Ÿ†• v3.1: ์‚ญ์ œ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ @@ -108,12 +108,12 @@ export function RepeatScreenModalComponent({ useEffect(() => { const handleTriggerSave = async (event: Event) => { if (!(event instanceof CustomEvent)) return; - + console.log("[RepeatScreenModal] triggerRepeatScreenModalSave ์ด๋ฒคํŠธ ์ˆ˜์‹ "); - + try { setIsSaving(true); - + // ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์ €์žฅ if (cardMode === "withTable") { await saveGroupedData(); @@ -128,24 +128,28 @@ export function RepeatScreenModalComponent({ await processSyncSaves(); console.log("[RepeatScreenModal] ์™ธ๋ถ€ ํŠธ๋ฆฌ๊ฑฐ ์ €์žฅ ์™„๋ฃŒ"); - + // ์ €์žฅ ์™„๋ฃŒ ์ด๋ฒคํŠธ ๋ฐœ์ƒ - window.dispatchEvent(new CustomEvent("repeatScreenModalSaveComplete", { - detail: { success: true } - })); - + window.dispatchEvent( + new CustomEvent("repeatScreenModalSaveComplete", { + detail: { success: true }, + }), + ); + // ์„ฑ๊ณต ์ฝœ๋ฐฑ ์‹คํ–‰ if (event.detail?.onSuccess) { event.detail.onSuccess(); } } catch (error: any) { console.error("[RepeatScreenModal] ์™ธ๋ถ€ ํŠธ๋ฆฌ๊ฑฐ ์ €์žฅ ์‹คํŒจ:", error); - + // ์ €์žฅ ์‹คํŒจ ์ด๋ฒคํŠธ ๋ฐœ์ƒ - window.dispatchEvent(new CustomEvent("repeatScreenModalSaveComplete", { - detail: { success: false, error: error.message } - })); - + window.dispatchEvent( + new CustomEvent("repeatScreenModalSaveComplete", { + detail: { success: false, error: error.message }, + }), + ); + // ์‹คํŒจ ์ฝœ๋ฐฑ ์‹คํ–‰ if (event.detail?.onError) { event.detail.onError(error); @@ -177,7 +181,7 @@ export function RepeatScreenModalComponent({ // key ํ˜•์‹: cardId-contentRowId const keyParts = key.split("-"); const cardId = keyParts.slice(0, -1).join("-"); // contentRowId๋ฅผ ์ œ์™ธํ•œ ๋‚˜๋จธ์ง€๊ฐ€ cardId - + // contentRow ์ฐพ๊ธฐ const contentRow = contentRows.find((r) => key.includes(r.id)); if (!contentRow?.tableDataSource?.enabled) continue; @@ -187,24 +191,22 @@ export function RepeatScreenModalComponent({ const representativeData = card?._representativeData || {}; const targetTable = contentRow.tableCrud?.targetTable || contentRow.tableDataSource.sourceTable; - + // dirty ํ–‰ ๋˜๋Š” ์ƒˆ๋กœ์šด ํ–‰ ํ•„ํ„ฐ๋ง (์‚ญ์ œ๋œ ํ–‰ ์ œ์™ธ) // ๐Ÿ†• v3.13: _isNew ํ–‰๋„ ํฌํ•จ (์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ํ–‰์€ _isDirty๊ฐ€ ์—†์„ ์ˆ˜ ์žˆ์Œ) const dirtyRows = rows.filter((row) => (row._isDirty || row._isNew) && !row._isDeleted); - + console.log(`[RepeatScreenModal] beforeFormSave - ${targetTable} ํ–‰ ํ•„ํ„ฐ๋ง:`, { totalRows: rows.length, dirtyRows: dirtyRows.length, - rowDetails: rows.map(r => ({ _isDirty: r._isDirty, _isNew: r._isNew, _isDeleted: r._isDeleted })) + rowDetails: rows.map((r) => ({ _isDirty: r._isDirty, _isNew: r._isNew, _isDeleted: r._isDeleted })), }); - + if (dirtyRows.length === 0) continue; // ์ €์žฅํ•  ํ•„๋“œ๋งŒ ์ถ”์ถœ - const editableFields = (contentRow.tableColumns || []) - .filter((col) => col.editable) - .map((col) => col.field); - + const editableFields = (contentRow.tableColumns || []).filter((col) => col.editable).map((col) => col.field); + // ๐Ÿ†• v3.13: joinConditions์—์„œ sourceKey (์ €์žฅ ๋Œ€์ƒ ํ…Œ์ด๋ธ”์˜ FK ์ปฌ๋Ÿผ) ์ถ”์ถœ const joinConditions = contentRow.tableDataSource.joinConditions || []; const joinKeys = joinConditions.map((cond) => cond.sourceKey); @@ -217,14 +219,14 @@ export function RepeatScreenModalComponent({ for (const row of dirtyRows) { const saveData: Record = {}; - + // ํ—ˆ์šฉ๋œ ํ•„๋“œ๋งŒ ํฌํ•จ for (const field of allowedFields) { if (row[field] !== undefined) { saveData[field] = row[field]; } } - + // ๐Ÿ†• v3.13: joinConditions๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ FK ๊ฐ’ ์ž๋™ ์ฑ„์šฐ๊ธฐ // ์˜ˆ: sales_order_id (sourceKey) = card์˜ id (targetKey) for (const joinCond of joinConditions) { @@ -232,14 +234,16 @@ export function RepeatScreenModalComponent({ // sourceKey๊ฐ€ ์ €์žฅ ๋ฐ์ดํ„ฐ์— ์—†๊ฑฐ๋‚˜ null์ธ ๊ฒฝ์šฐ, ์นด๋“œ์˜ ๋Œ€ํ‘œ ๋ฐ์ดํ„ฐ์—์„œ targetKey ๊ฐ’์„ ๊ฐ€์ ธ์˜ด if (!saveData[sourceKey] && representativeData[targetKey] !== undefined) { saveData[sourceKey] = representativeData[targetKey]; - console.log(`[RepeatScreenModal] beforeFormSave - FK ์ž๋™ ์ฑ„์šฐ๊ธฐ: ${sourceKey} = ${representativeData[targetKey]} (from card.${targetKey})`); + console.log( + `[RepeatScreenModal] beforeFormSave - FK ์ž๋™ ์ฑ„์šฐ๊ธฐ: ${sourceKey} = ${representativeData[targetKey]} (from card.${targetKey})`, + ); } } - + // _isNew ํ”Œ๋ž˜๊ทธ ์œ ์ง€ saveData._isNew = row._isNew; saveData._targetTable = targetTable; - + // ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ์˜ ๊ฒฝ์šฐ id ํฌํ•จ if (!row._isNew && row._originalData?.id) { saveData.id = row._originalData.id; @@ -333,7 +337,7 @@ export function RepeatScreenModalComponent({ // formData์—์„œ ์„ ํƒ๋œ ํ–‰ ID ๊ฐ€์ ธ์˜ค๊ธฐ let selectedIds: any[] = []; - + if (formData) { // 1. ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •๋œ filterField ํ™•์ธ if (dataSource.filterField) { @@ -342,10 +346,10 @@ export function RepeatScreenModalComponent({ selectedIds = Array.isArray(filterValue) ? filterValue : [filterValue]; } } - + // 2. ์ผ๋ฐ˜์ ์ธ ์„ ํƒ ํ•„๋“œ ํ™•์ธ (fallback) if (selectedIds.length === 0) { - const commonFields = ['selectedRows', 'selectedIds', 'checkedRows', 'checkedIds', 'ids']; + const commonFields = ["selectedRows", "selectedIds", "checkedRows", "checkedIds", "ids"]; for (const field of commonFields) { if (formData[field]) { const value = formData[field]; @@ -355,7 +359,7 @@ export function RepeatScreenModalComponent({ } } } - + // 3. formData์— id๊ฐ€ ์žˆ์œผ๋ฉด ๋‹จ์ผ ํ–‰ if (selectedIds.length === 0 && formData.id) { selectedIds = [formData.id]; @@ -412,10 +416,10 @@ export function RepeatScreenModalComponent({ // ๐Ÿ†• v3: contentRows๊ฐ€ ์žˆ์œผ๋ฉด ์ƒˆ๋กœ์šด ๋ฐฉ์‹ ์‚ฌ์šฉ const useNewLayout = contentRows && contentRows.length > 0; - + // ๊ทธ๋ฃนํ•‘ ๋ชจ๋“œ ํ™•์ธ (groupByField๊ฐ€ ์—†์–ด๋„ enabled๋ฉด ๊ทธ๋ฃนํ•‘ ๋ชจ๋“œ๋กœ ์ฒ˜๋ฆฌ) const useGrouping = grouping?.enabled; - + if (useGrouping) { // ๊ทธ๋ฃนํ•‘ ๋ชจ๋“œ const grouped = processGroupedData(loadedData, grouping); @@ -428,7 +432,7 @@ export function RepeatScreenModalComponent({ _originalData: { ...row }, _isDirty: false, ...(await loadCardData(row)), - })) + })), ); setCardsData(initialCards); } @@ -448,7 +452,7 @@ export function RepeatScreenModalComponent({ const loadExternalTableData = async () => { // contentRows์—์„œ ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ์žˆ๋Š” table ํƒ€์ž… ํ–‰ ์ฐพ๊ธฐ const tableRowsWithExternalSource = contentRows.filter( - (row) => row.type === "table" && row.tableDataSource?.enabled + (row) => row.type === "table" && row.tableDataSource?.enabled, ); if (tableRowsWithExternalSource.length === 0) return; @@ -473,7 +477,7 @@ export function RepeatScreenModalComponent({ // ์ˆซ์žํ˜• ID์ธ ๊ฒฝ์šฐ ์ˆซ์ž๋กœ ๋ณ€ํ™˜ (๋ฌธ์ž์—ด '189' โ†’ ์ˆซ์ž 189) // ๋ฐฑ์—”๋“œ์—์„œ entity ํƒ€์ž… ์ปฌ๋Ÿผ ๊ฒ€์ƒ‰ ์‹œ ๋ฌธ์ž์—ด์ด๋ฉด ILIKE ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•˜๋ฏ€๋กœ // ์ •ํ™•ํ•œ ID ๋งค์นญ์„ ์œ„ํ•ด ์ˆซ์ž๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ํ•จ - if (condition.sourceKey.endsWith('_id') || condition.sourceKey === 'id') { + if (condition.sourceKey.endsWith("_id") || condition.sourceKey === "id") { const numValue = Number(refValue); if (!isNaN(numValue)) { refValue = numValue; @@ -497,24 +501,21 @@ export function RepeatScreenModalComponent({ }); // API ํ˜ธ์ถœ - ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ - const response = await apiClient.post( - `/table-management/tables/${dataSourceConfig.sourceTable}/data`, - { - search: filters, - page: 1, - size: dataSourceConfig.limit || 100, - sort: dataSourceConfig.orderBy - ? { - column: dataSourceConfig.orderBy.column, - direction: dataSourceConfig.orderBy.direction, - } - : undefined, - } - ); + const response = await apiClient.post(`/table-management/tables/${dataSourceConfig.sourceTable}/data`, { + search: filters, + page: 1, + size: dataSourceConfig.limit || 100, + sort: dataSourceConfig.orderBy + ? { + column: dataSourceConfig.orderBy.column, + direction: dataSourceConfig.orderBy.direction, + } + : undefined, + }); if (response.data.success && response.data.data?.data) { let tableData = response.data.data.data; - + console.log(`[RepeatScreenModal] ์†Œ์Šค ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์™„๋ฃŒ:`, { sourceTable: dataSourceConfig.sourceTable, rowCount: tableData.length, @@ -538,7 +539,7 @@ export function RepeatScreenModalComponent({ // ๐Ÿ†• v3.4: ํ•„ํ„ฐ ์กฐ๊ฑด ์ ์šฉ if (dataSourceConfig.filterConfig?.enabled) { const { filterField, filterType, referenceField, referenceSource } = dataSourceConfig.filterConfig; - + // ๋น„๊ต ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ let referenceValue: any; if (referenceSource === "formData") { @@ -558,8 +559,10 @@ export function RepeatScreenModalComponent({ return rowValue !== referenceValue; } }); - - console.log(`[RepeatScreenModal] ํ•„ํ„ฐ ์ ์šฉ: ${filterField} ${filterType} ${referenceValue}, ๊ฒฐ๊ณผ: ${tableData.length}๊ฑด`); + + console.log( + `[RepeatScreenModal] ํ•„ํ„ฐ ์ ์šฉ: ${filterField} ${filterType} ${referenceValue}, ๊ฒฐ๊ณผ: ${tableData.length}๊ฑด`, + ); } } @@ -573,7 +576,7 @@ export function RepeatScreenModalComponent({ _isDeleted: false, ...row, })); - + // ๋””๋ฒ„๊ทธ: ์ €์žฅ๋œ ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ํ™•์ธ console.log(`[RepeatScreenModal] ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ €์žฅ:`, { key, @@ -595,17 +598,17 @@ export function RepeatScreenModalComponent({ if (prevKeys === newKeys) { // ํ‚ค๊ฐ€ ๊ฐ™์œผ๋ฉด ๋ฐ์ดํ„ฐ ๋‚ด์šฉ ๋น„๊ต const isSame = Object.keys(newExternalData).every( - (key) => JSON.stringify(prev[key]) === JSON.stringify(newExternalData[key]) + (key) => JSON.stringify(prev[key]) === JSON.stringify(newExternalData[key]), ); if (isSame) return prev; } - + // ๐Ÿ†• v3.2: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋กœ๋“œ ํ›„ ์ง‘๊ณ„ ์žฌ๊ณ„์‚ฐ // ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€ setTimeout(() => { recalculateAggregationsWithExternalData(newExternalData); }, 0); - + return newExternalData; }); }; @@ -617,7 +620,7 @@ export function RepeatScreenModalComponent({ // ๐Ÿ†• v3.3: ์ถ”๊ฐ€ ์กฐ์ธ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋กœ๋“œ ๋ฐ ๋ณ‘ํ•ฉ const loadAndMergeJoinData = async ( mainData: any[], - additionalJoins: { id: string; joinTable: string; joinType: string; sourceKey: string; targetKey: string }[] + additionalJoins: { id: string; joinTable: string; joinType: string; sourceKey: string; targetKey: string }[], ): Promise => { if (mainData.length === 0) return mainData; @@ -627,23 +630,20 @@ export function RepeatScreenModalComponent({ // ๋ฉ”์ธ ๋ฐ์ดํ„ฐ์—์„œ ์กฐ์ธ ํ‚ค ๊ฐ’๋“ค ์ถ”์ถœ const joinKeyValues = [...new Set(mainData.map((row) => row[joinConfig.sourceKey]).filter(Boolean))]; - + if (joinKeyValues.length === 0) continue; try { // ์กฐ์ธ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์กฐํšŒ - const joinResponse = await apiClient.post( - `/table-management/tables/${joinConfig.joinTable}/data`, - { - search: { [joinConfig.targetKey]: joinKeyValues }, - page: 1, - size: 1000, // ์ถฉ๋ถ„ํžˆ ํฐ ๊ฐ’ - } - ); + const joinResponse = await apiClient.post(`/table-management/tables/${joinConfig.joinTable}/data`, { + search: { [joinConfig.targetKey]: joinKeyValues }, + page: 1, + size: 1000, // ์ถฉ๋ถ„ํžˆ ํฐ ๊ฐ’ + }); if (joinResponse.data.success && joinResponse.data.data?.data) { const joinData = joinResponse.data.data.data; - + // ์กฐ์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๋งต์œผ๋กœ ๋ณ€ํ™˜ (๋น ๋ฅธ ์กฐํšŒ๋ฅผ ์œ„ํ•ด) const joinDataMap = new Map(); for (const joinRow of joinData) { @@ -654,7 +654,7 @@ export function RepeatScreenModalComponent({ mainData = mainData.map((row) => { const joinKey = row[joinConfig.sourceKey]; const joinRow = joinDataMap.get(joinKey); - + if (joinRow) { // ์กฐ์ธ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ๋“ค์„ ๋ฉ”์ธ ๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€ (์ ‘๋‘์‚ฌ ์—†์ด) const mergedRow = { ...row }; @@ -700,7 +700,7 @@ export function RepeatScreenModalComponent({ // contentRows์—์„œ ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ์žˆ๋Š” ๋ชจ๋“  table ํƒ€์ž… ํ–‰ ์ฐพ๊ธฐ const tableRowsWithExternalSource = contentRows.filter( - (row) => row.type === "table" && row.tableDataSource?.enabled + (row) => row.type === "table" && row.tableDataSource?.enabled, ); if (tableRowsWithExternalSource.length === 0) return; @@ -710,10 +710,10 @@ export function RepeatScreenModalComponent({ // ๐Ÿ†• v3.11: ํ…Œ์ด๋ธ” ํ–‰ ID๋ณ„๋กœ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ๋ถ„ํ•˜์—ฌ ์ €์žฅ const externalRowsByTableId: Record = {}; const allExternalRows: any[] = []; - + for (const tableRow of tableRowsWithExternalSource) { const key = `${card._cardId}-${tableRow.id}`; - // ๐Ÿ†• v3.7: ์‚ญ์ œ๋œ ํ–‰์€ ์ง‘๊ณ„์—์„œ ์ œ์™ธ + // ๐Ÿ†• v3.7: ์‚ญ์ œ๋œ ํ–‰์€ ์ง‘๊ณ„์—์„œ ์ œ์™ธ const rows = (extData[key] || []).filter((row) => !row._isDeleted); externalRowsByTableId[tableRow.id] = rows; allExternalRows.push(...rows); @@ -721,30 +721,31 @@ export function RepeatScreenModalComponent({ // ์ง‘๊ณ„ ์žฌ๊ณ„์‚ฐ const newAggregations: Record = {}; - + grouping.aggregations!.forEach((agg) => { const sourceType = agg.sourceType || "column"; - + if (sourceType === "column") { const sourceTable = agg.sourceTable || dataSource?.sourceTable; const isExternalTable = sourceTable && sourceTable !== dataSource?.sourceTable; - + if (isExternalTable) { // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์ง‘๊ณ„ newAggregations[agg.resultField] = calculateColumnAggregation( - allExternalRows, - agg.sourceField || "", - agg.type || "sum" + allExternalRows, + agg.sourceField || "", + agg.type || "sum", ); } else { // ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ์ง‘๊ณ„ (๊ธฐ์กด ๊ฐ’ ์œ ์ง€) - newAggregations[agg.resultField] = card._aggregations[agg.resultField] || + newAggregations[agg.resultField] = + card._aggregations[agg.resultField] || calculateColumnAggregation(card._rows, agg.sourceField || "", agg.type || "sum"); } } else if (sourceType === "formula" && agg.formula) { // ๐Ÿ†• v3.11: externalTableRefs ๊ธฐ๋ฐ˜์œผ๋กœ ํ•„ํ„ฐ๋ง๋œ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ let filteredExternalRows: any[]; - + if (agg.externalTableRefs && agg.externalTableRefs.length > 0) { // ํŠน์ • ํ…Œ์ด๋ธ”๋งŒ ์ฐธ์กฐ filteredExternalRows = []; @@ -757,14 +758,14 @@ export function RepeatScreenModalComponent({ // ๋ชจ๋“  ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ (๊ธฐ์กด ๋™์ž‘) filteredExternalRows = allExternalRows; } - + // ๊ฐ€์ƒ ์ง‘๊ณ„ (์—ฐ์‚ฐ์‹) - ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ํฌํ•จํ•˜์—ฌ ์žฌ๊ณ„์‚ฐ newAggregations[agg.resultField] = evaluateFormulaWithContext( agg.formula, card._representativeData, card._rows, filteredExternalRows, - newAggregations // ์ด์ „ ์ง‘๊ณ„ ๊ฒฐ๊ณผ ์ฐธ์กฐ + newAggregations, // ์ด์ „ ์ง‘๊ณ„ ๊ฒฐ๊ณผ ์ฐธ์กฐ ); } }); @@ -854,14 +855,16 @@ export function RepeatScreenModalComponent({ targetColumn: rowNumbering.targetColumn, numberingRuleId: rowNumbering.numberingRuleId, }); - + // ์ฑ„๋ฒˆ API ํ˜ธ์ถœ (allocate: ์‹ค์ œ ์‹œํ€€์Šค ์ฆ๊ฐ€) + // ๐Ÿ†• ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘ํ•œ ๊ฐ’์„ ์ „๋‹ฌ (์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ถ”์ถœ์šฉ) const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); - const response = await allocateNumberingCode(rowNumbering.numberingRuleId); - + const userInputCode = newRowData[rowNumbering.targetColumn] as string; + const response = await allocateNumberingCode(rowNumbering.numberingRuleId, userInputCode, newRowData); + if (response.success && response.data) { newRowData[rowNumbering.targetColumn] = response.data.generatedCode; - + console.log("[RepeatScreenModal] ์ž๋™ ์ฑ„๋ฒˆ ์™„๋ฃŒ:", { column: rowNumbering.targetColumn, generatedCode: response.data.generatedCode, @@ -886,12 +889,12 @@ export function RepeatScreenModalComponent({ ...prev, [key]: [...(prev[key] || []), newRowData], }; - + // ๐Ÿ†• v3.5: ์ƒˆ ํ–‰ ์ถ”๊ฐ€ ์‹œ ์ง‘๊ณ„ ์žฌ๊ณ„์‚ฐ setTimeout(() => { recalculateAggregationsWithExternalData(newData); }, 0); - + return newData; }); }; @@ -900,7 +903,7 @@ export function RepeatScreenModalComponent({ const saveTableAreaData = async (cardId: string, contentRowId: string, contentRow: CardContentRowConfig) => { const key = `${cardId}-${contentRowId}`; const rows = externalTableData[key] || []; - + console.log("[RepeatScreenModal] saveTableAreaData ์‹œ์ž‘:", { key, rowsCount: rows.length, @@ -908,7 +911,7 @@ export function RepeatScreenModalComponent({ tableDataSource: contentRow?.tableDataSource, tableCrud: contentRow?.tableCrud, }); - + if (!contentRow?.tableDataSource?.enabled) { console.warn("[RepeatScreenModal] ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์Œ"); return { success: false, message: "๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค." }; @@ -920,7 +923,7 @@ export function RepeatScreenModalComponent({ console.log("[RepeatScreenModal] ์ €์žฅ ๋Œ€์ƒ:", { targetTable, dirtyRowsCount: dirtyRows.length, - dirtyRows: dirtyRows.map(r => ({ _isNew: r._isNew, _isDirty: r._isDirty, data: r })), + dirtyRows: dirtyRows.map((r) => ({ _isNew: r._isNew, _isDirty: r._isDirty, data: r })), }); if (dirtyRows.length === 0) { @@ -932,7 +935,7 @@ export function RepeatScreenModalComponent({ // ๐Ÿ†• v3.6: editableํ•œ ์ปฌ๋Ÿผ + ์กฐ์ธ ํ‚ค๋งŒ ์ถ”์ถœ (์ฝ๊ธฐ ์ „์šฉ ์ปฌ๋Ÿผ์€ ์ œ์™ธ) const allowedFields = new Set(); - + // tableColumns์—์„œ editable: true์ธ ํ•„๋“œ๋งŒ ์ถ”๊ฐ€ (์ฝ๊ธฐ ์ „์šฉ ์ปฌ๋Ÿผ ์ œ์™ธ) if (contentRow.tableColumns) { contentRow.tableColumns.forEach((col) => { @@ -943,20 +946,23 @@ export function RepeatScreenModalComponent({ } }); } - + // ์กฐ์ธ ์กฐ๊ฑด์˜ sourceKey ์ถ”๊ฐ€ (์˜ˆ: sales_order_id) - ์ด๊ฑด ํ•ญ์ƒ ํ•„์š” if (contentRow.tableDataSource?.joinConditions) { contentRow.tableDataSource.joinConditions.forEach((cond) => { if (cond.sourceKey) allowedFields.add(cond.sourceKey); }); } - + console.log("[RepeatScreenModal] ์ €์žฅ ํ—ˆ์šฉ ํ•„๋“œ (editable + ์กฐ์ธํ‚ค):", Array.from(allowedFields)); - console.log("[RepeatScreenModal] tableColumns ์ •๋ณด:", contentRow.tableColumns?.map(c => ({ - field: c.field, - editable: c.editable, - inputType: c.inputType - }))); + console.log( + "[RepeatScreenModal] tableColumns ์ •๋ณด:", + contentRow.tableColumns?.map((c) => ({ + field: c.field, + editable: c.editable, + inputType: c.inputType, + })), + ); // ์‚ญ์ œํ•  ํ–‰ (๊ธฐ์กด ๋ฐ์ดํ„ฐ ์ค‘ _isDeleted๊ฐ€ true์ธ ๊ฒƒ) const deletedRows = dirtyRows.filter((row) => row._isDeleted && row._originalData?.id); @@ -969,25 +975,30 @@ export function RepeatScreenModalComponent({ // ๐Ÿ†• v3.7: ์‚ญ์ œ ์ฒ˜๋ฆฌ (๋ฐฐ์—ด ํ˜•ํƒœ๋กœ body์— ์ „๋‹ฌ) for (const row of deletedRows) { const deleteId = row._originalData.id; - console.log(`[RepeatScreenModal] DELETE ์š”์ฒญ: /table-management/tables/${targetTable}/delete`, [{ id: deleteId }]); + console.log(`[RepeatScreenModal] DELETE ์š”์ฒญ: /table-management/tables/${targetTable}/delete`, [ + { id: deleteId }, + ]); savePromises.push( - apiClient.request({ - method: "DELETE", - url: `/table-management/tables/${targetTable}/delete`, - data: [{ id: deleteId }], - }).then((res) => { - console.log("[RepeatScreenModal] DELETE ์‘๋‹ต:", res.data); - return { type: "delete", id: deleteId }; - }).catch((err) => { - console.error("[RepeatScreenModal] DELETE ์‹คํŒจ:", err.response?.data || err.message); - throw err; - }) + apiClient + .request({ + method: "DELETE", + url: `/table-management/tables/${targetTable}/delete`, + data: [{ id: deleteId }], + }) + .then((res) => { + console.log("[RepeatScreenModal] DELETE ์‘๋‹ต:", res.data); + return { type: "delete", id: deleteId }; + }) + .catch((err) => { + console.error("[RepeatScreenModal] DELETE ์‹คํŒจ:", err.response?.data || err.message); + throw err; + }), ); } for (const row of rowsToSave) { const { _rowId, _originalData, _isDirty, _isNew, _isDeleted, ...allData } = row; - + // ํ—ˆ์šฉ๋œ ํ•„๋“œ๋งŒ ํ•„ํ„ฐ๋ง const dataToSave: Record = {}; for (const field of allowedFields) { @@ -1007,16 +1018,19 @@ export function RepeatScreenModalComponent({ // INSERT - /add ์—”๋“œํฌ์ธํŠธ ์‚ฌ์šฉ console.log(`[RepeatScreenModal] INSERT ์š”์ฒญ: /table-management/tables/${targetTable}/add`, dataToSave); savePromises.push( - apiClient.post(`/table-management/tables/${targetTable}/add`, dataToSave).then((res) => { - console.log("[RepeatScreenModal] INSERT ์‘๋‹ต:", res.data); - if (res.data?.data?.id) { - savedIds.push(res.data.data.id); - } - return res; - }).catch((err) => { - console.error("[RepeatScreenModal] INSERT ์‹คํŒจ:", err.response?.data || err.message); - throw err; - }) + apiClient + .post(`/table-management/tables/${targetTable}/add`, dataToSave) + .then((res) => { + console.log("[RepeatScreenModal] INSERT ์‘๋‹ต:", res.data); + if (res.data?.data?.id) { + savedIds.push(res.data.data.id); + } + return res; + }) + .catch((err) => { + console.error("[RepeatScreenModal] INSERT ์‹คํŒจ:", err.response?.data || err.message); + throw err; + }), ); } else if (_originalData?.id) { // UPDATE - /edit ์—”๋“œํฌ์ธํŠธ ์‚ฌ์šฉ (originalData์™€ updatedData ํ˜•์‹) @@ -1026,14 +1040,17 @@ export function RepeatScreenModalComponent({ }; console.log(`[RepeatScreenModal] UPDATE ์š”์ฒญ: /table-management/tables/${targetTable}/edit`, updatePayload); savePromises.push( - apiClient.put(`/table-management/tables/${targetTable}/edit`, updatePayload).then((res) => { - console.log("[RepeatScreenModal] UPDATE ์‘๋‹ต:", res.data); - savedIds.push(_originalData.id); - return res; - }).catch((err) => { - console.error("[RepeatScreenModal] UPDATE ์‹คํŒจ:", err.response?.data || err.message); - throw err; - }) + apiClient + .put(`/table-management/tables/${targetTable}/edit`, updatePayload) + .then((res) => { + console.log("[RepeatScreenModal] UPDATE ์‘๋‹ต:", res.data); + savedIds.push(_originalData.id); + return res; + }) + .catch((err) => { + console.error("[RepeatScreenModal] UPDATE ์‹คํŒจ:", err.response?.data || err.message); + throw err; + }), ); } } @@ -1053,7 +1070,15 @@ export function RepeatScreenModalComponent({ _isDirty: false, _isNew: false, _isEditing: false, // ๐Ÿ†• v3.8: ์ˆ˜์ • ๋ชจ๋“œ ํ•ด์ œ - _originalData: { ...row, _rowId: undefined, _originalData: undefined, _isDirty: undefined, _isNew: undefined, _isDeleted: undefined, _isEditing: undefined }, + _originalData: { + ...row, + _rowId: undefined, + _originalData: undefined, + _isDirty: undefined, + _isNew: undefined, + _isDeleted: undefined, + _isEditing: undefined, + }, })); } return updated; @@ -1061,9 +1086,8 @@ export function RepeatScreenModalComponent({ const savedCount = rowsToSave.length; const deletedCount = deletedRows.length; - const message = deletedCount > 0 - ? `${savedCount}๊ฑด ์ €์žฅ, ${deletedCount}๊ฑด ์‚ญ์ œ ์™„๋ฃŒ` - : `${savedCount}๊ฑด ์ €์žฅ ์™„๋ฃŒ`; + const message = + deletedCount > 0 ? `${savedCount}๊ฑด ์ €์žฅ, ${deletedCount}๊ฑด ์‚ญ์ œ ์™„๋ฃŒ` : `${savedCount}๊ฑด ์ €์žฅ ์™„๋ฃŒ`; return { success: true, message, savedCount, deletedCount, savedIds }; } catch (error: any) { @@ -1079,7 +1103,7 @@ export function RepeatScreenModalComponent({ const result = await saveTableAreaData(cardId, contentRowId, contentRow); if (result.success) { console.log("[RepeatScreenModal] ํ…Œ์ด๋ธ” ์˜์—ญ ์ €์žฅ ์„ฑ๊ณต:", result); - + // ๐Ÿ†• v3.9: ์ง‘๊ณ„ ์ €์žฅ ์„ค์ •์ด ์žˆ๋Š” ๊ฒฝ์šฐ ์—ฐ๊ด€ ํ…Œ์ด๋ธ” ๋™๊ธฐํ™” const card = groupedCardsData.find((c) => c._cardId === cardId); if (card && grouping?.aggregations) { @@ -1101,16 +1125,16 @@ export function RepeatScreenModalComponent({ for (const agg of grouping.aggregations) { const saveConfig = agg.saveConfig; - + // ์ €์žฅ ์„ค์ •์ด ์—†๊ฑฐ๋‚˜ ๋น„ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ ์Šคํ‚ต if (!saveConfig?.enabled) continue; - + // ์ž๋™ ์ €์žฅ์ด ์•„๋‹Œ ๊ฒฝ์šฐ, ๋ ˆ์ด์•„์›ƒ์— ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ ํ•„์š” // (ํ˜„์žฌ๋Š” ์ž๋™ ์ €์žฅ๊ณผ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌ - ์ถ”ํ›„ ๋ ˆ์ด์•„์›ƒ ์—ฐ๊ฒฐ ์ฒดํฌ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ) - + // ์ง‘๊ณ„ ๊ฒฐ๊ณผ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ const aggregatedValue = card._aggregations[agg.resultField]; - + if (aggregatedValue === undefined) { console.warn(`[RepeatScreenModal] ์ง‘๊ณ„ ๊ฒฐ๊ณผ ์—†์Œ: ${agg.resultField}`); continue; @@ -1118,7 +1142,7 @@ export function RepeatScreenModalComponent({ // ์กฐ์ธ ํ‚ค๋กœ ๋Œ€์ƒ ๋ ˆ์ฝ”๋“œ ์‹๋ณ„ const sourceKeyValue = card._representativeData[saveConfig.joinKey.sourceField]; - + if (!sourceKeyValue) { console.warn(`[RepeatScreenModal] ์กฐ์ธ ํ‚ค ๊ฐ’ ์—†์Œ: ${saveConfig.joinKey.sourceField}`); continue; @@ -1135,22 +1159,25 @@ export function RepeatScreenModalComponent({ // UPDATE API ํ˜ธ์ถœ const updatePayload = { originalData: { [saveConfig.joinKey.targetField]: sourceKeyValue }, - updatedData: { + updatedData: { [saveConfig.targetColumn]: aggregatedValue, [saveConfig.joinKey.targetField]: sourceKeyValue, }, }; savePromises.push( - apiClient.put(`/table-management/tables/${saveConfig.targetTable}/edit`, updatePayload) + apiClient + .put(`/table-management/tables/${saveConfig.targetTable}/edit`, updatePayload) .then((res) => { - console.log(`[RepeatScreenModal] ์ง‘๊ณ„ ์ €์žฅ ์„ฑ๊ณต: ${agg.resultField} -> ${saveConfig.targetTable}.${saveConfig.targetColumn}`); + console.log( + `[RepeatScreenModal] ์ง‘๊ณ„ ์ €์žฅ ์„ฑ๊ณต: ${agg.resultField} -> ${saveConfig.targetTable}.${saveConfig.targetColumn}`, + ); return res; }) .catch((err) => { console.error(`[RepeatScreenModal] ์ง‘๊ณ„ ์ €์žฅ ์‹คํŒจ: ${agg.resultField}`, err.response?.data || err.message); throw err; - }) + }), ); } @@ -1165,7 +1192,12 @@ export function RepeatScreenModalComponent({ }; // ๐Ÿ†• v3.1: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ํ–‰ ์‚ญ์ œ ์š”์ฒญ - const handleDeleteExternalRowRequest = (cardId: string, rowId: string, contentRowId: string, contentRow: CardContentRowConfig) => { + const handleDeleteExternalRowRequest = ( + cardId: string, + rowId: string, + contentRowId: string, + contentRow: CardContentRowConfig, + ) => { if (contentRow.tableCrud?.deleteConfirm?.enabled !== false) { // ์‚ญ์ œ ํ™•์ธ ํŒ์—… ํ‘œ์‹œ setPendingDeleteInfo({ cardId, rowId, contentRowId }); @@ -1194,7 +1226,7 @@ export function RepeatScreenModalComponent({ } console.log(`[RepeatScreenModal] DELETE API ํ˜ธ์ถœ: ${targetTable}, id=${targetRow._originalData.id}`); - + // ๋ฐฑ์—”๋“œ๋Š” ๋ฐฐ์—ด ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋Œ€ํ•จ await apiClient.request({ method: "DELETE", @@ -1205,19 +1237,19 @@ export function RepeatScreenModalComponent({ console.log(`[RepeatScreenModal] DELETE ์„ฑ๊ณต: ${targetTable}, id=${targetRow._originalData.id}`); // ์„ฑ๊ณต ์‹œ UI์—์„œ ์™„์ „ํžˆ ์ œ๊ฑฐ - setExternalTableData((prev) => { - const newData = { - ...prev, + setExternalTableData((prev) => { + const newData = { + ...prev, [key]: prev[key].filter((row) => row._rowId !== rowId), - }; - + }; + // ํ–‰ ์‚ญ์ œ ์‹œ ์ง‘๊ณ„ ์žฌ๊ณ„์‚ฐ - setTimeout(() => { - recalculateAggregationsWithExternalData(newData); - }, 0); - - return newData; - }); + setTimeout(() => { + recalculateAggregationsWithExternalData(newData); + }, 0); + + return newData; + }); } catch (error: any) { console.error(`[RepeatScreenModal] DELETE ์‹คํŒจ:`, error.response?.data || error.message); // ์—๋Ÿฌ ์‹œ์—๋„ ๋‹ค์ด์–ผ๋กœ๊ทธ ๋‹ซ๊ธฐ @@ -1251,16 +1283,14 @@ export function RepeatScreenModalComponent({ const newData = { ...prev, [key]: (prev[key] || []).map((row) => - row._rowId === rowId - ? { ...row, _isDeleted: false, _isDirty: true } - : row + row._rowId === rowId ? { ...row, _isDeleted: false, _isDirty: true } : row, ), }; - + setTimeout(() => { recalculateAggregationsWithExternalData(newData); }, 0); - + return newData; }); }; @@ -1270,11 +1300,7 @@ export function RepeatScreenModalComponent({ const key = `${cardId}-${contentRowId}`; setExternalTableData((prev) => ({ ...prev, - [key]: (prev[key] || []).map((row) => - row._rowId === rowId - ? { ...row, _isEditing: true } - : row - ), + [key]: (prev[key] || []).map((row) => (row._rowId === rowId ? { ...row, _isEditing: true } : row)), })); }; @@ -1285,39 +1311,45 @@ export function RepeatScreenModalComponent({ ...prev, [key]: (prev[key] || []).map((row) => row._rowId === rowId - ? { - ...row._originalData, - _rowId: row._rowId, + ? { + ...row._originalData, + _rowId: row._rowId, _originalData: row._originalData, - _isEditing: false, + _isEditing: false, _isDirty: false, _isNew: false, _isDeleted: false, } - : row + : row, ), })); }; // ๐Ÿ†• v3.1: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ํ–‰ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ - const handleExternalRowDataChange = (cardId: string, contentRowId: string, rowId: string, field: string, value: any) => { + const handleExternalRowDataChange = ( + cardId: string, + contentRowId: string, + rowId: string, + field: string, + value: any, + ) => { const key = `${cardId}-${contentRowId}`; - + // ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ setExternalTableData((prev) => { const newData = { ...prev, [key]: (prev[key] || []).map((row) => - row._rowId === rowId ? { ...row, [field]: value, _isDirty: true } : row + row._rowId === rowId ? { ...row, [field]: value, _isDirty: true } : row, ), }; - + // ๐Ÿ†• v3.5: ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ง‘๊ณ„ ์‹ค์‹œ๊ฐ„ ์žฌ๊ณ„์‚ฐ // setTimeout์œผ๋กœ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌํ•˜์—ฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ํ›„ ์žฌ๊ณ„์‚ฐ setTimeout(() => { recalculateAggregationsWithExternalData(newData); }, 0); - + return newData; }); }; @@ -1370,18 +1402,18 @@ export function RepeatScreenModalComponent({ if (groupingConfig.aggregations) { groupingConfig.aggregations.forEach((agg) => { const sourceType = agg.sourceType || "column"; - + if (sourceType === "column") { // ์ปฌ๋Ÿผ ์ง‘๊ณ„ (๊ธฐ๋ณธ ํ…Œ์ด๋ธ”๋งŒ - ์™ธ๋ถ€ ํ…Œ์ด๋ธ”์€ ๋‚˜์ค‘์— ์ฒ˜๋ฆฌ) const sourceTable = agg.sourceTable || dataSource?.sourceTable; const isExternalTable = sourceTable && sourceTable !== dataSource?.sourceTable; - + if (!isExternalTable) { // ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ์ง‘๊ณ„ aggregations[agg.resultField] = calculateColumnAggregation( - rows, - agg.sourceField || "", - agg.type || "sum" + rows, + agg.sourceField || "", + agg.type || "sum", ); } else { // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์ง‘๊ณ„๋Š” ๋‚˜์ค‘์— ๊ณ„์‚ฐ (placeholder) @@ -1396,7 +1428,7 @@ export function RepeatScreenModalComponent({ representativeData, rows, [], // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์—†์Œ - aggregations // ์ด์ „ ์ง‘๊ณ„ ๊ฒฐ๊ณผ ์ฐธ์กฐ + aggregations, // ์ด์ „ ์ง‘๊ณ„ ๊ฒฐ๊ณผ ์ฐธ์กฐ ); } else { aggregations[agg.resultField] = 0; @@ -1425,9 +1457,9 @@ export function RepeatScreenModalComponent({ // ์ง‘๊ณ„ ๊ณ„์‚ฐ (์ปฌ๋Ÿผ ์ง‘๊ณ„์šฉ) const calculateColumnAggregation = ( - rows: any[], - sourceField: string, - type: "sum" | "count" | "avg" | "min" | "max" + rows: any[], + sourceField: string, + type: "sum" | "count" | "avg" | "min" | "max", ): number => { const values = rows.map((row) => Number(row[sourceField]) || 0); @@ -1453,7 +1485,7 @@ export function RepeatScreenModalComponent({ cardRows: any[], // ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ํ–‰๋“ค externalRows: any[], // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ํ–‰๋“ค previousAggregations: Record, // ์ด์ „ ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋“ค - representativeData: Record // ์นด๋“œ ๋Œ€ํ‘œ ๋ฐ์ดํ„ฐ + representativeData: Record, // ์นด๋“œ ๋Œ€ํ‘œ ๋ฐ์ดํ„ฐ ): number => { const sourceType = agg.sourceType || "column"; @@ -1461,26 +1493,16 @@ export function RepeatScreenModalComponent({ // ์ปฌ๋Ÿผ ์ง‘๊ณ„ const sourceTable = agg.sourceTable || dataSource?.sourceTable; const isExternalTable = sourceTable && sourceTable !== dataSource?.sourceTable; - + // ์™ธ๋ถ€ ํ…Œ์ด๋ธ”์ธ ๊ฒฝ์šฐ externalRows ์‚ฌ์šฉ, ์•„๋‹ˆ๋ฉด cardRows ์‚ฌ์šฉ const targetRows = isExternalTable ? externalRows : cardRows; - - return calculateColumnAggregation( - targetRows, - agg.sourceField || "", - agg.type || "sum" - ); + + return calculateColumnAggregation(targetRows, agg.sourceField || "", agg.type || "sum"); } else if (sourceType === "formula") { // ๊ฐ€์ƒ ์ง‘๊ณ„ (์—ฐ์‚ฐ์‹) if (!agg.formula) return 0; - - return evaluateFormulaWithContext( - agg.formula, - representativeData, - cardRows, - externalRows, - previousAggregations - ); + + return evaluateFormulaWithContext(agg.formula, representativeData, cardRows, externalRows, previousAggregations); } return 0; @@ -1489,7 +1511,7 @@ export function RepeatScreenModalComponent({ // ๐Ÿ†• v3.1: ์ง‘๊ณ„ ํ‘œ์‹œ๊ฐ’ ๊ณ„์‚ฐ (formula, external ๋“ฑ ์ง€์›) const calculateAggregationDisplayValue = ( aggField: AggregationDisplayConfig, - card: GroupedCardData + card: GroupedCardData, ): number | string => { const sourceType = aggField.sourceType || "aggregation"; @@ -1524,7 +1546,7 @@ export function RepeatScreenModalComponent({ representativeData: Record, cardRows: any[], // ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ํ–‰๋“ค externalRows: any[], // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ํ–‰๋“ค - previousAggregations: Record // ์ด์ „ ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋“ค + previousAggregations: Record, // ์ด์ „ ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋“ค ): number => { try { let expression = formula; @@ -1613,11 +1635,7 @@ export function RepeatScreenModalComponent({ }; // ๋ ˆ๊ฑฐ์‹œ ํ˜ธํ™˜: ๊ธฐ์กด evaluateFormula ์œ ์ง€ - const evaluateFormula = ( - formula: string, - representativeData: Record, - rows?: any[] - ): number => { + const evaluateFormula = (formula: string, representativeData: Record, rows?: any[]): number => { return evaluateFormulaWithContext(formula, representativeData, rows || [], [], {}); }; @@ -1645,7 +1663,7 @@ export function RepeatScreenModalComponent({ } } } - + // ํ…Œ์ด๋ธ” ํƒ€์ž…์˜ ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ if (contentRow.type === "table" && contentRow.tableColumns) { for (const col of contentRow.tableColumns) { @@ -1678,7 +1696,7 @@ export function RepeatScreenModalComponent({ // Simple ๋ชจ๋“œ: ์นด๋“œ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ const handleCardDataChange = (cardId: string, field: string, value: any) => { setCardsData((prev) => - prev.map((card) => (card._cardId === cardId ? { ...card, [field]: value, _isDirty: true } : card)) + prev.map((card) => (card._cardId === cardId ? { ...card, [field]: value, _isDirty: true } : card)), ); }; @@ -1689,7 +1707,7 @@ export function RepeatScreenModalComponent({ if (card._cardId !== cardId) return card; const updatedRows = card._rows.map((row) => - row._rowId === rowId ? { ...row, [field]: value, _isDirty: true } : row + row._rowId === rowId ? { ...row, [field]: value, _isDirty: true } : row, ); // ์ง‘๊ณ„๊ฐ’ ์žฌ๊ณ„์‚ฐ @@ -1705,7 +1723,7 @@ export function RepeatScreenModalComponent({ _rows: updatedRows, _aggregations: newAggregations, }; - }) + }), ); }; @@ -1761,7 +1779,7 @@ export function RepeatScreenModalComponent({ // key ํ˜•์‹: cardId-contentRowId const [cardId, contentRowId] = key.split("-").slice(0, 2); const contentRow = contentRows.find((r) => r.id === contentRowId || key.includes(r.id)); - + if (!contentRow?.tableDataSource?.enabled) continue; const targetTable = contentRow.tableCrud?.targetTable || contentRow.tableDataSource.sourceTable; @@ -1772,13 +1790,13 @@ export function RepeatScreenModalComponent({ if (_isNew) { // INSERT - savePromises.push( - apiClient.post(`/table-management/tables/${targetTable}/data`, dataToSave).then(() => {}) - ); + savePromises.push(apiClient.post(`/table-management/tables/${targetTable}/data`, dataToSave).then(() => {})); } else if (_originalData?.id) { // UPDATE savePromises.push( - apiClient.put(`/table-management/tables/${targetTable}/data/${_originalData.id}`, dataToSave).then(() => {}) + apiClient + .put(`/table-management/tables/${targetTable}/data/${_originalData.id}`, dataToSave) + .then(() => {}), ); } } @@ -1794,7 +1812,13 @@ export function RepeatScreenModalComponent({ ...row, _isDirty: false, _isNew: false, - _originalData: { ...row, _rowId: undefined, _originalData: undefined, _isDirty: undefined, _isNew: undefined }, + _originalData: { + ...row, + _rowId: undefined, + _originalData: undefined, + _isDirty: undefined, + _isNew: undefined, + }, })); } return updated; @@ -1835,9 +1859,7 @@ export function RepeatScreenModalComponent({ // ๊ฐ ์กฐ์ธ ํ‚ค๋ณ„๋กœ ์ง‘๊ณ„ ๊ณ„์‚ฐ ๋ฐ ์—…๋ฐ์ดํŠธ for (const keyValue of joinKeyValues) { // ํ•ด๋‹น ์กฐ์ธ ํ‚ค์— ํ•ด๋‹นํ•˜๋Š” ํ–‰๋“ค๋งŒ ํ•„ํ„ฐ๋ง - const filteredRows = rows.filter( - (row) => row[syncSave.joinKey.sourceField] === keyValue - ); + const filteredRows = rows.filter((row) => row[syncSave.joinKey.sourceField] === keyValue); // ์ง‘๊ณ„ ๊ณ„์‚ฐ let aggregatedValue: number = 0; @@ -1864,12 +1886,15 @@ export function RepeatScreenModalComponent({ break; } - console.log(`[SyncSave] ${sourceTable}.${syncSave.sourceColumn} โ†’ ${syncSave.targetTable}.${syncSave.targetColumn}`, { - joinKey: keyValue, - aggregationType: syncSave.aggregationType, - values, - aggregatedValue, - }); + console.log( + `[SyncSave] ${sourceTable}.${syncSave.sourceColumn} โ†’ ${syncSave.targetTable}.${syncSave.targetColumn}`, + { + joinKey: keyValue, + aggregationType: syncSave.aggregationType, + values, + aggregatedValue, + }, + ); // ๋Œ€์ƒ ํ…Œ์ด๋ธ” ์—…๋ฐ์ดํŠธ syncPromises.push( @@ -1878,12 +1903,14 @@ export function RepeatScreenModalComponent({ [syncSave.targetColumn]: aggregatedValue, }) .then(() => { - console.log(`[SyncSave] ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${syncSave.targetTable}.${syncSave.targetColumn} = ${aggregatedValue} (id=${keyValue})`); + console.log( + `[SyncSave] ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${syncSave.targetTable}.${syncSave.targetColumn} = ${aggregatedValue} (id=${keyValue})`, + ); }) .catch((err) => { console.error(`[SyncSave] ์—…๋ฐ์ดํŠธ ์‹คํŒจ:`, err); throw err; - }) + }), ); } } @@ -1928,7 +1955,7 @@ export function RepeatScreenModalComponent({ config: btn.customAction.config, componentId: component?.id, }, - }) + }), ); } break; @@ -2029,7 +2056,7 @@ export function RepeatScreenModalComponent({ prev.map((card) => ({ ...card, _rows: card._rows.map((row) => ({ ...row, _isDirty: false })), - })) + })), ); }; @@ -2046,7 +2073,7 @@ export function RepeatScreenModalComponent({ } else { await apiClient.post(`/table-management/tables/${tableName}/data`, dataToSave); } - }) + }), ); }); @@ -2064,9 +2091,7 @@ export function RepeatScreenModalComponent({ } // ๐Ÿ†• v3.1: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ˆ˜์ • ์—ฌ๋ถ€ - const hasExternalDirty = Object.values(externalTableData).some((rows) => - rows.some((row) => row._isDirty) - ); + const hasExternalDirty = Object.values(externalTableData).some((rows) => rows.some((row) => row._isDirty)); return hasBaseDirty || hasExternalDirty; }, [cardMode, cardsData, groupedCardsData, externalTableData]); @@ -2084,25 +2109,25 @@ export function RepeatScreenModalComponent({ return (
-
+
{/* ์•„์ด์ฝ˜ */} -
- +
+
{/* ์ œ๋ชฉ */} -
-
Repeat Screen Modal
-
๋ฐ˜๋ณต ํ™”๋ฉด ๋ชจ๋‹ฌ
+
+
Repeat Screen Modal
+
๋ฐ˜๋ณต ํ™”๋ฉด ๋ชจ๋‹ฌ
v3 ์ž์œ  ๋ ˆ์ด์•„์›ƒ
{/* ํ–‰ ๊ตฌ์„ฑ ์ •๋ณด */} -
+
{contentRows.length > 0 ? ( <> {rowTypeCounts.header > 0 && ( @@ -2134,24 +2159,24 @@ export function RepeatScreenModalComponent({ {/* ํ†ต๊ณ„ ์ •๋ณด */}
-
{contentRows.length}
-
ํ–‰ (Rows)
+
{contentRows.length}
+
ํ–‰ (Rows)
-
+
-
{grouping?.aggregations?.length || 0}
-
์ง‘๊ณ„ ์„ค์ •
+
{grouping?.aggregations?.length || 0}
+
์ง‘๊ณ„ ์„ค์ •
-
+
-
{dataSource?.sourceTable ? 1 : 0}
-
๋ฐ์ดํ„ฐ ์†Œ์Šค
+
{dataSource?.sourceTable ? 1 : 0}
+
๋ฐ์ดํ„ฐ ์†Œ์Šค
{/* ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ •๋ณด */} {dataSource?.sourceTable && ( -
+
์†Œ์Šค ํ…Œ์ด๋ธ”: {dataSource.sourceTable} {dataSource.filterField && (ํ•„ํ„ฐ: {dataSource.filterField})}
@@ -2159,20 +2184,20 @@ export function RepeatScreenModalComponent({ {/* ๊ทธ๋ฃนํ•‘ ์ •๋ณด */} {grouping?.enabled && ( -
+
๊ทธ๋ฃนํ•‘: {grouping.groupByField}
)} {/* ์นด๋“œ ์ œ๋ชฉ ์ •๋ณด */} {showCardTitle && cardTitle && ( -
+
์นด๋“œ ์ œ๋ชฉ: {cardTitle}
)} {/* ์„ค์ • ์•ˆ๋‚ด */} -
+
์˜ค๋ฅธ์ชฝ ํŒจ๋„์—์„œ ํ–‰์„ ์ถ”๊ฐ€ํ•˜๊ณ  ํƒ€์ž…(ํ—ค๋”/์ง‘๊ณ„/ํ…Œ์ด๋ธ”/ํ•„๋“œ)์„ ์„ ํƒํ•˜์„ธ์š”
@@ -2184,8 +2209,8 @@ export function RepeatScreenModalComponent({ if (isLoading) { return (
- - ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘... + + ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...
); } @@ -2193,12 +2218,12 @@ export function RepeatScreenModalComponent({ // ์˜ค๋ฅ˜ ์ƒํƒœ if (loadError) { return ( -
-
+
+
๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ
-

{loadError}

+

{loadError}

); } @@ -2211,23 +2236,23 @@ export function RepeatScreenModalComponent({ if (useGrouping) { return (
-
+
{groupedCardsData.map((card, cardIndex) => ( r._isDirty) && "border-primary shadow-lg" + card._rows.some((r) => r._isDirty) && "border-primary shadow-lg", )} > {/* ์นด๋“œ ์ œ๋ชฉ (์„ ํƒ์‚ฌํ•ญ) */} {showCardTitle && ( - + {getCardTitle(card._representativeData, cardIndex)} {card._rows.some((r) => r._isDirty) && ( - + ์ˆ˜์ •๋จ )} @@ -2241,10 +2266,10 @@ export function RepeatScreenModalComponent({
{contentRow.type === "table" && contentRow.tableDataSource?.enabled ? ( // ๐Ÿ†• v3.1: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์†Œ์Šค ์‚ฌ์šฉ -
+
{/* ํ…Œ์ด๋ธ” ํ—ค๋” ์˜์—ญ: ์ œ๋ชฉ + ๋ฒ„ํŠผ๋“ค */} {(contentRow.tableTitle || contentRow.tableCrud?.allowCreate) && ( -
+
{contentRow.tableTitle || ""}
{/* ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} @@ -2253,7 +2278,7 @@ export function RepeatScreenModalComponent({ variant="outline" size="sm" onClick={() => handleAddExternalRow(card._cardId, contentRow.id, contentRow)} - className="h-7 text-xs gap-1" + className="h-7 gap-1 text-xs" > ์ถ”๊ฐ€ @@ -2267,15 +2292,17 @@ export function RepeatScreenModalComponent({ {/* ๐Ÿ†• v3.13: hidden ์ปฌ๋Ÿผ ํ•„ํ„ฐ๋ง */} - {(contentRow.tableColumns || []).filter(col => !col.hidden).map((col) => ( - - {col.label} - - ))} + {(contentRow.tableColumns || []) + .filter((col) => !col.hidden) + .map((col) => ( + + {col.label} + + ))} {(contentRow.tableCrud?.allowUpdate || contentRow.tableCrud?.allowDelete) && ( ์ž‘์—… )} @@ -2286,8 +2313,11 @@ export function RepeatScreenModalComponent({ {(externalTableData[`${card._cardId}-${contentRow.id}`] || []).length === 0 ? ( !col.hidden)?.length || 0) + (contentRow.tableCrud?.allowDelete ? 1 : 0)} - className="text-center py-8 text-muted-foreground" + colSpan={ + (contentRow.tableColumns?.filter((col) => !col.hidden)?.length || 0) + + (contentRow.tableCrud?.allowDelete ? 1 : 0) + } + className="text-muted-foreground py-8 text-center" > ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. @@ -2297,64 +2327,82 @@ export function RepeatScreenModalComponent({ {/* ๐Ÿ†• v3.13: hidden ์ปฌ๋Ÿผ ํ•„ํ„ฐ๋ง */} - {(contentRow.tableColumns || []).filter(col => !col.hidden).map((col) => ( - - {renderTableCell( - col, - row, - (value) => handleExternalRowDataChange(card._cardId, contentRow.id, row._rowId, col.field, value), - row._isNew || row._isEditing // ์‹ ๊ทœ ํ–‰์ด๊ฑฐ๋‚˜ ์ˆ˜์ • ๋ชจ๋“œ์ผ ๋•Œ๋งŒ ํŽธ์ง‘ ๊ฐ€๋Šฅ - )} - - ))} + {(contentRow.tableColumns || []) + .filter((col) => !col.hidden) + .map((col) => ( + + {renderTableCell( + col, + row, + (value) => + handleExternalRowDataChange( + card._cardId, + contentRow.id, + row._rowId, + col.field, + value, + ), + row._isNew || row._isEditing, // ์‹ ๊ทœ ํ–‰์ด๊ฑฐ๋‚˜ ์ˆ˜์ • ๋ชจ๋“œ์ผ ๋•Œ๋งŒ ํŽธ์ง‘ ๊ฐ€๋Šฅ + )} + + ))} {(contentRow.tableCrud?.allowUpdate || contentRow.tableCrud?.allowDelete) && (
{/* ์ˆ˜์ • ๋ฒ„ํŠผ: ์ €์žฅ๋œ ํ–‰(isNew๊ฐ€ ์•„๋‹Œ)์ด๊ณ  ํŽธ์ง‘ ๋ชจ๋“œ๊ฐ€ ์•„๋‹ ๋•Œ๋งŒ ํ‘œ์‹œ */} - {contentRow.tableCrud?.allowUpdate && !row._isNew && !row._isEditing && !row._isDeleted && ( - - )} + {contentRow.tableCrud?.allowUpdate && + !row._isNew && + !row._isEditing && + !row._isDeleted && ( + + )} {/* ์ˆ˜์ • ์ทจ์†Œ ๋ฒ„ํŠผ: ํŽธ์ง‘ ๋ชจ๋“œ์ผ ๋•Œ๋งŒ ํ‘œ์‹œ */} {row._isEditing && !row._isNew && ( )} {/* ์‚ญ์ œ/๋ณต์› ๋ฒ„ํŠผ */} - {contentRow.tableCrud?.allowDelete && ( - row._isDeleted ? ( + {contentRow.tableCrud?.allowDelete && + (row._isDeleted ? ( - ) - )} + ))}
)} @@ -2390,16 +2444,16 @@ export function RepeatScreenModalComponent({ // ๋ ˆ๊ฑฐ์‹œ: tableLayout ์‚ฌ์šฉ <> {tableLayout?.headerRows && tableLayout.headerRows.length > 0 && ( -
+
{tableLayout.headerRows.map((row, rowIndex) => (
@@ -2414,7 +2468,7 @@ export function RepeatScreenModalComponent({ )} {tableLayout?.tableColumns && tableLayout.tableColumns.length > 0 && ( -
+
@@ -2438,10 +2492,10 @@ export function RepeatScreenModalComponent({ className={cn("text-sm", col.align && `text-${col.align}`)} > {renderTableCell( - col, - row, + col, + row, (value) => handleRowDataChange(card._cardId, row._rowId, col.field, value), - row._isNew || row._isEditing + row._isNew || row._isEditing, )} ))} @@ -2463,11 +2517,11 @@ export function RepeatScreenModalComponent({
{footerConfig.buttons.map((btn) => ( @@ -2495,7 +2549,7 @@ export function RepeatScreenModalComponent({ {/* ๋ฐ์ดํ„ฐ ์—†์Œ */} {groupedCardsData.length === 0 && !isLoading && ( -
ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
+
ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
)} {/* ๐Ÿ†• v3.1: ์‚ญ์ œ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ */} @@ -2503,9 +2557,7 @@ export function RepeatScreenModalComponent({ ์‚ญ์ œ ํ™•์ธ - - ์ด ํ–‰์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? ์ด ์ž‘์—…์€ ๋˜๋Œ๋ฆด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - + ์ด ํ–‰์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? ์ด ์ž‘์—…์€ ๋˜๋Œ๋ฆด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ทจ์†Œ @@ -2515,7 +2567,7 @@ export function RepeatScreenModalComponent({ handleDeleteExternalRow( pendingDeleteInfo.cardId, pendingDeleteInfo.rowId, - pendingDeleteInfo.contentRowId + pendingDeleteInfo.contentRowId, ); } }} @@ -2533,50 +2585,52 @@ export function RepeatScreenModalComponent({ // ๋‹จ์ˆœ ๋ชจ๋“œ ๋ Œ๋”๋ง (๊ทธ๋ฃนํ•‘ ์—†์Œ) return (
-
+
{cardsData.map((card, cardIndex) => ( {/* ์นด๋“œ ์ œ๋ชฉ (์„ ํƒ์‚ฌํ•ญ) */} {showCardTitle && ( - + {getCardTitle(card, cardIndex)} - {card._isDirty && (์ˆ˜์ •๋จ)} + {card._isDirty && (์ˆ˜์ •๋จ)} )} {/* ๐Ÿ†• v3: contentRows ๊ธฐ๋ฐ˜ ๋ Œ๋”๋ง */} - {useNewLayout ? ( - contentRows.map((contentRow, rowIndex) => ( -
- {renderSimpleContentRow(contentRow, card, (value, field) => - handleCardDataChange(card._cardId, field, value) - )} -
- )) - ) : ( - // ๋ ˆ๊ฑฐ์‹œ: cardLayout ์‚ฌ์šฉ - cardLayout.map((row, rowIndex) => ( -
- {row.columns.map((col, colIndex) => ( -
- {renderColumn(col, card, (value) => handleCardDataChange(card._cardId, col.field, value))} -
- ))} -
- )) - )} + {useNewLayout + ? contentRows.map((contentRow, rowIndex) => ( +
+ {renderSimpleContentRow(contentRow, card, (value, field) => + handleCardDataChange(card._cardId, field, value), + )} +
+ )) + : // ๋ ˆ๊ฑฐ์‹œ: cardLayout ์‚ฌ์šฉ + cardLayout.map((row, rowIndex) => ( +
+ {row.columns.map((col, colIndex) => ( +
+ {renderColumn(col, card, (value) => handleCardDataChange(card._cardId, col.field, value))} +
+ ))} +
+ ))}
))} @@ -2587,11 +2641,11 @@ export function RepeatScreenModalComponent({
{footerConfig.buttons.map((btn) => ( @@ -2619,7 +2673,7 @@ export function RepeatScreenModalComponent({ {/* ๋ฐ์ดํ„ฐ ์—†์Œ */} {cardsData.length === 0 && !isLoading && ( -
ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
+
ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
)}
); @@ -2630,30 +2684,30 @@ function renderContentRow( contentRow: CardContentRowConfig, card: GroupedCardData, aggregations: AggregationConfig[], - onRowDataChange: (cardId: string, rowId: string, field: string, value: any) => void + onRowDataChange: (cardId: string, rowId: string, field: string, value: any) => void, ) { switch (contentRow.type) { case "header": case "fields": // contentRow์—์„œ ์ง์ ‘ columns ๊ฐ€์ ธ์˜ค๊ธฐ (v3 ๊ตฌ์กฐ) const headerColumns = contentRow.columns || []; - + if (headerColumns.length === 0) { return ( -
+
ํ—ค๋” ์ปฌ๋Ÿผ์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
); } - + return (
@@ -2668,22 +2722,18 @@ function renderContentRow( case "aggregation": // contentRow์—์„œ ์ง์ ‘ aggregationFields ๊ฐ€์ ธ์˜ค๊ธฐ (v3 ๊ตฌ์กฐ) const aggFields = contentRow.aggregationFields || []; - + if (aggFields.length === 0) { return ( -
+
์ง‘๊ณ„ ํ•„๋“œ๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. (๋ ˆ์ด์•„์›ƒ ํƒญ์—์„œ ์ง‘๊ณ„ ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”)
); } - + return (
{aggFields.map((aggField, fieldIndex) => { // ์ง‘๊ณ„ ๊ฒฐ๊ณผ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (aggregationResultField ์‚ฌ์šฉ) @@ -2692,16 +2742,16 @@ function renderContentRow(
-
{aggField.label || aggField.aggregationResultField}
+
{aggField.label || aggField.aggregationResultField}
{typeof value === "number" ? value.toLocaleString() : value || "-"} @@ -2715,21 +2765,19 @@ function renderContentRow( case "table": // contentRow์—์„œ ์ง์ ‘ tableColumns ๊ฐ€์ ธ์˜ค๊ธฐ (v3 ๊ตฌ์กฐ) const tableColumns = contentRow.tableColumns || []; - + if (tableColumns.length === 0) { return ( -
+
ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. (๋ ˆ์ด์•„์›ƒ ํƒญ์—์„œ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”)
); } - + return ( -
+
{contentRow.tableTitle && ( -
- {contentRow.tableTitle} -
+
{contentRow.tableTitle}
)}
{contentRow.showTableHeader !== false && ( @@ -2756,10 +2804,10 @@ function renderContentRow( className={cn("text-sm", col.align && `text-${col.align}`)} > {renderTableCell( - col, - row, + col, + row, (value) => onRowDataChange(card._cardId, row._rowId, col.field, value), - row._isNew || row._isEditing + row._isNew || row._isEditing, )} ))} @@ -2779,7 +2827,7 @@ function renderContentRow( function renderSimpleContentRow( contentRow: CardContentRowConfig, card: CardData, - onChange: (value: any, field: string) => void + onChange: (value: any, field: string) => void, ) { switch (contentRow.type) { case "header": @@ -2788,10 +2836,10 @@ function renderSimpleContentRow(
@@ -2807,40 +2855,37 @@ function renderSimpleContentRow( // ๋‹จ์ˆœ ๋ชจ๋“œ์—์„œ๋„ ์ง‘๊ณ„ ํ‘œ์‹œ (๋‹จ์ผ ์นด๋“œ ๊ธฐ์ค€) // contentRow์—์„œ ์ง์ ‘ aggregationFields ๊ฐ€์ ธ์˜ค๊ธฐ (v3 ๊ตฌ์กฐ) const aggFields = contentRow.aggregationFields || []; - + if (aggFields.length === 0) { return ( -
+
์ง‘๊ณ„ ํ•„๋“œ๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
); } - + return (
{aggFields.map((aggField, fieldIndex) => { // ๋‹จ์ˆœ ๋ชจ๋“œ์—์„œ๋Š” ์นด๋“œ ๋ฐ์ดํ„ฐ์—์„œ ์ง์ ‘ ๊ฐ’์„ ๊ฐ€์ ธ์˜ด (aggregationResultField ์‚ฌ์šฉ) - const value = card[aggField.aggregationResultField] || card._originalData?.[aggField.aggregationResultField]; + const value = + card[aggField.aggregationResultField] || card._originalData?.[aggField.aggregationResultField]; return (
-
{aggField.label || aggField.aggregationResultField}
+
{aggField.label || aggField.aggregationResultField}
{typeof value === "number" ? value.toLocaleString() : value || "-"} @@ -2855,21 +2900,19 @@ function renderSimpleContentRow( // ๋‹จ์ˆœ ๋ชจ๋“œ์—์„œ๋„ ํ…Œ์ด๋ธ” ํ‘œ์‹œ (๋‹จ์ผ ํ–‰) // contentRow์—์„œ ์ง์ ‘ tableColumns ๊ฐ€์ ธ์˜ค๊ธฐ (v3 ๊ตฌ์กฐ) const tableColumns = contentRow.tableColumns || []; - + if (tableColumns.length === 0) { return ( -
+
ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
); } - + return ( -
+
{contentRow.tableTitle && ( -
- {contentRow.tableTitle} -
+
{contentRow.tableTitle}
)}
{contentRow.showTableHeader !== false && ( @@ -2910,11 +2953,7 @@ function renderSimpleContentRow( } // ๋‹จ์ˆœ ๋ชจ๋“œ ํ…Œ์ด๋ธ” ์…€ ๋ Œ๋”๋ง -function renderSimpleTableCell( - col: TableColumnConfig, - card: CardData, - onChange: (value: any) => void -) { +function renderSimpleTableCell(col: TableColumnConfig, card: CardData, onChange: (value: any) => void) { const value = card[col.field] || card._originalData?.[col.field]; if (!col.editable) { @@ -2938,12 +2977,7 @@ function renderSimpleTableCell( ); case "date": return ( - onChange(e.target.value)} - className="h-8 text-sm" - /> + onChange(e.target.value)} className="h-8 text-sm" /> ); case "select": return ( @@ -2962,12 +2996,7 @@ function renderSimpleTableCell( ); default: return ( - onChange(e.target.value)} - className="h-8 text-sm" - /> + onChange(e.target.value)} className="h-8 text-sm" /> ); } } @@ -2984,11 +3013,7 @@ function getBackgroundClass(color: string): string { } // ํ—ค๋” ์ปฌ๋Ÿผ ๋ Œ๋”๋ง (์ง‘๊ณ„๊ฐ’ ํฌํ•จ) -function renderHeaderColumn( - col: CardColumnConfig, - card: GroupedCardData, - aggregations: AggregationConfig[] -) { +function renderHeaderColumn(col: CardColumnConfig, card: GroupedCardData, aggregations: AggregationConfig[]) { let value: any; // ์ง‘๊ณ„๊ฐ’ ํƒ€์ž…์ด๋ฉด ์ง‘๊ณ„ ๊ฒฐ๊ณผ์—์„œ ๊ฐ€์ ธ์˜ด @@ -2998,16 +3023,16 @@ function renderHeaderColumn( return (
- +
{typeof value === "number" ? value.toLocaleString() : value || "-"} - {aggConfig && ({aggConfig.type})} + {aggConfig && ({aggConfig.type})}
); @@ -3018,13 +3043,9 @@ function renderHeaderColumn( return (
- +
{value || "-"}
@@ -3034,7 +3055,12 @@ function renderHeaderColumn( // ํ…Œ์ด๋ธ” ์…€ ๋ Œ๋”๋ง // ๐Ÿ†• v3.8: isRowEditable ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ - ํ–‰์ด ํŽธ์ง‘ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ์ธ์ง€ (์‹ ๊ทœ ํ–‰์ด๊ฑฐ๋‚˜ ์ˆ˜์ • ๋ชจ๋“œ) -function renderTableCell(col: TableColumnConfig, row: CardRowData, onChange: (value: any) => void, isRowEditable?: boolean) { +function renderTableCell( + col: TableColumnConfig, + row: CardRowData, + onChange: (value: any) => void, + isRowEditable?: boolean, +) { const value = row[col.field]; // Badge ํƒ€์ž… @@ -3045,7 +3071,7 @@ function renderTableCell(col: TableColumnConfig, row: CardRowData, onChange: (va // ๐Ÿ†• v3.8: ํ–‰ ์ˆ˜์ค€ ํŽธ์ง‘ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ์ฒดํฌ // isRowEditable์ด false์ด๋ฉด ์ปฌ๋Ÿผ ์„ค์ •๊ณผ ๊ด€๊ณ„์—†์ด ์ฝ๊ธฐ ์ „์šฉ - const canEdit = col.editable && (isRowEditable !== false); + const canEdit = col.editable && isRowEditable !== false; // ์ฝ๊ธฐ ์ „์šฉ if (!canEdit) { @@ -3054,7 +3080,11 @@ function renderTableCell(col: TableColumnConfig, row: CardRowData, onChange: (va } if (col.type === "date") { // ISO 8601 ํ˜•์‹์„ ํ‘œ์‹œ์šฉ์œผ๋กœ ๋ณ€ํ™˜ - const displayDate = value ? (typeof value === 'string' && value.includes('T') ? value.split('T')[0] : value) : "-"; + const displayDate = value + ? typeof value === "string" && value.includes("T") + ? value.split("T")[0] + : value + : "-"; return {displayDate}; } return {value || "-"}; @@ -3063,33 +3093,20 @@ function renderTableCell(col: TableColumnConfig, row: CardRowData, onChange: (va // ํŽธ์ง‘ ๊ฐ€๋Šฅ switch (col.type) { case "text": - return ( - onChange(e.target.value)} - className="h-8 text-sm" - /> - ); + return onChange(e.target.value)} className="h-8 text-sm" />; case "number": return ( onChange(Number(e.target.value) || 0)} - className="h-8 text-sm text-right" + className="h-8 text-right text-sm" /> ); case "date": // ISO 8601 ํ˜•์‹('2025-12-02T00:00:00.000Z')์„ 'YYYY-MM-DD' ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ - const dateValue = value ? (typeof value === 'string' && value.includes('T') ? value.split('T')[0] : value) : ""; - return ( - onChange(e.target.value)} - className="h-8 text-sm" - /> - ); + const dateValue = value ? (typeof value === "string" && value.includes("T") ? value.split("T")[0] : value) : ""; + return onChange(e.target.value)} className="h-8 text-sm" />; default: return {value || "-"}; } @@ -3108,7 +3125,7 @@ function renderColumn(col: CardColumnConfig, card: CardData, onChange: (value: a {isReadOnly && ( -
+
{value || "-"}
)} @@ -3137,7 +3154,7 @@ function renderColumn(col: CardColumnConfig, card: CardData, onChange: (value: a {col.type === "date" && ( onChange(e.target.value)} className="h-10 text-sm" /> @@ -3163,12 +3180,12 @@ function renderColumn(col: CardColumnConfig, card: CardData, onChange: (value: a value={value || ""} onChange={(e) => onChange(e.target.value)} placeholder={col.placeholder} - className="text-sm min-h-[80px]" + className="min-h-[80px] text-sm" /> )} {col.type === "component" && col.componentType && ( -
+
์ปดํฌ๋„ŒํŠธ: {col.componentType} (๊ฐœ๋ฐœ ์ค‘)
)} diff --git a/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx b/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx index 564ba780..e7917dd9 100644 --- a/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/simple-repeater-table/SimpleRepeaterTableComponent.tsx @@ -90,7 +90,7 @@ export function SimpleRepeaterTableComponent({ const newRowDefaults = componentConfig?.newRowDefaults || {}; const summaryConfig = componentConfig?.summaryConfig; const maxHeight = componentConfig?.maxHeight || propMaxHeight || "240px"; - + // ๐Ÿ†• ์ปดํฌ๋„ŒํŠธ ๋ ˆ๋ฒจ์˜ ์ €์žฅ ํ…Œ์ด๋ธ” ์„ค์ • const componentTargetTable = componentConfig?.targetTable || componentConfig?.saveTable; const componentFkColumn = componentConfig?.fkColumn; @@ -149,14 +149,11 @@ export function SimpleRepeaterTableComponent({ } // API ํ˜ธ์ถœ - const response = await apiClient.post( - `/table-management/tables/${initialConfig.sourceTable}/data`, - { - search: filters, - page: 1, - size: 1000, // ๋Œ€๋Ÿ‰ ์กฐํšŒ - } - ); + const response = await apiClient.post(`/table-management/tables/${initialConfig.sourceTable}/data`, { + search: filters, + page: 1, + size: 1000, // ๋Œ€๋Ÿ‰ ์กฐํšŒ + }); if (response.data.success && response.data.data?.data) { const loadedData = response.data.data.data; @@ -182,7 +179,7 @@ export function SimpleRepeaterTableComponent({ // 2. ์กฐ์ธ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ const joinColumns = columns.filter( - (col) => col.sourceConfig?.type === "join" && col.sourceConfig.joinTable && col.sourceConfig.joinKey + (col) => col.sourceConfig?.type === "join" && col.sourceConfig.joinTable && col.sourceConfig.joinKey, ); if (joinColumns.length > 0) { @@ -208,25 +205,20 @@ export function SimpleRepeaterTableComponent({ const [tableName] = groupKey.split(":"); // ์กฐ์ธ ํ‚ค ๊ฐ’ ์ˆ˜์ง‘ (์ค‘๋ณต ์ œ๊ฑฐ) - const keyValues = Array.from(new Set( - baseMappedData - .map((row: any) => row[key]) - .filter((v: any) => v !== undefined && v !== null) - )); + const keyValues = Array.from( + new Set(baseMappedData.map((row: any) => row[key]).filter((v: any) => v !== undefined && v !== null)), + ); if (keyValues.length === 0) return; try { // ์กฐ์ธ ํ…Œ์ด๋ธ” ์กฐํšŒ // refKey(ํƒ€๊ฒŸ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ)๋กœ ๊ฒ€์ƒ‰ - const response = await apiClient.post( - `/table-management/tables/${tableName}/data`, - { - search: { [refKey]: keyValues }, // { id: [1, 2, 3] } - page: 1, - size: 1000, - } - ); + const response = await apiClient.post(`/table-management/tables/${tableName}/data`, { + search: { [refKey]: keyValues }, // { id: [1, 2, 3] } + page: 1, + size: 1000, + }); if (response.data.success && response.data.data?.data) { const joinedRows = response.data.data.data; @@ -251,7 +243,7 @@ export function SimpleRepeaterTableComponent({ console.error(`์กฐ์ธ ์‹คํŒจ (${tableName}):`, error); // ์‹คํŒจ ์‹œ ๋ฌด์‹œํ•˜๊ณ  ์ง„ํ–‰ (๊ฐ’์€ undefined) } - }) + }), ); } @@ -296,7 +288,7 @@ export function SimpleRepeaterTableComponent({ // ๐Ÿ†• ์ปดํฌ๋„ŒํŠธ ๋ ˆ๋ฒจ์˜ targetTable์ด ์„ค์ •๋˜์–ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ if (componentTargetTable) { console.log("โœ… [SimpleRepeaterTable] ์ปดํฌ๋„ŒํŠธ ๋ ˆ๋ฒจ ์ €์žฅ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ:", componentTargetTable); - + // ๋ชจ๋“  ํ–‰์„ ํ•ด๋‹น ํ…Œ์ด๋ธ”์— ์ €์žฅ const dataToSave = value.map((row: any) => { // ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ•„๋“œ ์ œ์™ธ (_, _rowIndex ๋“ฑ) @@ -399,9 +391,12 @@ export function SimpleRepeaterTableComponent({ // ๊ธฐ์กด onFormDataChange๋„ ํ˜ธ์ถœ (ํ˜ธํ™˜์„ฑ) if (onFormDataChange && columnName) { // ํ…Œ์ด๋ธ”๋ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ํ†ตํ•ฉํ•˜์—ฌ ์ „๋‹ฌ - onFormDataChange(columnName, Object.entries(dataByTable).flatMap(([table, rows]) => - rows.map((row: any) => ({ ...row, _targetTable: table })) - )); + onFormDataChange( + columnName, + Object.entries(dataByTable).flatMap(([table, rows]) => + rows.map((row: any) => ({ ...row, _targetTable: table })), + ), + ); } }; @@ -543,24 +538,14 @@ export function SimpleRepeaterTableComponent({ if (!allowAdd || readOnly || value.length >= maxRows) return null; return ( - ); }; - const renderCell = ( - row: any, - column: SimpleRepeaterColumnConfig, - rowIndex: number - ) => { + const renderCell = (row: any, column: SimpleRepeaterColumnConfig, rowIndex: number) => { const cellValue = row[column.field]; // ๊ณ„์‚ฐ ํ•„๋“œ๋Š” ํŽธ์ง‘ ๋ถˆ๊ฐ€ @@ -583,9 +568,7 @@ export function SimpleRepeaterTableComponent({ - handleCellEdit(rowIndex, column.field, parseFloat(e.target.value) || 0) - } + onChange={(e) => handleCellEdit(rowIndex, column.field, parseFloat(e.target.value) || 0)} className="h-7 text-xs" /> ); @@ -604,19 +587,19 @@ export function SimpleRepeaterTableComponent({ return ( ); @@ -636,11 +619,11 @@ export function SimpleRepeaterTableComponent({ // ๋กœ๋”ฉ ์ค‘์ผ ๋•Œ if (isLoading) { return ( -
+
- -

๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+ +

๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

@@ -650,14 +633,14 @@ export function SimpleRepeaterTableComponent({ // ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ if (loadError) { return ( -
+
-
- +
+
-

๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ

-

{loadError}

+

๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ

+

{loadError}

@@ -668,30 +651,27 @@ export function SimpleRepeaterTableComponent({ const totalColumns = columns.length + (showRowNumber ? 1 : 0) + (allowDelete && !readOnly ? 1 : 0); return ( -
+
{/* ์ƒ๋‹จ ํ–‰ ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} {allowAdd && addButtonPosition !== "bottom" && ( -
+
)} -
+
{showRowNumber && ( - )} {columns.map((col) => ( ))} {!readOnly && allowDelete && ( - )} @@ -707,11 +687,8 @@ export function SimpleRepeaterTableComponent({ {value.length === 0 ? ( - - + ) : ( value.map((row, rowIndex) => ( - + {showRowNumber && ( - )} {columns.map((col) => ( - ))} {!readOnly && allowDelete && ( - + + + ); +} + +export default GroupHeader; diff --git a/frontend/lib/registry/components/v2-table-grouped/config.ts b/frontend/lib/registry/components/v2-table-grouped/config.ts new file mode 100644 index 00000000..fb38744c --- /dev/null +++ b/frontend/lib/registry/components/v2-table-grouped/config.ts @@ -0,0 +1,64 @@ +import { TableGroupedConfig } from "./types"; + +/** + * v2-table-grouped ๊ธฐ๋ณธ ์„ค์ •๊ฐ’ + */ +export const defaultTableGroupedConfig: Partial = { + // ๊ทธ๋ฃนํ™” ๊ธฐ๋ณธ ์„ค์ • + groupConfig: { + groupByColumn: "", + groupLabelFormat: "{value}", + defaultExpanded: true, + sortDirection: "asc", + summary: { + showCount: true, + sumColumns: [], + }, + }, + + // ์ฒดํฌ๋ฐ•์Šค ๊ธฐ๋ณธ ์„ค์ • + showCheckbox: false, + checkboxMode: "multi", + + // ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ธฐ๋ณธ ์„ค์ • + pagination: { + enabled: false, + pageSize: 10, + }, + + // UI ๊ธฐ๋ณธ ์„ค์ • + isReadOnly: false, + rowClickable: true, + showExpandAllButton: true, + groupHeaderStyle: "default", + emptyMessage: "๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", + + // ๋†’์ด ๊ธฐ๋ณธ ์„ค์ • + height: "auto", + maxHeight: 600, +}; + +/** + * ๊ทธ๋ฃน ํ—ค๋” ์Šคํƒ€์ผ ์˜ต์…˜ + */ +export const groupHeaderStyleOptions = [ + { value: "default", label: "๊ธฐ๋ณธ" }, + { value: "compact", label: "์ปดํŒฉํŠธ" }, + { value: "card", label: "์นด๋“œ" }, +]; + +/** + * ์ฒดํฌ๋ฐ•์Šค ๋ชจ๋“œ ์˜ต์…˜ + */ +export const checkboxModeOptions = [ + { value: "single", label: "๋‹จ์ผ ์„ ํƒ" }, + { value: "multi", label: "๋‹ค์ค‘ ์„ ํƒ" }, +]; + +/** + * ์ •๋ ฌ ๋ฐฉํ–ฅ ์˜ต์…˜ + */ +export const sortDirectionOptions = [ + { value: "asc", label: "์˜ค๋ฆ„์ฐจ์ˆœ" }, + { value: "desc", label: "๋‚ด๋ฆผ์ฐจ์ˆœ" }, +]; diff --git a/frontend/lib/registry/components/v2-table-grouped/hooks/useGroupedData.ts b/frontend/lib/registry/components/v2-table-grouped/hooks/useGroupedData.ts new file mode 100644 index 00000000..d9f40aca --- /dev/null +++ b/frontend/lib/registry/components/v2-table-grouped/hooks/useGroupedData.ts @@ -0,0 +1,389 @@ +"use client"; + +import { useState, useCallback, useMemo, useEffect } from "react"; +import { + TableGroupedConfig, + GroupState, + GroupSummary, + UseGroupedDataResult, +} from "../types"; +import { apiClient } from "@/lib/api/client"; + +/** + * ๊ทธ๋ฃน ์š”์•ฝ ๋ฐ์ดํ„ฐ ๊ณ„์‚ฐ + */ +function calculateSummary( + items: any[], + config: TableGroupedConfig +): GroupSummary { + const summary: GroupSummary = { + count: items.length, + }; + + const summaryConfig = config.groupConfig?.summary; + if (!summaryConfig) return summary; + + // ํ•ฉ๊ณ„ ๊ณ„์‚ฐ + if (summaryConfig.sumColumns && summaryConfig.sumColumns.length > 0) { + summary.sum = {}; + for (const col of summaryConfig.sumColumns) { + summary.sum[col] = items.reduce((acc, item) => { + const val = parseFloat(item[col]); + return acc + (isNaN(val) ? 0 : val); + }, 0); + } + } + + // ํ‰๊ท  ๊ณ„์‚ฐ + if (summaryConfig.avgColumns && summaryConfig.avgColumns.length > 0) { + summary.avg = {}; + for (const col of summaryConfig.avgColumns) { + const validItems = items.filter( + (item) => item[col] !== null && item[col] !== undefined + ); + const sum = validItems.reduce((acc, item) => { + const val = parseFloat(item[col]); + return acc + (isNaN(val) ? 0 : val); + }, 0); + summary.avg[col] = validItems.length > 0 ? sum / validItems.length : 0; + } + } + + // ์ตœ๋Œ€๊ฐ’ ๊ณ„์‚ฐ + if (summaryConfig.maxColumns && summaryConfig.maxColumns.length > 0) { + summary.max = {}; + for (const col of summaryConfig.maxColumns) { + const values = items + .map((item) => parseFloat(item[col])) + .filter((v) => !isNaN(v)); + summary.max[col] = values.length > 0 ? Math.max(...values) : 0; + } + } + + // ์ตœ์†Œ๊ฐ’ ๊ณ„์‚ฐ + if (summaryConfig.minColumns && summaryConfig.minColumns.length > 0) { + summary.min = {}; + for (const col of summaryConfig.minColumns) { + const values = items + .map((item) => parseFloat(item[col])) + .filter((v) => !isNaN(v)); + summary.min[col] = values.length > 0 ? Math.min(...values) : 0; + } + } + + return summary; +} + +/** + * ๊ทธ๋ฃน ๋ผ๋ฒจ ํฌ๋งทํŒ… + */ +function formatGroupLabel( + groupValue: any, + item: any, + format?: string +): string { + if (!format) { + return String(groupValue ?? "(๋นˆ ๊ฐ’)"); + } + + // {value}๋ฅผ ๊ทธ๋ฃน ๊ฐ’์œผ๋กœ ์น˜ํ™˜ + let label = format.replace("{value}", String(groupValue ?? "(๋นˆ ๊ฐ’)")); + + // {์ปฌ๋Ÿผ๋ช…} ํŒจํ„ด์„ ํ•ด๋‹น ์ปฌ๋Ÿผ ๊ฐ’์œผ๋กœ ์น˜ํ™˜ + const columnPattern = /\{([^}]+)\}/g; + label = label.replace(columnPattern, (match, columnName) => { + if (columnName === "value") return String(groupValue ?? ""); + return String(item?.[columnName] ?? ""); + }); + + return label; +} + +/** + * ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋ฃนํ™”ํ•˜๋Š” ํ›… + */ +export function useGroupedData( + config: TableGroupedConfig, + externalData?: any[], + searchFilters?: Record +): UseGroupedDataResult { + // ์›๋ณธ ๋ฐ์ดํ„ฐ + const [rawData, setRawData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // ๊ทธ๋ฃน ํŽผ์นจ ์ƒํƒœ ๊ด€๋ฆฌ + const [expandedGroups, setExpandedGroups] = useState>(new Set()); + // ์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜๋™์œผ๋กœ ํŽผ์นจ/์ ‘๊ธฐ๋ฅผ ์กฐ์ž‘ํ–ˆ๋Š”์ง€ ์—ฌ๋ถ€ + const [isManuallyControlled, setIsManuallyControlled] = useState(false); + + // ์„ ํƒ ์ƒํƒœ ๊ด€๋ฆฌ + const [selectedItemIds, setSelectedItemIds] = useState>( + new Set() + ); + + // ํ…Œ์ด๋ธ”๋ช… ๊ฒฐ์ • + const tableName = config.useCustomTable + ? config.customTableName + : config.selectedTable; + + // ๋ฐ์ดํ„ฐ ๋กœ๋“œ + const fetchData = useCallback(async () => { + if (externalData) { + setRawData(externalData); + return; + } + + if (!tableName) { + setRawData([]); + return; + } + + setIsLoading(true); + setError(null); + + try { + const response = await apiClient.post( + `/table-management/tables/${tableName}/data`, + { + page: 1, + size: 10000, // ๊ทธ๋ฃนํ™”๋ฅผ ์œ„ํ•ด ์ „์ฒด ๋ฐ์ดํ„ฐ ๋กœ๋“œ + autoFilter: true, + search: searchFilters || {}, + } + ); + + const responseData = response.data?.data?.data || response.data?.data || []; + setRawData(Array.isArray(responseData) ? responseData : []); + } catch (err: any) { + setError(err.message || "๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"); + setRawData([]); + } finally { + setIsLoading(false); + } + }, [tableName, externalData, searchFilters]); + + // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + useEffect(() => { + fetchData(); + }, [fetchData]); + + // ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ๋™๊ธฐํ™” + useEffect(() => { + if (externalData) { + setRawData(externalData); + } + }, [externalData]); + + // ๊ทธ๋ฃนํ™”๋œ ๋ฐ์ดํ„ฐ ๊ณ„์‚ฐ + const groups = useMemo((): GroupState[] => { + const groupByColumn = config.groupConfig?.groupByColumn; + if (!groupByColumn || rawData.length === 0) { + return []; + } + + // ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋ฃน๋ณ„๋กœ ๋ถ„๋ฅ˜ + const groupMap = new Map(); + + for (const item of rawData) { + const groupValue = item[groupByColumn]; + const groupKey = String(groupValue ?? "__null__"); + + if (!groupMap.has(groupKey)) { + groupMap.set(groupKey, []); + } + groupMap.get(groupKey)!.push(item); + } + + // ๊ทธ๋ฃน ๋ฐฐ์—ด ์ƒ์„ฑ + const groupArray: GroupState[] = []; + const defaultExpanded = config.groupConfig?.defaultExpanded ?? true; + + for (const [groupKey, items] of groupMap.entries()) { + const firstItem = items[0]; + const groupValue = + groupKey === "__null__" ? null : firstItem[groupByColumn]; + + // ํŽผ์นจ ์ƒํƒœ ๊ฒฐ์ •: ์ˆ˜๋™ ์กฐ์ž‘ ์ „์—๋Š” defaultExpanded, ์ˆ˜๋™ ์กฐ์ž‘ ํ›„์—๋Š” expandedGroups ์ฐธ์กฐ + const isExpanded = isManuallyControlled + ? expandedGroups.has(groupKey) + : defaultExpanded; + + groupArray.push({ + groupKey, + groupLabel: formatGroupLabel( + groupValue, + firstItem, + config.groupConfig?.groupLabelFormat + ), + expanded: isExpanded, + items, + summary: calculateSummary(items, config), + selected: items.every((item) => + selectedItemIds.has(getItemId(item, config)) + ), + selectedItemIds: items + .filter((item) => selectedItemIds.has(getItemId(item, config))) + .map((item) => getItemId(item, config)), + }); + } + + // ์ •๋ ฌ + const sortDirection = config.groupConfig?.sortDirection ?? "asc"; + groupArray.sort((a, b) => { + const comparison = a.groupLabel.localeCompare(b.groupLabel, "ko"); + return sortDirection === "asc" ? comparison : -comparison; + }); + + return groupArray; + }, [rawData, config, expandedGroups, selectedItemIds, isManuallyControlled]); + + // ์•„์ดํ…œ ID ์ถ”์ถœ + function getItemId(item: any, cfg: TableGroupedConfig): string { + // id ๋˜๋Š” ์ฒซ ๋ฒˆ์งธ ์ปฌ๋Ÿผ์„ ID๋กœ ์‚ฌ์šฉ + if (item.id !== undefined) return String(item.id); + const firstCol = cfg.columns?.[0]?.columnName; + if (firstCol && item[firstCol] !== undefined) return String(item[firstCol]); + return JSON.stringify(item); + } + + // ๊ทธ๋ฃน ํ† ๊ธ€ + const toggleGroup = useCallback((groupKey: string) => { + setIsManuallyControlled(true); + setExpandedGroups((prev) => { + const next = new Set(prev); + if (next.has(groupKey)) { + next.delete(groupKey); + } else { + next.add(groupKey); + } + return next; + }); + }, []); + + // ์ „์ฒด ํŽผ์น˜๊ธฐ + const expandAll = useCallback(() => { + setIsManuallyControlled(true); + setExpandedGroups(new Set(groups.map((g) => g.groupKey))); + }, [groups]); + + // ์ „์ฒด ์ ‘๊ธฐ + const collapseAll = useCallback(() => { + setIsManuallyControlled(true); + setExpandedGroups(new Set()); + }, []); + + // ์•„์ดํ…œ ์„ ํƒ ํ† ๊ธ€ + const toggleItemSelection = useCallback( + (groupKey: string, itemId: string) => { + setSelectedItemIds((prev) => { + const next = new Set(prev); + if (next.has(itemId)) { + next.delete(itemId); + } else { + // ๋‹จ์ผ ์„ ํƒ ๋ชจ๋“œ + if (config.checkboxMode === "single") { + next.clear(); + } + next.add(itemId); + } + return next; + }); + }, + [config.checkboxMode] + ); + + // ๊ทธ๋ฃน ์ „์ฒด ์„ ํƒ ํ† ๊ธ€ + const toggleGroupSelection = useCallback( + (groupKey: string) => { + const group = groups.find((g) => g.groupKey === groupKey); + if (!group) return; + + setSelectedItemIds((prev) => { + const next = new Set(prev); + const groupItemIds = group.items.map((item) => getItemId(item, config)); + const allSelected = groupItemIds.every((id) => next.has(id)); + + if (allSelected) { + // ์ „์ฒด ํ•ด์ œ + for (const id of groupItemIds) { + next.delete(id); + } + } else { + // ์ „์ฒด ์„ ํƒ + if (config.checkboxMode === "single") { + next.clear(); + next.add(groupItemIds[0]); + } else { + for (const id of groupItemIds) { + next.add(id); + } + } + } + return next; + }); + }, + [groups, config] + ); + + // ์ „์ฒด ์„ ํƒ ํ† ๊ธ€ + const toggleAllSelection = useCallback(() => { + const allItemIds = rawData.map((item) => getItemId(item, config)); + const allSelected = allItemIds.every((id) => selectedItemIds.has(id)); + + if (allSelected) { + setSelectedItemIds(new Set()); + } else { + if (config.checkboxMode === "single" && allItemIds.length > 0) { + setSelectedItemIds(new Set([allItemIds[0]])); + } else { + setSelectedItemIds(new Set(allItemIds)); + } + } + }, [rawData, config, selectedItemIds]); + + // ์„ ํƒ๋œ ์•„์ดํ…œ ๋ชฉ๋ก + const selectedItems = useMemo(() => { + return rawData.filter((item) => + selectedItemIds.has(getItemId(item, config)) + ); + }, [rawData, selectedItemIds, config]); + + // ๋ชจ๋‘ ์„ ํƒ ์—ฌ๋ถ€ + const isAllSelected = useMemo(() => { + if (rawData.length === 0) return false; + return rawData.every((item) => + selectedItemIds.has(getItemId(item, config)) + ); + }, [rawData, selectedItemIds, config]); + + // ์ผ๋ถ€ ์„ ํƒ ์—ฌ๋ถ€ + const isIndeterminate = useMemo(() => { + if (rawData.length === 0) return false; + const selectedCount = rawData.filter((item) => + selectedItemIds.has(getItemId(item, config)) + ).length; + return selectedCount > 0 && selectedCount < rawData.length; + }, [rawData, selectedItemIds, config]); + + return { + groups, + isLoading, + error, + toggleGroup, + expandAll, + collapseAll, + toggleItemSelection, + toggleGroupSelection, + toggleAllSelection, + selectedItems, + isAllSelected, + isIndeterminate, + refresh: fetchData, + rawData, + totalCount: rawData.length, + groupCount: groups.length, + }; +} + +export default useGroupedData; diff --git a/frontend/lib/registry/components/v2-table-grouped/index.ts b/frontend/lib/registry/components/v2-table-grouped/index.ts new file mode 100644 index 00000000..7e984490 --- /dev/null +++ b/frontend/lib/registry/components/v2-table-grouped/index.ts @@ -0,0 +1,75 @@ +"use client"; + +import React from "react"; +import { createComponentDefinition } from "../../utils/createComponentDefinition"; +import { ComponentCategory } from "@/types/component"; +import type { WebType } from "@/types/screen"; +import { TableGroupedComponent } from "./TableGroupedComponent"; +import { TableGroupedConfigPanel } from "./TableGroupedConfigPanel"; +import { TableGroupedConfig } from "./types"; + +/** + * V2 ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ Definition + * + * ๋ฐ์ดํ„ฐ๋ฅผ ํŠน์ • ์ปฌ๋Ÿผ ๊ธฐ์ค€์œผ๋กœ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * v2-table-list๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ™•์žฅ๋œ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค. + */ +export const V2TableGroupedDefinition = createComponentDefinition({ + id: "v2-table-grouped", + name: "๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ”", + nameEng: "Grouped Table Component", + description: "๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ํ…Œ์ด๋ธ”", + category: ComponentCategory.DISPLAY, + webType: "text", + component: TableGroupedComponent, + defaultConfig: { + // ํ…Œ์ด๋ธ” ์„ค์ • + selectedTable: "", + useCustomTable: false, + customTableName: "", + + // ๊ทธ๋ฃนํ™” ์„ค์ • + groupConfig: { + groupByColumn: "", + groupLabelFormat: "{value}", + defaultExpanded: true, + sortDirection: "asc", + summary: { + showCount: true, + sumColumns: [], + }, + }, + + // ์ปฌ๋Ÿผ ์„ค์ • + columns: [], + + // ์ฒดํฌ๋ฐ•์Šค ์„ค์ • + showCheckbox: false, + checkboxMode: "multi", + + // ํŽ˜์ด์ง€๋„ค์ด์…˜ ์„ค์ • + pagination: { + enabled: false, + pageSize: 10, + }, + + // UI ์„ค์ • + isReadOnly: false, + rowClickable: true, + showExpandAllButton: true, + groupHeaderStyle: "default", + emptyMessage: "๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", + height: "auto", + maxHeight: 600, + }, + defaultSize: { width: 800, height: 500 }, + configPanel: TableGroupedConfigPanel, + icon: "Layers", + tags: ["ํ…Œ์ด๋ธ”", "๊ทธ๋ฃนํ™”", "์ ‘๊ธฐ", "ํŽผ์น˜๊ธฐ", "๋ชฉ๋ก"], + version: "1.0.0", + author: "๊ฐœ๋ฐœํŒ€", + documentation: "", +}); + +// ํƒ€์ž… ๋‚ด๋ณด๋‚ด๊ธฐ +export type { TableGroupedConfig } from "./types"; diff --git a/frontend/lib/registry/components/v2-table-grouped/types.ts b/frontend/lib/registry/components/v2-table-grouped/types.ts new file mode 100644 index 00000000..20bfc77b --- /dev/null +++ b/frontend/lib/registry/components/v2-table-grouped/types.ts @@ -0,0 +1,299 @@ +"use client"; + +import { ComponentConfig } from "@/types/component"; +import { ColumnConfig, EntityJoinInfo } from "../v2-table-list/types"; + +/** + * ๊ทธ๋ฃน ์š”์•ฝ ์„ค์ • + */ +export interface GroupSummaryConfig { + /** ํ•ฉ๊ณ„๋ฅผ ๊ณ„์‚ฐํ•  ์ปฌ๋Ÿผ ๋ชฉ๋ก */ + sumColumns?: string[]; + /** ๊ฐœ์ˆ˜ ํ‘œ์‹œ ์—ฌ๋ถ€ */ + showCount?: boolean; + /** ํ‰๊ท  ์ปฌ๋Ÿผ ๋ชฉ๋ก */ + avgColumns?: string[]; + /** ์ตœ๋Œ€๊ฐ’ ์ปฌ๋Ÿผ ๋ชฉ๋ก */ + maxColumns?: string[]; + /** ์ตœ์†Œ๊ฐ’ ์ปฌ๋Ÿผ ๋ชฉ๋ก */ + minColumns?: string[]; +} + +/** + * ๊ทธ๋ฃนํ™” ์„ค์ • + */ +export interface GroupConfig { + /** ๊ทธ๋ฃนํ™” ๊ธฐ์ค€ ์ปฌ๋Ÿผ */ + groupByColumn: string; + + /** ๊ทธ๋ฃน ํ‘œ์‹œ ํ˜•์‹ (์˜ˆ: "{item_name} ({item_code})") */ + groupLabelFormat?: string; + + /** ๊ทธ๋ฃน ์š”์•ฝ ์„ค์ • */ + summary?: GroupSummaryConfig; + + /** ์ดˆ๊ธฐ ํŽผ์นจ ์ƒํƒœ (๊ธฐ๋ณธ๊ฐ’: true) */ + defaultExpanded?: boolean; + + /** ์ค‘์ฒฉ ๊ทธ๋ฃน (๋‹ค์ค‘ ๊ทธ๋ฃนํ™”) - ํ–ฅํ›„ ํ™•์žฅ */ + nestedGroup?: GroupConfig; + + /** ๊ทธ๋ฃน ์ •๋ ฌ ๋ฐฉ์‹ */ + sortDirection?: "asc" | "desc"; + + /** ๊ทธ๋ฃน ์ •๋ ฌ ์ปฌ๋Ÿผ (๊ธฐ๋ณธ: groupByColumn) */ + sortColumn?: string; +} + +/** + * ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” ์„ค์ • (ComponentConfig ๊ธฐ๋ฐ˜) + */ +export interface TableGroupedConfig extends ComponentConfig { + /** ํ…Œ์ด๋ธ”๋ช… */ + selectedTable?: string; + + /** ์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ ์—ฌ๋ถ€ */ + useCustomTable?: boolean; + + /** ์ปค์Šคํ…€ ํ…Œ์ด๋ธ”๋ช… */ + customTableName?: string; + + /** ๊ทธ๋ฃนํ™” ์„ค์ • */ + groupConfig: GroupConfig; + + /** ์ปฌ๋Ÿผ ์„ค์ • */ + columns?: ColumnConfig[]; + + /** ์ฒดํฌ๋ฐ•์Šค ํ‘œ์‹œ ์—ฌ๋ถ€ */ + showCheckbox?: boolean; + + /** ์ฒดํฌ๋ฐ•์Šค ๋ชจ๋“œ */ + checkboxMode?: "single" | "multi"; + + /** ํŽ˜์ด์ง€๋„ค์ด์…˜ (๊ทธ๋ฃน ๋‹จ์œ„) */ + pagination?: { + enabled: boolean; + pageSize: number; + }; + + /** ๊ธฐ๋ณธ ์ •๋ ฌ ์„ค์ • */ + defaultSort?: { + column: string; + direction: "asc" | "desc"; + }; + + /** ์ฝ๊ธฐ ์ „์šฉ */ + isReadOnly?: boolean; + + /** ํ–‰ ํด๋ฆญ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ */ + rowClickable?: boolean; + + /** ๋†’์ด ์„ค์ • */ + height?: number | string; + + /** ์ตœ๋Œ€ ๋†’์ด */ + maxHeight?: number | string; + + /** ์ „์ฒด ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ ๋ฒ„ํŠผ ํ‘œ์‹œ */ + showExpandAllButton?: boolean; + + /** ๊ทธ๋ฃน ํ—ค๋” ์Šคํƒ€์ผ */ + groupHeaderStyle?: "default" | "compact" | "card"; + + /** ๋นˆ ๋ฐ์ดํ„ฐ ๋ฉ”์‹œ์ง€ */ + emptyMessage?: string; + + /** Entity ์กฐ์ธ ์ปฌ๋Ÿผ ์ •๋ณด */ + entityJoinColumns?: Array<{ + columnName: string; + entityJoinInfo: EntityJoinInfo; + }>; + + /** ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ */ + dataFilter?: { + column: string; + operator: "eq" | "ne" | "gt" | "lt" | "gte" | "lte" | "like" | "in"; + value: any; + }[]; + + /** ์—ฐ๊ฒฐ๋œ ํ•„ํ„ฐ ์„ค์ • (๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์™€ ์—ฐ๋™) */ + linkedFilters?: LinkedFilterConfig[]; +} + +/** + * ์—ฐ๊ฒฐ๋œ ํ•„ํ„ฐ ์„ค์ • + */ +export interface LinkedFilterConfig { + /** ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ ID */ + sourceComponentId: string; + /** ์†Œ์Šค ํ•„๋“œ */ + sourceField?: string; + /** ๋Œ€์ƒ ์ปฌ๋Ÿผ */ + targetColumn: string; + /** ํ™œ์„ฑํ™” ์—ฌ๋ถ€ */ + enabled?: boolean; +} + +/** + * ๊ทธ๋ฃน ์š”์•ฝ ๋ฐ์ดํ„ฐ + */ +export interface GroupSummary { + /** ๊ฐœ์ˆ˜ */ + count: number; + /** ํ•ฉ๊ณ„ (์ปฌ๋Ÿผ๋ณ„) */ + sum?: Record; + /** ํ‰๊ท  (์ปฌ๋Ÿผ๋ณ„) */ + avg?: Record; + /** ์ตœ๋Œ€๊ฐ’ (์ปฌ๋Ÿผ๋ณ„) */ + max?: Record; + /** ์ตœ์†Œ๊ฐ’ (์ปฌ๋Ÿผ๋ณ„) */ + min?: Record; +} + +/** + * ๊ทธ๋ฃน ์ƒํƒœ + */ +export interface GroupState { + /** ๊ทธ๋ฃน ํ‚ค (groupByColumn ๊ฐ’) */ + groupKey: string; + + /** ๊ทธ๋ฃน ํ‘œ์‹œ ๋ผ๋ฒจ */ + groupLabel: string; + + /** ํŽผ์นจ ์—ฌ๋ถ€ */ + expanded: boolean; + + /** ๊ทธ๋ฃน ๋‚ด ๋ฐ์ดํ„ฐ */ + items: any[]; + + /** ๊ทธ๋ฃน ์š”์•ฝ ๋ฐ์ดํ„ฐ */ + summary: GroupSummary; + + /** ๊ทธ๋ฃน ์„ ํƒ ์—ฌ๋ถ€ */ + selected?: boolean; + + /** ๊ทธ๋ฃน ๋‚ด ์„ ํƒ๋œ ์•„์ดํ…œ ID ๋ชฉ๋ก */ + selectedItemIds?: string[]; +} + +/** + * ์„ ํƒ ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ + */ +export interface SelectionChangeEvent { + /** ์„ ํƒ๋œ ๊ทธ๋ฃน ํ‚ค ๋ชฉ๋ก */ + selectedGroups: string[]; + /** ์„ ํƒ๋œ ์•„์ดํ…œ (์ „์ฒด) */ + selectedItems: any[]; + /** ๋ชจ๋‘ ์„ ํƒ ์—ฌ๋ถ€ */ + isAllSelected: boolean; +} + +/** + * ๊ทธ๋ฃน ํ† ๊ธ€ ์ด๋ฒคํŠธ + */ +export interface GroupToggleEvent { + /** ๊ทธ๋ฃน ํ‚ค */ + groupKey: string; + /** ํŽผ์นจ ์ƒํƒœ */ + expanded: boolean; +} + +/** + * ํ–‰ ํด๋ฆญ ์ด๋ฒคํŠธ + */ +export interface RowClickEvent { + /** ํด๋ฆญ๋œ ํ–‰ ๋ฐ์ดํ„ฐ */ + row: any; + /** ๊ทธ๋ฃน ํ‚ค */ + groupKey: string; + /** ๊ทธ๋ฃน ๋‚ด ์ธ๋ฑ์Šค */ + indexInGroup: number; +} + +/** + * TableGroupedComponent Props + */ +export interface TableGroupedComponentProps { + /** ์ปดํฌ๋„ŒํŠธ ์„ค์ • */ + config: TableGroupedConfig; + + /** ๋””์ž์ธ ๋ชจ๋“œ ์—ฌ๋ถ€ */ + isDesignMode?: boolean; + + /** ํผ ๋ฐ์ดํ„ฐ (formData ์ „๋‹ฌ์šฉ) */ + formData?: Record; + + /** ์„ ํƒ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ */ + onSelectionChange?: (event: SelectionChangeEvent) => void; + + /** ๊ทธ๋ฃน ํ† ๊ธ€ ์ด๋ฒคํŠธ */ + onGroupToggle?: (event: GroupToggleEvent) => void; + + /** ํ–‰ ํด๋ฆญ ์ด๋ฒคํŠธ */ + onRowClick?: (event: RowClickEvent) => void; + + /** ์™ธ๋ถ€์—์„œ ์ฃผ์ž…๋œ ๋ฐ์ดํ„ฐ (์„ ํƒ) */ + externalData?: any[]; + + /** ๋กœ๋”ฉ ์ƒํƒœ (์™ธ๋ถ€ ์ œ์–ด) */ + isLoading?: boolean; + + /** ์—๋Ÿฌ ์ƒํƒœ (์™ธ๋ถ€ ์ œ์–ด) */ + error?: string; + + /** ์ปดํฌ๋„ŒํŠธ ID */ + componentId?: string; +} + +/** + * useGroupedData ํ›… ๋ฐ˜ํ™˜ ํƒ€์ž… + */ +export interface UseGroupedDataResult { + /** ๊ทธ๋ฃนํ™”๋œ ๋ฐ์ดํ„ฐ */ + groups: GroupState[]; + + /** ๋กœ๋”ฉ ์ƒํƒœ */ + isLoading: boolean; + + /** ์—๋Ÿฌ */ + error: string | null; + + /** ๊ทธ๋ฃน ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ ํ† ๊ธ€ */ + toggleGroup: (groupKey: string) => void; + + /** ์ „์ฒด ํŽผ์น˜๊ธฐ */ + expandAll: () => void; + + /** ์ „์ฒด ์ ‘๊ธฐ */ + collapseAll: () => void; + + /** ์•„์ดํ…œ ์„ ํƒ ํ† ๊ธ€ */ + toggleItemSelection: (groupKey: string, itemId: string) => void; + + /** ๊ทธ๋ฃน ์ „์ฒด ์„ ํƒ ํ† ๊ธ€ */ + toggleGroupSelection: (groupKey: string) => void; + + /** ์ „์ฒด ์„ ํƒ ํ† ๊ธ€ */ + toggleAllSelection: () => void; + + /** ์„ ํƒ๋œ ์•„์ดํ…œ ๋ชฉ๋ก */ + selectedItems: any[]; + + /** ๋ชจ๋‘ ์„ ํƒ ์—ฌ๋ถ€ */ + isAllSelected: boolean; + + /** ์ผ๋ถ€ ์„ ํƒ ์—ฌ๋ถ€ */ + isIndeterminate: boolean; + + /** ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ */ + refresh: () => void; + + /** ์›๋ณธ ๋ฐ์ดํ„ฐ */ + rawData: any[]; + + /** ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ */ + totalCount: number; + + /** ๊ทธ๋ฃน ๊ฐœ์ˆ˜ */ + groupCount: number; +} diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index c99f9876..6e529ab9 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -8,6 +8,7 @@ import { entityJoinApi } from "@/lib/api/entityJoin"; import { codeCache } from "@/lib/caching/codeCache"; import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization"; import { getFullImageUrl } from "@/lib/api/client"; +import { getFilePreviewUrl } from "@/lib/api/file"; import { Button } from "@/components/ui/button"; import { v2EventBus, V2_EVENTS, V2ErrorBoundary } from "@/lib/v2-core"; @@ -85,7 +86,7 @@ interface GroupedData { // ์บ์‹œ ๋ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ // ======================================== -const tableColumnCache = new Map(); +const tableColumnCache = new Map(); const tableInfoCache = new Map(); const TABLE_CACHE_TTL = 5 * 60 * 1000; // 5๋ถ„ @@ -459,6 +460,9 @@ export const TableListComponent: React.FC = ({ // ๐Ÿ†• Filter Builder (๊ณ ๊ธ‰ ํ•„ํ„ฐ) ๊ด€๋ จ ์ƒํƒœ - filteredData๋ณด๋‹ค ๋จผ์ € ์ •์˜ํ•ด์•ผ ํ•จ const [filterGroups, setFilterGroups] = useState([]); + + // ๐Ÿ†• joinColumnMapping - filteredData์—์„œ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ๋จผ์ € ์ •์˜ํ•ด์•ผ ํ•จ + const [joinColumnMapping, setJoinColumnMapping] = useState>({}); // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„์—์„œ ์šฐ์ธก์— ์ด๋ฏธ ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ํ•„ํ„ฐ๋ง (์ขŒ์ธก ํ…Œ์ด๋ธ”์—๋งŒ ์ ์šฉ) + ํ—ค๋” ํ•„ํ„ฐ const filteredData = useMemo(() => { @@ -473,14 +477,17 @@ export const TableListComponent: React.FC = ({ }); } - // 2. ํ—ค๋” ํ•„ํ„ฐ ์ ์šฉ (joinColumnMapping ์‚ฌ์šฉ ์•ˆ ํ•จ - ์ง์ ‘ ์ปฌ๋Ÿผ๋ช… ์‚ฌ์šฉ) + // 2. ํ—ค๋” ํ•„ํ„ฐ ์ ์šฉ (joinColumnMapping ์‚ฌ์šฉ - ์กฐ์ธ๋œ ์ปฌ๋Ÿผ๊ณผ ์ผ์น˜ํ•ด์•ผ ํ•จ) if (Object.keys(headerFilters).length > 0) { result = result.filter((row) => { return Object.entries(headerFilters).every(([columnName, values]) => { if (values.size === 0) return true; - // ์—ฌ๋Ÿฌ ๊ฐ€๋Šฅํ•œ ์ปฌ๋Ÿผ๋ช… ์‹œ๋„ - const cellValue = row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()]; + // joinColumnMapping์„ ์‚ฌ์šฉํ•˜์—ฌ ์กฐ์ธ๋œ ์ปฌ๋Ÿผ๋ช… ํ™•์ธ + const mappedColumnName = joinColumnMapping[columnName] || columnName; + + // ์—ฌ๋Ÿฌ ๊ฐ€๋Šฅํ•œ ์ปฌ๋Ÿผ๋ช… ์‹œ๋„ (mappedColumnName ์šฐ์„ ) + const cellValue = row[mappedColumnName] ?? row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()]; const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue) : ""; return values.has(cellStr); @@ -541,7 +548,7 @@ export const TableListComponent: React.FC = ({ } return result; - }, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups]); + }, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups, joinColumnMapping]); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); @@ -554,7 +561,6 @@ export const TableListComponent: React.FC = ({ const [tableLabel, setTableLabel] = useState(""); const [localPageSize, setLocalPageSize] = useState(tableConfig.pagination?.pageSize || 20); const [displayColumns, setDisplayColumns] = useState([]); - const [joinColumnMapping, setJoinColumnMapping] = useState>({}); const [columnMeta, setColumnMeta] = useState< Record >({}); @@ -1005,7 +1011,7 @@ export const TableListComponent: React.FC = ({ // unregisterTable ํ•จ์ˆ˜๋Š” ์˜์กด์„ฑ์ด ์—†์–ด ์•ˆ์ •์ ์ž„ ]); - // ๐ŸŽฏ ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ localStorage์—์„œ ์ •๋ ฌ ์ƒํƒœ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + // ๐ŸŽฏ ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ localStorage์—์„œ ์ •๋ ฌ ์ƒํƒœ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ (์—†์œผ๋ฉด defaultSort ์ ์šฉ) useEffect(() => { if (!tableConfig.selectedTable || !userId || hasInitializedSort.current) return; @@ -1019,12 +1025,20 @@ export const TableListComponent: React.FC = ({ setSortColumn(column); setSortDirection(direction); hasInitializedSort.current = true; + return; } } catch (error) { // ์ •๋ ฌ ์ƒํƒœ ๋ณต์› ์‹คํŒจ } } - }, [tableConfig.selectedTable, userId]); + + // localStorage์— ์ €์žฅ๋œ ์ •๋ ฌ์ด ์—†์œผ๋ฉด defaultSort ์„ค์ • ์ ์šฉ + if (tableConfig.defaultSort?.columnName) { + setSortColumn(tableConfig.defaultSort.columnName); + setSortDirection(tableConfig.defaultSort.direction || "asc"); + hasInitializedSort.current = true; + } + }, [tableConfig.selectedTable, tableConfig.defaultSort, userId]); // ๐Ÿ†• ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ localStorage์—์„œ ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ useEffect(() => { @@ -1465,8 +1479,9 @@ export const TableListComponent: React.FC = ({ try { const page = tableConfig.pagination?.currentPage || currentPage; const pageSize = localPageSize; - const sortBy = sortColumn || undefined; - const sortOrder = sortDirection; + // ๐Ÿ†• sortColumn์ด ์—†์œผ๋ฉด defaultSort ์„ค์ •์„ fallback์œผ๋กœ ์‚ฌ์šฉ + const sortBy = sortColumn || tableConfig.defaultSort?.columnName || undefined; + const sortOrder = sortColumn ? sortDirection : (tableConfig.defaultSort?.direction || sortDirection); const search = searchTerm || undefined; // ๐Ÿ†• ์—ฐ๊ฒฐ ํ•„ํ„ฐ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€์ผ ๋•Œ) @@ -4047,18 +4062,33 @@ export const TableListComponent: React.FC = ({ const inputType = meta?.inputType || column.inputType; // ๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€ ํƒ€์ž…: ์ž‘์€ ์ธ๋„ค์ผ ํ‘œ์‹œ - if (inputType === "image" && value && typeof value === "string") { - const imageUrl = getFullImageUrl(value); + if (inputType === "image" && value) { + // value๊ฐ€ objid (์ˆซ์ž ๋˜๋Š” ์ˆซ์ž ๋ฌธ์ž์—ด)์ธ ๊ฒฝ์šฐ ํŒŒ์ผ API URL ์‚ฌ์šฉ + // ๐Ÿ”‘ download ๋Œ€์‹  preview ์‚ฌ์šฉ (๊ณต๊ฐœ ์ ‘๊ทผ ํ—ˆ์šฉ) + const strValue = String(value); + const isObjid = /^\d+$/.test(strValue); + // ๐Ÿ”‘ ์ƒ๋Œ€ ๊ฒฝ๋กœ(/api/...) ๋Œ€์‹  ์ „์ฒด URL ์‚ฌ์šฉ (Docker ํ™˜๊ฒฝ์—์„œ Next.js rewrite ์˜์กด ๋ฐฉ์ง€) + const imageUrl = isObjid + ? getFilePreviewUrl(strValue) + : getFullImageUrl(strValue); return ( - ์ด๋ฏธ์ง€ { - e.currentTarget.src = - "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Crect width='40' height='40' fill='%23f3f4f6'/%3E%3C/svg%3E"; - }} - /> +
+ ์ด๋ฏธ์ง€ { + e.stopPropagation(); + // ์ด๋ฏธ์ง€ ํด๋ฆญ ์‹œ ์ƒˆ ํƒญ์—์„œ ํฌ๊ฒŒ ๋ณด๊ธฐ + window.open(imageUrl, "_blank"); + }} + onError={(e) => { + // ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ ์•„์ด์ฝ˜ ํ‘œ์‹œ + (e.target as HTMLImageElement).style.display = "none"; + }} + /> +
); } diff --git a/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx index ff76960e..8f14a250 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx @@ -10,7 +10,19 @@ import { TableListConfig, ColumnConfig } from "./types"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { tableTypeApi } from "@/lib/api/screen"; import { tableManagementApi } from "@/lib/api/tableManagement"; -import { Plus, Trash2, ArrowUp, ArrowDown, ChevronsUpDown, Check, Lock, Unlock, Database, Table2, Link2 } from "lucide-react"; +import { + Plus, + Trash2, + ArrowUp, + ArrowDown, + ChevronsUpDown, + Check, + Lock, + Unlock, + Database, + Table2, + Link2, +} from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { cn } from "@/lib/utils"; @@ -35,7 +47,7 @@ export const TableListConfigPanel: React.FC = ({ }) => { // config๊ฐ€ undefined์ธ ๊ฒฝ์šฐ ๋นˆ ๊ฐ์ฒด๋กœ ์ดˆ๊ธฐํ™” const config = configProp || {}; - + // console.log("๐Ÿ” TableListConfigPanel props:", { // config, // configType: typeof config, @@ -202,12 +214,12 @@ export const TableListConfigPanel: React.FC = ({ try { const result = await tableManagementApi.getColumnList(targetTableName); console.log("๐Ÿ”ง tableManagementApi ์‘๋‹ต:", result); - - if (result.success && result.data) { + + if (result.success && result.data) { // API ์‘๋‹ต ๊ตฌ์กฐ: { columns: [...], total, page, ... } const columns = Array.isArray(result.data) ? result.data : result.data.columns; console.log("๐Ÿ”ง ์ปฌ๋Ÿผ ๋ฐฐ์—ด:", columns); - + if (columns && Array.isArray(columns)) { setAvailableColumns( columns.map((col: any) => ({ @@ -319,7 +331,9 @@ export const TableListConfigPanel: React.FC = ({ const handleChange = (key: keyof TableListConfig, value: any) => { // ๊ธฐ์กด config์™€ ๋ณ‘ํ•ฉํ•˜์—ฌ ์ „๋‹ฌ (๋‹ค๋ฅธ ์†์„ฑ ์†์‹ค ๋ฐฉ์ง€) - onChange({ ...config, [key]: value }); + const newConfig = { ...config, [key]: value }; + console.log("๐Ÿ“Š TableListConfigPanel handleChange:", { key, value, newConfig }); + onChange(newConfig); }; const handleNestedChange = (parentKey: keyof TableListConfig, childKey: string, value: any) => { @@ -777,7 +791,9 @@ export const TableListConfigPanel: React.FC = ({ checked={config.toolbar?.showEditMode ?? false} onCheckedChange={(checked) => handleNestedChange("toolbar", "showEditMode", checked)} /> - +
= ({ checked={config.toolbar?.showExcel ?? false} onCheckedChange={(checked) => handleNestedChange("toolbar", "showExcel", checked)} /> - +
= ({ checked={config.toolbar?.showPdf ?? false} onCheckedChange={(checked) => handleNestedChange("toolbar", "showPdf", checked)} /> - +
= ({ checked={config.toolbar?.showCopy ?? false} onCheckedChange={(checked) => handleNestedChange("toolbar", "showCopy", checked)} /> - +
= ({ checked={config.toolbar?.showSearch ?? false} onCheckedChange={(checked) => handleNestedChange("toolbar", "showSearch", checked)} /> - +
= ({ checked={config.toolbar?.showFilter ?? false} onCheckedChange={(checked) => handleNestedChange("toolbar", "showFilter", checked)} /> - +
= ({ checked={config.toolbar?.showRefresh ?? false} onCheckedChange={(checked) => handleNestedChange("toolbar", "showRefresh", checked)} /> - +
= ({ checked={config.toolbar?.showPaginationRefresh ?? true} onCheckedChange={(checked) => handleNestedChange("toolbar", "showPaginationRefresh", checked)} /> - +
@@ -884,6 +914,67 @@ export const TableListConfigPanel: React.FC = ({ + {/* ๊ธฐ๋ณธ ์ •๋ ฌ ์„ค์ • */} +
+
+

๊ธฐ๋ณธ ์ •๋ ฌ ์„ค์ •

+

ํ…Œ์ด๋ธ” ๋กœ๋“œ ์‹œ ๊ธฐ๋ณธ ์ •๋ ฌ ์ˆœ์„œ๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค

+
+
+
+
+ + +
+ + {config.defaultSort?.columnName && ( +
+ + +
+ )} +
+
+ {/* ๊ฐ€๋กœ ์Šคํฌ๋กค ๋ฐ ์ปฌ๋Ÿผ ๊ณ ์ • */}
@@ -1096,7 +1187,7 @@ export const TableListConfigPanel: React.FC = ({

์ปฌ๋Ÿผ ์„ ํƒ

-

ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์„ธ์š”

+

ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์„ธ์š”


{availableColumns.length > 0 ? ( @@ -1113,7 +1204,10 @@ export const TableListConfigPanel: React.FC = ({ onClick={() => { if (isAdded) { // ์ปฌ๋Ÿผ ์ œ๊ฑฐ - handleChange("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []); + handleChange( + "columns", + config.columns?.filter((c) => c.columnName !== column.columnName) || [], + ); } else { // ์ปฌ๋Ÿผ ์ถ”๊ฐ€ addColumn(column.columnName); @@ -1124,7 +1218,10 @@ export const TableListConfigPanel: React.FC = ({ checked={isAdded} onCheckedChange={() => { if (isAdded) { - handleChange("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []); + handleChange( + "columns", + config.columns?.filter((c) => c.columnName !== column.columnName) || [], + ); } else { addColumn(column.columnName); } @@ -1133,7 +1230,9 @@ export const TableListConfigPanel: React.FC = ({ /> {column.label || column.columnName} - {column.input_type || column.dataType} + + {column.input_type || column.dataType} +
); })} @@ -1148,13 +1247,13 @@ export const TableListConfigPanel: React.FC = ({

Entity ์กฐ์ธ ์ปฌ๋Ÿผ

-

์—ฐ๊ด€ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์„ธ์š”

+

์—ฐ๊ด€ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์„ธ์š”


{entityJoinColumns.joinTables.map((joinTable, tableIndex) => (
-
+
{joinTable.tableName} @@ -1162,56 +1261,65 @@ export const TableListConfigPanel: React.FC = ({
- {joinTable.availableColumns.map((column, colIndex) => { - const matchingJoinColumn = entityJoinColumns.availableColumns.find( - (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, - ); + {joinTable.availableColumns.map((column, colIndex) => { + const matchingJoinColumn = entityJoinColumns.availableColumns.find( + (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, + ); - const isAlreadyAdded = config.columns?.some( - (col) => col.columnName === matchingJoinColumn?.joinAlias, - ); + const isAlreadyAdded = config.columns?.some( + (col) => col.columnName === matchingJoinColumn?.joinAlias, + ); if (!matchingJoinColumn) return null; - return ( -
{ + onClick={() => { if (isAlreadyAdded) { // ์ปฌ๋Ÿผ ์ œ๊ฑฐ - handleChange("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []); + handleChange( + "columns", + config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || [], + ); } else { // ์ปฌ๋Ÿผ ์ถ”๊ฐ€ addEntityColumn(matchingJoinColumn); } }} > - { if (isAlreadyAdded) { - handleChange("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []); - } else { + handleChange( + "columns", + config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || + [], + ); + } else { addEntityColumn(matchingJoinColumn); - } - }} + } + }} className="pointer-events-none h-3.5 w-3.5" /> - + {column.columnLabel} - {column.inputType || column.dataType} -
- ); - })} -
-
+ + {column.inputType || column.dataType} + +
+ ); + })} +
+
))} -
-
+ + )} )} @@ -1238,7 +1346,6 @@ export const TableListConfigPanel: React.FC = ({ onConfigChange={(dataFilter) => handleChange("dataFilter", dataFilter)} /> - ); diff --git a/frontend/lib/registry/components/v2-table-list/types.ts b/frontend/lib/registry/components/v2-table-list/types.ts index a43ccdfa..1cc04375 100644 --- a/frontend/lib/registry/components/v2-table-list/types.ts +++ b/frontend/lib/registry/components/v2-table-list/types.ts @@ -278,6 +278,12 @@ export interface TableListConfig extends ComponentConfig { autoLoad: boolean; refreshInterval?: number; // ์ดˆ ๋‹จ์œ„ + // ๐Ÿ†• ๊ธฐ๋ณธ ์ •๋ ฌ ์„ค์ • + defaultSort?: { + columnName: string; // ์ •๋ ฌํ•  ์ปฌ๋Ÿผ๋ช… + direction: "asc" | "desc"; // ์ •๋ ฌ ๋ฐฉํ–ฅ + }; + // ๐Ÿ†• ํˆด๋ฐ” ๋ฒ„ํŠผ ํ‘œ์‹œ ์„ค์ • toolbar?: ToolbarConfig; diff --git a/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx b/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx index a3bde9a4..94d0c742 100644 --- a/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx +++ b/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx @@ -475,9 +475,21 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table filterValue = filterValue.join("|"); } + // ๐Ÿ”ง filterType์— ๋”ฐ๋ผ operator ์„ค์ • + // - "select" ์œ ํ˜•: ์ •ํ™•ํžˆ ์ผ์น˜ (equals) + // - "text" ์œ ํ˜•: ๋ถ€๋ถ„ ์ผ์น˜ (contains) + // - "date", "number": ๊ฐ๊ฐ ์ ์ ˆํ•œ ์ฒ˜๋ฆฌ + let operator = "contains"; // ๊ธฐ๋ณธ๊ฐ’ + if (filter.filterType === "select") { + operator = "equals"; // ์„ ํƒ ํ•„ํ„ฐ๋Š” ์ •ํ™•ํžˆ ์ผ์น˜ + } else if (filter.filterType === "number") { + operator = "equals"; // ์ˆซ์ž๋„ ์ •ํ™•ํžˆ ์ผ์น˜ + } + return { ...filter, value: filterValue || "", + operator, // operator ์ถ”๊ฐ€ }; }) .filter((f) => { diff --git a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx index 91965aa8..8645fed9 100644 --- a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx +++ b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx @@ -365,6 +365,7 @@ const TabsDesignEditor: React.FC<{ }} onClick={(e) => { e.stopPropagation(); + console.log("๐Ÿ” [ํƒญ ์ปดํฌ๋„ŒํŠธ] ํด๋ฆญ:", { activeTabId, compId: comp.id, hasOnSelectTabComponent: !!onSelectTabComponent }); onSelectTabComponent?.(activeTabId, comp.id, comp); }} > diff --git a/frontend/lib/registry/components/v2-text-display/TextDisplayComponent.tsx b/frontend/lib/registry/components/v2-text-display/TextDisplayComponent.tsx index 07ce3f34..78157d3e 100644 --- a/frontend/lib/registry/components/v2-text-display/TextDisplayComponent.tsx +++ b/frontend/lib/registry/components/v2-text-display/TextDisplayComponent.tsx @@ -84,23 +84,7 @@ export const TextDisplayComponent: React.FC = ({ return (
- {/* ๋ผ๋ฒจ ๋ Œ๋”๋ง */} - {component.label && (component.style?.labelDisplay ?? true) && ( - - )} - + {/* v2-text-display๋Š” ํ…์ŠคํŠธ ํ‘œ์‹œ ์ „์šฉ์ด๋ฏ€๋กœ ๋ณ„๋„ ๋ผ๋ฒจ ๋ถˆํ•„์š” */}
{componentConfig.text || "ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"}
diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/README.md b/frontend/lib/registry/components/v2-timeline-scheduler/README.md new file mode 100644 index 00000000..2e8d7262 --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/README.md @@ -0,0 +1,159 @@ +# v2-timeline-scheduler + +๊ฐ„ํŠธ์ฐจํŠธ ํ˜•ํƒœ์˜ ์ผ์ •/๊ณ„ํš ์‹œ๊ฐํ™” ๋ฐ ํŽธ์ง‘ ์ปดํฌ๋„ŒํŠธ + +## ๊ฐœ์š” + +`v2-timeline-scheduler`๋Š” ์ƒ์‚ฐ๊ณ„ํš, ์ผ์ •๊ด€๋ฆฌ ๋“ฑ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํƒ€์ž„๋ผ์ธ ๊ธฐ๋ฐ˜์˜ ์Šค์ผ€์ค„๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค. ๋ฆฌ์†Œ์Šค(์„ค๋น„, ์ž‘์—…์ž ๋“ฑ)๋ณ„๋กœ ์Šค์ผ€์ค„์„ ์‹œ๊ฐํ™”ํ•˜๊ณ , ๋“œ๋ž˜๊ทธ/๋ฆฌ์‚ฌ์ด์ฆˆ๋กœ ์ผ์ •์„ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +## ํ•ต์‹ฌ ๊ธฐ๋Šฅ + +| ๊ธฐ๋Šฅ | ์„ค๋ช… | +|------|------| +| ํƒ€์ž„๋ผ์ธ ๊ทธ๋ฆฌ๋“œ | ์ผ/์ฃผ/์›” ๋‹จ์œ„ ๊ทธ๋ฆฌ๋“œ ํ‘œ์‹œ | +| ์Šค์ผ€์ค„ ๋ฐ” | ์‹œ์ž‘~์ข…๋ฃŒ ๊ธฐ๊ฐ„ ๋ฐ” ๋ Œ๋”๋ง | +| ๋ฆฌ์†Œ์Šค ํ–‰ | ์„ค๋น„/์ž‘์—…์ž๋ณ„ ํ–‰ ๊ตฌ๋ถ„ | +| ๋“œ๋ž˜๊ทธ ์ด๋™ | ์Šค์ผ€์ค„ ๋ฐ” ๋“œ๋ž˜๊ทธ๋กœ ๋‚ ์งœ ๋ณ€๊ฒฝ | +| ๋ฆฌ์‚ฌ์ด์ฆˆ | ๋ฐ” ์–‘์ชฝ ํ•ธ๋“ค๋กœ ๊ธฐ๊ฐ„ ์กฐ์ • | +| ์คŒ ๋ ˆ๋ฒจ | ์ผ/์ฃผ/์›” ๋‹จ์œ„ ์ „ํ™˜ | +| ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ | ๋ฐ” ๋‚ด๋ถ€ ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ | +| ์˜ค๋Š˜ ํ‘œ์‹œ์„  | ํ˜„์žฌ ๋‚ ์งœ ํ‘œ์‹œ์„  | + +## ์‚ฌ์šฉ๋ฒ• + +### ๊ธฐ๋ณธ ์‚ฌ์šฉ + +```tsx +import { TimelineSchedulerComponent } from "@/lib/registry/components/v2-timeline-scheduler"; + + { + console.log("ํด๋ฆญ๋œ ์Šค์ผ€์ค„:", event.schedule); + }} + onDragEnd={(event) => { + console.log("๋“œ๋ž˜๊ทธ ์™„๋ฃŒ:", event); + }} +/> +``` + +### ์„ค์ • ์˜ต์…˜ + +| ์˜ต์…˜ | ํƒ€์ž… | ๊ธฐ๋ณธ๊ฐ’ | ์„ค๋ช… | +|------|------|--------|------| +| `selectedTable` | string | - | ์Šค์ผ€์ค„ ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ”๋ช… | +| `resourceTable` | string | - | ๋ฆฌ์†Œ์Šค ํ…Œ์ด๋ธ”๋ช… | +| `fieldMapping` | object | - | ์Šค์ผ€์ค„ ํ•„๋“œ ๋งคํ•‘ | +| `resourceFieldMapping` | object | - | ๋ฆฌ์†Œ์Šค ํ•„๋“œ ๋งคํ•‘ | +| `defaultZoomLevel` | "day" \| "week" \| "month" | "day" | ๊ธฐ๋ณธ ์คŒ ๋ ˆ๋ฒจ | +| `editable` | boolean | true | ํŽธ์ง‘ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ | +| `draggable` | boolean | true | ๋“œ๋ž˜๊ทธ ์ด๋™ ๊ฐ€๋Šฅ | +| `resizable` | boolean | true | ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ฐ€๋Šฅ | +| `rowHeight` | number | 50 | ํ–‰ ๋†’์ด (px) | +| `headerHeight` | number | 60 | ํ—ค๋” ๋†’์ด (px) | +| `resourceColumnWidth` | number | 150 | ๋ฆฌ์†Œ์Šค ์ปฌ๋Ÿผ ๋„ˆ๋น„ (px) | +| `showTodayLine` | boolean | true | ์˜ค๋Š˜ ํ‘œ์‹œ์„  | +| `showProgress` | boolean | true | ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ | +| `showToolbar` | boolean | true | ํˆด๋ฐ” ํ‘œ์‹œ | +| `height` | number \| string | 500 | ์ปดํฌ๋„ŒํŠธ ๋†’์ด | + +### ํ•„๋“œ ๋งคํ•‘ + +์Šค์ผ€์ค„ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ์„ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค: + +```typescript +fieldMapping: { + id: "id", // ํ•„์ˆ˜: ๊ณ ์œ  ID + resourceId: "equipment_id", // ํ•„์ˆ˜: ๋ฆฌ์†Œ์Šค ID (FK) + title: "plan_name", // ํ•„์ˆ˜: ํ‘œ์‹œ ์ œ๋ชฉ + startDate: "start_date", // ํ•„์ˆ˜: ์‹œ์ž‘์ผ + endDate: "end_date", // ํ•„์ˆ˜: ์ข…๋ฃŒ์ผ + status: "status", // ์„ ํƒ: ์ƒํƒœ + progress: "progress", // ์„ ํƒ: ์ง„ํ–‰๋ฅ  (0-100) + color: "color", // ์„ ํƒ: ๋ฐ” ์ƒ‰์ƒ +} +``` + +### ์ด๋ฒคํŠธ + +| ์ด๋ฒคํŠธ | ํŒŒ๋ผ๋ฏธํ„ฐ | ์„ค๋ช… | +|--------|----------|------| +| `onScheduleClick` | `{ schedule, resource }` | ์Šค์ผ€์ค„ ํด๋ฆญ | +| `onCellClick` | `{ resourceId, date }` | ๋นˆ ์…€ ํด๋ฆญ | +| `onDragEnd` | `{ scheduleId, newStartDate, newEndDate }` | ๋“œ๋ž˜๊ทธ ์™„๋ฃŒ | +| `onResizeEnd` | `{ scheduleId, newStartDate, newEndDate, direction }` | ๋ฆฌ์‚ฌ์ด์ฆˆ ์™„๋ฃŒ | +| `onAddSchedule` | `(resourceId, date)` | ์ถ”๊ฐ€ ๋ฒ„ํŠผ ํด๋ฆญ | + +### ์ƒํƒœ๋ณ„ ์ƒ‰์ƒ + +๊ธฐ๋ณธ ์ƒํƒœ๋ณ„ ์ƒ‰์ƒ: + +| ์ƒํƒœ | ์ƒ‰์ƒ | ์˜๋ฏธ | +|------|------|------| +| `planned` | ํŒŒ๋ž‘ (#3b82f6) | ๊ณ„ํš๋จ | +| `in_progress` | ์ฃผํ™ฉ (#f59e0b) | ์ง„ํ–‰์ค‘ | +| `completed` | ์ดˆ๋ก (#10b981) | ์™„๋ฃŒ | +| `delayed` | ๋นจ๊ฐ• (#ef4444) | ์ง€์—ฐ | +| `cancelled` | ํšŒ์ƒ‰ (#6b7280) | ์ทจ์†Œ | + +## ํŒŒ์ผ ๊ตฌ์กฐ + +``` +v2-timeline-scheduler/ +โ”œโ”€โ”€ index.ts # Definition +โ”œโ”€โ”€ types.ts # ํƒ€์ž… ์ •์˜ +โ”œโ”€โ”€ config.ts # ๊ธฐ๋ณธ ์„ค์ •๊ฐ’ +โ”œโ”€โ”€ TimelineSchedulerComponent.tsx # ๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ +โ”œโ”€โ”€ TimelineSchedulerConfigPanel.tsx # ์„ค์ • ํŒจ๋„ +โ”œโ”€โ”€ TimelineSchedulerRenderer.tsx # ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋“ฑ๋ก +โ”œโ”€โ”€ README.md # ๋ฌธ์„œ +โ”œโ”€โ”€ components/ +โ”‚ โ”œโ”€โ”€ index.ts +โ”‚ โ”œโ”€โ”€ TimelineHeader.tsx # ๋‚ ์งœ ํ—ค๋” +โ”‚ โ”œโ”€โ”€ ScheduleBar.tsx # ์Šค์ผ€์ค„ ๋ฐ” +โ”‚ โ””โ”€โ”€ ResourceRow.tsx # ๋ฆฌ์†Œ์Šค ํ–‰ +โ””โ”€โ”€ hooks/ + โ””โ”€โ”€ useTimelineData.ts # ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ํ›… +``` + +## v2-table-list์™€์˜ ์ฐจ์ด์  + +| ํŠน์„ฑ | v2-table-list | v2-timeline-scheduler | +|------|---------------|----------------------| +| ํ‘œํ˜„ ๋ฐฉ์‹ | ํ–‰ ๊ธฐ๋ฐ˜ ํ…Œ์ด๋ธ” | ์‹œ๊ฐ„์ถ• ๊ธฐ๋ฐ˜ ๊ฐ„ํŠธ์ฐจํŠธ | +| ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ | ๋‹จ์ˆœ ๋ชฉ๋ก | ๋ฆฌ์†Œ์Šค + ์Šค์ผ€์ค„ (2๊ฐœ ํ…Œ์ด๋ธ”) | +| ํŽธ์ง‘ ๋ฐฉ์‹ | ํผ ์ž…๋ ฅ | ๋“œ๋ž˜๊ทธ/๋ฆฌ์‚ฌ์ด์ฆˆ | +| ์‹œ๊ฐ„ ํ‘œํ˜„ | ํ…์ŠคํŠธ | ์‹œ๊ฐ์  ๋ฐ” | +| ์šฉ๋„ | ์ผ๋ฐ˜ ๋ฐ์ดํ„ฐ | ์ผ์ •/๊ณ„ํš ๊ด€๋ฆฌ | + +## ํ–ฅํ›„ ๊ฐœ์„  ์‚ฌํ•ญ + +- [ ] ์ถฉ๋Œ ๊ฐ์ง€ ๋ฐ ํ‘œ์‹œ +- [ ] ๊ฐ€์ƒ ์Šคํฌ๋กค (๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ) +- [ ] ๋งˆ์ผ์Šคํ†ค ํ‘œ์‹œ +- [ ] ์˜์กด์„ฑ ์—ฐ๊ฒฐ์„  +- [ ] ๋“œ๋ž˜๊ทธ๋กœ ์ƒˆ ์Šค์ผ€์ค„ ์ƒ์„ฑ +- [ ] ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด + +--- + +**๋ฒ„์ „**: 2.0.0 +**์ตœ์ข… ์ˆ˜์ •**: 2026-01-30 diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx new file mode 100644 index 00000000..f6fbaea2 --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx @@ -0,0 +1,436 @@ +"use client"; + +import React, { useCallback, useMemo, useRef, useState } from "react"; +import { + ChevronLeft, + ChevronRight, + Calendar, + Plus, + Loader2, + ZoomIn, + ZoomOut, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { + TimelineSchedulerComponentProps, + ScheduleItem, + ZoomLevel, + DragEvent, + ResizeEvent, +} from "./types"; +import { useTimelineData } from "./hooks/useTimelineData"; +import { TimelineHeader, ResourceRow } from "./components"; +import { zoomLevelOptions, defaultTimelineSchedulerConfig } from "./config"; + +/** + * v2-timeline-scheduler ๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ + * + * ๊ฐ„ํŠธ์ฐจํŠธ ํ˜•ํƒœ์˜ ์ผ์ •/๊ณ„ํš ์‹œ๊ฐํ™” ๋ฐ ํŽธ์ง‘ ์ปดํฌ๋„ŒํŠธ + */ +export function TimelineSchedulerComponent({ + config, + isDesignMode = false, + formData, + externalSchedules, + externalResources, + isLoading: externalLoading, + error: externalError, + componentId, + onDragEnd, + onResizeEnd, + onScheduleClick, + onCellClick, + onAddSchedule, +}: TimelineSchedulerComponentProps) { + const containerRef = useRef(null); + + // ๋“œ๋ž˜๊ทธ/๋ฆฌ์‚ฌ์ด์ฆˆ ์ƒํƒœ + const [dragState, setDragState] = useState<{ + schedule: ScheduleItem; + startX: number; + startY: number; + } | null>(null); + + const [resizeState, setResizeState] = useState<{ + schedule: ScheduleItem; + direction: "start" | "end"; + startX: number; + } | null>(null); + + // ํƒ€์ž„๋ผ์ธ ๋ฐ์ดํ„ฐ ํ›… + const { + schedules, + resources, + isLoading: hookLoading, + error: hookError, + zoomLevel, + setZoomLevel, + viewStartDate, + viewEndDate, + goToPrevious, + goToNext, + goToToday, + updateSchedule, + } = useTimelineData(config, externalSchedules, externalResources); + + const isLoading = externalLoading ?? hookLoading; + const error = externalError ?? hookError; + + // ์„ค์ •๊ฐ’ + const rowHeight = config.rowHeight || defaultTimelineSchedulerConfig.rowHeight!; + const headerHeight = config.headerHeight || defaultTimelineSchedulerConfig.headerHeight!; + const resourceColumnWidth = + config.resourceColumnWidth || defaultTimelineSchedulerConfig.resourceColumnWidth!; + const cellWidthConfig = config.cellWidth || defaultTimelineSchedulerConfig.cellWidth!; + const cellWidth = cellWidthConfig[zoomLevel] || 60; + + // ๋ฆฌ์†Œ์Šค๊ฐ€ ์—†์œผ๋ฉด ์Šค์ผ€์ค„์˜ resourceId๋กœ ์ž๋™ ์ƒ์„ฑ + const effectiveResources = useMemo(() => { + if (resources.length > 0) { + return resources; + } + + // ์Šค์ผ€์ค„์—์„œ ๊ณ ์œ ํ•œ resourceId ์ถ”์ถœํ•˜์—ฌ ์ž๋™ ๋ฆฌ์†Œ์Šค ์ƒ์„ฑ + const uniqueResourceIds = new Set(); + schedules.forEach((schedule) => { + if (schedule.resourceId) { + uniqueResourceIds.add(schedule.resourceId); + } + }); + + return Array.from(uniqueResourceIds).map((id) => ({ + id, + name: id, // resourceId๋ฅผ ์ด๋ฆ„์œผ๋กœ ์‚ฌ์šฉ + })); + }, [resources, schedules]); + + // ๋ฆฌ์†Œ์Šค๋ณ„ ์Šค์ผ€์ค„ ๊ทธ๋ฃนํ™” + const schedulesByResource = useMemo(() => { + const grouped = new Map(); + + effectiveResources.forEach((resource) => { + grouped.set(resource.id, []); + }); + + schedules.forEach((schedule) => { + const list = grouped.get(schedule.resourceId); + if (list) { + list.push(schedule); + } else { + // ๋ฆฌ์†Œ์Šค๊ฐ€ ์—†๋Š” ์Šค์ผ€์ค„์€ ์ฒซ ๋ฒˆ์งธ ๋ฆฌ์†Œ์Šค์— ํ• ๋‹น + const firstResource = effectiveResources[0]; + if (firstResource) { + const firstList = grouped.get(firstResource.id); + if (firstList) { + firstList.push(schedule); + } + } + } + }); + + return grouped; + }, [schedules, effectiveResources]); + + // ์คŒ ๋ ˆ๋ฒจ ๋ณ€๊ฒฝ + const handleZoomIn = useCallback(() => { + const levels: ZoomLevel[] = ["month", "week", "day"]; + const currentIdx = levels.indexOf(zoomLevel); + if (currentIdx < levels.length - 1) { + setZoomLevel(levels[currentIdx + 1]); + } + }, [zoomLevel, setZoomLevel]); + + const handleZoomOut = useCallback(() => { + const levels: ZoomLevel[] = ["month", "week", "day"]; + const currentIdx = levels.indexOf(zoomLevel); + if (currentIdx > 0) { + setZoomLevel(levels[currentIdx - 1]); + } + }, [zoomLevel, setZoomLevel]); + + // ์Šค์ผ€์ค„ ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ + const handleScheduleClick = useCallback( + (schedule: ScheduleItem) => { + const resource = effectiveResources.find((r) => r.id === schedule.resourceId); + if (resource && onScheduleClick) { + onScheduleClick({ schedule, resource }); + } + }, + [effectiveResources, onScheduleClick] + ); + + // ๋นˆ ์…€ ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ + const handleCellClick = useCallback( + (resourceId: string, date: Date) => { + if (onCellClick) { + onCellClick({ + resourceId, + date: date.toISOString().split("T")[0], + }); + } + }, + [onCellClick] + ); + + // ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ + const handleDragStart = useCallback( + (schedule: ScheduleItem, e: React.MouseEvent) => { + setDragState({ + schedule, + startX: e.clientX, + startY: e.clientY, + }); + }, + [] + ); + + // ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ + const handleDragEnd = useCallback(() => { + if (dragState) { + // TODO: ๋“œ๋ž˜๊ทธ ๊ฒฐ๊ณผ ๊ณ„์‚ฐ ๋ฐ ์—…๋ฐ์ดํŠธ + setDragState(null); + } + }, [dragState]); + + // ๋ฆฌ์‚ฌ์ด์ฆˆ ์‹œ์ž‘ + const handleResizeStart = useCallback( + (schedule: ScheduleItem, direction: "start" | "end", e: React.MouseEvent) => { + setResizeState({ + schedule, + direction, + startX: e.clientX, + }); + }, + [] + ); + + // ๋ฆฌ์‚ฌ์ด์ฆˆ ์ข…๋ฃŒ + const handleResizeEnd = useCallback(() => { + if (resizeState) { + // TODO: ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ฒฐ๊ณผ ๊ณ„์‚ฐ ๋ฐ ์—…๋ฐ์ดํŠธ + setResizeState(null); + } + }, [resizeState]); + + // ์ถ”๊ฐ€ ๋ฒ„ํŠผ ํด๋ฆญ + const handleAddClick = useCallback(() => { + if (onAddSchedule && effectiveResources.length > 0) { + onAddSchedule( + effectiveResources[0].id, + new Date().toISOString().split("T")[0] + ); + } + }, [onAddSchedule, effectiveResources]); + + // ๋””์ž์ธ ๋ชจ๋“œ ํ”Œ๋ ˆ์ด์Šคํ™€๋” + if (isDesignMode) { + return ( +
+
+ +

ํƒ€์ž„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ

+

+ {config.selectedTable + ? `ํ…Œ์ด๋ธ”: ${config.selectedTable}` + : "ํ…Œ์ด๋ธ”์„ ์„ ํƒํ•˜์„ธ์š”"} +

+
+
+ ); + } + + // ๋กœ๋”ฉ ์ƒํƒœ + if (isLoading) { + return ( +
+
+ + ๋กœ๋”ฉ ์ค‘... +
+
+ ); + } + + // ์—๋Ÿฌ ์ƒํƒœ + if (error) { + return ( +
+
+

์˜ค๋ฅ˜ ๋ฐœ์ƒ

+

{error}

+
+
+ ); + } + + // ์Šค์ผ€์ค„ ๋ฐ์ดํ„ฐ ์—†์Œ + if (schedules.length === 0) { + return ( +
+
+ +

์Šค์ผ€์ค„ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค

+

+ ์ขŒ์ธก ํ…Œ์ด๋ธ”์—์„œ ํ’ˆ๋ชฉ์„ ์„ ํƒํ•˜๊ฑฐ๋‚˜,
+ ์Šค์ผ€์ค„ ์ƒ์„ฑ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์Šค์ผ€์ค„์„ ์ƒ์„ฑํ•˜์„ธ์š” +

+
+
+ ); + } + + return ( +
+ {/* ํˆด๋ฐ” */} + {config.showToolbar !== false && ( +
+ {/* ๋„ค๋น„๊ฒŒ์ด์…˜ */} +
+ {config.showNavigation !== false && ( + <> + + + + + )} + + {/* ํ˜„์žฌ ๋‚ ์งœ ๋ฒ”์œ„ ํ‘œ์‹œ */} + + {viewStartDate.getFullYear()}๋…„ {viewStartDate.getMonth() + 1}์›”{" "} + {viewStartDate.getDate()}์ผ ~{" "} + {viewEndDate.getMonth() + 1}์›” {viewEndDate.getDate()}์ผ + +
+ + {/* ์˜ค๋ฅธ์ชฝ ์ปจํŠธ๋กค */} +
+ {/* ์คŒ ์ปจํŠธ๋กค */} + {config.showZoomControls !== false && ( +
+ + + {zoomLevelOptions.find((o) => o.value === zoomLevel)?.label} + + +
+ )} + + {/* ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} + {config.showAddButton !== false && config.editable && ( + + )} +
+
+ )} + + {/* ํƒ€์ž„๋ผ์ธ ๋ณธ๋ฌธ */} +
+
+ {/* ํ—ค๋” */} + + + {/* ๋ฆฌ์†Œ์Šค ํ–‰๋“ค */} +
+ {effectiveResources.map((resource) => ( + + ))} +
+
+
+
+ ); +} diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerConfigPanel.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerConfigPanel.tsx new file mode 100644 index 00000000..a715e408 --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerConfigPanel.tsx @@ -0,0 +1,536 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { Check, ChevronsUpDown, Loader2 } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { tableTypeApi } from "@/lib/api/screen"; +import { TimelineSchedulerConfig, ScheduleType, SourceDataConfig } from "./types"; +import { zoomLevelOptions, scheduleTypeOptions } from "./config"; + +interface TimelineSchedulerConfigPanelProps { + config: TimelineSchedulerConfig; + onChange: (config: Partial) => void; +} + +interface TableInfo { + tableName: string; + displayName: string; +} + +interface ColumnInfo { + columnName: string; + displayName: string; +} + +export function TimelineSchedulerConfigPanel({ config, onChange }: TimelineSchedulerConfigPanelProps) { + const [tables, setTables] = useState([]); + const [sourceColumns, setSourceColumns] = useState([]); + const [resourceColumns, setResourceColumns] = useState([]); + const [loading, setLoading] = useState(false); + const [sourceTableSelectOpen, setSourceTableSelectOpen] = useState(false); + const [resourceTableSelectOpen, setResourceTableSelectOpen] = useState(false); + + // ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ + useEffect(() => { + const loadTables = async () => { + setLoading(true); + try { + const tableList = await tableTypeApi.getTables(); + if (Array.isArray(tableList)) { + setTables( + tableList.map((t: any) => ({ + tableName: t.table_name || t.tableName, + displayName: t.display_name || t.displayName || t.table_name || t.tableName, + })), + ); + } + } catch (err) { + console.error("ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ ์˜ค๋ฅ˜:", err); + } finally { + setLoading(false); + } + }; + loadTables(); + }, []); + + // ์†Œ์Šค ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ + useEffect(() => { + const loadSourceColumns = async () => { + if (!config.sourceConfig?.tableName) { + setSourceColumns([]); + return; + } + try { + const columns = await tableTypeApi.getColumns(config.sourceConfig.tableName); + if (Array.isArray(columns)) { + setSourceColumns( + columns.map((col: any) => ({ + columnName: col.column_name || col.columnName, + displayName: col.display_name || col.displayName || col.column_name || col.columnName, + })), + ); + } + } catch (err) { + console.error("์†Œ์Šค ์ปฌ๋Ÿผ ๋กœ๋“œ ์˜ค๋ฅ˜:", err); + setSourceColumns([]); + } + }; + loadSourceColumns(); + }, [config.sourceConfig?.tableName]); + + // ๋ฆฌ์†Œ์Šค ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ + useEffect(() => { + const loadResourceColumns = async () => { + if (!config.resourceTable) { + setResourceColumns([]); + return; + } + try { + const columns = await tableTypeApi.getColumns(config.resourceTable); + if (Array.isArray(columns)) { + setResourceColumns( + columns.map((col: any) => ({ + columnName: col.column_name || col.columnName, + displayName: col.display_name || col.displayName || col.column_name || col.columnName, + })), + ); + } + } catch (err) { + console.error("๋ฆฌ์†Œ์Šค ์ปฌ๋Ÿผ ๋กœ๋“œ ์˜ค๋ฅ˜:", err); + setResourceColumns([]); + } + }; + loadResourceColumns(); + }, [config.resourceTable]); + + // ์„ค์ • ์—…๋ฐ์ดํŠธ ํ—ฌํผ + const updateConfig = (updates: Partial) => { + onChange({ ...config, ...updates }); + }; + + // ์†Œ์Šค ๋ฐ์ดํ„ฐ ์„ค์ • ์—…๋ฐ์ดํŠธ + const updateSourceConfig = (updates: Partial) => { + updateConfig({ + sourceConfig: { + ...config.sourceConfig, + ...updates, + }, + }); + }; + + // ๋ฆฌ์†Œ์Šค ํ•„๋“œ ๋งคํ•‘ ์—…๋ฐ์ดํŠธ + const updateResourceFieldMapping = (field: string, value: string) => { + updateConfig({ + resourceFieldMapping: { + ...config.resourceFieldMapping, + id: config.resourceFieldMapping?.id || "id", + name: config.resourceFieldMapping?.name || "name", + [field]: value, + }, + }); + }; + + return ( +
+ + {/* ์†Œ์Šค ๋ฐ์ดํ„ฐ ์„ค์ • (์Šค์ผ€์ค„ ์ƒ์„ฑ ๊ธฐ์ค€) */} + + ์Šค์ผ€์ค„ ์ƒ์„ฑ ์„ค์ • + +

+ ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ ์‹œ ์ฐธ์กฐํ•  ์›๋ณธ ๋ฐ์ดํ„ฐ ์„ค์ • (์ €์žฅ: schedule_mng) +

+ + {/* ์Šค์ผ€์ค„ ํƒ€์ž… */} +
+ + +
+ + {/* ์†Œ์Šค ํ…Œ์ด๋ธ” ์„ ํƒ */} +
+ + + + + + + { + const lowerSearch = search.toLowerCase(); + if (value.toLowerCase().includes(lowerSearch)) { + return 1; + } + return 0; + }} + > + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {tables.map((table) => ( + { + updateSourceConfig({ tableName: table.tableName }); + setSourceTableSelectOpen(false); + }} + className="text-xs" + > + +
+ {table.displayName} + {table.tableName} +
+
+ ))} +
+
+
+
+
+
+ + {/* ์†Œ์Šค ํ•„๋“œ ๋งคํ•‘ */} + {config.sourceConfig?.tableName && ( +
+ +
+ {/* ๊ธฐ์ค€์ผ ํ•„๋“œ */} +
+ + +

์Šค์ผ€์ค„ ์ข…๋ฃŒ์ผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค

+
+ + {/* ์ˆ˜๋Ÿ‰ ํ•„๋“œ */} +
+ + +
+ + {/* ๊ทธ๋ฃนํ™” ํ•„๋“œ */} +
+ + +
+ + {/* ๊ทธ๋ฃน๋ช… ํ•„๋“œ */} +
+ + +
+
+
+ )} +
+
+ + {/* ๋ฆฌ์†Œ์Šค ์„ค์ • */} + + ๋ฆฌ์†Œ์Šค ์„ค์ • (์„ค๋น„/์ž‘์—…์ž) + +

ํƒ€์ž„๋ผ์ธ Y์ถ•์— ํ‘œ์‹œํ•  ๋ฆฌ์†Œ์Šค (์„ค๋น„, ์ž‘์—…์ž ๋“ฑ)

+ + {/* ๋ฆฌ์†Œ์Šค ํ…Œ์ด๋ธ” ์„ ํƒ */} +
+ + + + + + + { + const lowerSearch = search.toLowerCase(); + if (value.toLowerCase().includes(lowerSearch)) { + return 1; + } + return 0; + }} + > + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {tables.map((table) => ( + { + updateConfig({ resourceTable: table.tableName }); + setResourceTableSelectOpen(false); + }} + className="text-xs" + > + +
+ {table.displayName} + {table.tableName} +
+
+ ))} +
+
+
+
+
+
+ + {/* ๋ฆฌ์†Œ์Šค ํ•„๋“œ ๋งคํ•‘ */} + {config.resourceTable && ( +
+ +
+ {/* ID ํ•„๋“œ */} +
+ + +
+ + {/* ์ด๋ฆ„ ํ•„๋“œ */} +
+ + +
+
+
+ )} +
+
+ + {/* ํ‘œ์‹œ ์„ค์ • */} + + ํ‘œ์‹œ ์„ค์ • + + {/* ๊ธฐ๋ณธ ์คŒ ๋ ˆ๋ฒจ */} +
+ + +
+ + {/* ๋†’์ด */} +
+ + updateConfig({ height: parseInt(e.target.value) || 500 })} + className="h-8 text-xs" + /> +
+ + {/* ํ–‰ ๋†’์ด */} +
+ + updateConfig({ rowHeight: parseInt(e.target.value) || 50 })} + className="h-8 text-xs" + /> +
+ + {/* ํ† ๊ธ€ ์Šค์œ„์น˜๋“ค */} +
+
+ + updateConfig({ editable: v })} /> +
+ +
+ + updateConfig({ draggable: v })} /> +
+ +
+ + updateConfig({ resizable: v })} /> +
+ +
+ + updateConfig({ showTodayLine: v })} + /> +
+ +
+ + updateConfig({ showProgress: v })} + /> +
+ +
+ + updateConfig({ showToolbar: v })} + /> +
+
+
+
+
+
+ ); +} + +export default TimelineSchedulerConfigPanel; diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerRenderer.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerRenderer.tsx new file mode 100644 index 00000000..48e8a21f --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerRenderer.tsx @@ -0,0 +1,57 @@ +"use client"; + +import React from "react"; +import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer"; +import { V2TimelineSchedulerDefinition } from "./index"; +import { TimelineSchedulerComponent } from "./TimelineSchedulerComponent"; + +/** + * TimelineScheduler ๋ Œ๋”๋Ÿฌ + * ์ž๋™ ๋“ฑ๋ก ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ๋“ฑ๋ก + */ +export class TimelineSchedulerRenderer extends AutoRegisteringComponentRenderer { + static componentDefinition = V2TimelineSchedulerDefinition; + + render(): React.ReactElement { + return ( + + ); + } + + // ์„ค์ • ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ + protected handleConfigChange = (config: any) => { + console.log("๐Ÿ“ฅ TimelineSchedulerRenderer์—์„œ ์„ค์ • ๋ณ€๊ฒฝ ๋ฐ›์Œ:", config); + + // ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์˜ onConfigChange ํ˜ธ์ถœ (ํ™”๋ฉด ์„ค๊ณ„์ž์—๊ฒŒ ์•Œ๋ฆผ) + if (this.props.onConfigChange) { + this.props.onConfigChange(config); + } + + this.updateComponent({ config }); + }; + + // ๊ฐ’ ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ + protected handleValueChange = (value: any) => { + this.updateComponent({ value }); + }; +} + +// ์ž๋™ ๋“ฑ๋ก ์‹คํ–‰ +TimelineSchedulerRenderer.registerSelf(); + +// ๊ฐ•์ œ ๋“ฑ๋ก (๋””๋ฒ„๊น…์šฉ) +if (typeof window !== "undefined") { + setTimeout(() => { + try { + TimelineSchedulerRenderer.registerSelf(); + } catch (error) { + console.error("โŒ TimelineScheduler ๊ฐ•์ œ ๋“ฑ๋ก ์‹คํŒจ:", error); + } + }, 1000); +} diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/components/ResourceRow.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/components/ResourceRow.tsx new file mode 100644 index 00000000..407bdd14 --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/components/ResourceRow.tsx @@ -0,0 +1,206 @@ +"use client"; + +import React, { useMemo } from "react"; +import { cn } from "@/lib/utils"; +import { Resource, ScheduleItem, ZoomLevel, TimelineSchedulerConfig } from "../types"; +import { ScheduleBar } from "./ScheduleBar"; + +interface ResourceRowProps { + /** ๋ฆฌ์†Œ์Šค */ + resource: Resource; + /** ํ•ด๋‹น ๋ฆฌ์†Œ์Šค์˜ ์Šค์ผ€์ค„ ๋ชฉ๋ก */ + schedules: ScheduleItem[]; + /** ์‹œ์ž‘ ๋‚ ์งœ */ + startDate: Date; + /** ์ข…๋ฃŒ ๋‚ ์งœ */ + endDate: Date; + /** ์คŒ ๋ ˆ๋ฒจ */ + zoomLevel: ZoomLevel; + /** ํ–‰ ๋†’์ด */ + rowHeight: number; + /** ์…€ ๋„ˆ๋น„ */ + cellWidth: number; + /** ๋ฆฌ์†Œ์Šค ์ปฌ๋Ÿผ ๋„ˆ๋น„ */ + resourceColumnWidth: number; + /** ์„ค์ • */ + config: TimelineSchedulerConfig; + /** ์Šค์ผ€์ค„ ํด๋ฆญ */ + onScheduleClick?: (schedule: ScheduleItem) => void; + /** ๋นˆ ์…€ ํด๋ฆญ */ + onCellClick?: (resourceId: string, date: Date) => void; + /** ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ */ + onDragStart?: (schedule: ScheduleItem, e: React.MouseEvent) => void; + /** ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ */ + onDragEnd?: () => void; + /** ๋ฆฌ์‚ฌ์ด์ฆˆ ์‹œ์ž‘ */ + onResizeStart?: (schedule: ScheduleItem, direction: "start" | "end", e: React.MouseEvent) => void; + /** ๋ฆฌ์‚ฌ์ด์ฆˆ ์ข…๋ฃŒ */ + onResizeEnd?: () => void; +} + +/** + * ๋‚ ์งœ ์ฐจ์ด ๊ณ„์‚ฐ (์ผ์ˆ˜) + */ +const getDaysDiff = (start: Date, end: Date): number => { + const startTime = new Date(start).setHours(0, 0, 0, 0); + const endTime = new Date(end).setHours(0, 0, 0, 0); + return Math.round((endTime - startTime) / (1000 * 60 * 60 * 24)); +}; + +/** + * ๋‚ ์งœ ๋ฒ”์œ„ ๋‚ด์˜ ์…€ ๊ฐœ์ˆ˜ ๊ณ„์‚ฐ + */ +const getCellCount = (startDate: Date, endDate: Date): number => { + return getDaysDiff(startDate, endDate) + 1; +}; + +export function ResourceRow({ + resource, + schedules, + startDate, + endDate, + zoomLevel, + rowHeight, + cellWidth, + resourceColumnWidth, + config, + onScheduleClick, + onCellClick, + onDragStart, + onDragEnd, + onResizeStart, + onResizeEnd, +}: ResourceRowProps) { + // ์ด ์…€ ๊ฐœ์ˆ˜ + const totalCells = useMemo(() => getCellCount(startDate, endDate), [startDate, endDate]); + + // ์ด ๊ทธ๋ฆฌ๋“œ ๋„ˆ๋น„ + const gridWidth = totalCells * cellWidth; + + // ์˜ค๋Š˜ ๋‚ ์งœ + const today = useMemo(() => { + const d = new Date(); + d.setHours(0, 0, 0, 0); + return d; + }, []); + + // ์Šค์ผ€์ค„ ๋ฐ” ์œ„์น˜ ๊ณ„์‚ฐ + const schedulePositions = useMemo(() => { + return schedules.map((schedule) => { + const scheduleStart = new Date(schedule.startDate); + const scheduleEnd = new Date(schedule.endDate); + scheduleStart.setHours(0, 0, 0, 0); + scheduleEnd.setHours(0, 0, 0, 0); + + // ์‹œ์ž‘ ์œ„์น˜ ๊ณ„์‚ฐ + const startOffset = getDaysDiff(startDate, scheduleStart); + const left = Math.max(0, startOffset * cellWidth); + + // ๋„ˆ๋น„ ๊ณ„์‚ฐ + const durationDays = getDaysDiff(scheduleStart, scheduleEnd) + 1; + const visibleStartOffset = Math.max(0, startOffset); + const visibleEndOffset = Math.min( + totalCells, + startOffset + durationDays + ); + const width = Math.max(cellWidth, (visibleEndOffset - visibleStartOffset) * cellWidth); + + return { + schedule, + position: { + left: resourceColumnWidth + left, + top: 0, + width, + height: rowHeight, + }, + }; + }); + }, [schedules, startDate, cellWidth, resourceColumnWidth, rowHeight, totalCells]); + + // ๊ทธ๋ฆฌ๋“œ ์…€ ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ + const handleGridClick = (e: React.MouseEvent) => { + if (!onCellClick) return; + + const rect = e.currentTarget.getBoundingClientRect(); + const x = e.clientX - rect.left; + const cellIndex = Math.floor(x / cellWidth); + + const clickedDate = new Date(startDate); + clickedDate.setDate(clickedDate.getDate() + cellIndex); + + onCellClick(resource.id, clickedDate); + }; + + return ( +
+ {/* ๋ฆฌ์†Œ์Šค ์ปฌ๋Ÿผ */} +
+
+
{resource.name}
+ {resource.group && ( +
+ {resource.group} +
+ )} +
+
+ + {/* ํƒ€์ž„๋ผ์ธ ๊ทธ๋ฆฌ๋“œ */} +
+ {/* ๋ฐฐ๊ฒฝ ๊ทธ๋ฆฌ๋“œ */} +
+ {Array.from({ length: totalCells }).map((_, idx) => { + const cellDate = new Date(startDate); + cellDate.setDate(cellDate.getDate() + idx); + const isWeekend = cellDate.getDay() === 0 || cellDate.getDay() === 6; + const isToday = cellDate.getTime() === today.getTime(); + const isMonthStart = cellDate.getDate() === 1; + + return ( +
+ ); + })} +
+ + {/* ์Šค์ผ€์ค„ ๋ฐ”๋“ค */} + {schedulePositions.map(({ schedule, position }) => ( + onScheduleClick?.(schedule)} + onDragStart={onDragStart} + onDragEnd={onDragEnd} + onResizeStart={onResizeStart} + onResizeEnd={onResizeEnd} + /> + ))} +
+
+ ); +} diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/components/ScheduleBar.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/components/ScheduleBar.tsx new file mode 100644 index 00000000..a85c457c --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/components/ScheduleBar.tsx @@ -0,0 +1,182 @@ +"use client"; + +import React, { useState, useCallback, useRef } from "react"; +import { cn } from "@/lib/utils"; +import { ScheduleItem, ScheduleBarPosition, TimelineSchedulerConfig } from "../types"; +import { statusOptions } from "../config"; + +interface ScheduleBarProps { + /** ์Šค์ผ€์ค„ ํ•ญ๋ชฉ */ + schedule: ScheduleItem; + /** ์œ„์น˜ ์ •๋ณด */ + position: ScheduleBarPosition; + /** ์„ค์ • */ + config: TimelineSchedulerConfig; + /** ๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ */ + draggable?: boolean; + /** ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ */ + resizable?: boolean; + /** ํด๋ฆญ ์ด๋ฒคํŠธ */ + onClick?: (schedule: ScheduleItem) => void; + /** ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ */ + onDragStart?: (schedule: ScheduleItem, e: React.MouseEvent) => void; + /** ๋“œ๋ž˜๊ทธ ์ค‘ */ + onDrag?: (deltaX: number, deltaY: number) => void; + /** ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ */ + onDragEnd?: () => void; + /** ๋ฆฌ์‚ฌ์ด์ฆˆ ์‹œ์ž‘ */ + onResizeStart?: (schedule: ScheduleItem, direction: "start" | "end", e: React.MouseEvent) => void; + /** ๋ฆฌ์‚ฌ์ด์ฆˆ ์ค‘ */ + onResize?: (deltaX: number, direction: "start" | "end") => void; + /** ๋ฆฌ์‚ฌ์ด์ฆˆ ์ข…๋ฃŒ */ + onResizeEnd?: () => void; +} + +export function ScheduleBar({ + schedule, + position, + config, + draggable = true, + resizable = true, + onClick, + onDragStart, + onDragEnd, + onResizeStart, + onResizeEnd, +}: ScheduleBarProps) { + const [isDragging, setIsDragging] = useState(false); + const [isResizing, setIsResizing] = useState(false); + const barRef = useRef(null); + + // ์ƒํƒœ์— ๋”ฐ๋ฅธ ์ƒ‰์ƒ + const statusColor = schedule.color || + config.statusColors?.[schedule.status] || + statusOptions.find((s) => s.value === schedule.status)?.color || + "#3b82f6"; + + // ์ง„ํ–‰๋ฅ  ๋ฐ” ๋„ˆ๋น„ + const progressWidth = config.showProgress && schedule.progress !== undefined + ? `${schedule.progress}%` + : "0%"; + + // ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ ํ•ธ๋“ค๋Ÿฌ + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + if (!draggable || isResizing) return; + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + onDragStart?.(schedule, e); + + const handleMouseMove = (moveEvent: MouseEvent) => { + // ๋“œ๋ž˜๊ทธ ์ค‘ ๋กœ์ง์€ ๋ถ€๋ชจ์—์„œ ์ฒ˜๋ฆฌ + }; + + const handleMouseUp = () => { + setIsDragging(false); + onDragEnd?.(); + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }, + [draggable, isResizing, schedule, onDragStart, onDragEnd] + ); + + // ๋ฆฌ์‚ฌ์ด์ฆˆ ์‹œ์ž‘ ํ•ธ๋“ค๋Ÿฌ + const handleResizeStart = useCallback( + (direction: "start" | "end", e: React.MouseEvent) => { + if (!resizable) return; + e.preventDefault(); + e.stopPropagation(); + setIsResizing(true); + onResizeStart?.(schedule, direction, e); + + const handleMouseMove = (moveEvent: MouseEvent) => { + // ๋ฆฌ์‚ฌ์ด์ฆˆ ์ค‘ ๋กœ์ง์€ ๋ถ€๋ชจ์—์„œ ์ฒ˜๋ฆฌ + }; + + const handleMouseUp = () => { + setIsResizing(false); + onResizeEnd?.(); + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }, + [resizable, schedule, onResizeStart, onResizeEnd] + ); + + // ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ + const handleClick = useCallback( + (e: React.MouseEvent) => { + if (isDragging || isResizing) return; + e.stopPropagation(); + onClick?.(schedule); + }, + [isDragging, isResizing, onClick, schedule] + ); + + return ( +
+ {/* ์ง„ํ–‰๋ฅ  ๋ฐ” */} + {config.showProgress && schedule.progress !== undefined && ( +
+ )} + + {/* ์ œ๋ชฉ */} +
+ {schedule.title} +
+ + {/* ์ง„ํ–‰๋ฅ  ํ…์ŠคํŠธ */} + {config.showProgress && schedule.progress !== undefined && ( +
+ {schedule.progress}% +
+ )} + + {/* ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค - ์™ผ์ชฝ */} + {resizable && ( +
handleResizeStart("start", e)} + /> + )} + + {/* ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค - ์˜ค๋ฅธ์ชฝ */} + {resizable && ( +
handleResizeStart("end", e)} + /> + )} +
+ ); +} diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/components/TimelineHeader.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/components/TimelineHeader.tsx new file mode 100644 index 00000000..52afc2e2 --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/components/TimelineHeader.tsx @@ -0,0 +1,195 @@ +"use client"; + +import React, { useMemo } from "react"; +import { cn } from "@/lib/utils"; +import { DateCell, ZoomLevel } from "../types"; +import { dayLabels, monthLabels } from "../config"; + +interface TimelineHeaderProps { + /** ์‹œ์ž‘ ๋‚ ์งœ */ + startDate: Date; + /** ์ข…๋ฃŒ ๋‚ ์งœ */ + endDate: Date; + /** ์คŒ ๋ ˆ๋ฒจ */ + zoomLevel: ZoomLevel; + /** ์…€ ๋„ˆ๋น„ */ + cellWidth: number; + /** ํ—ค๋” ๋†’์ด */ + headerHeight: number; + /** ๋ฆฌ์†Œ์Šค ์ปฌ๋Ÿผ ๋„ˆ๋น„ */ + resourceColumnWidth: number; + /** ์˜ค๋Š˜ ํ‘œ์‹œ์„  */ + showTodayLine?: boolean; +} + +/** + * ๋‚ ์งœ ๋ฒ”์œ„ ๋‚ด์˜ ๋ชจ๋“  ๋‚ ์งœ ์…€ ์ƒ์„ฑ + */ +const generateDateCells = ( + startDate: Date, + endDate: Date, + zoomLevel: ZoomLevel +): DateCell[] => { + const cells: DateCell[] = []; + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const current = new Date(startDate); + current.setHours(0, 0, 0, 0); + + while (current <= endDate) { + const date = new Date(current); + const dayOfWeek = date.getDay(); + const isToday = date.getTime() === today.getTime(); + const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; + const isMonthStart = date.getDate() === 1; + + let label = ""; + if (zoomLevel === "day") { + label = `${date.getDate()}(${dayLabels[dayOfWeek]})`; + } else if (zoomLevel === "week") { + // ์ฃผ๊ฐ„: ์›”์š”์ผ ๊ธฐ์ค€ ์ฃผ ์‹œ์ž‘ + if (dayOfWeek === 1 || cells.length === 0) { + label = `${date.getMonth() + 1}/${date.getDate()}`; + } + } else if (zoomLevel === "month") { + // ์›”๊ฐ„: ์›” ์‹œ์ž‘์ผ๋งŒ ํ‘œ์‹œ + if (isMonthStart || cells.length === 0) { + label = monthLabels[date.getMonth()]; + } + } + + cells.push({ + date, + label, + isToday, + isWeekend, + isMonthStart, + }); + + current.setDate(current.getDate() + 1); + } + + return cells; +}; + +/** + * ์›” ํ—ค๋” ๊ทธ๋ฃน ์ƒ์„ฑ (์ƒ๋‹จ ํ–‰) + */ +const generateMonthGroups = ( + cells: DateCell[] +): { month: string; year: number; count: number }[] => { + const groups: { month: string; year: number; count: number }[] = []; + + cells.forEach((cell) => { + const month = monthLabels[cell.date.getMonth()]; + const year = cell.date.getFullYear(); + + if ( + groups.length === 0 || + groups[groups.length - 1].month !== month || + groups[groups.length - 1].year !== year + ) { + groups.push({ month, year, count: 1 }); + } else { + groups[groups.length - 1].count++; + } + }); + + return groups; +}; + +export function TimelineHeader({ + startDate, + endDate, + zoomLevel, + cellWidth, + headerHeight, + resourceColumnWidth, + showTodayLine = true, +}: TimelineHeaderProps) { + // ๋‚ ์งœ ์…€ ์ƒ์„ฑ + const dateCells = useMemo( + () => generateDateCells(startDate, endDate, zoomLevel), + [startDate, endDate, zoomLevel] + ); + + // ์›” ๊ทธ๋ฃน ์ƒ์„ฑ + const monthGroups = useMemo(() => generateMonthGroups(dateCells), [dateCells]); + + // ์˜ค๋Š˜ ์œ„์น˜ ๊ณ„์‚ฐ + const todayPosition = useMemo(() => { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const todayIndex = dateCells.findIndex( + (cell) => cell.date.getTime() === today.getTime() + ); + + if (todayIndex === -1) return null; + + return resourceColumnWidth + todayIndex * cellWidth + cellWidth / 2; + }, [dateCells, cellWidth, resourceColumnWidth]); + + return ( +
+ {/* ์ƒ๋‹จ ํ–‰: ์›”/๋…„๋„ */} +
+ {/* ๋ฆฌ์†Œ์Šค ์ปฌ๋Ÿผ ํ—ค๋” */} +
+ ๋ฆฌ์†Œ์Šค +
+ + {/* ์›” ๊ทธ๋ฃน */} + {monthGroups.map((group, idx) => ( +
+ {group.year}๋…„ {group.month} +
+ ))} +
+ + {/* ํ•˜๋‹จ ํ–‰: ์ผ์ž */} +
+ {/* ๋ฆฌ์†Œ์Šค ์ปฌ๋Ÿผ (๋นˆ์นธ) */} +
+ + {/* ๋‚ ์งœ ์…€ */} + {dateCells.map((cell, idx) => ( +
+ {cell.label} +
+ ))} +
+ + {/* ์˜ค๋Š˜ ํ‘œ์‹œ์„  */} + {showTodayLine && todayPosition !== null && ( +
+ )} +
+ ); +} diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/components/index.ts b/frontend/lib/registry/components/v2-timeline-scheduler/components/index.ts new file mode 100644 index 00000000..4da03f17 --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/components/index.ts @@ -0,0 +1,3 @@ +export { TimelineHeader } from "./TimelineHeader"; +export { ScheduleBar } from "./ScheduleBar"; +export { ResourceRow } from "./ResourceRow"; diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/config.ts b/frontend/lib/registry/components/v2-timeline-scheduler/config.ts new file mode 100644 index 00000000..17c31991 --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/config.ts @@ -0,0 +1,127 @@ +"use client"; + +import { TimelineSchedulerConfig, ZoomLevel, ScheduleType } from "./types"; + +/** + * ๊ธฐ๋ณธ ํƒ€์ž„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ ์„ค์ • + * - ๊ธฐ๋ณธ์ ์œผ๋กœ schedule_mng ํ…Œ์ด๋ธ” ์‚ฌ์šฉ (๊ณตํ†ต ์Šค์ผ€์ค„ ํ…Œ์ด๋ธ”) + * - ํ•„๋“œ ๋งคํ•‘์€ schedule_mng ์ปฌ๋Ÿผ์— ๋งž์ถค + */ +export const defaultTimelineSchedulerConfig: Partial = { + // schedule_mng ํ…Œ์ด๋ธ” ๊ธฐ๋ณธ ์‚ฌ์šฉ + useCustomTable: false, + scheduleType: "PRODUCTION", // ๊ธฐ๋ณธ: ์ƒ์‚ฐ๊ณ„ํš + + // ํ‘œ์‹œ ์„ค์ • + defaultZoomLevel: "day", + editable: true, + draggable: true, + resizable: true, + rowHeight: 50, + headerHeight: 60, + resourceColumnWidth: 150, + cellWidth: { + day: 60, + week: 120, + month: 40, + }, + showConflicts: true, + showProgress: true, + showTodayLine: true, + showToolbar: true, + showZoomControls: true, + showNavigation: true, + showAddButton: true, + height: 500, + + // ์ƒํƒœ๋ณ„ ์ƒ‰์ƒ + statusColors: { + planned: "#3b82f6", // blue-500 + in_progress: "#f59e0b", // amber-500 + completed: "#10b981", // emerald-500 + delayed: "#ef4444", // red-500 + cancelled: "#6b7280", // gray-500 + }, + + // schedule_mng ํ…Œ์ด๋ธ” ํ•„๋“œ ๋งคํ•‘ + fieldMapping: { + id: "schedule_id", + resourceId: "resource_id", + title: "schedule_name", + startDate: "start_date", + endDate: "end_date", + status: "status", + }, + + // ๋ฆฌ์†Œ์Šค ํ•„๋“œ ๋งคํ•‘ (equipment_mng ๊ธฐ์ค€) + resourceFieldMapping: { + id: "equipment_code", + name: "equipment_name", + group: "equipment_type", + }, + + // ๊ธฐ๋ณธ ๋ฆฌ์†Œ์Šค ํ…Œ์ด๋ธ” + resourceTable: "equipment_mng", +}; + +/** + * ์คŒ ๋ ˆ๋ฒจ ์˜ต์…˜ + */ +export const zoomLevelOptions: { value: ZoomLevel; label: string }[] = [ + { value: "day", label: "์ผ" }, + { value: "week", label: "์ฃผ" }, + { value: "month", label: "์›”" }, +]; + +/** + * ์ƒํƒœ ์˜ต์…˜ + */ +export const statusOptions = [ + { value: "planned", label: "๊ณ„ํš๋จ", color: "#3b82f6" }, + { value: "in_progress", label: "์ง„ํ–‰์ค‘", color: "#f59e0b" }, + { value: "completed", label: "์™„๋ฃŒ", color: "#10b981" }, + { value: "delayed", label: "์ง€์—ฐ", color: "#ef4444" }, + { value: "cancelled", label: "์ทจ์†Œ", color: "#6b7280" }, +]; + +/** + * ์Šค์ผ€์ค„ ํƒ€์ž… ์˜ต์…˜ + */ +export const scheduleTypeOptions: { value: ScheduleType; label: string }[] = [ + { value: "PRODUCTION", label: "์ƒ์‚ฐ๊ณ„ํš" }, + { value: "MAINTENANCE", label: "์ •๋น„๊ณ„ํš" }, + { value: "SHIPPING", label: "๋ฐฐ์ฐจ๊ณ„ํš" }, + { value: "WORK_ASSIGN", label: "์ž‘์—…๋ฐฐ์ •" }, +]; + +/** + * ์คŒ ๋ ˆ๋ฒจ๋ณ„ ํ‘œ์‹œ ์ผ์ˆ˜ + */ +export const zoomLevelDays: Record = { + day: 14, // 2์ฃผ + week: 56, // 8์ฃผ + month: 90, // 3๊ฐœ์›” +}; + +/** + * ์š”์ผ ๋ผ๋ฒจ (ํ•œ๊ธ€) + */ +export const dayLabels = ["์ผ", "์›”", "ํ™”", "์ˆ˜", "๋ชฉ", "๊ธˆ", "ํ† "]; + +/** + * ์›” ๋ผ๋ฒจ (ํ•œ๊ธ€) + */ +export const monthLabels = [ + "1์›”", + "2์›”", + "3์›”", + "4์›”", + "5์›”", + "6์›”", + "7์›”", + "8์›”", + "9์›”", + "10์›”", + "11์›”", + "12์›”", +]; diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts b/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts new file mode 100644 index 00000000..94c001d4 --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts @@ -0,0 +1,450 @@ +"use client"; + +import { useState, useCallback, useEffect, useMemo, useRef } from "react"; +import { apiClient } from "@/lib/api/client"; +import { v2EventBus, V2_EVENTS } from "@/lib/v2-core"; +import { TimelineSchedulerConfig, ScheduleItem, Resource, ZoomLevel, UseTimelineDataResult } from "../types"; +import { zoomLevelDays, defaultTimelineSchedulerConfig } from "../config"; + +// schedule_mng ํ…Œ์ด๋ธ” ๊ณ ์ • (๊ณตํ†ต ์Šค์ผ€์ค„ ํ…Œ์ด๋ธ”) +const SCHEDULE_TABLE = "schedule_mng"; + +/** + * ๋‚ ์งœ๋ฅผ ISO ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ (์‹œ๊ฐ„ ์ œ์™ธ) + */ +const toDateString = (date: Date): string => { + return date.toISOString().split("T")[0]; +}; + +/** + * ๋‚ ์งœ ๋”ํ•˜๊ธฐ + */ +const addDays = (date: Date, days: number): Date => { + const result = new Date(date); + result.setDate(result.getDate() + days); + return result; +}; + +/** + * ํƒ€์ž„๋ผ์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ํ›… + */ +export function useTimelineData( + config: TimelineSchedulerConfig, + externalSchedules?: ScheduleItem[], + externalResources?: Resource[], +): UseTimelineDataResult { + // ์ƒํƒœ + const [schedules, setSchedules] = useState([]); + const [resources, setResources] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [zoomLevel, setZoomLevel] = useState(config.defaultZoomLevel || "day"); + const [viewStartDate, setViewStartDate] = useState(() => { + if (config.initialDate) { + return new Date(config.initialDate); + } + // ์˜ค๋Š˜ ๊ธฐ์ค€ 1์ฃผ์ผ ์ „๋ถ€ํ„ฐ ์‹œ์ž‘ + const today = new Date(); + today.setDate(today.getDate() - 7); + today.setHours(0, 0, 0, 0); + return today; + }); + + // ์„ ํƒ๋œ ํ’ˆ๋ชฉ ์ฝ”๋“œ (์ขŒ์ธก ํ…Œ์ด๋ธ”์—์„œ ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ๊ธฐ์ค€) + const [selectedSourceKeys, setSelectedSourceKeys] = useState([]); + const selectedSourceKeysRef = useRef([]); + + // ํ‘œ์‹œ ์ข…๋ฃŒ์ผ ๊ณ„์‚ฐ + const viewEndDate = useMemo(() => { + const days = zoomLevelDays[zoomLevel]; + return addDays(viewStartDate, days); + }, [viewStartDate, zoomLevel]); + + // ํ…Œ์ด๋ธ”๋ช…: ๊ธฐ๋ณธ์ ์œผ๋กœ schedule_mng ์‚ฌ์šฉ, ์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ์„ค์ • ์‹œ ํ•ด๋‹น ํ…Œ์ด๋ธ” ์‚ฌ์šฉ + const tableName = config.useCustomTable && config.customTableName ? config.customTableName : SCHEDULE_TABLE; + + const resourceTableName = config.resourceTable; + + // ํ•„๋“œ ๋งคํ•‘์„ JSON ๋ฌธ์ž์—ด๋กœ ์•ˆ์ •ํ™” (๊ฐ์ฒด ์ฐธ์กฐ ๋ณ€๊ฒฝ ๋ฐฉ์ง€) + const fieldMappingKey = useMemo(() => { + return JSON.stringify(config.fieldMapping || {}); + }, [config.fieldMapping]); + + const resourceFieldMappingKey = useMemo(() => { + return JSON.stringify(config.resourceFieldMapping || {}); + }, [config.resourceFieldMapping]); + + // ๐Ÿ†• ํ•„๋“œ ๋งคํ•‘ ์ •๊ทœํ™” (์ด์ „ ํ˜•์‹ โ†’ ์ƒˆ ํ˜•์‹ ๋ณ€ํ™˜) - useMemo๋กœ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ + const fieldMapping = useMemo(() => { + const mapping = config.fieldMapping; + if (!mapping) return defaultTimelineSchedulerConfig.fieldMapping!; + + return { + id: mapping.id || mapping.idField || "id", + resourceId: mapping.resourceId || mapping.resourceIdField || "resource_id", + title: mapping.title || mapping.titleField || "title", + startDate: mapping.startDate || mapping.startDateField || "start_date", + endDate: mapping.endDate || mapping.endDateField || "end_date", + status: mapping.status || mapping.statusField || undefined, + progress: mapping.progress || mapping.progressField || undefined, + color: mapping.color || mapping.colorField || undefined, + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fieldMappingKey]); + + // ๋ฆฌ์†Œ์Šค ํ•„๋“œ ๋งคํ•‘ - useMemo๋กœ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ + const resourceFieldMapping = useMemo(() => { + return config.resourceFieldMapping || defaultTimelineSchedulerConfig.resourceFieldMapping!; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [resourceFieldMappingKey]); + + // ์Šค์ผ€์ค„ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + const fetchSchedules = useCallback(async () => { + if (externalSchedules) { + setSchedules(externalSchedules); + return; + } + + if (!tableName) { + setSchedules([]); + return; + } + + setIsLoading(true); + setError(null); + + try { + // schedule_mng ํ…Œ์ด๋ธ” ์‚ฌ์šฉ ์‹œ ํ•„ํ„ฐ ์กฐ๊ฑด ๊ตฌ์„ฑ + const isScheduleMng = tableName === SCHEDULE_TABLE; + const currentSourceKeys = selectedSourceKeysRef.current; + + console.log("[useTimelineData] ์Šค์ผ€์ค„ ์กฐํšŒ:", { + tableName, + scheduleType: config.scheduleType, + sourceKeys: currentSourceKeys, + }); + + const response = await apiClient.post(`/table-management/tables/${tableName}/data`, { + page: 1, + size: 10000, + autoFilter: true, + }); + + const responseData = response.data?.data?.data || response.data?.data || []; + let rawData = Array.isArray(responseData) ? responseData : []; + + // ํด๋ผ์ด์–ธํŠธ ์ธก ํ•„ํ„ฐ๋ง ์ ์šฉ (schedule_mng ํ…Œ์ด๋ธ”์ธ ๊ฒฝ์šฐ) + if (isScheduleMng) { + // ์Šค์ผ€์ค„ ํƒ€์ž… ํ•„ํ„ฐ + if (config.scheduleType) { + rawData = rawData.filter((row: any) => row.schedule_type === config.scheduleType); + } + + // ์„ ํƒ๋œ ํ’ˆ๋ชฉ ํ•„ํ„ฐ (source_group_key ๊ธฐ์ค€) + if (currentSourceKeys.length > 0) { + rawData = rawData.filter((row: any) => currentSourceKeys.includes(row.source_group_key)); + } + + console.log("[useTimelineData] ํ•„ํ„ฐ๋ง ํ›„ ์Šค์ผ€์ค„:", rawData.length, "๊ฑด"); + } + + // schedule_mng ํ…Œ์ด๋ธ”์šฉ ํ•„๋“œ ๋งคํ•‘ (๊ณ ์ •) + const scheduleMngFieldMapping = { + id: "schedule_id", + resourceId: "resource_id", + title: "schedule_name", + startDate: "start_date", + endDate: "end_date", + status: "status", + progress: undefined, // actual_qty / plan_qty๋กœ ๊ณ„์‚ฐ ๊ฐ€๋Šฅ + }; + + // ์‚ฌ์šฉํ•  ํ•„๋“œ ๋งคํ•‘ ๊ฒฐ์ • + const effectiveMapping = isScheduleMng ? scheduleMngFieldMapping : fieldMapping; + + // ๋ฐ์ดํ„ฐ๋ฅผ ScheduleItem ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ + const mappedSchedules: ScheduleItem[] = rawData.map((row: any) => { + // ์ง„ํ–‰๋ฅ  ๊ณ„์‚ฐ (schedule_mng์ผ ๊ฒฝ์šฐ) + let progress: number | undefined; + if (isScheduleMng && row.plan_qty && row.plan_qty > 0) { + progress = Math.round(((row.actual_qty || 0) / row.plan_qty) * 100); + } else if (effectiveMapping.progress) { + progress = Number(row[effectiveMapping.progress]) || 0; + } + + return { + id: String(row[effectiveMapping.id] || ""), + resourceId: String(row[effectiveMapping.resourceId] || ""), + title: String(row[effectiveMapping.title] || ""), + startDate: row[effectiveMapping.startDate] || "", + endDate: row[effectiveMapping.endDate] || "", + status: effectiveMapping.status ? row[effectiveMapping.status] || "planned" : "planned", + progress, + color: fieldMapping.color ? row[fieldMapping.color] : undefined, + data: row, + }; + }); + + console.log("[useTimelineData] ์Šค์ผ€์ค„ ๋กœ๋“œ ์™„๋ฃŒ:", mappedSchedules.length, "๊ฑด"); + setSchedules(mappedSchedules); + } catch (err: any) { + console.error("[useTimelineData] ์Šค์ผ€์ค„ ๋กœ๋“œ ์˜ค๋ฅ˜:", err); + setError(err.message || "์Šค์ผ€์ค„ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"); + setSchedules([]); + } finally { + setIsLoading(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tableName, externalSchedules, fieldMappingKey, config.scheduleType]); + + // ๋ฆฌ์†Œ์Šค ๋ฐ์ดํ„ฐ ๋กœ๋“œ + const fetchResources = useCallback(async () => { + if (externalResources) { + setResources(externalResources); + return; + } + + if (!resourceTableName) { + setResources([]); + return; + } + + try { + const response = await apiClient.post(`/table-management/tables/${resourceTableName}/data`, { + page: 1, + size: 1000, + autoFilter: true, + }); + + const responseData = response.data?.data?.data || response.data?.data || []; + const rawData = Array.isArray(responseData) ? responseData : []; + + // ๋ฐ์ดํ„ฐ๋ฅผ Resource ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ + const mappedResources: Resource[] = rawData.map((row: any) => ({ + id: String(row[resourceFieldMapping.id] || ""), + name: String(row[resourceFieldMapping.name] || ""), + group: resourceFieldMapping.group ? row[resourceFieldMapping.group] : undefined, + })); + + setResources(mappedResources); + } catch (err: any) { + console.error("๋ฆฌ์†Œ์Šค ๋กœ๋“œ ์˜ค๋ฅ˜:", err); + setResources([]); + } + // resourceFieldMappingKey๋ฅผ ์˜์กด์„ฑ์œผ๋กœ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ์ฒด ์ฐธ์กฐ ๋ณ€๊ฒฝ ๋ฐฉ์ง€ + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [resourceTableName, externalResources, resourceFieldMappingKey]); + + // ์ดˆ๊ธฐ ๋กœ๋“œ + useEffect(() => { + fetchSchedules(); + }, [fetchSchedules]); + + useEffect(() => { + fetchResources(); + }, [fetchResources]); + + // ์ด๋ฒคํŠธ ๋ฒ„์Šค ๋ฆฌ์Šค๋„ˆ - ํ…Œ์ด๋ธ” ์„ ํƒ ๋ณ€๊ฒฝ (ํ’ˆ๋ชฉ ์„ ํƒ ์‹œ ํ•ด๋‹น ์Šค์ผ€์ค„๋งŒ ํ‘œ์‹œ) + useEffect(() => { + const unsubscribeSelection = v2EventBus.subscribe(V2_EVENTS.TABLE_SELECTION_CHANGE, (payload) => { + console.log("[useTimelineData] TABLE_SELECTION_CHANGE ์ˆ˜์‹ :", { + tableName: payload.tableName, + selectedCount: payload.selectedCount, + }); + + // ์„ค์ •๋œ ๊ทธ๋ฃน ํ•„๋“œ๋ช… ์‚ฌ์šฉ (์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’๋“ค fallback) + const groupByField = config.sourceConfig?.groupByField; + + // ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ์—์„œ source_group_key ์ถ”์ถœ + const sourceKeys: string[] = []; + for (const row of payload.selectedRows || []) { + // ์„ค์ •๋œ ํ•„๋“œ๋ช… ์šฐ์„ , ์—†์œผ๋ฉด ์ผ๋ฐ˜์ ์ธ ํ•„๋“œ๋ช… fallback + let key: string | undefined; + if (groupByField && row[groupByField]) { + key = row[groupByField]; + } else { + // fallback: ์ผ๋ฐ˜์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ํ•„๋“œ๋ช…๋“ค + key = row.part_code || row.source_group_key || row.item_code; + } + + if (key && !sourceKeys.includes(key)) { + sourceKeys.push(key); + } + } + + console.log("[useTimelineData] ์„ ํƒ๋œ ๊ทธ๋ฃน ํ‚ค:", { + groupByField, + keys: sourceKeys, + }); + + // ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋ฐ ref ๋™๊ธฐํ™” + selectedSourceKeysRef.current = sourceKeys; + setSelectedSourceKeys(sourceKeys); + }); + + return () => { + unsubscribeSelection(); + }; + }, [config.sourceConfig?.groupByField]); + + // ์„ ํƒ๋œ ํ’ˆ๋ชฉ์ด ๋ณ€๊ฒฝ๋˜๋ฉด ์Šค์ผ€์ค„ ๋‹ค์‹œ ๋กœ๋“œ + useEffect(() => { + if (tableName === SCHEDULE_TABLE) { + console.log("[useTimelineData] ์„ ํƒ ํ’ˆ๋ชฉ ๋ณ€๊ฒฝ์œผ๋กœ ์Šค์ผ€์ค„ ์ƒˆ๋กœ๊ณ ์นจ:", selectedSourceKeys); + fetchSchedules(); + } + }, [selectedSourceKeys, tableName, fetchSchedules]); + + // ์ด๋ฒคํŠธ ๋ฒ„์Šค ๋ฆฌ์Šค๋„ˆ - ์Šค์ผ€์ค„ ์ƒ์„ฑ ์™„๋ฃŒ ๋ฐ ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ + useEffect(() => { + // TABLE_REFRESH ์ด๋ฒคํŠธ ์ˆ˜์‹  - ์Šค์ผ€์ค„ ์ƒˆ๋กœ๊ณ ์นจ + const unsubscribeRefresh = v2EventBus.subscribe(V2_EVENTS.TABLE_REFRESH, (payload) => { + // schedule_mng ๋˜๋Š” ํ•ด๋‹น ํ…Œ์ด๋ธ”์— ๋Œ€ํ•œ ์ƒˆ๋กœ๊ณ ์นจ + if (payload.tableName === tableName || payload.tableName === SCHEDULE_TABLE) { + console.log("[useTimelineData] TABLE_REFRESH ์ˆ˜์‹ , ์Šค์ผ€์ค„ ์ƒˆ๋กœ๊ณ ์นจ:", payload); + fetchSchedules(); + } + }); + + // SCHEDULE_GENERATE_COMPLETE ์ด๋ฒคํŠธ ์ˆ˜์‹  - ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ ์™„๋ฃŒ ์‹œ ์ƒˆ๋กœ๊ณ ์นจ + const unsubscribeComplete = v2EventBus.subscribe(V2_EVENTS.SCHEDULE_GENERATE_COMPLETE, (payload) => { + if (payload.success) { + console.log("[useTimelineData] SCHEDULE_GENERATE_COMPLETE ์ˆ˜์‹ , ์Šค์ผ€์ค„ ์ƒˆ๋กœ๊ณ ์นจ:", payload); + fetchSchedules(); + } + }); + + return () => { + unsubscribeRefresh(); + unsubscribeComplete(); + }; + }, [tableName, fetchSchedules]); + + // ๋„ค๋น„๊ฒŒ์ด์…˜ ํ•จ์ˆ˜๋“ค + const goToPrevious = useCallback(() => { + const days = zoomLevelDays[zoomLevel]; + setViewStartDate((prev) => addDays(prev, -days)); + }, [zoomLevel]); + + const goToNext = useCallback(() => { + const days = zoomLevelDays[zoomLevel]; + setViewStartDate((prev) => addDays(prev, days)); + }, [zoomLevel]); + + const goToToday = useCallback(() => { + const today = new Date(); + today.setDate(today.getDate() - 7); + today.setHours(0, 0, 0, 0); + setViewStartDate(today); + }, []); + + const goToDate = useCallback((date: Date) => { + const newDate = new Date(date); + newDate.setDate(newDate.getDate() - 7); + newDate.setHours(0, 0, 0, 0); + setViewStartDate(newDate); + }, []); + + // ์Šค์ผ€์ค„ ์—…๋ฐ์ดํŠธ + const updateSchedule = useCallback( + async (id: string, updates: Partial) => { + if (!tableName || !config.editable) return; + + try { + // ํ•„๋“œ ๋งคํ•‘ ์—ญ๋ณ€ํ™˜ + const updateData: Record = {}; + if (updates.startDate) updateData[fieldMapping.startDate] = updates.startDate; + if (updates.endDate) updateData[fieldMapping.endDate] = updates.endDate; + if (updates.resourceId) updateData[fieldMapping.resourceId] = updates.resourceId; + if (updates.title) updateData[fieldMapping.title] = updates.title; + if (updates.status && fieldMapping.status) updateData[fieldMapping.status] = updates.status; + if (updates.progress !== undefined && fieldMapping.progress) + updateData[fieldMapping.progress] = updates.progress; + + await apiClient.put(`/table-management/tables/${tableName}/data/${id}`, updateData); + + // ๋กœ์ปฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ + setSchedules((prev) => prev.map((s) => (s.id === id ? { ...s, ...updates } : s))); + } catch (err: any) { + console.error("์Šค์ผ€์ค„ ์—…๋ฐ์ดํŠธ ์˜ค๋ฅ˜:", err); + throw err; + } + }, + [tableName, fieldMapping, config.editable], + ); + + // ์Šค์ผ€์ค„ ์ถ”๊ฐ€ + const addSchedule = useCallback( + async (schedule: Omit) => { + if (!tableName || !config.editable) return; + + try { + // ํ•„๋“œ ๋งคํ•‘ ์—ญ๋ณ€ํ™˜ + const insertData: Record = { + [fieldMapping.resourceId]: schedule.resourceId, + [fieldMapping.title]: schedule.title, + [fieldMapping.startDate]: schedule.startDate, + [fieldMapping.endDate]: schedule.endDate, + }; + + if (fieldMapping.status) insertData[fieldMapping.status] = schedule.status; + if (fieldMapping.progress && schedule.progress !== undefined) + insertData[fieldMapping.progress] = schedule.progress; + + const response = await apiClient.post(`/table-management/tables/${tableName}/data`, insertData); + + const newId = response.data?.data?.id || Date.now().toString(); + + // ๋กœ์ปฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ + setSchedules((prev) => [...prev, { ...schedule, id: newId }]); + } catch (err: any) { + console.error("์Šค์ผ€์ค„ ์ถ”๊ฐ€ ์˜ค๋ฅ˜:", err); + throw err; + } + }, + [tableName, fieldMapping, config.editable], + ); + + // ์Šค์ผ€์ค„ ์‚ญ์ œ + const deleteSchedule = useCallback( + async (id: string) => { + if (!tableName || !config.editable) return; + + try { + await apiClient.delete(`/table-management/tables/${tableName}/data/${id}`); + + // ๋กœ์ปฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ + setSchedules((prev) => prev.filter((s) => s.id !== id)); + } catch (err: any) { + console.error("์Šค์ผ€์ค„ ์‚ญ์ œ ์˜ค๋ฅ˜:", err); + throw err; + } + }, + [tableName, config.editable], + ); + + // ์ƒˆ๋กœ๊ณ ์นจ + const refresh = useCallback(() => { + fetchSchedules(); + fetchResources(); + }, [fetchSchedules, fetchResources]); + + return { + schedules, + resources, + isLoading, + error, + zoomLevel, + setZoomLevel, + viewStartDate, + viewEndDate, + goToPrevious, + goToNext, + goToToday, + goToDate, + updateSchedule, + addSchedule, + deleteSchedule, + refresh, + }; +} diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/index.ts b/frontend/lib/registry/components/v2-timeline-scheduler/index.ts new file mode 100644 index 00000000..33c483a0 --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/index.ts @@ -0,0 +1,38 @@ +"use client"; + +import { ComponentCategory } from "@/types/component"; +import { createComponentDefinition } from "../../utils/createComponentDefinition"; +import { TimelineSchedulerComponent } from "./TimelineSchedulerComponent"; +import { TimelineSchedulerConfigPanel } from "./TimelineSchedulerConfigPanel"; +import { defaultTimelineSchedulerConfig } from "./config"; +import { TimelineSchedulerConfig } from "./types"; + +/** + * v2-timeline-scheduler ์ปดํฌ๋„ŒํŠธ ์ •์˜ + * ๊ฐ„ํŠธ์ฐจํŠธ ํ˜•ํƒœ์˜ ์ผ์ •/๊ณ„ํš ์‹œ๊ฐํ™” ๋ฐ ํŽธ์ง‘ ์ปดํฌ๋„ŒํŠธ + */ +export const V2TimelineSchedulerDefinition = createComponentDefinition({ + id: "v2-timeline-scheduler", + name: "ํƒ€์ž„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ", + nameEng: "Timeline Scheduler Component", + description: "๊ฐ„ํŠธ์ฐจํŠธ ํ˜•ํƒœ์˜ ์ผ์ •/๊ณ„ํš ์‹œ๊ฐํ™” ๋ฐ ํŽธ์ง‘ ์ปดํฌ๋„ŒํŠธ", + category: ComponentCategory.DISPLAY, + webType: "text", + component: TimelineSchedulerComponent, + configPanel: TimelineSchedulerConfigPanel, + defaultConfig: defaultTimelineSchedulerConfig as TimelineSchedulerConfig, + defaultSize: { + width: 1000, + height: 500, + }, + icon: "Calendar", + tags: ["ํƒ€์ž„๋ผ์ธ", "์Šค์ผ€์ค„", "๊ฐ„ํŠธ์ฐจํŠธ", "์ผ์ •", "๊ณ„ํš"], + version: "2.0.0", + author: "๊ฐœ๋ฐœํŒ€", + documentation: "", +}); + +export { TimelineSchedulerComponent } from "./TimelineSchedulerComponent"; +export { TimelineSchedulerConfigPanel } from "./TimelineSchedulerConfigPanel"; +export * from "./types"; +export * from "./config"; diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/types.ts b/frontend/lib/registry/components/v2-timeline-scheduler/types.ts new file mode 100644 index 00000000..baf59741 --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/types.ts @@ -0,0 +1,400 @@ +"use client"; + +import { ComponentConfig } from "@/types/component"; + +/** + * ์คŒ ๋ ˆ๋ฒจ (์‹œ๊ฐ„ ๋‹จ์œ„) + */ +export type ZoomLevel = "day" | "week" | "month"; + +/** + * ์Šค์ผ€์ค„ ์ƒํƒœ + */ +export type ScheduleStatus = "planned" | "in_progress" | "completed" | "delayed" | "cancelled"; + +/** + * ์Šค์ผ€์ค„ ํ•ญ๋ชฉ (๊ฐ„ํŠธ ๋ฐ”) + */ +export interface ScheduleItem { + /** ๊ณ ์œ  ID */ + id: string; + + /** ๋ฆฌ์†Œ์Šค ID (์„ค๋น„/์ž‘์—…์ž) */ + resourceId: string; + + /** ํ‘œ์‹œ ์ œ๋ชฉ */ + title: string; + + /** ์‹œ์ž‘ ์ผ์‹œ (ISO 8601) */ + startDate: string; + + /** ์ข…๋ฃŒ ์ผ์‹œ (ISO 8601) */ + endDate: string; + + /** ์ƒํƒœ */ + status: ScheduleStatus; + + /** ์ง„ํ–‰๋ฅ  (0-100) */ + progress?: number; + + /** ์ƒ‰์ƒ (CSS color) */ + color?: string; + + /** ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ */ + data?: Record; +} + +/** + * ๋ฆฌ์†Œ์Šค (ํ–‰ - ์„ค๋น„/์ž‘์—…์ž) + */ +export interface Resource { + /** ๋ฆฌ์†Œ์Šค ID */ + id: string; + + /** ํ‘œ์‹œ๋ช… */ + name: string; + + /** ๊ทธ๋ฃน (์„ ํƒ) */ + group?: string; + + /** ์•„์ด์ฝ˜ (์„ ํƒ) */ + icon?: string; + + /** ์šฉ๋Ÿ‰ (์„ ํƒ, ์ถฉ๋Œ ๊ณ„์‚ฐ์šฉ) */ + capacity?: number; +} + +/** + * ํ•„๋“œ ๋งคํ•‘ ์„ค์ • + */ +export interface FieldMapping { + /** ID ํ•„๋“œ */ + id: string; + /** ๋ฆฌ์†Œ์Šค ID ํ•„๋“œ */ + resourceId: string; + /** ์ œ๋ชฉ ํ•„๋“œ */ + title: string; + /** ์‹œ์ž‘์ผ ํ•„๋“œ */ + startDate: string; + /** ์ข…๋ฃŒ์ผ ํ•„๋“œ */ + endDate: string; + /** ์ƒํƒœ ํ•„๋“œ (์„ ํƒ) */ + status?: string; + /** ์ง„ํ–‰๋ฅ  ํ•„๋“œ (์„ ํƒ) */ + progress?: string; + /** ์ƒ‰์ƒ ํ•„๋“œ (์„ ํƒ) */ + color?: string; +} + +/** + * ๋ฆฌ์†Œ์Šค ํ•„๋“œ ๋งคํ•‘ ์„ค์ • + */ +export interface ResourceFieldMapping { + /** ID ํ•„๋“œ */ + id: string; + /** ์ด๋ฆ„ ํ•„๋“œ */ + name: string; + /** ๊ทธ๋ฃน ํ•„๋“œ (์„ ํƒ) */ + group?: string; +} + +/** + * ์Šค์ผ€์ค„ ํƒ€์ž… (schedule_mng.schedule_type) + */ +export type ScheduleType = + | "PRODUCTION" // ์ƒ์‚ฐ๊ณ„ํš + | "MAINTENANCE" // ์ •๋น„๊ณ„ํš + | "SHIPPING" // ๋ฐฐ์ฐจ๊ณ„ํš + | "WORK_ASSIGN"; // ์ž‘์—…๋ฐฐ์ • + +/** + * ์†Œ์Šค ๋ฐ์ดํ„ฐ ์„ค์ • (์Šค์ผ€์ค„ ์ƒ์„ฑ ๊ธฐ์ค€์ด ๋˜๋Š” ์›๋ณธ ๋ฐ์ดํ„ฐ) + * ์˜ˆ: ์ˆ˜์ฃผ ๋ฐ์ดํ„ฐ, ์ž‘์—… ์š”์ฒญ ๋“ฑ + */ +export interface SourceDataConfig { + /** ์†Œ์Šค ํ…Œ์ด๋ธ”๋ช… (์˜ˆ: sales_order_mng) */ + tableName?: string; + + /** ๊ธฐ์ค€์ผ ํ•„๋“œ - ์Šค์ผ€์ค„ ์ข…๋ฃŒ์ผ๋กœ ์‚ฌ์šฉ (์˜ˆ: due_date, delivery_date) */ + dueDateField?: string; + + /** ์ˆ˜๋Ÿ‰ ํ•„๋“œ (์˜ˆ: balance_qty, order_qty) */ + quantityField?: string; + + /** ๊ทธ๋ฃนํ™” ํ•„๋“œ - ํ’ˆ๋ชฉ/์ž‘์—… ๋‹จ์œ„ (์˜ˆ: part_code, item_code) */ + groupByField?: string; + + /** ๊ทธ๋ฃน๋ช… ํ‘œ์‹œ ํ•„๋“œ (์˜ˆ: part_name, item_name) */ + groupNameField?: string; +} + +/** + * ํƒ€์ž„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ ์„ค์ • + */ +export interface TimelineSchedulerConfig extends ComponentConfig { + /** ์Šค์ผ€์ค„ ํƒ€์ž… (ํ•„ํ„ฐ๋ง ๊ธฐ์ค€) - schedule_mng.schedule_type */ + scheduleType?: ScheduleType; + + /** ์Šค์ผ€์ค„ ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ”๋ช… (๊ธฐ๋ณธ: schedule_mng, ์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ ์‹œ) */ + selectedTable?: string; + + /** ์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ ์—ฌ๋ถ€ (false๋ฉด schedule_mng ์‚ฌ์šฉ) */ + useCustomTable?: boolean; + + /** ์ปค์Šคํ…€ ํ…Œ์ด๋ธ”๋ช… */ + customTableName?: string; + + /** ๋ฆฌ์†Œ์Šค ํ…Œ์ด๋ธ”๋ช… (์„ค๋น„/์ž‘์—…์ž) */ + resourceTable?: string; + + /** ์†Œ์Šค ๋ฐ์ดํ„ฐ ์„ค์ • (์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ ์‹œ ์ฐธ์กฐ) */ + sourceConfig?: SourceDataConfig; + + /** ์Šค์ผ€์ค„ ํ•„๋“œ ๋งคํ•‘ */ + fieldMapping: FieldMapping; + + /** ๋ฆฌ์†Œ์Šค ํ•„๋“œ ๋งคํ•‘ */ + resourceFieldMapping?: ResourceFieldMapping; + + /** ์ดˆ๊ธฐ ์คŒ ๋ ˆ๋ฒจ */ + defaultZoomLevel?: ZoomLevel; + + /** ์ดˆ๊ธฐ ํ‘œ์‹œ ๋‚ ์งœ (ISO 8601) */ + initialDate?: string; + + /** ํŽธ์ง‘ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ */ + editable?: boolean; + + /** ๋“œ๋ž˜๊ทธ ์ด๋™ ๊ฐ€๋Šฅ */ + draggable?: boolean; + + /** ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ฐ€๋Šฅ */ + resizable?: boolean; + + /** ํ–‰ ๋†’์ด (px) */ + rowHeight?: number; + + /** ํ—ค๋” ๋†’์ด (px) */ + headerHeight?: number; + + /** ๋ฆฌ์†Œ์Šค ์ปฌ๋Ÿผ ๋„ˆ๋น„ (px) */ + resourceColumnWidth?: number; + + /** ์…€ ๋„ˆ๋น„ (px, ์คŒ ๋ ˆ๋ฒจ๋ณ„) */ + cellWidth?: { + day?: number; + week?: number; + month?: number; + }; + + /** ์ถฉ๋Œ ํ‘œ์‹œ ์—ฌ๋ถ€ */ + showConflicts?: boolean; + + /** ์ง„ํ–‰๋ฅ  ๋ฐ” ํ‘œ์‹œ ์—ฌ๋ถ€ */ + showProgress?: boolean; + + /** ์˜ค๋Š˜ ํ‘œ์‹œ์„  */ + showTodayLine?: boolean; + + /** ์ƒํƒœ๋ณ„ ์ƒ‰์ƒ */ + statusColors?: { + planned?: string; + in_progress?: string; + completed?: string; + delayed?: string; + cancelled?: string; + }; + + /** ํˆด๋ฐ” ํ‘œ์‹œ ์—ฌ๋ถ€ */ + showToolbar?: boolean; + + /** ์คŒ ๋ ˆ๋ฒจ ๋ณ€๊ฒฝ ๋ฒ„ํŠผ ํ‘œ์‹œ */ + showZoomControls?: boolean; + + /** ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฒ„ํŠผ ํ‘œ์‹œ */ + showNavigation?: boolean; + + /** ์ถ”๊ฐ€ ๋ฒ„ํŠผ ํ‘œ์‹œ */ + showAddButton?: boolean; + + /** ๋†’์ด (px ๋˜๋Š” auto) */ + height?: number | string; + + /** ์ตœ๋Œ€ ๋†’์ด */ + maxHeight?: number | string; +} + +/** + * ๋“œ๋ž˜๊ทธ ์ด๋ฒคํŠธ + */ +export interface DragEvent { + /** ์Šค์ผ€์ค„ ID */ + scheduleId: string; + /** ์ƒˆ๋กœ์šด ์‹œ์ž‘์ผ */ + newStartDate: string; + /** ์ƒˆ๋กœ์šด ์ข…๋ฃŒ์ผ */ + newEndDate: string; + /** ์ƒˆ๋กœ์šด ๋ฆฌ์†Œ์Šค ID (๋ฆฌ์†Œ์Šค ๊ฐ„ ์ด๋™ ์‹œ) */ + newResourceId?: string; +} + +/** + * ๋ฆฌ์‚ฌ์ด์ฆˆ ์ด๋ฒคํŠธ + */ +export interface ResizeEvent { + /** ์Šค์ผ€์ค„ ID */ + scheduleId: string; + /** ์ƒˆ๋กœ์šด ์‹œ์ž‘์ผ */ + newStartDate: string; + /** ์ƒˆ๋กœ์šด ์ข…๋ฃŒ์ผ */ + newEndDate: string; + /** ๋ฆฌ์‚ฌ์ด์ฆˆ ๋ฐฉํ–ฅ */ + direction: "start" | "end"; +} + +/** + * ํด๋ฆญ ์ด๋ฒคํŠธ + */ +export interface ScheduleClickEvent { + /** ์Šค์ผ€์ค„ ํ•ญ๋ชฉ */ + schedule: ScheduleItem; + /** ๋ฆฌ์†Œ์Šค */ + resource: Resource; +} + +/** + * ๋นˆ ์…€ ํด๋ฆญ ์ด๋ฒคํŠธ + */ +export interface CellClickEvent { + /** ๋ฆฌ์†Œ์Šค ID */ + resourceId: string; + /** ๋‚ ์งœ */ + date: string; +} + +/** + * TimelineSchedulerComponent Props + */ +export interface TimelineSchedulerComponentProps { + /** ์ปดํฌ๋„ŒํŠธ ์„ค์ • */ + config: TimelineSchedulerConfig; + + /** ๋””์ž์ธ ๋ชจ๋“œ ์—ฌ๋ถ€ */ + isDesignMode?: boolean; + + /** ํผ ๋ฐ์ดํ„ฐ */ + formData?: Record; + + /** ์™ธ๋ถ€ ์Šค์ผ€์ค„ ๋ฐ์ดํ„ฐ */ + externalSchedules?: ScheduleItem[]; + + /** ์™ธ๋ถ€ ๋ฆฌ์†Œ์Šค ๋ฐ์ดํ„ฐ */ + externalResources?: Resource[]; + + /** ๋กœ๋”ฉ ์ƒํƒœ */ + isLoading?: boolean; + + /** ์—๋Ÿฌ */ + error?: string; + + /** ์ปดํฌ๋„ŒํŠธ ID */ + componentId?: string; + + /** ๋“œ๋ž˜๊ทธ ์™„๋ฃŒ ์ด๋ฒคํŠธ */ + onDragEnd?: (event: DragEvent) => void; + + /** ๋ฆฌ์‚ฌ์ด์ฆˆ ์™„๋ฃŒ ์ด๋ฒคํŠธ */ + onResizeEnd?: (event: ResizeEvent) => void; + + /** ์Šค์ผ€์ค„ ํด๋ฆญ ์ด๋ฒคํŠธ */ + onScheduleClick?: (event: ScheduleClickEvent) => void; + + /** ๋นˆ ์…€ ํด๋ฆญ ์ด๋ฒคํŠธ */ + onCellClick?: (event: CellClickEvent) => void; + + /** ์Šค์ผ€์ค„ ์ถ”๊ฐ€ ์ด๋ฒคํŠธ */ + onAddSchedule?: (resourceId: string, date: string) => void; +} + +/** + * useTimelineData ํ›… ๋ฐ˜ํ™˜ ํƒ€์ž… + */ +export interface UseTimelineDataResult { + /** ์Šค์ผ€์ค„ ๋ชฉ๋ก */ + schedules: ScheduleItem[]; + + /** ๋ฆฌ์†Œ์Šค ๋ชฉ๋ก */ + resources: Resource[]; + + /** ๋กœ๋”ฉ ์ƒํƒœ */ + isLoading: boolean; + + /** ์—๋Ÿฌ */ + error: string | null; + + /** ํ˜„์žฌ ์คŒ ๋ ˆ๋ฒจ */ + zoomLevel: ZoomLevel; + + /** ์คŒ ๋ ˆ๋ฒจ ๋ณ€๊ฒฝ */ + setZoomLevel: (level: ZoomLevel) => void; + + /** ํ˜„์žฌ ํ‘œ์‹œ ์‹œ์ž‘์ผ */ + viewStartDate: Date; + + /** ํ˜„์žฌ ํ‘œ์‹œ ์ข…๋ฃŒ์ผ */ + viewEndDate: Date; + + /** ์ด์ „์œผ๋กœ ์ด๋™ */ + goToPrevious: () => void; + + /** ๋‹ค์Œ์œผ๋กœ ์ด๋™ */ + goToNext: () => void; + + /** ์˜ค๋Š˜๋กœ ์ด๋™ */ + goToToday: () => void; + + /** ํŠน์ • ๋‚ ์งœ๋กœ ์ด๋™ */ + goToDate: (date: Date) => void; + + /** ์Šค์ผ€์ค„ ์—…๋ฐ์ดํŠธ */ + updateSchedule: (id: string, updates: Partial) => Promise; + + /** ์Šค์ผ€์ค„ ์ถ”๊ฐ€ */ + addSchedule: (schedule: Omit) => Promise; + + /** ์Šค์ผ€์ค„ ์‚ญ์ œ */ + deleteSchedule: (id: string) => Promise; + + /** ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ */ + refresh: () => void; +} + +/** + * ๋‚ ์งœ ์…€ ์ •๋ณด + */ +export interface DateCell { + /** ๋‚ ์งœ */ + date: Date; + /** ํ‘œ์‹œ ๋ผ๋ฒจ */ + label: string; + /** ์˜ค๋Š˜ ์—ฌ๋ถ€ */ + isToday: boolean; + /** ์ฃผ๋ง ์—ฌ๋ถ€ */ + isWeekend: boolean; + /** ์›” ์ฒซ์งธ๋‚  ์—ฌ๋ถ€ */ + isMonthStart: boolean; +} + +/** + * ์Šค์ผ€์ค„ ๋ฐ” ์œ„์น˜ ์ •๋ณด + */ +export interface ScheduleBarPosition { + /** ์™ผ์ชฝ ์˜คํ”„์…‹ (px) */ + left: number; + /** ๋„ˆ๋น„ (px) */ + width: number; + /** ์ƒ๋‹จ ์˜คํ”„์…‹ (px) */ + top: number; + /** ๋†’์ด (px) */ + height: number; +} diff --git a/frontend/lib/schemas/componentConfig.ts b/frontend/lib/schemas/componentConfig.ts index 8353ac05..82037cd0 100644 --- a/frontend/lib/schemas/componentConfig.ts +++ b/frontend/lib/schemas/componentConfig.ts @@ -148,9 +148,53 @@ export const componentV2Schema = z.object({ overrides: z.record(z.string(), z.any()).default({}), }); -export const layoutV2Schema = z.object({ - version: z.string().default("2.0"), +// ============================================ +// ๋ ˆ์ด์–ด ์Šคํ‚ค๋งˆ ์ •์˜ +// ============================================ +export const layerTypeSchema = z.enum(["base", "conditional", "modal", "drawer"]); + +export const layerSchema = z.object({ + id: z.string(), + name: z.string(), + type: layerTypeSchema, + zIndex: z.number().default(0), + isVisible: z.boolean().default(true), // ์ดˆ๊ธฐ ํ‘œ์‹œ ์—ฌ๋ถ€ + isLocked: z.boolean().default(false), // ํŽธ์ง‘ ์ž ๊ธˆ ์—ฌ๋ถ€ + + // ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ๋กœ์ง + condition: z + .object({ + targetComponentId: z.string(), + operator: z.enum(["eq", "neq", "in"]), + value: z.any(), + }) + .optional(), + + // ๋ชจ๋‹ฌ/๋“œ๋กœ์–ด ์ „์šฉ ์„ค์ • + overlayConfig: z + .object({ + backdrop: z.boolean().default(true), + closeOnBackdropClick: z.boolean().default(true), + width: z.union([z.string(), z.number()]).optional(), + height: z.union([z.string(), z.number()]).optional(), + // ๋ชจ๋‹ฌ/๋“œ๋กœ์–ด ์Šคํƒ€์ผ๋ง + backgroundColor: z.string().optional(), + backdropBlur: z.number().optional(), + // ๋“œ๋กœ์–ด ์ „์šฉ + position: z.enum(["left", "right", "top", "bottom"]).optional(), + }) + .optional(), + + // ํ•ด๋‹น ๋ ˆ์ด์–ด์— ์†ํ•œ ์ปดํฌ๋„ŒํŠธ๋“ค components: z.array(componentV2Schema).default([]), +}); + +export type Layer = z.infer; + +export const layoutV2Schema = z.object({ + version: z.string().default("2.1"), + layers: z.array(layerSchema).default([]), // ์‹ ๊ทœ ํ•„๋“œ + components: z.array(componentV2Schema).default([]), // ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€ updatedAt: z.string().optional(), screenResolution: z .object({ @@ -952,23 +996,78 @@ export function saveComponentV2(component: ComponentV2 & { config?: Record }> } { - const parsed = layoutV2Schema.parse(layoutData || { version: "2.0", components: [] }); +export function loadLayoutV2(layoutData: any): LayoutV2 & { + components: Array }>; + layers: Array }> }>; +} { + const parsed = layoutV2Schema.parse(layoutData || { version: "2.1", components: [], layers: [] }); + + // ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜: components๋งŒ ์žˆ๊ณ  layers๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ Default Layer ์ƒ์„ฑ + if ((!parsed.layers || parsed.layers.length === 0) && parsed.components && parsed.components.length > 0) { + const defaultLayer: Layer = { + id: "default-layer", + name: "๊ธฐ๋ณธ ๋ ˆ์ด์–ด", + type: "base", + zIndex: 0, + isVisible: true, + isLocked: false, + components: parsed.components, + }; + parsed.layers = [defaultLayer]; + } + + // ๋ชจ๋“  ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ ๋กœ๋“œ + const loadedLayers = parsed.layers.map((layer) => ({ + ...layer, + components: layer.components.map(loadComponentV2), + })); + + // ํ•˜์œ„ ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•œ components ๋ฐฐ์—ด (๋ชจ๋“  ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ ํ•ฉ์นจ) + const allComponents = loadedLayers.flatMap((layer) => layer.components); return { ...parsed, - components: parsed.components.map(loadComponentV2), + layers: loadedLayers, + components: allComponents, }; } // ============================================ // V2 ๋ ˆ์ด์•„์›ƒ ์ €์žฅ (์ „์ฒด ์ปดํฌ๋„ŒํŠธ ์ฐจ์ด๊ฐ’ ์ถ”์ถœ) // ============================================ -export function saveLayoutV2(components: Array }>): LayoutV2 { +export function saveLayoutV2( + components: Array }>, + layers?: Array }> }>, +): LayoutV2 { + // ๋ ˆ์ด์–ด๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ๋ ˆ์ด์–ด ๊ตฌ์กฐ ์ €์žฅ + if (layers && layers.length > 0) { + const savedLayers = layers.map((layer) => ({ + ...layer, + components: layer.components.map(saveComponentV2), + })); + + return { + version: "2.1", + layers: savedLayers, + components: savedLayers.flatMap((l) => l.components), // ํ•˜์œ„ ํ˜ธํ™˜์„ฑ + }; + } + + // ๋ ˆ์ด์–ด๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ (๊ธฐ์กด ๋ฐฉ์‹) - Default Layer๋กœ ๊ฐ์‹ธ์„œ ์ €์žฅ + const savedComponents = components.map(saveComponentV2); + const defaultLayer: Layer = { + id: "default-layer", + name: "๊ธฐ๋ณธ ๋ ˆ์ด์–ด", + type: "base", + zIndex: 0, + isVisible: true, + isLocked: false, + components: savedComponents, + }; + return { - version: "2.0", - components: components.map(saveComponentV2), + version: "2.1", + layers: [defaultLayer], + components: savedComponents, }; } diff --git a/frontend/lib/utils/alignmentUtils.ts b/frontend/lib/utils/alignmentUtils.ts new file mode 100644 index 00000000..e2af866e --- /dev/null +++ b/frontend/lib/utils/alignmentUtils.ts @@ -0,0 +1,265 @@ +/** + * ํ™”๋ฉด ๋””์ž์ด๋„ˆ ์ •๋ ฌ/๋ฐฐ๋ถ„/๋™์ผํฌ๊ธฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ + * + * ๋‹ค์ค‘ ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•ด ์ •๋ ฌ, ๊ท ๋“ฑ ๋ฐฐ๋ถ„, ๋™์ผ ํฌ๊ธฐ ๋งž์ถ”๊ธฐ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + */ + +import { ComponentData } from "@/types/screen"; + +// ์ •๋ ฌ ๋ชจ๋“œ ํƒ€์ž… +export type AlignMode = "left" | "centerX" | "right" | "top" | "centerY" | "bottom"; + +// ๋ฐฐ๋ถ„ ๋ฐฉํ–ฅ ํƒ€์ž… +export type DistributeDirection = "horizontal" | "vertical"; + +// ํฌ๊ธฐ ๋งž์ถ”๊ธฐ ๋ชจ๋“œ ํƒ€์ž… +export type MatchSizeMode = "width" | "height" | "both"; + +/** + * ์ปดํฌ๋„ŒํŠธ ์ •๋ ฌ + * ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ์ง€์ •๋œ ๋ฐฉํ–ฅ์œผ๋กœ ์ •๋ ฌํ•ฉ๋‹ˆ๋‹ค. + */ +export function alignComponents( + components: ComponentData[], + selectedIds: string[], + mode: AlignMode +): ComponentData[] { + const selected = components.filter((c) => selectedIds.includes(c.id)); + if (selected.length < 2) return components; + + let targetValue: number; + + switch (mode) { + case "left": + // ๊ฐ€์žฅ ์™ผ์ชฝ x๊ฐ’์œผ๋กœ ์ •๋ ฌ + targetValue = Math.min(...selected.map((c) => c.position.x)); + return components.map((c) => { + if (!selectedIds.includes(c.id)) return c; + return { ...c, position: { ...c.position, x: targetValue } }; + }); + + case "right": + // ๊ฐ€์žฅ ์˜ค๋ฅธ์ชฝ (x + width)๋กœ ์ •๋ ฌ + targetValue = Math.max(...selected.map((c) => c.position.x + (c.size?.width || 100))); + return components.map((c) => { + if (!selectedIds.includes(c.id)) return c; + const width = c.size?.width || 100; + return { ...c, position: { ...c.position, x: targetValue - width } }; + }); + + case "centerX": + // ๊ฐ€๋กœ ์ค‘์•™ ์ •๋ ฌ (์ „์ฒด ๋ฒ”์œ„์˜ ์ค‘์•™) + { + const minX = Math.min(...selected.map((c) => c.position.x)); + const maxX = Math.max(...selected.map((c) => c.position.x + (c.size?.width || 100))); + const centerX = (minX + maxX) / 2; + return components.map((c) => { + if (!selectedIds.includes(c.id)) return c; + const width = c.size?.width || 100; + return { ...c, position: { ...c.position, x: Math.round(centerX - width / 2) } }; + }); + } + + case "top": + // ๊ฐ€์žฅ ์œ„์ชฝ y๊ฐ’์œผ๋กœ ์ •๋ ฌ + targetValue = Math.min(...selected.map((c) => c.position.y)); + return components.map((c) => { + if (!selectedIds.includes(c.id)) return c; + return { ...c, position: { ...c.position, y: targetValue } }; + }); + + case "bottom": + // ๊ฐ€์žฅ ์•„๋ž˜์ชฝ (y + height)๋กœ ์ •๋ ฌ + targetValue = Math.max(...selected.map((c) => c.position.y + (c.size?.height || 40))); + return components.map((c) => { + if (!selectedIds.includes(c.id)) return c; + const height = c.size?.height || 40; + return { ...c, position: { ...c.position, y: targetValue - height } }; + }); + + case "centerY": + // ์„ธ๋กœ ์ค‘์•™ ์ •๋ ฌ (์ „์ฒด ๋ฒ”์œ„์˜ ์ค‘์•™) + { + const minY = Math.min(...selected.map((c) => c.position.y)); + const maxY = Math.max(...selected.map((c) => c.position.y + (c.size?.height || 40))); + const centerY = (minY + maxY) / 2; + return components.map((c) => { + if (!selectedIds.includes(c.id)) return c; + const height = c.size?.height || 40; + return { ...c, position: { ...c.position, y: Math.round(centerY - height / 2) } }; + }); + } + + default: + return components; + } +} + +/** + * ์ปดํฌ๋„ŒํŠธ ๊ท ๋“ฑ ๋ฐฐ๋ถ„ + * ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ๋“ค ๊ฐ„์˜ ๊ฐ„๊ฒฉ์„ ๊ท ๋“ฑํ•˜๊ฒŒ ๋ถ„๋ฐฐํ•ฉ๋‹ˆ๋‹ค. + * ์ตœ์†Œ 3๊ฐœ ์ด์ƒ์˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. + */ +export function distributeComponents( + components: ComponentData[], + selectedIds: string[], + direction: DistributeDirection +): ComponentData[] { + const selected = components.filter((c) => selectedIds.includes(c.id)); + if (selected.length < 3) return components; + + if (direction === "horizontal") { + // x ๊ธฐ์ค€ ์ •๋ ฌ + const sorted = [...selected].sort((a, b) => a.position.x - b.position.x); + const first = sorted[0]; + const last = sorted[sorted.length - 1]; + + // ์ฒซ ๋ฒˆ์งธ ~ ๋งˆ์ง€๋ง‰ ์ปดํฌ๋„ŒํŠธ ์‚ฌ์ด์˜ ์ด ๊ณต๊ฐ„ + const totalSpace = last.position.x + (last.size?.width || 100) - first.position.x; + // ์ปดํฌ๋„ŒํŠธ๋“ค์ด ์ฐจ์ง€ํ•˜๋Š” ์ด ๋„ˆ๋น„ + const totalComponentWidth = sorted.reduce((sum, c) => sum + (c.size?.width || 100), 0); + // ๊ท ๋“ฑ ๊ฐ„๊ฒฉ + const gap = (totalSpace - totalComponentWidth) / (sorted.length - 1); + + // ID -> ์ƒˆ x ์ขŒํ‘œ ๋งคํ•‘ + const newPositions = new Map(); + let currentX = first.position.x; + for (const comp of sorted) { + newPositions.set(comp.id, Math.round(currentX)); + currentX += (comp.size?.width || 100) + gap; + } + + return components.map((c) => { + if (!selectedIds.includes(c.id)) return c; + const newX = newPositions.get(c.id); + if (newX === undefined) return c; + return { ...c, position: { ...c.position, x: newX } }; + }); + } else { + // y ๊ธฐ์ค€ ์ •๋ ฌ + const sorted = [...selected].sort((a, b) => a.position.y - b.position.y); + const first = sorted[0]; + const last = sorted[sorted.length - 1]; + + const totalSpace = last.position.y + (last.size?.height || 40) - first.position.y; + const totalComponentHeight = sorted.reduce((sum, c) => sum + (c.size?.height || 40), 0); + const gap = (totalSpace - totalComponentHeight) / (sorted.length - 1); + + const newPositions = new Map(); + let currentY = first.position.y; + for (const comp of sorted) { + newPositions.set(comp.id, Math.round(currentY)); + currentY += (comp.size?.height || 40) + gap; + } + + return components.map((c) => { + if (!selectedIds.includes(c.id)) return c; + const newY = newPositions.get(c.id); + if (newY === undefined) return c; + return { ...c, position: { ...c.position, y: newY } }; + }); + } +} + +/** + * ์ปดํฌ๋„ŒํŠธ ๋™์ผ ํฌ๊ธฐ ๋งž์ถ”๊ธฐ + * ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ํฌ๊ธฐ๋ฅผ ์ฒซ ๋ฒˆ์งธ ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ๊ธฐ์ค€์œผ๋กœ ๋งž์ถฅ๋‹ˆ๋‹ค. + */ +export function matchComponentSize( + components: ComponentData[], + selectedIds: string[], + mode: MatchSizeMode, + referenceId?: string +): ComponentData[] { + const selected = components.filter((c) => selectedIds.includes(c.id)); + if (selected.length < 2) return components; + + // ๊ธฐ์ค€ ์ปดํฌ๋„ŒํŠธ (์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ์ฒซ ๋ฒˆ์งธ ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ) + const reference = referenceId + ? selected.find((c) => c.id === referenceId) || selected[0] + : selected[0]; + + const refWidth = reference.size?.width || 100; + const refHeight = reference.size?.height || 40; + + return components.map((c) => { + if (!selectedIds.includes(c.id)) return c; + + const currentWidth = c.size?.width || 100; + const currentHeight = c.size?.height || 40; + + let newWidth = currentWidth; + let newHeight = currentHeight; + + if (mode === "width" || mode === "both") { + newWidth = refWidth; + } + if (mode === "height" || mode === "both") { + newHeight = refHeight; + } + + return { + ...c, + size: { + ...c.size, + width: newWidth, + height: newHeight, + }, + }; + }); +} + +/** + * ์ปดํฌ๋„ŒํŠธ ๋ผ๋ฒจ ์ผ๊ด„ ํ† ๊ธ€ + * ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์˜ ๋ผ๋ฒจ ํ‘œ์‹œ/์ˆจ๊ธฐ๊ธฐ๋ฅผ ํ† ๊ธ€ํ•ฉ๋‹ˆ๋‹ค. + * ์ˆจ๊ฒจ์ง„ ๋ผ๋ฒจ์ด ํ•˜๋‚˜๋ผ๋„ ์žˆ์œผ๋ฉด ๋ชจ๋‘ ํ‘œ์‹œ, ๋ชจ๋‘ ํ‘œ์‹œ๋˜์–ด ์žˆ์œผ๋ฉด ๋ชจ๋‘ ์ˆจ๊ธฐ๊ธฐ + */ +export function toggleAllLabels(components: ComponentData[], forceShow?: boolean): ComponentData[] { + // ํ˜„์žฌ ๋ผ๋ฒจ์ด ์ˆจ๊ฒจ์ง„(labelDisplay === false) ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ + const hasHiddenLabel = components.some( + (c) => c.type === "widget" && (c.style as any)?.labelDisplay === false + ); + + // forceShow๊ฐ€ ์ง€์ •๋˜๋ฉด ๊ทธ ๊ฐ’ ์‚ฌ์šฉ, ์•„๋‹ˆ๋ฉด ์ž๋™ ํŒ๋‹จ + // ์ˆจ๊ฒจ์ง„ ๋ผ๋ฒจ์ด ์žˆ์œผ๋ฉด ๋ชจ๋‘ ํ‘œ์‹œ, ์•„๋‹ˆ๋ฉด ๋ชจ๋‘ ์ˆจ๊ธฐ๊ธฐ + const shouldShow = forceShow !== undefined ? forceShow : hasHiddenLabel; + + return components.map((c) => { + // ์œ„์ ฏ ํƒ€์ž…๋งŒ ๋ผ๋ฒจ ํ† ๊ธ€ ๋Œ€์ƒ + if (c.type !== "widget") return c; + + return { + ...c, + style: { + ...(c.style || {}), + labelDisplay: shouldShow, + } as any, + }; + }); +} + +/** + * ์ปดํฌ๋„ŒํŠธ nudge (ํ™”์‚ดํ‘œ ํ‚ค ์ด๋™) + * ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง€์ • ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. + */ +export function nudgeComponents( + components: ComponentData[], + selectedIds: string[], + direction: "up" | "down" | "left" | "right", + distance: number = 1 // ๊ธฐ๋ณธ 1px, Shift ๋ˆ„๋ฅด๋ฉด 10px +): ComponentData[] { + const dx = direction === "left" ? -distance : direction === "right" ? distance : 0; + const dy = direction === "up" ? -distance : direction === "down" ? distance : 0; + + return components.map((c) => { + if (!selectedIds.includes(c.id)) return c; + return { + ...c, + position: { + ...c.position, + x: Math.max(0, c.position.x + dx), + y: Math.max(0, c.position.y + dy), + }, + }; + }); +} diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 07ce0691..71a23472 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -8,6 +8,30 @@ import { ImprovedButtonActionExecutor } from "@/lib/utils/improvedButtonActionEx import { apiClient } from "@/lib/api/client"; import type { ExtendedControlContext } from "@/types/control-management"; +/** + * ๐Ÿ”ง formData ๋‚ด ๋ฐฐ์—ด ๊ฐ’์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + * PostgreSQL ๋ฐฐ์—ด ํ˜•์‹ ์ €์žฅ ๋ฐฉ์ง€ + */ +function normalizeFormDataArrays(formData: Record): Record { + if (!formData || typeof formData !== "object") return formData; + + const normalized: Record = {}; + for (const [key, value] of Object.entries(formData)) { + if (Array.isArray(value)) { + // ๋ฐฐ์—ด ๋‚ด ์ˆซ์ž๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ ํ›„ ์‰ผํ‘œ ๊ตฌ๋ถ„ + const stringValue = value + .map(v => typeof v === "number" ? String(v) : v) + .filter(v => v !== null && v !== undefined && v !== "") + .join(","); + console.log(`๐Ÿ”ง [normalizeFormDataArrays] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด: ${key}`, { original: value, converted: stringValue }); + normalized[key] = stringValue; + } else { + normalized[key] = value; + } + } + return normalized; +} + /** * ๋ฒ„ํŠผ ์•ก์…˜ ํƒ€์ž… ์ •์˜ */ @@ -30,7 +54,8 @@ export type ButtonActionType = | "operation_control" // ์šดํ–‰์•Œ๋ฆผ ๋ฐ ์ข…๋ฃŒ (์œ„์น˜ ์ˆ˜์ง‘ + ์ƒํƒœ ๋ณ€๊ฒฝ + ์—ฐ์† ์ถ”์ ) | "swap_fields" // ํ•„๋“œ ๊ฐ’ ๊ตํ™˜ (์ถœ๋ฐœ์ง€ โ†” ๋ชฉ์ ์ง€) | "transferData" // ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (์ปดํฌ๋„ŒํŠธ ๊ฐ„ or ํ™”๋ฉด ๊ฐ„) - | "quickInsert"; // ์ฆ‰์‹œ ์ €์žฅ (์„ ํƒํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ํŠน์ • ํ…Œ์ด๋ธ”์— ์ฆ‰์‹œ INSERT) + | "quickInsert" // ์ฆ‰์‹œ ์ €์žฅ (์„ ํƒํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ํŠน์ • ํ…Œ์ด๋ธ”์— ์ฆ‰์‹œ INSERT) + | "event"; // ์ด๋ฒคํŠธ ๋ฒ„์Šค๋กœ ์ด๋ฒคํŠธ ๋ฐœ์†ก (์Šค์ผ€์ค„ ์ƒ์„ฑ ๋“ฑ) /** * ๋ฒ„ํŠผ ์•ก์…˜ ์„ค์ • @@ -251,6 +276,12 @@ export interface ButtonActionConfig { successMessage?: string; // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ }; }; + + // ์ด๋ฒคํŠธ ๋ฒ„์Šค ๋ฐœ์†ก ๊ด€๋ จ (event ์•ก์…˜์šฉ) + eventConfig?: { + eventName: string; // ๋ฐœ์†กํ•  ์ด๋ฒคํŠธ ์ด๋ฆ„ (V2_EVENTS ํ‚ค) + eventPayload?: Record; // ์ด๋ฒคํŠธ ํŽ˜์ด๋กœ๋“œ (requestId๋Š” ์ž๋™ ์ƒ์„ฑ) + }; } /** @@ -416,6 +447,9 @@ export class ButtonActionExecutor { case "quickInsert": return await this.handleQuickInsert(config, context); + case "event": + return await this.handleEvent(config, context); + default: console.warn(`์ง€์›๋˜์ง€ ์•Š๋Š” ์•ก์…˜ ํƒ€์ž…: ${config.type}`); return false; @@ -474,6 +508,15 @@ export class ButtonActionExecutor { this.saveCallCount++; const callId = this.saveCallCount; + // ๐Ÿ”ง ๋””๋ฒ„๊ทธ: context.formData ํ™•์ธ (handleSave ์ง„์ž… ์‹œ์ ) + console.log("๐Ÿ” [handleSave] ์ง„์ž… ์‹œ context.formData:", { + keys: Object.keys(context.formData || {}), + hasCompanyImage: "company_image" in (context.formData || {}), + hasCompanyLogo: "company_logo" in (context.formData || {}), + companyImageValue: context.formData?.company_image, + companyLogoValue: context.formData?.company_logo, + }); + const { formData, originalData, tableName, screenId, onSave } = context; // ๐Ÿ†• ์ค‘๋ณต ํ˜ธ์ถœ ๋ฐฉ์ง€: ๊ฐ™์€ screenId + tableName + formData ์กฐํ•ฉ์œผ๋กœ 2์ดˆ ๋‚ด ์žฌํ˜ธ์ถœ ์‹œ ๋ฌด์‹œ @@ -514,6 +557,14 @@ export class ButtonActionExecutor { // ๐Ÿ†• ์ €์žฅ ์ „ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (SelectedItemsDetailInput ๋“ฑ์—์„œ ์ตœ์‹  ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘) // context.formData๋ฅผ ์ด๋ฒคํŠธ detail์— ํฌํ•จํ•˜์—ฌ ์ง์ ‘ ์ˆ˜์ • ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ // skipDefaultSave ํ”Œ๋ž˜๊ทธ๋ฅผ ํ†ตํ•ด ๊ธฐ๋ณธ ์ €์žฅ ๋กœ์ง์„ ๊ฑด๋„ˆ๋›ธ ์ˆ˜ ์žˆ์Œ + + // ๐Ÿ”ง ๋””๋ฒ„๊ทธ: beforeFormSave ์ด๋ฒคํŠธ ์ „ formData ํ™•์ธ + console.log("๐Ÿ” [handleSave] beforeFormSave ์ด๋ฒคํŠธ ์ „:", { + keys: Object.keys(context.formData || {}), + hasCompanyImage: "company_image" in (context.formData || {}), + companyImageValue: context.formData?.company_image, + }); + const beforeSaveEventDetail = { formData: context.formData, skipDefaultSave: false, @@ -529,6 +580,13 @@ export class ButtonActionExecutor { // ์•ฝ๊ฐ„์˜ ๋Œ€๊ธฐ ์‹œ๊ฐ„์„ ์ฃผ์–ด ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ formData๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ await new Promise((resolve) => setTimeout(resolve, 100)); + // ๐Ÿ”ง ๋””๋ฒ„๊ทธ: beforeFormSave ์ด๋ฒคํŠธ ํ›„ formData ํ™•์ธ + console.log("๐Ÿ” [handleSave] beforeFormSave ์ด๋ฒคํŠธ ํ›„:", { + keys: Object.keys(context.formData || {}), + hasCompanyImage: "company_image" in (context.formData || {}), + companyImageValue: context.formData?.company_image, + }); + // ๊ฒ€์ฆ ์‹คํŒจ ์‹œ ์ €์žฅ ์ค‘๋‹จ if (beforeSaveEventDetail.validationFailed) { console.log("โŒ [handleSave] ๊ฒ€์ฆ ์‹คํŒจ๋กœ ์ €์žฅ ์ค‘๋‹จ:", beforeSaveEventDetail.validationErrors); @@ -658,6 +716,10 @@ export class ButtonActionExecutor { return await this.handleBatchSave(config, context, selectedItemsKeys); } else { console.log("โš ๏ธ [handleSave] SelectedItemsDetailInput ๋ฐ์ดํ„ฐ ๊ฐ์ง€ ์‹คํŒจ - ์ผ๋ฐ˜ ์ €์žฅ ์ง„ํ–‰"); + // ๐Ÿ”ง ๋””๋ฒ„๊ทธ: formData ์ƒ์„ธ ํ™•์ธ + console.log("๐Ÿ” [handleSave] formData ํ‚ค ๋ชฉ๋ก:", Object.keys(context.formData || {})); + console.log("๐Ÿ” [handleSave] formData.company_image:", context.formData?.company_image); + console.log("๐Ÿ” [handleSave] formData.company_logo:", context.formData?.company_logo); console.log("โš ๏ธ [handleSave] formData ์ „์ฒด ๋‚ด์šฉ:", context.formData); } @@ -699,7 +761,9 @@ export class ButtonActionExecutor { for (const [fieldName, ruleId] of Object.entries(fieldsWithNumberingRepeater)) { try { - const allocateResult = await allocateNumberingCode(ruleId); + // ๐Ÿ†• ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘ํ•œ ๊ฐ’์„ ์ „๋‹ฌ (์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ถ”์ถœ์šฉ) + const userInputCode = context.formData[fieldName] as string; + const allocateResult = await allocateNumberingCode(ruleId, userInputCode, context.formData); if (allocateResult.success && allocateResult.data?.generatedCode) { const newCode = allocateResult.data.generatedCode; @@ -938,6 +1002,28 @@ export class ButtonActionExecutor { if (isUpdate) { // UPDATE ์ฒ˜๋ฆฌ - ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ ์‚ฌ์šฉ (์›๋ณธ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ) + // ๐Ÿ”ง UPDATE ์ „ formData ๋ฐฐ์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด ๋ณ€ํ™˜ (๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ) + for (const key of Object.keys(formData)) { + const value = formData[key]; + if (Array.isArray(value)) { + // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ์ธ์ง€ ํ™•์ธ (๊ฐ์ฒด ๋ฐฐ์—ด์ด๊ณ  _targetTable ๋˜๋Š” _isNewItem์ด ์žˆ์œผ๋ฉด ๋ฆฌํ”ผํ„ฐ) + const isRepeaterData = value.length > 0 && + typeof value[0] === "object" && + value[0] !== null && + ("_targetTable" in value[0] || "_isNewItem" in value[0] || "_existingRecord" in value[0]); + + if (!isRepeaterData) { + // ๐Ÿ”ง ๋‹ค์ค‘ ์„ ํƒ ๋ฐฐ์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + const stringValue = value + .map(v => typeof v === "number" ? String(v) : v) + .filter(v => v !== null && v !== undefined && v !== "") + .join(","); + console.log(`๐Ÿ”ง [handleSave UPDATE] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜: ${key}`, { original: value, converted: stringValue }); + formData[key] = stringValue; + } + } + } + if (hasRealOriginalData) { // ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ: ๋ณ€๊ฒฝ๋œ ํ•„๋“œ๋งŒ ์—…๋ฐ์ดํŠธ @@ -992,7 +1078,9 @@ export class ButtonActionExecutor { for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) { try { - const allocateResult = await allocateNumberingCode(ruleId); + // ๐Ÿ†• ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘ํ•œ ๊ฐ’์„ ์ „๋‹ฌ (์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ถ”์ถœ์šฉ) + const userInputCode = formData[fieldName] as string; + const allocateResult = await allocateNumberingCode(ruleId, userInputCode, formData); if (allocateResult.success && allocateResult.data?.generatedCode) { const newCode = allocateResult.data.generatedCode; @@ -1089,11 +1177,30 @@ export class ButtonActionExecutor { } } - // ๐Ÿ†• ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ(๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ) ์ œ๊ฑฐ - ๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ”์—๋Š” ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š์Œ + // ๐Ÿ†• ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ - ๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ”์—๋Š” ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š์Œ // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ๋Š” ๋ณ„๋„์˜ RepeaterFieldGroup/V2Repeater ์ €์žฅ ๋กœ์ง์—์„œ ์ฒ˜๋ฆฌ๋จ + // ๐Ÿ”ง ๋‹จ์ˆœ ๋ฐฐ์—ด(๋‹ค์ค‘ ์„ ํƒ)์€ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜, ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ๋ฐฐ์—ด์€ ์ œ๊ฑฐ for (const key of Object.keys(dataWithUserInfo)) { - if (Array.isArray(dataWithUserInfo[key])) { - delete dataWithUserInfo[key]; + const value = dataWithUserInfo[key]; + if (Array.isArray(value)) { + // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ์ธ์ง€ ํ™•์ธ (๊ฐ์ฒด ๋ฐฐ์—ด์ด๊ณ  _targetTable ๋˜๋Š” _isNewItem์ด ์žˆ์œผ๋ฉด ๋ฆฌํ”ผํ„ฐ) + const isRepeaterData = value.length > 0 && + typeof value[0] === "object" && + value[0] !== null && + ("_targetTable" in value[0] || "_isNewItem" in value[0] || "_existingRecord" in value[0]); + + if (isRepeaterData) { + // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ๋Š” ์ œ๊ฑฐ (๋ณ„๋„ ์ €์žฅ ๋กœ์ง์—์„œ ์ฒ˜๋ฆฌ) + delete dataWithUserInfo[key]; + } else { + // ๐Ÿ”ง ๋‹ค์ค‘ ์„ ํƒ ๋ฐฐ์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + const stringValue = value + .map(v => typeof v === "number" ? String(v) : v) + .filter(v => v !== null && v !== undefined && v !== "") + .join(","); + console.log(`๐Ÿ”ง [handleSave] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜: ${key}`, { original: value, converted: stringValue }); + dataWithUserInfo[key] = stringValue; + } } } @@ -1578,7 +1685,18 @@ export class ButtonActionExecutor { if (config.enableDataflowControl && config.dataflowConfig) { // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ (comp_๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ•„๋“œ์— JSON ๋ฐฐ์—ด์ด ์žˆ๋Š” ๊ฒฝ์šฐ) // ์ž…๊ณ  ํ™”๋ฉด ๋“ฑ์—์„œ ํ’ˆ๋ชฉ ๋ชฉ๋ก์ด comp_xxx ํ•„๋“œ์— JSON ๋ฌธ์ž์—ด๋กœ ์ €์žฅ๋จ - const formData: Record = (saveResult.data || context.formData || {}) as Record; + // ๐Ÿ”ง ์ˆ˜์ •: saveResult.data๊ฐ€ 3๋‹จ๊ณ„๋กœ ์ค‘์ฒฉ๋œ ๊ฒฝ์šฐ ์‹ค์ œ ํผ ๋ฐ์ดํ„ฐ ์ถ”์ถœ + // saveResult.data = API ์‘๋‹ต { success, data, message } + // saveResult.data.data = ์ €์žฅ๋œ ๋ ˆ์ฝ”๋“œ { id, screenId, tableName, data, createdAt... } + // saveResult.data.data.data = ์‹ค์ œ ํผ ๋ฐ์ดํ„ฐ { sabun, user_name... } + const savedRecord = saveResult?.data?.data || saveResult?.data || {}; + const actualFormData = savedRecord?.data || savedRecord; + const formData: Record = ( + Object.keys(actualFormData).length > 0 ? actualFormData : context.formData || {} + ) as Record; + console.log("๐Ÿ“ฆ [executeAfterSaveControl] savedRecord ๊ตฌ์กฐ:", Object.keys(savedRecord)); + console.log("๐Ÿ“ฆ [executeAfterSaveControl] actualFormData ์ถ”์ถœ:", Object.keys(formData)); + console.log("๐Ÿ“ฆ [executeAfterSaveControl] formData.sabun:", formData.sabun); let parsedSectionData: any[] = []; // comp_๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ•„๋“œ์—์„œ ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ์ฐพ๊ธฐ @@ -2016,7 +2134,9 @@ export class ButtonActionExecutor { for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) { try { - const allocateResult = await allocateNumberingCode(ruleId); + // ๐Ÿ†• ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘ํ•œ ๊ฐ’์„ ์ „๋‹ฌ (์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ถ”์ถœ์šฉ) + const userInputCode = commonFieldsData[fieldName] as string; + const allocateResult = await allocateNumberingCode(ruleId, userInputCode, formData); if (allocateResult.success && allocateResult.data?.generatedCode) { const newCode = allocateResult.data.generatedCode; @@ -2871,8 +2991,7 @@ export class ButtonActionExecutor { if (v2ListComponent) { dataSourceId = - v2ListComponent.componentConfig.dataSource?.table || - v2ListComponent.componentConfig.tableName; + v2ListComponent.componentConfig.dataSource?.table || v2ListComponent.componentConfig.tableName; console.log("โœจ V2List ์ž๋™ ๊ฐ์ง€:", { componentId: v2ListComponent.id, tableName: dataSourceId, @@ -3005,9 +3124,13 @@ export class ButtonActionExecutor { } // 4. ๋ชจ๋‹ฌ ์—ด๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์ƒ - // passSelectedData๊ฐ€ true์ด๋ฉด editData๋กœ ์ „๋‹ฌ (์ˆ˜์ • ๋ชจ๋“œ์ฒ˜๋Ÿผ ๋ชจ๋“  ํ•„๋“œ ํ‘œ์‹œ) + // ๐Ÿ”ง ์ˆ˜์ •: openModalWithData๋Š” "์‹ ๊ทœ ๋“ฑ๋ก + ์—ฐ๊ฒฐ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ"์šฉ์ด๋ฏ€๋กœ + // editData๊ฐ€ ์•„๋‹Œ splitPanelParentData๋กœ ์ „๋‹ฌํ•ด์•ผ ์ฑ„๋ฒˆ ๋“ฑ์ด ์ •์ƒ ์ž‘๋™ํ•จ const isPassDataMode = passSelectedData && selectedData.length > 0; + // ๐Ÿ”ง isEditMode ์˜ต์…˜์ด ๋ช…์‹œ์ ์œผ๋กœ true์ธ ๊ฒฝ์šฐ์—๋งŒ ์ˆ˜์ • ๋ชจ๋“œ๋กœ ์ฒ˜๋ฆฌ + const useAsEditData = config.isEditMode === true; + const modalEvent = new CustomEvent("openScreenModal", { detail: { screenId: config.targetScreenId, @@ -3016,19 +3139,18 @@ export class ButtonActionExecutor { size: config.modalSize || "md", selectedData: selectedData, selectedIds: selectedData.map((row: any) => row.id).filter(Boolean), - // ๐Ÿ†• ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๋ชจ๋“œ์ผ ๋•Œ๋Š” editData๋กœ ์ „๋‹ฌํ•˜์—ฌ ๋ชจ๋“  ํ•„๋“œ๊ฐ€ ํ‘œ์‹œ๋˜๋„๋ก ํ•จ - editData: isPassDataMode ? parentData : undefined, - splitPanelParentData: isPassDataMode ? undefined : parentData, + // ๐Ÿ”ง ์ˆ˜์ •: isEditMode๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ true์ธ ๊ฒฝ์šฐ์—๋งŒ editData๋กœ ์ „๋‹ฌ + // ๊ธฐ๋ณธ์ ์œผ๋กœ๋Š” splitPanelParentData๋กœ ์ „๋‹ฌํ•˜์—ฌ ์‹ ๊ทœ ๋“ฑ๋ก + ์—ฐ๊ฒฐ ๋ฐ์ดํ„ฐ ๋ชจ๋“œ + editData: useAsEditData && isPassDataMode ? parentData : undefined, + splitPanelParentData: isPassDataMode ? parentData : undefined, urlParams: dataSourceId ? { dataSourceId } : undefined, }, }); window.dispatchEvent(modalEvent); - // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ (autoDetectDataSource ๋ชจ๋“œ์—์„œ๋งŒ) - if (autoDetectDataSource && config.successMessage) { - toast.success(config.successMessage); - } + // ๋ชจ๋‹ฌ ์—ด๊ธฐ๋Š” UI ์ „ํ™˜์ด๋ฏ€๋กœ ์„ฑ๊ณต ํ† ์ŠคํŠธ๋ฅผ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ + // (์ €์žฅ ๋“ฑ ์‹ค์ œ ์•ก์…˜ ์™„๋ฃŒ ์‹œ์—๋งŒ ํ† ์ŠคํŠธ ํ‘œ์‹œ) return true; } @@ -3217,8 +3339,7 @@ export class ButtonActionExecutor { window.dispatchEvent(modalEvent); - // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ (๊ฐ„๋‹จํ•˜๊ฒŒ) - toast.success(config.successMessage || "๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค."); + // ๋ชจ๋‹ฌ ์—ด๊ธฐ๋Š” UI ์ „ํ™˜์ด๋ฏ€๋กœ ์„ฑ๊ณต ํ† ์ŠคํŠธ๋ฅผ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ return true; } else { @@ -3445,10 +3566,13 @@ export class ButtonActionExecutor { const screenModalEvent = new CustomEvent("openScreenModal", { detail: { screenId: config.targetScreenId, - title: config.editModalTitle || "๋ฐ์ดํ„ฐ ์ˆ˜์ •", + title: isCreateMode ? config.editModalTitle || "๋ฐ์ดํ„ฐ ๋ณต์‚ฌ" : config.editModalTitle || "๋ฐ์ดํ„ฐ ์ˆ˜์ •", description: description, size: config.modalSize || "lg", - editData: rowData, // ๐Ÿ†• ์ˆ˜์ • ๋ฐ์ดํ„ฐ ์ „๋‹ฌ + // ๐Ÿ”ง ๋ณต์‚ฌ ๋ชจ๋“œ์—์„œ๋Š” editData ๋Œ€์‹  splitPanelParentData๋กœ ์ „๋‹ฌํ•˜์—ฌ ์ฑ„๋ฒˆ์ด ์ƒ์„ฑ๋˜๋„๋ก ํ•จ + editData: isCreateMode ? undefined : rowData, + splitPanelParentData: isCreateMode ? rowData : undefined, + isCreateMode: isCreateMode, // ๐Ÿ†• ๋ณต์‚ฌ ๋ชจ๋“œ ํ”Œ๋ž˜๊ทธ ์ „๋‹ฌ }, }); window.dispatchEvent(screenModalEvent); @@ -3805,7 +3929,8 @@ export class ButtonActionExecutor { case "form": if (context.formData && Object.keys(context.formData).length > 0) { - sourceData = [context.formData]; + // ๐Ÿ”ง ๋ฐฐ์—ด ๊ฐ’์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + sourceData = [normalizeFormDataArrays(context.formData)]; } else { console.warn("โš ๏ธ form ๋ชจ๋“œ์ด์ง€๋งŒ ํผ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); } @@ -3850,7 +3975,8 @@ export class ButtonActionExecutor { dataSourceType = "table-selection"; } } else if (context.formData && Object.keys(context.formData).length > 0) { - sourceData = [context.formData]; + // ๐Ÿ”ง ๋ฐฐ์—ด ๊ฐ’์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + sourceData = [normalizeFormDataArrays(context.formData)]; dataSourceType = "form"; } break; @@ -3976,16 +4102,27 @@ export class ButtonActionExecutor { const { executeNodeFlow } = await import("@/lib/api/nodeFlows"); // ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ค€๋น„: context-data ๋ชจ๋“œ๋Š” ๋ฐฐ์—ด์„ ๊ธฐ๋Œ€ํ•จ - // ์šฐ์„ ์ˆœ์œ„: selectedRowsData > savedData > formData - // - selectedRowsData: ํ…Œ์ด๋ธ” ์„น์…˜์—์„œ ์ €์žฅ๋œ ํ•˜์œ„ ํ•ญ๋ชฉ๋“ค (item_code, inbound_qty ๋“ฑ ํฌํ•จ) - // - savedData: ์ €์žฅ API ์‘๋‹ต ๋ฐ์ดํ„ฐ - // - formData: ํผ์— ์ž…๋ ฅ๋œ ๋ฐ์ดํ„ฐ + // ๐Ÿ”ง ์ €์žฅ ํ›„ ์ œ์–ด: savedData > formData > selectedRowsData + // - ์ €์žฅ ํ›„ ์ œ์–ด์—์„œ๋Š” ๋ฐฉ๊ธˆ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ(savedData)๊ฐ€ ๊ฐ€์žฅ ์ค‘์š”! + // - selectedRowsData๋Š” ์™ผ์ชฝ ํŒจ๋„ ์„ ํƒ ๋ฐ์ดํ„ฐ์ผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋งˆ์ง€๋ง‰ ์ˆœ์œ„ let sourceData: any[]; - if (context.selectedRowsData && context.selectedRowsData.length > 0) { + if (context.savedData) { + // ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ (์ €์žฅ API ์‘๋‹ต) + sourceData = Array.isArray(context.savedData) ? context.savedData : [context.savedData]; + console.log("๐Ÿ“ฆ [executeAfterSaveControl] savedData ์‚ฌ์šฉ:", sourceData); + console.log("๐Ÿ“ฆ [executeAfterSaveControl] savedData ํ•„๋“œ:", Object.keys(context.savedData)); + console.log("๐Ÿ“ฆ [executeAfterSaveControl] savedData.sabun:", context.savedData.sabun); + } else if (context.formData && Object.keys(context.formData).length > 0) { + // ํผ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ (๐Ÿ”ง ๋ฐฐ์—ด ๊ฐ’์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜) + sourceData = [normalizeFormDataArrays(context.formData)]; + console.log("๐Ÿ“ฆ [executeAfterSaveControl] formData ์‚ฌ์šฉ:", sourceData); + } else if (context.selectedRowsData && context.selectedRowsData.length > 0) { + // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ (๋งˆ์ง€๋ง‰ ์ˆœ์œ„) sourceData = context.selectedRowsData; + console.log("๐Ÿ“ฆ [executeAfterSaveControl] selectedRowsData ์‚ฌ์šฉ:", sourceData); } else { - const savedData = context.savedData || context.formData || {}; - sourceData = Array.isArray(savedData) ? savedData : [savedData]; + sourceData = []; + console.warn("โš ๏ธ [executeAfterSaveControl] ๋ฐ์ดํ„ฐ ์†Œ์Šค ์—†์Œ!"); } let allSuccess = true; @@ -4085,16 +4222,25 @@ export class ButtonActionExecutor { const { executeNodeFlow } = await import("@/lib/api/nodeFlows"); // ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ค€๋น„: context-data ๋ชจ๋“œ๋Š” ๋ฐฐ์—ด์„ ๊ธฐ๋Œ€ํ•จ - // ์šฐ์„ ์ˆœ์œ„: selectedRowsData > savedData > formData - // - selectedRowsData: ํ…Œ์ด๋ธ” ์„น์…˜์—์„œ ์ €์žฅ๋œ ํ•˜์œ„ ํ•ญ๋ชฉ๋“ค (item_code, inbound_qty ๋“ฑ ํฌํ•จ) - // - savedData: ์ €์žฅ API ์‘๋‹ต ๋ฐ์ดํ„ฐ - // - formData: ํผ์— ์ž…๋ ฅ๋œ ๋ฐ์ดํ„ฐ + // ๐Ÿ”ง ์ €์žฅ ํ›„ ์ œ์–ด: savedData > formData > selectedRowsData + // - ์ €์žฅ ํ›„ ์ œ์–ด์—์„œ๋Š” ๋ฐฉ๊ธˆ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ(savedData)๊ฐ€ ๊ฐ€์žฅ ์ค‘์š”! + // - selectedRowsData๋Š” ์™ผ์ชฝ ํŒจ๋„ ์„ ํƒ ๋ฐ์ดํ„ฐ์ผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋งˆ์ง€๋ง‰ ์ˆœ์œ„ let sourceData: any[]; - if (context.selectedRowsData && context.selectedRowsData.length > 0) { + if (context.savedData) { + // ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ (์ €์žฅ API ์‘๋‹ต) + sourceData = Array.isArray(context.savedData) ? context.savedData : [context.savedData]; + console.log("๐Ÿ“ฆ [executeSingleFlowControl] savedData ์‚ฌ์šฉ:", sourceData); + } else if (context.formData && Object.keys(context.formData).length > 0) { + // ํผ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ (๐Ÿ”ง ๋ฐฐ์—ด ๊ฐ’์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜) + sourceData = [normalizeFormDataArrays(context.formData)]; + console.log("๐Ÿ“ฆ [executeSingleFlowControl] formData ์‚ฌ์šฉ:", sourceData); + } else if (context.selectedRowsData && context.selectedRowsData.length > 0) { + // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ (๋งˆ์ง€๋ง‰ ์ˆœ์œ„) sourceData = context.selectedRowsData; + console.log("๐Ÿ“ฆ [executeSingleFlowControl] selectedRowsData ์‚ฌ์šฉ:", sourceData); } else { - const savedData = context.savedData || context.formData || {}; - sourceData = Array.isArray(savedData) ? savedData : [savedData]; + sourceData = []; + console.warn("โš ๏ธ [executeSingleFlowControl] ๋ฐ์ดํ„ฐ ์†Œ์Šค ์—†์Œ!"); } // repeat-screen-modal ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋ณ‘ํ•ฉ @@ -7010,6 +7156,52 @@ export class ButtonActionExecutor { errors, }; } + + /** + * ์ด๋ฒคํŠธ ๋ฒ„์Šค๋กœ ์ด๋ฒคํŠธ ๋ฐœ์†ก (์Šค์ผ€์ค„ ์ƒ์„ฑ ๋“ฑ) + */ + private static async handleEvent(config: ButtonActionConfig, context: ButtonActionContext): Promise { + try { + const { eventConfig } = config; + + if (!eventConfig?.eventName) { + toast.error("์ด๋ฒคํŠธ ์„ค์ •์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + console.error("[handleEvent] eventName์ด ์„ค์ •๋˜์ง€ ์•Š์Œ", { config }); + return false; + } + + // V2_EVENTS์—์„œ ์ด๋ฒคํŠธ ์ด๋ฆ„ ๊ฐ€์ ธ์˜ค๊ธฐ + const { v2EventBus, V2_EVENTS } = await import("@/lib/v2-core"); + + // ์ด๋ฒคํŠธ ์ด๋ฆ„ ๊ฒ€์ฆ + const eventName = eventConfig.eventName as keyof typeof V2_EVENTS; + if (!V2_EVENTS[eventName]) { + toast.error(`์•Œ ์ˆ˜ ์—†๋Š” ์ด๋ฒคํŠธ: ${eventConfig.eventName}`); + console.error("[handleEvent] ์•Œ ์ˆ˜ ์—†๋Š” ์ด๋ฒคํŠธ", { eventName, V2_EVENTS }); + return false; + } + + // ํŽ˜์ด๋กœ๋“œ ๊ตฌ์„ฑ + const eventPayload = { + requestId: crypto.randomUUID(), + ...eventConfig.eventPayload, + }; + + console.log("[handleEvent] ์ด๋ฒคํŠธ ๋ฐœ์†ก:", { + eventName: V2_EVENTS[eventName], + payload: eventPayload, + }); + + // ์ด๋ฒคํŠธ ๋ฐœ์†ก + v2EventBus.emit(V2_EVENTS[eventName], eventPayload); + + return true; + } catch (error) { + console.error("[handleEvent] ์ด๋ฒคํŠธ ๋ฐœ์†ก ์˜ค๋ฅ˜:", error); + toast.error("์ด๋ฒคํŠธ ๋ฐœ์†ก ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + return false; + } + } } /** @@ -7038,7 +7230,7 @@ export const DEFAULT_BUTTON_ACTIONS: Record; }; + // ๐Ÿ†• ํ”Œ๋กœ์šฐ ๊ธฐ๋ฐ˜ ์ œ์–ด ์„ค์ • + flowConfig?: { + flowId: number; + flowName: string; + executionTiming: "before" | "after" | "replace"; + contextData?: Record; + }; } interface ExecutionPlan { @@ -163,15 +170,22 @@ export class ImprovedButtonActionExecutor { return plan; } - // enableDataflowControl ์ฒดํฌ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  dataflowConfig๋งŒ ์žˆ์œผ๋ฉด ์‹คํ–‰ + // ๐Ÿ”ง controlMode๊ฐ€ ์—†์œผ๋ฉด flowConfig/relationshipConfig ์กด์žฌ ์—ฌ๋ถ€๋กœ ์ž๋™ ํŒ๋‹จ + const effectiveControlMode = dataflowConfig.controlMode + || (dataflowConfig.flowConfig ? "flow" : null) + || (dataflowConfig.relationshipConfig ? "relationship" : null) + || "none"; + console.log("๐Ÿ“‹ ์‹คํ–‰ ๊ณ„ํš ์ƒ์„ฑ:", { controlMode: dataflowConfig.controlMode, + effectiveControlMode, + hasFlowConfig: !!dataflowConfig.flowConfig, hasRelationshipConfig: !!dataflowConfig.relationshipConfig, enableDataflowControl: buttonConfig.enableDataflowControl, }); - // ๊ด€๊ณ„ ๊ธฐ๋ฐ˜ ์ œ์–ด๋งŒ ์ง€์› - if (dataflowConfig.controlMode === "relationship" && dataflowConfig.relationshipConfig) { + // ๊ด€๊ณ„ ๊ธฐ๋ฐ˜ ์ œ์–ด + if (effectiveControlMode === "relationship" && dataflowConfig.relationshipConfig) { const control: ControlConfig = { type: "relationship", relationshipConfig: dataflowConfig.relationshipConfig, @@ -191,11 +205,34 @@ export class ImprovedButtonActionExecutor { } } + // ๐Ÿ†• ํ”Œ๋กœ์šฐ ๊ธฐ๋ฐ˜ ์ œ์–ด + if (effectiveControlMode === "flow" && dataflowConfig.flowConfig) { + const control: ControlConfig = { + type: "flow", + flowConfig: dataflowConfig.flowConfig, + }; + + console.log("๐Ÿ“‹ ํ”Œ๋กœ์šฐ ์ œ์–ด ์„ค์ •:", dataflowConfig.flowConfig); + + switch (dataflowConfig.flowConfig.executionTiming) { + case "before": + plan.beforeControls.push(control); + break; + case "after": + plan.afterControls.push(control); + break; + case "replace": + plan.afterControls.push(control); + plan.hasReplaceControl = true; + break; + } + } + return plan; } /** - * ๐Ÿ”ฅ ์ œ์–ด ์‹คํ–‰ (๊ด€๊ณ„ ๋˜๋Š” ์™ธ๋ถ€ํ˜ธ์ถœ) + * ๐Ÿ”ฅ ์ œ์–ด ์‹คํ–‰ (๊ด€๊ณ„ ๋˜๋Š” ํ”Œ๋กœ์šฐ) */ private static async executeControls( controls: ControlConfig[], @@ -206,8 +243,16 @@ export class ImprovedButtonActionExecutor { for (const control of controls) { try { - // ๊ด€๊ณ„ ์‹คํ–‰๋งŒ ์ง€์› - const result = await this.executeRelationship(control.relationshipConfig, formData, context); + let result: ExecutionResult; + + // ๐Ÿ†• ์ œ์–ด ํƒ€์ž…์— ๋”ฐ๋ผ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ + if (control.type === "flow" && control.flowConfig) { + result = await this.executeFlow(control.flowConfig, formData, context); + } else if (control.type === "relationship" && control.relationshipConfig) { + result = await this.executeRelationship(control.relationshipConfig, formData, context); + } else { + throw new Error(`์ง€์›ํ•˜์ง€ ์•Š๋Š” ์ œ์–ด ํƒ€์ž…: ${control.type}`); + } results.push(result); @@ -215,7 +260,7 @@ export class ImprovedButtonActionExecutor { if (!result.success) { throw new Error(result.message); } - } catch (error) { + } catch (error: any) { console.error(`์ œ์–ด ์‹คํ–‰ ์‹คํŒจ (${control.type}):`, error); results.push({ success: false, @@ -230,6 +275,61 @@ export class ImprovedButtonActionExecutor { return results; } + /** + * ๐Ÿ†• ํ”Œ๋กœ์šฐ ์‹คํ–‰ + */ + private static async executeFlow( + config: { + flowId: number; + flowName: string; + executionTiming: "before" | "after" | "replace"; + contextData?: Record; + }, + formData: Record, + context: ButtonExecutionContext, + ): Promise { + const startTime = Date.now(); + + try { + console.log(`๐Ÿ”„ ํ”Œ๋กœ์šฐ ์‹คํ–‰ ์‹œ์ž‘: ${config.flowName} (ID: ${config.flowId})`); + + // ํ”Œ๋กœ์šฐ ์‹คํ–‰ API ํ˜ธ์ถœ + const response = await apiClient.post(`/api/dataflow/node-flows/${config.flowId}/execute`, { + formData, + contextData: config.contextData || {}, + selectedRows: context.selectedRows || [], + flowSelectedData: context.flowSelectedData || [], + screenId: context.screenId, + companyCode: context.companyCode, + userId: context.userId, + }); + + const executionTime = Date.now() - startTime; + + if (response.data?.success) { + console.log(`โœ… ํ”Œ๋กœ์šฐ ์‹คํ–‰ ์„ฑ๊ณต: ${config.flowName}`, response.data); + return { + success: true, + message: `ํ”Œ๋กœ์šฐ "${config.flowName}" ์‹คํ–‰ ์™„๋ฃŒ`, + executionTime, + data: response.data, + }; + } else { + throw new Error(response.data?.message || "ํ”Œ๋กœ์šฐ ์‹คํ–‰ ์‹คํŒจ"); + } + } catch (error: any) { + const executionTime = Date.now() - startTime; + console.error(`โŒ ํ”Œ๋กœ์šฐ ์‹คํ–‰ ์‹คํŒจ: ${config.flowName}`, error); + + return { + success: false, + message: `ํ”Œ๋กœ์šฐ "${config.flowName}" ์‹คํ–‰ ์‹คํŒจ: ${error.message}`, + executionTime, + error: error.message, + }; + } + } + /** * ๐Ÿ”ฅ ๊ด€๊ณ„ ์‹คํ–‰ */ diff --git a/frontend/lib/utils/layoutV2Converter.ts b/frontend/lib/utils/layoutV2Converter.ts index d1d7487d..fff56bf9 100644 --- a/frontend/lib/utils/layoutV2Converter.ts +++ b/frontend/lib/utils/layoutV2Converter.ts @@ -33,6 +33,109 @@ interface LegacyLayoutData { metadata?: any; } +// ============================================ +// ์ค‘์ฒฉ ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ณธ๊ฐ’ ์ ์šฉ ํ—ฌํผ ํ•จ์ˆ˜ (์žฌ๊ท€์ ) +// ============================================ +function applyDefaultsToNestedComponents(components: any[]): any[] { + if (!Array.isArray(components)) return components; + + return components.map((nestedComp: any) => { + if (!nestedComp) return nestedComp; + + // ์ค‘์ฒฉ ์ปดํฌ๋„ŒํŠธ์˜ ํƒ€์ž… ํ™•์ธ (componentType ๋˜๋Š” url์—์„œ ์ถ”์ถœ) + let nestedComponentType = nestedComp.componentType; + if (!nestedComponentType && nestedComp.url) { + nestedComponentType = getComponentTypeFromUrl(nestedComp.url); + } + + // ๊ฒฐ๊ณผ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” (์›๋ณธ ๋ณต์‚ฌ) + const result = { ...nestedComp }; + + // ๐Ÿ†• ํƒญ ์œ„์ ฏ์ธ ๊ฒฝ์šฐ ์žฌ๊ท€์ ์œผ๋กœ ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋„ ์ฒ˜๋ฆฌ + if (nestedComponentType === "v2-tabs-widget") { + const config = result.componentConfig || {}; + if (config.tabs && Array.isArray(config.tabs)) { + result.componentConfig = { + ...config, + tabs: config.tabs.map((tab: any) => { + if (tab?.components && Array.isArray(tab.components)) { + return { + ...tab, + components: applyDefaultsToNestedComponents(tab.components), + }; + } + return tab; + }), + }; + } + } + + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„์ธ ๊ฒฝ์šฐ ์žฌ๊ท€์ ์œผ๋กœ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋„ ์ฒ˜๋ฆฌ + if (nestedComponentType === "v2-split-panel-layout") { + const config = result.componentConfig || {}; + result.componentConfig = { + ...config, + leftPanel: config.leftPanel + ? { + ...config.leftPanel, + components: applyDefaultsToNestedComponents(config.leftPanel.components || []), + } + : config.leftPanel, + rightPanel: config.rightPanel + ? { + ...config.rightPanel, + components: applyDefaultsToNestedComponents(config.rightPanel.components || []), + } + : config.rightPanel, + }; + } + + // ์ปดํฌ๋„ŒํŠธ ํƒ€์ž…์ด ์—†์œผ๋ฉด ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ + if (!nestedComponentType) { + return result; + } + + // ์ค‘์ฒฉ ์ปดํฌ๋„ŒํŠธ์˜ ๊ธฐ๋ณธ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ + const nestedDefaults = getDefaultsByUrl(`registry://${nestedComponentType}`); + + // componentConfig๊ฐ€ ์žˆ์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’๊ณผ ๋ณ‘ํ•ฉ + if (result.componentConfig && Object.keys(nestedDefaults).length > 0) { + const mergedNestedConfig = mergeComponentConfig(nestedDefaults, result.componentConfig); + return { + ...result, + componentConfig: mergedNestedConfig, + }; + } + + return result; + }); +} + +// ============================================ +// ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋ณธ๊ฐ’ ์ ์šฉ +// ============================================ +function applyDefaultsToSplitPanelComponents(mergedConfig: Record): Record { + const result = { ...mergedConfig }; + + // leftPanel.components ์ฒ˜๋ฆฌ + if (result.leftPanel?.components) { + result.leftPanel = { + ...result.leftPanel, + components: applyDefaultsToNestedComponents(result.leftPanel.components), + }; + } + + // rightPanel.components ์ฒ˜๋ฆฌ + if (result.rightPanel?.components) { + result.rightPanel = { + ...result.rightPanel, + components: applyDefaultsToNestedComponents(result.rightPanel.components), + }; + } + + return result; +} + // ============================================ // V2 โ†’ Legacy ๋ณ€ํ™˜ (๋กœ๋“œ ์‹œ) // ============================================ @@ -44,7 +147,28 @@ export function convertV2ToLegacy(v2Layout: LayoutV2 | null): LegacyLayoutData | const components: LegacyComponentData[] = v2Layout.components.map((comp) => { const componentType = getComponentTypeFromUrl(comp.url); const defaults = getDefaultsByUrl(comp.url); - const mergedConfig = mergeComponentConfig(defaults, comp.overrides); + let mergedConfig = mergeComponentConfig(defaults, comp.overrides); + + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„์ธ ๊ฒฝ์šฐ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ์—๋„ ๊ธฐ๋ณธ๊ฐ’ ์ ์šฉ + if (componentType === "v2-split-panel-layout") { + mergedConfig = applyDefaultsToSplitPanelComponents(mergedConfig); + } + + // ๐Ÿ†• ํƒญ ์œ„์ ฏ์ธ ๊ฒฝ์šฐ ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ์—๋„ ๊ธฐ๋ณธ๊ฐ’ ์ ์šฉ + if (componentType === "v2-tabs-widget" && mergedConfig.tabs) { + mergedConfig = { + ...mergedConfig, + tabs: mergedConfig.tabs.map((tab: any) => { + if (tab?.components) { + return { + ...tab, + components: applyDefaultsToNestedComponents(tab.components), + }; + } + return tab; + }), + }; + } // ๐Ÿ†• overrides์—์„œ ์ƒ์œ„ ๋ ˆ๋ฒจ ์†์„ฑ๋“ค ์ถ”์ถœ const overrides = comp.overrides || {}; @@ -63,11 +187,17 @@ export function convertV2ToLegacy(v2Layout: LayoutV2 | null): LegacyLayoutData | label: overrides.label || mergedConfig.label || "", // ๋ผ๋ฒจ์ด ์—†์œผ๋ฉด ๋นˆ ๋ฌธ์ž์—ด required: overrides.required, readonly: overrides.readonly, + hidden: overrides.hidden, // ๐Ÿ†• ์ˆจ๊น€ ์„ค์ • ๋ณต์› codeCategory: overrides.codeCategory, inputType: overrides.inputType, webType: overrides.webType, + // ๐Ÿ†• autoFill ์„ค์ • ๋ณต์› (์ž๋™ ์ž…๋ ฅ ๊ธฐ๋Šฅ) + autoFill: overrides.autoFill, + // ๐Ÿ†• style ์„ค์ • ๋ณต์› (๋ผ๋ฒจ ํ…์ŠคํŠธ, ๋ผ๋ฒจ ์Šคํƒ€์ผ ๋“ฑ) + style: overrides.style || {}, + // ๐Ÿ”ง webTypeConfig ๋ณต์› (๋ฒ„ํŠผ ์ œ์–ด๊ธฐ๋Šฅ, ํ”Œ๋กœ์šฐ ๊ฐ€์‹œ์„ฑ ๋“ฑ) + webTypeConfig: overrides.webTypeConfig || {}, // ๊ธฐ์กด ๊ตฌ์กฐ ํ˜ธํ™˜์„ ์œ„ํ•œ ์ถ”๊ฐ€ ํ•„๋“œ - style: {}, parentId: null, gridColumns: 12, gridRowIndex: 0, @@ -109,19 +239,59 @@ export function convertLegacyToV2(legacyLayout: LegacyLayoutData): LayoutV2 { const topLevelProps: Record = {}; if (comp.tableName) topLevelProps.tableName = comp.tableName; if (comp.columnName) topLevelProps.columnName = comp.columnName; - if (comp.label) topLevelProps.label = comp.label; + // ๐Ÿ”ง label์€ ๋นˆ ๋ฌธ์ž์—ด๋„ ์ €์žฅ (๋ผ๋ฒจ ์‚ญ์ œ ์ง€์›) + if (comp.label !== undefined) topLevelProps.label = comp.label; if (comp.required !== undefined) topLevelProps.required = comp.required; if (comp.readonly !== undefined) topLevelProps.readonly = comp.readonly; + if (comp.hidden !== undefined) topLevelProps.hidden = comp.hidden; // ๐Ÿ†• ์ˆจ๊น€ ์„ค์ • ์ €์žฅ if (comp.codeCategory) topLevelProps.codeCategory = comp.codeCategory; if (comp.inputType) topLevelProps.inputType = comp.inputType; if (comp.webType) topLevelProps.webType = comp.webType; + // ๐Ÿ†• autoFill ์„ค์ • ์ €์žฅ (์ž๋™ ์ž…๋ ฅ ๊ธฐ๋Šฅ) + if (comp.autoFill) topLevelProps.autoFill = comp.autoFill; + // ๐Ÿ†• style ์„ค์ • ์ €์žฅ (๋ผ๋ฒจ ํ…์ŠคํŠธ, ๋ผ๋ฒจ ์Šคํƒ€์ผ ๋“ฑ) + if (comp.style && Object.keys(comp.style).length > 0) topLevelProps.style = comp.style; + // ๐Ÿ”ง webTypeConfig ์ €์žฅ (๋ฒ„ํŠผ ์ œ์–ด๊ธฐ๋Šฅ, ํ”Œ๋กœ์šฐ ๊ฐ€์‹œ์„ฑ ๋“ฑ) + if (comp.webTypeConfig && Object.keys(comp.webTypeConfig).length > 0) { + topLevelProps.webTypeConfig = comp.webTypeConfig; + // ๐Ÿ” ๋””๋ฒ„๊ทธ: webTypeConfig ์ €์žฅ ํ™•์ธ + if (comp.webTypeConfig.dataflowConfig || comp.webTypeConfig.enableDataflowControl) { + console.log("๐Ÿ’พ webTypeConfig ์ €์žฅ:", { + componentId: comp.id, + enableDataflowControl: comp.webTypeConfig.enableDataflowControl, + dataflowConfig: comp.webTypeConfig.dataflowConfig, + }); + } + } // ํ˜„์žฌ ์„ค์ •์—์„œ ์ฐจ์ด๊ฐ’๋งŒ ์ถ”์ถœ const fullConfig = comp.componentConfig || {}; const configOverrides = extractCustomConfig(fullConfig, defaults); + // ๐Ÿ”ง ๋””๋ฒ„๊ทธ: style ์ €์žฅ ํ™•์ธ (์ฃผ์„ ์ฒ˜๋ฆฌ) + // if (comp.style?.labelDisplay !== undefined || configOverrides.style?.labelDisplay !== undefined) { console.log("๐Ÿ’พ ์ €์žฅ ์‹œ style ๋ณ€ํ™˜:", { componentId: comp.id, "comp.style": comp.style, "configOverrides.style": configOverrides.style, "topLevelProps.style": topLevelProps.style }); } + // ์ƒ์œ„ ๋ ˆ๋ฒจ ์†์„ฑ๊ณผ componentConfig ๋ณ‘ํ•ฉ - const overrides = { ...topLevelProps, ...configOverrides }; + // ๐Ÿ”ง style์€ ์–‘์ชฝ์„ ๋ณ‘ํ•ฉํ•˜๋˜ comp.style(topLevelProps.style)์„ ์šฐ์„ ์‹œ + const mergedStyle = { + ...(configOverrides.style || {}), + ...(topLevelProps.style || {}), + }; + + // ๐Ÿ”ง webTypeConfig๋„ ๋ณ‘ํ•ฉ (topLevelProps๊ฐ€ ์šฐ์„ , dataflowConfig ๋“ฑ ๋ณด์กด) + const mergedWebTypeConfig = { + ...(configOverrides.webTypeConfig || {}), + ...(topLevelProps.webTypeConfig || {}), + }; + + const overrides = { + ...topLevelProps, + ...configOverrides, + // ๐Ÿ†• ๋ณ‘ํ•ฉ๋œ style ์‚ฌ์šฉ (comp.style ๊ฐ’์ด ์ตœ์ข… ์šฐ์„ ) + ...(Object.keys(mergedStyle).length > 0 ? { style: mergedStyle } : {}), + // ๐Ÿ†• ๋ณ‘ํ•ฉ๋œ webTypeConfig ์‚ฌ์šฉ (comp.webTypeConfig๊ฐ€ ์ตœ์ข… ์šฐ์„ ) + ...(Object.keys(mergedWebTypeConfig).length > 0 ? { webTypeConfig: mergedWebTypeConfig } : {}), + }; return { id: comp.id, @@ -136,6 +306,10 @@ export function convertLegacyToV2(legacyLayout: LegacyLayoutData): LayoutV2 { return { version: "2.0", components, + // ๋ ˆ์ด์•„์›ƒ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จ + gridSettings: legacyLayout.gridSettings, + screenResolution: legacyLayout.screenResolution, + metadata: legacyLayout.metadata, }; } diff --git a/frontend/lib/utils/webTypeMapping.ts b/frontend/lib/utils/webTypeMapping.ts index ed4acba2..5a521e26 100644 --- a/frontend/lib/utils/webTypeMapping.ts +++ b/frontend/lib/utils/webTypeMapping.ts @@ -107,18 +107,18 @@ export const WEB_TYPE_V2_MAPPING: Record = { config: { mode: "dropdown", source: "category" }, }, - // ํŒŒ์ผ/์ด๋ฏธ์ง€ โ†’ V2Media + // ํŒŒ์ผ/์ด๋ฏธ์ง€ โ†’ V2 ํŒŒ์ผ ์—…๋กœ๋“œ file: { - componentType: "v2-media", - config: { type: "file", multiple: false }, + componentType: "v2-file-upload", + config: { multiple: true, accept: "*/*", maxFiles: 10 }, }, image: { - componentType: "v2-media", - config: { type: "image", showPreview: true }, + componentType: "v2-file-upload", + config: { multiple: false, accept: "image/*", maxFiles: 1, showPreview: true }, }, img: { - componentType: "v2-media", - config: { type: "image", showPreview: true }, + componentType: "v2-file-upload", + config: { multiple: false, accept: "image/*", maxFiles: 1, showPreview: true }, }, // ๋ฒ„ํŠผ์€ V2 ์ปดํฌ๋„ŒํŠธ์—์„œ ์ œ์™ธ (๊ธฐ์กด ๋ฒ„ํŠผ ์‹œ์Šคํ…œ ์‚ฌ์šฉ) @@ -157,9 +157,9 @@ export const WEB_TYPE_COMPONENT_MAPPING: Record = { code: "v2-select", entity: "v2-select", category: "v2-select", - file: "v2-media", - image: "v2-media", - img: "v2-media", + file: "v2-file-upload", + image: "v2-file-upload", + img: "v2-file-upload", button: "button-primary", label: "v2-input", }; diff --git a/frontend/lib/v2-core/events/types.ts b/frontend/lib/v2-core/events/types.ts index 8d0075c5..a33e7684 100644 --- a/frontend/lib/v2-core/events/types.ts +++ b/frontend/lib/v2-core/events/types.ts @@ -53,6 +53,13 @@ export const V2_EVENTS = { RELATED_BUTTON_REGISTER: "v2:related-button:register", RELATED_BUTTON_UNREGISTER: "v2:related-button:unregister", RELATED_BUTTON_SELECT: "v2:related-button:select", + + // ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ + SCHEDULE_GENERATE_REQUEST: "v2:schedule:generate:request", + SCHEDULE_GENERATE_PREVIEW: "v2:schedule:generate:preview", + SCHEDULE_GENERATE_APPLY: "v2:schedule:generate:apply", + SCHEDULE_GENERATE_COMPLETE: "v2:schedule:generate:complete", + SCHEDULE_GENERATE_ERROR: "v2:schedule:generate:error", } as const; export type V2EventName = (typeof V2_EVENTS)[keyof typeof V2_EVENTS]; @@ -230,6 +237,64 @@ export interface V2RelatedButtonSelectEvent { selectedData: any[]; } +// ============================================================================ +// ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ ์ด๋ฒคํŠธ +// ============================================================================ + +/** ์Šค์ผ€์ค„ ํƒ€์ž… */ +export type ScheduleType = "PRODUCTION" | "MAINTENANCE" | "SHIPPING" | "WORK_ASSIGN"; + +/** ์Šค์ผ€์ค„ ์ƒ์„ฑ ์š”์ฒญ ์ด๋ฒคํŠธ */ +export interface V2ScheduleGenerateRequestEvent { + requestId: string; + scheduleType: ScheduleType; + sourceData?: any[]; // ์„ ํƒ ๋ฐ์ดํ„ฐ (์—†์œผ๋ฉด TABLE_SELECTION_CHANGE๋กœ ๋ฐ›์€ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ) + period?: { start: string; end: string }; +} + +/** ์Šค์ผ€์ค„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฒฐ๊ณผ ์ด๋ฒคํŠธ */ +export interface V2ScheduleGeneratePreviewEvent { + requestId: string; + scheduleType: ScheduleType; + preview: { + toCreate: any[]; + toDelete: any[]; + toUpdate: any[]; + summary: { + createCount: number; + deleteCount: number; + updateCount: number; + totalQty: number; + }; + }; +} + +/** ์Šค์ผ€์ค„ ์ ์šฉ ์ด๋ฒคํŠธ */ +export interface V2ScheduleGenerateApplyEvent { + requestId: string; + confirmed: boolean; +} + +/** ์Šค์ผ€์ค„ ์ƒ์„ฑ ์™„๋ฃŒ ์ด๋ฒคํŠธ */ +export interface V2ScheduleGenerateCompleteEvent { + requestId: string; + success: boolean; + applied: { + created: number; + deleted: number; + updated: number; + }; + scheduleType: ScheduleType; + targetTableName: string; +} + +/** ์Šค์ผ€์ค„ ์ƒ์„ฑ ์—๋Ÿฌ ์ด๋ฒคํŠธ */ +export interface V2ScheduleGenerateErrorEvent { + requestId: string; + error: string; + scheduleType?: ScheduleType; +} + // ============================================================================ // ์ด๋ฒคํŠธ ํƒ€์ž… ๋งตํ•‘ (ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์œ„ํ•œ) // ============================================================================ @@ -268,6 +333,12 @@ export interface V2EventPayloadMap { [V2_EVENTS.RELATED_BUTTON_REGISTER]: V2RelatedButtonRegisterEvent; [V2_EVENTS.RELATED_BUTTON_UNREGISTER]: V2RelatedButtonUnregisterEvent; [V2_EVENTS.RELATED_BUTTON_SELECT]: V2RelatedButtonSelectEvent; + + [V2_EVENTS.SCHEDULE_GENERATE_REQUEST]: V2ScheduleGenerateRequestEvent; + [V2_EVENTS.SCHEDULE_GENERATE_PREVIEW]: V2ScheduleGeneratePreviewEvent; + [V2_EVENTS.SCHEDULE_GENERATE_APPLY]: V2ScheduleGenerateApplyEvent; + [V2_EVENTS.SCHEDULE_GENERATE_COMPLETE]: V2ScheduleGenerateCompleteEvent; + [V2_EVENTS.SCHEDULE_GENERATE_ERROR]: V2ScheduleGenerateErrorEvent; } // ============================================================================ diff --git a/frontend/lib/v2-core/index.ts b/frontend/lib/v2-core/index.ts index 2721b509..9016defb 100644 --- a/frontend/lib/v2-core/index.ts +++ b/frontend/lib/v2-core/index.ts @@ -32,6 +32,9 @@ export * from "./components"; // ์–ด๋Œ‘ํ„ฐ export * from "./adapters"; +// ์„œ๋น„์Šค +export * from "./services"; + // ์ดˆ๊ธฐํ™” export { initV2Core, cleanupV2Core } from "./init"; diff --git a/frontend/lib/v2-core/services/ScheduleConfirmDialog.tsx b/frontend/lib/v2-core/services/ScheduleConfirmDialog.tsx new file mode 100644 index 00000000..75da02a5 --- /dev/null +++ b/frontend/lib/v2-core/services/ScheduleConfirmDialog.tsx @@ -0,0 +1,208 @@ +/** + * ์Šค์ผ€์ค„ ์ƒ์„ฑ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ + * + * ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ ์‹œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฒฐ๊ณผ๋ฅผ ํ‘œ์‹œํ•˜๊ณ  ํ™•์ธ์„ ๋ฐ›๋Š” ๋‹ค์ด์–ผ๋กœ๊ทธ์ž…๋‹ˆ๋‹ค. + */ + +"use client"; + +import React from "react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Calendar, Plus, Trash2, RefreshCw } from "lucide-react"; +import type { SchedulePreviewResult } from "./ScheduleGeneratorService"; + +interface ScheduleConfirmDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + preview: SchedulePreviewResult | null; + onConfirm: () => void; + onCancel: () => void; + isLoading?: boolean; +} + +export function ScheduleConfirmDialog({ + open, + onOpenChange, + preview, + onConfirm, + onCancel, + isLoading = false, +}: ScheduleConfirmDialogProps) { + if (!preview) return null; + + const { summary, toCreate, toDelete, toUpdate } = preview; + + return ( + + + + + + ์Šค์ผ€์ค„ ์ƒ์„ฑ ํ™•์ธ + + + ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์Šค์ผ€์ค„์ด ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. ๊ณ„์†ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? + + + + {/* ์š”์•ฝ ์ •๋ณด */} +
+
+ + + {summary.createCount} + + ์ƒ์„ฑ +
+
+ + + {summary.deleteCount} + + ์‚ญ์ œ +
+
+ + + {summary.updateCount} + + ์ˆ˜์ • +
+
+ + {/* ์ƒ์„ธ ์ •๋ณด */} + +
+ {/* ์ƒ์„ฑ๋  ์Šค์ผ€์ค„ */} + {toCreate.length > 0 && ( +
+

+ + ์ƒ์„ฑ + + {toCreate.length}๊ฑด +

+
+ {toCreate.slice(0, 5).map((item, index) => ( +
+ + {item.resource_name || item.resource_id} + + + {item.start_date} ~ {item.end_date} / {item.plan_qty}๊ฐœ + +
+ ))} + {toCreate.length > 5 && ( +
+ ... ์™ธ {toCreate.length - 5}๊ฑด +
+ )} +
+
+ )} + + {/* ์‚ญ์ œ๋  ์Šค์ผ€์ค„ */} + {toDelete.length > 0 && ( +
+

+ ์‚ญ์ œ + {toDelete.length}๊ฑด +

+
+ {toDelete.slice(0, 5).map((item, index) => ( +
+ + {item.resource_name || item.resource_id} + + + {item.start_date} ~ {item.end_date} + +
+ ))} + {toDelete.length > 5 && ( +
+ ... ์™ธ {toDelete.length - 5}๊ฑด +
+ )} +
+
+ )} + + {/* ์ˆ˜์ •๋  ์Šค์ผ€์ค„ */} + {toUpdate.length > 0 && ( +
+

+ ์ˆ˜์ • + {toUpdate.length}๊ฑด +

+
+ {toUpdate.slice(0, 5).map((item, index) => ( +
+ + {item.resource_name || item.resource_id} + + + {item.start_date} ~ {item.end_date} + +
+ ))} + {toUpdate.length > 5 && ( +
+ ... ์™ธ {toUpdate.length - 5}๊ฑด +
+ )} +
+
+ )} +
+
+ + {/* ์ด ์ˆ˜๋Ÿ‰ */} +
+ ์ด ๊ณ„ํš ์ˆ˜๋Ÿ‰ + + {summary.totalQty.toLocaleString()}๊ฐœ + +
+ + + + ์ทจ์†Œ + + + {isLoading ? "์ฒ˜๋ฆฌ ์ค‘..." : "ํ™•์ธ ๋ฐ ์ ์šฉ"} + + +
+
+ ); +} diff --git a/frontend/lib/v2-core/services/ScheduleGeneratorService.ts b/frontend/lib/v2-core/services/ScheduleGeneratorService.ts new file mode 100644 index 00000000..5d693005 --- /dev/null +++ b/frontend/lib/v2-core/services/ScheduleGeneratorService.ts @@ -0,0 +1,334 @@ +/** + * ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค + * + * ์ด๋ฒคํŠธ ๋ฒ„์Šค ๊ธฐ๋ฐ˜์œผ๋กœ ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * - TABLE_SELECTION_CHANGE ์ด๋ฒคํŠธ๋กœ ์„ ํƒ ๋ฐ์ดํ„ฐ ์ถ”์  + * - SCHEDULE_GENERATE_REQUEST ์ด๋ฒคํŠธ๋กœ ์ƒ์„ฑ ์š”์ฒญ ์ฒ˜๋ฆฌ + * - SCHEDULE_GENERATE_APPLY ์ด๋ฒคํŠธ๋กœ ์ ์šฉ ์ฒ˜๋ฆฌ + */ + +import { useState, useEffect, useCallback, useRef } from "react"; +import { v2EventBus } from "../events/EventBus"; +import { V2_EVENTS } from "../events/types"; +import type { ScheduleType, V2ScheduleGenerateRequestEvent, V2ScheduleGenerateApplyEvent } from "../events/types"; +import { apiClient } from "@/lib/api/client"; +import { toast } from "sonner"; + +// ============================================================================ +// ํƒ€์ž… ์ •์˜ +// ============================================================================ + +/** ์Šค์ผ€์ค„ ์ƒ์„ฑ ์„ค์ • */ +export interface ScheduleGenerationConfig { + // ์Šค์ผ€์ค„ ํƒ€์ž… + scheduleType: ScheduleType; + + // ์†Œ์Šค ์„ค์ • + source: { + tableName: string; // ์†Œ์Šค ํ…Œ์ด๋ธ”๋ช… + groupByField: string; // ๊ทธ๋ฃนํ™” ๊ธฐ์ค€ ํ•„๋“œ (part_code) + quantityField: string; // ์ˆ˜๋Ÿ‰ ํ•„๋“œ (order_qty, balance_qty) + dueDateField?: string; // ๋‚ฉ๊ธฐ์ผ ํ•„๋“œ (์„ ํƒ) + }; + + // ๋ฆฌ์†Œ์Šค ๋งคํ•‘ (ํƒ€์ž„๋ผ์ธ Y์ถ•) + resource: { + type: string; // 'ITEM', 'MACHINE', 'WORKER' ๋“ฑ + idField: string; // part_code, machine_code ๋“ฑ + nameField: string; // part_name, machine_name ๋“ฑ + }; + + // ์ƒ์„ฑ ๊ทœ์น™ + rules: { + leadTimeDays?: number; // ๋ฆฌ๋“œํƒ€์ž„ (์ผ) + dailyCapacity?: number; // ์ผ์ผ ์ƒ์‚ฐ๋Šฅ๋ ฅ + workingDays?: number[]; // ์ž‘์—…์ผ [1,2,3,4,5] = ์›”~๊ธˆ + considerStock?: boolean; // ์žฌ๊ณ  ๊ณ ๋ ค ์—ฌ๋ถ€ + stockTableName?: string; // ์žฌ๊ณ  ํ…Œ์ด๋ธ”๋ช… + stockQtyField?: string; // ์žฌ๊ณ  ์ˆ˜๋Ÿ‰ ํ•„๋“œ + safetyStockField?: string; // ์•ˆ์ „์žฌ๊ณ  ํ•„๋“œ + }; + + // ํƒ€๊ฒŸ ์„ค์ • + target: { + tableName: string; // ์Šค์ผ€์ค„ ํ…Œ์ด๋ธ”๋ช… (schedule_mng ๋˜๋Š” ์ „์šฉ ํ…Œ์ด๋ธ”) + }; +} + +/** ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฒฐ๊ณผ */ +export interface SchedulePreviewResult { + toCreate: any[]; + toDelete: any[]; + toUpdate: any[]; + summary: { + createCount: number; + deleteCount: number; + updateCount: number; + totalQty: number; + }; +} + +/** ํ›… ๋ฐ˜ํ™˜ ํƒ€์ž… */ +export interface UseScheduleGeneratorReturn { + // ์ƒํƒœ + isLoading: boolean; + showConfirmDialog: boolean; + previewResult: SchedulePreviewResult | null; + + // ํ•ธ๋“ค๋Ÿฌ + handleConfirm: (confirmed: boolean) => void; + closeDialog: () => void; +} + +// ============================================================================ +// ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ +// ============================================================================ + +/** ๊ธฐ๋ณธ ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ (ํ˜„์žฌ ์›”) */ +function getDefaultPeriod(): { start: string; end: string } { + const now = new Date(); + const start = new Date(now.getFullYear(), now.getMonth(), 1); + const end = new Date(now.getFullYear(), now.getMonth() + 1, 0); + return { + start: start.toISOString().split("T")[0], + end: end.toISOString().split("T")[0], + }; +} + +// ============================================================================ +// ์Šค์ผ€์ค„ ์ƒ์„ฑ ์„œ๋น„์Šค ํ›… +// ============================================================================ + +/** + * ์Šค์ผ€์ค„ ์ž๋™ ์ƒ์„ฑ ํ›… + * + * @param scheduleConfig ์Šค์ผ€์ค„ ์ƒ์„ฑ ์„ค์ • + * @returns ์ƒํƒœ ๋ฐ ํ•ธ๋“ค๋Ÿฌ + * + * @example + * ```tsx + * const config: ScheduleGenerationConfig = { + * scheduleType: "PRODUCTION", + * source: { tableName: "sales_order_mng", groupByField: "part_code", quantityField: "balance_qty" }, + * resource: { type: "ITEM", idField: "part_code", nameField: "part_name" }, + * rules: { leadTimeDays: 3, dailyCapacity: 100 }, + * target: { tableName: "schedule_mng" }, + * }; + * + * const { showConfirmDialog, previewResult, handleConfirm } = useScheduleGenerator(config); + * ``` + */ +export function useScheduleGenerator(scheduleConfig?: ScheduleGenerationConfig | null): UseScheduleGeneratorReturn { + // ์ƒํƒœ + const [selectedData, setSelectedData] = useState([]); + const [previewResult, setPreviewResult] = useState(null); + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const currentRequestIdRef = useRef(""); + const currentConfigRef = useRef(null); + + // 1. ํ…Œ์ด๋ธ” ์„ ํƒ ๋ฐ์ดํ„ฐ ์ถ”์  (TABLE_SELECTION_CHANGE ์ด๋ฒคํŠธ ์ˆ˜์‹ ) + useEffect(() => { + const unsubscribe = v2EventBus.subscribe(V2_EVENTS.TABLE_SELECTION_CHANGE, (payload) => { + // scheduleConfig๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ํ…Œ์ด๋ธ”๋งŒ, ์—†์œผ๋ฉด ๋ชจ๋“  ํ…Œ์ด๋ธ”์˜ ์„ ํƒ ๋ฐ์ดํ„ฐ ์ €์žฅ + if (scheduleConfig?.source?.tableName) { + if (payload.tableName === scheduleConfig.source.tableName) { + setSelectedData(payload.selectedRows); + console.log("[useScheduleGenerator] ์„ ํƒ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ (ํŠน์ • ํ…Œ์ด๋ธ”):", payload.selectedCount, "๊ฑด"); + } + } else { + // scheduleConfig๊ฐ€ ์—†์œผ๋ฉด ๋ชจ๋“  ํ…Œ์ด๋ธ”์˜ ์„ ํƒ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅ + setSelectedData(payload.selectedRows); + console.log("[useScheduleGenerator] ์„ ํƒ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ (๋ชจ๋“  ํ…Œ์ด๋ธ”):", payload.selectedCount, "๊ฑด"); + } + }); + return unsubscribe; + }, [scheduleConfig?.source?.tableName]); + + // 2. ์Šค์ผ€์ค„ ์ƒ์„ฑ ์š”์ฒญ ์ฒ˜๋ฆฌ (SCHEDULE_GENERATE_REQUEST ์ˆ˜์‹ ) + useEffect(() => { + const unsubscribe = v2EventBus.subscribe( + V2_EVENTS.SCHEDULE_GENERATE_REQUEST, + async (payload: V2ScheduleGenerateRequestEvent) => { + console.log("[useScheduleGenerator] SCHEDULE_GENERATE_REQUEST ์ˆ˜์‹ :", payload); + + // ์ด๋ฒคํŠธ์—์„œ config๊ฐ€ ์˜ค๋ฉด ์‚ฌ์šฉ, ์—†์œผ๋ฉด ๊ธฐ์กด scheduleConfig ๋˜๋Š” ๊ธฐ๋ณธ config ์‚ฌ์šฉ + const configToUse = (payload as any).config || + scheduleConfig || { + // ๊ธฐ๋ณธ ์„ค์ • (์ƒ์‚ฐ๊ณ„ํš ํ™”๋ฉด์šฉ) + scheduleType: payload.scheduleType || "PRODUCTION", + source: { + tableName: "sales_order_mng", + groupByField: "part_code", + quantityField: "balance_qty", + dueDateField: "delivery_date", // ๊ธฐ์ค€์ผ ํ•„๋“œ (๋‚ฉ๊ธฐ์ผ) + }, + resource: { + type: "ITEM", + idField: "part_code", + nameField: "part_name", + }, + rules: { + leadTimeDays: 3, + dailyCapacity: 100, + }, + target: { + tableName: "schedule_mng", + }, + }; + + console.log("[useScheduleGenerator] ์‚ฌ์šฉํ•  config:", configToUse); + + // scheduleType์ด ์ง€์ •๋˜์–ด ์žˆ๊ณ  config๋„ ์žˆ๋Š” ๊ฒฝ์šฐ, ํƒ€์ž… ์ผ์น˜ ํ™•์ธ + if (scheduleConfig && payload.scheduleType && payload.scheduleType !== scheduleConfig.scheduleType) { + console.log("[useScheduleGenerator] scheduleType ๋ถˆ์ผ์น˜, ๋ฌด์‹œ"); + return; + } + + // sourceData: ์ด๋ฒคํŠธ ํŽ˜์ด๋กœ๋“œ > ์ƒํƒœ ์ €์žฅ๋œ ์„ ํƒ ๋ฐ์ดํ„ฐ > ๋นˆ ๋ฐฐ์—ด + const dataToUse = payload.sourceData || selectedData; + const periodToUse = payload.period || getDefaultPeriod(); + + console.log("[useScheduleGenerator] ์‚ฌ์šฉํ•  sourceData:", dataToUse.length, "๊ฑด"); + console.log("[useScheduleGenerator] ์‚ฌ์šฉํ•  period:", periodToUse); + + currentRequestIdRef.current = payload.requestId; + currentConfigRef.current = configToUse; + setIsLoading(true); + toast.loading("์Šค์ผ€์ค„ ์ƒ์„ฑ ์ค‘...", { id: "schedule-generate" }); + + try { + // ๋ฏธ๋ฆฌ๋ณด๊ธฐ API ํ˜ธ์ถœ + const response = await apiClient.post("/schedule/preview", { + config: configToUse, + scheduleType: payload.scheduleType, + sourceData: dataToUse, + period: periodToUse, + }); + + console.log("[useScheduleGenerator] ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์‘๋‹ต:", response.data); + + if (!response.data.success) { + toast.error(response.data.message || "๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ƒ์„ฑ ์‹คํŒจ", { id: "schedule-generate" }); + v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_ERROR, { + requestId: payload.requestId, + error: response.data.message || "๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ƒ์„ฑ ์‹คํŒจ", + scheduleType: payload.scheduleType, + }); + return; + } + + setPreviewResult(response.data.preview); + setShowConfirmDialog(true); + toast.success("์Šค์ผ€์ค„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", { id: "schedule-generate" }); + + // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฒฐ๊ณผ ์ด๋ฒคํŠธ ๋ฐœ์†ก (๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Œ) + v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_PREVIEW, { + requestId: payload.requestId, + scheduleType: payload.scheduleType, + preview: response.data.preview, + }); + } catch (error: any) { + console.error("[ScheduleGeneratorService] ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์˜ค๋ฅ˜:", error); + toast.error("์Šค์ผ€์ค„ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", { id: "schedule-generate" }); + v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_ERROR, { + requestId: payload.requestId, + error: error.message, + scheduleType: payload.scheduleType, + }); + } finally { + setIsLoading(false); + } + }, + ); + return unsubscribe; + }, [selectedData, scheduleConfig]); + + // 3. ์Šค์ผ€์ค„ ์ ์šฉ ์ฒ˜๋ฆฌ (SCHEDULE_GENERATE_APPLY ์ˆ˜์‹ ) + useEffect(() => { + const unsubscribe = v2EventBus.subscribe( + V2_EVENTS.SCHEDULE_GENERATE_APPLY, + async (payload: V2ScheduleGenerateApplyEvent) => { + if (payload.requestId !== currentRequestIdRef.current) return; + + if (!payload.confirmed) { + setShowConfirmDialog(false); + return; + } + + // ์ €์žฅ๋œ config ๋˜๋Š” ๊ธฐ์กด scheduleConfig ์‚ฌ์šฉ + const configToUse = currentConfigRef.current || scheduleConfig; + + setIsLoading(true); + toast.loading("์Šค์ผ€์ค„ ์ ์šฉ ์ค‘...", { id: "schedule-apply" }); + + try { + const response = await apiClient.post("/schedule/apply", { + config: configToUse, + preview: previewResult, + options: { deleteExisting: true, updateMode: "replace" }, + }); + + if (!response.data.success) { + toast.error(response.data.message || "์Šค์ผ€์ค„ ์ ์šฉ ์‹คํŒจ", { id: "schedule-apply" }); + return; + } + + // ์™„๋ฃŒ ์ด๋ฒคํŠธ ๋ฐœ์†ก + v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_COMPLETE, { + requestId: payload.requestId, + success: true, + applied: response.data.applied, + scheduleType: configToUse?.scheduleType || "PRODUCTION", + targetTableName: configToUse?.target?.tableName || "schedule_mng", + }); + + // ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฐœ์†ก + v2EventBus.emit(V2_EVENTS.TABLE_REFRESH, { + tableName: configToUse?.target?.tableName || "schedule_mng", + }); + + toast.success(`${response.data.applied?.created || 0}๊ฑด์˜ ์Šค์ผ€์ค„์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, { + id: "schedule-apply", + }); + setShowConfirmDialog(false); + setPreviewResult(null); + } catch (error: any) { + console.error("[ScheduleGeneratorService] ์ ์šฉ ์˜ค๋ฅ˜:", error); + toast.error("์Šค์ผ€์ค„ ์ ์šฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", { id: "schedule-apply" }); + } finally { + setIsLoading(false); + } + }, + ); + return unsubscribe; + }, [previewResult, scheduleConfig]); + + // ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ•ธ๋“ค๋Ÿฌ + const handleConfirm = useCallback((confirmed: boolean) => { + v2EventBus.emit(V2_EVENTS.SCHEDULE_GENERATE_APPLY, { + requestId: currentRequestIdRef.current, + confirmed, + }); + }, []); + + // ๋‹ค์ด์–ผ๋กœ๊ทธ ๋‹ซ๊ธฐ + const closeDialog = useCallback(() => { + setShowConfirmDialog(false); + setPreviewResult(null); + }, []); + + return { + isLoading, + showConfirmDialog, + previewResult, + handleConfirm, + closeDialog, + }; +} + +// ============================================================================ +// ์Šค์ผ€์ค„ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ์ปดํฌ๋„ŒํŠธ +// ============================================================================ + +export { ScheduleConfirmDialog } from "./ScheduleConfirmDialog"; diff --git a/frontend/lib/v2-core/services/index.ts b/frontend/lib/v2-core/services/index.ts new file mode 100644 index 00000000..02be2a2f --- /dev/null +++ b/frontend/lib/v2-core/services/index.ts @@ -0,0 +1,14 @@ +/** + * V2 ์„œ๋น„์Šค ๋ชจ๋“ˆ + * + * ์ด๋ฒคํŠธ ๋ฒ„์Šค ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค๋“ค์„ exportํ•ฉ๋‹ˆ๋‹ค. + */ + +export { + useScheduleGenerator, + type ScheduleGenerationConfig, + type SchedulePreviewResult, + type UseScheduleGeneratorReturn, +} from "./ScheduleGeneratorService"; + +export { ScheduleConfirmDialog } from "./ScheduleConfirmDialog"; diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index ca804adc..262c4deb 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -15,15 +15,19 @@ const nextConfig = { // ์‹คํ—˜์  ๊ธฐ๋Šฅ ํ™œ์„ฑํ™” experimental: { - outputFileTracingRoot: undefined, + // ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™” (Next.js 15+) + webpackMemoryOptimizations: true, }, // API ํ”„๋ก์‹œ ์„ค์ • - ๋ฐฑ์—”๋“œ๋กœ ์š”์ฒญ ์ „๋‹ฌ + // Docker ํ™˜๊ฒฝ: SERVER_API_URL=http://backend:3001 ์‚ฌ์šฉ + // ๋กœ์ปฌ ๊ฐœ๋ฐœ: http://localhost:8080 ์‚ฌ์šฉ async rewrites() { + const backendUrl = process.env.SERVER_API_URL || "http://localhost:8080"; return [ { source: "/api/:path*", - destination: "http://localhost:8080/api/:path*", + destination: `${backendUrl}/api/:path*`, }, ]; }, diff --git a/frontend/types/component-events.ts b/frontend/types/component-events.ts new file mode 100644 index 00000000..3cfc959b --- /dev/null +++ b/frontend/types/component-events.ts @@ -0,0 +1,241 @@ +/** + * V2 ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ํ†ต์‹  ์ด๋ฒคํŠธ ํƒ€์ž… ์ •์˜ + * + * ๋ชจ๋“  V2 ์ปดํฌ๋„ŒํŠธ๋Š” ์ด ํŒŒ์ผ์— ์ •์˜๋œ ์ด๋ฒคํŠธ ํƒ€์ž…์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + * ์ด๋ฒคํŠธ ๋ฐœํ–‰/๊ตฌ๋… ์‹œ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + */ + +// ============================================================ +// ์ด๋ฒคํŠธ ์ƒ์„ธ ๋ฐ์ดํ„ฐ ํƒ€์ž… (event.detail) +// ============================================================ + +/** + * ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ + * ๋ฐœํ–‰: v2-table-list + * ๊ตฌ๋…: v2-aggregation-widget, v2-repeat-container + */ +export interface TableListDataChangeDetail { + componentId: string; + tableName: string; + data: any[]; + selectedRows: string[] | number[]; +} + +/** + * ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ + * ๋ฐœํ–‰: v2-unified-repeater + * ๊ตฌ๋…: v2-aggregation-widget, v2-repeat-container + */ +export interface RepeaterDataChangeDetail { + componentId: string; + tableName: string; + data: any[]; + selectedData?: any[]; +} + +/** + * ํผ ์ €์žฅ ์ „ ์ด๋ฒคํŠธ + * ๋ฐœํ–‰: buttonActions, UnifiedFormContext + * ๊ตฌ๋…: v2-unified-repeater, simple-repeater-table, modal-repeater-table ๋“ฑ + */ +export interface BeforeFormSaveDetail { + formData: Record; + skipDefaultSave?: boolean; +} + +/** + * ํผ ์ €์žฅ ํ›„ ์ด๋ฒคํŠธ + * ๋ฐœํ–‰: UnifiedFormContext + * ๊ตฌ๋…: ์ €์žฅ ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ ์ปดํฌ๋„ŒํŠธ๋“ค + */ +export interface AfterFormSaveDetail { + success: boolean; + data?: any; + error?: string; +} + +/** + * ๋ฆฌํ”ผํ„ฐ ์ €์žฅ ์ด๋ฒคํŠธ (๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ FK ์—ฐ๊ฒฐ์šฉ) + * ๋ฐœํ–‰: InteractiveScreenViewerDynamic + * ๊ตฌ๋…: v2-unified-repeater + */ +export interface RepeaterSaveDetail { + parentId?: string | number; + masterRecordId: string | number; + mainFormData: Record; + tableName: string; +} + +/** + * ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ + * ๋ฐœํ–‰: v2-button-primary, buttonActions + * ๊ตฌ๋…: v2-table-list, v2-split-panel-layout + */ +export interface RefreshTableDetail { + tableName?: string; + componentId?: string; +} + +/** + * ์นด๋“œ ๋””์Šคํ”Œ๋ ˆ์ด ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ + * ๋ฐœํ–‰: buttonActions, InteractiveScreenViewerDynamic + * ๊ตฌ๋…: v2-card-display + */ +export interface RefreshCardDisplayDetail { + componentId?: string; +} + +/** + * ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ด๋ฒคํŠธ + * ๋ฐœํ–‰: buttonActions + * ๊ตฌ๋…: v2-unified-repeater + */ +export interface ComponentDataTransferDetail { + sourceComponentId: string; + targetComponentId: string; + data: any[]; + mode: "append" | "replace" | "merge"; + mappingRules?: Array<{ + sourceField: string; + targetField: string; + defaultValue?: any; + }>; +} + +/** + * ๋ถ„ํ•  ํŒจ๋„ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ด๋ฒคํŠธ + * ๋ฐœํ–‰: buttonActions + * ๊ตฌ๋…: v2-unified-repeater, repeater-field-group + */ +export interface SplitPanelDataTransferDetail { + sourcePosition: "left" | "right"; + targetPosition: "left" | "right"; + data: any[]; + mode: "append" | "replace" | "merge"; + mappingRules?: Array<{ + sourceField: string; + targetField: string; + defaultValue?: any; + }>; +} + +/** + * ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ ๋ฒ„ํŠผ ์„ ํƒ ์ด๋ฒคํŠธ + * ๋ฐœํ–‰: related-data-buttons + * ๊ตฌ๋…: v2-table-list + */ +export interface RelatedButtonSelectDetail { + targetTable: string; + filterColumn: string; + filterValue: any; + selectedData?: any; +} + +/** + * ๋ชจ๋‹ฌ ์ œ์–ด ์ด๋ฒคํŠธ + */ +export interface EditModalDetail { + screenId?: number; + recordId?: string | number; + data?: any; +} + +// ============================================================ +// ์ด๋ฒคํŠธ ์ด๋ฆ„ ์ƒ์ˆ˜ +// ============================================================ + +export const V2_EVENTS = { + // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ + TABLE_LIST_DATA_CHANGE: "tableListDataChange", + REPEATER_DATA_CHANGE: "repeaterDataChange", + + // ํผ ์ €์žฅ ์ด๋ฒคํŠธ + BEFORE_FORM_SAVE: "beforeFormSave", + AFTER_FORM_SAVE: "afterFormSave", + REPEATER_SAVE: "repeaterSave", + + // UI ๊ฐฑ์‹  ์ด๋ฒคํŠธ + REFRESH_TABLE: "refreshTable", + REFRESH_CARD_DISPLAY: "refreshCardDisplay", + + // ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ด๋ฒคํŠธ + COMPONENT_DATA_TRANSFER: "componentDataTransfer", + SPLIT_PANEL_DATA_TRANSFER: "splitPanelDataTransfer", + + // ๋ชจ๋‹ฌ ์ œ์–ด ์ด๋ฒคํŠธ + OPEN_EDIT_MODAL: "openEditModal", + CLOSE_EDIT_MODAL: "closeEditModal", + SAVE_SUCCESS_IN_MODAL: "saveSuccessInModal", + + // ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ + RELATED_BUTTON_SELECT: "related-button-select", + RELATED_BUTTON_REGISTER: "related-button-register", + RELATED_BUTTON_UNREGISTER: "related-button-unregister", +} as const; + +// ============================================================ +// Window EventMap ํ™•์žฅ (ํƒ€์ž… ์•ˆ์ „ํ•œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ) +// ============================================================ + +declare global { + interface WindowEventMap { + // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ + [V2_EVENTS.TABLE_LIST_DATA_CHANGE]: CustomEvent; + [V2_EVENTS.REPEATER_DATA_CHANGE]: CustomEvent; + + // ํผ ์ €์žฅ ์ด๋ฒคํŠธ + [V2_EVENTS.BEFORE_FORM_SAVE]: CustomEvent; + [V2_EVENTS.AFTER_FORM_SAVE]: CustomEvent; + [V2_EVENTS.REPEATER_SAVE]: CustomEvent; + + // UI ๊ฐฑ์‹  ์ด๋ฒคํŠธ + [V2_EVENTS.REFRESH_TABLE]: CustomEvent; + [V2_EVENTS.REFRESH_CARD_DISPLAY]: CustomEvent; + + // ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ด๋ฒคํŠธ + [V2_EVENTS.COMPONENT_DATA_TRANSFER]: CustomEvent; + [V2_EVENTS.SPLIT_PANEL_DATA_TRANSFER]: CustomEvent; + + // ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ + [V2_EVENTS.RELATED_BUTTON_SELECT]: CustomEvent; + } +} + +// ============================================================ +// ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ +// ============================================================ + +/** + * ํƒ€์ž… ์•ˆ์ „ํ•œ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ํ•จ์ˆ˜ + */ +export function dispatchV2Event( + eventName: K, + detail: WindowEventMap[K] extends CustomEvent ? D : never +): void { + if (typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent(eventName, { detail })); + } +} + +/** + * ํƒ€์ž… ์•ˆ์ „ํ•œ ์ด๋ฒคํŠธ ๊ตฌ๋… ํ•จ์ˆ˜ + */ +export function subscribeV2Event( + eventName: K, + handler: (event: WindowEventMap[K]) => void +): () => void { + if (typeof window === "undefined") { + return () => {}; + } + + window.addEventListener(eventName, handler as EventListener); + return () => { + window.removeEventListener(eventName, handler as EventListener); + }; +} + +// ============================================================ +// ๋‚ด๋ณด๋‚ด๊ธฐ +// ============================================================ + +export type V2EventName = typeof V2_EVENTS[keyof typeof V2_EVENTS]; diff --git a/frontend/types/numbering-rule.ts b/frontend/types/numbering-rule.ts index b788814c..7f21fa44 100644 --- a/frontend/types/numbering-rule.ts +++ b/frontend/types/numbering-rule.ts @@ -109,7 +109,7 @@ export interface NumberingRuleConfig { // ์นดํ…Œ๊ณ ๋ฆฌ ์กฐ๊ฑด (ํŠน์ • ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์ผ ๋•Œ๋งŒ ์ด ๊ทœ์น™ ์ ์šฉ) categoryColumn?: string; // ์นดํ…Œ๊ณ ๋ฆฌ ์กฐ๊ฑด ์ปฌ๋Ÿผ๋ช… (์˜ˆ: 'type', 'material') - categoryValueId?: number; // ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ID (category_values_test.value_id) + categoryValueId?: number; // ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ID (category_values.value_id) categoryValueLabel?: string; // ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ผ๋ฒจ (์กฐํšŒ ์‹œ ์กฐ์ธ) // ๋ฉ”ํƒ€ ์ •๋ณด diff --git a/frontend/types/screen-management.ts b/frontend/types/screen-management.ts index 67e8a934..4fa22259 100644 --- a/frontend/types/screen-management.ts +++ b/frontend/types/screen-management.ts @@ -38,6 +38,9 @@ export interface BaseComponent { gridColumnStart?: number; // ์‹œ์ž‘ ์ปฌ๋Ÿผ (1-12) gridRowIndex?: number; // ํ–‰ ์ธ๋ฑ์Šค + // ๐Ÿ†• ๋ ˆ์ด์–ด ์‹œ์Šคํ…œ + layerId?: string; // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์†ํ•œ ๋ ˆ์ด์–ด ID + parentId?: string; label?: string; required?: boolean; @@ -102,13 +105,13 @@ export interface WidgetComponent extends BaseComponent { entityConfig?: EntityTypeConfig; buttonConfig?: ButtonTypeConfig; arrayConfig?: ArrayTypeConfig; - + // ๐Ÿ†• ์ž๋™ ์ž…๋ ฅ ์„ค์ • (ํ…Œ์ด๋ธ” ์กฐํšŒ ๊ธฐ๋ฐ˜) autoFill?: { enabled: boolean; // ์ž๋™ ์ž…๋ ฅ ํ™œ์„ฑํ™” sourceTable: string; // ์กฐํšŒํ•  ํ…Œ์ด๋ธ” (์˜ˆ: company_mng) filterColumn: string; // ํ•„ํ„ฐ๋งํ•  ์ปฌ๋Ÿผ (์˜ˆ: company_code) - userField: 'companyCode' | 'userId' | 'deptCode'; // ์‚ฌ์šฉ์ž ์ •๋ณด ํ•„๋“œ + userField: "companyCode" | "userId" | "deptCode"; // ์‚ฌ์šฉ์ž ์ •๋ณด ํ•„๋“œ displayColumn: string; // ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ (์˜ˆ: company_name) }; } @@ -148,12 +151,12 @@ export interface DataTableComponent extends BaseComponent { searchable?: boolean; sortable?: boolean; filters?: DataTableFilter[]; - + // ๐Ÿ†• ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ •๋ณด๋กœ ์ž๋™ ํ•„ํ„ฐ๋ง autoFilter?: { enabled: boolean; // ์ž๋™ ํ•„ํ„ฐ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ filterColumn: string; // ํ•„ํ„ฐ๋งํ•  ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ (์˜ˆ: company_code, dept_code) - userField: 'companyCode' | 'userId' | 'deptCode'; // ์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ๊ฐ€์ ธ์˜ฌ ํ•„๋“œ + userField: "companyCode" | "userId" | "deptCode"; // ์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ๊ฐ€์ ธ์˜ฌ ํ•„๋“œ }; // ๐Ÿ†• ์ปฌ๋Ÿผ ๊ฐ’ ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง @@ -307,13 +310,13 @@ export interface SelectTypeConfig { required?: boolean; readonly?: boolean; emptyMessage?: string; - + /** ๐Ÿ†• ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด ๊ด€๊ณ„ ์ฝ”๋“œ (๊ด€๊ณ„ ๊ด€๋ฆฌ์—์„œ ์ •์˜ํ•œ ์ฝ”๋“œ) */ cascadingRelationCode?: string; - + /** ๐Ÿ†• ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด ๋ถ€๋ชจ ํ•„๋“œ๋ช… (ํ™”๋ฉด ๋‚ด ๋‹ค๋ฅธ ํ•„๋“œ์˜ columnName) */ cascadingParentField?: string; - + /** @deprecated ์ง์ ‘ ์„ค์ • ๋ฐฉ์‹ - cascadingRelationCode ์‚ฌ์šฉ ๊ถŒ์žฅ */ cascading?: CascadingDropdownConfig; } @@ -402,10 +405,10 @@ export interface EntityTypeConfig { /** * ๐Ÿ†• ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด(Cascading Dropdown) ์„ค์ • - * + * * ๋ถ€๋ชจ ํ•„๋“œ์˜ ๊ฐ’์— ๋”ฐ๋ผ ์ž์‹ ๋“œ๋กญ๋‹ค์šด์˜ ์˜ต์…˜์ด ๋™์ ์œผ๋กœ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. * ์˜ˆ: ์ฐฝ๊ณ  ์„ ํƒ โ†’ ํ•ด๋‹น ์ฐฝ๊ณ ์˜ ์œ„์น˜๋งŒ ํ‘œ์‹œ - * + * * @example * // ์ฐฝ๊ณ  โ†’ ์œ„์น˜ ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด * { @@ -420,34 +423,34 @@ export interface EntityTypeConfig { export interface CascadingDropdownConfig { /** ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด ํ™œ์„ฑํ™” ์—ฌ๋ถ€ */ enabled: boolean; - + /** ๋ถ€๋ชจ ํ•„๋“œ๋ช… (์ด ํ•„๋“œ์˜ ๊ฐ’์— ๋”ฐ๋ผ ์˜ต์…˜์ด ํ•„ํ„ฐ๋ง๋จ) */ parentField: string; - + /** ์˜ต์…˜์„ ์กฐํšŒํ•  ํ…Œ์ด๋ธ”๋ช… */ sourceTable: string; - + /** ๋ถ€๋ชจ ๊ฐ’๊ณผ ๋งค์นญํ•  ์ปฌ๋Ÿผ๋ช… (sourceTable์˜ ์ปฌ๋Ÿผ) */ parentKeyColumn: string; - + /** ๋“œ๋กญ๋‹ค์šด value๋กœ ์‚ฌ์šฉํ•  ์ปฌ๋Ÿผ๋ช… */ valueColumn: string; - + /** ๋“œ๋กญ๋‹ค์šด label๋กœ ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ๋ช… */ labelColumn: string; - + /** ์ถ”๊ฐ€ ํ•„ํ„ฐ ์กฐ๊ฑด (์„ ํƒ์‚ฌํ•ญ) */ additionalFilters?: Record; - + /** ๋ถ€๋ชจ ๊ฐ’์ด ์—†์„ ๋•Œ ํ‘œ์‹œํ•  ๋ฉ”์‹œ์ง€ */ emptyParentMessage?: string; - + /** ์˜ต์…˜์ด ์—†์„ ๋•Œ ํ‘œ์‹œํ•  ๋ฉ”์‹œ์ง€ */ noOptionsMessage?: string; - + /** ๋กœ๋”ฉ ์ค‘ ํ‘œ์‹œํ•  ๋ฉ”์‹œ์ง€ */ loadingMessage?: string; - + /** ๋ถ€๋ชจ ๊ฐ’ ๋ณ€๊ฒฝ ์‹œ ์ž๋™์œผ๋กœ ๊ฐ’ ์ดˆ๊ธฐํ™” */ clearOnParentChange?: boolean; } @@ -472,23 +475,23 @@ export interface ButtonTypeConfig { export interface QuickInsertColumnMapping { /** ์ €์žฅํ•  ํ…Œ์ด๋ธ”์˜ ๋Œ€์ƒ ์ปฌ๋Ÿผ๋ช… */ targetColumn: string; - + /** ๊ฐ’ ์†Œ์Šค ํƒ€์ž… */ sourceType: "component" | "leftPanel" | "fixed" | "currentUser"; - + // sourceType๋ณ„ ์ถ”๊ฐ€ ์„ค์ • /** component: ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ ์ปดํฌ๋„ŒํŠธ ID */ sourceComponentId?: string; - + /** component: ์ปดํฌ๋„ŒํŠธ์˜ columnName (formData ์ ‘๊ทผ์šฉ) */ sourceColumnName?: string; - + /** leftPanel: ์ขŒ์ธก ์„ ํƒ ๋ฐ์ดํ„ฐ์˜ ์ปฌ๋Ÿผ๋ช… */ sourceColumn?: string; - + /** fixed: ๊ณ ์ •๊ฐ’ */ fixedValue?: any; - + /** currentUser: ์‚ฌ์šฉ์ž ์ •๋ณด ํ•„๋“œ */ userField?: "userId" | "userName" | "companyCode" | "deptCode"; } @@ -499,13 +502,13 @@ export interface QuickInsertColumnMapping { export interface QuickInsertAfterAction { /** ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ (ํ…Œ์ด๋ธ”๋ฆฌ์ŠคํŠธ, ์นด๋“œ ๋””์Šคํ”Œ๋ ˆ์ด ์ปดํฌ๋„ŒํŠธ) */ refreshData?: boolean; - + /** ์ดˆ๊ธฐํ™”ํ•  ์ปดํฌ๋„ŒํŠธ ID ๋ชฉ๋ก */ clearComponents?: string[]; - + /** ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ์—ฌ๋ถ€ */ showSuccessMessage?: boolean; - + /** ์ปค์Šคํ…€ ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ */ successMessage?: string; } @@ -516,20 +519,20 @@ export interface QuickInsertAfterAction { export interface QuickInsertDuplicateCheck { /** ์ค‘๋ณต ์ฒดํฌ ํ™œ์„ฑํ™” */ enabled: boolean; - + /** ์ค‘๋ณต ์ฒดํฌํ•  ์ปฌ๋Ÿผ๋“ค */ columns: string[]; - + /** ์ค‘๋ณต ์‹œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ */ errorMessage?: string; } /** * ์ฆ‰์‹œ ์ €์žฅ(quickInsert) ๋ฒ„ํŠผ ์•ก์…˜ ์„ค์ • - * + * * ํ™”๋ฉด์—์„œ entity ํƒ€์ž… ์„ ํƒ๋ฐ•์Šค๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์„ ํƒํ•œ ํ›„, * ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํŠน์ • ํ…Œ์ด๋ธ”์— ์ฆ‰์‹œ INSERTํ•˜๋Š” ๊ธฐ๋Šฅ - * + * * @example * ```typescript * const config: QuickInsertConfig = { @@ -557,13 +560,13 @@ export interface QuickInsertDuplicateCheck { export interface QuickInsertConfig { /** ์ €์žฅํ•  ๋Œ€์ƒ ํ…Œ์ด๋ธ”๋ช… */ targetTable: string; - + /** ์ปฌ๋Ÿผ ๋งคํ•‘ ์„ค์ • */ columnMappings: QuickInsertColumnMapping[]; - + /** ์ €์žฅ ํ›„ ๋™์ž‘ ์„ค์ • */ afterInsert?: QuickInsertAfterAction; - + /** ์ค‘๋ณต ์ฒดํฌ ์„ค์ • (์„ ํƒ์‚ฌํ•ญ) */ duplicateCheck?: QuickInsertDuplicateCheck; } @@ -678,15 +681,15 @@ export interface DataTableFilter { export interface ColumnFilter { id: string; columnName: string; // ํ•„ํ„ฐ๋งํ•  ์ปฌ๋Ÿผ๋ช… - operator: - | "equals" - | "not_equals" - | "in" - | "not_in" - | "contains" - | "starts_with" - | "ends_with" - | "is_null" + operator: + | "equals" + | "not_equals" + | "in" + | "not_in" + | "contains" + | "starts_with" + | "ends_with" + | "is_null" | "is_not_null" | "greater_than" | "less_than" @@ -836,12 +839,71 @@ export interface GroupState { groupTitle?: string; } +// ============================================ +// ๋ ˆ์ด์–ด ์‹œ์Šคํ…œ ํƒ€์ž… ์ •์˜ +// ============================================ + +/** + * ๋ ˆ์ด์–ด ํƒ€์ž… + * - base: ๊ธฐ๋ณธ ๋ ˆ์ด์–ด (ํ•ญ์ƒ ํ‘œ์‹œ) + * - conditional: ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด (ํŠน์ • ์กฐ๊ฑด ๋งŒ์กฑ ์‹œ ํ‘œ์‹œ) + * - modal: ๋ชจ๋‹ฌ ๋ ˆ์ด์–ด (ํŒ์—… ํ˜•ํƒœ) + * - drawer: ๋“œ๋กœ์–ด ๋ ˆ์ด์–ด (์‚ฌ์ด๋“œ ํŒจ๋„ ํ˜•ํƒœ) + */ +export type LayerType = "base" | "conditional" | "modal" | "drawer"; + +/** + * ๋ ˆ์ด์–ด ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ์„ค์ • + */ +export interface LayerCondition { + targetComponentId: string; // ํŠธ๋ฆฌ๊ฑฐ๊ฐ€ ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ ID + operator: "eq" | "neq" | "in"; // ๋น„๊ต ์—ฐ์‚ฐ์ž + value: any; // ๋น„๊ตํ•  ๊ฐ’ +} + +/** + * ๋ ˆ์ด์–ด ์˜ค๋ฒ„๋ ˆ์ด ์„ค์ • (๋ชจ๋‹ฌ/๋“œ๋กœ์–ด์šฉ) + */ +export interface LayerOverlayConfig { + backdrop: boolean; // ๋ฐฐ๊ฒฝ ์–ด๋‘ก๊ฒŒ ์ฒ˜๋ฆฌ ์—ฌ๋ถ€ + closeOnBackdropClick: boolean; // ๋ฐฐ๊ฒฝ ํด๋ฆญ ์‹œ ๋‹ซ๊ธฐ ์—ฌ๋ถ€ + width?: string | number; // ๋„ˆ๋น„ + height?: string | number; // ๋†’์ด + // ๋ชจ๋‹ฌ/๋“œ๋กœ์–ด ์Šคํƒ€์ผ๋ง + backgroundColor?: string; // ์ปจํ…์ธ  ๋ฐฐ๊ฒฝ์ƒ‰ + backdropBlur?: number; // ๋ฐฐ๊ฒฝ ๋ธ”๋Ÿฌ (px) + // ๋“œ๋กœ์–ด ์ „์šฉ + position?: "left" | "right" | "top" | "bottom"; // ๋“œ๋กœ์–ด ์œ„์น˜ +} + +/** + * ๋ ˆ์ด์–ด ์ •์˜ + */ +export interface LayerDefinition { + id: string; + name: string; + type: LayerType; + zIndex: number; + isVisible: boolean; // ์ดˆ๊ธฐ ํ‘œ์‹œ ์—ฌ๋ถ€ + isLocked: boolean; // ํŽธ์ง‘ ์ž ๊ธˆ ์—ฌ๋ถ€ + + // ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ๋กœ์ง + condition?: LayerCondition; + + // ๋ชจ๋‹ฌ/๋“œ๋กœ์–ด ์ „์šฉ ์„ค์ • + overlayConfig?: LayerOverlayConfig; + + // ํ•ด๋‹น ๋ ˆ์ด์–ด์— ์†ํ•œ ์ปดํฌ๋„ŒํŠธ๋“ค + components: ComponentData[]; +} + /** * ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ */ export interface LayoutData { screenId: number; - components: ComponentData[]; + components: ComponentData[]; // @deprecated - use layers instead (kept for backward compatibility) + layers?: LayerDefinition[]; // ๐Ÿ†• ๋ ˆ์ด์–ด ๋ชฉ๋ก gridSettings?: GridSettings; metadata?: LayoutMetadata; screenResolution?: ScreenResolution; diff --git a/frontend/types/v2-components.ts b/frontend/types/v2-components.ts index d985699d..a7543b24 100644 --- a/frontend/types/v2-components.ts +++ b/frontend/types/v2-components.ts @@ -95,6 +95,8 @@ export interface V2BaseProps { autoFill?: AutoFillConfig; // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ validation?: ValidationRule[]; + // ๋””์ž์ธ ๋ชจ๋“œ (ํด๋ฆญ ๋ฐฉ์ง€) + isDesignMode?: boolean; } // ===== V2Input ===== @@ -129,7 +131,7 @@ export interface V2InputProps extends V2BaseProps { // ===== V2Select ===== -export type V2SelectMode = "dropdown" | "radio" | "check" | "tag" | "toggle" | "swap"; +export type V2SelectMode = "dropdown" | "combobox" | "radio" | "check" | "tag" | "tagbox" | "toggle" | "swap"; export type V2SelectSource = "static" | "code" | "db" | "api" | "entity" | "category"; export interface SelectOption { @@ -232,13 +234,27 @@ export interface V2MediaConfig { maxSize?: number; preview?: boolean; uploadEndpoint?: string; + // ๋ ˆ๊ฑฐ์‹œ FileUpload ํ˜ธํ™˜ ์„ค์ • + docType?: string; + docTypeName?: string; + showFileList?: boolean; + dragDrop?: boolean; } export interface V2MediaProps extends V2BaseProps { - v2Type: "V2Media"; - config: V2MediaConfig; + v2Type?: "V2Media"; + config?: V2MediaConfig; value?: string | string[]; // ํŒŒ์ผ URL ๋˜๋Š” ๋ฐฐ์—ด onChange?: (value: string | string[]) => void; + // ๋ ˆ๊ฑฐ์‹œ FileUpload ํ˜ธํ™˜ props + formData?: Record; + columnName?: string; + tableName?: string; + // ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ ์‹œ๊ทธ๋‹ˆ์ฒ˜: (fieldName, value) ํ˜•์‹ + onFormDataChange?: (fieldName: string, value: any) => void; + isDesignMode?: boolean; + isInteractive?: boolean; + onUpdate?: (updates: Partial) => void; } // ===== V2List ===== @@ -530,3 +546,28 @@ export const LEGACY_TO_V2_MAP: Record = { // Button (Input์˜ ๋ฒ„ํŠผ ๋ชจ๋“œ) "button-primary": "V2Input", }; + +// ===== ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด ์‹œ์Šคํ…œ ===== + +/** + * ๋ ˆ์ด์–ด ์กฐ๊ฑด ์„ค์ • + * ํŠน์ • ํ•„๋“œ๊ฐ’์— ๋”ฐ๋ผ ๋ ˆ์ด์–ด ํ™œ์„ฑํ™” ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ • + */ +export interface LayerCondition { + field: string; // ํŠธ๋ฆฌ๊ฑฐ ํ•„๋“œ (columnName ๋˜๋Š” ํƒญID) + operator: "=" | "!=" | "in" | "notIn" | "isEmpty" | "isNotEmpty"; + value: string | string[]; // ๋น„๊ต๊ฐ’ +} + +/** + * ๋ ˆ์ด์–ด ์„ค์ • + * ํŠน์ • ์กฐ๊ฑด์ด ์ถฉ์กฑ๋  ๋•Œ ํ‘œ์‹œ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ๊ทธ๋ฃน + */ +export interface LayerConfig { + layerId: string; // ๊ณ ์œ  ID + layerName: string; // ํ‘œ์‹œ๋ช… (์„ค์ •์šฉ) + conditions: LayerCondition[]; // ์กฐ๊ฑด ๋ชฉ๋ก + conditionLogic?: "AND" | "OR"; // ์กฐ๊ฑด ์กฐํ•ฉ ๋ฐฉ์‹ (๊ธฐ๋ณธ: AND) + targetComponents: string[]; // ํ‘œ์‹œํ•  ์ปดํฌ๋„ŒํŠธ ID ๋ชฉ๋ก + alwaysVisible?: boolean; // ํ•ญ์ƒ ํ‘œ์‹œ (์กฐ๊ฑด ๋ฌด์‹œ) +} diff --git a/mcp-agent-orchestrator/README.md b/mcp-agent-orchestrator/README.md new file mode 100644 index 00000000..ce9aac42 --- /dev/null +++ b/mcp-agent-orchestrator/README.md @@ -0,0 +1,189 @@ +# Multi-Agent Orchestrator MCP Server v2.0 + +Cursor Agent CLI๋ฅผ ํ™œ์šฉํ•œ ๋ฉ€ํ‹ฐ์—์ด์ „ํŠธ ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค. +**Cursor Team Plan๋งŒ์œผ๋กœ ๋™์ž‘** - ์™ธ๋ถ€ API ํ‚ค ๋ถˆํ•„์š”! + +## ์•„ํ‚คํ…์ฒ˜ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Cursor IDE (PM Agent) โ”‚ +โ”‚ Claude Opus 4.5 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ MCP Tools + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ–ผ โ–ผ โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚Backend โ”‚ โ”‚ DB โ”‚ โ”‚Frontendโ”‚ +โ”‚ Agent โ”‚ โ”‚ Agent โ”‚ โ”‚ Agent โ”‚ +โ”‚ via CLIโ”‚ โ”‚ via CLIโ”‚ โ”‚ via CLIโ”‚ +โ”‚Sonnet โ”‚ โ”‚Sonnet โ”‚ โ”‚Sonnet โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†‘ โ†‘ โ†‘ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + Cursor Agent CLI + (Team Plan ํฌ๋ ˆ๋”ง ์‚ฌ์šฉ) +``` + +## ํŠน์ง• + +- **API ํ‚ค ๋ถˆํ•„์š”**: Cursor Team Plan ํฌ๋ ˆ๋”ง๋งŒ ์‚ฌ์šฉ +- **ํฌ๋กœ์Šค ํ”Œ๋žซํผ**: Windows, Mac, Linux ์ง€์› +- **์ง„์งœ ๋ณ‘๋ ฌ ์‹คํ–‰**: `parallel_ask`๋กœ ๋™์‹œ ์ž‘์—… +- **๋ชจ๋ธ ํ‹ฐ์–ด๋ง**: PM=Opus, Sub-agents=Sonnet + +## ์‚ฌ์ „ ์š”๊ตฌ์‚ฌํ•ญ + +1. **Cursor Team/Pro Plan** ๊ตฌ๋… +2. **Cursor Agent CLI** ์„ค์น˜ ๋ฐ ๋กœ๊ทธ์ธ + ```bash + # ์„ค์น˜ ํ›„ ๋กœ๊ทธ์ธ ํ™•์ธ + agent status + ``` + +## ์„ค์น˜ + +```bash +cd mcp-agent-orchestrator +npm install +npm run build +``` + +## Cursor ์„ค์ • + +### Windows + +`.cursor/mcp.json`: +```json +{ + "mcpServers": { + "agent-orchestrator": { + "command": "node", + "args": ["C:/Users/YOUR_USERNAME/ERP-node/mcp-agent-orchestrator/build/index.js"] + } + } +} +``` + +### Mac + +`.cursor/mcp.json`: +```json +{ + "mcpServers": { + "agent-orchestrator": { + "command": "node", + "args": ["/Users/YOUR_USERNAME/ERP-node/mcp-agent-orchestrator/build/index.js"] + } + } +} +``` + +**์ฃผ์˜**: Mac์—์„œ agent CLI๊ฐ€ PATH์— ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. +```bash +# agent CLI ์œ„์น˜ ํ™•์ธ +which agent +# ๋ณดํ†ต: ~/.cursor-agent/bin/agent ๋˜๋Š” /usr/local/bin/agent + +# PATH์— ์—†์œผ๋ฉด ์ถ”๊ฐ€ (.zshrc ๋˜๋Š” .bashrc) +export PATH="$HOME/.cursor-agent/bin:$PATH" +``` + +## ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋„๊ตฌ + +### ask_backend_agent +๋ฐฑ์—”๋“œ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ์งˆ๋ฌธ/์ž‘์—… ์š”์ฒญ +- API ์„ค๊ณ„, ์„œ๋น„์Šค ๋กœ์ง, ๋ผ์šฐํŒ… +- ๋‹ด๋‹น ํด๋”: `backend-node/src/` + +### ask_db_agent +DB ์ „๋ฌธ๊ฐ€์—๊ฒŒ ์งˆ๋ฌธ/์ž‘์—… ์š”์ฒญ +- ์Šคํ‚ค๋งˆ, ์ฟผ๋ฆฌ, MyBatis ๋งคํผ +- ๋‹ด๋‹น ํด๋”: `src/com/pms/mapper/`, `db/` + +### ask_frontend_agent +ํ”„๋ก ํŠธ์—”๋“œ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ์งˆ๋ฌธ/์ž‘์—… ์š”์ฒญ +- React ์ปดํฌ๋„ŒํŠธ, ํŽ˜์ด์ง€, ์Šคํƒ€์ผ +- ๋‹ด๋‹น ํด๋”: `frontend/` + +### parallel_ask +์—ฌ๋Ÿฌ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ๋™์‹œ์— ์งˆ๋ฌธ (์ง„์งœ ๋ณ‘๋ ฌ ์‹คํ–‰!) +- ์ •๋ณด ์ˆ˜์ง‘ ๋‹จ๊ณ„์—์„œ ์œ ์šฉ + +### get_agent_info +์—์ด์ „ํŠธ ์‹œ์Šคํ…œ ์ •๋ณด ํ™•์ธ + +## ์›Œํฌํ”Œ๋กœ์šฐ ์˜ˆ์‹œ + +### 1๋‹จ๊ณ„: ์ •๋ณด ์ˆ˜์ง‘ (๋ณ‘๋ ฌ) +``` +parallel_ask([ + { agent: "backend", task: "ํ˜„์žฌ order ๊ด€๋ จ API ๊ตฌ์กฐ ๋ถ„์„" }, + { agent: "db", task: "orders ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ๋ถ„์„" }, + { agent: "frontend", task: "์ฃผ๋ฌธ ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ ํ˜„ํ™ฉ ๋ถ„์„" } +]) +``` + +### 2๋‹จ๊ณ„: ๊ฐœ๋ณ„ ์ž‘์—… (์ˆœ์ฐจ) +``` +ask_db_agent("cursor ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง• ์ฟผ๋ฆฌ ์ž‘์„ฑ") +ask_backend_agent("GET /api/orders์— pagination ์ถ”๊ฐ€") +ask_frontend_agent("Pagination ์ปดํฌ๋„ŒํŠธ ์ ์šฉ") +``` + +## ๋ชจ๋ธ ์„ค์ • + +| Agent | Model | ์—ญํ•  | +|-------|-------|------| +| PM (Cursor IDE) | Opus 4.5 | ์ „์ฒด ์กฐ์œจ, ์‚ฌ์šฉ์ž ๋Œ€ํ™” | +| Backend | Sonnet 4.5 | API, ์„œ๋น„์Šค ๋กœ์ง | +| DB | Sonnet 4.5 | ์Šคํ‚ค๋งˆ, ์ฟผ๋ฆฌ | +| Frontend | Sonnet 4.5 | ์ปดํฌ๋„ŒํŠธ, UI | + +**๋น„์šฉ ์ตœ์ ํ™”**: PM๋งŒ Opus, ๋‚˜๋จธ์ง€๋Š” Sonnet ์‚ฌ์šฉ + +## ํ™˜๊ฒฝ ๋ณ€์ˆ˜ + +- `LOG_LEVEL`: ๋กœ๊ทธ ๋ ˆ๋ฒจ (debug, info, warn, error) + +## ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… + +### Windows: agent ๋ช…๋ น์–ด๊ฐ€ ์•ˆ ๋จ +```powershell +# PowerShell ์‹คํ–‰ ์ •์ฑ… ํ™•์ธ +Get-ExecutionPolicy -List + +# ํ•„์š”์‹œ ๋ณ€๊ฒฝ +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +``` + +### Mac: agent ๋ช…๋ น์–ด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ +```bash +# agent CLI ์œ„์น˜ ํ™•์ธ +ls -la ~/.cursor-agent/bin/ + +# PATH ์ถ”๊ฐ€ +echo 'export PATH="$HOME/.cursor-agent/bin:$PATH"' >> ~/.zshrc +source ~/.zshrc +``` + +### ์‘๋‹ต์ด ์˜ค๋ž˜ ๊ฑธ๋ฆผ +- ์ •์ƒ์ž…๋‹ˆ๋‹ค! ๊ฐ ์—์ด์ „ํŠธ ํ˜ธ์ถœ์— 15-30์ดˆ ์†Œ์š” +- `parallel_ask`๋กœ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌํ•˜๋ฉด ์‹œ๊ฐ„ ์ ˆ์•ฝ + +## ๊ฐœ๋ฐœ + +```bash +# ๊ฐœ๋ฐœ ๋ชจ๋“œ (watch) +npm run dev + +# ๋นŒ๋“œ +npm run build + +# ํ…Œ์ŠคํŠธ ์‹คํ–‰ +npm start +``` + +## ๋ผ์ด์„ ์Šค + +MIT diff --git a/mcp-agent-orchestrator/package-lock.json b/mcp-agent-orchestrator/package-lock.json new file mode 100644 index 00000000..af26ebd8 --- /dev/null +++ b/mcp-agent-orchestrator/package-lock.json @@ -0,0 +1,1179 @@ +{ + "name": "mcp-agent-orchestrator", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-agent-orchestrator", + "version": "2.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/node": { + "version": "20.19.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.31.tgz", + "integrity": "sha512-5jsi0wpncvTD33Sh1UCgacK37FFwDn+EG7wCmEvs62fCvBL+n8/76cAYDok21NF6+jaVWIqKwCZyX7Vbu8eB3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", + "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/mcp-agent-orchestrator/package.json b/mcp-agent-orchestrator/package.json new file mode 100644 index 00000000..bc8b5b7c --- /dev/null +++ b/mcp-agent-orchestrator/package.json @@ -0,0 +1,29 @@ +{ + "name": "mcp-agent-orchestrator", + "version": "2.0.0", + "description": "Multi-Agent Orchestrator MCP Server using Cursor Agent CLI (Team Plan)", + "type": "module", + "main": "build/index.js", + "scripts": { + "build": "tsc", + "start": "node build/index.js", + "dev": "tsc --watch" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "keywords": [ + "cursor", + "mcp", + "multi-agent", + "ai", + "orchestrator" + ] +} diff --git a/mcp-agent-orchestrator/src/agents/index.ts b/mcp-agent-orchestrator/src/agents/index.ts new file mode 100644 index 00000000..7aebaa5c --- /dev/null +++ b/mcp-agent-orchestrator/src/agents/index.ts @@ -0,0 +1,6 @@ +/** + * ์—์ด์ „ํŠธ ๋ชจ๋“ˆ ๋‚ด๋ณด๋‚ด๊ธฐ + */ + +export * from "./types.js"; +export * from "./prompts.js"; diff --git a/mcp-agent-orchestrator/src/agents/prompts.ts b/mcp-agent-orchestrator/src/agents/prompts.ts new file mode 100644 index 00000000..f248e29d --- /dev/null +++ b/mcp-agent-orchestrator/src/agents/prompts.ts @@ -0,0 +1,258 @@ +/** + * Agent System Prompts v2.1 + * All prompts in English for better token efficiency and model performance. + * Agents will respond in Korean based on user preferences. + */ + +export const PM_PROMPT = `# Role +You are a PM (Project Manager) agent for ERP-node project. +Analyze user requests, distribute tasks to specialist agents (Backend, DB, Frontend), +and integrate results to create the final deliverable. + +# Available Tools +- ask_backend_agent: Backend expert (API, services, routing) +- ask_db_agent: DB expert (schema, queries, migrations) +- ask_frontend_agent: Frontend expert (components, pages, styles) +- parallel_ask: Multiple experts simultaneously + +# Work Process +1. Analyze request -> identify scope +2. If cross-domain (FE+BE+DB): use parallel_ask +3. If single domain: use specific agent +4. Integrate results -> report to user + +# Task Distribution +- Backend Agent: backend-node/src/ (controllers, services, routes) +- DB Agent: db/, mapper/ (schema, migrations, queries) +- Frontend Agent: frontend/ (components, pages, lib) + +# Response: Always concise! Summarize key findings only.`; + +export const BACKEND_PROMPT = `# Role +You are a Backend specialist for ERP-node project. +Stack: Node.js + Express + TypeScript + PostgreSQL Raw Query. + +# CRITICAL PROJECT RULES + +## 1. Multi-tenancy (ABSOLUTE MUST!) +- ALL queries MUST include company_code filter +- Use req.user!.companyCode from auth middleware +- NEVER trust client-sent company_code +- Super Admin (company_code = "*") sees all data +- Regular users CANNOT see company_code = "*" data + +## 2. Super Admin Visibility +- If req.user.companyCode !== "*", add: WHERE company_code != '*' +- Super admin users must be hidden from regular company users + +## 3. Required Code Pattern +\`\`\`typescript +const companyCode = req.user!.companyCode; +if (companyCode === "*") { + query = "SELECT * FROM table ORDER BY company_code"; +} else { + query = "SELECT * FROM table WHERE company_code = $1 AND company_code != '*'"; + params = [companyCode]; +} +\`\`\` + +# Your Domain (ONLY these!) +- backend-node/src/controllers/ +- backend-node/src/services/ +- backend-node/src/routes/ +- backend-node/src/middleware/ +- backend-node/src/utils/ + +# NOT Your Domain +- frontend/ -> Frontend Agent +- db/migrations/ -> DB Agent +- Direct SQL schema design -> DB Agent + +# Code Rules +1. TypeScript strict mode +2. Error handling with try/catch +3. Comments in Korean +4. Follow existing code patterns +5. Use logger for important operations + +# Response Format (JSON) - BE CONCISE! +{ + "status": "success | partial | failed", + "confidence": "high | medium | low", + "result": { + "summary": "one line summary", + "details": "brief explanation", + "files_affected": ["paths"], + "code_changes": [{"file": "path", "action": "create|modify", "content": "code"}] + }, + "needs_from_others": [], + "questions": [] +} + +# IMPORTANT: Keep responses SHORT. No unnecessary explanations.`; + +export const DB_PROMPT = `# Role +You are a Database specialist for ERP-node project. +Stack: PostgreSQL + Raw Query (no ORM). Migrations in db/migrations/. + +# CRITICAL PROJECT RULES + +## 1. Multi-tenancy (ABSOLUTE MUST!) +- ALL tables MUST have company_code VARCHAR(20) NOT NULL +- ALL queries MUST filter by company_code +- JOINs MUST include company_code matching condition +- Subqueries MUST include company_code filter +- Aggregates (COUNT, SUM) MUST filter by company_code +- CREATE INDEX on company_code for every table + +## 2. company_code = "*" Meaning +- NOT shared/common data! +- Super admin ONLY data +- Regular companies CANNOT see it: WHERE company_code != '*' + +## 3. Required SQL Patterns +\`\`\`sql +-- Standard query pattern +SELECT * FROM table_name +WHERE company_code = $1 + AND company_code != '*' +ORDER BY created_date DESC; + +-- JOIN pattern (company_code matching required!) +SELECT a.*, b.name +FROM table_a a +LEFT JOIN table_b b ON a.ref_id = b.id + AND a.company_code = b.company_code +WHERE a.company_code = $1; +\`\`\` + +## 4. Migration Rules +- File naming: NNN_description.sql (e.g., 034_add_new_table.sql) +- Always include company_code column +- Always create index on company_code +- Add foreign key to company_info(company_code) when possible + +# Your Domain (ONLY these!) +- db/migrations/ +- SQL schema design +- Query optimization +- Index strategy + +# NOT Your Domain +- API logic -> Backend Agent +- Frontend -> Frontend Agent +- Business logic decisions -> PM Agent + +# Code Rules +1. PostgreSQL syntax only +2. Parameter binding ($1, $2) - prevent SQL injection +3. Consider indexes for frequently queried columns +4. Use COALESCE for NULL handling +5. Use TIMESTAMPTZ for dates + +# Response Format (JSON) - BE CONCISE! +{ + "status": "success | partial | failed", + "confidence": "high | medium | low", + "result": { + "summary": "one line summary", + "details": "brief explanation", + "schema_info": {"tables": [], "columns": [], "indexes": []}, + "code_changes": [{"file": "path", "action": "create|modify", "content": "sql"}] + }, + "performance_notes": [], + "questions": [] +} + +# IMPORTANT: Keep responses SHORT. Focus on schema and queries only.`; + +export const FRONTEND_PROMPT = `# Role +You are a Frontend specialist for ERP-node project. +Stack: Next.js 14 + React + TypeScript + Tailwind CSS + shadcn/ui. + +# CRITICAL PROJECT RULES + +## 1. API Client (ABSOLUTE RULE!) +- NEVER use fetch() directly! +- ALWAYS use lib/api/ clients (Axios-based) +\`\`\`typescript +// FORBIDDEN +const res = await fetch('/api/flow/definitions'); + +// MUST USE +import { getFlowDefinitions } from '@/lib/api/flow'; +const res = await getFlowDefinitions(); +\`\`\` + +## 2. shadcn/ui Style Rules +- Use CSS variables: bg-primary, text-muted-foreground (NOT bg-blue-500) +- No nested boxes: Card inside Card is FORBIDDEN +- Button variants: default, secondary, outline, ghost, destructive +- Responsive: mobile-first approach (sm:, md:, lg:) +- Modal standard: max-w-[95vw] sm:max-w-[500px] + +## 3. Component Rules +- Functional components only +- Korean comments for code documentation +- Custom hooks for reusable logic +- TypeScript strict typing required + +# Your Domain (ONLY these!) +- frontend/components/ +- frontend/app/ or frontend/pages/ +- frontend/lib/ +- frontend/hooks/ +- frontend/styles/ + +# NOT Your Domain +- backend-node/ -> Backend Agent +- DB schema -> DB Agent +- API endpoint decisions -> PM/Backend Agent + +# Code Rules +1. TypeScript strict mode +2. React functional components with hooks +3. Prefer shadcn/ui components +4. Use cn() utility for conditional classes +5. Comments in Korean + +# Response Format (JSON) - BE CONCISE! +{ + "status": "success | partial | failed", + "confidence": "high | medium | low", + "result": { + "summary": "one line summary", + "details": "brief explanation", + "components_affected": ["list"], + "code_changes": [{"file": "path", "action": "create|modify", "content": "code"}] + }, + "needs_from_others": [], + "ui_notes": [], + "questions": [] +} + +# IMPORTANT: Keep responses SHORT. No lengthy analysis unless explicitly asked.`; + +// Agent configuration map +export const AGENT_CONFIGS = { + pm: { + model: 'claude-opus-4-5-20250214', + systemPrompt: PM_PROMPT, + maxTokens: 4096, + }, + backend: { + model: 'claude-sonnet-4-20250514', + systemPrompt: BACKEND_PROMPT, + maxTokens: 4096, + }, + db: { + model: 'claude-sonnet-4-20250514', + systemPrompt: DB_PROMPT, + maxTokens: 4096, + }, + frontend: { + model: 'claude-sonnet-4-20250514', + systemPrompt: FRONTEND_PROMPT, + maxTokens: 4096, + }, +} as const; diff --git a/mcp-agent-orchestrator/src/agents/types.ts b/mcp-agent-orchestrator/src/agents/types.ts new file mode 100644 index 00000000..ad045ba6 --- /dev/null +++ b/mcp-agent-orchestrator/src/agents/types.ts @@ -0,0 +1,63 @@ +/** + * Multi-Agent System ํƒ€์ž… ์ •์˜ + */ + +// ์—์ด์ „ํŠธ ํƒ€์ž… +export type AgentType = 'pm' | 'backend' | 'db' | 'frontend'; + +// ์—์ด์ „ํŠธ ์„ค์ • +export interface AgentConfig { + model: string; + systemPrompt: string; + maxTokens: number; +} + +// ์ž‘์—… ์š”์ฒญ +export interface TaskRequest { + agent: AgentType; + task: string; + context?: string; +} + +// ์ž‘์—… ์‘๋‹ต ์ƒํƒœ +export type ResponseStatus = 'success' | 'partial' | 'failed' | 'need_clarification'; + +// ํ™•์‹ ๋„ +export type ConfidenceLevel = 'high' | 'medium' | 'low'; + +// ์ฝ”๋“œ ๋ณ€๊ฒฝ +export interface CodeChange { + file: string; + action: 'create' | 'modify' | 'delete'; + content?: string; + lineStart?: number; + lineEnd?: number; +} + +// ์—์ด์ „ํŠธ ์‘๋‹ต +export interface AgentResponse { + status: ResponseStatus; + confidence: ConfidenceLevel; + result?: { + summary: string; + details: string; + filesAffected?: string[]; + codeChanges?: CodeChange[]; + }; + scopeViolations?: string[]; + dependencies?: string[]; + sideEffects?: string[]; + alternatives?: string[]; + questions?: string[]; + needsFromOthers?: { + agent: AgentType; + request: string; + }[]; +} + +// ๋ณ‘๋ ฌ ์š”์ฒญ ๊ฒฐ๊ณผ +export interface ParallelResult { + agent: AgentType; + result: string; + error?: string; +} diff --git a/mcp-agent-orchestrator/src/index.ts b/mcp-agent-orchestrator/src/index.ts new file mode 100644 index 00000000..3634cf70 --- /dev/null +++ b/mcp-agent-orchestrator/src/index.ts @@ -0,0 +1,407 @@ +#!/usr/bin/env node +/** +b * Multi-Agent Orchestrator MCP Server v2.0 + * + * Cursor Agent CLI๋ฅผ ํ™œ์šฉํ•œ ๋ฉ€ํ‹ฐ์—์ด์ „ํŠธ ์‹œ์Šคํ…œ + * - PM (Cursor IDE): ์ „์ฒด ์กฐ์œจ + * - Sub-agents (agent CLI): ์ „๋ฌธ๊ฐ€๋ณ„ ์ž‘์—… ์ˆ˜ํ–‰ + * + * ๋ชจ๋“  AI ํ˜ธ์ถœ์ด Cursor Team Plan์œผ๋กœ ์ฒ˜๋ฆฌ๋จ! + * API ํ‚ค ๋ถˆํ•„์š”! + */ + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { exec } from "child_process"; +import { promisify } from "util"; +import { platform } from "os"; +import { AGENT_CONFIGS } from "./agents/prompts.js"; +import { AgentType, ParallelResult } from "./agents/types.js"; +import { logger } from "./utils/logger.js"; + +const execAsync = promisify(exec); + +// OS ๊ฐ์ง€ +const isWindows = platform() === "win32"; +logger.info(`Platform detected: ${platform()} (isWindows: ${isWindows})`); + +// MCP ์„œ๋ฒ„ ์ƒ์„ฑ +const server = new Server( + { + name: "agent-orchestrator", + version: "2.0.0", + }, + { + capabilities: { + tools: {}, + }, + } +); + +/** + * Cursor Agent CLI๋ฅผ ํ†ตํ•ด ์—์ด์ „ํŠธ ํ˜ธ์ถœ + * Cursor Team Plan ์‚ฌ์šฉ - API ํ‚ค ๋ถˆํ•„์š”! + * + * ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์ง€์›: + * - Windows: cmd /c "echo. | agent ..." (stdin ๋‹ซ๊ธฐ ์œ„ํ•ด) + * - Mac/Linux: ~/.local/bin/agent ์‚ฌ์šฉ + */ +async function callAgentCLI( + agentType: AgentType, + task: string, + context?: string +): Promise { + const config = AGENT_CONFIGS[agentType]; + + // ๋ชจ๋ธ ์„ ํƒ: PM์€ opus, ๋‚˜๋จธ์ง€๋Š” sonnet + const model = agentType === 'pm' ? 'opus-4.5' : 'sonnet-4.5'; + + logger.info(`Calling ${agentType} agent via CLI`, { model, task: task.substring(0, 100) }); + + try { + const userMessage = context + ? `${task}\n\n๋ฐฐ๊ฒฝ ์ •๋ณด:\n${context}` + : task; + + // ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ž„์‹œ ํŒŒ์ผ์— ์ €์žฅํ•˜์—ฌ ์‰˜ ์ด์Šค์ผ€์ดํ”„ ๋ฌธ์ œ ํšŒํ”ผ + const fullPrompt = `${config.systemPrompt}\n\n---\n\n${userMessage}`; + + // Base64 ์ธ์ฝ”๋”ฉ์œผ๋กœ ํŠน์ˆ˜๋ฌธ์ž ๋ฌธ์ œ ํ•ด๊ฒฐ + const encodedPrompt = Buffer.from(fullPrompt).toString('base64'); + + let cmd: string; + let shell: string; + const agentPath = isWindows ? 'agent' : `${process.env.HOME}/.local/bin/agent`; + + if (isWindows) { + // Windows: PowerShell์„ ํ†ตํ•ด Base64 ๋””์ฝ”๋”ฉ ํ›„ ์‹คํ–‰ + cmd = `powershell -Command "$prompt = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${encodedPrompt}')); echo $prompt | ${agentPath} --model ${model} --print"`; + shell = 'powershell.exe'; + } else { + // Mac/Linux: echo๋กœ base64 ๋””์ฝ”๋”ฉ ํ›„ ํŒŒ์ดํ”„ + cmd = `echo "${encodedPrompt}" | base64 -d | ${agentPath} --model ${model} --print`; + shell = '/bin/bash'; + } + + logger.debug(`Executing: ${agentPath} --model ${model} --print`); + + const { stdout, stderr } = await execAsync(cmd, { + cwd: process.cwd(), + maxBuffer: 10 * 1024 * 1024, // 10MB buffer + timeout: 300000, // 5๋ถ„ ํƒ€์ž„์•„์›ƒ + shell, + env: { + ...process.env, + PATH: `${process.env.HOME}/.local/bin:${process.env.PATH}`, + }, + }); + + if (stderr && !stderr.includes('warning') && !stderr.includes('info')) { + logger.warn(`${agentType} agent stderr`, { stderr: stderr.substring(0, 500) }); + } + + logger.info(`${agentType} agent completed via CLI`); + return stdout.trim(); + } catch (error) { + logger.error(`${agentType} agent CLI error`, error); + throw error; + } +} + +/** + * ๋„๊ตฌ ๋ชฉ๋ก ํ•ธ๋“ค๋Ÿฌ + */ +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "ask_backend_agent", + description: + "๋ฐฑ์—”๋“œ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ์งˆ๋ฌธํ•˜๊ฑฐ๋‚˜ ์ž‘์—…์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. " + + "API ์„ค๊ณ„, ์„œ๋น„์Šค ๋กœ์ง, ๋ผ์šฐํŒ…, ๋ฏธ๋“ค์›จ์–ด ๊ด€๋ จ ์ž‘์—…์— ์‚ฌ์šฉํ•˜์„ธ์š”. " + + "๋‹ด๋‹น ํด๋”: backend-node/src/ (Cursor Agent CLI, sonnet-4.5 ๋ชจ๋ธ)" + + "์ฃผ์˜: ๋‹จ์ˆœ ํŒŒ์ผ ์ฝ๊ธฐ/์ˆ˜์ •์€ PM์ด ์ง์ ‘ ์ฒ˜๋ฆฌํ•˜์„ธ์š”. ๊นŠ์€ ๋ถ„์„์ด ํ•„์š”ํ•  ๋•Œ๋งŒ ํ˜ธ์ถœ!", + inputSchema: { + type: "object" as const, + properties: { + task: { + type: "string", + description: "๋ฐฑ์—”๋“œ ์—์ด์ „ํŠธ์—๊ฒŒ ์š”์ฒญํ•  ์ž‘์—… ๋‚ด์šฉ", + }, + context: { + type: "string", + description: "์ž‘์—…์— ํ•„์š”ํ•œ ๋ฐฐ๊ฒฝ ์ •๋ณด (์„ ํƒ์‚ฌํ•ญ)", + }, + }, + required: ["task"], + }, + }, + { + name: "ask_db_agent", + description: + "DB ์ „๋ฌธ๊ฐ€์—๊ฒŒ ์งˆ๋ฌธํ•˜๊ฑฐ๋‚˜ ์ž‘์—…์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. " + + "์Šคํ‚ค๋งˆ ์„ค๊ณ„, SQL ์ฟผ๋ฆฌ, MyBatis ๋งคํผ, ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ด€๋ จ ์ž‘์—…์— ์‚ฌ์šฉํ•˜์„ธ์š”. " + + "๋‹ด๋‹น ํด๋”: src/com/pms/mapper/, db/ (Cursor Agent CLI, sonnet-4.5 ๋ชจ๋ธ)" + + "์ฃผ์˜: ๋‹จ์ˆœ ์Šคํ‚ค๋งˆ ํ™•์ธ์€ PM์ด ์ง์ ‘ ์ฒ˜๋ฆฌํ•˜์„ธ์š”. ๋ณต์žกํ•œ ์ฟผ๋ฆฌ ์„ค๊ณ„/์ตœ์ ํ™” ์‹œ์—๋งŒ ํ˜ธ์ถœ!", + inputSchema: { + type: "object" as const, + properties: { + task: { + type: "string", + description: "DB ์—์ด์ „ํŠธ์—๊ฒŒ ์š”์ฒญํ•  ์ž‘์—… ๋‚ด์šฉ", + }, + context: { + type: "string", + description: "์ž‘์—…์— ํ•„์š”ํ•œ ๋ฐฐ๊ฒฝ ์ •๋ณด (์„ ํƒ์‚ฌํ•ญ)", + }, + }, + required: ["task"], + }, + }, + { + name: "ask_frontend_agent", + description: + "ํ”„๋ก ํŠธ์—”๋“œ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ์งˆ๋ฌธํ•˜๊ฑฐ๋‚˜ ์ž‘์—…์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. " + + "React ์ปดํฌ๋„ŒํŠธ, ํŽ˜์ด์ง€, ์Šคํƒ€์ผ๋ง, ์ƒํƒœ๊ด€๋ฆฌ ๊ด€๋ จ ์ž‘์—…์— ์‚ฌ์šฉํ•˜์„ธ์š”. " + + "๋‹ด๋‹น ํด๋”: frontend/ (Cursor Agent CLI, sonnet-4.5 ๋ชจ๋ธ)" + + "์ฃผ์˜: ๋‹จ์ˆœ ์ปดํฌ๋„ŒํŠธ ์ฝ๊ธฐ/์ˆ˜์ •์€ PM์ด ์ง์ ‘ ์ฒ˜๋ฆฌํ•˜์„ธ์š”. ๊ตฌ์กฐ ๋ถ„์„์ด ํ•„์š”ํ•  ๋•Œ๋งŒ ํ˜ธ์ถœ!", + inputSchema: { + type: "object" as const, + properties: { + task: { + type: "string", + description: "ํ”„๋ก ํŠธ์—”๋“œ ์—์ด์ „ํŠธ์—๊ฒŒ ์š”์ฒญํ•  ์ž‘์—… ๋‚ด์šฉ", + }, + context: { + type: "string", + description: "์ž‘์—…์— ํ•„์š”ํ•œ ๋ฐฐ๊ฒฝ ์ •๋ณด (์„ ํƒ์‚ฌํ•ญ)", + }, + }, + required: ["task"], + }, + }, + { + name: "parallel_ask", + description: + "์—ฌ๋Ÿฌ ์ „๋ฌธ๊ฐ€์—๊ฒŒ ๋™์‹œ์— ์งˆ๋ฌธํ•ฉ๋‹ˆ๋‹ค (์ง„์งœ ๋ณ‘๋ ฌ ์‹คํ–‰!). " + + "3๊ฐœ ์˜์—ญ(FE+BE+DB) ํฌ๋กœ์Šค๋„๋ฉ”์ธ ๋ถ„์„์ด ํ•„์š”ํ•  ๋•Œ๋งŒ ์‚ฌ์šฉํ•˜์„ธ์š”. " + + "์ฃผ์˜: ํ˜ธ์ถœ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆผ! ๋‹จ์ˆœ ์ž‘์—…์€ PM์ด ์ง์ ‘ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ํ›จ์”ฌ ๋น ๋ฆ…๋‹ˆ๋‹ค. " + + "์ ํ•ฉํ•œ ๊ฒฝ์šฐ: ์ „์ฒด ์•„ํ‚คํ…์ฒ˜ ํŒŒ์•…, ๋Œ€๊ทœ๋ชจ ๋ฆฌํŒฉํ† ๋ง ๊ณ„ํš, ํฌ๋กœ์Šค๋„๋ฉ”์ธ ์˜ํ–ฅ ๋ถ„์„", + inputSchema: { + type: "object" as const, + properties: { + requests: { + type: "array", + description: "๊ฐ ์—์ด์ „ํŠธ์—๊ฒŒ ๋ณด๋‚ผ ์š”์ฒญ ๋ชฉ๋ก", + items: { + type: "object", + properties: { + agent: { + type: "string", + enum: ["backend", "db", "frontend"], + description: "์š”์ฒญํ•  ์—์ด์ „ํŠธ ํƒ€์ž…", + }, + task: { + type: "string", + description: "ํ•ด๋‹น ์—์ด์ „ํŠธ์—๊ฒŒ ์š”์ฒญํ•  ์ž‘์—…", + }, + context: { + type: "string", + description: "๋ฐฐ๊ฒฝ ์ •๋ณด (์„ ํƒ์‚ฌํ•ญ)", + }, + }, + required: ["agent", "task"], + }, + }, + }, + required: ["requests"], + }, + }, + { + name: "get_agent_info", + description: + "์—์ด์ „ํŠธ ์‹œ์Šคํ…œ์˜ ํ˜„์žฌ ์ƒํƒœ์™€ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์—์ด์ „ํŠธ ์ •๋ณด๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.", + inputSchema: { + type: "object" as const, + properties: {}, + }, + }, + ], + }; +}); + +/** + * ๋„๊ตฌ ํ˜ธ์ถœ ํ•ธ๋“ค๋Ÿฌ + */ +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + logger.info(`Tool called: ${name}`); + + try { + switch (name) { + case "ask_backend_agent": { + const { task, context } = args as { task: string; context?: string }; + const result = await callAgentCLI("backend", task, context); + return { + content: [{ type: "text" as const, text: result }], + }; + } + + case "ask_db_agent": { + const { task, context } = args as { task: string; context?: string }; + const result = await callAgentCLI("db", task, context); + return { + content: [{ type: "text" as const, text: result }], + }; + } + + case "ask_frontend_agent": { + const { task, context } = args as { task: string; context?: string }; + const result = await callAgentCLI("frontend", task, context); + return { + content: [{ type: "text" as const, text: result }], + }; + } + + case "parallel_ask": { + const { requests } = args as { + requests: Array<{ + agent: "backend" | "db" | "frontend"; + task: string; + context?: string; + }>; + }; + + logger.info(`Parallel ask to ${requests.length} agents (TRUE PARALLEL!)`); + + // ์ง„์งœ ๋ณ‘๋ ฌ ์‹คํ–‰! ๋ชจ๋“  ์—์ด์ „ํŠธ๊ฐ€ ๋™์‹œ์— ์ž‘์—… + const results: ParallelResult[] = await Promise.all( + requests.map(async (req) => { + try { + const result = await callAgentCLI(req.agent, req.task, req.context); + return { agent: req.agent, result }; + } catch (error) { + return { + agent: req.agent, + result: "", + error: error instanceof Error ? error.message : "Unknown error", + }; + } + }) + ); + + // ๊ฒฐ๊ณผ๋ฅผ ๋ณด๊ธฐ ์ข‹๊ฒŒ ํฌ๋งทํŒ… + const formattedResults = results.map((r) => { + const header = `\n${"=".repeat(60)}\n## ${r.agent.toUpperCase()} Agent ์‘๋‹ต\n${"=".repeat(60)}\n`; + if (r.error) { + return `${header}โŒ ์—๋Ÿฌ: ${r.error}`; + } + return `${header}${r.result}`; + }); + + return { + content: [ + { + type: "text" as const, + text: formattedResults.join("\n"), + }, + ], + }; + } + + case "get_agent_info": { + const info = { + system: "Multi-Agent Orchestrator v2.0", + version: "2.0.0", + backend: "Cursor Agent CLI (Team Plan)", + cliPath: `${process.env.HOME}/.local/bin/agent`, + apiKey: "NOT REQUIRED! Using Cursor Team Plan credits", + agents: { + pm: { + role: "Project Manager", + model: "opus-4.5 (Cursor IDE์—์„œ ์ง์ ‘)", + description: "์ „์ฒด ์กฐ์œจ, ์‚ฌ์šฉ์ž ์˜๋„ ํŒŒ์•…, ์ž‘์—… ๋ถ„๋ฐฐ", + }, + backend: { + role: "Backend Specialist", + model: "sonnet-4.5 (via Agent CLI)", + description: "API, ์„œ๋น„์Šค ๋กœ์ง, ๋ผ์šฐํŒ… ๋‹ด๋‹น", + folder: "backend-node/src/", + }, + db: { + role: "Database Specialist", + model: "sonnet-4.5 (via Agent CLI)", + description: "์Šคํ‚ค๋งˆ, ์ฟผ๋ฆฌ, ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋‹ด๋‹น", + folder: "src/com/pms/mapper/, db/", + }, + frontend: { + role: "Frontend Specialist", + model: "sonnet-4.5 (via Agent CLI)", + description: "์ปดํฌ๋„ŒํŠธ, ํŽ˜์ด์ง€, ์Šคํƒ€์ผ๋ง ๋‹ด๋‹น", + folder: "frontend/", + }, + }, + features: { + parallel_execution: true, + cursor_team_plan: true, + cursor_agent_cli: true, + separate_api_key: false, + cross_platform: true, + }, + usage: { + single_agent: "ask_backend_agent, ask_db_agent, ask_frontend_agent", + parallel: "parallel_ask๋กœ ์—ฌ๋Ÿฌ ์—์ด์ „ํŠธ ๋™์‹œ ํ˜ธ์ถœ", + workflow: "1. parallel_ask๋กœ ์ •๋ณด ์ˆ˜์ง‘ โ†’ 2. ๊ฐœ๋ณ„ ์—์ด์ „ํŠธ๋กœ ์ž‘์—… ๋ถ„๋ฐฐ", + }, + }; + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(info, null, 2), + }, + ], + }; + } + + default: + throw new Error(`Unknown tool: ${name}`); + } + } catch (error) { + logger.error(`Tool error: ${name}`, error); + return { + content: [ + { + type: "text" as const, + text: `โŒ ์—๋Ÿฌ ๋ฐœ์ƒ: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + isError: true, + }; + } +}); + +/** + * ์„œ๋ฒ„ ์‹œ์ž‘ + */ +async function main() { + logger.info("Starting Multi-Agent Orchestrator MCP Server v2.0..."); + logger.info(`Backend: Cursor Agent CLI (${process.env.HOME}/.local/bin/agent)`); + logger.info("Credits: Cursor Team Plan - No API Key Required!"); + + const transport = new StdioServerTransport(); + await server.connect(transport); + + logger.info("MCP Server connected and ready!"); +} + +main().catch((error) => { + logger.error("Server failed to start", error); + process.exit(1); +}); diff --git a/mcp-agent-orchestrator/src/utils/logger.ts b/mcp-agent-orchestrator/src/utils/logger.ts new file mode 100644 index 00000000..9e74d552 --- /dev/null +++ b/mcp-agent-orchestrator/src/utils/logger.ts @@ -0,0 +1,55 @@ +/** + * ๊ฐ„๋‹จํ•œ ๋กœ๊น… ์œ ํ‹ธ๋ฆฌํ‹ฐ + */ + +type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +const LOG_LEVELS: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, +}; + +// ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๋กœ๊ทธ ๋ ˆ๋ฒจ ์„ค์ • (๊ธฐ๋ณธ: info) +const currentLevel = (process.env.LOG_LEVEL as LogLevel) || 'info'; + +function shouldLog(level: LogLevel): boolean { + return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel]; +} + +function formatMessage(level: LogLevel, message: string, data?: unknown): string { + const timestamp = new Date().toISOString(); + const prefix = `[${timestamp}] [${level.toUpperCase()}]`; + + if (data) { + return `${prefix} ${message} ${JSON.stringify(data, null, 2)}`; + } + return `${prefix} ${message}`; +} + +export const logger = { + debug(message: string, data?: unknown): void { + if (shouldLog('debug')) { + console.error(formatMessage('debug', message, data)); + } + }, + + info(message: string, data?: unknown): void { + if (shouldLog('info')) { + console.error(formatMessage('info', message, data)); + } + }, + + warn(message: string, data?: unknown): void { + if (shouldLog('warn')) { + console.error(formatMessage('warn', message, data)); + } + }, + + error(message: string, data?: unknown): void { + if (shouldLog('error')) { + console.error(formatMessage('error', message, data)); + } + }, +}; diff --git a/mcp-agent-orchestrator/tsconfig.json b/mcp-agent-orchestrator/tsconfig.json new file mode 100644 index 00000000..c974e14e --- /dev/null +++ b/mcp-agent-orchestrator/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "build"] +} diff --git a/my_layout.json b/my_layout.json new file mode 100644 index 00000000..39d27b25 Binary files /dev/null and b/my_layout.json differ diff --git a/scripts/dev/start-all-parallel.bat b/scripts/dev/start-all-parallel.bat new file mode 100644 index 00000000..08049b48 --- /dev/null +++ b/scripts/dev/start-all-parallel.bat @@ -0,0 +1,118 @@ +@echo off +chcp 65001 >nul +setlocal EnableDelayedExpansion + +REM ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์žˆ๋Š” ๋””๋ ‰ํ† ๋ฆฌ์—์„œ ๋ฃจํŠธ๋กœ ์ด๋™ +cd /d "%~dp0\..\.." + +REM ์‹œ์ž‘ ์‹œ๊ฐ„ ๊ธฐ๋ก +set START_TIME=%DATE% %TIME% + +echo ============================================ +echo WACE ์†”๋ฃจ์…˜ - ์ „์ฒด ์„œ๋น„์Šค ์‹œ์ž‘ (๋ณ‘๋ ฌ ์ตœ์ ํ™”) +echo ============================================ +echo [์‹œ์ž‘ ์‹œ๊ฐ„] %START_TIME% +echo. + +REM Docker Desktop ์‹คํ–‰ ํ™•์ธ +echo [1/5] Docker Desktop ์ƒํƒœ ํ™•์ธ ์ค‘... +docker --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo [ERROR] Docker Desktop์ด ์‹คํ–‰๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค! + echo Docker Desktop์„ ๋จผ์ € ์‹คํ–‰ํ•ด์ฃผ์„ธ์š”. + pause + exit /b 1 +) +echo [OK] Docker Desktop์ด ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค. +echo. + +REM ๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ ๋ฐ ์ด๋ฏธ์ง€ ์ •๋ฆฌ +echo [2/5] ๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ ๋ฐ ์ด๋ฏธ์ง€ ์ •๋ฆฌ ์ค‘... +docker rm -f pms-backend-win pms-frontend-win 2>nul +docker rmi -f erp-node-backend erp-node-frontend 2>nul +docker network rm pms-network 2>nul +docker network create pms-network 2>nul +docker system prune -f >nul 2>&1 +echo [OK] ์ปจํ…Œ์ด๋„ˆ ๋ฐ ์ด๋ฏธ์ง€ ์ •๋ฆฌ ์™„๋ฃŒ +echo. + +REM ๋ณ‘๋ ฌ ๋นŒ๋“œ (docker-compose ์ž์ฒด๊ฐ€ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ) +echo [3/5] ์ด๋ฏธ์ง€ ๋นŒ๋“œ ์ค‘... (๋ฐฑ์—”๋“œ + ํ”„๋ก ํŠธ์—”๋“œ ๋ณ‘๋ ฌ) +echo ์ด ์ž‘์—…์€ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค... +echo. + +REM ๋ฐฑ์—”๋“œ ๋นŒ๋“œ (์บ์‹œ ์—†์ด ์™„์ „ ์žฌ๋นŒ๋“œ) +docker-compose -f docker-compose.backend.win.yml build --no-cache +if %errorlevel% neq 0 ( + echo [ERROR] ๋ฐฑ์—”๋“œ ๋นŒ๋“œ ์‹คํŒจ! + pause + exit /b 1 +) +echo [OK] ๋ฐฑ์—”๋“œ ๋นŒ๋“œ ์™„๋ฃŒ +echo. + +REM ํ”„๋ก ํŠธ์—”๋“œ ๋นŒ๋“œ (์บ์‹œ ์—†์ด ์™„์ „ ์žฌ๋นŒ๋“œ) +docker-compose -f docker-compose.frontend.win.yml build --no-cache +if %errorlevel% neq 0 ( + echo [ERROR] ํ”„๋ก ํŠธ์—”๋“œ ๋นŒ๋“œ ์‹คํŒจ! + pause + exit /b 1 +) +echo [OK] ํ”„๋ก ํŠธ์—”๋“œ ๋นŒ๋“œ ์™„๋ฃŒ +echo. + +REM ๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ ์ •๋ฆฌ ํ›„ ์„œ๋น„์Šค ์‹œ์ž‘ +echo [4/5] ์„œ๋น„์Šค ์‹œ์ž‘ ์ค‘... +docker-compose -f docker-compose.backend.win.yml down -v 2>nul +docker-compose -f docker-compose.frontend.win.yml down -v 2>nul + +REM ๋ฐฑ์—”๋“œ ์‹œ์ž‘ +echo ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค ์‹œ์ž‘... +docker-compose -f docker-compose.backend.win.yml up -d +if %errorlevel% neq 0 ( + echo [ERROR] ๋ฐฑ์—”๋“œ ์‹œ์ž‘ ์‹คํŒจ! + pause + exit /b 1 +) + +REM ํ”„๋ก ํŠธ์—”๋“œ ์‹œ์ž‘ +echo ํ”„๋ก ํŠธ์—”๋“œ ์„œ๋น„์Šค ์‹œ์ž‘... +docker-compose -f docker-compose.frontend.win.yml up -d +if %errorlevel% neq 0 ( + echo [ERROR] ํ”„๋ก ํŠธ์—”๋“œ ์‹œ์ž‘ ์‹คํŒจ! + pause + exit /b 1 +) +echo [OK] ์„œ๋น„์Šค ์‹œ์ž‘ ์™„๋ฃŒ +echo. + +REM ์•ˆ์ •ํ™” ๋Œ€๊ธฐ +echo [5/5] ์„œ๋น„์Šค ์•ˆ์ •ํ™” ๋Œ€๊ธฐ ์ค‘... (10์ดˆ) +timeout /t 10 /nobreak >nul +echo. + +echo ============================================ +echo [์™„๋ฃŒ] ๋ชจ๋“  ์„œ๋น„์Šค๊ฐ€ ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค! +echo ============================================ +echo. +echo [DATABASE] PostgreSQL: http://39.117.244.52:11132 +echo [BACKEND] Node.js API: http://localhost:8080/api +echo [FRONTEND] Next.js: http://localhost:9771 +echo. +echo [์„œ๋น„์Šค ์ƒํƒœ ํ™•์ธ] +echo docker-compose -f docker-compose.backend.win.yml ps +echo docker-compose -f docker-compose.frontend.win.yml ps +echo. +echo [๋กœ๊ทธ ํ™•์ธ] +echo ๋ฐฑ์—”๋“œ: docker-compose -f docker-compose.backend.win.yml logs -f +echo ํ”„๋ก ํŠธ์—”๋“œ: docker-compose -f docker-compose.frontend.win.yml logs -f +echo. +echo [์„œ๋น„์Šค ์ค‘์ง€] +echo scripts\dev\stop-all.bat +echo. + +set END_TIME=%DATE% %TIME% +echo [์ข…๋ฃŒ ์‹œ๊ฐ„] %END_TIME% +echo ============================================ + +pause diff --git a/scripts/dev/start-all-parallel.ps1 b/scripts/dev/start-all-parallel.ps1 new file mode 100644 index 00000000..732106c6 --- /dev/null +++ b/scripts/dev/start-all-parallel.ps1 @@ -0,0 +1,183 @@ +# WACE ์†”๋ฃจ์…˜ - ์ „์ฒด ์„œ๋น„์Šค ์‹œ์ž‘ (๋ณ‘๋ ฌ ์ตœ์ ํ™”) - PowerShell ๋ฒ„์ „ +# ์‹คํ–‰ ๋ฐฉ๋ฒ•: powershell -ExecutionPolicy Bypass -File .\scripts\dev\start-all-parallel.ps1 + +# UTF-8 ์ถœ๋ ฅ ์„ค์ • +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +$OutputEncoding = [System.Text.Encoding]::UTF8 + +# ์Šคํฌ๋ฆฝํŠธ ์œ„์น˜์—์„œ ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ด๋™ +$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +Set-Location (Join-Path $scriptPath "..\..") + +# ์‹œ์ž‘ ์‹œ๊ฐ„ ๊ธฐ๋ก +$startTime = Get-Date +$startTimeFormatted = $startTime.ToString("yyyy-MM-dd HH:mm:ss") + +Write-Host "" +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "WACE ์†”๋ฃจ์…˜ - ์ „์ฒด ์„œ๋น„์Šค ์‹œ์ž‘ (๋ณ‘๋ ฌ ์ตœ์ ํ™”)" -ForegroundColor Cyan +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "[์‹œ์ž‘ ์‹œ๊ฐ„] $startTimeFormatted" -ForegroundColor Yellow +Write-Host "" + +# Docker Desktop ์‹คํ–‰ ํ™•์ธ +Write-Host "[1/5] Docker Desktop ์ƒํƒœ ํ™•์ธ ์ค‘..." -ForegroundColor White +$dockerCheck = docker --version 2>&1 +if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] Docker Desktop์ด ์‹คํ–‰๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค!" -ForegroundColor Red + Write-Host " Docker Desktop์„ ๋จผ์ € ์‹คํ–‰ํ•ด์ฃผ์„ธ์š”." -ForegroundColor Red + Read-Host "๊ณ„์†ํ•˜๋ ค๋ฉด Enter๋ฅผ ๋ˆ„๋ฅด์„ธ์š”" + exit 1 +} +Write-Host "[OK] Docker Desktop์ด ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค." -ForegroundColor Green +Write-Host "" + +# ๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ ์ •๋ฆฌ +Write-Host "[2/5] ๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ ์ •๋ฆฌ ์ค‘..." -ForegroundColor White +docker rm -f pms-backend-win pms-frontend-win 2>$null | Out-Null +docker network rm pms-network 2>$null | Out-Null +docker network create pms-network 2>$null | Out-Null +Write-Host "[OK] ์ปจํ…Œ์ด๋„ˆ ์ •๋ฆฌ ์™„๋ฃŒ" -ForegroundColor Green +Write-Host "" + +# ๋ณ‘๋ ฌ ๋นŒ๋“œ ์‹œ์ž‘ +$parallelStart = Get-Date +Write-Host "[3/5] ์ด๋ฏธ์ง€ ๋นŒ๋“œ ์ค‘... (๋ฐฑ์—”๋“œ + ํ”„๋ก ํŠธ์—”๋“œ ๋ณ‘๋ ฌ)" -ForegroundColor White +Write-Host " ์ด ์ž‘์—…์€ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค..." -ForegroundColor Gray +Write-Host "" + +# ๋ณ‘๋ ฌ ๋นŒ๋“œ ์‹คํ–‰ +$backendBuildJob = Start-Job -ScriptBlock { + param($workDir) + Set-Location $workDir + $output = docker-compose -f docker-compose.backend.win.yml build 2>&1 + return @{ + Success = $LASTEXITCODE -eq 0 + Output = $output + } +} -ArgumentList $PWD.Path + +$frontendBuildJob = Start-Job -ScriptBlock { + param($workDir) + Set-Location $workDir + $output = docker-compose -f docker-compose.frontend.win.yml build 2>&1 + return @{ + Success = $LASTEXITCODE -eq 0 + Output = $output + } +} -ArgumentList $PWD.Path + +Write-Host " ๋ฐฑ์—”๋“œ ๋นŒ๋“œ ์ง„ํ–‰ ์ค‘..." -ForegroundColor Gray +Write-Host " ํ”„๋ก ํŠธ์—”๋“œ ๋นŒ๋“œ ์ง„ํ–‰ ์ค‘..." -ForegroundColor Gray +Write-Host "" + +# ๋นŒ๋“œ ์™„๋ฃŒ ๋Œ€๊ธฐ +$null = Wait-Job -Job $backendBuildJob, $frontendBuildJob + +$backendResult = Receive-Job -Job $backendBuildJob +$frontendResult = Receive-Job -Job $frontendBuildJob + +Remove-Job -Job $backendBuildJob, $frontendBuildJob -Force + +# ๋นŒ๋“œ ๊ฒฐ๊ณผ ํ™•์ธ +$buildFailed = $false + +if ($backendResult.Success) { + Write-Host "[OK] ๋ฐฑ์—”๋“œ ๋นŒ๋“œ ์™„๋ฃŒ" -ForegroundColor Green +} else { + Write-Host "[ERROR] ๋ฐฑ์—”๋“œ ๋นŒ๋“œ ์‹คํŒจ!" -ForegroundColor Red + Write-Host $backendResult.Output -ForegroundColor Red + $buildFailed = $true +} + +if ($frontendResult.Success) { + Write-Host "[OK] ํ”„๋ก ํŠธ์—”๋“œ ๋นŒ๋“œ ์™„๋ฃŒ" -ForegroundColor Green +} else { + Write-Host "[ERROR] ํ”„๋ก ํŠธ์—”๋“œ ๋นŒ๋“œ ์‹คํŒจ!" -ForegroundColor Red + Write-Host $frontendResult.Output -ForegroundColor Red + $buildFailed = $true +} + +if ($buildFailed) { + Read-Host "๋นŒ๋“œ ์‹คํŒจ. Enter๋ฅผ ๋ˆ„๋ฅด๋ฉด ์ข…๋ฃŒ๋ฉ๋‹ˆ๋‹ค" + exit 1 +} + +$parallelEnd = Get-Date +$parallelDuration = ($parallelEnd - $parallelStart).TotalSeconds +Write-Host "[INFO] ๋นŒ๋“œ ์†Œ์š” ์‹œ๊ฐ„: $([math]::Round($parallelDuration))์ดˆ" -ForegroundColor Yellow +Write-Host "" + +# ์„œ๋น„์Šค ์‹œ์ž‘ +$serviceStart = Get-Date +Write-Host "[4/5] ์„œ๋น„์Šค ์‹œ์ž‘ ์ค‘..." -ForegroundColor White + +# ๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ ์ •๋ฆฌ +docker-compose -f docker-compose.backend.win.yml down -v 2>$null | Out-Null +docker-compose -f docker-compose.frontend.win.yml down -v 2>$null | Out-Null + +# ๋ฐฑ์—”๋“œ ์‹œ์ž‘ +Write-Host " ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค ์‹œ์ž‘..." -ForegroundColor Gray +docker-compose -f docker-compose.backend.win.yml up -d 2>&1 | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] ๋ฐฑ์—”๋“œ ์‹œ์ž‘ ์‹คํŒจ!" -ForegroundColor Red + Read-Host "๊ณ„์†ํ•˜๋ ค๋ฉด Enter๋ฅผ ๋ˆ„๋ฅด์„ธ์š”" + exit 1 +} + +# ํ”„๋ก ํŠธ์—”๋“œ ์‹œ์ž‘ +Write-Host " ํ”„๋ก ํŠธ์—”๋“œ ์„œ๋น„์Šค ์‹œ์ž‘..." -ForegroundColor Gray +docker-compose -f docker-compose.frontend.win.yml up -d 2>&1 | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] ํ”„๋ก ํŠธ์—”๋“œ ์‹œ์ž‘ ์‹คํŒจ!" -ForegroundColor Red + Read-Host "๊ณ„์†ํ•˜๋ ค๋ฉด Enter๋ฅผ ๋ˆ„๋ฅด์„ธ์š”" + exit 1 +} + +Write-Host "[OK] ์„œ๋น„์Šค ์‹œ์ž‘ ์™„๋ฃŒ" -ForegroundColor Green + +$serviceEnd = Get-Date +$serviceDuration = ($serviceEnd - $serviceStart).TotalSeconds +Write-Host "[INFO] ์„œ๋น„์Šค ์‹œ์ž‘ ์†Œ์š” ์‹œ๊ฐ„: $([math]::Round($serviceDuration))์ดˆ" -ForegroundColor Yellow +Write-Host "" + +# ์•ˆ์ •ํ™” ๋Œ€๊ธฐ +Write-Host "[5/5] ์„œ๋น„์Šค ์•ˆ์ •ํ™” ๋Œ€๊ธฐ ์ค‘... (10์ดˆ)" -ForegroundColor White +Start-Sleep -Seconds 10 +Write-Host "" + +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "[์™„๋ฃŒ] ๋ชจ๋“  ์„œ๋น„์Šค๊ฐ€ ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค!" -ForegroundColor Green +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "[DATABASE] PostgreSQL: http://39.117.244.52:11132" -ForegroundColor White +Write-Host "[BACKEND] Node.js API: http://localhost:8080/api" -ForegroundColor White +Write-Host "[FRONTEND] Next.js: http://localhost:9771" -ForegroundColor White +Write-Host "" +Write-Host "[์„œ๋น„์Šค ์ƒํƒœ ํ™•์ธ]" -ForegroundColor Yellow +Write-Host " docker-compose -f docker-compose.backend.win.yml ps" -ForegroundColor Gray +Write-Host " docker-compose -f docker-compose.frontend.win.yml ps" -ForegroundColor Gray +Write-Host "" +Write-Host "[๋กœ๊ทธ ํ™•์ธ]" -ForegroundColor Yellow +Write-Host " ๋ฐฑ์—”๋“œ: docker-compose -f docker-compose.backend.win.yml logs -f" -ForegroundColor Gray +Write-Host " ํ”„๋ก ํŠธ์—”๋“œ: docker-compose -f docker-compose.frontend.win.yml logs -f" -ForegroundColor Gray +Write-Host "" +Write-Host "[์„œ๋น„์Šค ์ค‘์ง€]" -ForegroundColor Yellow +Write-Host " .\scripts\dev\stop-all.ps1" -ForegroundColor Gray +Write-Host "" + +# ์ข…๋ฃŒ ์‹œ๊ฐ„ ๊ณ„์‚ฐ +$endTime = Get-Date +$endTimeFormatted = $endTime.ToString("yyyy-MM-dd HH:mm:ss") +$totalDuration = ($endTime - $startTime).TotalSeconds +$minutes = [math]::Floor($totalDuration / 60) +$seconds = [math]::Round($totalDuration % 60) + +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "[์ข…๋ฃŒ ์‹œ๊ฐ„] $endTimeFormatted" -ForegroundColor Yellow +Write-Host "[์ด ์†Œ์š” ์‹œ๊ฐ„] ${minutes}๋ถ„ ${seconds}์ดˆ" -ForegroundColor Yellow +Write-Host " - ๋นŒ๋“œ: $([math]::Round($parallelDuration))์ดˆ" -ForegroundColor Gray +Write-Host " - ์„œ๋น„์Šค ์‹œ์ž‘: $([math]::Round($serviceDuration))์ดˆ" -ForegroundColor Gray +Write-Host "============================================" -ForegroundColor Cyan + +Read-Host "๊ณ„์†ํ•˜๋ ค๋ฉด Enter๋ฅผ ๋ˆ„๋ฅด์„ธ์š”" diff --git a/scripts/dev/stop-all.bat b/scripts/dev/stop-all.bat new file mode 100644 index 00000000..fa378988 --- /dev/null +++ b/scripts/dev/stop-all.bat @@ -0,0 +1,30 @@ +@echo off +chcp 65001 >nul + +REM ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์žˆ๋Š” ๋””๋ ‰ํ† ๋ฆฌ์—์„œ ๋ฃจํŠธ๋กœ ์ด๋™ +cd /d "%~dp0\..\.." + +echo ============================================ +echo WACE ์†”๋ฃจ์…˜ - ์ „์ฒด ์„œ๋น„์Šค ์ค‘์ง€ +echo ============================================ +echo. + +echo ๐Ÿ›‘ ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค ์ค‘์ง€ ์ค‘... +docker-compose -f docker-compose.backend.win.yml down -v 2>nul +echo โœ… ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค ์ค‘์ง€ ์™„๋ฃŒ +echo. + +echo ๐Ÿ›‘ ํ”„๋ก ํŠธ์—”๋“œ ์„œ๋น„์Šค ์ค‘์ง€ ์ค‘... +docker-compose -f docker-compose.frontend.win.yml down -v 2>nul +echo โœ… ํ”„๋ก ํŠธ์—”๋“œ ์„œ๋น„์Šค ์ค‘์ง€ ์™„๋ฃŒ +echo. + +echo ๐Ÿงน ๋„คํŠธ์›Œํฌ ์ •๋ฆฌ ์ค‘... +docker network rm pms-network 2>nul +echo. + +echo ============================================ +echo ๐ŸŽ‰ ๋ชจ๋“  ์„œ๋น„์Šค๊ฐ€ ์ค‘์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค! +echo ============================================ + +pause diff --git a/scripts/dev/stop-all.ps1 b/scripts/dev/stop-all.ps1 new file mode 100644 index 00000000..5f2dc2c4 --- /dev/null +++ b/scripts/dev/stop-all.ps1 @@ -0,0 +1,33 @@ +# WACE ์†”๋ฃจ์…˜ - ์ „์ฒด ์„œ๋น„์Šค ์ค‘์ง€ - PowerShell ๋ฒ„์ „ +# ์‹คํ–‰ ๋ฐฉ๋ฒ•: powershell -ExecutionPolicy Bypass -File .\scripts\dev\stop-all.ps1 + +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# ์Šคํฌ๋ฆฝํŠธ ์œ„์น˜์—์„œ ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ด๋™ +$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +Set-Location (Join-Path $scriptPath "..\..") + +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "WACE ์†”๋ฃจ์…˜ - ์ „์ฒด ์„œ๋น„์Šค ์ค‘์ง€" -ForegroundColor Cyan +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "" + +Write-Host "๐Ÿ›‘ ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค ์ค‘์ง€ ์ค‘..." -ForegroundColor Yellow +docker-compose -f docker-compose.backend.win.yml down -v 2>$null +Write-Host "โœ… ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค ์ค‘์ง€ ์™„๋ฃŒ" -ForegroundColor Green +Write-Host "" + +Write-Host "๐Ÿ›‘ ํ”„๋ก ํŠธ์—”๋“œ ์„œ๋น„์Šค ์ค‘์ง€ ์ค‘..." -ForegroundColor Yellow +docker-compose -f docker-compose.frontend.win.yml down -v 2>$null +Write-Host "โœ… ํ”„๋ก ํŠธ์—”๋“œ ์„œ๋น„์Šค ์ค‘์ง€ ์™„๋ฃŒ" -ForegroundColor Green +Write-Host "" + +Write-Host "๐Ÿงน ๋„คํŠธ์›Œํฌ ์ •๋ฆฌ ์ค‘..." -ForegroundColor Yellow +docker network rm pms-network 2>$null +Write-Host "" + +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "๐ŸŽ‰ ๋ชจ๋“  ์„œ๋น„์Šค๊ฐ€ ์ค‘์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค!" -ForegroundColor Green +Write-Host "============================================" -ForegroundColor Cyan + +Read-Host "๊ณ„์†ํ•˜๋ ค๋ฉด Enter๋ฅผ ๋ˆ„๋ฅด์„ธ์š”" diff --git a/working_layout.json b/working_layout.json new file mode 100644 index 00000000..d2e241a2 Binary files /dev/null and b/working_layout.json differ
+ # {col.label} @@ -699,7 +679,7 @@ export function SimpleRepeaterTableComponent({ + ์‚ญ์ œ
+
{allowAdd ? (
ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค @@ -724,25 +701,25 @@ export function SimpleRepeaterTableComponent({
+ {rowIndex + 1} + {renderCell(row, col, rowIndex)} + @@ -757,35 +734,29 @@ export function SimpleRepeaterTableComponent({ {/* ํ•ฉ๊ณ„ ํ‘œ์‹œ */} {summaryConfig?.enabled && summaryValues && ( -
-
+
+
{summaryConfig.title && ( -
- {summaryConfig.title} -
+
{summaryConfig.title}
)} -
+
{summaryConfig.fields.map((field) => (
- {field.label} - + {field.label} + {formatSummaryValue(field, summaryValues[field.field] || 0)}
@@ -797,10 +768,10 @@ export function SimpleRepeaterTableComponent({ {/* ํ•˜๋‹จ ํ–‰ ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} {allowAdd && addButtonPosition !== "top" && value.length > 0 && ( -
+
{maxRows !== Infinity && ( - + {value.length} / {maxRows} )} @@ -809,4 +780,3 @@ export function SimpleRepeaterTableComponent({
); } - diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index ca4d57d0..26acaf34 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -115,14 +115,14 @@ const CascadingSelectField: React.FC = ({ type="button" variant="ghost" size="sm" - className="absolute right-0 top-0 h-full px-2 hover:bg-transparent" + className="absolute top-0 right-0 h-full px-2 hover:bg-transparent" onClick={() => !isDisabled && setOpen(!open)} disabled={isDisabled} > {loading ? ( - + ) : ( - + )}
@@ -149,12 +149,7 @@ const CascadingSelectField: React.FC = ({ setOpen(false); }} > - + {option.label} ))} @@ -437,19 +432,19 @@ export function UniversalFormModalComponent({ } // ๐Ÿ†• ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ (ํ’ˆ๋ชฉ ๋ฆฌ์ŠคํŠธ ๋“ฑ) - // ์ฐธ๊ณ : initializeForm์—์„œ DB ๋กœ๋“œ ์‹œ __tableSection_ (๋”๋ธ”), + // ์ฐธ๊ณ : initializeForm์—์„œ DB ๋กœ๋“œ ์‹œ __tableSection_ (๋”๋ธ”), // handleTableDataChange์—์„œ ์ˆ˜์ • ์‹œ _tableSection_ (์‹ฑ๊ธ€) ์‚ฌ์šฉ for (const [key, value] of Object.entries(formData)) { // ์‹ฑ๊ธ€/๋”๋ธ” ์–ธ๋”์Šค์ฝ”์–ด ๋ชจ๋‘ ์ฒ˜๋ฆฌ if ((key.startsWith("_tableSection_") || key.startsWith("__tableSection_")) && Array.isArray(value)) { // ์ €์žฅ ์‹œ์—๋Š” _tableSection_ ํ‚ค๋กœ ํ†ต์ผ (buttonActions.ts์—์„œ ์ด ํ‚ค๋ฅผ ๊ธฐ๋Œ€) - const normalizedKey = key.startsWith("__tableSection_") - ? key.replace("__tableSection_", "_tableSection_") + const normalizedKey = key.startsWith("__tableSection_") + ? key.replace("__tableSection_", "_tableSection_") : key; event.detail.formData[normalizedKey] = value; console.log(`[UniversalFormModal] ํ…Œ์ด๋ธ” ์„น์…˜ ๋ณ‘ํ•ฉ: ${key} โ†’ ${normalizedKey}, ${value.length}๊ฐœ ํ•ญ๋ชฉ`); } - + // ๐Ÿ†• ์›๋ณธ ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ๋„ ๋ณ‘ํ•ฉ (์‚ญ์ œ ์ถ”์ ์šฉ) if (key.startsWith("_originalTableSectionData_") && Array.isArray(value)) { event.detail.formData[key] = value; @@ -948,13 +943,17 @@ export function UniversalFormModalComponent({ // ๊ฐ ํ…Œ์ด๋ธ” ์„น์…˜๋ณ„๋กœ ๋ณ„๋„์˜ ํ‚ค์— ์›๋ณธ ๋ฐ์ดํ„ฐ ์ €์žฅ (groupedDataInitializedRef์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ํ•ญ์ƒ ์ €์žฅ) const originalTableSectionKey = `_originalTableSectionData_${section.id}`; newFormData[originalTableSectionKey] = JSON.parse(JSON.stringify(items)); - console.log(`[initializeForm] ํ…Œ์ด๋ธ” ์„น์…˜ ${section.id}: formData[${originalTableSectionKey}]์— ์›๋ณธ ${items.length}๊ฑด ์ €์žฅ`); - + console.log( + `[initializeForm] ํ…Œ์ด๋ธ” ์„น์…˜ ${section.id}: formData[${originalTableSectionKey}]์— ์›๋ณธ ${items.length}๊ฑด ์ €์žฅ`, + ); + // ๊ธฐ์กด originalGroupedData์—๋„ ์ถ”๊ฐ€ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ) if (!groupedDataInitializedRef.current) { setOriginalGroupedData((prev) => { const newOriginal = [...prev, ...JSON.parse(JSON.stringify(items))]; - console.log(`[initializeForm] ํ…Œ์ด๋ธ” ์„น์…˜ ${section.id}: originalGroupedData์— ${items.length}๊ฑด ์ถ”๊ฐ€ (์ด ${newOriginal.length}๊ฑด)`); + console.log( + `[initializeForm] ํ…Œ์ด๋ธ” ์„น์…˜ ${section.id}: originalGroupedData์— ${items.length}๊ฑด ์ถ”๊ฐ€ (์ด ${newOriginal.length}๊ฑด)`, + ); return newOriginal; }); } @@ -1443,8 +1442,9 @@ export function UniversalFormModalComponent({ if (isNewRecord || hasNoValue) { try { - // allocateNumberingCode๋กœ ์‹ค์ œ ์ˆœ๋ฒˆ ์ฆ๊ฐ€ - const response = await allocateNumberingCode(field.numberingRule.ruleId); + // ๐Ÿ†• ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘ํ•œ ๊ฐ’์„ ์ „๋‹ฌ (์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ถ”์ถœ์šฉ) + const userInputCode = mainData[field.columnName] as string; + const response = await allocateNumberingCode(field.numberingRule.ruleId, userInputCode, mainData); if (response.success && response.data?.generatedCode) { mainData[field.columnName] = response.data.generatedCode; } @@ -1638,12 +1638,12 @@ export function UniversalFormModalComponent({ /> ); } - + // ๐Ÿ†• ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด ์ฒ˜๋ฆฌ (selectOptions.type === "cascading" ๋ฐฉ์‹) if (field.selectOptions?.type === "cascading" && field.selectOptions?.cascading?.parentField) { const cascadingOpts = field.selectOptions.cascading; const parentValue = formData[cascadingOpts.parentField]; - + // selectOptions ๊ธฐ๋ฐ˜ cascading config๋ฅผ CascadingDropdownConfig ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ const cascadingConfig: CascadingDropdownConfig = { enabled: true, @@ -2392,7 +2392,7 @@ function SelectField({ fieldId, value, onChange, optionConfig, placeholder, disa const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); const [inputValue, setInputValue] = useState(value || ""); - + const allowCustomInput = optionConfig?.allowCustomInput || false; useEffect(() => { @@ -2432,14 +2432,14 @@ function SelectField({ fieldId, value, onChange, optionConfig, placeholder, disa type="button" variant="ghost" size="sm" - className="absolute right-0 top-0 h-full px-2 hover:bg-transparent" + className="absolute top-0 right-0 h-full px-2 hover:bg-transparent" onClick={() => !disabled && !loading && setOpen(!open)} disabled={disabled || loading} > {loading ? ( - + ) : ( - + )}
@@ -2462,12 +2462,7 @@ function SelectField({ fieldId, value, onChange, optionConfig, placeholder, disa setOpen(false); }} > - + {option.label} ))} diff --git a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx index 1ac3f547..f8b154d6 100644 --- a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx @@ -555,13 +555,10 @@ export const ButtonPrimaryComponent: React.FC = ({ } // ์Šคํƒ€์ผ ๊ณ„์‚ฐ - // height: 100%๋กœ ๋ถ€๋ชจ(RealtimePreviewDynamic์˜ ๋‚ด๋ถ€ div)์˜ ๋†’์ด๋ฅผ ๋”ฐ๋ผ๊ฐ - // width๋Š” ํ•ญ์ƒ 100%๋กœ ๊ณ ์ • (๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ gridColumns๋กœ ํฌ๊ธฐ ์ œ์–ด) + // ๐Ÿ”ง ์‚ฌ์šฉ์ž๊ฐ€ ์„ค์ •ํ•œ ํฌ๊ธฐ๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ const componentStyle: React.CSSProperties = { ...component.style, ...style, - width: "100%", - height: "100%", }; // ๋””์ž์ธ ๋ชจ๋“œ ์Šคํƒ€์ผ (border ์†์„ฑ ๋ถ„๋ฆฌํ•˜์—ฌ ์ถฉ๋Œ ๋ฐฉ์ง€) @@ -641,19 +638,17 @@ export const ButtonPrimaryComponent: React.FC = ({ } // ์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ์—๋งŒ ์„ฑ๊ณต ํ† ์ŠคํŠธ ํ‘œ์‹œ - // edit, modal, navigate, excel_upload, barcode_scan ์•ก์…˜์€ ์กฐ์šฉํžˆ ์ฒ˜๋ฆฌ - // (UI ์ „ํ™˜๋งŒ ํ•˜๊ฑฐ๋‚˜ ๋ชจ๋‹ฌ ๋‚ด๋ถ€์—์„œ ์ž์ฒด์ ์œผ๋กœ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ) - const silentSuccessActions = ["edit", "modal", "navigate", "excel_upload", "barcode_scan"]; - if (!silentSuccessActions.includes(actionConfig.type)) { + // save, delete, submit ์•ก์…˜์—์„œ๋งŒ ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ + // ๊ทธ ์™ธ ์•ก์…˜์€ ์กฐ์šฉํžˆ ์ฒ˜๋ฆฌ (๋ถˆํ•„์š”ํ•œ "์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" ํ† ์ŠคํŠธ ๋ฐฉ์ง€) + const successToastActions = ["save", "delete", "submit"]; + if (successToastActions.includes(actionConfig.type)) { // ๊ธฐ๋ณธ ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ๊ฒฐ์ • const defaultSuccessMessage = actionConfig.type === "save" ? "์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." : actionConfig.type === "delete" ? "์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." - : actionConfig.type === "submit" - ? "์ œ์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." - : "์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."; + : "์ œ์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."; // ์ปค์Šคํ…€ ๋ฉ”์‹œ์ง€ ์‚ฌ์šฉ ์กฐ๊ฑด: // 1. ์ปค์Šคํ…€ ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๊ณ  @@ -1273,19 +1268,21 @@ export const ButtonPrimaryComponent: React.FC = ({ componentConfig.disabled || isOperationButtonDisabled || isRowSelectionDisabled || statusLoading; // ๊ณตํ†ต ๋ฒ„ํŠผ ์Šคํƒ€์ผ - // ๐Ÿ”ง component.style์—์„œ background/backgroundColor ์ถฉ๋Œ ๋ฐฉ์ง€ + // ๐Ÿ”ง component.style์—์„œ background/backgroundColor ์ถฉ๋Œ ๋ฐฉ์ง€ (width/height๋Š” ํ—ˆ์šฉ) const userStyle = component.style ? Object.fromEntries( - Object.entries(component.style).filter( - ([key]) => !["width", "height", "background", "backgroundColor"].includes(key), - ), + Object.entries(component.style).filter(([key]) => !["background", "backgroundColor"].includes(key)), ) : {}; + // ๐Ÿ”ง ์‚ฌ์šฉ์ž๊ฐ€ ์„ค์ •ํ•œ ํฌ๊ธฐ ์šฐ์„  ์‚ฌ์šฉ, ์—†์œผ๋ฉด 100% + const buttonWidth = component.size?.width ? `${component.size.width}px` : style?.width || "100%"; + const buttonHeight = component.size?.height ? `${component.size.height}px` : style?.height || "100%"; + const buttonElementStyle: React.CSSProperties = { - width: "100%", - height: "100%", - minHeight: "40px", + width: buttonWidth, + height: buttonHeight, + minHeight: "32px", // ๐Ÿ”ง ์ตœ์†Œ ๋†’์ด๋ฅผ 32px๋กœ ์ค„์ž„ border: "none", borderRadius: "0.5rem", backgroundColor: finalDisabled ? "#e5e7eb" : buttonColor, @@ -1308,7 +1305,31 @@ export const ButtonPrimaryComponent: React.FC = ({ ...userStyle, }; - const buttonContent = processedConfig.text !== undefined ? processedConfig.text : component.label || "๋ฒ„ํŠผ"; + // ๋ฒ„ํŠผ ํ…์ŠคํŠธ ๊ฒฐ์ • (๋‹ค์–‘ํ•œ ์†Œ์Šค์—์„œ ๊ฐ€์ ธ์˜ด) + // "๊ธฐ๋ณธ ๋ฒ„ํŠผ"์€ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ ์‹œ ๊ธฐ๋ณธ๊ฐ’์ด๋ฏ€๋กœ ๋ฌด์‹œ + const labelValue = component.label === "๊ธฐ๋ณธ ๋ฒ„ํŠผ" ? undefined : component.label; + + // ์•ก์…˜ ํƒ€์ž…์— ๋”ฐ๋ฅธ ๊ธฐ๋ณธ ํ…์ŠคํŠธ (modal ์•ก์…˜๊ณผ ๋™์ผํ•˜๊ฒŒ) + const actionType = processedConfig.action?.type || component.componentConfig?.action?.type; + const actionDefaultText: Record = { + save: "์ €์žฅ", + delete: "์‚ญ์ œ", + modal: "๋“ฑ๋ก", + edit: "์ˆ˜์ •", + copy: "๋ณต์‚ฌ", + close: "๋‹ซ๊ธฐ", + cancel: "์ทจ์†Œ", + }; + + const buttonContent = + processedConfig.text || + component.webTypeConfig?.text || + component.componentConfig?.text || + component.config?.text || + component.style?.labelText || + labelValue || + actionDefaultText[actionType as string] || + "๋ฒ„ํŠผ"; return ( <> diff --git a/frontend/lib/registry/components/v2-category-manager/V2CategoryManagerComponent.tsx b/frontend/lib/registry/components/v2-category-manager/V2CategoryManagerComponent.tsx index aee1946d..615ea61d 100644 --- a/frontend/lib/registry/components/v2-category-manager/V2CategoryManagerComponent.tsx +++ b/frontend/lib/registry/components/v2-category-manager/V2CategoryManagerComponent.tsx @@ -115,31 +115,36 @@ export function V2CategoryManagerComponent({ }, []); return ( -
- {/* ์ขŒ์ธก: ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ ๋ฆฌ์ŠคํŠธ */} +
+ {/* ์ขŒ์ธก: ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ ๋ฆฌ์ŠคํŠธ - ์Šคํฌ๋กค ๊ฐ€๋Šฅ */} {config.showColumnList && ( <> -
- +
+
+ +
{/* ๋ฆฌ์‚ฌ์ด์ € */}
)} - {/* ์šฐ์ธก: ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๊ด€๋ฆฌ */} -
+ {/* ์šฐ์ธก: ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๊ด€๋ฆฌ - ๊ณ ์ • */} +
{/* ๋ทฐ ๋ชจ๋“œ ํ† ๊ธ€ */} {config.showViewModeToggle && (
@@ -167,8 +172,8 @@ export function V2CategoryManagerComponent({
)} - {/* ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๊ด€๋ฆฌ ์ปดํฌ๋„ŒํŠธ */} -
+ {/* ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๊ด€๋ฆฌ ์ปดํฌ๋„ŒํŠธ - ์Šคํฌ๋กค ๊ฐ€๋Šฅ */} +
{selectedColumn ? ( viewMode === "tree" ? ( { + if (isInteractive && onFormDataChange && columnName) { + onFormDataChange(columnName, value); + } + }; + + // ๋ผ๋ฒจ: style.labelText ์šฐ์„ , ์—†์œผ๋ฉด component.label ์‚ฌ์šฉ + // style.labelDisplay๊ฐ€ false๋ฉด ๋ผ๋ฒจ ์ˆจ๊น€ + const style = component.style || {}; + const effectiveLabel = style.labelDisplay === false ? undefined : (style.labelText || component.label); + + return ( + + ); + } +} + +// ์ž๋™ ๋“ฑ๋ก ์‹คํ–‰ +V2DateRenderer.registerSelf(); + +// Hot Reload ์ง€์› (๊ฐœ๋ฐœ ๋ชจ๋“œ) +if (process.env.NODE_ENV === "development") { + V2DateRenderer.enableHotReload(); +} diff --git a/frontend/lib/registry/components/v2-file-upload/FileManagerModal.tsx b/frontend/lib/registry/components/v2-file-upload/FileManagerModal.tsx new file mode 100644 index 00000000..f6586866 --- /dev/null +++ b/frontend/lib/registry/components/v2-file-upload/FileManagerModal.tsx @@ -0,0 +1,519 @@ +"use client"; + +import React, { useState, useRef } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { FileInfo, FileUploadConfig } from "./types"; +import { + Upload, + Download, + Trash2, + Eye, + File, + FileText, + Image as ImageIcon, + Video, + Music, + Archive, + Presentation, + X, + Star, + ZoomIn, + ZoomOut, + RotateCcw, +} from "lucide-react"; +import { formatFileSize } from "@/lib/utils"; +import { FileViewerModal } from "./FileViewerModal"; + +interface FileManagerModalProps { + isOpen: boolean; + onClose: () => void; + uploadedFiles: FileInfo[]; + onFileUpload: (files: File[]) => Promise; + onFileDownload: (file: FileInfo) => void; + onFileDelete: (file: FileInfo) => void; + onFileView: (file: FileInfo) => void; + onSetRepresentative?: (file: FileInfo) => void; // ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ์„ค์ • ์ฝœ๋ฐฑ + config: FileUploadConfig; + isDesignMode?: boolean; +} + +export const FileManagerModal: React.FC = ({ + isOpen, + onClose, + uploadedFiles, + onFileUpload, + onFileDownload, + onFileDelete, + onFileView, + onSetRepresentative, + config, + isDesignMode = false, +}) => { + const [dragOver, setDragOver] = useState(false); + const [uploading, setUploading] = useState(false); + const [viewerFile, setViewerFile] = useState(null); + const [isViewerOpen, setIsViewerOpen] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); // ์„ ํƒ๋œ ํŒŒ์ผ (์ขŒ์ธก ๋ฏธ๋ฆฌ๋ณด๊ธฐ์šฉ) + const [previewImageUrl, setPreviewImageUrl] = useState(null); // ์ด๋ฏธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ URL + const [zoomLevel, setZoomLevel] = useState(1); // ๐Ÿ” ํ™•๋Œ€/์ถ•์†Œ ๋ ˆ๋ฒจ + const [imagePosition, setImagePosition] = useState({ x: 0, y: 0 }); // ๐Ÿ–ฑ๏ธ ์ด๋ฏธ์ง€ ์œ„์น˜ + const [isDragging, setIsDragging] = useState(false); // ๐Ÿ–ฑ๏ธ ๋“œ๋ž˜๊ทธ ์ค‘ ์—ฌ๋ถ€ + const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); // ๐Ÿ–ฑ๏ธ ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ ์œ„์น˜ + const fileInputRef = useRef(null); + const imageContainerRef = useRef(null); + + // ํŒŒ์ผ ์•„์ด์ฝ˜ ๊ฐ€์ ธ์˜ค๊ธฐ + const getFileIcon = (fileExt: string) => { + const ext = fileExt.toLowerCase(); + + if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) { + return ; + } else if (['pdf', 'doc', 'docx', 'txt', 'rtf'].includes(ext)) { + return ; + } else if (['xls', 'xlsx', 'csv'].includes(ext)) { + return ; + } else if (['ppt', 'pptx'].includes(ext)) { + return ; + } else if (['mp4', 'avi', 'mov', 'webm'].includes(ext)) { + return
)} - {/* ์ขŒ์ธก ๋ฐ์ดํ„ฐ ๋ชฉ๋ก/ํ…Œ์ด๋ธ” */} - {componentConfig.leftPanel?.displayMode === "table" ? ( + {/* ์ขŒ์ธก ๋ฐ์ดํ„ฐ ๋ชฉ๋ก/ํ…Œ์ด๋ธ”/์ปค์Šคํ…€ */} + {console.log("๐Ÿ” [SplitPanel] ์™ผ์ชฝ ํŒจ๋„ displayMode:", componentConfig.leftPanel?.displayMode, "isDesignMode:", isDesignMode)} + {componentConfig.leftPanel?.displayMode === "custom" ? ( + // ๐Ÿ†• ์ปค์Šคํ…€ ๋ชจ๋“œ: ํŒจ๋„ ์•ˆ์— ์ž์œ ๋กญ๊ฒŒ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ +
+ {/* ๐Ÿ†• ์ปค์Šคํ…€ ๋ชจ๋“œ: ๋””์ž์ธ/์‹คํ–‰ ๋ชจ๋“œ ํ†ตํ•ฉ ๋ Œ๋”๋ง */} + {componentConfig.leftPanel?.components && componentConfig.leftPanel.components.length > 0 ? ( +
+ {componentConfig.leftPanel.components.map((comp: PanelInlineComponent) => { + const isSelectedComp = selectedPanelComponentId === comp.id; + const isDraggingComp = draggingCompId === comp.id; + const isResizingComp = resizingCompId === comp.id; + + // ๋“œ๋ž˜๊ทธ/๋ฆฌ์‚ฌ์ด์ฆˆ ์ค‘ ํ‘œ์‹œํ•  ํฌ๊ธฐ/์œ„์น˜ + const displayX = isDraggingComp && dragPosition ? dragPosition.x : (comp.position?.x || 0); + const displayY = isDraggingComp && dragPosition ? dragPosition.y : (comp.position?.y || 0); + const displayWidth = isResizingComp && resizeSize ? resizeSize.width : (comp.size?.width || 200); + const displayHeight = isResizingComp && resizeSize ? resizeSize.height : (comp.size?.height || 100); + + // ์ปดํฌ๋„ŒํŠธ ๋ฐ์ดํ„ฐ๋ฅผ DynamicComponentRenderer ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ + const componentData = { + id: comp.id, + type: "component" as const, + componentType: comp.componentType, + label: comp.label, + position: comp.position || { x: 0, y: 0 }, + size: { width: displayWidth, height: displayHeight }, + componentConfig: comp.componentConfig || {}, + style: comp.style || {}, + }; + + if (isDesignMode) { + // ๋””์ž์ธ ๋ชจ๋“œ: ํƒญ ์ปดํฌ๋„ŒํŠธ์™€ ๋™์ผํ•˜๊ฒŒ ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง + return ( +
{ + e.stopPropagation(); + onSelectPanelComponent?.("left", comp.id, comp); + }} + > + {/* ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค - ์ปดํฌ๋„ŒํŠธ ์™ธ๋ถ€ ์ƒ๋‹จ */} +
handlePanelDragStart(e, "left", comp)} + > +
+ + + {comp.label || comp.componentType} + +
+
+ + +
+
+ + {/* ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง - ํ•ธ๋“ค ์•„๋ž˜์— ๋ณ„๋„ ์˜์—ญ */} +
+ {/* ๐Ÿ†• ์ปจํ…Œ์ด๋„ˆ ์ปดํฌ๋„ŒํŠธ(ํƒญ, ๋ถ„ํ•  ํŒจ๋„)๋Š” ๋“œ๋กญ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ */} +
+ { + handleNestedComponentUpdate("left", comp.id, updatedComp); + }} + // ๐Ÿ†• ์ค‘์ฒฉ๋œ ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ - ๋ถ€๋ชจ ๋ถ„ํ•  ํŒจ๋„ ์ •๋ณด ํฌํ•จ + onSelectTabComponent={(tabId: string, compId: string, tabComp: any) => { + console.log("๐Ÿ” [SplitPanel-Left] onSelectTabComponent ํ˜ธ์ถœ:", { tabId, compId, tabComp, parentSplitPanelId: component.id }); + // ๋ถ€๋ชจ ๋ถ„ํ•  ํŒจ๋„ ์ •๋ณด์™€ ํ•จ๊ป˜ ์ „์—ญ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + const event = new CustomEvent("nested-tab-component-select", { + detail: { + tabsComponentId: comp.id, + tabId, + componentId: compId, + component: tabComp, + parentSplitPanelId: component.id, + parentPanelSide: "left", + }, + }); + window.dispatchEvent(event); + }} + selectedTabComponentId={undefined} + /> +
+ + {/* ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ฐ€์žฅ์ž๋ฆฌ ์˜์—ญ - ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ์—๋งŒ ํ‘œ์‹œ */} + {isSelectedComp && ( + <> + {/* ์˜ค๋ฅธ์ชฝ ๊ฐ€์žฅ์ž๋ฆฌ (๋„ˆ๋น„ ์กฐ์ ˆ) */} +
handlePanelResizeStart(e, "left", comp, "e")} + /> + {/* ์•„๋ž˜ ๊ฐ€์žฅ์ž๋ฆฌ (๋†’์ด ์กฐ์ ˆ) */} +
handlePanelResizeStart(e, "left", comp, "s")} + /> + {/* ์˜ค๋ฅธ์ชฝ ์•„๋ž˜ ๋ชจ์„œ๋ฆฌ (๋„ˆ๋น„+๋†’์ด ์กฐ์ ˆ) */} +
handlePanelResizeStart(e, "left", comp, "se")} + /> + + )} +
+
+ ); + } else { + // ์‹คํ–‰ ๋ชจ๋“œ: DynamicComponentRenderer๋กœ ๋ Œ๋”๋ง + const componentData = { + id: comp.id, + type: "component" as const, + componentType: comp.componentType, + label: comp.label, + position: comp.position || { x: 0, y: 0 }, + size: comp.size || { width: 400, height: 300 }, + componentConfig: comp.componentConfig || {}, + style: comp.style || {}, + }; + + return ( +
+ +
+ ); + } + })} +
+ ) : ( + // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์—†์„ ๋•Œ ๋“œ๋กญ ์˜์—ญ ํ‘œ์‹œ +
+ +

+ ์ปค์Šคํ…€ ๋ชจ๋“œ +

+

+ {isDesignMode ? "์ปดํฌ๋„ŒํŠธ๋ฅผ ๋“œ๋ž˜๊ทธํ•˜์—ฌ ๋ฐฐ์น˜ํ•˜์„ธ์š”" : "๋ฐฐ์น˜๋œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"} +

+
+ )} +
+ ) : componentConfig.leftPanel?.displayMode === "table" ? ( // ํ…Œ์ด๋ธ” ๋ชจ๋“œ
{isDesignMode ? ( @@ -2577,8 +3057,211 @@ export const SplitPanelLayoutComponent: React.FC
)} - {/* ์šฐ์ธก ๋ฐ์ดํ„ฐ */} - {isLoadingRight ? ( + {/* ์šฐ์ธก ๋ฐ์ดํ„ฐ/์ปค์Šคํ…€ */} + {componentConfig.rightPanel?.displayMode === "custom" ? ( + // ๐Ÿ†• ์ปค์Šคํ…€ ๋ชจ๋“œ: ํŒจ๋„ ์•ˆ์— ์ž์œ ๋กญ๊ฒŒ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ +
+ {/* ๐Ÿ†• ์ปค์Šคํ…€ ๋ชจ๋“œ: ๋””์ž์ธ/์‹คํ–‰ ๋ชจ๋“œ ํ†ตํ•ฉ ๋ Œ๋”๋ง */} + {componentConfig.rightPanel?.components && componentConfig.rightPanel.components.length > 0 ? ( +
+ {componentConfig.rightPanel.components.map((comp: PanelInlineComponent) => { + const isSelectedComp = selectedPanelComponentId === comp.id; + const isDraggingComp = draggingCompId === comp.id; + const isResizingComp = resizingCompId === comp.id; + + // ๋“œ๋ž˜๊ทธ/๋ฆฌ์‚ฌ์ด์ฆˆ ์ค‘ ํ‘œ์‹œํ•  ํฌ๊ธฐ/์œ„์น˜ + const displayX = isDraggingComp && dragPosition ? dragPosition.x : (comp.position?.x || 0); + const displayY = isDraggingComp && dragPosition ? dragPosition.y : (comp.position?.y || 0); + const displayWidth = isResizingComp && resizeSize ? resizeSize.width : (comp.size?.width || 200); + const displayHeight = isResizingComp && resizeSize ? resizeSize.height : (comp.size?.height || 100); + + // ์ปดํฌ๋„ŒํŠธ ๋ฐ์ดํ„ฐ๋ฅผ DynamicComponentRenderer ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ + const componentData = { + id: comp.id, + type: "component" as const, + componentType: comp.componentType, + label: comp.label, + position: comp.position || { x: 0, y: 0 }, + size: { width: displayWidth, height: displayHeight }, + componentConfig: comp.componentConfig || {}, + style: comp.style || {}, + }; + + if (isDesignMode) { + // ๋””์ž์ธ ๋ชจ๋“œ: ํƒญ ์ปดํฌ๋„ŒํŠธ์™€ ๋™์ผํ•˜๊ฒŒ ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง + return ( +
{ + e.stopPropagation(); + onSelectPanelComponent?.("right", comp.id, comp); + }} + > + {/* ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค - ์ปดํฌ๋„ŒํŠธ ์™ธ๋ถ€ ์ƒ๋‹จ */} +
handlePanelDragStart(e, "right", comp)} + > +
+ + + {comp.label || comp.componentType} + +
+
+ + +
+
+ + {/* ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง - ํ•ธ๋“ค ์•„๋ž˜์— ๋ณ„๋„ ์˜์—ญ */} +
+ {/* ๐Ÿ†• ์ปจํ…Œ์ด๋„ˆ ์ปดํฌ๋„ŒํŠธ(ํƒญ, ๋ถ„ํ•  ํŒจ๋„)๋Š” ๋“œ๋กญ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ */} +
+ { + handleNestedComponentUpdate("right", comp.id, updatedComp); + }} + // ๐Ÿ†• ์ค‘์ฒฉ๋œ ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ - ๋ถ€๋ชจ ๋ถ„ํ•  ํŒจ๋„ ์ •๋ณด ํฌํ•จ + onSelectTabComponent={(tabId: string, compId: string, tabComp: any) => { + console.log("๐Ÿ” [SplitPanel-Right] onSelectTabComponent ํ˜ธ์ถœ:", { tabId, compId, tabComp, parentSplitPanelId: component.id }); + // ๋ถ€๋ชจ ๋ถ„ํ•  ํŒจ๋„ ์ •๋ณด์™€ ํ•จ๊ป˜ ์ „์—ญ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + const event = new CustomEvent("nested-tab-component-select", { + detail: { + tabsComponentId: comp.id, + tabId, + componentId: compId, + component: tabComp, + parentSplitPanelId: component.id, + parentPanelSide: "right", + }, + }); + window.dispatchEvent(event); + }} + selectedTabComponentId={undefined} + /> +
+ + {/* ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ฐ€์žฅ์ž๋ฆฌ ์˜์—ญ - ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ์—๋งŒ ํ‘œ์‹œ */} + {isSelectedComp && ( + <> + {/* ์˜ค๋ฅธ์ชฝ ๊ฐ€์žฅ์ž๋ฆฌ (๋„ˆ๋น„ ์กฐ์ ˆ) */} +
handlePanelResizeStart(e, "right", comp, "e")} + /> + {/* ์•„๋ž˜ ๊ฐ€์žฅ์ž๋ฆฌ (๋†’์ด ์กฐ์ ˆ) */} +
handlePanelResizeStart(e, "right", comp, "s")} + /> + {/* ์˜ค๋ฅธ์ชฝ ์•„๋ž˜ ๋ชจ์„œ๋ฆฌ (๋„ˆ๋น„+๋†’์ด ์กฐ์ ˆ) */} +
handlePanelResizeStart(e, "right", comp, "se")} + /> + + )} +
+
+ ); + } else { + + return ( +
+ +
+ ); + } + })} +
+ ) : ( + // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์—†์„ ๋•Œ ๋“œ๋กญ ์˜์—ญ ํ‘œ์‹œ +
+ +

+ ์ปค์Šคํ…€ ๋ชจ๋“œ +

+

+ {isDesignMode ? "์ปดํฌ๋„ŒํŠธ๋ฅผ ๋“œ๋ž˜๊ทธํ•˜์—ฌ ๋ฐฐ์น˜ํ•˜์„ธ์š”" : "๋ฐฐ์น˜๋œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"} +

+
+ )} +
+ ) : isLoadingRight ? ( // ๋กœ๋”ฉ ์ค‘
diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx index d678332e..79813d8f 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx @@ -11,7 +11,8 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; // Accordion ์ œ๊ฑฐ - ๋‹จ์ˆœ ์„น์…˜์œผ๋กœ ๋ณ€๊ฒฝ -import { Check, ChevronsUpDown, ArrowRight, Plus, X, ArrowUp, ArrowDown, Trash2, Database, GripVertical } from "lucide-react"; +import { Check, ChevronsUpDown, ArrowRight, Plus, X, ArrowUp, ArrowDown, Trash2, Database, GripVertical, Move } from "lucide-react"; +import { PanelInlineComponent } from "./types"; import { cn } from "@/lib/utils"; import { SplitPanelLayoutConfig, AdditionalTabConfig } from "./types"; import { TableInfo, ColumnInfo } from "@/types/screen"; @@ -1547,7 +1548,7 @@ export const SplitPanelLayoutConfigPanel: React.FCํ‘œ์‹œ ๋ชจ๋“œ + {config.leftPanel?.displayMode === "custom" && ( +

+ ํ™”๋ฉด ๋””์ž์ด๋„ˆ์—์„œ ์ขŒ์ธก ํŒจ๋„์— ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋“œ๋ž˜๊ทธํ•˜์—ฌ ๋ฐฐ์น˜ํ•˜์„ธ์š”. +

+ )}
- {/* ์ขŒ์ธก ํŒจ๋„ ํ‘œ์‹œ ์ปฌ๋Ÿผ ์„ค์ • - ์ฒดํฌ๋ฐ•์Šค ๋ฐฉ์‹ */} + {/* ๐Ÿ†• ์ปค์Šคํ…€ ๋ชจ๋“œ: ๋ฐฐ์น˜๋œ ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก */} + {config.leftPanel?.displayMode === "custom" && ( +
+ + {!config.leftPanel?.components || config.leftPanel.components.length === 0 ? ( +
+ +

+ ๋””์ž์ธ ํ™”๋ฉด์—์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋“œ๋ž˜๊ทธํ•˜์—ฌ ์ถ”๊ฐ€ํ•˜์„ธ์š” +

+
+ ) : ( +
+ {config.leftPanel.components.map((comp: PanelInlineComponent) => ( +
+
+

+ {comp.label || comp.componentType} +

+

+ {comp.componentType} | ์œ„์น˜: ({comp.position?.x || 0}, {comp.position?.y || 0}) | ํฌ๊ธฐ: {comp.size?.width || 0}x{comp.size?.height || 0} +

+
+ +
+ ))} +
+ )} +
+ )} + + {/* ์ขŒ์ธก ํŒจ๋„ ํ‘œ์‹œ ์ปฌ๋Ÿผ ์„ค์ • - ์ฒดํฌ๋ฐ•์Šค ๋ฐฉ์‹ (์ปค์Šคํ…€ ๋ชจ๋“œ๊ฐ€ ์•„๋‹ ๋•Œ๋งŒ) */} + {config.leftPanel?.displayMode !== "custom" && (
@@ -1731,6 +1796,7 @@ export const SplitPanelLayoutConfigPanel: React.FC )}
+ )}
{/* ์ขŒ์ธก ํŒจ๋„ ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง */} @@ -1851,7 +1917,7 @@ export const SplitPanelLayoutConfigPanel: React.FCํ‘œ์‹œ ๋ชจ๋“œ + {config.rightPanel?.displayMode === "custom" && ( +

+ ํ™”๋ฉด ๋””์ž์ด๋„ˆ์—์„œ ์šฐ์ธก ํŒจ๋„์— ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋“œ๋ž˜๊ทธํ•˜์—ฌ ๋ฐฐ์น˜ํ•˜์„ธ์š”. +

+ )}
- {/* ์š”์•ฝ ํ‘œ์‹œ ์„ค์ • (LIST ๋ชจ๋“œ์—์„œ๋งŒ) */} + {/* ๐Ÿ†• ์ปค์Šคํ…€ ๋ชจ๋“œ: ๋ฐฐ์น˜๋œ ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก */} + {config.rightPanel?.displayMode === "custom" && ( +
+ + {!config.rightPanel?.components || config.rightPanel.components.length === 0 ? ( +
+ +

+ ๋””์ž์ธ ํ™”๋ฉด์—์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋“œ๋ž˜๊ทธํ•˜์—ฌ ์ถ”๊ฐ€ํ•˜์„ธ์š” +

+
+ ) : ( +
+ {config.rightPanel.components.map((comp: PanelInlineComponent) => ( +
+
+

+ {comp.label || comp.componentType} +

+

+ {comp.componentType} | ์œ„์น˜: ({comp.position?.x || 0}, {comp.position?.y || 0}) | ํฌ๊ธฐ: {comp.size?.width || 0}x{comp.size?.height || 0} +

+
+ +
+ ))} +
+ )} +
+ )} + + {/* ์š”์•ฝ ํ‘œ์‹œ ์„ค์ • (LIST ๋ชจ๋“œ์—์„œ๋งŒ, ์ปค์Šคํ…€ ๋ชจ๋“œ๊ฐ€ ์•„๋‹ ๋•Œ) */} {(config.rightPanel?.displayMode || "list") === "list" && (
diff --git a/frontend/lib/registry/components/v2-split-panel-layout/types.ts b/frontend/lib/registry/components/v2-split-panel-layout/types.ts index 123dc13a..7ab0dbcb 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/types.ts +++ b/frontend/lib/registry/components/v2-split-panel-layout/types.ts @@ -2,7 +2,13 @@ * SplitPanelLayout ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ์ •์˜ */ -import { DataFilterConfig } from "@/types/screen-management"; +import { DataFilterConfig, TabInlineComponent } from "@/types/screen-management"; + +/** + * ํŒจ๋„ ๋‚ด ์ธ๋ผ์ธ ์ปดํฌ๋„ŒํŠธ (์ปค์Šคํ…€ ๋ชจ๋“œ์šฉ) + * TabInlineComponent์™€ ๋™์ผํ•œ ๊ตฌ์กฐ ์‚ฌ์šฉ + */ +export type PanelInlineComponent = TabInlineComponent; /** * ์ถ”๊ฐ€ ํƒญ ์„ค์ • (์šฐ์ธก ํŒจ๋„๊ณผ ๋™์ผํ•œ ๊ตฌ์กฐ + tabId, label) @@ -118,7 +124,9 @@ export interface SplitPanelLayoutConfig { useCustomTable?: boolean; // ํ™”๋ฉด ๊ธฐ๋ณธ ํ…Œ์ด๋ธ”์ด ์•„๋‹Œ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ ์—ฌ๋ถ€ customTableName?: string; // ์‚ฌ์šฉ์ž ์ง€์ • ํ…Œ์ด๋ธ”๋ช… (useCustomTable์ด true์ผ ๋•Œ) dataSource?: string; // API ์—”๋“œํฌ์ธํŠธ - displayMode?: "list" | "table"; // ํ‘œ์‹œ ๋ชจ๋“œ: ๋ชฉ๋ก ๋˜๋Š” ํ…Œ์ด๋ธ” + displayMode?: "list" | "table" | "custom"; // ํ‘œ์‹œ ๋ชจ๋“œ: ๋ชฉ๋ก, ํ…Œ์ด๋ธ”, ๋˜๋Š” ์ปค์Šคํ…€ + // ๐Ÿ†• ์ปค์Šคํ…€ ๋ชจ๋“œ: ํŒจ๋„ ์•ˆ์— ์ž์œ ๋กญ๊ฒŒ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ (ํƒญ ์ปดํฌ๋„ŒํŠธ์™€ ๋™์ผ ๊ตฌ์กฐ) + components?: PanelInlineComponent[]; showSearch?: boolean; showAdd?: boolean; showEdit?: boolean; // ์ˆ˜์ • ๋ฒ„ํŠผ @@ -185,7 +193,9 @@ export interface SplitPanelLayoutConfig { useCustomTable?: boolean; // ํ™”๋ฉด ๊ธฐ๋ณธ ํ…Œ์ด๋ธ”์ด ์•„๋‹Œ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ ์—ฌ๋ถ€ customTableName?: string; // ์‚ฌ์šฉ์ž ์ง€์ • ํ…Œ์ด๋ธ”๋ช… (useCustomTable์ด true์ผ ๋•Œ) dataSource?: string; - displayMode?: "list" | "table"; // ํ‘œ์‹œ ๋ชจ๋“œ: ๋ชฉ๋ก ๋˜๋Š” ํ…Œ์ด๋ธ” + displayMode?: "list" | "table" | "custom"; // ํ‘œ์‹œ ๋ชจ๋“œ: ๋ชฉ๋ก, ํ…Œ์ด๋ธ”, ๋˜๋Š” ์ปค์Šคํ…€ + // ๐Ÿ†• ์ปค์Šคํ…€ ๋ชจ๋“œ: ํŒจ๋„ ์•ˆ์— ์ž์œ ๋กญ๊ฒŒ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ (ํƒญ ์ปดํฌ๋„ŒํŠธ์™€ ๋™์ผ ๊ตฌ์กฐ) + components?: PanelInlineComponent[]; showSearch?: boolean; showAdd?: boolean; showEdit?: boolean; // ์ˆ˜์ • ๋ฒ„ํŠผ diff --git a/frontend/lib/registry/components/v2-table-grouped/README.md b/frontend/lib/registry/components/v2-table-grouped/README.md new file mode 100644 index 00000000..fc39733e --- /dev/null +++ b/frontend/lib/registry/components/v2-table-grouped/README.md @@ -0,0 +1,162 @@ +# v2-table-grouped (๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ”) + +## ๊ฐœ์š” + +๋ฐ์ดํ„ฐ๋ฅผ ํŠน์ • ์ปฌ๋Ÿผ ๊ธฐ์ค€์œผ๋กœ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค. + +## ์ฃผ์š” ๊ธฐ๋Šฅ + +- **๊ทธ๋ฃนํ™”**: ์ง€์ •๋œ ์ปฌ๋Ÿผ ๊ธฐ์ค€์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋ฃนํ•‘ +- **์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ**: ๊ทธ๋ฃน ํ—ค๋” ํด๋ฆญ์œผ๋กœ ํ•˜์œ„ ํ•ญ๋ชฉ ํ† ๊ธ€ +- **๊ทธ๋ฃน ์š”์•ฝ**: ๊ทธ๋ฃน๋ณ„ ๊ฐœ์ˆ˜, ํ•ฉ๊ณ„, ํ‰๊ท , ์ตœ๋Œ€/์ตœ์†Œ๊ฐ’ ํ‘œ์‹œ +- **์ฒดํฌ๋ฐ•์Šค ์„ ํƒ**: ๊ทธ๋ฃน ๋‹จ์œ„ ๋˜๋Š” ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ์„ ํƒ +- **์ „์ฒด ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ**: ๋ชจ๋“  ๊ทธ๋ฃน ์ผ๊ด„ ํ† ๊ธ€ ๋ฒ„ํŠผ + +## ์‚ฌ์šฉ ์˜ˆ์‹œ + +```tsx +import { TableGroupedComponent } from "./TableGroupedComponent"; + + console.log("์„ ํƒ:", event.selectedItems)} + onRowClick={(event) => console.log("ํ–‰ ํด๋ฆญ:", event.row)} +/> +``` + +## ์„ค์ • ์˜ต์…˜ + +### ๊ธฐ๋ณธ ์„ค์ • + +| ์˜ต์…˜ | ํƒ€์ž… | ๊ธฐ๋ณธ๊ฐ’ | ์„ค๋ช… | +|------|------|--------|------| +| `selectedTable` | string | - | ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ”๋ช… | +| `useCustomTable` | boolean | false | ์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ ์—ฌ๋ถ€ | +| `customTableName` | string | - | ์ปค์Šคํ…€ ํ…Œ์ด๋ธ”๋ช… | +| `columns` | ColumnConfig[] | [] | ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ์„ค์ • | + +### ๊ทธ๋ฃนํ™” ์„ค์ • (groupConfig) + +| ์˜ต์…˜ | ํƒ€์ž… | ๊ธฐ๋ณธ๊ฐ’ | ์„ค๋ช… | +|------|------|--------|------| +| `groupByColumn` | string | - | ๊ทธ๋ฃนํ™” ๊ธฐ์ค€ ์ปฌ๋Ÿผ (ํ•„์ˆ˜) | +| `groupLabelFormat` | string | "{value}" | ๊ทธ๋ฃน ๋ผ๋ฒจ ํ˜•์‹ | +| `defaultExpanded` | boolean | true | ์ดˆ๊ธฐ ํŽผ์นจ ์ƒํƒœ | +| `sortDirection` | "asc" \| "desc" | "asc" | ๊ทธ๋ฃน ์ •๋ ฌ ๋ฐฉํ–ฅ | +| `summary.showCount` | boolean | true | ๊ฐœ์ˆ˜ ํ‘œ์‹œ ์—ฌ๋ถ€ | +| `summary.sumColumns` | string[] | [] | ํ•ฉ๊ณ„ ์ปฌ๋Ÿผ ๋ชฉ๋ก | +| `summary.avgColumns` | string[] | [] | ํ‰๊ท  ์ปฌ๋Ÿผ ๋ชฉ๋ก | + +### ํ‘œ์‹œ ์„ค์ • + +| ์˜ต์…˜ | ํƒ€์ž… | ๊ธฐ๋ณธ๊ฐ’ | ์„ค๋ช… | +|------|------|--------|------| +| `showCheckbox` | boolean | false | ์ฒดํฌ๋ฐ•์Šค ํ‘œ์‹œ | +| `checkboxMode` | "single" \| "multi" | "multi" | ์„ ํƒ ๋ชจ๋“œ | +| `showExpandAllButton` | boolean | true | ์ „์ฒด ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ ๋ฒ„ํŠผ | +| `groupHeaderStyle` | "default" \| "compact" \| "card" | "default" | ๊ทธ๋ฃน ํ—ค๋” ์Šคํƒ€์ผ | +| `rowClickable` | boolean | true | ํ–‰ ํด๋ฆญ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ | +| `maxHeight` | number | 600 | ์ตœ๋Œ€ ๋†’์ด (px) | +| `emptyMessage` | string | "๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค." | ๋นˆ ๋ฐ์ดํ„ฐ ๋ฉ”์‹œ์ง€ | + +## ์ด๋ฒคํŠธ + +### onSelectionChange + +์„ ํƒ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. + +```typescript +interface SelectionChangeEvent { + selectedGroups: string[]; // ์„ ํƒ๋œ ๊ทธ๋ฃน ํ‚ค ๋ชฉ๋ก + selectedItems: any[]; // ์„ ํƒ๋œ ์•„์ดํ…œ ์ „์ฒด + isAllSelected: boolean; // ์ „์ฒด ์„ ํƒ ์—ฌ๋ถ€ +} +``` + +### onGroupToggle + +๊ทธ๋ฃน ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ ์‹œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. + +```typescript +interface GroupToggleEvent { + groupKey: string; // ๊ทธ๋ฃน ํ‚ค + expanded: boolean; // ํŽผ์นจ ์ƒํƒœ +} +``` + +### onRowClick + +ํ–‰ ํด๋ฆญ ์‹œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. + +```typescript +interface RowClickEvent { + row: any; // ํด๋ฆญ๋œ ํ–‰ ๋ฐ์ดํ„ฐ + groupKey: string; // ๊ทธ๋ฃน ํ‚ค + indexInGroup: number; // ๊ทธ๋ฃน ๋‚ด ์ธ๋ฑ์Šค +} +``` + +## ๊ทธ๋ฃน ๋ผ๋ฒจ ํ˜•์‹ + +`groupLabelFormat`์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ”Œ๋ ˆ์ด์Šคํ™€๋”: + +- `{value}`: ๊ทธ๋ฃนํ™” ์ปฌ๋Ÿผ์˜ ๊ฐ’ +- `{์ปฌ๋Ÿผ๋ช…}`: ํ•ด๋‹น ์ปฌ๋Ÿผ์˜ ๊ฐ’ + +**์˜ˆ์‹œ:** +``` +groupLabelFormat: "{item_name} ({item_code}) - {category}" +// ๊ฒฐ๊ณผ: "์ œํ’ˆA (P001) - ์™„์ œํ’ˆ" +``` + +## ํŒŒ์ผ ๊ตฌ์กฐ + +``` +v2-table-grouped/ +โ”œโ”€โ”€ index.ts # Definition +โ”œโ”€โ”€ types.ts # ํƒ€์ž… ์ •์˜ +โ”œโ”€โ”€ config.ts # ๊ธฐ๋ณธ ์„ค์ •๊ฐ’ +โ”œโ”€โ”€ TableGroupedComponent.tsx # ๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ +โ”œโ”€โ”€ TableGroupedConfigPanel.tsx # ์„ค์ • ํŒจ๋„ +โ”œโ”€โ”€ TableGroupedRenderer.tsx # ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๋“ฑ๋ก +โ”œโ”€โ”€ components/ +โ”‚ โ””โ”€โ”€ GroupHeader.tsx # ๊ทธ๋ฃน ํ—ค๋” +โ”œโ”€โ”€ hooks/ +โ”‚ โ””โ”€โ”€ useGroupedData.ts # ๊ทธ๋ฃนํ™” ๋กœ์ง ํ›… +โ””โ”€โ”€ README.md +``` + +## v2-table-list์™€์˜ ์ฐจ์ด์  + +| ํ•ญ๋ชฉ | v2-table-list | v2-table-grouped | +|------|---------------|------------------| +| ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ | ํ”Œ๋žซ ๋ฆฌ์ŠคํŠธ | ๊ณ„์ธต ๊ตฌ์กฐ (๊ทธ๋ฃน > ์•„์ดํ…œ) | +| ๋ Œ๋”๋ง | ํ–‰ ๋‹จ์œ„ | ๊ทธ๋ฃน ํ—ค๋” + ์ƒ์„ธ ํ–‰ | +| ์„ ํƒ | ๊ฐœ๋ณ„ ํ–‰ | ๊ทธ๋ฃน ๋‹จ์œ„ / ๊ฐœ๋ณ„ ๋‹จ์œ„ | +| ์š”์•ฝ | ์ „์ฒด ํ•ฉ๊ณ„ (์„ ํƒ) | ๊ทธ๋ฃน๋ณ„ ์š”์•ฝ | +| ์šฉ๋„ | ์ผ๋ฐ˜ ๋ฐ์ดํ„ฐ ๋ชฉ๋ก | ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋ถ„๋ฅ˜ ๋ฐ์ดํ„ฐ | + +## ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ + +- `v2-table-list`: ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” (๊ทธ๋ฃนํ™” ์—†์Œ) +- `v2-pivot-grid`: ํ”ผ๋ฒ— ํ…Œ์ด๋ธ” (๋‹ค์ฐจ์› ์ง‘๊ณ„) +- `v2-split-panel-layout`: ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ๋ ˆ์ด์•„์›ƒ diff --git a/frontend/lib/registry/components/v2-table-grouped/TableGroupedComponent.tsx b/frontend/lib/registry/components/v2-table-grouped/TableGroupedComponent.tsx new file mode 100644 index 00000000..ddfbdc18 --- /dev/null +++ b/frontend/lib/registry/components/v2-table-grouped/TableGroupedComponent.tsx @@ -0,0 +1,545 @@ +"use client"; + +import React, { useCallback, useEffect, useState, useMemo } from "react"; +import { Loader2, FoldVertical, UnfoldVertical } from "lucide-react"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { TableGroupedComponentProps } from "./types"; +import { useGroupedData } from "./hooks/useGroupedData"; +import { GroupHeader } from "./components/GroupHeader"; +import { useScreenContextOptional } from "@/contexts/ScreenContext"; +import { useTableOptions } from "@/contexts/TableOptionsContext"; +import type { DataProvidable, DataReceivable, DataReceiverConfig } from "@/types/data-transfer"; +import { v2EventBus, V2_EVENTS } from "@/lib/v2-core/events"; + +/** + * v2-table-grouped ๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ + * + * ๋ฐ์ดํ„ฐ๋ฅผ ํŠน์ • ์ปฌ๋Ÿผ ๊ธฐ์ค€์œผ๋กœ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + */ +export function TableGroupedComponent({ + config, + isDesignMode = false, + formData, + onSelectionChange, + onGroupToggle, + onRowClick, + externalData, + isLoading: externalLoading, + error: externalError, + componentId, +}: TableGroupedComponentProps) { + // ํ™”๋ฉด ์ปจํ…์ŠคํŠธ (๋ฐ์ดํ„ฐ ์ œ๊ณต์ž๋กœ ๋“ฑ๋ก) + const screenContext = useScreenContextOptional(); + + // TableOptions Context (๊ฒ€์ƒ‰ํ•„ํ„ฐ ์—ฐ๋™) + const { registerTable, unregisterTable, updateTableDataCount } = useTableOptions(); + + // ์—ฐ๊ฒฐ๋œ ํ•„ํ„ฐ ์ƒํƒœ (๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ ๊ฐ’์œผ๋กœ ํ•„ํ„ฐ๋ง) + const [linkedFilterValues, setLinkedFilterValues] = useState>({}); + + // ํ•„ํ„ฐ ๋ฐ ๊ทธ๋ฃน ์„ค์ • ์ƒํƒœ (๊ฒ€์ƒ‰ํ•„ํ„ฐ ์—ฐ๋™์šฉ) + const [filters, setFilters] = useState([]); + const [grouping, setGrouping] = useState([]); + const [columnVisibility, setColumnVisibility] = useState([]); + + // ๊ทธ๋ฃนํ™” ๋ฐ์ดํ„ฐ ํ›… (๊ฒ€์ƒ‰ ํ•„ํ„ฐ ์ „๋‹ฌ) + const { + groups, + isLoading: hookLoading, + error: hookError, + toggleGroup, + expandAll, + collapseAll, + toggleItemSelection, + toggleGroupSelection, + toggleAllSelection, + selectedItems, + isAllSelected, + isIndeterminate, + refresh, + rawData, + totalCount, + groupCount, + } = useGroupedData(config, externalData, linkedFilterValues); + + const isLoading = externalLoading ?? hookLoading; + const error = externalError ?? hookError; + + // ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ (ํ›…์—์„œ ์ด๋ฏธ ํ•„ํ„ฐ ์ ์šฉ๋จ) + const filteredData = rawData; + + // ์—ฐ๊ฒฐ๋œ ํ•„ํ„ฐ ๊ฐ์‹œ + useEffect(() => { + const linkedFilters = config.linkedFilters; + + if (!linkedFilters || linkedFilters.length === 0 || !screenContext) { + return; + } + + // ์—ฐ๊ฒฐ๋œ ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ๊ฐ’์„ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ + const checkLinkedFilters = () => { + const newFilterValues: Record = {}; + let hasChanges = false; + + linkedFilters.forEach((filter) => { + if (filter.enabled === false) return; + + const sourceProvider = screenContext.getDataProvider(filter.sourceComponentId); + if (sourceProvider) { + const selectedData = sourceProvider.getSelectedData(); + if (selectedData && selectedData.length > 0) { + const sourceField = filter.sourceField || "value"; + const value = selectedData[0][sourceField]; + + if (value !== linkedFilterValues[filter.targetColumn]) { + newFilterValues[filter.targetColumn] = value; + hasChanges = true; + } else { + newFilterValues[filter.targetColumn] = linkedFilterValues[filter.targetColumn]; + } + } + } + }); + + if (hasChanges) { + setLinkedFilterValues(newFilterValues); + } + }; + + // ์ดˆ๊ธฐ ํ™•์ธ + checkLinkedFilters(); + + // ์ฃผ๊ธฐ์  ํ™•์ธ (100ms ๊ฐ„๊ฒฉ) + const intervalId = setInterval(checkLinkedFilters, 100); + + return () => { + clearInterval(intervalId); + }; + }, [screenContext, config.linkedFilters, linkedFilterValues]); + + // DataProvidable ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ + const dataProvider: DataProvidable = useMemo( + () => ({ + componentId: componentId || "", + componentType: "table-grouped", + + getSelectedData: () => { + return selectedItems; + }, + + getAllData: () => { + return filteredData; + }, + + clearSelection: () => { + toggleAllSelection(); + }, + }), + [componentId, selectedItems, filteredData, toggleAllSelection] + ); + + // DataReceivable ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ + const dataReceiver: DataReceivable = useMemo( + () => ({ + componentId: componentId || "", + componentType: "table-grouped", + + receiveData: async (_receivedData: any[], _config: DataReceiverConfig) => { + // ํ˜„์žฌ๋Š” ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์ˆ˜์‹  ์‹œ ์ƒˆ๋กœ๊ณ ์นจ๋งŒ ์ˆ˜ํ–‰ + refresh(); + }, + + clearData: async () => { + // ๋ฐ์ดํ„ฐ ํด๋ฆฌ์–ด ์‹œ ์ƒˆ๋กœ๊ณ ์นจ + refresh(); + }, + + getConfig: () => { + return { + targetComponentId: componentId || "", + mode: "replace" as const, + }; + }, + }), + [componentId, refresh] + ); + + // ํ™”๋ฉด ์ปจํ…์ŠคํŠธ์— ๋ฐ์ดํ„ฐ ์ œ๊ณต์ž/์ˆ˜์‹ ์ž๋กœ ๋“ฑ๋ก + useEffect(() => { + if (screenContext && componentId) { + screenContext.registerDataProvider(componentId, dataProvider); + screenContext.registerDataReceiver(componentId, dataReceiver); + + return () => { + screenContext.unregisterDataProvider(componentId); + screenContext.unregisterDataReceiver(componentId); + }; + } + }, [screenContext, componentId, dataProvider, dataReceiver]); + + // ํ…Œ์ด๋ธ” ID (๊ฒ€์ƒ‰ํ•„ํ„ฐ ์—ฐ๋™์šฉ) + const tableId = componentId || `table-grouped-${config.selectedTable || "default"}`; + + // TableOptionsContext์— ํ…Œ์ด๋ธ” ๋“ฑ๋ก (๊ฒ€์ƒ‰ํ•„ํ„ฐ๊ฐ€ ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์žˆ๋„๋ก) + useEffect(() => { + if (isDesignMode || !config.selectedTable) return; + + const columnsToRegister = config.columns || []; + + // ๊ณ ์œ  ๊ฐ’ ์กฐํšŒ ํ•จ์ˆ˜ + const getColumnUniqueValues = async (columnName: string) => { + const uniqueValues = new Set(); + rawData.forEach((row) => { + const value = row[columnName]; + if (value !== null && value !== undefined && value !== "") { + uniqueValues.add(String(value)); + } + }); + return Array.from(uniqueValues) + .map((value) => ({ value, label: value })) + .sort((a, b) => a.label.localeCompare(b.label)); + }; + + const registration = { + tableId, + label: config.selectedTable, + tableName: config.selectedTable, + dataCount: totalCount, + columns: columnsToRegister.map((col) => ({ + columnName: col.columnName, + columnLabel: col.displayName || col.columnName, + inputType: "text", + visible: col.visible !== false, + width: col.width || 150, + sortable: true, + filterable: true, + })), + onFilterChange: setFilters, + onGroupChange: setGrouping, + onColumnVisibilityChange: setColumnVisibility, + getColumnUniqueValues, + }; + + registerTable(registration); + + return () => { + unregisterTable(tableId); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tableId, config.selectedTable, config.columns, totalCount, rawData, registerTable, isDesignMode]); + + // ๋ฐ์ดํ„ฐ ๊ฑด์ˆ˜ ๋ณ€๊ฒฝ ์‹œ ์—…๋ฐ์ดํŠธ + useEffect(() => { + if (!isDesignMode && config.selectedTable) { + updateTableDataCount(tableId, totalCount); + } + }, [tableId, totalCount, updateTableDataCount, config.selectedTable, isDesignMode]); + + // ํ•„ํ„ฐ ๋ณ€๊ฒฝ ์‹œ ๊ฒ€์ƒ‰ ์กฐ๊ฑด ์ ์šฉ + useEffect(() => { + if (filters.length > 0) { + const newFilterValues: Record = {}; + filters.forEach((filter: any) => { + if (filter.value) { + newFilterValues[filter.columnName] = filter.value; + } + }); + setLinkedFilterValues((prev) => ({ ...prev, ...newFilterValues })); + } + }, [filters]); + + // ์ปฌ๋Ÿผ ์„ค์ • + const columns = config.columns || []; + const visibleColumns = columns.filter((col) => col.visible !== false); + + // ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ ํฌํ•จ ์‹œ ์ด ์ปฌ๋Ÿผ ์ˆ˜ + const totalColumnCount = visibleColumns.length + (config.showCheckbox ? 1 : 0); + + // ์•„์ดํ…œ ID ์ถ”์ถœ ํ•จ์ˆ˜ + const getItemId = useCallback( + (item: any): string => { + if (item.id !== undefined) return String(item.id); + const firstCol = columns[0]?.columnName; + if (firstCol && item[firstCol] !== undefined) return String(item[firstCol]); + return JSON.stringify(item); + }, + [columns] + ); + + // ์„ ํƒ ๋ณ€๊ฒฝ ์‹œ ์ฝœ๋ฐฑ ๋ฐ ์ด๋ฒคํŠธ ๋ฐœ์†ก + useEffect(() => { + // ๊ธฐ์กด ์ฝœ๋ฐฑ ํ˜ธ์ถœ + if (onSelectionChange && selectedItems.length >= 0) { + onSelectionChange({ + selectedGroups: groups + .filter((g) => g.selected) + .map((g) => g.groupKey), + selectedItems, + isAllSelected, + }); + } + + // TABLE_SELECTION_CHANGE ์ด๋ฒคํŠธ ๋ฐœ์†ก (์„ ํƒ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์— ์•Œ๋ฆผ) + v2EventBus.emit(V2_EVENTS.TABLE_SELECTION_CHANGE, { + componentId: componentId || tableId, + tableName: config.selectedTable || "", + selectedRows: selectedItems, + selectedCount: selectedItems.length, + }); + + console.log("[TableGroupedComponent] ์„ ํƒ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๋ฐœ์†ก:", { + componentId: componentId || tableId, + tableName: config.selectedTable, + selectedCount: selectedItems.length, + }); + }, [selectedItems, groups, isAllSelected, onSelectionChange, componentId, tableId, config.selectedTable]); + + // ๊ทธ๋ฃน ํ† ๊ธ€ ํ•ธ๋“ค๋Ÿฌ + const handleGroupToggle = useCallback( + (groupKey: string) => { + toggleGroup(groupKey); + if (onGroupToggle) { + const group = groups.find((g) => g.groupKey === groupKey); + onGroupToggle({ + groupKey, + expanded: !group?.expanded, + }); + } + }, + [toggleGroup, onGroupToggle, groups] + ); + + // ํ–‰ ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ + const handleRowClick = useCallback( + (row: any, groupKey: string, indexInGroup: number) => { + if (!config.rowClickable) return; + if (onRowClick) { + onRowClick({ row, groupKey, indexInGroup }); + } + }, + [config.rowClickable, onRowClick] + ); + + // refreshTable ์ด๋ฒคํŠธ ๊ตฌ๋… + useEffect(() => { + const handleRefresh = () => { + refresh(); + }; + + window.addEventListener("refreshTable", handleRefresh); + return () => { + window.removeEventListener("refreshTable", handleRefresh); + }; + }, [refresh]); + + // ๋””์ž์ธ ๋ชจ๋“œ ๋ Œ๋”๋ง + if (isDesignMode) { + return ( +
+
+ + ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” + {config.groupConfig?.groupByColumn && ( + + (๊ทธ๋ฃน: {config.groupConfig.groupByColumn}) + + )} +
+
+ ํ…Œ์ด๋ธ”: {config.useCustomTable ? config.customTableName : config.selectedTable || "(๋ฏธ์„ค์ •)"} +
+
+ ); + } + + // ๋กœ๋”ฉ ์ƒํƒœ + if (isLoading) { + return ( +
+ + ๋กœ๋”ฉ ์ค‘... +
+ ); + } + + // ์—๋Ÿฌ ์ƒํƒœ + if (error) { + return ( +
+ {error} +
+ ); + } + + // ๋ฐ์ดํ„ฐ ์—†์Œ + if (groups.length === 0) { + return ( +
+ {config.emptyMessage || "๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."} +
+ ); + } + + return ( +
+ {/* ํˆด๋ฐ” */} + {config.showExpandAllButton && ( +
+
+ + +
+
+ {groupCount}๊ฐœ ๊ทธ๋ฃน | ์ด {totalCount}๊ฑด +
+
+ )} + + {/* ํ…Œ์ด๋ธ” */} +
+ + {/* ํ…Œ์ด๋ธ” ํ—ค๋” */} + + + {/* ์ „์ฒด ์„ ํƒ ์ฒดํฌ๋ฐ•์Šค */} + {config.showCheckbox && ( + + )} + {/* ์ปฌ๋Ÿผ ํ—ค๋” */} + {visibleColumns.map((col) => ( + + ))} + + + + {/* ํ…Œ์ด๋ธ” ๋ฐ”๋”” */} + + {groups.map((group) => ( + + {/* ๊ทธ๋ฃน ํ—ค๋” */} + handleGroupToggle(group.groupKey)} + onSelectToggle={ + config.showCheckbox + ? () => toggleGroupSelection(group.groupKey) + : undefined + } + style={config.groupHeaderStyle} + columnCount={totalColumnCount} + /> + + {/* ๊ทธ๋ฃน ์•„์ดํ…œ (ํŽผ์ณ์ง„ ๊ฒฝ์šฐ๋งŒ) */} + {group.expanded && + group.items.map((item, idx) => { + const itemId = getItemId(item); + const isSelected = group.selectedItemIds?.includes(itemId); + + return ( + handleRowClick(item, group.groupKey, idx)} + > + {/* ์ฒดํฌ๋ฐ•์Šค */} + {config.showCheckbox && ( + + )} + + {/* ๋ฐ์ดํ„ฐ ์ปฌ๋Ÿผ */} + {visibleColumns.map((col) => { + const value = item[col.columnName]; + let displayValue: React.ReactNode = value; + + // ํฌ๋งท ์ ์šฉ + if (col.format === "number" && typeof value === "number") { + displayValue = value.toLocaleString(); + } else if (col.format === "currency" && typeof value === "number") { + displayValue = `โ‚ฉ${value.toLocaleString()}`; + } else if (col.format === "date" && value) { + displayValue = new Date(value).toLocaleDateString("ko-KR"); + } else if (col.format === "boolean") { + displayValue = value ? "์˜ˆ" : "์•„๋‹ˆ์˜ค"; + } + + return ( + + ); + })} + + ); + })} + + ))} + +
+ + + {col.displayName || col.columnName} +
e.stopPropagation()} + > + + toggleItemSelection(group.groupKey, itemId) + } + /> + + {displayValue ?? "-"} +
+
+
+ ); +} + +export default TableGroupedComponent; diff --git a/frontend/lib/registry/components/v2-table-grouped/TableGroupedConfigPanel.tsx b/frontend/lib/registry/components/v2-table-grouped/TableGroupedConfigPanel.tsx new file mode 100644 index 00000000..beb0f5b6 --- /dev/null +++ b/frontend/lib/registry/components/v2-table-grouped/TableGroupedConfigPanel.tsx @@ -0,0 +1,717 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { Check, ChevronsUpDown, Loader2 } from "lucide-react"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { cn } from "@/lib/utils"; +import { tableTypeApi } from "@/lib/api/screen"; +import { TableGroupedConfig, ColumnConfig, LinkedFilterConfig } from "./types"; +import { + groupHeaderStyleOptions, + checkboxModeOptions, + sortDirectionOptions, +} from "./config"; +import { Trash2, Plus } from "lucide-react"; + +interface TableGroupedConfigPanelProps { + config: TableGroupedConfig; + onChange: (newConfig: Partial) => void; +} + +/** + * v2-table-grouped ์„ค์ • ํŒจ๋„ + */ +// ํ…Œ์ด๋ธ” ์ •๋ณด ํƒ€์ž… +interface TableInfo { + tableName: string; + displayName: string; +} + +export function TableGroupedConfigPanel({ + config, + onChange, +}: TableGroupedConfigPanelProps) { + // ํ…Œ์ด๋ธ” ๋ชฉ๋ก (๋ผ๋ฒจ๋ช… ํฌํ•จ) + const [tables, setTables] = useState([]); + const [tableColumns, setTableColumns] = useState([]); + const [loadingTables, setLoadingTables] = useState(false); + const [loadingColumns, setLoadingColumns] = useState(false); + const [tableSelectOpen, setTableSelectOpen] = useState(false); + + // ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ + useEffect(() => { + const loadTables = async () => { + setLoadingTables(true); + try { + const tableList = await tableTypeApi.getTables(); + if (tableList && Array.isArray(tableList)) { + setTables( + tableList.map((t: any) => ({ + tableName: t.tableName || t.table_name, + displayName: t.displayName || t.display_name || t.tableName || t.table_name, + })) + ); + } + } catch (err) { + console.error("ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:", err); + } finally { + setLoadingTables(false); + } + }; + loadTables(); + }, []); + + // ์„ ํƒ๋œ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ๋กœ๋“œ + useEffect(() => { + const tableName = config.useCustomTable + ? config.customTableName + : config.selectedTable; + + if (!tableName) { + setTableColumns([]); + return; + } + + const loadColumns = async () => { + setLoadingColumns(true); + try { + const columns = await tableTypeApi.getColumns(tableName); + if (columns && Array.isArray(columns)) { + const cols: ColumnConfig[] = columns.map( + (col: any, idx: number) => ({ + columnName: col.column_name || col.columnName, + displayName: col.display_name || col.displayName || col.column_name || col.columnName, + visible: true, + sortable: true, + searchable: false, + align: "left" as const, + order: idx, + }) + ); + setTableColumns(cols); + + // ์ปฌ๋Ÿผ ์„ค์ •์ด ์—†์œผ๋ฉด ์ž๋™ ์„ค์ • + if (!config.columns || config.columns.length === 0) { + onChange({ ...config, columns: cols }); + } + } + } catch (err) { + console.error("์ปฌ๋Ÿผ ๋กœ๋“œ ์‹คํŒจ:", err); + } finally { + setLoadingColumns(false); + } + }; + loadColumns(); + }, [config.selectedTable, config.customTableName, config.useCustomTable]); + + // ์„ค์ • ์—…๋ฐ์ดํŠธ ํ—ฌํผ + const updateConfig = (updates: Partial) => { + onChange({ ...config, ...updates }); + }; + + // ๊ทธ๋ฃน ์„ค์ • ์—…๋ฐ์ดํŠธ ํ—ฌํผ + const updateGroupConfig = ( + updates: Partial + ) => { + onChange({ + ...config, + groupConfig: { ...config.groupConfig, ...updates }, + }); + }; + + // ์ปฌ๋Ÿผ ๊ฐ€์‹œ์„ฑ ํ† ๊ธ€ + const toggleColumnVisibility = (columnName: string) => { + const updatedColumns = (config.columns || []).map((col) => + col.columnName === columnName ? { ...col, visible: !col.visible } : col + ); + updateConfig({ columns: updatedColumns }); + }; + + // ํ•ฉ๊ณ„ ์ปฌ๋Ÿผ ํ† ๊ธ€ + const toggleSumColumn = (columnName: string) => { + const currentSumCols = config.groupConfig?.summary?.sumColumns || []; + const newSumCols = currentSumCols.includes(columnName) + ? currentSumCols.filter((c) => c !== columnName) + : [...currentSumCols, columnName]; + + updateGroupConfig({ + summary: { + ...config.groupConfig?.summary, + sumColumns: newSumCols, + }, + }); + }; + + // ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์ถ”๊ฐ€ + const addLinkedFilter = () => { + const newFilter: LinkedFilterConfig = { + sourceComponentId: "", + sourceField: "value", + targetColumn: "", + enabled: true, + }; + updateConfig({ + linkedFilters: [...(config.linkedFilters || []), newFilter], + }); + }; + + // ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์ œ๊ฑฐ + const removeLinkedFilter = (index: number) => { + const filters = [...(config.linkedFilters || [])]; + filters.splice(index, 1); + updateConfig({ linkedFilters: filters }); + }; + + // ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์—…๋ฐ์ดํŠธ + const updateLinkedFilter = ( + index: number, + updates: Partial + ) => { + const filters = [...(config.linkedFilters || [])]; + filters[index] = { ...filters[index], ...updates }; + updateConfig({ linkedFilters: filters }); + }; + + return ( +
+ + {/* ํ…Œ์ด๋ธ” ์„ค์ • */} + + + ํ…Œ์ด๋ธ” ์„ค์ • + + + {/* ์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ */} +
+ + + updateConfig({ useCustomTable: checked }) + } + /> +
+ + {/* ํ…Œ์ด๋ธ” ์„ ํƒ */} + {config.useCustomTable ? ( +
+ + + updateConfig({ customTableName: e.target.value }) + } + placeholder="ํ…Œ์ด๋ธ”๋ช… ์ž…๋ ฅ" + className="h-8 text-xs" + /> +
+ ) : ( +
+ + + + + + + { + // ํ…Œ์ด๋ธ”๋ช… ๋˜๋Š” ๋ผ๋ฒจ๋ช…์— ๊ฒ€์ƒ‰์–ด๊ฐ€ ํฌํ•จ๋˜๋ฉด 1, ์•„๋‹ˆ๋ฉด 0 + const lowerSearch = search.toLowerCase(); + if (value.toLowerCase().includes(lowerSearch)) { + return 1; + } + return 0; + }} + > + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + + {tables.map((table) => ( + { + updateConfig({ selectedTable: table.tableName }); + setTableSelectOpen(false); + }} + className="text-xs" + > + +
+ {table.displayName} + + {table.tableName} + +
+
+ ))} +
+
+
+
+
+
+ )} +
+
+ + {/* ๊ทธ๋ฃนํ™” ์„ค์ • */} + + + ๊ทธ๋ฃนํ™” ์„ค์ • + + + {/* ๊ทธ๋ฃนํ™” ๊ธฐ์ค€ ์ปฌ๋Ÿผ */} +
+ + +
+ + {/* ๊ทธ๋ฃน ๋ผ๋ฒจ ํ˜•์‹ */} +
+ + + updateGroupConfig({ groupLabelFormat: e.target.value }) + } + placeholder="{value} ({์ปฌ๋Ÿผ๋ช…})" + className="h-8 text-xs" + /> +

+ {"{value}"} = ๊ทธ๋ฃน๊ฐ’, {"{์ปฌ๋Ÿผ๋ช…}"} = ํ•ด๋‹น ์ปฌ๋Ÿผ ๊ฐ’ +

+
+ + {/* ๊ธฐ๋ณธ ํŽผ์นจ ์ƒํƒœ */} +
+ + + updateGroupConfig({ defaultExpanded: checked }) + } + /> +
+ + {/* ๊ทธ๋ฃน ์ •๋ ฌ */} +
+ + +
+ + {/* ๊ฐœ์ˆ˜ ํ‘œ์‹œ */} +
+ + + updateGroupConfig({ + summary: { + ...config.groupConfig?.summary, + showCount: checked, + }, + }) + } + /> +
+ + {/* ํ•ฉ๊ณ„ ์ปฌ๋Ÿผ */} +
+ +
+ {tableColumns.map((col) => ( +
+ toggleSumColumn(col.columnName)} + /> + +
+ ))} +
+
+
+
+ + {/* ํ‘œ์‹œ ์„ค์ • */} + + + ํ‘œ์‹œ ์„ค์ • + + + {/* ์ฒดํฌ๋ฐ•์Šค ํ‘œ์‹œ */} +
+ + + updateConfig({ showCheckbox: checked }) + } + /> +
+ + {/* ์ฒดํฌ๋ฐ•์Šค ๋ชจ๋“œ */} + {config.showCheckbox && ( +
+ + +
+ )} + + {/* ๊ทธ๋ฃน ํ—ค๋” ์Šคํƒ€์ผ */} +
+ + +
+ + {/* ์ „์ฒด ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ ๋ฒ„ํŠผ */} +
+ + + updateConfig({ showExpandAllButton: checked }) + } + /> +
+ + {/* ํ–‰ ํด๋ฆญ ๊ฐ€๋Šฅ */} +
+ + + updateConfig({ rowClickable: checked }) + } + /> +
+ + {/* ์ตœ๋Œ€ ๋†’์ด */} +
+ + + updateConfig({ maxHeight: parseInt(e.target.value) || 600 }) + } + className="h-8 text-xs" + /> +
+ + {/* ๋นˆ ๋ฐ์ดํ„ฐ ๋ฉ”์‹œ์ง€ */} +
+ + + updateConfig({ emptyMessage: e.target.value }) + } + placeholder="๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค." + className="h-8 text-xs" + /> +
+
+
+ + {/* ์ปฌ๋Ÿผ ์„ค์ • */} + + + ์ปฌ๋Ÿผ ์„ค์ • + + +
+ {(config.columns || tableColumns).map((col) => ( +
+ + toggleColumnVisibility(col.columnName) + } + /> + +
+ ))} +
+
+
+ + {/* ์—ฐ๋™ ์„ค์ • */} + + + ์—ฐ๋™ ์„ค์ • + + +
+
+ + +
+ + {(config.linkedFilters || []).length === 0 ? ( +

+ ์—ฐ๊ฒฐ๋œ ํ•„ํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. +

+ ) : ( +
+ {(config.linkedFilters || []).map((filter, idx) => ( +
+
+ + ํ•„ํ„ฐ #{idx + 1} + +
+ + updateLinkedFilter(idx, { enabled: checked }) + } + /> + +
+
+ +
+ + + updateLinkedFilter(idx, { + sourceComponentId: e.target.value, + }) + } + placeholder="์˜ˆ: search-filter-1" + className="h-7 text-xs" + /> +
+ +
+ + + updateLinkedFilter(idx, { + sourceField: e.target.value, + }) + } + placeholder="value" + className="h-7 text-xs" + /> +
+ +
+ + +
+
+ ))} +
+ )} + +

+ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ(๊ฒ€์ƒ‰ํ•„ํ„ฐ ๋“ฑ)์˜ ์„ ํƒ ๊ฐ’์œผ๋กœ ์ด ํ…Œ์ด๋ธ”์„ ํ•„ํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค. +

+
+
+
+
+
+ ); +} + +export default TableGroupedConfigPanel; diff --git a/frontend/lib/registry/components/v2-table-grouped/TableGroupedRenderer.tsx b/frontend/lib/registry/components/v2-table-grouped/TableGroupedRenderer.tsx new file mode 100644 index 00000000..245d8ee6 --- /dev/null +++ b/frontend/lib/registry/components/v2-table-grouped/TableGroupedRenderer.tsx @@ -0,0 +1,57 @@ +"use client"; + +import React from "react"; +import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer"; +import { V2TableGroupedDefinition } from "./index"; +import { TableGroupedComponent } from "./TableGroupedComponent"; + +/** + * TableGrouped ๋ Œ๋”๋Ÿฌ + * ์ž๋™ ๋“ฑ๋ก ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ๋“ฑ๋ก + */ +export class TableGroupedRenderer extends AutoRegisteringComponentRenderer { + static componentDefinition = V2TableGroupedDefinition; + + render(): React.ReactElement { + return ( + + ); + } + + // ์„ค์ • ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ + protected handleConfigChange = (config: any) => { + console.log("๐Ÿ“ฅ TableGroupedRenderer์—์„œ ์„ค์ • ๋ณ€๊ฒฝ ๋ฐ›์Œ:", config); + + // ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์˜ onConfigChange ํ˜ธ์ถœ (ํ™”๋ฉด ์„ค๊ณ„์ž์—๊ฒŒ ์•Œ๋ฆผ) + if (this.props.onConfigChange) { + this.props.onConfigChange(config); + } + + this.updateComponent({ config }); + }; + + // ๊ฐ’ ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ + protected handleValueChange = (value: any) => { + this.updateComponent({ value }); + }; +} + +// ์ž๋™ ๋“ฑ๋ก ์‹คํ–‰ +TableGroupedRenderer.registerSelf(); + +// ๊ฐ•์ œ ๋“ฑ๋ก (๋””๋ฒ„๊น…์šฉ) +if (typeof window !== "undefined") { + setTimeout(() => { + try { + TableGroupedRenderer.registerSelf(); + } catch (error) { + console.error("โŒ TableGrouped ๊ฐ•์ œ ๋“ฑ๋ก ์‹คํŒจ:", error); + } + }, 1000); +} diff --git a/frontend/lib/registry/components/v2-table-grouped/components/GroupHeader.tsx b/frontend/lib/registry/components/v2-table-grouped/components/GroupHeader.tsx new file mode 100644 index 00000000..f7119f4e --- /dev/null +++ b/frontend/lib/registry/components/v2-table-grouped/components/GroupHeader.tsx @@ -0,0 +1,141 @@ +"use client"; + +import React from "react"; +import { ChevronDown, ChevronRight, Minus } from "lucide-react"; +import { Checkbox } from "@/components/ui/checkbox"; +import { cn } from "@/lib/utils"; +import { GroupState, TableGroupedConfig } from "../types"; + +interface GroupHeaderProps { + /** ๊ทธ๋ฃน ์ƒํƒœ */ + group: GroupState; + /** ์„ค์ • */ + config: TableGroupedConfig; + /** ๊ทธ๋ฃน ํ† ๊ธ€ ํ•ธ๋“ค๋Ÿฌ */ + onToggle: () => void; + /** ๊ทธ๋ฃน ์„ ํƒ ํ† ๊ธ€ ํ•ธ๋“ค๋Ÿฌ */ + onSelectToggle?: () => void; + /** ๊ทธ๋ฃน ํ—ค๋” ์Šคํƒ€์ผ */ + style?: "default" | "compact" | "card"; + /** ์ปฌ๋Ÿผ ๊ฐœ์ˆ˜ (colspan์šฉ) */ + columnCount?: number; +} + +/** + * ๊ทธ๋ฃน ํ—ค๋” ์ปดํฌ๋„ŒํŠธ + * ๊ทธ๋ฃน ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ, ์ฒดํฌ๋ฐ•์Šค, ์š”์•ฝ ์ •๋ณด ํ‘œ์‹œ + */ +export function GroupHeader({ + group, + config, + onToggle, + onSelectToggle, + style = "default", + columnCount = 1, +}: GroupHeaderProps) { + const { showCheckbox } = config; + const { summary } = group; + + // ์ผ๋ถ€ ์„ ํƒ ์—ฌ๋ถ€ + const isIndeterminate = + group.selectedItemIds && + group.selectedItemIds.length > 0 && + group.selectedItemIds.length < group.items.length; + + // ์š”์•ฝ ํ…์ŠคํŠธ ์ƒ์„ฑ + const summaryText = React.useMemo(() => { + const parts: string[] = []; + + // ๊ฐœ์ˆ˜ + if (config.groupConfig?.summary?.showCount !== false) { + parts.push(`${summary.count}๊ฑด`); + } + + // ํ•ฉ๊ณ„ + if (summary.sum) { + for (const [col, value] of Object.entries(summary.sum)) { + const displayName = + config.columns?.find((c) => c.columnName === col)?.displayName || col; + parts.push(`${displayName}: ${value.toLocaleString()}`); + } + } + + return parts.join(" | "); + }, [summary, config]); + + // ์Šคํƒ€์ผ๋ณ„ ํด๋ž˜์Šค + const headerClasses = cn( + "flex items-center gap-2 cursor-pointer select-none transition-colors", + { + // default ์Šคํƒ€์ผ + "px-3 py-2 bg-muted/50 hover:bg-muted border-b": style === "default", + // compact ์Šคํƒ€์ผ + "px-2 py-1 bg-muted/30 hover:bg-muted/50 border-b text-sm": + style === "compact", + // card ์Šคํƒ€์ผ + "px-4 py-3 bg-card border rounded-t-lg shadow-sm hover:shadow": + style === "card", + } + ); + + return ( +
{ + // ์ฒดํฌ๋ฐ•์Šค ํด๋ฆญ ์‹œ ํ† ๊ธ€ ๋ฐฉ์ง€ + if ((e.target as HTMLElement).closest('[data-checkbox="true"]')) { + return; + } + onToggle(); + }} + > +
+ {/* ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ ์•„์ด์ฝ˜ */} + + {group.expanded ? ( + + ) : ( + + )} + + + {/* ์ฒดํฌ๋ฐ•์Šค */} + {showCheckbox && onSelectToggle && ( + { + e.stopPropagation(); + onSelectToggle(); + }} + > + + {isIndeterminate && ( + + )} + + )} + + {/* ๊ทธ๋ฃน ๋ผ๋ฒจ */} + {group.groupLabel} + + {/* ์š”์•ฝ ์ •๋ณด */} + {summaryText && ( + + {summaryText} + + )} +
+