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/controllers/fileController.ts b/backend-node/src/controllers/fileController.ts index d4e8d0cf..28a46232 100644 --- a/backend-node/src/controllers/fileController.ts +++ b/backend-node/src/controllers/fileController.ts @@ -793,8 +793,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, @@ -1260,5 +1261,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'`, + [parseInt(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/screenGroupController.ts b/backend-node/src/controllers/screenGroupController.ts index df0c4f4d..32ce60c3 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 @@ -1737,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, @@ -1750,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]); @@ -2118,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 4fa08eed..a494ae3d 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -2344,6 +2344,8 @@ export async function getTableEntityRelations( * * table_type_columns์—์„œ reference_table์ด ํ˜„์žฌ ํ…Œ์ด๋ธ”์ธ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ฐพ์•„์„œ * ํ•ด๋‹น ํ…Œ์ด๋ธ”๊ณผ FK ์ปฌ๋Ÿผ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * ์šฐ์„ ์ˆœ์œ„: ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ company_code > ๊ณตํ†ต('*') */ export async function getReferencedByTables( req: AuthenticatedRequest, @@ -2351,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) { @@ -2371,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, @@ -2400,7 +2422,7 @@ export async function getReferencedByTables( })); logger.info( - `ํ…Œ์ด๋ธ” ์ฐธ์กฐ ๊ด€๊ณ„ ์กฐํšŒ ์™„๋ฃŒ: ${referencedByTables.length}๊ฐœ ๋ฐœ๊ฒฌ` + `ํ…Œ์ด๋ธ” ์ฐธ์กฐ ๊ด€๊ณ„ ์กฐํšŒ ์™„๋ฃŒ: ${referencedByTables.length}๊ฐœ ๋ฐœ๊ฒฌ (ํšŒ์‚ฌ์ฝ”๋“œ: ${userCompanyCode})` ); const response: ApiResponse = { 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/services/nodeFlowExecutionService.ts b/backend-node/src/services/nodeFlowExecutionService.ts index eadddf9f..cadfdefc 100644 --- a/backend-node/src/services/nodeFlowExecutionService.ts +++ b/backend-node/src/services/nodeFlowExecutionService.ts @@ -845,6 +845,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; } diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index abdfd739..4749bde5 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -886,8 +886,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 || {}; @@ -1014,11 +1015,13 @@ class NumberingRuleService { * @param ruleId ์ฑ„๋ฒˆ ๊ทœ์น™ ID * @param companyCode ํšŒ์‚ฌ ์ฝ”๋“œ * @param formData ํผ ๋ฐ์ดํ„ฐ (๋‚ ์งœ ์ปฌ๋Ÿผ ๊ธฐ์ค€ ์ƒ์„ฑ ์‹œ ์‚ฌ์šฉ) + * @param userInputCode ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘ํ•œ ์ตœ์ข… ์ฝ”๋“œ (์ˆ˜๋™ ์ž…๋ ฅ ๋ถ€๋ถ„ ์ถ”์ถœ์šฉ) */ async allocateCode( ruleId: string, companyCode: string, - formData?: Record + formData?: Record, + userInputCode?: string ): Promise { const pool = getPool(); const client = await pool.connect(); @@ -1029,11 +1032,107 @@ 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 || {}; @@ -1096,6 +1195,68 @@ 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 ""; diff --git a/backend-node/src/services/scheduleService.ts b/backend-node/src/services/scheduleService.ts index 62eecb59..0ce378d5 100644 --- a/backend-node/src/services/scheduleService.ts +++ b/backend-node/src/services/scheduleService.ts @@ -119,17 +119,14 @@ export class ScheduleService { companyCode ); toCreate.push(...schedules); - totalQty += schedules.reduce( - (sum, s) => sum + (s.plan_qty || 0), - 0 - ); + 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 resourceIds = [ + ...new Set(Object.keys(groupedData).map((key) => key.split("|")[0])), + ]; const toDelete = await this.getExistingSchedules( config.scheduleType, resourceIds, @@ -369,7 +366,9 @@ export class ScheduleService { let groupKey = resourceId; if (dueDateField && item[dueDateField]) { // ๋‚ ์งœ๋ฅผ YYYY-MM-DD ํ˜•์‹์œผ๋กœ ์ •๊ทœํ™” - const dueDate = new Date(item[dueDateField]).toISOString().split("T")[0]; + const dueDate = new Date(item[dueDateField]) + .toISOString() + .split("T")[0]; groupKey = `${resourceId}|${dueDate}`; } @@ -403,8 +402,7 @@ export class ScheduleService { // ๊ทธ๋ฃน ํ‚ค์—์„œ ๋ฆฌ์†Œ์ŠคID์™€ ๊ธฐ์ค€์ผ ๋ถ„๋ฆฌ const [resourceId, groupDueDate] = groupKey.split("|"); - const resourceName = - items[0]?.[config.resource.nameField] || resourceId; + const resourceName = items[0]?.[config.resource.nameField] || resourceId; // ์ด ์ˆ˜๋Ÿ‰ ๊ณ„์‚ฐ const totalQty = items.reduce((sum, item) => { @@ -469,7 +467,9 @@ export class ScheduleService { 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_id: items + .map((i) => i.id || i.order_no || i.sales_order_no) + .join(","), source_group_key: resourceId, metadata: { sourceCount: items.length, diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 572f2443..37a21a0a 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -731,6 +731,14 @@ export class ScreenManagementService { 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 }); }); } @@ -5110,18 +5118,6 @@ export class ScreenManagementService { console.log( `V2 ๋ ˆ์ด์•„์›ƒ ๋กœ๋“œ ์™„๋ฃŒ: ${layout.layout_data?.components?.length || 0}๊ฐœ ์ปดํฌ๋„ŒํŠธ`, ); - - // ๐Ÿ› ๋””๋ฒ„๊น…: finished_timeline์˜ fieldMapping ํ™•์ธ - const splitPanel = layout.layout_data?.components?.find((c: any) => - c.url?.includes("v2-split-panel-layout") - ); - const finishedTimeline = splitPanel?.overrides?.rightPanel?.components?.find( - (c: any) => c.id === "finished_timeline" - ); - if (finishedTimeline) { - console.log("๐Ÿ› [Backend] finished_timeline fieldMapping:", JSON.stringify(finishedTimeline.componentConfig?.fieldMapping)); - } - return layout.layout_data; } @@ -5161,20 +5157,16 @@ export class ScreenManagementService { ...layoutData }; - // SUPER_ADMIN์ธ ๊ฒฝ์šฐ ํ™”๋ฉด ์ •์˜์˜ company_code๋กœ ์ €์žฅ (๋กœ๋“œ์™€ ์ผ๊ด€์„ฑ ์œ ์ง€) - const saveCompanyCode = companyCode === "*" ? existingScreen.company_code : companyCode; - console.log(`์ €์žฅํ•  company_code: ${saveCompanyCode} (์›๋ณธ: ${companyCode}, ํ™”๋ฉด ์ •์˜: ${existingScreen.company_code})`); - // UPSERT (์žˆ์œผ๋ฉด ์—…๋ฐ์ดํŠธ, ์—†์œผ๋ฉด ์‚ฝ์ž…) await 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()`, - [screenId, saveCompanyCode, JSON.stringify(dataToSave)], + [screenId, companyCode, JSON.stringify(dataToSave)], ); - console.log(`V2 ๋ ˆ์ด์•„์›ƒ ์ €์žฅ ์™„๋ฃŒ (company_code: ${saveCompanyCode})`); + console.log(`V2 ๋ ˆ์ด์•„์›ƒ ์ €์žฅ ์™„๋ฃŒ`); } } diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 09a9691d..2d4aa581 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -322,7 +322,9 @@ export class TableManagementService { }); } else { // menu_objid ์ปฌ๋Ÿผ์ด ์—†๋Š” ๊ฒฝ์šฐ - ๋งคํ•‘ ์—†์ด ์ง„ํ–‰ - logger.info("โš ๏ธ getColumnList: menu_objid ์ปฌ๋Ÿผ์ด ์—†์Œ, ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์Šคํ‚ต"); + logger.info( + "โš ๏ธ getColumnList: menu_objid ์ปฌ๋Ÿผ์ด ์—†์Œ, ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์Šคํ‚ต" + ); } } catch (mappingError: any) { logger.warn("โš ๏ธ getColumnList: ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ์กฐํšŒ ์‹คํŒจ, ์Šคํ‚ต", { @@ -488,7 +490,10 @@ export class TableManagementService { // 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); } @@ -734,7 +739,7 @@ export class TableManagementService { inputType?: string ): Promise { try { - // ๐Ÿ”ฅ 'direct'๋‚˜ 'auto'๋Š” ํ”„๋ก ํŠธ์—”๋“œ์˜ ์ž…๋ ฅ ๋ฐฉ์‹ ๊ตฌ๋ถ„๊ฐ’์ด๋ฏ€๋กœ + // ๐Ÿ”ฅ 'direct'๋‚˜ 'auto'๋Š” ํ”„๋ก ํŠธ์—”๋“œ์˜ ์ž…๋ ฅ ๋ฐฉ์‹ ๊ตฌ๋ถ„๊ฐ’์ด๋ฏ€๋กœ // DB์˜ input_type(์›นํƒ€์ž…)์œผ๋กœ ์ €์žฅํ•˜๋ฉด ์•ˆ ๋จ - 'text'๋กœ ๋ณ€ํ™˜ let finalWebType = webType; if (webType === "direct" || webType === "auto") { @@ -749,7 +754,8 @@ export class TableManagementService { ); // ์›น ํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ์ƒ์„ธ ์„ค์ • ์ƒ์„ฑ - const defaultDetailSettings = this.generateDefaultDetailSettings(finalWebType); + const defaultDetailSettings = + this.generateDefaultDetailSettings(finalWebType); // ์‚ฌ์šฉ์ž ์ •์˜ ์„ค์ •๊ณผ ๊ธฐ๋ณธ ์„ค์ • ๋ณ‘ํ•ฉ const finalDetailSettings = { @@ -768,7 +774,12 @@ export class TableManagementService { input_type = EXCLUDED.input_type, detail_settings = EXCLUDED.detail_settings, updated_date = NOW()`, - [tableName, columnName, finalWebType, JSON.stringify(finalDetailSettings)] + [ + tableName, + columnName, + finalWebType, + JSON.stringify(finalDetailSettings), + ] ); logger.info( `์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ์™„๋ฃŒ: ${tableName}.${columnName} = ${finalWebType}` @@ -796,7 +807,7 @@ export class TableManagementService { detailSettings?: Record ): Promise { try { - // ๐Ÿ”ฅ 'direct'๋‚˜ 'auto'๋Š” ํ”„๋ก ํŠธ์—”๋“œ์˜ ์ž…๋ ฅ ๋ฐฉ์‹ ๊ตฌ๋ถ„๊ฐ’์ด๋ฏ€๋กœ + // ๐Ÿ”ฅ 'direct'๋‚˜ 'auto'๋Š” ํ”„๋ก ํŠธ์—”๋“œ์˜ ์ž…๋ ฅ ๋ฐฉ์‹ ๊ตฌ๋ถ„๊ฐ’์ด๋ฏ€๋กœ // DB์˜ input_type(์›นํƒ€์ž…)์œผ๋กœ ์ €์žฅํ•˜๋ฉด ์•ˆ ๋จ - 'text'๋กœ ๋ณ€ํ™˜ let finalInputType = inputType; if (inputType === "direct" || inputType === "auto") { @@ -1461,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__" || @@ -3369,14 +3418,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"; } // ๋นˆ ๊ฐ’์ด๋ฉด ์Šคํ‚ต @@ -3428,15 +3479,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) => @@ -3473,10 +3558,33 @@ 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}%'` + ); + } } } } diff --git a/docker-compose.frontend.win.yml b/docker-compose.frontend.win.yml index f81e2287..79589463 100644 --- a/docker-compose.frontend.win.yml +++ b/docker-compose.frontend.win.yml @@ -12,6 +12,13 @@ services: 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/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 index 42cd872b..411fdd1f 100644 --- a/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md +++ b/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md @@ -103,6 +103,162 @@ - ๋ถ„ํ•  ํŒจ๋„ ๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ ``` +### 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. ๊ธฐ์ˆ  ๊ฒฐ์ • @@ -649,6 +805,10 @@ ALTER TABLE screen_layouts_v2_backup_20260130 RENAME TO screen_layouts_v2; - [ ] ํƒœ๋ธ”๋ฆฟ (768px, 1024px) ํ…Œ์ŠคํŠธ - [ ] ๋ชจ๋ฐ”์ผ (375px, 414px) ํ…Œ์ŠคํŠธ - [ ] ๋ถ„ํ•  ํŒจ๋„ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] GridLayout ์ปดํฌ๋„ŒํŠธ ํฌํ•จ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] FlexboxLayout ์ปดํฌ๋„ŒํŠธ ํฌํ•จ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] TabsLayout ์ปดํฌ๋„ŒํŠธ ํฌํ•จ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] ์ค‘์ฒฉ ๋ ˆ์ด์•„์›ƒ (GridLayout ์•ˆ์— ์ปดํฌ๋„ŒํŠธ) ํ…Œ์ŠคํŠธ --- @@ -659,6 +819,8 @@ ALTER TABLE screen_layouts_v2_backup_20260130 RENAME TO screen_layouts_v2; | ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํŒจ | ๋†’์Œ | ๋ฐฑ์—… ํ…Œ์ด๋ธ”์—์„œ ์ฆ‰์‹œ ๋กค๋ฐฑ | | ๊ธฐ์กด ํ™”๋ฉด ๊นจ์ง | ์ค‘๊ฐ„ | `layoutMode` ์—†์œผ๋ฉด ๊ธฐ์กด ๋ฐฉ์‹ ์‚ฌ์šฉ (ํด๋ฐฑ) | | ๋””์ž์ธ ๋ชจ๋“œ ํ˜ผ๋ž€ | ๋‚ฎ์Œ | position/size ํ•„๋“œ ์œ ์ง€ | +| GridLayout ๋‚ด๋ถ€ ๊นจ์ง | ๋‚ฎ์Œ | ๋‚ด๋ถ€๋Š” ๊ธฐ์กด ๋ฐฉ์‹ ์œ ์ง€, ์™ธ๋ถ€ ๋ฐฐ์น˜๋งŒ ๋ณ€๊ฒฝ | +| ์ค‘์ฒฉ ๋ ˆ์ด์•„์›ƒ ๋ฌธ์ œ | ๋‚ฎ์Œ | ๊ฐ ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ | --- 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 index 7e1afcba..c60f1dfb 100644 --- a/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md +++ b/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md @@ -467,9 +467,9 @@ V2 ์ „ํ™˜ ๋กค๋ฐฑ (ํ•„์š”์‹œ): - [x] copyScreens() - Legacy ์ œ๊ฑฐ, V2๋กœ ๊ต์ฒด โœ… 2026-01-28 - [x] hasLayoutChangesV2() ํ•จ์ˆ˜ ์ถ”๊ฐ€ โœ… 2026-01-28 - [x] updateTabScreenReferences() V2 ์ง€์› ์ถ”๊ฐ€ โœ… 2026-01-28 -- [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ -- [ ] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ -- [ ] V2 ์ „์šฉ ๋ณต์ œ ๋™์ž‘ ํ™•์ธ +- [x] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ โœ… 2026-01-30 +- [x] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ โœ… 2026-01-30 +- [x] V2 ์ „์šฉ ๋ณต์ œ ๋™์ž‘ ํ™•์ธ โœ… 2026-01-30 ### 9.3 Phase 2 ์™„๋ฃŒ ์กฐ๊ฑด @@ -522,3 +522,4 @@ V2 ์ „ํ™˜ ๋กค๋ฐฑ (ํ•„์š”์‹œ): | 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/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/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)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 828d1aca..9f043adf 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -161,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, @@ -227,7 +226,6 @@ function ScreenViewPage() { ); if (hasTableWidget) { - console.log("๐Ÿ“‹ ํ…Œ์ด๋ธ” ์œ„์ ฏ์ด ์žˆ์–ด ์ž๋™ ๋กœ๋“œ ๊ฑด๋„ˆ๋œ€ (ํ–‰ ์„ ํƒ์œผ๋กœ ๋ฐ์ดํ„ฐ ๋กœ๋“œ)"); return; } @@ -238,7 +236,9 @@ function ScreenViewPage() { compType?.includes("select") || compType?.includes("textarea") || compType?.includes("v2-input") || - compType?.includes("v2-select"); + compType?.includes("v2-select") || + compType?.includes("v2-media") || + compType?.includes("file-upload"); // ๐Ÿ†• ๋ ˆ๊ฑฐ์‹œ ํŒŒ์ผ ์—…๋กœ๋“œ ํฌํ•จ const hasColumnName = !!(comp as any).columnName; return isInputType && hasColumnName; }); 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 2ab42e75..9320f00e 100644 --- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx +++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx @@ -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 }, @@ -345,15 +345,30 @@ export const NumberingRuleDesigner: React.FC = ({ 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/EditModal.tsx b/frontend/components/screen/EditModal.tsx index db722991..877a65c4 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -704,7 +704,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 +767,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}`); @@ -863,7 +878,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"); @@ -936,7 +956,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..582aa413 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,37 @@ 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)); + const imageUrl = isObjid + ? `/api/files/preview/${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/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/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 ( = ({ (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%" }} > = ({ />
- {/* ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด ํ‘œ์‹œ */} + {/* ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด ํ‘œ์‹œ - ๐Ÿ”ง ์˜ค๋ฅธ์ชฝ์œผ๋กœ ์ด๋™ (๋ผ๋ฒจ๊ณผ ๊ฒน์น˜์ง€ ์•Š๋„๋ก) */} {isSelected && ( -
+
{type === "widget" && (
{getWidgetIcon((component as WidgetComponent).widgetType)} @@ -690,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 c73e6598..c6ad7437 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, @@ -209,20 +213,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ (์ค‘์ฒฉ ๊ตฌ์กฐ ์ง€์›) const handleSelectTabComponent = useCallback( ( - tabsComponentId: string, - tabId: string, - compId: string, + tabsComponentId: string, + tabId: string, + compId: string, comp: any, // ๐Ÿ†• ์ค‘์ฒฉ ๊ตฌ์กฐ์šฉ: ๋ถ€๋ชจ ๋ถ„ํ•  ํŒจ๋„ ์ •๋ณด (์„ ํƒ์ ) parentSplitPanelId?: string | null, - parentPanelSide?: "left" | "right" | null + parentPanelSide?: "left" | "right" | null, ) => { if (!compId) { // ํƒญ ์˜์—ญ ๋นˆ ๊ณต๊ฐ„ ํด๋ฆญ ์‹œ ์„ ํƒ ํ•ด์ œ setSelectedTabComponentInfo(null); return; } - + setSelectedTabComponentInfo({ tabsComponentId, tabId, @@ -250,13 +254,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU fieldMapping: comp?.componentConfig?.fieldMapping, fieldMappingKeys: comp?.componentConfig?.fieldMapping ? Object.keys(comp.componentConfig.fieldMapping) : [], }); - + if (!compId) { // ํŒจ๋„ ์˜์—ญ ๋นˆ ๊ณต๊ฐ„ ํด๋ฆญ ์‹œ ์„ ํƒ ํ•ด์ œ setSelectedPanelComponentInfo(null); return; } - + setSelectedPanelComponentInfo({ splitPanelId, panelSide, @@ -275,14 +279,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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, @@ -295,15 +299,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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([]); @@ -472,14 +475,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // ์ด๋ฏธ ๋ฐฐ์น˜๋œ ์ปฌ๋Ÿผ ๋ชฉ๋ก ๊ณ„์‚ฐ 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); } @@ -492,7 +501,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU collectColumns(layout.components); return placed; - }, [layout.components]); + }, [layout.components, selectedScreen?.tableName]); // ํžˆ์Šคํ† ๋ฆฌ์— ์ €์žฅ const saveToHistory = useCallback( @@ -511,9 +520,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const handleUpdateTabComponentConfig = useCallback( (path: string, value: any) => { if (!selectedTabComponentInfo) return; - + const { tabsComponentId, tabId, componentId, parentSplitPanelId, parentPanelSide } = selectedTabComponentInfo; - + // ํƒญ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ (์žฌ์‚ฌ์šฉ) const updateTabsComponent = (tabsComponent: any) => { const currentConfig = tabsComponent.componentConfig || {}; @@ -550,11 +559,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU return { ...tabsComponent, componentConfig: { ...currentConfig, tabs: updatedTabs } }; }; - + setLayout((prevLayout) => { let newLayout; let updatedTabs; - + if (parentSplitPanelId && parentPanelSide) { // ๐Ÿ†• ์ค‘์ฒฉ ๊ตฌ์กฐ: ๋ถ„ํ•  ํŒจ๋„ ์•ˆ์˜ ํƒญ ์—…๋ฐ์ดํŠธ newLayout = { @@ -565,13 +574,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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: { @@ -579,7 +588,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU [panelKey]: { ...panelConfig, components: panelComponents.map((pc: any) => - pc.id === tabsComponentId ? updatedTabsComponent : pc + pc.id === tabsComponentId ? updatedTabsComponent : pc, ), }, }, @@ -592,15 +601,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // ์ผ๋ฐ˜ ๊ตฌ์กฐ: ์ตœ์ƒ์œ„ ํƒญ ์—…๋ฐ์ดํŠธ 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 - ), + components: prevLayout.components.map((c) => (c.id === tabsComponentId ? updatedTabsComponent : c)), }; } @@ -610,9 +617,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU .find((t: any) => t.id === tabId) ?.components?.find((c: any) => c.id === componentId); if (updatedComp) { - setSelectedTabComponentInfo((prev) => - prev ? { ...prev, component: updatedComp } : null - ); + setSelectedTabComponentInfo((prev) => (prev ? { ...prev, component: updatedComp } : null)); } } @@ -770,11 +775,27 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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") { @@ -788,7 +809,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU newComp.style.height = `${value.height}px`; } } - + console.log("๐Ÿ”„ size ๋ณ€๊ฒฝ โ†’ style ๋™๊ธฐํ™”:", { componentId: newComp.id, path, @@ -1093,18 +1114,19 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // ์ „์ฒด 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์—์„œ ์ถ”์ถœ) @@ -1113,13 +1135,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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, @@ -1144,12 +1166,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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]); @@ -1256,95 +1278,107 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU }; 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(() => { @@ -1369,43 +1403,27 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU let response: any; if (USE_V2_API) { 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 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" + (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) : [], + fieldMappingKeys: finishedTimelineInV2?.componentConfig?.fieldMapping + ? Object.keys(finishedTimelineInV2?.componentConfig?.fieldMapping) + : [], hasId: !!finishedTimelineInV2?.componentConfig?.fieldMapping?.id, idValue: finishedTimelineInV2?.componentConfig?.fieldMapping?.id, }); - + response = v2Response ? convertV2ToLegacy(v2Response) : null; - - // ๐Ÿ› ๋””๋ฒ„๊น…: convertV2ToLegacy ํ›„ fieldMapping.id ํ™•์ธ - const splitPanelInLegacy = response?.components?.find((c: any) => - c.componentType === "v2-split-panel-layout" - ); - const finishedTimelineInLegacy = splitPanelInLegacy?.componentConfig?.rightPanel?.components?.find( - (c: any) => c.id === "finished_timeline" - ); - console.log("๐Ÿ› [๋ณ€ํ™˜ ํ›„] finished_timeline fieldMapping:", { - fieldMapping: JSON.stringify(finishedTimelineInLegacy?.componentConfig?.fieldMapping), - fieldMappingKeys: finishedTimelineInLegacy?.componentConfig?.fieldMapping ? Object.keys(finishedTimelineInLegacy?.componentConfig?.fieldMapping) : [], - hasId: !!finishedTimelineInLegacy?.componentConfig?.fieldMapping?.id, - idValue: finishedTimelineInLegacy?.componentConfig?.fieldMapping?.id, - }); - - console.log("๐Ÿ“ฆ V2 ๋ ˆ์ด์•„์›ƒ ๋กœ๋“œ:", v2Response?.components?.length || 0, "๊ฐœ ์ปดํฌ๋„ŒํŠธ"); } else { response = await screenApi.getLayout(selectedScreen.screenId); } - + if (response) { // ๐Ÿ”„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•„์š” ์—ฌ๋ถ€ ํ™•์ธ (V2๋Š” ์Šคํ‚ต) let layoutToUse = response; @@ -1447,15 +1465,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU } // ๐Ÿ” ๋””๋ฒ„๊น…: ๋กœ๋“œ๋œ ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ์˜ 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]); @@ -1481,8 +1502,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU if ( activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement || - activeElement?.getAttribute('contenteditable') === 'true' || - activeElement?.getAttribute('role') === 'textbox' + activeElement?.getAttribute("contenteditable") === "true" || + activeElement?.getAttribute("role") === "textbox" ) { return; } @@ -1508,8 +1529,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU if ( activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement || - activeElement?.getAttribute('contenteditable') === 'true' || - activeElement?.getAttribute('role') === 'textbox' + activeElement?.getAttribute("contenteditable") === "true" || + activeElement?.getAttribute("role") === "textbox" ) { return; } @@ -1623,55 +1644,16 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU }; }, [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], ); // ํ•ด์ƒ๋„ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ (์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ/์œ„์น˜ ์œ ์ง€) @@ -1699,7 +1681,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU setLayout(updatedLayout); saveToHistory(updatedLayout); - toast.success(`ํ•ด์ƒ๋„๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, { + toast.success("ํ•ด์ƒ๋„๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", { description: `${oldWidth}ร—${oldHeight} โ†’ ${newWidth}ร—${newHeight}`, }); @@ -1815,7 +1797,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // ํ•ด์ƒ๋„ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ // ํ˜„์žฌ ์„ ํƒ๋œ ํ…Œ์ด๋ธ”์„ ํ™”๋ฉด์˜ ๊ธฐ๋ณธ ํ…Œ์ด๋ธ”๋กœ ์ €์žฅ const currentMainTableName = tables.length > 0 ? tables[0].tableName : null; - + const layoutWithResolution = { ...layout, components: updatedComponents, @@ -1826,97 +1808,21 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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, - })), - }); - - // ๐Ÿ” ๋””๋ฒ„๊ทธ: ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€์˜ ํƒญ ๋ฐ ์ปดํฌ๋„ŒํŠธ ์„ค์ • ํ™•์ธ - const splitPanels = layoutWithResolution.components.filter( - (c: any) => c.componentType === "v2-split-panel-layout" || c.componentType === "split-panel-layout" - ); - splitPanels.forEach((sp: any) => { - console.log("๐Ÿ” [์ €์žฅ] ๋ถ„ํ•  ํŒจ๋„ ์„ค์ •:", { - id: sp.id, - leftPanel: sp.componentConfig?.leftPanel, - rightPanel: sp.componentConfig?.rightPanel, - }); - // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋‚ด ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์˜ componentConfig ๋กœ๊ทธ - const rightComponents = sp.componentConfig?.rightPanel?.components || []; - console.log("๐Ÿ” [์ €์žฅ] ์˜ค๋ฅธ์ชฝ ํŒจ๋„ ์ปดํฌ๋„ŒํŠธ๋“ค:", rightComponents.map((c: any) => ({ - id: c.id, - componentType: c.componentType, - hasComponentConfig: !!c.componentConfig, - componentConfig: JSON.parse(JSON.stringify(c.componentConfig || {})), - }))); - // ์™ผ์ชฝ ํŒจ๋„์˜ ํƒญ ์ปดํฌ๋„ŒํŠธ ํ™•์ธ - const leftTabs = sp.componentConfig?.leftPanel?.components?.filter( - (c: any) => c.componentType === "v2-tabs-widget" - ); - leftTabs?.forEach((tabWidget: any) => { - console.log("๐Ÿ” [์ €์žฅ] ์™ผ์ชฝ ํŒจ๋„ ํƒญ ์œ„์ ฏ ์ „์ฒด componentConfig:", { - tabWidgetId: tabWidget.id, - fullComponentConfig: JSON.parse(JSON.stringify(tabWidget.componentConfig || {})), - }); - console.log("๐Ÿ” [์ €์žฅ] ์™ผ์ชฝ ํŒจ๋„ ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ:", { - tabId: tabWidget.id, - tabs: tabWidget.componentConfig?.tabs?.map((t: any) => ({ - id: t.id, - label: t.label, - componentsCount: t.components?.length || 0, - components: t.components, - })), - }); - }); - // ์˜ค๋ฅธ์ชฝ ํŒจ๋„์˜ ํƒญ ์ปดํฌ๋„ŒํŠธ ํ™•์ธ - const rightTabs = sp.componentConfig?.rightPanel?.components?.filter( - (c: any) => c.componentType === "v2-tabs-widget" - ); - rightTabs?.forEach((tabWidget: any) => { - console.log("๐Ÿ” [์ €์žฅ] ์˜ค๋ฅธ์ชฝ ํŒจ๋„ ํƒญ ์œ„์ ฏ ์ „์ฒด componentConfig:", { - tabWidgetId: tabWidget.id, - fullComponentConfig: JSON.parse(JSON.stringify(tabWidget.componentConfig || {})), - }); - console.log("๐Ÿ” [์ €์žฅ] ์˜ค๋ฅธ์ชฝ ํŒจ๋„ ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ:", { - tabId: tabWidget.id, - tabs: tabWidget.componentConfig?.tabs?.map((t: any) => ({ - id: t.id, - label: t.label, - componentsCount: t.components?.length || 0, - components: t.components, - })), - }); - }); - }); + // ๐Ÿ’พ ์ €์žฅ ๋กœ๊ทธ (๋””๋ฒ„๊ทธ ์™„๋ฃŒ - ๊ฐ„์†Œํ™”) + // console.log("๐Ÿ’พ ์ €์žฅ ์‹œ์ž‘:", { screenId: selectedScreen.screenId, componentsCount: layoutWithResolution.components.length }); + // ๋ถ„ํ•  ํŒจ๋„ ๋””๋ฒ„๊ทธ ๋กœ๊ทธ (์ฃผ์„ ์ฒ˜๋ฆฌ) // V2 API ์‚ฌ์šฉ ์—ฌ๋ถ€์— ๋”ฐ๋ผ ๋ถ„๊ธฐ if (USE_V2_API) { + // ๐Ÿ”ง V2 ๋ ˆ์ด์•„์›ƒ ์ €์žฅ (๋””๋ฒ„๊ทธ ๋กœ๊ทธ ์ฃผ์„ ์ฒ˜๋ฆฌ) const v2Layout = convertLegacyToV2(layoutWithResolution); - console.log("๐Ÿ“ฆ V2 ๋ณ€ํ™˜ ๊ฒฐ๊ณผ (๋ถ„ํ•  ํŒจ๋„ overrides):", v2Layout.components - .filter((c: any) => c.url?.includes("split-panel")) - .map((c: any) => ({ - id: c.id, - url: c.url, - overrides: c.overrides, - })) - ); 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("ํ™”๋ฉด์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); // ์ €์žฅ ์„ฑ๊ณต ํ›„ ๋ถ€๋ชจ์—๊ฒŒ ํ™”๋ฉด ์ •๋ณด ์—…๋ฐ์ดํŠธ ์•Œ๋ฆผ (ํ…Œ์ด๋ธ”๋ช… ์ฆ‰์‹œ ๋ฐ˜์˜) @@ -2000,14 +1906,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU if (response.success && response.data) { // ์ž๋™ ๋งคํ•‘ ์ ์šฉ const updatedComponents = applyMultilangMappings(layout.components, response.data); - + // ๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ const updatedLayout = { ...layout, components: updatedComponents, screenResolution: screenResolution, }; - + setLayout(updatedLayout); // ์ž๋™ ์ €์žฅ (๋งคํ•‘ ์ •๋ณด๊ฐ€ ์†์‹ค๋˜์ง€ ์•Š๋„๋ก) @@ -2611,7 +2517,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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)}`, @@ -2622,7 +2528,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU size: component.defaultSize || { width: 200, height: 32 }, componentConfig: component.defaultConfig || {}, }; - + // ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ const updatedComponent = { ...targetComponent, @@ -2631,14 +2537,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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; // ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ ์ฒ˜๋ฆฌ ์™„๋ฃŒ @@ -2656,14 +2560,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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); @@ -2673,7 +2577,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU parentPanelSide = "left"; break; } - + // ์šฐ์ธก ํŒจ๋„์—์„œ ์ฐพ๊ธฐ const rightComponents = config.rightPanel?.components || []; const foundInRight = rightComponents.find((c: any) => c.id === containerId); @@ -2686,20 +2590,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU } } } - + 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; - + // ์ƒˆ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ const componentType = component.id || component.componentType || "v2-text-display"; - + console.log("๐ŸŽฏ ํƒญ์— ์ปดํฌ๋„ŒํŠธ ๋“œ๋กญ:", { componentId: component.id, componentType: componentType, @@ -2714,7 +2618,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU zoomLevel, calculatedPosition: { x: dropX, y: dropY }, }); - + const newTabComponent = { id: `tab_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, componentType: componentType, @@ -2723,7 +2627,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU size: component.defaultSize || { width: 200, height: 100 }, componentConfig: component.defaultConfig || {}, }; - + // ํ•ด๋‹น ํƒญ์— ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ const updatedTabs = tabs.map((tab: any) => { if (tab.id === activeTabId) { @@ -2734,7 +2638,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU } return tab; }); - + const updatedTabsComponent = { ...targetComponent, componentConfig: { @@ -2742,9 +2646,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU tabs: updatedTabs, }, }; - + let newLayout; - + if (parentSplitPanelId && parentPanelSide) { // ๐Ÿ†• ์ค‘์ฒฉ ๊ตฌ์กฐ: ๋ถ„ํ•  ํŒจ๋„ ์•ˆ์˜ ํƒญ ์—…๋ฐ์ดํŠธ newLayout = { @@ -2755,7 +2659,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; const panelConfig = splitConfig[panelKey] || {}; const panelComponents = panelConfig.components || []; - + return { ...c, componentConfig: { @@ -2763,7 +2667,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU [panelKey]: { ...panelConfig, components: panelComponents.map((pc: any) => - pc.id === containerId ? updatedTabsComponent : pc + pc.id === containerId ? updatedTabsComponent : pc, ), }, }, @@ -2777,13 +2681,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // ์ผ๋ฐ˜ ๊ตฌ์กฐ: ์ตœ์ƒ์œ„ ํƒญ ์—…๋ฐ์ดํŠธ newLayout = { ...layout, - components: layout.components.map((c) => - c.id === containerId ? updatedTabsComponent : c - ), + components: layout.components.map((c) => (c.id === containerId ? updatedTabsComponent : c)), }; toast.success("์ปดํฌ๋„ŒํŠธ๊ฐ€ ํƒญ์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); } - + setLayout(newLayout); saveToHistory(newLayout); return; // ํƒญ ์ปจํ…Œ์ด๋„ˆ ์ฒ˜๋ฆฌ ์™„๋ฃŒ @@ -2804,22 +2706,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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, @@ -2828,12 +2730,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU size: component.defaultSize || { width: 200, height: 100 }, componentConfig: component.defaultConfig || {}, }; - + const updatedPanelConfig = { ...panelConfig, components: [...currentComponents, newPanelComponent], }; - + const updatedComponent = { ...targetComponent, componentConfig: { @@ -2841,14 +2743,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU [panelKey]: updatedPanelConfig, }, }; - + 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); toast.success(`์ปดํฌ๋„ŒํŠธ๊ฐ€ ${panelSide === "left" ? "์ขŒ์ธก" : "์šฐ์ธก"} ํŒจ๋„์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค`); @@ -3123,7 +3023,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU }, webTypeConfig: getDefaultWebTypeConfig(component.webType), style: { - labelDisplay: false, // ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์˜ ๊ธฐ๋ณธ ๋ผ๋ฒจ ํ‘œ์‹œ๋ฅผ false๋กœ ์„ค์ • + labelDisplay: true, // ๐Ÿ†• ๋ผ๋ฒจ ๊ธฐ๋ณธ ํ‘œ์‹œ (์‚ฌ์šฉ์ž๊ฐ€ ๋„๊ณ  ์‹ถ์œผ๋ฉด ์ฒดํฌ ํ•ด์ œ) labelFontSize: "14px", labelColor: "#212121", labelFontWeight: "500", @@ -3207,7 +3107,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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)}`, @@ -3218,7 +3118,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU size: { width: 200, height: 32 }, componentConfig: {}, }; - + const updatedComponent = { ...targetComponent, componentConfig: { @@ -3226,14 +3126,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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; @@ -3251,14 +3149,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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); @@ -3268,7 +3166,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU parentPanelSide = "left"; break; } - + // ์šฐ์ธก ํŒจ๋„์—์„œ ์ฐพ๊ธฐ const rightComponents = config.rightPanel?.components || []; const foundInRight = rightComponents.find((c: any) => c.id === containerId); @@ -3281,17 +3179,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU } } } - + 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, @@ -3305,7 +3203,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU referenceColumn: column.referenceColumn, displayColumn: column.displayColumn, }); - + // ์›นํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ const getTabComponentSize = (widgetType: string) => { const sizeMap: Record = { @@ -3325,9 +3223,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU }; 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, @@ -3343,7 +3241,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU inputType: column.inputType || column.widgetType, }, }; - + // ํ•ด๋‹น ํƒญ์— ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ const updatedTabs = tabs.map((tab: any) => { if (tab.id === activeTabId) { @@ -3354,7 +3252,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU } return tab; }); - + const updatedTabsComponent = { ...targetComponent, componentConfig: { @@ -3362,9 +3260,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU tabs: updatedTabs, }, }; - + let newLayout; - + if (parentSplitPanelId && parentPanelSide) { // ๐Ÿ†• ์ค‘์ฒฉ ๊ตฌ์กฐ: ๋ถ„ํ•  ํŒจ๋„ ์•ˆ์˜ ํƒญ ์—…๋ฐ์ดํŠธ newLayout = { @@ -3375,7 +3273,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const panelKey = parentPanelSide === "left" ? "leftPanel" : "rightPanel"; const panelConfig = splitConfig[panelKey] || {}; const panelComponents = panelConfig.components || []; - + return { ...c, componentConfig: { @@ -3383,7 +3281,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU [panelKey]: { ...panelConfig, components: panelComponents.map((pc: any) => - pc.id === containerId ? updatedTabsComponent : pc + pc.id === containerId ? updatedTabsComponent : pc, ), }, }, @@ -3397,13 +3295,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // ์ผ๋ฐ˜ ๊ตฌ์กฐ: ์ตœ์ƒ์œ„ ํƒญ ์—…๋ฐ์ดํŠธ newLayout = { ...layout, - components: layout.components.map((c) => - c.id === containerId ? updatedTabsComponent : c - ), + components: layout.components.map((c) => (c.id === containerId ? updatedTabsComponent : c)), }; toast.success("์ปฌ๋Ÿผ์ด ํƒญ์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); } - + setLayout(newLayout); saveToHistory(newLayout); return; @@ -3424,12 +3320,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU 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, @@ -3443,7 +3339,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU referenceColumn: column.referenceColumn, displayColumn: column.displayColumn, }); - + // ์›นํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ const getPanelComponentSize = (widgetType: string) => { const sizeMap: Record = { @@ -3463,9 +3359,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU }; 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, @@ -3481,12 +3377,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU inputType: column.inputType || column.widgetType, }, }; - + const updatedPanelConfig = { ...panelConfig, components: [...currentComponents, newPanelComponent], }; - + const updatedComponent = { ...targetComponent, componentConfig: { @@ -3494,14 +3390,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU [panelKey]: updatedPanelConfig, }, }; - + 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); toast.success(`์ปฌ๋Ÿผ์ด ${panelSide === "left" ? "์ขŒ์ธก" : "์šฐ์ธก"} ํŒจ๋„์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค`); @@ -3787,9 +3681,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU isEntityJoin: true, entityJoinTable: column.entityJoinTable, entityJoinColumn: column.entityJoinColumn, - }), + }), style: { - labelDisplay: false, // ๋ผ๋ฒจ ์ˆจ๊น€ + labelDisplay: true, // ๐Ÿ†• ๋ผ๋ฒจ ๊ธฐ๋ณธ ํ‘œ์‹œ labelFontSize: "12px", labelColor: "#212121", labelFontWeight: "500", @@ -3853,9 +3747,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU isEntityJoin: true, entityJoinTable: column.entityJoinTable, entityJoinColumn: column.entityJoinColumn, - }), + }), style: { - labelDisplay: false, // ๋ผ๋ฒจ ์ˆจ๊น€ + labelDisplay: true, // ๐Ÿ†• ๋ผ๋ฒจ ๊ธฐ๋ณธ ํ‘œ์‹œ labelFontSize: "14px", labelColor: "#000000", // ์ˆœ์ˆ˜ํ•œ ๊ฒ€์ • labelFontWeight: "500", @@ -4123,322 +4017,325 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU ); // ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ - 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; + 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; + } } } } - } - - const compType = (targetComponent as any)?.componentType; - - // ์ž๊ธฐ ์ž์‹ ์„ ์ž์‹ ์—๊ฒŒ ๋“œ๋กญํ•˜๋Š” ๊ฒƒ ๋ฐฉ์ง€ - if (targetComponent && + + 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 - ), + 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("์ปดํฌ๋„ŒํŠธ๊ฐ€ ํƒญ์œผ๋กœ ์ด๋™๋˜์—ˆ์Šต๋‹ˆ๋‹ค"); + }; + } + 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; // ํƒญ์œผ๋กœ ์ด๋™ ์™„๋ฃŒ, ์ผ๋ฐ˜ ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ ๋กœ์ง ์Šคํ‚ต } - - 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( @@ -5491,6 +5388,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU ); } + // ๐Ÿ”ง ScreenDesigner ๋ Œ๋”๋ง ํ™•์ธ (๋””๋ฒ„๊ทธ ์™„๋ฃŒ - ์ฃผ์„ ์ฒ˜๋ฆฌ) + // console.log("๐Ÿ  ScreenDesigner ๋ Œ๋”!", Date.now()); + return ( @@ -5509,15 +5409,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU onGenerateMultilang={handleGenerateMultilang} isGeneratingMultilang={isGeneratingMultilang} onOpenMultilangSettings={() => setShowMultilangSettingsModal(true)} - isPanelOpen={panelStates.v2?.isOpen || false} - onTogglePanel={() => togglePanel("v2")} + isPanelOpen={panelStates.v2?.isOpen || false} + onTogglePanel={() => togglePanel("v2")} /> {/* ๋ฉ”์ธ ์ปจํ…Œ์ด๋„ˆ (ํŒจ๋„๋“ค + ์บ”๋ฒ„์Šค) */}
{/* ํ†ตํ•ฉ ํŒจ๋„ - ์ขŒ์ธก ์‚ฌ์ด๋“œ๋ฐ” ์ œ๊ฑฐ ํ›„ ๋„ˆ๋น„ 300px๋กœ ํ™•์žฅ */} {panelStates.v2?.isOpen && ( -
-
+
+

ํŒจ๋„

-
- - {(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} +
+
+ ))} +
+
+
+
+
+ + {/* ์‚ญ์ œ ๋ฒ„ํŠผ */} + +
+ ))} +
+ )}
)}
@@ -1183,9 +1228,10 @@ export const ButtonConfigPanel: React.FC = ({ {/* ๐Ÿ†• ๋ฐ์ดํ„ฐ ์ „๋‹ฌ + ๋ชจ๋‹ฌ ์—ด๊ธฐ ์•ก์…˜ ์„ค์ • (deprecated - ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€) */} {component.componentConfig?.action?.type === "openModalWithData" && (
-

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

+

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

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

{/* ๐Ÿ†• ๋ธ”๋ก ๊ธฐ๋ฐ˜ ์ œ๋ชฉ ๋นŒ๋” */} @@ -3544,8 +3590,8 @@ export const ButtonConfigPanel: React.FC = ({

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

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

@@ -3595,11 +3641,13 @@ export const ButtonConfigPanel: React.FC = ({ type="number" className="h-8 text-xs" placeholder="3" - value={component.componentConfig?.action?.eventConfig?.eventPayload?.config?.scheduling?.leadTimeDays || 3} + value={ + component.componentConfig?.action?.eventConfig?.eventPayload?.config?.scheduling?.leadTimeDays || 3 + } onChange={(e) => { onUpdateProperty( "componentConfig.action.eventConfig.eventPayload.config.scheduling.leadTimeDays", - parseInt(e.target.value) || 3 + parseInt(e.target.value) || 3, ); }} /> @@ -3611,11 +3659,14 @@ export const ButtonConfigPanel: React.FC = ({ type="number" className="h-8 text-xs" placeholder="100" - value={component.componentConfig?.action?.eventConfig?.eventPayload?.config?.scheduling?.maxDailyCapacity || 100} + value={ + component.componentConfig?.action?.eventConfig?.eventPayload?.config?.scheduling + ?.maxDailyCapacity || 100 + } onChange={(e) => { onUpdateProperty( "componentConfig.action.eventConfig.eventPayload.config.scheduling.maxDailyCapacity", - parseInt(e.target.value) || 100 + parseInt(e.target.value) || 100, ); }} /> @@ -3623,8 +3674,8 @@ export const ButtonConfigPanel: React.FC = ({

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

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 = ({ definitionName: definition.name, hasConfigPanel: !!definition.configPanel, currentConfig, + defaultSort: currentConfig?.defaultSort, // ๐Ÿ” defaultSort ํ™•์ธ }); // ๐Ÿ”ง ConfigPanelWrapper๋ฅผ ์ธ๋ผ์ธ ํ•จ์ˆ˜ ๋Œ€์‹  ์ง์ ‘ JSX ๋ฐ˜ํ™˜ (๋ฆฌ๋งˆ์šดํŠธ ๋ฐฉ์ง€) @@ -822,8 +823,12 @@ 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 +862,23 @@ 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 +888,9 @@ export const V2PropertiesPanel: React.FC = ({ )} - {/* ์˜ต์…˜ */} + {/* ์˜ต์…˜ - ์ž…๋ ฅ ํ•„๋“œ์—์„œ๋Š” ํ•ญ์ƒ ํ‘œ์‹œ, ๊ธฐํƒ€ ์ปดํฌ๋„ŒํŠธ๋Š” ์†์„ฑ์ด ์ •์˜๋œ ๊ฒฝ์šฐ๋งŒ ํ‘œ์‹œ */}
- {widget.required !== undefined && ( + {(isInputField || widget.required !== undefined) && (
= ({
)} - {widget.readonly !== undefined && ( + {(isInputField || widget.readonly !== undefined) && (
= ({
)} - {/* ์ˆจ๊น€ ์˜ต์…˜ */} + {/* ์ˆจ๊น€ ์˜ต์…˜ - ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์—์„œ ํ‘œ์‹œ */}
= ({ 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 />
+ {/* ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • */} +
+ + +

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

+
+ {/* ๋‹ค์ค‘ ์„ ํƒ */}
{/* ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค ํ—ค๋” - ์ขŒ์ธก ๊ณ ์ • */} - {/* ์ฒดํฌ๋ฐ•์Šค ํ—ค๋” - ์ขŒ์ธก ๊ณ ์ • */} - - {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 && ( -
+ # {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" ? ( 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
+ {/* ๊ธฐ๋ณธ ์ •๋ ฌ ์„ค์ • */} +
+
+

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

+

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

+
+
+
+
+ + +
+ + {config.defaultSort?.columnName && ( +
+ + +
+ )} +
+
+ {/* ๊ฐ€๋กœ ์Šคํฌ๋กค ๋ฐ ์ปฌ๋Ÿผ ๊ณ ์ • */}
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-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/TimelineSchedulerConfigPanel.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerConfigPanel.tsx index d297f860..a715e408 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerConfigPanel.tsx @@ -5,32 +5,10 @@ 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 { 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"; @@ -52,10 +30,7 @@ interface ColumnInfo { displayName: string; } -export function TimelineSchedulerConfigPanel({ - config, - onChange, -}: TimelineSchedulerConfigPanelProps) { +export function TimelineSchedulerConfigPanel({ config, onChange }: TimelineSchedulerConfigPanelProps) { const [tables, setTables] = useState([]); const [sourceColumns, setSourceColumns] = useState([]); const [resourceColumns, setResourceColumns] = useState([]); @@ -74,7 +49,7 @@ export function TimelineSchedulerConfigPanel({ tableList.map((t: any) => ({ tableName: t.table_name || t.tableName, displayName: t.display_name || t.displayName || t.table_name || t.tableName, - })) + })), ); } } catch (err) { @@ -100,7 +75,7 @@ export function TimelineSchedulerConfigPanel({ columns.map((col: any) => ({ columnName: col.column_name || col.columnName, displayName: col.display_name || col.displayName || col.column_name || col.columnName, - })) + })), ); } } catch (err) { @@ -125,7 +100,7 @@ export function TimelineSchedulerConfigPanel({ columns.map((col: any) => ({ columnName: col.column_name || col.columnName, displayName: col.display_name || col.displayName || col.column_name || col.columnName, - })) + })), ); } } catch (err) { @@ -168,11 +143,9 @@ export function TimelineSchedulerConfigPanel({ {/* ์†Œ์Šค ๋ฐ์ดํ„ฐ ์„ค์ • (์Šค์ผ€์ค„ ์ƒ์„ฑ ๊ธฐ์ค€) */} - - ์Šค์ผ€์ค„ ์ƒ์„ฑ ์„ค์ • - + ์Šค์ผ€์ค„ ์ƒ์„ฑ ์„ค์ • -

+

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

@@ -208,20 +181,14 @@ export function TimelineSchedulerConfigPanel({ className="h-8 w-full justify-between text-xs" disabled={loading} > - {config.sourceConfig?.tableName ? ( - tables.find((t) => t.tableName === config.sourceConfig?.tableName) - ?.displayName || config.sourceConfig.tableName - ) : ( - "์†Œ์Šค ํ…Œ์ด๋ธ” ์„ ํƒ..." - )} + {config.sourceConfig?.tableName + ? tables.find((t) => t.tableName === config.sourceConfig?.tableName)?.displayName || + config.sourceConfig.tableName + : "์†Œ์Šค ํ…Œ์ด๋ธ” ์„ ํƒ..."} - + { const lowerSearch = search.toLowerCase(); @@ -233,9 +200,7 @@ export function TimelineSchedulerConfigPanel({ > - - ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. {tables.map((table) => (
{table.displayName} - - {table.tableName} - + {table.tableName}
))} @@ -272,11 +233,11 @@ export function TimelineSchedulerConfigPanel({ {/* ์†Œ์Šค ํ•„๋“œ ๋งคํ•‘ */} {config.sourceConfig?.tableName && ( -
+
{/* ๊ธฐ์ค€์ผ ํ•„๋“œ */} -
+
-

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

+

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

{/* ์ˆ˜๋Ÿ‰ ํ•„๋“œ */} @@ -339,7 +298,7 @@ export function TimelineSchedulerConfigPanel({
{/* ๊ทธ๋ฃน๋ช… ํ•„๋“œ */} -
+
- updateConfig({ defaultZoomLevel: v as any }) - } + onValueChange={(v) => updateConfig({ defaultZoomLevel: v as any })} > @@ -534,9 +469,7 @@ export function TimelineSchedulerConfigPanel({ - updateConfig({ height: parseInt(e.target.value) || 500 }) - } + onChange={(e) => updateConfig({ height: parseInt(e.target.value) || 500 })} className="h-8 text-xs" />
@@ -547,9 +480,7 @@ export function TimelineSchedulerConfigPanel({ - updateConfig({ rowHeight: parseInt(e.target.value) || 50 }) - } + onChange={(e) => updateConfig({ rowHeight: parseInt(e.target.value) || 50 })} className="h-8 text-xs" />
@@ -558,26 +489,17 @@ export function TimelineSchedulerConfigPanel({
- updateConfig({ editable: v })} - /> + updateConfig({ editable: v })} />
- updateConfig({ draggable: v })} - /> + updateConfig({ draggable: v })} />
- updateConfig({ resizable: v })} - /> + updateConfig({ resizable: v })} />
diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts b/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts index 7ce7a9d6..94c001d4 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts +++ b/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts @@ -3,13 +3,7 @@ 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 { TimelineSchedulerConfig, ScheduleItem, Resource, ZoomLevel, UseTimelineDataResult } from "../types"; import { zoomLevelDays, defaultTimelineSchedulerConfig } from "../config"; // schedule_mng ํ…Œ์ด๋ธ” ๊ณ ์ • (๊ณตํ†ต ์Šค์ผ€์ค„ ํ…Œ์ด๋ธ”) @@ -37,16 +31,14 @@ const addDays = (date: Date, days: number): Date => { export function useTimelineData( config: TimelineSchedulerConfig, externalSchedules?: ScheduleItem[], - externalResources?: Resource[] + 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 [zoomLevel, setZoomLevel] = useState(config.defaultZoomLevel || "day"); const [viewStartDate, setViewStartDate] = useState(() => { if (config.initialDate) { return new Date(config.initialDate); @@ -69,9 +61,7 @@ export function useTimelineData( }, [viewStartDate, zoomLevel]); // ํ…Œ์ด๋ธ”๋ช…: ๊ธฐ๋ณธ์ ์œผ๋กœ schedule_mng ์‚ฌ์šฉ, ์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ์„ค์ • ์‹œ ํ•ด๋‹น ํ…Œ์ด๋ธ” ์‚ฌ์šฉ - const tableName = config.useCustomTable && config.customTableName - ? config.customTableName - : SCHEDULE_TABLE; + const tableName = config.useCustomTable && config.customTableName ? config.customTableName : SCHEDULE_TABLE; const resourceTableName = config.resourceTable; @@ -88,7 +78,7 @@ export function useTimelineData( 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", @@ -134,17 +124,13 @@ export function useTimelineData( sourceKeys: currentSourceKeys, }); - const response = await apiClient.post( - `/table-management/tables/${tableName}/data`, - { - page: 1, - size: 10000, - autoFilter: true, - } - ); + 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 || []; + const responseData = response.data?.data?.data || response.data?.data || []; let rawData = Array.isArray(responseData) ? responseData : []; // ํด๋ผ์ด์–ธํŠธ ์ธก ํ•„ํ„ฐ๋ง ์ ์šฉ (schedule_mng ํ…Œ์ด๋ธ”์ธ ๊ฒฝ์šฐ) @@ -156,9 +142,7 @@ export function useTimelineData( // ์„ ํƒ๋œ ํ’ˆ๋ชฉ ํ•„ํ„ฐ (source_group_key ๊ธฐ์ค€) if (currentSourceKeys.length > 0) { - rawData = rawData.filter((row: any) => - currentSourceKeys.includes(row.source_group_key) - ); + rawData = rawData.filter((row: any) => currentSourceKeys.includes(row.source_group_key)); } console.log("[useTimelineData] ํ•„ํ„ฐ๋ง ํ›„ ์Šค์ผ€์ค„:", rawData.length, "๊ฑด"); @@ -194,9 +178,7 @@ export function useTimelineData( title: String(row[effectiveMapping.title] || ""), startDate: row[effectiveMapping.startDate] || "", endDate: row[effectiveMapping.endDate] || "", - status: effectiveMapping.status - ? row[effectiveMapping.status] || "planned" - : "planned", + status: effectiveMapping.status ? row[effectiveMapping.status] || "planned" : "planned", progress, color: fieldMapping.color ? row[fieldMapping.color] : undefined, data: row, @@ -228,26 +210,20 @@ export function useTimelineData( } try { - const response = await apiClient.post( - `/table-management/tables/${resourceTableName}/data`, - { - page: 1, - size: 1000, - autoFilter: true, - } - ); + 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 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, + group: resourceFieldMapping.group ? row[resourceFieldMapping.group] : undefined, })); setResources(mappedResources); @@ -270,44 +246,41 @@ export function useTimelineData( // ์ด๋ฒคํŠธ ๋ฒ„์Šค ๋ฆฌ์Šค๋„ˆ - ํ…Œ์ด๋ธ” ์„ ํƒ ๋ณ€๊ฒฝ (ํ’ˆ๋ชฉ ์„ ํƒ ์‹œ ํ•ด๋‹น ์Šค์ผ€์ค„๋งŒ ํ‘œ์‹œ) useEffect(() => { - const unsubscribeSelection = v2EventBus.subscribe( - V2_EVENTS.TABLE_SELECTION_CHANGE, - (payload) => { - console.log("[useTimelineData] TABLE_SELECTION_CHANGE ์ˆ˜์‹ :", { - tableName: payload.tableName, - selectedCount: payload.selectedCount, - }); + 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; + // ์„ค์ •๋œ ๊ทธ๋ฃน ํ•„๋“œ๋ช… ์‚ฌ์šฉ (์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’๋“ค 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); - } + // ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ์—์„œ 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; } - console.log("[useTimelineData] ์„ ํƒ๋œ ๊ทธ๋ฃน ํ‚ค:", { - groupByField, - keys: sourceKeys, - }); - - // ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋ฐ ref ๋™๊ธฐํ™” - selectedSourceKeysRef.current = sourceKeys; - setSelectedSourceKeys(sourceKeys); + if (key && !sourceKeys.includes(key)) { + sourceKeys.push(key); + } } - ); + + console.log("[useTimelineData] ์„ ํƒ๋œ ๊ทธ๋ฃน ํ‚ค:", { + groupByField, + keys: sourceKeys, + }); + + // ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋ฐ ref ๋™๊ธฐํ™” + selectedSourceKeysRef.current = sourceKeys; + setSelectedSourceKeys(sourceKeys); + }); return () => { unsubscribeSelection(); @@ -325,27 +298,21 @@ export function useTimelineData( // ์ด๋ฒคํŠธ ๋ฒ„์Šค ๋ฆฌ์Šค๋„ˆ - ์Šค์ผ€์ค„ ์ƒ์„ฑ ์™„๋ฃŒ ๋ฐ ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ 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(); - } + 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(); - } + const unsubscribeComplete = v2EventBus.subscribe(V2_EVENTS.SCHEDULE_GENERATE_COMPLETE, (payload) => { + if (payload.success) { + console.log("[useTimelineData] SCHEDULE_GENERATE_COMPLETE ์ˆ˜์‹ , ์Šค์ผ€์ค„ ์ƒˆ๋กœ๊ณ ์นจ:", payload); + fetchSchedules(); } - ); + }); return () => { unsubscribeRefresh(); @@ -390,23 +357,20 @@ export function useTimelineData( 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.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)) - ); + setSchedules((prev) => prev.map((s) => (s.id === id ? { ...s, ...updates } : s))); } catch (err: any) { console.error("์Šค์ผ€์ค„ ์—…๋ฐ์ดํŠธ ์˜ค๋ฅ˜:", err); throw err; } }, - [tableName, fieldMapping, config.editable] + [tableName, fieldMapping, config.editable], ); // ์Šค์ผ€์ค„ ์ถ”๊ฐ€ @@ -427,10 +391,7 @@ export function useTimelineData( if (fieldMapping.progress && schedule.progress !== undefined) insertData[fieldMapping.progress] = schedule.progress; - const response = await apiClient.post( - `/table-management/tables/${tableName}/data`, - insertData - ); + const response = await apiClient.post(`/table-management/tables/${tableName}/data`, insertData); const newId = response.data?.data?.id || Date.now().toString(); @@ -441,7 +402,7 @@ export function useTimelineData( throw err; } }, - [tableName, fieldMapping, config.editable] + [tableName, fieldMapping, config.editable], ); // ์Šค์ผ€์ค„ ์‚ญ์ œ @@ -459,7 +420,7 @@ export function useTimelineData( throw err; } }, - [tableName, config.editable] + [tableName, config.editable], ); // ์ƒˆ๋กœ๊ณ ์นจ diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/types.ts b/frontend/lib/registry/components/v2-timeline-scheduler/types.ts index b7a836a6..baf59741 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/types.ts +++ b/frontend/lib/registry/components/v2-timeline-scheduler/types.ts @@ -10,12 +10,7 @@ export type ZoomLevel = "day" | "week" | "month"; /** * ์Šค์ผ€์ค„ ์ƒํƒœ */ -export type ScheduleStatus = - | "planned" - | "in_progress" - | "completed" - | "delayed" - | "cancelled"; +export type ScheduleStatus = "planned" | "in_progress" | "completed" | "delayed" | "cancelled"; /** * ์Šค์ผ€์ค„ ํ•ญ๋ชฉ (๊ฐ„ํŠธ ๋ฐ”) @@ -107,10 +102,10 @@ export interface ResourceFieldMapping { * ์Šค์ผ€์ค„ ํƒ€์ž… (schedule_mng.schedule_type) */ export type ScheduleType = - | "PRODUCTION" // ์ƒ์‚ฐ๊ณ„ํš - | "MAINTENANCE" // ์ •๋น„๊ณ„ํš - | "SHIPPING" // ๋ฐฐ์ฐจ๊ณ„ํš - | "WORK_ASSIGN"; // ์ž‘์—…๋ฐฐ์ • + | "PRODUCTION" // ์ƒ์‚ฐ๊ณ„ํš + | "MAINTENANCE" // ์ •๋น„๊ณ„ํš + | "SHIPPING" // ๋ฐฐ์ฐจ๊ณ„ํš + | "WORK_ASSIGN"; // ์ž‘์—…๋ฐฐ์ • /** * ์†Œ์Šค ๋ฐ์ดํ„ฐ ์„ค์ • (์Šค์ผ€์ค„ ์ƒ์„ฑ ๊ธฐ์ค€์ด ๋˜๋Š” ์›๋ณธ ๋ฐ์ดํ„ฐ) diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 0798b00e..87864625 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -484,6 +484,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์ดˆ ๋‚ด ์žฌํ˜ธ์ถœ ์‹œ ๋ฌด์‹œ @@ -524,6 +533,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, @@ -539,6 +556,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); @@ -668,6 +692,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); } @@ -709,7 +737,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; @@ -1002,7 +1032,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; @@ -1588,7 +1620,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_๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ•„๋“œ์—์„œ ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ์ฐพ๊ธฐ @@ -2026,7 +2069,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; @@ -2881,8 +2926,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, @@ -3015,9 +3059,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, @@ -3026,19 +3074,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; } @@ -3227,8 +3274,7 @@ export class ButtonActionExecutor { window.dispatchEvent(modalEvent); - // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ (๊ฐ„๋‹จํ•˜๊ฒŒ) - toast.success(config.successMessage || "๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค."); + // ๋ชจ๋‹ฌ ์—ด๊ธฐ๋Š” UI ์ „ํ™˜์ด๋ฏ€๋กœ ์„ฑ๊ณต ํ† ์ŠคํŠธ๋ฅผ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ return true; } else { @@ -3455,10 +3501,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); @@ -3986,16 +4035,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 = [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; @@ -4095,16 +4155,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 = [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 ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋ณ‘ํ•ฉ @@ -7094,7 +7163,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 fa7606b6..fff56bf9 100644 --- a/frontend/lib/utils/layoutV2Converter.ts +++ b/frontend/lib/utils/layoutV2Converter.ts @@ -38,19 +38,19 @@ interface LegacyLayoutData { // ============================================ 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); } - + // ๊ฒฐ๊ณผ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” (์›๋ณธ ๋ณต์‚ฌ) - let result = { ...nestedComp }; - + const result = { ...nestedComp }; + // ๐Ÿ†• ํƒญ ์œ„์ ฏ์ธ ๊ฒฝ์šฐ ์žฌ๊ท€์ ์œผ๋กœ ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋„ ์ฒ˜๋ฆฌ if (nestedComponentType === "v2-tabs-widget") { const config = result.componentConfig || {}; @@ -69,31 +69,35 @@ function applyDefaultsToNestedComponents(components: any[]): any[] { }; } } - + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„์ธ ๊ฒฝ์šฐ ์žฌ๊ท€์ ์œผ๋กœ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋„ ์ฒ˜๋ฆฌ 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, + 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); @@ -102,7 +106,7 @@ function applyDefaultsToNestedComponents(components: any[]): any[] { componentConfig: mergedNestedConfig, }; } - + return result; }); } @@ -112,7 +116,7 @@ function applyDefaultsToNestedComponents(components: any[]): any[] { // ============================================ function applyDefaultsToSplitPanelComponents(mergedConfig: Record): Record { const result = { ...mergedConfig }; - + // leftPanel.components ์ฒ˜๋ฆฌ if (result.leftPanel?.components) { result.leftPanel = { @@ -120,7 +124,7 @@ function applyDefaultsToSplitPanelComponents(mergedConfig: Record): components: applyDefaultsToNestedComponents(result.leftPanel.components), }; } - + // rightPanel.components ์ฒ˜๋ฆฌ if (result.rightPanel?.components) { result.rightPanel = { @@ -128,7 +132,7 @@ function applyDefaultsToSplitPanelComponents(mergedConfig: Record): components: applyDefaultsToNestedComponents(result.rightPanel.components), }; } - + return result; } @@ -149,7 +153,7 @@ export function convertV2ToLegacy(v2Layout: LayoutV2 | null): LegacyLayoutData | if (componentType === "v2-split-panel-layout") { mergedConfig = applyDefaultsToSplitPanelComponents(mergedConfig); } - + // ๐Ÿ†• ํƒญ ์œ„์ ฏ์ธ ๊ฒฝ์šฐ ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ์—๋„ ๊ธฐ๋ณธ๊ฐ’ ์ ์šฉ if (componentType === "v2-tabs-widget" && mergedConfig.tabs) { mergedConfig = { @@ -183,13 +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, @@ -231,21 +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, 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/services/ScheduleGeneratorService.ts b/frontend/lib/v2-core/services/ScheduleGeneratorService.ts index d73dd3a3..5d693005 100644 --- a/frontend/lib/v2-core/services/ScheduleGeneratorService.ts +++ b/frontend/lib/v2-core/services/ScheduleGeneratorService.ts @@ -10,11 +10,7 @@ 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 type { ScheduleType, V2ScheduleGenerateRequestEvent, V2ScheduleGenerateApplyEvent } from "../events/types"; import { apiClient } from "@/lib/api/client"; import { toast } from "sonner"; @@ -122,13 +118,10 @@ function getDefaultPeriod(): { start: string; end: string } { * const { showConfirmDialog, previewResult, handleConfirm } = useScheduleGenerator(config); * ``` */ -export function useScheduleGenerator( - scheduleConfig?: ScheduleGenerationConfig | null -): UseScheduleGeneratorReturn { +export function useScheduleGenerator(scheduleConfig?: ScheduleGenerationConfig | null): UseScheduleGeneratorReturn { // ์ƒํƒœ const [selectedData, setSelectedData] = useState([]); - const [previewResult, setPreviewResult] = - useState(null); + const [previewResult, setPreviewResult] = useState(null); const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [isLoading, setIsLoading] = useState(false); const currentRequestIdRef = useRef(""); @@ -136,57 +129,53 @@ export function useScheduleGenerator( // 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๊ฐ€ ์—†์œผ๋ฉด ๋ชจ๋“  ํ…Œ์ด๋ธ”์˜ ์„ ํƒ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅ + 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, "๊ฑด"); + 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(() => { - console.log("[useScheduleGenerator] ์ด๋ฒคํŠธ ๊ตฌ๋… ์‹œ์ž‘"); - 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", - }, - }; + 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); @@ -250,7 +239,7 @@ export function useScheduleGenerator( } finally { setIsLoading(false); } - } + }, ); return unsubscribe; }, [selectedData, scheduleConfig]); @@ -299,10 +288,9 @@ export function useScheduleGenerator( tableName: configToUse?.target?.tableName || "schedule_mng", }); - toast.success( - `${response.data.applied?.created || 0}๊ฑด์˜ ์Šค์ผ€์ค„์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, - { id: "schedule-apply" } - ); + toast.success(`${response.data.applied?.created || 0}๊ฑด์˜ ์Šค์ผ€์ค„์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, { + id: "schedule-apply", + }); setShowConfirmDialog(false); setPreviewResult(null); } catch (error: any) { @@ -311,7 +299,7 @@ export function useScheduleGenerator( } finally { setIsLoading(false); } - } + }, ); return unsubscribe; }, [previewResult, scheduleConfig]); diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index ca804adc..2e23bc81 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -15,7 +15,8 @@ const nextConfig = { // ์‹คํ—˜์  ๊ธฐ๋Šฅ ํ™œ์„ฑํ™” experimental: { - outputFileTracingRoot: undefined, + // ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™” (Next.js 15+) + webpackMemoryOptimizations: true, }, // API ํ”„๋ก์‹œ ์„ค์ • - ๋ฐฑ์—”๋“œ๋กœ ์š”์ฒญ ์ „๋‹ฌ diff --git a/frontend/types/v2-components.ts b/frontend/types/v2-components.ts index d985699d..68539c08 100644 --- a/frontend/types/v2-components.ts +++ b/frontend/types/v2-components.ts @@ -232,13 +232,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 +544,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/scripts/dev/start-all-parallel.bat b/scripts/dev/start-all-parallel.bat index ea10551e..08049b48 100644 --- a/scripts/dev/start-all-parallel.bat +++ b/scripts/dev/start-all-parallel.bat @@ -26,12 +26,14 @@ if %errorlevel% neq 0 ( echo [OK] Docker Desktop์ด ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค. echo. -REM ๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ ์ •๋ฆฌ -echo [2/5] ๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ ์ •๋ฆฌ ์ค‘... +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 -echo [OK] ์ปจํ…Œ์ด๋„ˆ ์ •๋ฆฌ ์™„๋ฃŒ +docker system prune -f >nul 2>&1 +echo [OK] ์ปจํ…Œ์ด๋„ˆ ๋ฐ ์ด๋ฏธ์ง€ ์ •๋ฆฌ ์™„๋ฃŒ echo. REM ๋ณ‘๋ ฌ ๋นŒ๋“œ (docker-compose ์ž์ฒด๊ฐ€ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ) @@ -39,8 +41,8 @@ echo [3/5] ์ด๋ฏธ์ง€ ๋นŒ๋“œ ์ค‘... (๋ฐฑ์—”๋“œ + ํ”„๋ก ํŠธ์—”๋“œ ๋ณ‘๋ ฌ) echo ์ด ์ž‘์—…์€ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค... echo. -REM ๋ฐฑ์—”๋“œ ๋นŒ๋“œ -docker-compose -f docker-compose.backend.win.yml build +REM ๋ฐฑ์—”๋“œ ๋นŒ๋“œ (์บ์‹œ ์—†์ด ์™„์ „ ์žฌ๋นŒ๋“œ) +docker-compose -f docker-compose.backend.win.yml build --no-cache if %errorlevel% neq 0 ( echo [ERROR] ๋ฐฑ์—”๋“œ ๋นŒ๋“œ ์‹คํŒจ! pause @@ -49,8 +51,8 @@ if %errorlevel% neq 0 ( echo [OK] ๋ฐฑ์—”๋“œ ๋นŒ๋“œ ์™„๋ฃŒ echo. -REM ํ”„๋ก ํŠธ์—”๋“œ ๋นŒ๋“œ -docker-compose -f docker-compose.frontend.win.yml build +REM ํ”„๋ก ํŠธ์—”๋“œ ๋นŒ๋“œ (์บ์‹œ ์—†์ด ์™„์ „ ์žฌ๋นŒ๋“œ) +docker-compose -f docker-compose.frontend.win.yml build --no-cache if %errorlevel% neq 0 ( echo [ERROR] ํ”„๋ก ํŠธ์—”๋“œ ๋นŒ๋“œ ์‹คํŒจ! pause