diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index 9661ab0a..c4c29503 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -62,9 +62,23 @@ export async function getColumnList( try { const { tableName } = req.params; const { page = 1, size = 50 } = req.query; + + // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) + let companyCode = req.user?.companyCode; + + if (!companyCode && req.user?.userId) { + // JWT์— ์—†์œผ๋ฉด DB์—์„œ ์กฐํšŒ + const { query } = require("../database/db"); + const userResult = await query( + `SELECT company_code FROM user_info WHERE user_id = $1`, + [req.user.userId] + ); + companyCode = userResult[0]?.company_code; + logger.info(`DB์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ ์กฐํšŒ (์ปฌ๋Ÿผ ๋ชฉ๋ก): ${req.user.userId} โ†’ ${companyCode}`); + } logger.info( - `=== ์ปฌ๋Ÿผ ์ •๋ณด ์กฐํšŒ ์‹œ์ž‘: ${tableName} (page: ${page}, size: ${size}) ===` + `=== ์ปฌ๋Ÿผ ์ •๋ณด ์กฐํšŒ ์‹œ์ž‘: ${tableName} (page: ${page}, size: ${size}), company: ${companyCode} ===` ); if (!tableName) { @@ -84,7 +98,8 @@ export async function getColumnList( const result = await tableManagementService.getColumnList( tableName, parseInt(page as string), - parseInt(size as string) + parseInt(size as string), + companyCode // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ „๋‹ฌ ); logger.info( @@ -124,8 +139,22 @@ export async function updateColumnSettings( try { const { tableName, columnName } = req.params; const settings: ColumnSettings = req.body; + + // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) + let companyCode = req.user?.companyCode; + + if (!companyCode && req.user?.userId) { + // JWT์— ์—†์œผ๋ฉด DB์—์„œ ์กฐํšŒ + const { query } = require("../database/db"); + const userResult = await query( + `SELECT company_code FROM user_info WHERE user_id = $1`, + [req.user.userId] + ); + companyCode = userResult[0]?.company_code; + logger.info(`DB์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ ์กฐํšŒ: ${req.user.userId} โ†’ ${companyCode}`); + } - logger.info(`=== ์ปฌ๋Ÿผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}.${columnName} ===`); + logger.info(`=== ์ปฌ๋Ÿผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}.${columnName}, company: ${companyCode} ===`); if (!tableName || !columnName) { const response: ApiResponse = { @@ -153,14 +182,34 @@ export async function updateColumnSettings( return; } + if (!companyCode) { + logger.error(`ํšŒ์‚ฌ ์ฝ”๋“œ ๋ˆ„๋ฝ: ${tableName}.${columnName}`, { + user: req.user, + hasUser: !!req.user, + userId: req.user?.userId, + companyCodeFromJWT: req.user?.companyCode, + }); + const response: ApiResponse = { + success: false, + message: "ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + error: { + code: "MISSING_COMPANY_CODE", + details: "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", + }, + }; + res.status(400).json(response); + return; + } + const tableManagementService = new TableManagementService(); await tableManagementService.updateColumnSettings( tableName, columnName, - settings + settings, + companyCode // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ „๋‹ฌ ); - logger.info(`์ปฌ๋Ÿผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${tableName}.${columnName}`); + logger.info(`์ปฌ๋Ÿผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${tableName}.${columnName}, company: ${companyCode}`); const response: ApiResponse = { success: true, @@ -194,8 +243,28 @@ export async function updateAllColumnSettings( try { const { tableName } = req.params; const columnSettings: ColumnSettings[] = req.body; + + // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) + let companyCode = req.user?.companyCode; + + if (!companyCode && req.user?.userId) { + // JWT์— ์—†์œผ๋ฉด DB์—์„œ ์กฐํšŒ + const { query } = require("../database/db"); + const userResult = await query( + `SELECT company_code FROM user_info WHERE user_id = $1`, + [req.user.userId] + ); + companyCode = userResult[0]?.company_code; + logger.info(`DB์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ ์กฐํšŒ: ${req.user.userId} โ†’ ${companyCode}`); + } - logger.info(`=== ์ „์ฒด ์ปฌ๋Ÿผ ์„ค์ • ์ผ๊ด„ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName} ===`); + // ๐Ÿ” ๋””๋ฒ„๊น…: ์‚ฌ์šฉ์ž ์ •๋ณด ์ถœ๋ ฅ + logger.info(`[DEBUG] req.user:`, JSON.stringify(req.user, null, 2)); + logger.info(`[DEBUG] req.user?.companyCode: ${req.user?.companyCode}`); + logger.info(`[DEBUG] req.user?.userId: ${req.user?.userId}`); + logger.info(`[DEBUG] companyCode ์ตœ์ข…๊ฐ’: ${companyCode}`); + + logger.info(`=== ์ „์ฒด ์ปฌ๋Ÿผ ์„ค์ • ์ผ๊ด„ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}, company: ${companyCode} ===`); if (!tableName) { const response: ApiResponse = { @@ -223,14 +292,35 @@ export async function updateAllColumnSettings( return; } + if (!companyCode) { + logger.error(`ํšŒ์‚ฌ ์ฝ”๋“œ ๋ˆ„๋ฝ (์ผ๊ด„ ์—…๋ฐ์ดํŠธ): ${tableName}`, { + user: req.user, + hasUser: !!req.user, + userId: req.user?.userId, + companyCodeFromJWT: req.user?.companyCode, + settingsCount: columnSettings.length, + }); + const response: ApiResponse = { + success: false, + message: "ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + error: { + code: "MISSING_COMPANY_CODE", + details: "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", + }, + }; + res.status(400).json(response); + return; + } + const tableManagementService = new TableManagementService(); await tableManagementService.updateAllColumnSettings( tableName, - columnSettings + columnSettings, + companyCode // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ „๋‹ฌ ); logger.info( - `์ „์ฒด ์ปฌ๋Ÿผ ์„ค์ • ์ผ๊ด„ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${tableName}, ${columnSettings.length}๊ฐœ` + `์ „์ฒด ์ปฌ๋Ÿผ ์„ค์ • ์ผ๊ด„ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${tableName}, ${columnSettings.length}๊ฐœ, company: ${companyCode}` ); const response: ApiResponse = { @@ -453,9 +543,23 @@ export async function updateColumnInputType( try { const { tableName, columnName } = req.params; const { inputType, detailSettings } = req.body; + + // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) + let companyCode = req.user?.companyCode; + + if (!companyCode && req.user?.userId) { + // JWT์— ์—†์œผ๋ฉด DB์—์„œ ์กฐํšŒ + const { query } = require("../database/db"); + const userResult = await query( + `SELECT company_code FROM user_info WHERE user_id = $1`, + [req.user.userId] + ); + companyCode = userResult[0]?.company_code; + logger.info(`DB์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ ์กฐํšŒ: ${req.user.userId} โ†’ ${companyCode}`); + } logger.info( - `=== ์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ์‹œ์ž‘: ${tableName}.${columnName} = ${inputType} ===` + `=== ์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ์‹œ์ž‘: ${tableName}.${columnName} = ${inputType}, company: ${companyCode} ===` ); if (!tableName || !columnName || !inputType) { @@ -471,16 +575,37 @@ export async function updateColumnInputType( return; } + if (!companyCode) { + logger.error(`ํšŒ์‚ฌ ์ฝ”๋“œ ๋ˆ„๋ฝ (์ž…๋ ฅ ํƒ€์ž…): ${tableName}.${columnName}`, { + user: req.user, + hasUser: !!req.user, + userId: req.user?.userId, + companyCodeFromJWT: req.user?.companyCode, + inputType, + }); + const response: ApiResponse = { + success: false, + message: "ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + error: { + code: "MISSING_COMPANY_CODE", + details: "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", + }, + }; + res.status(400).json(response); + return; + } + const tableManagementService = new TableManagementService(); await tableManagementService.updateColumnInputType( tableName, columnName, inputType, + companyCode, detailSettings ); logger.info( - `์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ์™„๋ฃŒ: ${tableName}.${columnName} = ${inputType}` + `์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ์™„๋ฃŒ: ${tableName}.${columnName} = ${inputType}, company: ${companyCode}` ); const response: ApiResponse = { @@ -960,7 +1085,24 @@ export async function getColumnWebTypes( ): Promise { try { const { tableName } = req.params; - logger.info(`=== ์ปฌ๋Ÿผ ์›นํƒ€์ž… ์ •๋ณด ์กฐํšŒ ์‹œ์ž‘: ${tableName} ===`); + + // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) + let companyCode = req.user?.companyCode; + + if (!companyCode && req.user?.userId) { + // JWT์— ์—†์œผ๋ฉด DB์—์„œ ์กฐํšŒ + const { query } = require("../database/db"); + const userResult = await query( + `SELECT company_code FROM user_info WHERE user_id = $1`, + [req.user.userId] + ); + companyCode = userResult[0]?.company_code; + logger.info(`DB์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ ์กฐํšŒ (์กฐํšŒ): ${req.user.userId} โ†’ ${companyCode}`); + } + + logger.info( + `=== ์ปฌ๋Ÿผ ์›นํƒ€์ž… ์ •๋ณด ์กฐํšŒ ์‹œ์ž‘: ${tableName}, company: ${companyCode} ===` + ); if (!tableName) { const response: ApiResponse = { @@ -975,12 +1117,33 @@ export async function getColumnWebTypes( return; } + if (!companyCode) { + logger.error(`ํšŒ์‚ฌ ์ฝ”๋“œ ๋ˆ„๋ฝ (์กฐํšŒ): ${tableName}`, { + user: req.user, + hasUser: !!req.user, + userId: req.user?.userId, + companyCodeFromJWT: req.user?.companyCode, + }); + const response: ApiResponse = { + success: false, + message: "ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + error: { + code: "MISSING_COMPANY_CODE", + details: "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", + }, + }; + res.status(400).json(response); + return; + } + const tableManagementService = new TableManagementService(); - const inputTypes = - await tableManagementService.getColumnInputTypes(tableName); + const inputTypes = await tableManagementService.getColumnInputTypes( + tableName, + companyCode + ); logger.info( - `์ปฌ๋Ÿผ ์ž…๋ ฅํƒ€์ž… ์ •๋ณด ์กฐํšŒ ์™„๋ฃŒ: ${tableName}, ${inputTypes.length}๊ฐœ ์ปฌ๋Ÿผ` + `์ปฌ๋Ÿผ ์ž…๋ ฅํƒ€์ž… ์ •๋ณด ์กฐํšŒ ์™„๋ฃŒ: ${tableName}, company: ${companyCode}, ${inputTypes.length}๊ฐœ ์ปฌ๋Ÿผ` ); const response: ApiResponse = { diff --git a/backend-node/src/routes/tableManagementRoutes.ts b/backend-node/src/routes/tableManagementRoutes.ts index 9840c9c4..0ec8c162 100644 --- a/backend-node/src/routes/tableManagementRoutes.ts +++ b/backend-node/src/routes/tableManagementRoutes.ts @@ -27,8 +27,8 @@ import { const router = express.Router(); -// ๋ชจ๋“  ๋ผ์šฐํŠธ์— ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ์ ์šฉ (ํ…Œ์ŠคํŠธ ์‹œ์—๋Š” ์ฃผ์„ ์ฒ˜๋ฆฌ) -// router.use(authenticateToken); +// ๋ชจ๋“  ๋ผ์šฐํŠธ์— ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ์ ์šฉ +router.use(authenticateToken); /** * ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์กฐํšŒ diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index cad0727e..0c612b51 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -42,48 +42,100 @@ class NumberingRuleService { logger.info("์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ ์‹œ์ž‘", { companyCode }); const pool = getPool(); - const query = ` - SELECT - rule_id AS "ruleId", - rule_name AS "ruleName", - description, - separator, - reset_period AS "resetPeriod", - current_sequence AS "currentSequence", - table_name AS "tableName", - column_name AS "columnName", - company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", - created_at AS "createdAt", - updated_at AS "updatedAt", - created_by AS "createdBy" - FROM numbering_rules - WHERE company_code = $1 OR company_code = '*' - ORDER BY created_at DESC - `; + + // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ company_code="*" ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Œ + let query: string; + let params: any[]; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๊ฐ€๋Šฅ + query = ` + SELECT + rule_id AS "ruleId", + rule_name AS "ruleName", + description, + separator, + reset_period AS "resetPeriod", + current_sequence AS "currentSequence", + table_name AS "tableName", + column_name AS "columnName", + company_code AS "companyCode", + menu_objid AS "menuObjid", + scope_type AS "scopeType", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy" + FROM numbering_rules + ORDER BY created_at DESC + `; + params = []; + logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์ฒด ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ"); + } else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ๋งŒ ์กฐํšŒ (company_code="*" ์ œ์™ธ) + query = ` + SELECT + rule_id AS "ruleId", + rule_name AS "ruleName", + description, + separator, + reset_period AS "resetPeriod", + current_sequence AS "currentSequence", + table_name AS "tableName", + column_name AS "columnName", + company_code AS "companyCode", + menu_objid AS "menuObjid", + scope_type AS "scopeType", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy" + FROM numbering_rules + WHERE company_code = $1 + ORDER BY created_at DESC + `; + params = [companyCode]; + logger.info("ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ", { companyCode }); + } - const result = await pool.query(query, [companyCode]); + const result = await pool.query(query, params); // ๊ฐ ๊ทœ์น™์˜ ํŒŒํŠธ ์ •๋ณด ์กฐํšŒ for (const rule of result.rows) { - const partsQuery = ` - SELECT - id, - part_order AS "order", - part_type AS "partType", - generation_method AS "generationMethod", - auto_config AS "autoConfig", - manual_config AS "manualConfig" - FROM numbering_rule_parts - WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*') - ORDER BY part_order - `; + let partsQuery: string; + let partsParams: any[]; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ํŒŒํŠธ ์กฐํšŒ + partsQuery = ` + SELECT + id, + part_order AS "order", + part_type AS "partType", + generation_method AS "generationMethod", + auto_config AS "autoConfig", + manual_config AS "manualConfig" + FROM numbering_rule_parts + WHERE rule_id = $1 + ORDER BY part_order + `; + partsParams = [rule.ruleId]; + } else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ํŒŒํŠธ๋งŒ ์กฐํšŒ + partsQuery = ` + SELECT + id, + part_order AS "order", + part_type AS "partType", + generation_method AS "generationMethod", + auto_config AS "autoConfig", + manual_config AS "manualConfig" + FROM numbering_rule_parts + WHERE rule_id = $1 AND company_code = $2 + ORDER BY part_order + `; + partsParams = [rule.ruleId, companyCode]; + } - const partsResult = await pool.query(partsQuery, [ - rule.ruleId, - companyCode, - ]); + const partsResult = await pool.query(partsQuery, partsParams); rule.parts = partsResult.rows; } @@ -114,49 +166,95 @@ class NumberingRuleService { // menuObjid๊ฐ€ ์—†์œผ๋ฉด global ๊ทœ์น™๋งŒ ๋ฐ˜ํ™˜ if (!menuObjid) { - const query = ` - SELECT - rule_id AS "ruleId", - rule_name AS "ruleName", - description, - separator, - reset_period AS "resetPeriod", - current_sequence AS "currentSequence", - table_name AS "tableName", - column_name AS "columnName", - company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", - created_at AS "createdAt", - updated_at AS "updatedAt", - created_by AS "createdBy" - FROM numbering_rules - WHERE (company_code = $1 OR company_code = '*') - AND scope_type = 'global' - ORDER BY created_at DESC - `; + let query: string; + let params: any[]; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  global ๊ทœ์น™ ์กฐํšŒ + query = ` + SELECT + rule_id AS "ruleId", + rule_name AS "ruleName", + description, + separator, + reset_period AS "resetPeriod", + current_sequence AS "currentSequence", + table_name AS "tableName", + column_name AS "columnName", + company_code AS "companyCode", + menu_objid AS "menuObjid", + scope_type AS "scopeType", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy" + FROM numbering_rules + WHERE scope_type = 'global' + ORDER BY created_at DESC + `; + params = []; + } else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ global ๊ทœ์น™๋งŒ ์กฐํšŒ (company_code="*" ์ œ์™ธ) + query = ` + SELECT + rule_id AS "ruleId", + rule_name AS "ruleName", + description, + separator, + reset_period AS "resetPeriod", + current_sequence AS "currentSequence", + table_name AS "tableName", + column_name AS "columnName", + company_code AS "companyCode", + menu_objid AS "menuObjid", + scope_type AS "scopeType", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy" + FROM numbering_rules + WHERE company_code = $1 AND scope_type = 'global' + ORDER BY created_at DESC + `; + params = [companyCode]; + } - const result = await pool.query(query, [companyCode]); + const result = await pool.query(query, params); // ํŒŒํŠธ ์ •๋ณด ์ถ”๊ฐ€ for (const rule of result.rows) { - const partsQuery = ` - SELECT - id, - part_order AS "order", - part_type AS "partType", - generation_method AS "generationMethod", - auto_config AS "autoConfig", - manual_config AS "manualConfig" - FROM numbering_rule_parts - WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*') - ORDER BY part_order - `; + let partsQuery: string; + let partsParams: any[]; + + if (companyCode === "*") { + partsQuery = ` + SELECT + id, + part_order AS "order", + part_type AS "partType", + generation_method AS "generationMethod", + auto_config AS "autoConfig", + manual_config AS "manualConfig" + FROM numbering_rule_parts + WHERE rule_id = $1 + ORDER BY part_order + `; + partsParams = [rule.ruleId]; + } else { + partsQuery = ` + SELECT + id, + part_order AS "order", + part_type AS "partType", + generation_method AS "generationMethod", + auto_config AS "autoConfig", + manual_config AS "manualConfig" + FROM numbering_rule_parts + WHERE rule_id = $1 AND company_code = $2 + ORDER BY part_order + `; + partsParams = [rule.ruleId, companyCode]; + } - const partsResult = await pool.query(partsQuery, [ - rule.ruleId, - companyCode, - ]); + const partsResult = await pool.query(partsQuery, partsParams); rule.parts = partsResult.rows; } @@ -186,53 +284,102 @@ class NumberingRuleService { const level2MenuObjid = hierarchyResult.rowCount > 0 ? hierarchyResult.rows[0].objid : null; - // ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ทœ์น™ ์กฐํšŒ - const query = ` - SELECT - rule_id AS "ruleId", - rule_name AS "ruleName", - description, - separator, - reset_period AS "resetPeriod", - current_sequence AS "currentSequence", - table_name AS "tableName", - column_name AS "columnName", - company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", - created_at AS "createdAt", - updated_at AS "updatedAt", - created_by AS "createdBy" - FROM numbering_rules - WHERE (company_code = $1 OR company_code = '*') - AND ( + // ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ทœ์น™ ์กฐํšŒ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ ์šฉ) + let query: string; + let params: any[]; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ๊ทœ์น™ ์กฐํšŒ + query = ` + SELECT + rule_id AS "ruleId", + rule_name AS "ruleName", + description, + separator, + reset_period AS "resetPeriod", + current_sequence AS "currentSequence", + table_name AS "tableName", + column_name AS "columnName", + company_code AS "companyCode", + menu_objid AS "menuObjid", + scope_type AS "scopeType", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy" + FROM numbering_rules + WHERE scope_type = 'global' - OR (scope_type = 'menu' AND menu_objid = $2) - ) - ORDER BY scope_type DESC, created_at DESC - `; + OR (scope_type = 'menu' AND menu_objid = $1) + ORDER BY scope_type DESC, created_at DESC + `; + params = [level2MenuObjid]; + } else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ๊ทœ์น™๋งŒ ์กฐํšŒ + query = ` + SELECT + rule_id AS "ruleId", + rule_name AS "ruleName", + description, + separator, + reset_period AS "resetPeriod", + current_sequence AS "currentSequence", + table_name AS "tableName", + column_name AS "columnName", + company_code AS "companyCode", + menu_objid AS "menuObjid", + scope_type AS "scopeType", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy" + FROM numbering_rules + WHERE company_code = $1 + AND ( + scope_type = 'global' + OR (scope_type = 'menu' AND menu_objid = $2) + ) + ORDER BY scope_type DESC, created_at DESC + `; + params = [companyCode, level2MenuObjid]; + } - const result = await pool.query(query, [companyCode, level2MenuObjid]); + const result = await pool.query(query, params); // ํŒŒํŠธ ์ •๋ณด ์ถ”๊ฐ€ for (const rule of result.rows) { - const partsQuery = ` - SELECT - id, - part_order AS "order", - part_type AS "partType", - generation_method AS "generationMethod", - auto_config AS "autoConfig", - manual_config AS "manualConfig" - FROM numbering_rule_parts - WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*') - ORDER BY part_order - `; + let partsQuery: string; + let partsParams: any[]; + + if (companyCode === "*") { + partsQuery = ` + SELECT + id, + part_order AS "order", + part_type AS "partType", + generation_method AS "generationMethod", + auto_config AS "autoConfig", + manual_config AS "manualConfig" + FROM numbering_rule_parts + WHERE rule_id = $1 + ORDER BY part_order + `; + partsParams = [rule.ruleId]; + } else { + partsQuery = ` + SELECT + id, + part_order AS "order", + part_type AS "partType", + generation_method AS "generationMethod", + auto_config AS "autoConfig", + manual_config AS "manualConfig" + FROM numbering_rule_parts + WHERE rule_id = $1 AND company_code = $2 + ORDER BY part_order + `; + partsParams = [rule.ruleId, companyCode]; + } - const partsResult = await pool.query(partsQuery, [ - rule.ruleId, - companyCode, - ]); + const partsResult = await pool.query(partsQuery, partsParams); rule.parts = partsResult.rows; } @@ -262,45 +409,97 @@ class NumberingRuleService { companyCode: string ): Promise { const pool = getPool(); - const query = ` - SELECT - rule_id AS "ruleId", - rule_name AS "ruleName", - description, - separator, - reset_period AS "resetPeriod", - current_sequence AS "currentSequence", - table_name AS "tableName", - column_name AS "columnName", - company_code AS "companyCode", - menu_objid AS "menuObjid", - scope_type AS "scopeType", - created_at AS "createdAt", - updated_at AS "updatedAt", - created_by AS "createdBy" - FROM numbering_rules - WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*') - `; + + // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ company_code="*" ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Œ + let query: string; + let params: any[]; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ๊ทœ์น™ ์กฐํšŒ ๊ฐ€๋Šฅ + query = ` + SELECT + rule_id AS "ruleId", + rule_name AS "ruleName", + description, + separator, + reset_period AS "resetPeriod", + current_sequence AS "currentSequence", + table_name AS "tableName", + column_name AS "columnName", + company_code AS "companyCode", + menu_objid AS "menuObjid", + scope_type AS "scopeType", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy" + FROM numbering_rules + WHERE rule_id = $1 + `; + params = [ruleId]; + } else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ๊ทœ์น™๋งŒ ์กฐํšŒ + query = ` + SELECT + rule_id AS "ruleId", + rule_name AS "ruleName", + description, + separator, + reset_period AS "resetPeriod", + current_sequence AS "currentSequence", + table_name AS "tableName", + column_name AS "columnName", + company_code AS "companyCode", + menu_objid AS "menuObjid", + scope_type AS "scopeType", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy" + FROM numbering_rules + WHERE rule_id = $1 AND company_code = $2 + `; + params = [ruleId, companyCode]; + } - const result = await pool.query(query, [ruleId, companyCode]); + const result = await pool.query(query, params); if (result.rowCount === 0) return null; const rule = result.rows[0]; - const partsQuery = ` - SELECT - id, - part_order AS "order", - part_type AS "partType", - generation_method AS "generationMethod", - auto_config AS "autoConfig", - manual_config AS "manualConfig" - FROM numbering_rule_parts - WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*') - ORDER BY part_order - `; + // ํŒŒํŠธ ์ •๋ณด ์กฐํšŒ + let partsQuery: string; + let partsParams: any[]; + + if (companyCode === "*") { + partsQuery = ` + SELECT + id, + part_order AS "order", + part_type AS "partType", + generation_method AS "generationMethod", + auto_config AS "autoConfig", + manual_config AS "manualConfig" + FROM numbering_rule_parts + WHERE rule_id = $1 + ORDER BY part_order + `; + partsParams = [ruleId]; + } else { + partsQuery = ` + SELECT + id, + part_order AS "order", + part_type AS "partType", + generation_method AS "generationMethod", + auto_config AS "autoConfig", + manual_config AS "manualConfig" + FROM numbering_rule_parts + WHERE rule_id = $1 AND company_code = $2 + ORDER BY part_order + `; + partsParams = [ruleId, companyCode]; + } - const partsResult = await pool.query(partsQuery, [ruleId, companyCode]); + const partsResult = await pool.query(partsQuery, partsParams); rule.parts = partsResult.rows; return rule; diff --git a/backend-node/src/services/tableCategoryValueService.ts b/backend-node/src/services/tableCategoryValueService.ts index 5e91d332..8a20aac1 100644 --- a/backend-node/src/services/tableCategoryValueService.ts +++ b/backend-node/src/services/tableCategoryValueService.ts @@ -17,23 +17,50 @@ class TableCategoryValueService { logger.info("์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ ๋ชฉ๋ก ์กฐํšŒ", { tableName, companyCode }); const pool = getPool(); - const query = ` - SELECT - tc.table_name AS "tableName", - tc.column_name AS "columnName", - tc.column_name AS "columnLabel", - COUNT(cv.value_id) AS "valueCount" - FROM table_type_columns tc - LEFT JOIN table_column_category_values cv - ON tc.table_name = cv.table_name - AND tc.column_name = cv.column_name - AND cv.is_active = true - AND (cv.company_code = $2 OR cv.company_code = '*') - WHERE tc.table_name = $1 - AND tc.input_type = 'category' - GROUP BY tc.table_name, tc.column_name, tc.display_order - ORDER BY tc.display_order, tc.column_name - `; + + // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ company_code="*" ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Œ + let query: string; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ + query = ` + SELECT + tc.table_name AS "tableName", + tc.column_name AS "columnName", + tc.column_name AS "columnLabel", + COUNT(cv.value_id) AS "valueCount" + FROM table_type_columns tc + LEFT JOIN table_column_category_values cv + ON tc.table_name = cv.table_name + AND tc.column_name = cv.column_name + AND cv.is_active = true + WHERE tc.table_name = $1 + AND tc.input_type = 'category' + GROUP BY tc.table_name, tc.column_name, tc.display_order + ORDER BY tc.display_order, tc.column_name + `; + logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ ์กฐํšŒ"); + } else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’๋งŒ ์กฐํšŒ + query = ` + SELECT + tc.table_name AS "tableName", + tc.column_name AS "columnName", + tc.column_name AS "columnLabel", + COUNT(cv.value_id) AS "valueCount" + FROM table_type_columns tc + LEFT JOIN table_column_category_values cv + ON tc.table_name = cv.table_name + AND tc.column_name = cv.column_name + AND cv.is_active = true + AND cv.company_code = $2 + WHERE tc.table_name = $1 + AND tc.input_type = 'category' + GROUP BY tc.table_name, tc.column_name, tc.display_order + ORDER BY tc.display_order, tc.column_name + `; + logger.info("ํšŒ์‚ฌ๋ณ„ ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ ์กฐํšŒ", { companyCode }); + } const result = await pool.query(query, [tableName, companyCode]); @@ -67,33 +94,69 @@ class TableCategoryValueService { }); const pool = getPool(); - let query = ` - SELECT - value_id AS "valueId", - table_name AS "tableName", - column_name AS "columnName", - value_code AS "valueCode", - value_label AS "valueLabel", - value_order AS "valueOrder", - parent_value_id AS "parentValueId", - depth, - description, - color, - icon, - is_active AS "isActive", - is_default AS "isDefault", - company_code AS "companyCode", - created_at AS "createdAt", - updated_at AS "updatedAt", - created_by AS "createdBy", - updated_by AS "updatedBy" - FROM table_column_category_values - WHERE table_name = $1 - AND column_name = $2 - AND (company_code = $3 OR company_code = '*') - `; - - const params: any[] = [tableName, columnName, companyCode]; + + // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ company_code="*" ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Œ + let query: string; + let params: any[]; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ + query = ` + SELECT + value_id AS "valueId", + table_name AS "tableName", + column_name AS "columnName", + value_code AS "valueCode", + value_label AS "valueLabel", + value_order AS "valueOrder", + parent_value_id AS "parentValueId", + depth, + description, + color, + icon, + is_active AS "isActive", + is_default AS "isDefault", + company_code AS "companyCode", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy", + updated_by AS "updatedBy" + FROM table_column_category_values + WHERE table_name = $1 + AND column_name = $2 + `; + params = [tableName, columnName]; + logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ"); + } else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’๋งŒ ์กฐํšŒ + query = ` + SELECT + value_id AS "valueId", + table_name AS "tableName", + column_name AS "columnName", + value_code AS "valueCode", + value_label AS "valueLabel", + value_order AS "valueOrder", + parent_value_id AS "parentValueId", + depth, + description, + color, + icon, + is_active AS "isActive", + is_default AS "isDefault", + company_code AS "companyCode", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy", + updated_by AS "updatedBy" + FROM table_column_category_values + WHERE table_name = $1 + AND column_name = $2 + AND company_code = $3 + `; + params = [tableName, columnName, companyCode]; + logger.info("ํšŒ์‚ฌ๋ณ„ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ", { companyCode }); + } if (!includeInactive) { query += ` AND is_active = true`; @@ -109,6 +172,7 @@ class TableCategoryValueService { logger.info(`์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ${result.rows.length}๊ฐœ ์กฐํšŒ ์™„๋ฃŒ`, { tableName, columnName, + companyCode, }); return values; @@ -129,22 +193,34 @@ class TableCategoryValueService { const pool = getPool(); try { - // ์ค‘๋ณต ์ฝ”๋“œ ์ฒดํฌ - const duplicateQuery = ` - SELECT value_id - FROM table_column_category_values - WHERE table_name = $1 - AND column_name = $2 - AND value_code = $3 - AND (company_code = $4 OR company_code = '*') - `; + // ์ค‘๋ณต ์ฝ”๋“œ ์ฒดํฌ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ ์šฉ) + let duplicateQuery: string; + let duplicateParams: any[]; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ํšŒ์‚ฌ์—์„œ ์ค‘๋ณต ์ฒดํฌ + duplicateQuery = ` + SELECT value_id + FROM table_column_category_values + WHERE table_name = $1 + AND column_name = $2 + AND value_code = $3 + `; + duplicateParams = [value.tableName, value.columnName, value.valueCode]; + } else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ํšŒ์‚ฌ์—์„œ๋งŒ ์ค‘๋ณต ์ฒดํฌ + duplicateQuery = ` + SELECT value_id + FROM table_column_category_values + WHERE table_name = $1 + AND column_name = $2 + AND value_code = $3 + AND company_code = $4 + `; + duplicateParams = [value.tableName, value.columnName, value.valueCode, companyCode]; + } - const duplicateResult = await pool.query(duplicateQuery, [ - value.tableName, - value.columnName, - value.valueCode, - companyCode, - ]); + const duplicateResult = await pool.query(duplicateQuery, duplicateParams); if (duplicateResult.rows.length > 0) { throw new Error("์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค"); @@ -260,28 +336,57 @@ class TableCategoryValueService { setClauses.push(`updated_by = $${paramIndex++}`); values.push(userId); - values.push(valueId, companyCode); - - const updateQuery = ` - UPDATE table_column_category_values - SET ${setClauses.join(", ")} - WHERE value_id = $${paramIndex++} - AND (company_code = $${paramIndex++} OR company_code = '*') - RETURNING - value_id AS "valueId", - table_name AS "tableName", - column_name AS "columnName", - value_code AS "valueCode", - value_label AS "valueLabel", - value_order AS "valueOrder", - description, - color, - icon, - is_active AS "isActive", - is_default AS "isDefault", - updated_at AS "updatedAt", - updated_by AS "updatedBy" - `; + // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋Š” company_code ์กฐ๊ฑด ์ œ์™ธ + let updateQuery: string; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ˆ˜์ • ๊ฐ€๋Šฅ + values.push(valueId); + updateQuery = ` + UPDATE table_column_category_values + SET ${setClauses.join(", ")} + WHERE value_id = $${paramIndex++} + RETURNING + value_id AS "valueId", + table_name AS "tableName", + column_name AS "columnName", + value_code AS "valueCode", + value_label AS "valueLabel", + value_order AS "valueOrder", + description, + color, + icon, + is_active AS "isActive", + is_default AS "isDefault", + company_code AS "companyCode", + updated_at AS "updatedAt", + updated_by AS "updatedBy" + `; + } else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’๋งŒ ์ˆ˜์ • ๊ฐ€๋Šฅ + values.push(valueId, companyCode); + updateQuery = ` + UPDATE table_column_category_values + SET ${setClauses.join(", ")} + WHERE value_id = $${paramIndex++} + AND company_code = $${paramIndex++} + RETURNING + value_id AS "valueId", + table_name AS "tableName", + column_name AS "columnName", + value_code AS "valueCode", + value_label AS "valueLabel", + value_order AS "valueOrder", + description, + color, + icon, + is_active AS "isActive", + is_default AS "isDefault", + company_code AS "companyCode", + updated_at AS "updatedAt", + updated_by AS "updatedBy" + `; + } const result = await pool.query(updateQuery, values); @@ -309,30 +414,65 @@ class TableCategoryValueService { const pool = getPool(); try { - // ํ•˜์œ„ ๊ฐ’ ์ฒดํฌ - const checkQuery = ` - SELECT COUNT(*) as count - FROM table_column_category_values - WHERE parent_value_id = $1 - AND (company_code = $2 OR company_code = '*') - AND is_active = true - `; + // ํ•˜์œ„ ๊ฐ’ ์ฒดํฌ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ ์šฉ) + let checkQuery: string; + let checkParams: any[]; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ํ•˜์œ„ ๊ฐ’ ์ฒดํฌ + checkQuery = ` + SELECT COUNT(*) as count + FROM table_column_category_values + WHERE parent_value_id = $1 + AND is_active = true + `; + checkParams = [valueId]; + } else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ํ•˜์œ„ ๊ฐ’๋งŒ ์ฒดํฌ + checkQuery = ` + SELECT COUNT(*) as count + FROM table_column_category_values + WHERE parent_value_id = $1 + AND company_code = $2 + AND is_active = true + `; + checkParams = [valueId, companyCode]; + } - const checkResult = await pool.query(checkQuery, [valueId, companyCode]); + const checkResult = await pool.query(checkQuery, checkParams); if (parseInt(checkResult.rows[0].count) > 0) { throw new Error("ํ•˜์œ„ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์ด ์žˆ์–ด ์‚ญ์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"); } - // ๋น„ํ™œ์„ฑํ™” - const deleteQuery = ` - UPDATE table_column_category_values - SET is_active = false, updated_at = NOW(), updated_by = $3 - WHERE value_id = $1 - AND (company_code = $2 OR company_code = '*') - `; + // ๋น„ํ™œ์„ฑํ™” (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ ์šฉ) + let deleteQuery: string; + let deleteParams: any[]; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์‚ญ์ œ ๊ฐ€๋Šฅ + deleteQuery = ` + UPDATE table_column_category_values + SET is_active = false, updated_at = NOW(), updated_by = $2 + WHERE value_id = $1 + `; + deleteParams = [valueId, userId]; + } else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’๋งŒ ์‚ญ์ œ ๊ฐ€๋Šฅ + deleteQuery = ` + UPDATE table_column_category_values + SET is_active = false, updated_at = NOW(), updated_by = $3 + WHERE value_id = $1 + AND company_code = $2 + `; + deleteParams = [valueId, companyCode, userId]; + } - await pool.query(deleteQuery, [valueId, companyCode, userId]); + const result = await pool.query(deleteQuery, deleteParams); + + if (result.rowCount === 0) { + throw new Error("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์„ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค"); + } logger.info("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์‚ญ์ œ(๋น„ํ™œ์„ฑํ™”) ์™„๋ฃŒ", { valueId, @@ -355,14 +495,30 @@ class TableCategoryValueService { const pool = getPool(); try { - const deleteQuery = ` - UPDATE table_column_category_values - SET is_active = false, updated_at = NOW(), updated_by = $3 - WHERE value_id = ANY($1::int[]) - AND (company_code = $2 OR company_code = '*') - `; + // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ ์šฉ + let deleteQuery: string; + let deleteParams: any[]; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ผ๊ด„ ์‚ญ์ œ ๊ฐ€๋Šฅ + deleteQuery = ` + UPDATE table_column_category_values + SET is_active = false, updated_at = NOW(), updated_by = $2 + WHERE value_id = ANY($1::int[]) + `; + deleteParams = [valueIds, userId]; + } else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’๋งŒ ์ผ๊ด„ ์‚ญ์ œ ๊ฐ€๋Šฅ + deleteQuery = ` + UPDATE table_column_category_values + SET is_active = false, updated_at = NOW(), updated_by = $3 + WHERE value_id = ANY($1::int[]) + AND company_code = $2 + `; + deleteParams = [valueIds, companyCode, userId]; + } - await pool.query(deleteQuery, [valueIds, companyCode, userId]); + await pool.query(deleteQuery, deleteParams); logger.info("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ผ๊ด„ ์‚ญ์ œ ์™„๋ฃŒ", { count: valueIds.length, @@ -388,18 +544,30 @@ class TableCategoryValueService { await client.query("BEGIN"); for (let i = 0; i < orderedValueIds.length; i++) { - const updateQuery = ` - UPDATE table_column_category_values - SET value_order = $1, updated_at = NOW() - WHERE value_id = $2 - AND (company_code = $3 OR company_code = '*') - `; + // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ ์šฉ + let updateQuery: string; + let updateParams: any[]; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ˆœ์„œ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ + updateQuery = ` + UPDATE table_column_category_values + SET value_order = $1, updated_at = NOW() + WHERE value_id = $2 + `; + updateParams = [i + 1, orderedValueIds[i]]; + } else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’๋งŒ ์ˆœ์„œ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ + updateQuery = ` + UPDATE table_column_category_values + SET value_order = $1, updated_at = NOW() + WHERE value_id = $2 + AND company_code = $3 + `; + updateParams = [i + 1, orderedValueIds[i], companyCode]; + } - await client.query(updateQuery, [ - i + 1, - orderedValueIds[i], - companyCode, - ]); + await client.query(updateQuery, updateParams); } await client.query("COMMIT"); diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index df67e2fe..806ed025 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -113,7 +113,8 @@ export class TableManagementService { async getColumnList( tableName: string, page: number = 1, - size: number = 50 + size: number = 50, + companyCode?: string // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”๊ฐ€ ): Promise<{ columns: ColumnTypeInfo[]; total: number; @@ -123,11 +124,11 @@ export class TableManagementService { }> { try { logger.info( - `์ปฌ๋Ÿผ ์ •๋ณด ์กฐํšŒ ์‹œ์ž‘: ${tableName} (page: ${page}, size: ${size})` + `์ปฌ๋Ÿผ ์ •๋ณด ์กฐํšŒ ์‹œ์ž‘: ${tableName} (page: ${page}, size: ${size}), company: ${companyCode}` ); - // ์บ์‹œ ํ‚ค ์ƒ์„ฑ - const cacheKey = CacheKeys.TABLE_COLUMNS(tableName, page, size); + // ์บ์‹œ ํ‚ค ์ƒ์„ฑ (companyCode ํฌํ•จ) + const cacheKey = CacheKeys.TABLE_COLUMNS(tableName, page, size) + `_${companyCode}`; const countCacheKey = CacheKeys.TABLE_COLUMN_COUNT(tableName); // ์บ์‹œ์—์„œ ๋จผ์ € ํ™•์ธ @@ -161,49 +162,92 @@ export class TableManagementService { // ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ ์šฉํ•œ ์ปฌ๋Ÿผ ์กฐํšŒ const offset = (page - 1) * size; - const rawColumns = await query( - `SELECT - c.column_name as "columnName", - COALESCE(cl.column_label, c.column_name) as "displayName", - c.data_type as "dataType", - c.data_type as "dbType", - COALESCE(cl.input_type, 'text') as "webType", - COALESCE(cl.input_type, 'direct') as "inputType", - COALESCE(cl.detail_settings, '') as "detailSettings", - COALESCE(cl.description, '') as "description", - c.is_nullable as "isNullable", - CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as "isPrimaryKey", - c.column_default as "defaultValue", - c.character_maximum_length as "maxLength", - c.numeric_precision as "numericPrecision", - c.numeric_scale as "numericScale", - cl.code_category as "codeCategory", - cl.code_value as "codeValue", - cl.reference_table as "referenceTable", - cl.reference_column as "referenceColumn", - cl.display_column as "displayColumn", - cl.display_order as "displayOrder", - cl.is_visible as "isVisible", - -- Entity ์กฐ์ธ ์ปฌ๋Ÿผ์˜ ํ‘œ์‹œ ์ปฌ๋Ÿผ ๋ผ๋ฒจ ์กฐํšŒ - dcl.column_label as "displayColumnLabel" - FROM information_schema.columns c - LEFT JOIN column_labels cl ON c.table_name = cl.table_name AND c.column_name = cl.column_name - -- Entity ์กฐ์ธ์˜ display_column์— ๋Œ€ํ•œ ๋ผ๋ฒจ ์ •๋ณด ์กฐํšŒ - LEFT JOIN column_labels dcl ON cl.reference_table = dcl.table_name AND cl.display_column = dcl.column_name - LEFT JOIN ( - SELECT kcu.column_name, kcu.table_name - FROM information_schema.table_constraints tc - JOIN information_schema.key_column_usage kcu - ON tc.constraint_name = kcu.constraint_name - AND tc.table_schema = kcu.table_schema - WHERE tc.constraint_type = 'PRIMARY KEY' - AND tc.table_name = $1 - ) pk ON c.column_name = pk.column_name AND c.table_name = pk.table_name - WHERE c.table_name = $1 - ORDER BY c.ordinal_position - LIMIT $2 OFFSET $3`, - [tableName, size, offset] - ); + + // ๐Ÿ”ฅ company_code๊ฐ€ ์žˆ์œผ๋ฉด table_type_columns ์กฐ์ธํ•˜์—ฌ ํšŒ์‚ฌ๋ณ„ inputType ๊ฐ€์ ธ์˜ค๊ธฐ + const rawColumns = companyCode + ? await query( + `SELECT + c.column_name as "columnName", + COALESCE(cl.column_label, c.column_name) as "displayName", + c.data_type as "dataType", + c.data_type as "dbType", + COALESCE(cl.input_type, 'text') as "webType", + COALESCE(ttc.input_type, cl.input_type, 'direct') as "inputType", + COALESCE(ttc.detail_settings::text, cl.detail_settings, '') as "detailSettings", + COALESCE(cl.description, '') as "description", + c.is_nullable as "isNullable", + CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as "isPrimaryKey", + c.column_default as "defaultValue", + c.character_maximum_length as "maxLength", + c.numeric_precision as "numericPrecision", + c.numeric_scale as "numericScale", + cl.code_category as "codeCategory", + cl.code_value as "codeValue", + cl.reference_table as "referenceTable", + cl.reference_column as "referenceColumn", + cl.display_column as "displayColumn", + cl.display_order as "displayOrder", + cl.is_visible as "isVisible", + dcl.column_label as "displayColumnLabel" + FROM information_schema.columns c + LEFT JOIN column_labels cl ON c.table_name = cl.table_name AND c.column_name = cl.column_name + LEFT JOIN table_type_columns ttc ON c.table_name = ttc.table_name AND c.column_name = ttc.column_name AND ttc.company_code = $4 + LEFT JOIN column_labels dcl ON cl.reference_table = dcl.table_name AND cl.display_column = dcl.column_name + LEFT JOIN ( + SELECT kcu.column_name, kcu.table_name + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + WHERE tc.constraint_type = 'PRIMARY KEY' + AND tc.table_name = $1 + ) pk ON c.column_name = pk.column_name AND c.table_name = pk.table_name + WHERE c.table_name = $1 + ORDER BY c.ordinal_position + LIMIT $2 OFFSET $3`, + [tableName, size, offset, companyCode] + ) + : await query( + `SELECT + c.column_name as "columnName", + COALESCE(cl.column_label, c.column_name) as "displayName", + c.data_type as "dataType", + c.data_type as "dbType", + COALESCE(cl.input_type, 'text') as "webType", + COALESCE(cl.input_type, 'direct') as "inputType", + COALESCE(cl.detail_settings, '') as "detailSettings", + COALESCE(cl.description, '') as "description", + c.is_nullable as "isNullable", + CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as "isPrimaryKey", + c.column_default as "defaultValue", + c.character_maximum_length as "maxLength", + c.numeric_precision as "numericPrecision", + c.numeric_scale as "numericScale", + cl.code_category as "codeCategory", + cl.code_value as "codeValue", + cl.reference_table as "referenceTable", + cl.reference_column as "referenceColumn", + cl.display_column as "displayColumn", + cl.display_order as "displayOrder", + cl.is_visible as "isVisible", + dcl.column_label as "displayColumnLabel" + FROM information_schema.columns c + LEFT JOIN column_labels cl ON c.table_name = cl.table_name AND c.column_name = cl.column_name + LEFT JOIN column_labels dcl ON cl.reference_table = dcl.table_name AND cl.display_column = dcl.column_name + LEFT JOIN ( + SELECT kcu.column_name, kcu.table_name + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + WHERE tc.constraint_type = 'PRIMARY KEY' + AND tc.table_name = $1 + ) pk ON c.column_name = pk.column_name AND c.table_name = pk.table_name + WHERE c.table_name = $1 + ORDER BY c.ordinal_position + LIMIT $2 OFFSET $3`, + [tableName, size, offset] + ); // BigInt๋ฅผ Number๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ JSON ์ง๋ ฌํ™” ๋ฌธ์ œ ํ•ด๊ฒฐ const columns: ColumnTypeInfo[] = rawColumns.map((column) => ({ @@ -312,10 +356,11 @@ export class TableManagementService { async updateColumnSettings( tableName: string, columnName: string, - settings: ColumnSettings + settings: ColumnSettings, + companyCode: string // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”๊ฐ€ ): Promise { try { - logger.info(`์ปฌ๋Ÿผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}.${columnName}`); + logger.info(`์ปฌ๋Ÿผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}.${columnName}, company: ${companyCode}`); // ํ…Œ์ด๋ธ”์ด table_labels์— ์—†์œผ๋ฉด ์ž๋™ ์ถ”๊ฐ€ await this.insertTableIfNotExists(tableName); @@ -356,6 +401,27 @@ export class TableManagementService { ] ); + // ๐Ÿ”ฅ table_type_columns๋„ ์—…๋ฐ์ดํŠธ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ง€์›) + if (settings.inputType) { + // detailSettings๊ฐ€ ๋ฌธ์ž์—ด์ด๋ฉด ํŒŒ์‹ฑ, ๊ฐ์ฒด๋ฉด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ + let parsedDetailSettings = settings.detailSettings; + if (typeof settings.detailSettings === 'string') { + try { + parsedDetailSettings = JSON.parse(settings.detailSettings); + } catch (e) { + logger.warn(`detailSettings ํŒŒ์‹ฑ ์‹คํŒจ, ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ: ${settings.detailSettings}`); + } + } + + await this.updateColumnInputType( + tableName, + columnName, + settings.inputType, + companyCode, + parsedDetailSettings + ); + } + // ์บ์‹œ ๋ฌดํšจํ™” - ํ•ด๋‹น ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ์บ์‹œ ์‚ญ์ œ cache.deleteByPattern(`table_columns:${tableName}:`); cache.delete(CacheKeys.TABLE_COLUMN_COUNT(tableName)); @@ -378,11 +444,12 @@ export class TableManagementService { */ async updateAllColumnSettings( tableName: string, - columnSettings: ColumnSettings[] + columnSettings: ColumnSettings[], + companyCode: string // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”๊ฐ€ ): Promise { try { logger.info( - `์ „์ฒด ์ปฌ๋Ÿผ ์„ค์ • ์ผ๊ด„ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}, ${columnSettings.length}๊ฐœ` + `์ „์ฒด ์ปฌ๋Ÿผ ์„ค์ • ์ผ๊ด„ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}, ${columnSettings.length}๊ฐœ, company: ${companyCode}` ); // Raw Query ํŠธ๋žœ์žญ์…˜ ์‚ฌ์šฉ @@ -398,7 +465,8 @@ export class TableManagementService { await this.updateColumnSettings( tableName, columnName, - columnSetting + columnSetting, + companyCode // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ „๋‹ฌ ); } else { logger.warn( @@ -412,7 +480,7 @@ export class TableManagementService { cache.deleteByPattern(`table_columns:${tableName}:`); cache.delete(CacheKeys.TABLE_COLUMN_COUNT(tableName)); - logger.info(`์ „์ฒด ์ปฌ๋Ÿผ ์„ค์ • ์ผ๊ด„ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${tableName}`); + logger.info(`์ „์ฒด ์ปฌ๋Ÿผ ์„ค์ • ์ผ๊ด„ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${tableName}, company: ${companyCode}`); } catch (error) { logger.error( `์ „์ฒด ์ปฌ๋Ÿผ ์„ค์ • ์ผ๊ด„ ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${tableName}`, @@ -590,16 +658,18 @@ export class TableManagementService { /** * ์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • (์ƒˆ๋กœ์šด ์‹œ์Šคํ…œ) + * @param companyCode - ํšŒ์‚ฌ ์ฝ”๋“œ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ) */ async updateColumnInputType( tableName: string, columnName: string, inputType: string, + companyCode: string, detailSettings?: Record ): Promise { try { logger.info( - `์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ์‹œ์ž‘: ${tableName}.${columnName} = ${inputType}` + `์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ์‹œ์ž‘: ${tableName}.${columnName} = ${inputType}, company: ${companyCode}` ); // ์ž…๋ ฅ ํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ์ƒ์„ธ ์„ค์ • ์ƒ์„ฑ @@ -612,22 +682,28 @@ export class TableManagementService { ...detailSettings, }; - // table_type_columns ํ…Œ์ด๋ธ”์—์„œ ์—…๋ฐ์ดํŠธ + // table_type_columns ํ…Œ์ด๋ธ”์—์„œ ์—…๋ฐ์ดํŠธ (company_code ์ถ”๊ฐ€) await query( `INSERT INTO table_type_columns ( table_name, column_name, input_type, detail_settings, - is_nullable, display_order, created_date, updated_date - ) VALUES ($1, $2, $3, $4, 'Y', 0, now(), now()) - ON CONFLICT (table_name, column_name) + is_nullable, display_order, company_code, created_date, updated_date + ) VALUES ($1, $2, $3, $4, 'Y', 0, $5, now(), now()) + ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET input_type = EXCLUDED.input_type, detail_settings = EXCLUDED.detail_settings, updated_date = now()`, - [tableName, columnName, inputType, JSON.stringify(finalDetailSettings)] + [ + tableName, + columnName, + inputType, + JSON.stringify(finalDetailSettings), + companyCode, + ] ); logger.info( - `์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ์™„๋ฃŒ: ${tableName}.${columnName} = ${inputType}` + `์ปฌ๋Ÿผ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ์™„๋ฃŒ: ${tableName}.${columnName} = ${inputType}, company: ${companyCode}` ); } catch (error) { logger.error( @@ -2978,26 +3054,36 @@ export class TableManagementService { /** * ์ปฌ๋Ÿผ ์ž…๋ ฅํƒ€์ž… ์ •๋ณด ์กฐํšŒ (ํ™”๋ฉด๊ด€๋ฆฌ ์—ฐ๋™์šฉ) + * @param companyCode - ํšŒ์‚ฌ ์ฝ”๋“œ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ) */ - async getColumnInputTypes(tableName: string): Promise { + async getColumnInputTypes( + tableName: string, + companyCode: string + ): Promise { try { - logger.info(`์ปฌ๋Ÿผ ์ž…๋ ฅํƒ€์ž… ์ •๋ณด ์กฐํšŒ: ${tableName}`); + logger.info( + `์ปฌ๋Ÿผ ์ž…๋ ฅํƒ€์ž… ์ •๋ณด ์กฐํšŒ: ${tableName}, company: ${companyCode}` + ); - // column_labels์—์„œ ์ž…๋ ฅํƒ€์ž… ์ •๋ณด ์กฐํšŒ + // table_type_columns์—์„œ ์ž…๋ ฅํƒ€์ž… ์ •๋ณด ์กฐํšŒ (company_code ํ•„ํ„ฐ๋ง) const rawInputTypes = await query( `SELECT - cl.column_name as "columnName", - cl.column_label as "displayName", - COALESCE(cl.input_type, 'text') as "inputType", - '{}'::jsonb as "detailSettings", - ic.is_nullable as "isNullable", - ic.data_type as "dataType" - FROM column_labels cl + ttc.column_name as "columnName", + COALESCE(cl.column_label, ttc.column_name) as "displayName", + ttc.input_type as "inputType", + COALESCE(ttc.detail_settings, '{}'::jsonb) as "detailSettings", + ttc.is_nullable as "isNullable", + ic.data_type as "dataType", + ttc.company_code as "companyCode" + FROM table_type_columns ttc + LEFT JOIN column_labels cl + ON ttc.table_name = cl.table_name AND ttc.column_name = cl.column_name LEFT JOIN information_schema.columns ic - ON cl.table_name = ic.table_name AND cl.column_name = ic.column_name - WHERE cl.table_name = $1 - ORDER BY cl.column_name`, - [tableName] + ON ttc.table_name = ic.table_name AND ttc.column_name = ic.column_name + WHERE ttc.table_name = $1 + AND ttc.company_code = $2 + ORDER BY ttc.display_order, ttc.column_name`, + [tableName, companyCode] ); const inputTypes: ColumnTypeInfo[] = rawInputTypes.map((col) => ({ @@ -3008,18 +3094,21 @@ export class TableManagementService { inputType: col.inputType, detailSettings: col.detailSettings, description: "", // ํ•„์ˆ˜ ํ•„๋“œ ์ถ”๊ฐ€ - isNullable: col.isNullable, + isNullable: col.isNullable === "Y" ? "Y" : "N", // ๐Ÿ”ฅ FIX: string ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜ isPrimaryKey: false, displayOrder: 0, isVisible: true, })); logger.info( - `์ปฌ๋Ÿผ ์ž…๋ ฅํƒ€์ž… ์ •๋ณด ์กฐํšŒ ์™„๋ฃŒ: ${tableName}, ${inputTypes.length}๊ฐœ ์ปฌ๋Ÿผ` + `์ปฌ๋Ÿผ ์ž…๋ ฅํƒ€์ž… ์ •๋ณด ์กฐํšŒ ์™„๋ฃŒ: ${tableName}, company: ${companyCode}, ${inputTypes.length}๊ฐœ ์ปฌ๋Ÿผ` ); return inputTypes; } catch (error) { - logger.error(`์ปฌ๋Ÿผ ์ž…๋ ฅํƒ€์ž… ์ •๋ณด ์กฐํšŒ ์‹คํŒจ: ${tableName}`, error); + logger.error( + `์ปฌ๋Ÿผ ์ž…๋ ฅํƒ€์ž… ์ •๋ณด ์กฐํšŒ ์‹คํŒจ: ${tableName}, company: ${companyCode}`, + error + ); throw error; } } @@ -3028,11 +3117,11 @@ export class TableManagementService { * ๋ ˆ๊ฑฐ์‹œ ์ง€์›: ์ปฌ๋Ÿผ ์›นํƒ€์ž… ์ •๋ณด ์กฐํšŒ * @deprecated getColumnInputTypes ์‚ฌ์šฉ ๊ถŒ์žฅ */ - async getColumnWebTypes(tableName: string): Promise { + async getColumnWebTypes(tableName: string, companyCode: string): Promise { logger.warn( `๋ ˆ๊ฑฐ์‹œ ๋ฉ”์„œ๋“œ ์‚ฌ์šฉ: getColumnWebTypes โ†’ getColumnInputTypes ์‚ฌ์šฉ ๊ถŒ์žฅ` ); - return this.getColumnInputTypes(tableName); + return this.getColumnInputTypes(tableName, companyCode); // ๐Ÿ”ฅ FIX: companyCode ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ } /** diff --git a/db/migrations/RUN_044_MIGRATION.md b/db/migrations/RUN_044_MIGRATION.md new file mode 100644 index 00000000..1ece80e8 --- /dev/null +++ b/db/migrations/RUN_044_MIGRATION.md @@ -0,0 +1,280 @@ +# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ 044: table_type_columns์— company_code ์ถ”๊ฐ€ + +## ๋ชฉ์  + +ํšŒ์‚ฌ๋ณ„๋กœ ๋…๋ฆฝ์ ์ธ ์ปฌ๋Ÿผ ํƒ€์ž… ์ •์˜๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. + +### ํ•ด๊ฒฐํ•˜๋Š” ๋ฌธ์ œ + +**ํ˜„์žฌ ๋ฌธ์ œ**: +- ํšŒ์‚ฌ A: `item_info.material` โ†’ `category` (๋“œ๋กญ๋‹ค์šด) +- ํšŒ์‚ฌ B: `item_info.material` โ†’ `text` (์ž์œ  ์ž…๋ ฅ) +- โŒ ํ˜„์žฌ๋Š” ๋‘˜ ์ค‘ ํ•˜๋‚˜๋งŒ ์„ ํƒ ๊ฐ€๋Šฅ! + +**์ˆ˜์ • ํ›„**: +- โœ… ๊ฐ ํšŒ์‚ฌ๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ์ปฌ๋Ÿผ ํƒ€์ž…์„ ์„ค์ • ๊ฐ€๋Šฅ + +--- + +## ์˜ํ–ฅ๋ฐ›๋Š” ํ…Œ์ด๋ธ” + +- `table_type_columns` + - `company_code VARCHAR(20)` ์ปฌ๋Ÿผ ์ถ”๊ฐ€ + - ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋“  ํšŒ์‚ฌ์— ๋ณต์ œ + - ๋ณตํ•ฉ ์œ ๋‹ˆํฌ ์ธ๋ฑ์Šค ์ƒ์„ฑ + +--- + +## ์‹คํ–‰ ๋ฐฉ๋ฒ• + +### Docker ํ™˜๊ฒฝ (๊ถŒ์žฅ) + +```bash +docker exec -i erp-node-db-1 psql -U postgres -d ilshin < db/migrations/044_add_company_code_to_table_type_columns.sql +``` + +### ๋กœ์ปฌ PostgreSQL + +```bash +psql -U postgres -d ilshin -f db/migrations/044_add_company_code_to_table_type_columns.sql +``` + +### pgAdmin / DBeaver + +1. `db/migrations/044_add_company_code_to_table_type_columns.sql` ํŒŒ์ผ ์—ด๊ธฐ +2. ์ „์ฒด ๋‚ด์šฉ ๋ณต์‚ฌ +3. SQL ์ฟผ๋ฆฌ ์ฐฝ์— ๋ถ™์—ฌ๋„ฃ๊ธฐ +4. ์‹คํ–‰ (F5 ๋˜๋Š” Execute) + +--- + +## ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋‹จ๊ณ„ + +1. **company_code ์ปฌ๋Ÿผ ์ถ”๊ฐ€** (nullable) +2. **๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋ฐฑ์—…** (์ž„์‹œ ํ…Œ์ด๋ธ”) +3. **๋ฐ์ดํ„ฐ ๋ณต์ œ** (๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋“  ํšŒ์‚ฌ์— ๋ณต์ œ) +4. **๊ธฐ์กด ๋ฐ์ดํ„ฐ ์‚ญ์ œ** (company_code๊ฐ€ NULL์ธ ๊ฒƒ) +5. **NOT NULL ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€** +6. **๋ณตํ•ฉ ์œ ๋‹ˆํฌ ์ธ๋ฑ์Šค ์ƒ์„ฑ** (table_name, column_name, company_code) +7. **๋‹จ์ˆœ ์ธ๋ฑ์Šค ์ƒ์„ฑ** (company_code) +8. **์™ธ๋ž˜ํ‚ค ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€** (company_info ์ฐธ์กฐ) + +--- + +## ๊ฒ€์ฆ ๋ฐฉ๋ฒ• + +### 1. ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ํ™•์ธ + +```sql +SELECT column_name, data_type, is_nullable +FROM information_schema.columns +WHERE table_name = 'table_type_columns' AND column_name = 'company_code'; + +-- ์˜ˆ์ƒ ๊ฒฐ๊ณผ: +-- column_name | data_type | is_nullable +-- company_code | character varying | NO +``` + +### 2. ์ธ๋ฑ์Šค ์ƒ์„ฑ ํ™•์ธ + +```sql +SELECT indexname, indexdef +FROM pg_indexes +WHERE tablename = 'table_type_columns' +ORDER BY indexname; + +-- ์˜ˆ์ƒ ๊ฒฐ๊ณผ: +-- idx_table_column_type_company +-- idx_table_type_columns_company +``` + +### 3. ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ™•์ธ + +```sql +-- ํšŒ์‚ฌ๋ณ„ ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ +SELECT company_code, COUNT(*) as column_count +FROM table_type_columns +GROUP BY company_code +ORDER BY company_code; + +-- NULL ํ™•์ธ (์—†์–ด์•ผ ์ •์ƒ) +SELECT COUNT(*) as null_count +FROM table_type_columns +WHERE company_code IS NULL; + +-- ์˜ˆ์ƒ ๊ฒฐ๊ณผ: 0 +``` + +### 4. ํšŒ์‚ฌ๋ณ„ ๋…๋ฆฝ์„ฑ ํ™•์ธ + +```sql +-- ๊ฐ™์€ ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ์ด ํšŒ์‚ฌ๋ณ„๋กœ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ +SELECT + table_name, + column_name, + COUNT(DISTINCT company_code) as company_count, + STRING_AGG(DISTINCT company_code, ', ') as companies +FROM table_type_columns +GROUP BY table_name, column_name +HAVING COUNT(DISTINCT company_code) > 1 +ORDER BY company_count DESC +LIMIT 10; +``` + +### 5. ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ์กฐ๊ฑด ํ™•์ธ + +```sql +SELECT + tc.constraint_name, + tc.table_name, + kcu.column_name, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name +FROM information_schema.table_constraints AS tc +JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name +JOIN information_schema.constraint_column_usage AS ccu + ON ccu.constraint_name = tc.constraint_name +WHERE tc.table_name = 'table_type_columns' + AND tc.constraint_type = 'FOREIGN KEY'; + +-- ์˜ˆ์ƒ ๊ฒฐ๊ณผ: +-- fk_table_type_columns_company | table_type_columns | company_code | company_info | company_code +``` + +--- + +## ๋กค๋ฐฑ ๋ฐฉ๋ฒ• (๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ) + +```sql +BEGIN; + +-- 1. ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ์กฐ๊ฑด ์ œ๊ฑฐ +ALTER TABLE table_type_columns +DROP CONSTRAINT IF EXISTS fk_table_type_columns_company; + +-- 2. ์ธ๋ฑ์Šค ์ œ๊ฑฐ +DROP INDEX IF EXISTS idx_table_column_type_company; +DROP INDEX IF EXISTS idx_table_type_columns_company; + +-- 3. company_code๋ฅผ nullable๋กœ ๋ณ€๊ฒฝ +ALTER TABLE table_type_columns +ALTER COLUMN company_code DROP NOT NULL; + +-- 4. company_code ์ปฌ๋Ÿผ ์ œ๊ฑฐ +ALTER TABLE table_type_columns +DROP COLUMN IF EXISTS company_code; + +COMMIT; +``` + +--- + +## ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค + +### ์‹œ๋‚˜๋ฆฌ์˜ค 1: ํšŒ์‚ฌ๋ณ„ ๋‹ค๋ฅธ ํƒ€์ž… ์„ค์ • + +```sql +-- ํšŒ์‚ฌ A: material์„ ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ์„ค์ • +UPDATE table_type_columns +SET input_type = 'category' +WHERE table_name = 'item_info' + AND column_name = 'material' + AND company_code = 'COMPANY_A'; + +-- ํšŒ์‚ฌ B: material์„ ํ…์ŠคํŠธ๋กœ ์„ค์ • +UPDATE table_type_columns +SET input_type = 'text' +WHERE table_name = 'item_info' + AND column_name = 'material' + AND company_code = 'COMPANY_B'; + +-- ํ™•์ธ +SELECT table_name, column_name, input_type, company_code +FROM table_type_columns +WHERE table_name = 'item_info' AND column_name = 'material' +ORDER BY company_code; + +-- ์˜ˆ์ƒ ๊ฒฐ๊ณผ: +-- item_info | material | category | * +-- item_info | material | text | COMPANY_7 +``` + +### ์‹œ๋‚˜๋ฆฌ์˜ค 2: ์œ ๋‹ˆํฌ ์ œ์•ฝ์กฐ๊ฑด ํ™•์ธ + +```sql +-- ๊ฐ™์€ ํšŒ์‚ฌ์—์„œ ๊ฐ™์€ ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ ์ค‘๋ณต ์‚ฝ์ž… ์‹œ๋„ (์‹คํŒจํ•ด์•ผ ์ •์ƒ) +INSERT INTO table_type_columns (table_name, column_name, input_type, company_code) +VALUES ('test_table', 'test_column', 'text', 'COMPANY_A'); + +-- ๋‹ค์‹œ ์‹œ๋„ (์—๋Ÿฌ ๋ฐœ์ƒํ•ด์•ผ ํ•จ) +INSERT INTO table_type_columns (table_name, column_name, input_type, company_code) +VALUES ('test_table', 'test_column', 'number', 'COMPANY_A'); + +-- ์˜ˆ์ƒ ์—๋Ÿฌ: +-- ERROR: duplicate key value violates unique constraint "idx_table_column_type_company" +``` + +--- + +## ์ฃผ์˜์‚ฌํ•ญ + +1. **๋ฐฑ์—… ํ•„์ˆ˜**: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ์ „ ๋ฐ˜๋“œ์‹œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—… +2. **๋ฐ์ดํ„ฐ ๋ณต์ œ**: ๊ธฐ์กด ๋ฐ์ดํ„ฐ๊ฐ€ ๋ชจ๋“  ํšŒ์‚ฌ์— ๋ณต์ œ๋˜๋ฏ€๋กœ ๋ฐ์ดํ„ฐ ์–‘์ด ์ฆ๊ฐ€ +3. **ํŠธ๋žœ์žญ์…˜**: ์ „์ฒด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ์‹คํ–‰๋จ (์‹คํŒจ ์‹œ ์ž๋™ ๋กค๋ฐฑ) +4. **์„ฑ๋Šฅ ์˜ํ–ฅ**: ํšŒ์‚ฌ ์ˆ˜๊ฐ€ ๋งŽ์œผ๋ฉด ์‹คํ–‰ ์‹œ๊ฐ„์ด ๊ธธ์–ด์งˆ ์ˆ˜ ์žˆ์Œ +5. **์ฝ”๋“œ ์ˆ˜์ •**: ๋ฐฑ์—”๋“œ ์ฝ”๋“œ๋„ ํ•จ๊ป˜ ์ˆ˜์ •ํ•ด์•ผ ํ•จ + +--- + +## ์˜ˆ์ƒ ๋ฐ์ดํ„ฐ ๋ณ€ํ™” + +### Before (๊ธฐ์กด) + +``` +id | table_name | column_name | input_type | company_code +---|------------|-------------|------------|------------- +1 | item_info | material | text | NULL +2 | projects | type | category | NULL +``` + +### After (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ›„) + +``` +id | table_name | column_name | input_type | company_code +---|------------|-------------|------------|------------- +1 | item_info | material | text | * +2 | item_info | material | text | COMPANY_7 +3 | projects | type | category | * +4 | projects | type | category | COMPANY_7 +``` + +--- + +## ๋‹ค์Œ ๋‹จ๊ณ„ + +๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ ํ›„: + +1. **๋ฐฑ์—”๋“œ ์ฝ”๋“œ ์ˆ˜์ •**: `company_code` ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ + - `tableService.ts` + - `dataService.ts` + - `tableController.ts` + +2. **ํ”„๋ก ํŠธ์—”๋“œ ์ฝ”๋“œ ์ˆ˜์ •**: API ํ˜ธ์ถœ ์‹œ `company_code` ์ž๋™ ํฌํ•จ + +3. **ํ…Œ์ŠคํŠธ**: ํšŒ์‚ฌ๋ณ„๋กœ ๋‹ค๋ฅธ ์ปฌ๋Ÿผ ํƒ€์ž… ์„ค์ • ํ™•์ธ + +--- + +## ๊ด€๋ จ ํŒŒ์ผ + +- ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ: `db/migrations/044_add_company_code_to_table_type_columns.sql` +- ๋ถ„์„ ๋ฌธ์„œ: `docs/ํ…Œ์ด๋ธ”_์ปฌ๋Ÿผ_ํƒ€์ž…_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๊ตฌ์กฐ์ _๋ฌธ์ œ_๋ถ„์„.md` +- ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค: `backend-node/src/services/tableService.ts` + +--- + +**์ž‘์„ฑ์ผ**: 2025-11-06 +**์‹ฌ๊ฐ๋„**: ๐Ÿ”ด ๋†’์Œ +**์˜ํ–ฅ ๋ฒ”์œ„**: ์ „์ฒด ๋™์  ํ…Œ์ด๋ธ” ์‹œ์Šคํ…œ + diff --git a/docs/์ฑ„๋ฒˆ๊ทœ์น™_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๋ฒ„๊ทธ_์ˆ˜์ •_์™„๋ฃŒ.md b/docs/์ฑ„๋ฒˆ๊ทœ์น™_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๋ฒ„๊ทธ_์ˆ˜์ •_์™„๋ฃŒ.md new file mode 100644 index 00000000..f7f5e69b --- /dev/null +++ b/docs/์ฑ„๋ฒˆ๊ทœ์น™_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๋ฒ„๊ทธ_์ˆ˜์ •_์™„๋ฃŒ.md @@ -0,0 +1,332 @@ +# ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ฒ„๊ทธ ์ˆ˜์ • ์™„๋ฃŒ + +> **์ž‘์„ฑ์ผ**: 2025-11-06 +> **์ƒํƒœ**: โœ… ์™„๋ฃŒ + +--- + +## ๐Ÿ› ๋ฌธ์ œ ๋ฐœ๊ฒฌ + +### ์ฆ์ƒ +- ๋‹ค๋ฅธ ํšŒ์‚ฌ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธํ–ˆ๋Š”๋ฐ `company_code = "*"` (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ) ์ฑ„๋ฒˆ ๊ทœ์น™์ด ๋ณด์ž„ +- ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์›์น™ ์œ„๋ฐ˜ + +### ์›์ธ +`backend-node/src/services/numberingRuleService.ts`์˜ SQL ์ฟผ๋ฆฌ์—์„œ **์ž˜๋ชป๋œ WHERE ์กฐ๊ฑด** ์‚ฌ์šฉ: + +```typescript +// โŒ ์ž˜๋ชป๋œ ์ฟผ๋ฆฌ (๋ฒ„๊ทธ) +WHERE company_code = $1 OR company_code = '*' +``` + +**๋ฌธ์ œ์ :** +- `OR company_code = '*'` ์กฐ๊ฑด์ด **ํ•ญ์ƒ ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จ**์‹œํ‚ด +- ์ผ๋ฐ˜ ํšŒ์‚ฌ ์‚ฌ์šฉ์ž๋„ `company_code = "*"` ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Œ +- ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ณด์•ˆ ์œ„๋ฐ˜ + +--- + +## โœ… ์ˆ˜์ • ๋‚ด์šฉ + +### ์ˆ˜์ •๋œ ๋กœ์ง + +```typescript +// โœ… ์˜ฌ๋ฐ”๋ฅธ ์ฟผ๋ฆฌ (์ˆ˜์ • ํ›„) +if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๊ฐ€๋Šฅ + query = `SELECT * FROM numbering_rules`; + params = []; +} else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ๋งŒ ์กฐํšŒ (company_code="*" ์ œ์™ธ) + query = `SELECT * FROM numbering_rules WHERE company_code = $1`; + params = [companyCode]; +} +``` + +### ์ˆ˜์ •๋œ ๋ฉ”์„œ๋“œ ๋ชฉ๋ก + +| ๋ฉ”์„œ๋“œ | ์ˆ˜์ • ๋‚ด์šฉ | ๋ผ์ธ | +|--------|-----------|------| +| `getRuleList()` | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„ํ„ฐ๋ง ์ถ”๊ฐ€ | 40-150 | +| `getAvailableRulesForMenu()` | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„ํ„ฐ๋ง ์ถ”๊ฐ€ | 155-402 | +| `getRuleById()` | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„ํ„ฐ๋ง ์ถ”๊ฐ€ | 407-506 | + +--- + +## ๐Ÿ“Š ์ˆ˜์ • ์ „ํ›„ ๋น„๊ต + +### ์ˆ˜์ • ์ „ (๋ฒ„๊ทธ) + +```sql +-- ์ผ๋ฐ˜ ํšŒ์‚ฌ (COMPANY_A) ๋กœ๊ทธ์ธ ์‹œ +SELECT * FROM numbering_rules +WHERE company_code = 'COMPANY_A' OR company_code = '*'; + +-- ๊ฒฐ๊ณผ: 3๊ฑด +-- 1. SAMPLE_RULE (company_code = '*') โ† ๋ณด๋ฉด ์•ˆ ๋จ! +-- 2. ์‚ฌ๋ฒˆ์ฝ”๋“œ (company_code = '*') โ† ๋ณด๋ฉด ์•ˆ ๋จ! +-- 3. COMPANY_A ์ „์šฉ ๊ทœ์น™ (์žˆ๋‹ค๋ฉด) +``` + +### ์ˆ˜์ • ํ›„ (์ •์ƒ) + +```sql +-- ์ผ๋ฐ˜ ํšŒ์‚ฌ (COMPANY_A) ๋กœ๊ทธ์ธ ์‹œ +SELECT * FROM numbering_rules +WHERE company_code = 'COMPANY_A'; + +-- ๊ฒฐ๊ณผ: 1๊ฑด (๋˜๋Š” 0๊ฑด) +-- 1. COMPANY_A ์ „์šฉ ๊ทœ์น™๋งŒ ์กฐํšŒ +-- company_code="*" ๋ฐ์ดํ„ฐ๋Š” ์ œ์™ธ๋จ! +``` + +```sql +-- ์ตœ๊ณ  ๊ด€๋ฆฌ์ž (company_code = '*') ๋กœ๊ทธ์ธ ์‹œ +SELECT * FROM numbering_rules; + +-- ๊ฒฐ๊ณผ: ๋ชจ๋“  ๊ทœ์น™ ์กฐํšŒ ๊ฐ€๋Šฅ +-- - SAMPLE_RULE (company_code = '*') +-- - ์‚ฌ๋ฒˆ์ฝ”๋“œ (company_code = '*') +-- - COMPANY_A ์ „์šฉ ๊ทœ์น™ +-- - COMPANY_B ์ „์šฉ ๊ทœ์น™ +-- ๋“ฑ ๋ชจ๋“  ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ +``` + +--- + +## ๐Ÿ” ์ƒ์„ธ ์ˆ˜์ • ๋‚ด์—ญ + +### 1. `getRuleList()` ๋ฉ”์„œ๋“œ + +**Before:** +```typescript +const query = ` + SELECT * FROM numbering_rules + WHERE company_code = $1 OR company_code = '*' +`; +const result = await pool.query(query, [companyCode]); +``` + +**After:** +```typescript +let query: string; +let params: any[]; + +if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์กฐํšŒ + query = `SELECT * FROM numbering_rules ORDER BY created_at DESC`; + params = []; + logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์ฒด ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ"); +} else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ๋งŒ ์กฐํšŒ + query = `SELECT * FROM numbering_rules WHERE company_code = $1 ORDER BY created_at DESC`; + params = [companyCode]; + logger.info("ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ", { companyCode }); +} + +const result = await pool.query(query, params); +``` + +### 2. `getAvailableRulesForMenu()` ๋ฉ”์„œ๋“œ + +**Before:** +```typescript +// menuObjid ์—†์„ ๋•Œ +const query = ` + SELECT * FROM numbering_rules + WHERE (company_code = $1 OR company_code = '*') + AND scope_type = 'global' +`; + +// menuObjid ์žˆ์„ ๋•Œ +const query = ` + SELECT * FROM numbering_rules + WHERE (company_code = $1 OR company_code = '*') + AND (scope_type = 'global' OR (scope_type = 'menu' AND menu_objid = $2)) +`; +``` + +**After:** +```typescript +// ์ตœ๊ณ  ๊ด€๋ฆฌ์ž์™€ ์ผ๋ฐ˜ ํšŒ์‚ฌ๋ฅผ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ +if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ฟผ๋ฆฌ + query = `SELECT * FROM numbering_rules WHERE scope_type = 'global'`; +} else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ ์ฟผ๋ฆฌ (company_code="*" ์ œ์™ธ) + query = `SELECT * FROM numbering_rules WHERE company_code = $1 AND scope_type = 'global'`; +} +``` + +### 3. `getRuleById()` ๋ฉ”์„œ๋“œ + +**Before:** +```typescript +const query = ` + SELECT * FROM numbering_rules + WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*') +`; +const result = await pool.query(query, [ruleId, companyCode]); +``` + +**After:** +```typescript +if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: rule_id๋งŒ ์ฒดํฌ + query = `SELECT * FROM numbering_rules WHERE rule_id = $1`; + params = [ruleId]; +} else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: rule_id + company_code ์ฒดํฌ + query = `SELECT * FROM numbering_rules WHERE rule_id = $1 AND company_code = $2`; + params = [ruleId, companyCode]; +} +``` + +--- + +## ๐Ÿงช ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค + +### ์‹œ๋‚˜๋ฆฌ์˜ค 1: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ๋กœ๊ทธ์ธ + +```bash +# ๋กœ๊ทธ์ธ +POST /api/auth/login +{ + "userId": "admin", + "password": "****" +} +# โ†’ JWT ํ† ํฐ์— companyCode = "*" ํฌํ•จ + +# ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ +GET /api/numbering-rules +Authorization: Bearer {token} + +# ์˜ˆ์ƒ ๊ฒฐ๊ณผ: ๋ชจ๋“  ํšŒ์‚ฌ์˜ ๊ทœ์น™ ์กฐํšŒ ๊ฐ€๋Šฅ +[ + { "ruleId": "SAMPLE_RULE", "companyCode": "*" }, + { "ruleId": "์‚ฌ๋ฒˆ์ฝ”๋“œ", "companyCode": "*" }, + { "ruleId": "COMPANY_A_RULE", "companyCode": "COMPANY_A" }, + { "ruleId": "COMPANY_B_RULE", "companyCode": "COMPANY_B" } +] +``` + +### ์‹œ๋‚˜๋ฆฌ์˜ค 2: ์ผ๋ฐ˜ ํšŒ์‚ฌ (COMPANY_A) ๋กœ๊ทธ์ธ + +```bash +# ๋กœ๊ทธ์ธ +POST /api/auth/login +{ + "userId": "user_a", + "password": "****" +} +# โ†’ JWT ํ† ํฐ์— companyCode = "COMPANY_A" ํฌํ•จ + +# ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ +GET /api/numbering-rules +Authorization: Bearer {token} + +# ์˜ˆ์ƒ ๊ฒฐ๊ณผ: ์ž์‹ ์˜ ํšŒ์‚ฌ ๊ทœ์น™๋งŒ ์กฐํšŒ (company_code="*" ์ œ์™ธ) +[ + { "ruleId": "COMPANY_A_RULE", "companyCode": "COMPANY_A" } +] +``` + +### ์‹œ๋‚˜๋ฆฌ์˜ค 3: ์ผ๋ฐ˜ ํšŒ์‚ฌ (COMPANY_B) ๋กœ๊ทธ์ธ + +```bash +# ๋กœ๊ทธ์ธ +POST /api/auth/login +{ + "userId": "user_b", + "password": "****" +} +# โ†’ JWT ํ† ํฐ์— companyCode = "COMPANY_B" ํฌํ•จ + +# ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ +GET /api/numbering-rules +Authorization: Bearer {token} + +# ์˜ˆ์ƒ ๊ฒฐ๊ณผ: COMPANY_B ๊ทœ์น™๋งŒ ์กฐํšŒ +[ + { "ruleId": "COMPANY_B_RULE", "companyCode": "COMPANY_B" } +] +``` + +--- + +## ๐ŸŽฏ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์›์น™ ์žฌํ™•์ธ + +### ํ•ต์‹ฌ ์›์น™ + +**company_code = "*"๋Š” ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ ๋ฐ์ดํ„ฐ์ด๋ฉฐ, ์ผ๋ฐ˜ ํšŒ์‚ฌ๋Š” ์ ˆ๋Œ€ ์ ‘๊ทผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.** + +| ํšŒ์‚ฌ ์ฝ”๋“œ | ์กฐํšŒ ๊ฐ€๋Šฅ ๋ฐ์ดํ„ฐ | ์„ค๋ช… | +|-----------|------------------|------| +| `*` (์ตœ๊ณ  ๊ด€๋ฆฌ์ž) | ๋ชจ๋“  ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ | `company_code = "*"`, `"COMPANY_A"`, `"COMPANY_B"` ๋“ฑ ๋ชจ๋‘ ์กฐํšŒ | +| `COMPANY_A` | `COMPANY_A` ๋ฐ์ดํ„ฐ๋งŒ | `company_code = "*"` ๋ฐ์ดํ„ฐ๋Š” **์ ˆ๋Œ€ ์กฐํšŒ ๋ถˆ๊ฐ€** | +| `COMPANY_B` | `COMPANY_B` ๋ฐ์ดํ„ฐ๋งŒ | `company_code = "*"` ๋ฐ์ดํ„ฐ๋Š” **์ ˆ๋Œ€ ์กฐํšŒ ๋ถˆ๊ฐ€** | + +### SQL ํŒจํ„ด + +```sql +-- โŒ ์ž˜๋ชป๋œ ํŒจํ„ด (๋ฒ„๊ทธ) +WHERE company_code = $1 OR company_code = '*' + +-- โœ… ์˜ฌ๋ฐ”๋ฅธ ํŒจํ„ด (์ตœ๊ณ  ๊ด€๋ฆฌ์ž) +WHERE 1=1 -- ๋ชจ๋“  ๋ฐ์ดํ„ฐ + +-- โœ… ์˜ฌ๋ฐ”๋ฅธ ํŒจํ„ด (์ผ๋ฐ˜ ํšŒ์‚ฌ) +WHERE company_code = $1 -- company_code="*" ์ž๋™ ์ œ์™ธ +``` + +--- + +## ๐Ÿ“ ์ถ”๊ฐ€ ํ™•์ธ ์‚ฌํ•ญ + +### ๋‹ค๋ฅธ ์„œ๋น„์Šค์—๋„ ๊ฐ™์€ ๋ฒ„๊ทธ๊ฐ€ ์žˆ์„ ๊ฐ€๋Šฅ์„ฑ + +๋‹ค์Œ ์„œ๋น„์Šค๋“ค๋„ ๋™์ผํ•œ ํŒจํ„ด์œผ๋กœ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ฒ„๊ทธ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ ํ•„์š”: + +- [ ] `backend-node/src/services/screenService.ts` +- [ ] `backend-node/src/services/tableService.ts` +- [ ] `backend-node/src/services/flowService.ts` +- [ ] `backend-node/src/services/adminService.ts` +- [ ] ๊ธฐํƒ€ `company_code` ํ•„ํ„ฐ๋ง์„ ์‚ฌ์šฉํ•˜๋Š” ๋ชจ๋“  ์„œ๋น„์Šค + +### ํ™•์ธ ๋ฐฉ๋ฒ• + +```bash +# ์ž˜๋ชป๋œ ํŒจํ„ด ๊ฒ€์ƒ‰ +cd backend-node/src/services +grep -n "OR company_code = '\*'" *.ts +``` + +--- + +## ๐Ÿš€ ๋ฐฐํฌ ์ „ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +- [x] ์ฝ”๋“œ ์ˆ˜์ • ์™„๋ฃŒ +- [x] ๋ฆฐํŠธ ์—๋Ÿฌ ์—†์Œ +- [x] ๋กœ๊น… ์ถ”๊ฐ€ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž vs ์ผ๋ฐ˜ ํšŒ์‚ฌ ๊ตฌ๋ถ„) +- [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (์„ ํƒ) +- [ ] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (ํ•„์ˆ˜) + - [ ] ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋กœ ๋กœ๊ทธ์ธํ•˜์—ฌ ๋ชจ๋“  ๊ทœ์น™ ์กฐํšŒ ํ™•์ธ + - [ ] ์ผ๋ฐ˜ ํšŒ์‚ฌ๋กœ ๋กœ๊ทธ์ธํ•˜์—ฌ ์ž์‹ ์˜ ๊ทœ์น™๋งŒ ์กฐํšŒ ํ™•์ธ + - [ ] ๋‹ค๋ฅธ ํšŒ์‚ฌ ๊ทœ์น™์— ์ ‘๊ทผ ๋ถˆ๊ฐ€๋Šฅ ํ™•์ธ +- [ ] ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์žฌํ™•์ธ +- [ ] ๋ฐฑ์—”๋“œ ์žฌ์‹คํ–‰ (์ฝ”๋“œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ๋ฐ˜์˜) + +--- + +## ๐Ÿ“š ๊ด€๋ จ ๋ฌธ์„œ + +- [๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„์ˆ˜ ๊ทœ์น™](../README.md#๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ-ํ•„์ˆ˜-๊ทœ์น™) +- [์ฑ„๋ฒˆ ๊ทœ์น™ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ ์™„๋ฃŒ](./์ฑ„๋ฒˆ๊ทœ์น™_์ปดํฌ๋„ŒํŠธ_๊ตฌํ˜„_์™„๋ฃŒ.md) +- [๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ](../db/migrations/034_create_numbering_rules.sql) + +--- + +**์ˆ˜์ • ์™„๋ฃŒ์ผ**: 2025-11-06 +**์ˆ˜์ •์ž**: AI Assistant +**์˜ํ–ฅ ๋ฒ”์œ„**: `numberingRuleService.ts` ์ „์ฒด + diff --git a/docs/์นดํ…Œ๊ณ ๋ฆฌ_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๋ฒ„๊ทธ_๋ถ„์„.md b/docs/์นดํ…Œ๊ณ ๋ฆฌ_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๋ฒ„๊ทธ_๋ถ„์„.md new file mode 100644 index 00000000..8b2758a2 --- /dev/null +++ b/docs/์นดํ…Œ๊ณ ๋ฆฌ_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๋ฒ„๊ทธ_๋ถ„์„.md @@ -0,0 +1,261 @@ +# ์นดํ…Œ๊ณ ๋ฆฌ ์‹œ์Šคํ…œ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ฒ„๊ทธ ๋ถ„์„ + +> **์ž‘์„ฑ์ผ**: 2025-11-06 +> **์ƒํƒœ**: ๐Ÿ”ด ๋ฒ„๊ทธ ๋ฐœ๊ฒฌ, ์ˆ˜์ • ๋Œ€๊ธฐ + +--- + +## ๐Ÿ› ๋ฐœ๊ฒฌ๋œ ๋ฒ„๊ทธ + +### ์˜ํ–ฅ ๋ฐ›๋Š” ์„œ๋น„์Šค + +1. โœ… **CommonCodeService** (`commonCodeService.ts`) - ์ •์ƒ (์ด๋ฏธ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋จ) +2. ๐Ÿ”ด **TableCategoryValueService** (`tableCategoryValueService.ts`) - **๋ฒ„๊ทธ ์กด์žฌ (7๊ณณ)** + +--- + +## ๐Ÿ“Š ํ˜„์žฌ ์ƒํƒœ ํ™•์ธ + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ˜„ํ™ฉ + +```sql +SELECT value_id, table_name, column_name, value_label, company_code +FROM table_column_category_values +ORDER BY created_at DESC +LIMIT 10; +``` + +**๊ฒฐ๊ณผ**: ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์ด `company_code = "*"` (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ) + +| value_id | table_name | column_name | value_label | company_code | +|----------|------------|-------------|-------------|--------------| +| 16 | item_info | material | ์›์ž์žฌ | * | +| 15 | item_info | material | 153 | * | +| 1-8 | projects | project_type/status | ... | * | + +**๋ฌธ์ œ**: ์ผ๋ฐ˜ ํšŒ์‚ฌ ์‚ฌ์šฉ์ž๋„ ์ด ๋ฐ์ดํ„ฐ๋“ค์„ ๋ณผ ์ˆ˜ ์žˆ์Œ! + +--- + +## ๐Ÿ” ๋ฒ„๊ทธ ์ƒ์„ธ ๋ถ„์„ + +### 1. tableCategoryValueService.ts + +#### ๋ฒ„๊ทธ ์œ„์น˜ (7๊ณณ) + +| ๋ฉ”์„œ๋“œ | ๋ผ์ธ | ๋ฒ„๊ทธ ํŒจํ„ด | ์‹ฌ๊ฐ๋„ | +|--------|------|-----------|--------| +| `getCategoryColumns()` | 31 | `AND (cv.company_code = $2 OR cv.company_code = '*')` | ๐Ÿ”ด ๋†’์Œ (READ) | +| `getCategoryValues()` | 93 | `AND (company_code = $3 OR company_code = '*')` | ๐Ÿ”ด ๋†’์Œ (READ) | +| `addCategoryValue()` | 139 | `AND (company_code = $4 OR company_code = '*')` | ๐ŸŸก ์ค‘๊ฐ„ (์ค‘๋ณต ์ฒดํฌ) | +| `updateCategoryValue()` | 269 | `AND (company_code = $${paramIndex++} OR company_code = '*')` | ๐ŸŸข ๋‚ฎ์Œ (UPDATE) | +| `deleteCategoryValue()` - ํ•˜์œ„ ์ฒดํฌ | 317 | `AND (company_code = $2 OR company_code = '*')` | ๐ŸŸก ์ค‘๊ฐ„ (READ) | +| `deleteCategoryValue()` - ์‚ญ์ œ | 332 | `AND (company_code = $2 OR company_code = '*')` | ๐ŸŸข ๋‚ฎ์Œ (UPDATE) | +| `bulkDeleteCategoryValues()` | 362 | `AND (company_code = $2 OR company_code = '*')` | ๐ŸŸข ๋‚ฎ์Œ (UPDATE) | +| `reorderCategoryValues()` | 395 | `AND (company_code = $3 OR company_code = '*')` | ๐ŸŸข ๋‚ฎ์Œ (UPDATE) | + +#### ๋ฒ„๊ทธ ์ฝ”๋“œ ์˜ˆ์‹œ + +**โŒ ์ž˜๋ชป๋œ ์ฝ”๋“œ (93๋ฒˆ ๋ผ์ธ)** +```typescript +async getCategoryValues( + tableName: string, + columnName: string, + companyCode: string, + includeInactive: boolean = false +): Promise { + const query = ` + SELECT * + FROM table_column_category_values + WHERE table_name = $1 + AND column_name = $2 + AND (company_code = $3 OR company_code = '*') -- ๐Ÿ”ด ๋ฒ„๊ทธ! + `; + + const result = await pool.query(query, [tableName, columnName, companyCode]); + return result.rows; +} +``` + +**๋ฌธ์ œ์ **: +- ์ผ๋ฐ˜ ํšŒ์‚ฌ (์˜ˆ: `COMPANY_A`)๋กœ ๋กœ๊ทธ์ธํ•ด๋„ `company_code = "*"` ๋ฐ์ดํ„ฐ๊ฐ€ ์กฐํšŒ๋จ +- ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์›์น™ ์œ„๋ฐ˜ + +--- + +## โœ… ์ˆ˜์ • ๋ฐฉ์•ˆ + +### ํŒจํ„ด 1: Read ์ž‘์—… (getCategoryColumns, getCategoryValues) + +**Before:** +```typescript +AND (company_code = $3 OR company_code = '*') +``` + +**After:** +```typescript +let query: string; +let params: any[]; + +if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์กฐํšŒ + query = ` + SELECT * FROM table_column_category_values + WHERE table_name = $1 AND column_name = $2 + `; + params = [tableName, columnName]; +} else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ๋งŒ ์กฐํšŒ + query = ` + SELECT * FROM table_column_category_values + WHERE table_name = $1 + AND column_name = $2 + AND company_code = $3 + `; + params = [tableName, columnName, companyCode]; +} +``` + +### ํŒจํ„ด 2: Update/Delete ์ž‘์—… + +UPDATE/DELETE ์ž‘์—…์€ ์ด๋ฏธ ํšŒ์‚ฌ ์ฝ”๋“œ๊ฐ€ ๋งค์นญ๋˜๋Š” ๊ฒฝ์šฐ์—๋งŒ ์ž‘๋™ํ•˜๋ฏ€๋กœ, ๋ณด์•ˆ์ƒ ํฐ ๋ฌธ์ œ๋Š” ์—†์ง€๋งŒ ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด ์ˆ˜์ •: + +**Before:** +```typescript +WHERE value_id = $1 AND (company_code = $2 OR company_code = '*') +``` + +**After:** +```typescript +WHERE value_id = $1 AND company_code = $2 +``` + +**๋‹จ, ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ˆ˜์ • ๊ฐ€๋Šฅํ•ด์•ผ ํ•˜๋ฏ€๋กœ:** +```typescript +if (companyCode === "*") { + query = `UPDATE ... WHERE value_id = $1`; +} else { + query = `UPDATE ... WHERE value_id = $1 AND company_code = $2`; +} +``` + +--- + +## ๐Ÿ“‹ ์ˆ˜์ • ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### tableCategoryValueService.ts + +- [ ] `getCategoryColumns()` (31๋ฒˆ ๋ผ์ธ) + - JOIN ์กฐ๊ฑด์—์„œ `OR company_code = '*'` ์ œ๊ฑฐ + - ์ตœ๊ณ  ๊ด€๋ฆฌ์ž/์ผ๋ฐ˜ ํšŒ์‚ฌ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ + +- [ ] `getCategoryValues()` (93๋ฒˆ ๋ผ์ธ) + - WHERE ์กฐ๊ฑด์—์„œ `OR company_code = '*'` ์ œ๊ฑฐ + - ์ตœ๊ณ  ๊ด€๋ฆฌ์ž/์ผ๋ฐ˜ ํšŒ์‚ฌ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ + +- [ ] `addCategoryValue()` (139๋ฒˆ ๋ผ์ธ) + - ์ค‘๋ณต ์ฒดํฌ ์‹œ `OR company_code = '*'` ์ œ๊ฑฐ + - ์ตœ๊ณ  ๊ด€๋ฆฌ์ž/์ผ๋ฐ˜ ํšŒ์‚ฌ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ + +- [ ] `updateCategoryValue()` (269๋ฒˆ ๋ผ์ธ) + - UPDATE ์กฐ๊ฑด์—์„œ `OR company_code = '*'` ์ œ๊ฑฐ + - ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋Š” company_code ์กฐ๊ฑด ์ œ๊ฑฐ + +- [ ] `deleteCategoryValue()` (317, 332๋ฒˆ ๋ผ์ธ) + - ํ•˜์œ„ ์ฒดํฌ ๋ฐ ์‚ญ์ œ ์กฐ๊ฑด ์ˆ˜์ • + - ์ตœ๊ณ  ๊ด€๋ฆฌ์ž/์ผ๋ฐ˜ ํšŒ์‚ฌ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ + +- [ ] `bulkDeleteCategoryValues()` (362๋ฒˆ ๋ผ์ธ) + - ์ผ๊ด„ ์‚ญ์ œ ์กฐ๊ฑด ์ˆ˜์ • + +- [ ] `reorderCategoryValues()` (395๋ฒˆ ๋ผ์ธ) + - ์ˆœ์„œ ๋ณ€๊ฒฝ ์กฐ๊ฑด ์ˆ˜์ • + +--- + +## ๐Ÿงช ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค + +### ์‹œ๋‚˜๋ฆฌ์˜ค 1: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋กœ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ + +```bash +# ๋กœ๊ทธ์ธ +POST /api/auth/login +{ "userId": "admin", "companyCode": "*" } + +# ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ +GET /api/table-category-values/projects/project_type + +# ์˜ˆ์ƒ ๊ฒฐ๊ณผ: ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ ๊ฐ€๋Šฅ +[ + { "valueId": 1, "valueLabel": "๊ฐœ๋ฐœ", "companyCode": "*" }, + { "valueId": 2, "valueLabel": "์œ ์ง€๋ณด์ˆ˜", "companyCode": "*" }, + { "valueId": 100, "valueLabel": "COMPANY_A ์ „์šฉ", "companyCode": "COMPANY_A" } +] +``` + +### ์‹œ๋‚˜๋ฆฌ์˜ค 2: ์ผ๋ฐ˜ ํšŒ์‚ฌ๋กœ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ + +```bash +# ๋กœ๊ทธ์ธ +POST /api/auth/login +{ "userId": "user_a", "companyCode": "COMPANY_A" } + +# ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ +GET /api/table-category-values/projects/project_type + +# ์ˆ˜์ • ์ „ (๋ฒ„๊ทธ): company_code="*" ํฌํ•จ +[ + { "valueId": 1, "valueLabel": "๊ฐœ๋ฐœ", "companyCode": "*" }, โ† ๋ณด๋ฉด ์•ˆ ๋จ! + { "valueId": 100, "valueLabel": "COMPANY_A ์ „์šฉ", "companyCode": "COMPANY_A" } +] + +# ์ˆ˜์ • ํ›„ (์ •์ƒ): ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ๋งŒ +[ + { "valueId": 100, "valueLabel": "COMPANY_A ์ „์šฉ", "companyCode": "COMPANY_A" } +] +``` + +--- + +## ๐Ÿ”— ๊ด€๋ จ ํŒŒ์ผ + +- **๋ฒ„๊ทธ ์กด์žฌ**: `backend-node/src/services/tableCategoryValueService.ts` +- **์ •์ƒ ์ฐธ๊ณ **: `backend-node/src/services/commonCodeService.ts` (78-86๋ฒˆ ๋ผ์ธ) +- **์ •์ƒ ์ฐธ๊ณ **: `backend-node/src/services/numberingRuleService.ts` (์ˆ˜์ • ์™„๋ฃŒ) + +--- + +## ๐Ÿ“ ์ˆ˜์ • ์šฐ์„ ์ˆœ์œ„ + +1. **๐Ÿ”ด ๋†’์Œ (์ฆ‰์‹œ ์ˆ˜์ • ํ•„์š”)**: + - `getCategoryColumns()` (31๋ฒˆ) + - `getCategoryValues()` (93๋ฒˆ) + โ†’ ์ผ๋ฐ˜ ํšŒ์‚ฌ๊ฐ€ ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Œ + +2. **๐ŸŸก ์ค‘๊ฐ„ (๊ฐ€๋Šฅํ•œ ๋นจ๋ฆฌ)**: + - `addCategoryValue()` (139๋ฒˆ) - ์ค‘๋ณต ์ฒดํฌ + - `deleteCategoryValue()` (317๋ฒˆ) - ํ•˜์œ„ ์ฒดํฌ + +3. **๐ŸŸข ๋‚ฎ์Œ (์ผ๊ด€์„ฑ ์œ ์ง€)**: + - `updateCategoryValue()` (269๋ฒˆ) + - `deleteCategoryValue()` (332๋ฒˆ) + - `bulkDeleteCategoryValues()` (362๋ฒˆ) + - `reorderCategoryValues()` (395๋ฒˆ) + +--- + +## ๐Ÿšจ ๋‹ค๋ฅธ ์„œ๋น„์Šค ํ™•์ธ ํ•„์š” + +๋‹ค์Œ ์„œ๋น„์Šค๋“ค๋„ ๊ฐ™์€ ํŒจํ„ด์˜ ๋ฒ„๊ทธ๊ฐ€ ์žˆ์„ ๊ฐ€๋Šฅ์„ฑ: + +```bash +cd backend-node/src/services +grep -n "OR company_code = '\*'" *.ts +``` + +**๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ**: `tableCategoryValueService.ts` ์—๋งŒ ์กด์žฌ + +--- + +**๋‹ค์Œ ๋‹จ๊ณ„**: ์‚ฌ์šฉ์ž ์Šน์ธ ํ›„ `tableCategoryValueService.ts` ์ˆ˜์ • ์ง„ํ–‰ + diff --git a/docs/์นดํ…Œ๊ณ ๋ฆฌ_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๋ฒ„๊ทธ_์ˆ˜์ •_์™„๋ฃŒ.md b/docs/์นดํ…Œ๊ณ ๋ฆฌ_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๋ฒ„๊ทธ_์ˆ˜์ •_์™„๋ฃŒ.md new file mode 100644 index 00000000..fc94d6f9 --- /dev/null +++ b/docs/์นดํ…Œ๊ณ ๋ฆฌ_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๋ฒ„๊ทธ_์ˆ˜์ •_์™„๋ฃŒ.md @@ -0,0 +1,362 @@ +# ์นดํ…Œ๊ณ ๋ฆฌ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ฒ„๊ทธ ์ˆ˜์ • ์™„๋ฃŒ + +> **์ž‘์„ฑ์ผ**: 2025-11-06 +> **์ƒํƒœ**: โœ… ์™„๋ฃŒ + +--- + +## ๐Ÿ› ๋ฌธ์ œ ๋ฐœ๊ฒฌ + +### ์ฆ์ƒ +- ๋‹ค๋ฅธ ํšŒ์‚ฌ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธํ–ˆ๋Š”๋ฐ `company_code = "*"` (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ) ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์ด ๋ณด์ž„ +- ์ฑ„๋ฒˆ ๊ทœ์น™๊ณผ ๋™์ผํ•œ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ฒ„๊ทธ + +### ์›์ธ +`backend-node/src/services/tableCategoryValueService.ts`์˜ **7๊ฐœ ๋ฉ”์„œ๋“œ**์—์„œ ์ž˜๋ชป๋œ WHERE ์กฐ๊ฑด ์‚ฌ์šฉ: + +```typescript +// โŒ ์ž˜๋ชป๋œ ์ฟผ๋ฆฌ (๋ฒ„๊ทธ) +AND (company_code = $3 OR company_code = '*') +``` + +--- + +## โœ… ์ˆ˜์ • ๋‚ด์šฉ + +### ์ˆ˜์ •๋œ ๋ฉ”์„œ๋“œ (7๊ฐœ) + +| ๋ฉ”์„œ๋“œ | ๋ผ์ธ | ์ž‘์—… ์œ ํ˜• | ์ˆ˜์ • ๋‚ด์šฉ | +|--------|------|-----------|-----------| +| `getCategoryColumns()` | 12-77 | READ (JOIN) | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ถ„๊ธฐ ์ถ”๊ฐ€ | +| `getCategoryValues()` | 82-183 | READ | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ถ„๊ธฐ ์ถ”๊ฐ€ | +| `addCategoryValue()` | 188-269 | CREATE (์ค‘๋ณต ์ฒดํฌ) | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ถ„๊ธฐ ์ถ”๊ฐ€ | +| `updateCategoryValue()` | 274-403 | UPDATE | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ถ„๊ธฐ ์ถ”๊ฐ€ | +| `deleteCategoryValue()` | 409-485 | DELETE | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ถ„๊ธฐ ์ถ”๊ฐ€ | +| `bulkDeleteCategoryValues()` | 490-531 | DELETE (์ผ๊ด„) | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ถ„๊ธฐ ์ถ”๊ฐ€ | +| `reorderCategoryValues()` | 536-586 | UPDATE (์ˆœ์„œ) | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ถ„๊ธฐ ์ถ”๊ฐ€ | + +--- + +## ๐Ÿ“Š ์ˆ˜์ • ์ „ํ›„ ๋น„๊ต + +### 1. getCategoryValues() - ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ชฉ๋ก ์กฐํšŒ + +**Before:** +```typescript +const query = ` + SELECT * FROM table_column_category_values + WHERE table_name = $1 + AND column_name = $2 + AND (company_code = $3 OR company_code = '*') -- ๐Ÿ”ด ๋ฒ„๊ทธ! +`; +const params = [tableName, columnName, companyCode]; +``` + +**After:** +```typescript +let query: string; +let params: any[]; + +if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ + query = ` + SELECT * FROM table_column_category_values + WHERE table_name = $1 AND column_name = $2 + `; + params = [tableName, columnName]; +} else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’๋งŒ ์กฐํšŒ + query = ` + SELECT * FROM table_column_category_values + WHERE table_name = $1 + AND column_name = $2 + AND company_code = $3 + `; + params = [tableName, columnName, companyCode]; +} +``` + +### 2. getCategoryColumns() - ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ ๋ชฉ๋ก ์กฐํšŒ (JOIN) + +**Before:** +```typescript +const query = ` + SELECT ... + FROM table_type_columns tc + LEFT JOIN table_column_category_values cv + ON tc.table_name = cv.table_name + AND tc.column_name = cv.column_name + AND cv.is_active = true + AND (cv.company_code = $2 OR cv.company_code = '*') -- ๐Ÿ”ด ๋ฒ„๊ทธ! + WHERE tc.table_name = $1 +`; +``` + +**After:** +```typescript +if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: JOIN ์กฐ๊ฑด์—์„œ company_code ์ œ์™ธ + query = ` + SELECT ... + FROM table_type_columns tc + LEFT JOIN table_column_category_values cv + ON tc.table_name = cv.table_name + AND tc.column_name = cv.column_name + AND cv.is_active = true + WHERE tc.table_name = $1 + `; +} else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: JOIN ์กฐ๊ฑด์— company_code ์ถ”๊ฐ€ + query = ` + SELECT ... + FROM table_type_columns tc + LEFT JOIN table_column_category_values cv + ON tc.table_name = cv.table_name + AND tc.column_name = cv.column_name + AND cv.is_active = true + AND cv.company_code = $2 + WHERE tc.table_name = $1 + `; +} +``` + +### 3. updateCategoryValue() - ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ˆ˜์ • + +**Before:** +```typescript +const updateQuery = ` + UPDATE table_column_category_values + SET ... + WHERE value_id = $${paramIndex++} + AND (company_code = $${paramIndex++} OR company_code = '*') -- ๐Ÿ”ด ๋ฒ„๊ทธ! +`; +``` + +**After:** +```typescript +if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: company_code ์กฐ๊ฑด ์ œ์™ธ + updateQuery = ` + UPDATE table_column_category_values + SET ... + WHERE value_id = $${paramIndex++} + `; +} else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: company_code ์กฐ๊ฑด ํฌํ•จ + updateQuery = ` + UPDATE table_column_category_values + SET ... + WHERE value_id = $${paramIndex++} + AND company_code = $${paramIndex++} + `; +} +``` + +--- + +## ๐Ÿ” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ˜„ํ™ฉ + +### ํ˜„์žฌ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ (์ˆ˜์ • ์ „) + +```sql +SELECT value_id, table_name, column_name, value_label, company_code +FROM table_column_category_values +ORDER BY created_at DESC +LIMIT 10; +``` + +| value_id | table_name | column_name | value_label | company_code | +|----------|------------|-------------|-------------|--------------| +| 1-8 | projects | project_type/status | ๊ฐœ๋ฐœ/์œ ์ง€๋ณด์ˆ˜/... | * | +| 15-16 | item_info | material | ์›์ž์žฌ/153 | * | + +**๋ฌธ์ œ**: ์ผ๋ฐ˜ ํšŒ์‚ฌ ์‚ฌ์šฉ์ž๋„ ์ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Œ! + +### ์ˆ˜์ • ํ›„ ๋™์ž‘ + +| ์‚ฌ์šฉ์ž | ์ˆ˜์ • ์ „ | ์ˆ˜์ • ํ›„ | +|--------|---------|---------| +| **์ตœ๊ณ  ๊ด€๋ฆฌ์ž (*)** | ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์กฐํšŒ โœ… | ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์กฐํšŒ โœ… | +| **์ผ๋ฐ˜ ํšŒ์‚ฌ A** | A๋ฐ์ดํ„ฐ + `*` ๋ฐ์ดํ„ฐ โŒ | A๋ฐ์ดํ„ฐ๋งŒ โœ… | +| **์ผ๋ฐ˜ ํšŒ์‚ฌ B** | B๋ฐ์ดํ„ฐ + `*` ๋ฐ์ดํ„ฐ โŒ | B๋ฐ์ดํ„ฐ๋งŒ โœ… | + +--- + +## ๐Ÿงช ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค + +### ์‹œ๋‚˜๋ฆฌ์˜ค 1: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋กœ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ + +```bash +# ๋กœ๊ทธ์ธ +POST /api/auth/login +{ "userId": "admin", "companyCode": "*" } + +# ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ +GET /api/table-category-values/projects/project_type + +# ์˜ˆ์ƒ ๊ฒฐ๊ณผ: ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ ๊ฐ€๋Šฅ +[ + { "valueId": 1, "valueLabel": "๊ฐœ๋ฐœ", "companyCode": "*" }, + { "valueId": 2, "valueLabel": "์œ ์ง€๋ณด์ˆ˜", "companyCode": "*" }, + { "valueId": 100, "valueLabel": "COMPANY_A ์ „์šฉ", "companyCode": "COMPANY_A" } +] +``` + +### ์‹œ๋‚˜๋ฆฌ์˜ค 2: ์ผ๋ฐ˜ ํšŒ์‚ฌ๋กœ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ + +```bash +# ๋กœ๊ทธ์ธ +POST /api/auth/login +{ "userId": "user_a", "companyCode": "COMPANY_A" } + +# ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ +GET /api/table-category-values/projects/project_type + +# ์ˆ˜์ • ์ „ (๋ฒ„๊ทธ): company_code="*" ํฌํ•จ +[ + { "valueId": 1, "valueLabel": "๊ฐœ๋ฐœ", "companyCode": "*" }, โ† ๋ณด๋ฉด ์•ˆ ๋จ! + { "valueId": 100, "valueLabel": "COMPANY_A ์ „์šฉ", "companyCode": "COMPANY_A" } +] + +# ์ˆ˜์ • ํ›„ (์ •์ƒ): ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ๋งŒ +[ + { "valueId": 100, "valueLabel": "COMPANY_A ์ „์šฉ", "companyCode": "COMPANY_A" } +] +``` + +### ์‹œ๋‚˜๋ฆฌ์˜ค 3: ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ˆ˜์ • (๊ถŒํ•œ ์ฒดํฌ) + +```bash +# ์ผ๋ฐ˜ ํšŒ์‚ฌ A๋กœ ๋กœ๊ทธ์ธ +# company_code="*" ๋ฐ์ดํ„ฐ ์ˆ˜์ • ์‹œ๋„ +PUT /api/table-category-values/1 +{ "valueLabel": "ํ•ดํ‚น ์‹œ๋„" } + +# ์ˆ˜์ • ์ „: ์„ฑ๊ณต (๋ณด์•ˆ ์ทจ์•ฝ) +# ์ˆ˜์ • ํ›„: ์‹คํŒจ (๊ถŒํ•œ ์—†์Œ) +{ "success": false, "message": "์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" } +``` + +--- + +## ๐Ÿ“ ์ˆ˜์ • ์ƒ์„ธ ๋‚ด์—ญ + +### ๊ณตํ†ต ํŒจํ„ด + +๋ชจ๋“  ๋ฉ”์„œ๋“œ์— ๋‹ค์Œ ํŒจํ„ด ์ ์šฉ: + +```typescript +// ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ company_code="*" ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Œ +let query: string; +let params: any[]; + +if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: company_code ํ•„ํ„ฐ๋ง ์ œ์™ธ + query = `SELECT * FROM table WHERE ...`; + params = [...]; + logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์นดํ…Œ๊ณ ๋ฆฌ ์ž‘์—…"); +} else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: company_code ํ•„ํ„ฐ๋ง ํฌํ•จ + query = `SELECT * FROM table WHERE ... AND company_code = $N`; + params = [..., companyCode]; + logger.info("ํšŒ์‚ฌ๋ณ„ ์นดํ…Œ๊ณ ๋ฆฌ ์ž‘์—…", { companyCode }); +} +``` + +### ๋กœ๊น… ์ถ”๊ฐ€ + +๊ฐ ๋ฉ”์„œ๋“œ์— ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋กœ๊น… ์ถ”๊ฐ€: + +```typescript +// ์ตœ๊ณ  ๊ด€๋ฆฌ์ž +logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ ์กฐํšŒ"); +logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ"); + +// ์ผ๋ฐ˜ ํšŒ์‚ฌ +logger.info("ํšŒ์‚ฌ๋ณ„ ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ ์กฐํšŒ", { companyCode }); +logger.info("ํšŒ์‚ฌ๋ณ„ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ", { companyCode }); +``` + +--- + +## ๐ŸŽฏ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์›์น™ ์žฌํ™•์ธ + +### ํ•ต์‹ฌ ์›์น™ + +**company_code = "*"๋Š” ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ ๋ฐ์ดํ„ฐ์ด๋ฉฐ, ์ผ๋ฐ˜ ํšŒ์‚ฌ๋Š” ์ ˆ๋Œ€ ์ ‘๊ทผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.** + +| ์ž‘์—… | ์ตœ๊ณ  ๊ด€๋ฆฌ์ž (*) | ์ผ๋ฐ˜ ํšŒ์‚ฌ (COMPANY_A) | +|------|-----------------|----------------------| +| **์กฐํšŒ** | ๋ชจ๋“  ๋ฐ์ดํ„ฐ | ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ๋งŒ | +| **์ƒ์„ฑ** | ๋ชจ๋“  ํšŒ์‚ฌ์— | ์ž์‹ ์˜ ํšŒ์‚ฌ์—๋งŒ | +| **์ˆ˜์ •** | ๋ชจ๋“  ๋ฐ์ดํ„ฐ | ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ๋งŒ | +| **์‚ญ์ œ** | ๋ชจ๋“  ๋ฐ์ดํ„ฐ | ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ๋งŒ | + +### SQL ํŒจํ„ด + +```sql +-- โŒ ์ž˜๋ชป๋œ ํŒจํ„ด (๋ฒ„๊ทธ) +WHERE company_code = $1 OR company_code = '*' + +-- โœ… ์˜ฌ๋ฐ”๋ฅธ ํŒจํ„ด (์ตœ๊ณ  ๊ด€๋ฆฌ์ž) +WHERE 1=1 -- company_code ํ•„ํ„ฐ๋ง ์—†์Œ + +-- โœ… ์˜ฌ๋ฐ”๋ฅธ ํŒจํ„ด (์ผ๋ฐ˜ ํšŒ์‚ฌ) +WHERE company_code = $1 -- company_code="*" ์ž๋™ ์ œ์™ธ +``` + +--- + +## ๐Ÿ”— ๊ด€๋ จ ํŒŒ์ผ + +- **์ˆ˜์ • ์™„๋ฃŒ**: `backend-node/src/services/tableCategoryValueService.ts` +- **์ •์ƒ ์ฐธ๊ณ **: `backend-node/src/services/commonCodeService.ts` (์ด๋ฏธ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋จ) +- **์ •์ƒ ์ฐธ๊ณ **: `backend-node/src/services/numberingRuleService.ts` (์ˆ˜์ • ์™„๋ฃŒ) + +--- + +## ๐Ÿš€ ๋ฐฐํฌ ์ „ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +- [x] ์ฝ”๋“œ ์ˆ˜์ • ์™„๋ฃŒ (7๊ฐœ ๋ฉ”์„œ๋“œ) +- [x] ๋ฆฐํŠธ ์—๋Ÿฌ ์—†์Œ +- [x] ๋กœ๊น… ์ถ”๊ฐ€ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž vs ์ผ๋ฐ˜ ํšŒ์‚ฌ ๊ตฌ๋ถ„) +- [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (์„ ํƒ) +- [ ] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (ํ•„์ˆ˜) + - [ ] ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋กœ ๋กœ๊ทธ์ธํ•˜์—ฌ ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ ํ™•์ธ + - [ ] ์ผ๋ฐ˜ ํšŒ์‚ฌ๋กœ ๋กœ๊ทธ์ธํ•˜์—ฌ ์ž์‹ ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’๋งŒ ์กฐํšŒ ํ™•์ธ + - [ ] ๋‹ค๋ฅธ ํšŒ์‚ฌ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ ‘๊ทผ ๋ถˆ๊ฐ€๋Šฅ ํ™•์ธ + - [ ] ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ ๊ถŒํ•œ ํ™•์ธ +- [ ] ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ชฉ๋ก ์žฌํ™•์ธ +- [ ] ๋ฐฑ์—”๋“œ ์žฌ์‹คํ–‰ (์ฝ”๋“œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ๋ฐ˜์˜) + +--- + +## ๐Ÿ“š ๊ด€๋ จ ๋ฌธ์„œ + +- [๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„์ˆ˜ ๊ทœ์น™](../README.md#๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ-ํ•„์ˆ˜-๊ทœ์น™) +- [์ฑ„๋ฒˆ ๊ทœ์น™ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ฒ„๊ทธ ์ˆ˜์ •](./์ฑ„๋ฒˆ๊ทœ์น™_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๋ฒ„๊ทธ_์ˆ˜์ •_์™„๋ฃŒ.md) +- [์นดํ…Œ๊ณ ๋ฆฌ ์‹œ์Šคํ…œ ๊ตฌํ˜„ ์™„๋ฃŒ](./์นดํ…Œ๊ณ ๋ฆฌ_์‹œ์Šคํ…œ_์ตœ์ข…_์™„๋ฃŒ_๋ณด๊ณ ์„œ.md) + +--- + +## ๐Ÿ” ๋‹ค๋ฅธ ์„œ๋น„์Šค ํ™•์ธ ๊ฒฐ๊ณผ + +```bash +cd backend-node/src/services +grep -n "OR company_code = '\*'" *.ts +``` + +**๊ฒฐ๊ณผ**: `tableCategoryValueService.ts`์—๋งŒ ๋ฒ„๊ทธ ์กด์žฌ (์ˆ˜์ • ์™„๋ฃŒ) + +**ํ™•์ธ๋œ ์ •์ƒ ์„œ๋น„์Šค**: +- โœ… `commonCodeService.ts` - ์ด๋ฏธ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋จ +- โœ… `numberingRuleService.ts` - ์ˆ˜์ • ์™„๋ฃŒ +- โœ… `tableCategoryValueService.ts` - ์ˆ˜์ • ์™„๋ฃŒ + +--- + +**์ˆ˜์ • ์™„๋ฃŒ์ผ**: 2025-11-06 +**์ˆ˜์ •์ž**: AI Assistant +**์˜ํ–ฅ ๋ฒ”์œ„**: `tableCategoryValueService.ts` ์ „์ฒด (7๊ฐœ ๋ฉ”์„œ๋“œ) +**๋ฆฐํŠธ ์—๋Ÿฌ**: ์—†์Œ + diff --git a/docs/ํ…Œ์ด๋ธ”_์ปฌ๋Ÿผ_ํƒ€์ž…_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๊ตฌ์กฐ์ _๋ฌธ์ œ_๋ถ„์„.md b/docs/ํ…Œ์ด๋ธ”_์ปฌ๋Ÿผ_ํƒ€์ž…_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๊ตฌ์กฐ์ _๋ฌธ์ œ_๋ถ„์„.md new file mode 100644 index 00000000..76b4f67f --- /dev/null +++ b/docs/ํ…Œ์ด๋ธ”_์ปฌ๋Ÿผ_ํƒ€์ž…_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๊ตฌ์กฐ์ _๋ฌธ์ œ_๋ถ„์„.md @@ -0,0 +1,456 @@ +# ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ํƒ€์ž… ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๊ตฌ์กฐ์  ๋ฌธ์ œ ๋ถ„์„ + +> **์ž‘์„ฑ์ผ**: 2025-11-06 +> **์‹ฌ๊ฐ๋„**: ๐Ÿ”ด **์น˜๋ช…์  (Critical)** +> **์ƒํƒœ**: ๐Ÿšจ **๊ธด๊ธ‰ ๋ถ„์„ ํ•„์š”** + +--- + +## ๐Ÿšจ ๋ฐœ๊ฒฌ๋œ ๊ตฌ์กฐ์  ๋ฌธ์ œ + +### ๋ฌธ์ œ ์š”์•ฝ + +**ํ˜„์žฌ `table_type_columns` ํ…Œ์ด๋ธ”์— `company_code` ์ปฌ๋Ÿผ์ด ์—†์Œ!** + +```sql +-- ํ˜„์žฌ table_type_columns ๊ตฌ์กฐ +CREATE TABLE table_type_columns ( + id SERIAL PRIMARY KEY, + table_name VARCHAR NOT NULL, + column_name VARCHAR NOT NULL, + input_type VARCHAR NOT NULL, -- ๐Ÿ”ด ๋ฌธ์ œ: ํšŒ์‚ฌ๋ณ„๋กœ ๋‹ค๋ฅด๊ฒŒ ์„ค์ • ๋ถˆ๊ฐ€! + detail_settings TEXT, + is_nullable VARCHAR, + display_order INTEGER, + created_date TIMESTAMP, + updated_date TIMESTAMP + -- โŒ company_code ์ปฌ๋Ÿผ ์—†์Œ! +); +``` + +--- + +## ๐ŸŽฏ ์‚ฌ์šฉ์ž๊ฐ€ ์ง€์ ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค + +### ์‹œ๋‚˜๋ฆฌ์˜ค: "์žฌ์งˆ" ์ปฌ๋Ÿผ์˜ ์ถฉ๋Œ + +``` +ํšŒ์‚ฌ A: item_info.material ์ปฌ๋Ÿผ์„ "์นดํ…Œ๊ณ ๋ฆฌ" ํƒ€์ž…์œผ๋กœ ์‚ฌ์šฉ + โ†’ ๋“œ๋กญ๋‹ค์šด ์„ ํƒ (์ฒ , ์•Œ๋ฃจ๋ฏธ๋Š„, ํ”Œ๋ผ์Šคํ‹ฑ) + +ํšŒ์‚ฌ B: item_info.material ์ปฌ๋Ÿผ์„ "ํ…์ŠคํŠธ" ํƒ€์ž…์œผ๋กœ ์‚ฌ์šฉ + โ†’ ์ž์œ  ์ž…๋ ฅ (SUS304, AL6061, PVC ๋“ฑ) + +ํ˜„์žฌ ๊ตฌ์กฐ: + โŒ table_type_columns์— company_code๊ฐ€ ์—†์Œ + โŒ ๋‘˜ ์ค‘ ํ•˜๋‚˜๋งŒ ์„ ํƒ ๊ฐ€๋Šฅ + โŒ ํšŒ์‚ฌ๋ณ„๋กœ ๋‹ค๋ฅธ input_type ์„ค์ • ๋ถˆ๊ฐ€๋Šฅ! +``` + +--- + +## ๐Ÿ“Š ํ˜„์žฌ ๊ตฌ์กฐ์˜ ๋ฌธ์ œ์  + +### 1. ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ํ™•์ธ + +```sql +-- table_type_columns ์‹ค์ œ ์ปฌ๋Ÿผ ํ™•์ธ +SELECT column_name FROM information_schema.columns +WHERE table_name = 'table_type_columns'; + +-- ๊ฒฐ๊ณผ: +id +table_name +column_name +input_type โ† ๐Ÿ”ด ํšŒ์‚ฌ๋ณ„ ๊ตฌ๋ถ„ ์—†์Œ! +detail_settings +is_nullable +display_order +created_date +updated_date +-- โŒ company_code ์—†์Œ! +``` + +### 2. ํ˜„์žฌ ๋ฐ์ดํ„ฐ ์˜ˆ์‹œ + +```sql +-- ํ˜„์žฌ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ +SELECT * FROM table_type_columns +WHERE table_name = 'item_info' AND column_name = 'material'; + +-- ๊ฐ€๋Šฅํ•œ ๊ฒฐ๊ณผ: +id | table_name | column_name | input_type | company_code +---|------------|-------------|------------|------------- +1 | item_info | material | category | โŒ ์—†์Œ +``` + +**๋ฌธ์ œ**: + +- ํšŒ์‚ฌ A๊ฐ€ `material`์„ `category`๋กœ ์„ค์ •ํ•˜๋ฉด +- ํšŒ์‚ฌ B๋Š” `material`์„ `text`๋กœ ์„ค์ •ํ•  ์ˆ˜ ์—†์Œ! +- **ํ•˜๋‚˜์˜ ์ปฌ๋Ÿผ ํƒ€์ž… ์ •์˜๋ฅผ ๋ชจ๋“  ํšŒ์‚ฌ๊ฐ€ ๊ณต์œ ** + +--- + +## ๐Ÿ” ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ถฉ๋Œ ๋ถ„์„ + +### Case 1: ๊ฐ™์€ ํ…Œ์ด๋ธ”, ๊ฐ™์€ ์ปฌ๋Ÿผ, ๋‹ค๋ฅธ ํƒ€์ž… + +| ์š”๊ตฌ์‚ฌํ•ญ | ํšŒ์‚ฌ A | ํšŒ์‚ฌ B | ํ˜„์žฌ ๊ฐ€๋Šฅ? | +| ---------- | ----------- | ----------- | ------------- | +| ํ…Œ์ด๋ธ” | `item_info` | `item_info` | โœ… ๊ณต์œ  | +| ์ปฌ๋Ÿผ | `material` | `material` | โœ… ๊ณต์œ  | +| input_type | `category` | `text` | โŒ **๋ถˆ๊ฐ€๋Šฅ** | + +**ํ˜„์žฌ ๋™์ž‘**: + +```typescript +// ํšŒ์‚ฌ A๊ฐ€ ์„ค์ • +await updateColumnType("item_info", "material", "category"); +// โ†’ table_type_columns์— ์ €์žฅ (company_code ์—†์Œ) + +// ํšŒ์‚ฌ B๊ฐ€ ์„ค์ • ์‹œ๋„ +await updateColumnType("item_info", "material", "text"); +// โ†’ โŒ ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ ๋ฎ์–ด์“ฐ๊ธฐ ๋˜๋Š” ์ถฉ๋Œ! +``` + +### Case 2: ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถฉ๋Œ + +| ์š”๊ตฌ์‚ฌํ•ญ | ํšŒ์‚ฌ A | ํšŒ์‚ฌ B | ํ˜„์žฌ ์ƒํƒœ | +| ----------- | ---------------------- | ------------------- | ---------------------------- | +| ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ | ์ฒ , ์•Œ๋ฃจ๋ฏธ๋Š„, ํ”Œ๋ผ์Šคํ‹ฑ | SUS304, AL6061, PVC | ๐ŸŸก **company_code๋กœ ๋ถ„๋ฆฌ๋จ** | + +**์ด๋ฏธ ์ˆ˜์ • ์™„๋ฃŒ**: + +- `table_column_category_values`๋Š” `company_code` ์ปฌ๋Ÿผ์ด ์žˆ์Œ โœ… +- ์นดํ…Œ๊ณ ๋ฆฌ **๊ฐ’**์€ ํšŒ์‚ฌ๋ณ„๋กœ ๋‹ค๋ฅด๊ฒŒ ์ €์žฅ ๊ฐ€๋Šฅ โœ… +- ํ•˜์ง€๋งŒ ์นดํ…Œ๊ณ ๋ฆฌ **ํƒ€์ž… ์ž์ฒด**๋Š” ๊ณต์œ ๋จ โŒ + +--- + +## ๐Ÿ—๏ธ ํ˜„์žฌ ์•„ํ‚คํ…์ฒ˜ vs ํ•„์š”ํ•œ ์•„ํ‚คํ…์ฒ˜ + +### ํ˜„์žฌ (์ž˜๋ชป๋œ) ์•„ํ‚คํ…์ฒ˜ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ table_type_columns โ”‚ +โ”‚ (์ปฌ๋Ÿผ ํƒ€์ž… ์ •์˜ - ์ „์—ญ) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ id | table | column | type โ”‚ +โ”‚ 1 | item | material | โ“ โ”‚ โ† ๐Ÿ”ด ์ถฉ๋Œ! +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†“ +ํšŒ์‚ฌ A: material = category? +ํšŒ์‚ฌ B: material = text? +โ†’ โŒ ๋‘˜ ์ค‘ ํ•˜๋‚˜๋งŒ ๊ฐ€๋Šฅ +``` + +### ํ•„์š”ํ•œ (์˜ฌ๋ฐ”๋ฅธ) ์•„ํ‚คํ…์ฒ˜ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ table_type_columns โ”‚ +โ”‚ (์ปฌ๋Ÿผ ํƒ€์ž… ์ •์˜ - ํšŒ์‚ฌ๋ณ„ ๋ถ„๋ฆฌ) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ id | table | column | type | company โ”‚ +โ”‚ 1 | item | material | category | A โ”‚ โœ… ํšŒ์‚ฌ A +โ”‚ 2 | item | material | text | B โ”‚ โœ… ํšŒ์‚ฌ B +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## ๐Ÿ’ฅ ์‹ค์ œ ๋ฐœ์ƒ ๊ฐ€๋Šฅํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค + +### ์‹œ๋‚˜๋ฆฌ์˜ค 1: ํ”„๋กœ์ ํŠธ ํƒ€์ž… + +``` +ํšŒ์‚ฌ A (IT ํšŒ์‚ฌ): + - projects.project_type โ†’ category + - ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’: ๊ฐœ๋ฐœ, ์œ ์ง€๋ณด์ˆ˜, ์ปจ์„คํŒ… + +ํšŒ์‚ฌ B (๊ฑด์„ค ํšŒ์‚ฌ): + - projects.project_type โ†’ text + - ์ž์œ  ์ž…๋ ฅ: ์•„ํŒŒํŠธ ์‹ ์ถ•, ๋„๋กœ ๋ณด์ˆ˜ ๊ณต์‚ฌ, ๋ฆฌ๋ชจ๋ธ๋ง ๋“ฑ + +ํ˜„์žฌ: โŒ ๋‘˜ ์ค‘ ํ•˜๋‚˜๋งŒ ์„ ํƒ ๊ฐ€๋Šฅ +ํ•„์š”: โœ… ํšŒ์‚ฌ๋ณ„๋กœ ๋‹ค๋ฅธ input_type ์„ค์ • +``` + +### ์‹œ๋‚˜๋ฆฌ์˜ค 2: ๋‹ด๋‹น์ž ํ•„๋“œ + +``` +ํšŒ์‚ฌ A (์†Œ๊ทœ๋ชจ): + - tasks.assignee โ†’ text + - ์ž์œ  ์ž…๋ ฅ: ์ด๋ฆ„ ์ง์ ‘ ์ž…๋ ฅ + +ํšŒ์‚ฌ B (๋Œ€๊ทœ๋ชจ): + - tasks.assignee โ†’ reference + - ์ฐธ์กฐ: user_info ํ…Œ์ด๋ธ”์—์„œ ์„ ํƒ + +ํ˜„์žฌ: โŒ ํ•˜๋‚˜์˜ ํƒ€์ž…๋งŒ ์„ค์ • ๊ฐ€๋Šฅ +ํ•„์š”: โœ… ํšŒ์‚ฌ๋ณ„๋กœ ๋‹ค๋ฅธ ๋ฐฉ์‹ +``` + +### ์‹œ๋‚˜๋ฆฌ์˜ค 3: ๊ธˆ์•ก ํ•„๋“œ + +``` +ํšŒ์‚ฌ A: + - contracts.amount โ†’ number + - ์ˆซ์ž ์ž…๋ ฅ (10,000,000) + +ํšŒ์‚ฌ B: + - contracts.amount โ†’ text + - ํŠน์ˆ˜ ํ˜•์‹ ์ž…๋ ฅ (โ‚ฉ10M, $100K, negotiable) + +ํ˜„์žฌ: โŒ ํ•˜๋‚˜์˜ ํƒ€์ž…๋งŒ +ํ•„์š”: โœ… ํšŒ์‚ฌ๋ณ„ ๋‹ค๋ฅธ ํƒ€์ž… +``` + +--- + +## ๐Ÿ”ง ํ•ด๊ฒฐ ๋ฐฉ์•ˆ + +### ๋ฐฉ์•ˆ 1: company_code ์ถ”๊ฐ€ (๊ถŒ์žฅ) โญ + +**๋งˆ์ด๊ทธ๋ ˆ์ด์…˜**: + +```sql +-- 1. company_code ์ปฌ๋Ÿผ ์ถ”๊ฐ€ +ALTER TABLE table_type_columns +ADD COLUMN company_code VARCHAR(20); + +-- 2. ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (๋ชจ๋“  ํšŒ์‚ฌ์— ๋ณต์ œ) +INSERT INTO table_type_columns ( + table_name, column_name, input_type, detail_settings, + is_nullable, display_order, company_code, created_date +) +SELECT + table_name, column_name, input_type, detail_settings, + is_nullable, display_order, + ci.company_code, -- ๊ฐ ํšŒ์‚ฌ๋ณ„๋กœ ๋ณต์ œ + created_date +FROM table_type_columns ttc +CROSS JOIN company_info ci +WHERE ttc.company_code IS NULL; -- ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋งŒ + +-- 3. NOT NULL ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ +ALTER TABLE table_type_columns +ALTER COLUMN company_code SET NOT NULL; + +-- 4. ๋ณตํ•ฉ ์œ ๋‹ˆํฌ ์ธ๋ฑ์Šค ์ƒ์„ฑ +CREATE UNIQUE INDEX idx_table_column_type_company +ON table_type_columns(table_name, column_name, company_code); + +-- 5. company_code ์ธ๋ฑ์Šค ์ƒ์„ฑ +CREATE INDEX idx_table_type_columns_company +ON table_type_columns(company_code); + +-- 6. ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ +ALTER TABLE table_type_columns +ADD CONSTRAINT fk_table_type_columns_company +FOREIGN KEY (company_code) REFERENCES company_info(company_code); +``` + +**์žฅ์ **: + +- โœ… ํšŒ์‚ฌ๋ณ„๋กœ ์™„์ „ํžˆ ๋…๋ฆฝ์ ์ธ ์ปฌ๋Ÿผ ํƒ€์ž… ์ •์˜ +- โœ… ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์›์น™ ์ค€์ˆ˜ +- โœ… ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”๊ณผ ์ผ๊ด€๋œ ๊ตฌ์กฐ + +**๋‹จ์ **: + +- ๐ŸŸก ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•„์š” +- ๐ŸŸก ๋ชจ๋“  ํšŒ์‚ฌ์— ๋™์ผํ•œ ํƒ€์ž… ์ •์˜๊ฐ€ ๋ณต์ œ๋จ + +--- + +### ๋ฐฉ์•ˆ 2: ๋ณ„๋„ ํ…Œ์ด๋ธ” ์ƒ์„ฑ (๋Œ€์•ˆ) + +```sql +-- company_specific_column_types ํ…Œ์ด๋ธ” ์ƒ์„ฑ +CREATE TABLE company_specific_column_types ( + id SERIAL PRIMARY KEY, + company_code VARCHAR(20) NOT NULL, + table_name VARCHAR NOT NULL, + column_name VARCHAR NOT NULL, + input_type VARCHAR NOT NULL, + detail_settings TEXT, + created_at TIMESTAMP DEFAULT NOW(), + FOREIGN KEY (company_code) REFERENCES company_info(company_code), + UNIQUE(company_code, table_name, column_name) +); + +-- ์กฐํšŒ ์‹œ ์šฐ์„ ์ˆœ์œ„ +-- 1์ˆœ์œ„: company_specific_column_types (ํšŒ์‚ฌ๋ณ„ ์„ค์ •) +-- 2์ˆœ์œ„: table_type_columns (์ „์—ญ ๊ธฐ๋ณธ๊ฐ’) +``` + +**์žฅ์ **: + +- โœ… ๊ธฐ์กด table_type_columns๋Š” ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์œ ์ง€ +- โœ… ํšŒ์‚ฌ๋ณ„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์€ ๋ณ„๋„ ๊ด€๋ฆฌ + +**๋‹จ์ **: + +- โŒ ๋ณต์žกํ•œ ์กฐํšŒ ๋กœ์ง (2๊ฐœ ํ…Œ์ด๋ธ” ์กฐ์ธ) +- โŒ ์ผ๊ด€์„ฑ ์—†๋Š” ๊ตฌ์กฐ + +--- + +### ๋ฐฉ์•ˆ 3: JSON ํ•„๋“œ ์‚ฌ์šฉ (๋น„์ถ”์ฒœ) + +```sql +-- company_overrides JSON ์ปฌ๋Ÿผ ์ถ”๊ฐ€ +ALTER TABLE table_type_columns +ADD COLUMN company_overrides JSONB; + +-- ์˜ˆ์‹œ: +{ + "COMPANY_A": { "input_type": "category" }, + "COMPANY_B": { "input_type": "text" } +} +``` + +**๋‹จ์ **: + +- โŒ ์ฟผ๋ฆฌ ๋ณต์žก๋„ ์ฆ๊ฐ€ +- โŒ ์ธ๋ฑ์‹ฑ ์–ด๋ ค์›€ +- โŒ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๋ณด์žฅ ์–ด๋ ค์›€ + +--- + +## ๐Ÿ“‹ ์˜ํ–ฅ ๋ฐ›๋Š” ์ฝ”๋“œ + +### ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค + +```typescript +// โŒ ํ˜„์žฌ ์ฝ”๋“œ (company_code ์—†์Œ) +async getColumnType(tableName: string, columnName: string) { + const query = ` + SELECT input_type FROM table_type_columns + WHERE table_name = $1 AND column_name = $2 + `; + return await pool.query(query, [tableName, columnName]); +} + +// โœ… ์ˆ˜์ • ํ•„์š” (company_code ์ถ”๊ฐ€) +async getColumnType(tableName: string, columnName: string, companyCode: string) { + const query = ` + SELECT input_type FROM table_type_columns + WHERE table_name = $1 + AND column_name = $2 + AND company_code = $3 + `; + return await pool.query(query, [tableName, columnName, companyCode]); +} +``` + +### ์˜ํ–ฅ๋ฐ›๋Š” ํŒŒ์ผ (์˜ˆ์ƒ) + +- `backend-node/src/services/tableService.ts` +- `backend-node/src/services/dataService.ts` +- `backend-node/src/controllers/tableController.ts` +- `frontend/components/table-category/CategoryColumnList.tsx` +- ๊ธฐํƒ€ `table_type_columns`๋ฅผ ์ฐธ์กฐํ•˜๋Š” ๋ชจ๋“  ์ฝ”๋“œ + +--- + +## ๐Ÿงช ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค + +### ํ…Œ์ŠคํŠธ 1: ํšŒ์‚ฌ๋ณ„ ๋‹ค๋ฅธ ํƒ€์ž… ์„ค์ • + +```sql +-- ํšŒ์‚ฌ A: material์„ ์นดํ…Œ๊ณ ๋ฆฌ๋กœ +INSERT INTO table_type_columns (table_name, column_name, input_type, company_code) +VALUES ('item_info', 'material', 'category', 'COMPANY_A'); + +-- ํšŒ์‚ฌ B: material์„ ํ…์ŠคํŠธ๋กœ +INSERT INTO table_type_columns (table_name, column_name, input_type, company_code) +VALUES ('item_info', 'material', 'text', 'COMPANY_B'); + +-- ์กฐํšŒ ํ™•์ธ +SELECT * FROM table_type_columns +WHERE table_name = 'item_info' AND column_name = 'material'; + +-- ์˜ˆ์ƒ ๊ฒฐ๊ณผ: +-- id | table_name | column_name | input_type | company_code +-- 1 | item_info | material | category | COMPANY_A +-- 2 | item_info | material | text | COMPANY_B +``` + +### ํ…Œ์ŠคํŠธ 2: ํšŒ์‚ฌ๋ณ„ ํ™”๋ฉด ํ‘œ์‹œ + +```typescript +// ํšŒ์‚ฌ A ์‚ฌ์šฉ์ž๊ฐ€ item_info ํ…Œ์ด๋ธ” ์—ด๋žŒ +GET /api/tables/item_info/columns +Authorization: Bearer {token_company_a} + +// ์˜ˆ์ƒ ๊ฒฐ๊ณผ: +{ + "material": { + "inputType": "category", // ๋“œ๋กญ๋‹ค์šด + "categoryValues": ["์ฒ ", "์•Œ๋ฃจ๋ฏธ๋Š„", "ํ”Œ๋ผ์Šคํ‹ฑ"] + } +} + +// ํšŒ์‚ฌ B ์‚ฌ์šฉ์ž๊ฐ€ item_info ํ…Œ์ด๋ธ” ์—ด๋žŒ +GET /api/tables/item_info/columns +Authorization: Bearer {token_company_b} + +// ์˜ˆ์ƒ ๊ฒฐ๊ณผ: +{ + "material": { + "inputType": "text", // ํ…์ŠคํŠธ ์ž…๋ ฅ + "placeholder": "์žฌ์งˆ์„ ์ž…๋ ฅํ•˜์„ธ์š”" + } +} +``` + +--- + +## ๐Ÿšจ ๊ธด๊ธ‰๋„ ํ‰๊ฐ€ + +| ํ•ญ๋ชฉ | ํ‰๊ฐ€ | ์„ค๋ช… | +| --------------- | -------------- | ---------------------------------- | +| **์‹ฌ๊ฐ๋„** | ๐Ÿ”ด ๋†’์Œ | ํšŒ์‚ฌ๋ณ„ ๋…๋ฆฝ์ ์ธ ํ…Œ์ด๋ธ” ์„ค์ • ๋ถˆ๊ฐ€๋Šฅ | +| **์˜ํ–ฅ ๋ฒ”์œ„** | ๐Ÿ”ด ์ „์ฒด ์‹œ์Šคํ…œ | ๋ชจ๋“  ๋™์  ํ…Œ์ด๋ธ” ๊ธฐ๋Šฅ์— ์˜ํ–ฅ | +| **์ˆ˜์ • ๋‚œ์ด๋„** | ๐ŸŸก ์ค‘๊ฐ„ | ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + ์ฝ”๋“œ ์ˆ˜์ • ํ•„์š” | +| **๊ธด๊ธ‰๋„** | ๐Ÿ”ด ๋†’์Œ | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•ต์‹ฌ ๊ธฐ๋Šฅ | + +--- + +## ๐Ÿ“ ๊ถŒ์žฅ ์กฐ์น˜ + +### ์šฐ์„ ์ˆœ์œ„ 1: ์ฆ‰์‹œ ํ™•์ธ + +- [ ] ํ˜„์žฌ `table_type_columns` ์‚ฌ์šฉ ํ˜„ํ™ฉ ํŒŒ์•… +- [ ] ์‹ค์ œ๋กœ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•˜๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธ +- [ ] ํšŒ์‚ฌ๋ณ„๋กœ ๋‹ค๋ฅธ ํƒ€์ž… ์„ค์ •์ด ํ•„์š”ํ•œ ์ผ€์ด์Šค ์ˆ˜์ง‘ + +### ์šฐ์„ ์ˆœ์œ„ 2: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ค€๋น„ + +- [ ] `company_code` ์ถ”๊ฐ€ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ž‘์„ฑ +- [ ] ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋ฐฑ์—… ๊ณ„ํš ์ˆ˜๋ฆฝ +- [ ] ๋กค๋ฐฑ ๋ฐฉ์•ˆ ์ค€๋น„ + +### ์šฐ์„ ์ˆœ์œ„ 3: ์ฝ”๋“œ ์ˆ˜์ • + +- [ ] ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค ์ˆ˜์ • (company_code ์ถ”๊ฐ€) +- [ ] API ์—”๋“œํฌ์ธํŠธ ์ˆ˜์ • +- [ ] ํ”„๋ก ํŠธ์—”๋“œ ์ปดํฌ๋„ŒํŠธ ์ˆ˜์ • + +--- + +## ๐Ÿ”— ๊ด€๋ จ ์ด์Šˆ + +- [์ฑ„๋ฒˆ ๊ทœ์น™ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ฒ„๊ทธ](./์ฑ„๋ฒˆ๊ทœ์น™_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๋ฒ„๊ทธ_์ˆ˜์ •_์™„๋ฃŒ.md) โœ… ์ˆ˜์ • ์™„๋ฃŒ +- [์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ฒ„๊ทธ](./์นดํ…Œ๊ณ ๋ฆฌ_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๋ฒ„๊ทธ_์ˆ˜์ •_์™„๋ฃŒ.md) โœ… ์ˆ˜์ • ์™„๋ฃŒ +- ๐Ÿšจ **ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ํƒ€์ž… ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ** โ† ํ˜„์žฌ ๋ฌธ์„œ (๋ฏธ์ˆ˜์ •) + +--- + +**์ž‘์„ฑ์ผ**: 2025-11-06 +**๋ถ„์„์ž**: AI Assistant (์‚ฌ์šฉ์ž ์ง€์  ๊ธฐ๋ฐ˜) +**๋‹ค์Œ ๋‹จ๊ณ„**: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ž‘์„ฑ ๋ฐ ์ฝ”๋“œ ์ˆ˜์ • ํ•„์š” diff --git a/docs/ํ…Œ์ด๋ธ”_์ปฌ๋Ÿผ_ํƒ€์ž…_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_์ˆ˜์ •_์™„๋ฃŒ.md b/docs/ํ…Œ์ด๋ธ”_์ปฌ๋Ÿผ_ํƒ€์ž…_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_์ˆ˜์ •_์™„๋ฃŒ.md new file mode 100644 index 00000000..7332dfef --- /dev/null +++ b/docs/ํ…Œ์ด๋ธ”_์ปฌ๋Ÿผ_ํƒ€์ž…_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_์ˆ˜์ •_์™„๋ฃŒ.md @@ -0,0 +1,611 @@ +# ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ํƒ€์ž… ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ˆ˜์ • ์™„๋ฃŒ ๋ณด๊ณ ์„œ + +## ๐Ÿ“‹ ๊ฐœ์š” + +**์ผ์‹œ**: 2025-11-06 +**์ž‘์—…์ž**: AI Assistant +**์‹ฌ๊ฐ๋„**: ๐Ÿ”ด ๋†’์Œ โ†’ โœ… ํ•ด๊ฒฐ +**๊ด€๋ จ ๋ฌธ์„œ**: [ํ…Œ์ด๋ธ”_์ปฌ๋Ÿผ_ํƒ€์ž…_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๊ตฌ์กฐ์ _๋ฌธ์ œ_๋ถ„์„.md](./ํ…Œ์ด๋ธ”_์ปฌ๋Ÿผ_ํƒ€์ž…_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๊ตฌ์กฐ์ _๋ฌธ์ œ_๋ถ„์„.md) + +--- + +## ๐Ÿ” ๋ฌธ์ œ ์š”์•ฝ + +### ๋ฐœ๊ฒฌ๋œ ๋ฌธ์ œ + +**ํšŒ์‚ฌ๋ณ„๋กœ ๊ฐ™์€ ํ…Œ์ด๋ธ”์˜ ๊ฐ™์€ ์ปฌ๋Ÿผ์— ๋Œ€ํ•ด ๋‹ค๋ฅธ ์ž…๋ ฅ ํƒ€์ž…์„ ์„ค์ •ํ•  ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.** + +#### ์‹ค์ œ ์‹œ๋‚˜๋ฆฌ์˜ค + +``` +ํšŒ์‚ฌ A: item_info.material โ†’ category (๋“œ๋กญ๋‹ค์šด ์„ ํƒ) +ํšŒ์‚ฌ B: item_info.material โ†’ text (์ž์œ  ์ž…๋ ฅ) + +โŒ ํ˜„์žฌ: ๋‘˜ ์ค‘ ํ•˜๋‚˜๋งŒ ์„ ํƒ ๊ฐ€๋Šฅ +โœ… ์ˆ˜์ • ํ›„: ๊ฐ ํšŒ์‚ฌ๋ณ„๋กœ ๋…๋ฆฝ์ ์œผ๋กœ ์„ค์ • ๊ฐ€๋Šฅ +``` + +#### ๊ทผ๋ณธ ์›์ธ + +- `table_type_columns` ํ…Œ์ด๋ธ”์— `company_code` ์ปฌ๋Ÿผ์ด ์—†์Œ +- ์œ ๋‹ˆํฌ ์ œ์•ฝ์กฐ๊ฑด: `(table_name, column_name)` โ† company_code ์—†์Œ! +- ๋ชจ๋“  ํšŒ์‚ฌ๊ฐ€ ๊ฐ™์€ ์ปฌ๋Ÿผ ํƒ€์ž… ์ •์˜๋ฅผ ๊ณต์œ ํ•จ + +--- + +## ๐Ÿ› ๏ธ ์ˆ˜์ • ๋‚ด์šฉ + +### 1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +#### ํŒŒ์ผ: `db/migrations/044_add_company_code_to_table_type_columns.sql` + +**์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ**: +- `company_code VARCHAR(20) NOT NULL` ์ปฌ๋Ÿผ ์ถ”๊ฐ€ +- ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋“  ํšŒ์‚ฌ์— ๋ณต์ œ (510๊ฑด โ†’ 1,020๊ฑด) +- ๋ณตํ•ฉ ์œ ๋‹ˆํฌ ์ธ๋ฑ์Šค ์ƒ์„ฑ: `(table_name, column_name, company_code)` +- ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€: `company_mng(company_code)` ์ฐธ์กฐ + +**๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ๋ฐฉ๋ฒ•**: +```bash +docker exec -i erp-node-db-1 psql -U postgres -d ilshin < db/migrations/044_add_company_code_to_table_type_columns.sql +``` + +**๊ฒ€์ฆ ์ฟผ๋ฆฌ**: +```sql +-- 1. ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ํ™•์ธ +SELECT column_name, data_type, is_nullable +FROM information_schema.columns +WHERE table_name = 'table_type_columns' AND column_name = 'company_code'; + +-- ์˜ˆ์ƒ: data_type=character varying, is_nullable=NO + +-- 2. ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ™•์ธ +SELECT + COUNT(*) as total, + COUNT(DISTINCT company_code) as company_count, + COUNT(CASE WHEN company_code IS NULL THEN 1 END) as null_count +FROM table_type_columns; + +-- ์˜ˆ์ƒ: total=1020, company_count=2, null_count=0 + +-- 3. ํšŒ์‚ฌ๋ณ„ ๋ฐ์ดํ„ฐ ๋ถ„ํฌ +SELECT company_code, COUNT(*) as count +FROM table_type_columns +GROUP BY company_code +ORDER BY company_code; + +-- ์˜ˆ์ƒ: ๊ฐ ํšŒ์‚ฌ๋งˆ๋‹ค 510๊ฑด์”ฉ (์ด 2๊ฐœ ํšŒ์‚ฌ: * + COMPANY_7) +``` + +--- + +### 2. ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค ์ˆ˜์ • + +#### ํŒŒ์ผ: `backend-node/src/services/tableManagementService.ts` + +#### (1) `getColumnInputTypes` ๋ฉ”์„œ๋“œ + +**๋ณ€๊ฒฝ ์ „**: +```typescript +async getColumnInputTypes(tableName: string): Promise +``` + +**๋ณ€๊ฒฝ ํ›„**: +```typescript +async getColumnInputTypes( + tableName: string, + companyCode: string // โœ… ์ถ”๊ฐ€ +): Promise +``` + +**SQL ์ฟผ๋ฆฌ ๋ณ€๊ฒฝ**: +```typescript +// โŒ ์ด์ „ +`SELECT ... FROM column_labels cl WHERE cl.table_name = $1` + +// โœ… ์ˆ˜์ • ํ›„ +`SELECT ... + FROM table_type_columns ttc + LEFT JOIN column_labels cl ... + WHERE ttc.table_name = $1 + AND ttc.company_code = $2 -- ํšŒ์‚ฌ๋ณ„ ํ•„ํ„ฐ๋ง + ORDER BY ttc.display_order, ttc.column_name` +``` + +#### (2) `updateColumnInputType` ๋ฉ”์„œ๋“œ + +**๋ณ€๊ฒฝ ์ „**: +```typescript +async updateColumnInputType( + tableName: string, + columnName: string, + inputType: string, + detailSettings?: Record +): Promise +``` + +**๋ณ€๊ฒฝ ํ›„**: +```typescript +async updateColumnInputType( + tableName: string, + columnName: string, + inputType: string, + companyCode: string, // โœ… ์ถ”๊ฐ€ + detailSettings?: Record +): Promise +``` + +**SQL ์ฟผ๋ฆฌ ๋ณ€๊ฒฝ**: +```typescript +// โŒ ์ด์ „ +`INSERT INTO table_type_columns ( + table_name, column_name, input_type, detail_settings, + is_nullable, display_order, created_date, updated_date +) VALUES ($1, $2, $3, $4, 'Y', 0, now(), now()) +ON CONFLICT (table_name, column_name) -- company_code ์—†์Œ! +DO UPDATE SET ...` + +// โœ… ์ˆ˜์ • ํ›„ +`INSERT INTO table_type_columns ( + table_name, column_name, input_type, detail_settings, + is_nullable, display_order, company_code, created_date, updated_date +) VALUES ($1, $2, $3, $4, 'Y', 0, $5, now(), now()) +ON CONFLICT (table_name, column_name, company_code) -- ํšŒ์‚ฌ๋ณ„ ์œ ๋‹ˆํฌ! +DO UPDATE SET ...` +``` + +--- + +### 3. API ์—”๋“œํฌ์ธํŠธ ์ˆ˜์ • + +#### ํŒŒ์ผ: `backend-node/src/controllers/tableManagementController.ts` + +#### (1) `getColumnWebTypes` ์ปจํŠธ๋กค๋Ÿฌ + +**๋ณ€๊ฒฝ ์ „**: +```typescript +export async function getColumnWebTypes( + req: AuthenticatedRequest, + res: Response +): Promise { + const { tableName } = req.params; + + // โŒ companyCode ์—†์Œ + const inputTypes = await tableManagementService.getColumnInputTypes(tableName); +} +``` + +**๋ณ€๊ฒฝ ํ›„**: +```typescript +export async function getColumnWebTypes( + req: AuthenticatedRequest, + res: Response +): Promise { + const { tableName } = req.params; + const companyCode = req.user?.companyCode; // โœ… ์ธ์ฆ ์ •๋ณด์—์„œ ์ถ”์ถœ + + if (!companyCode) { + return res.status(401).json({ + success: false, + message: "ํšŒ์‚ฌ ์ฝ”๋“œ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + error: { code: "MISSING_COMPANY_CODE" } + }); + } + + const inputTypes = await tableManagementService.getColumnInputTypes( + tableName, + companyCode // โœ… ์ „๋‹ฌ + ); +} +``` + +#### (2) `updateColumnInputType` ์ปจํŠธ๋กค๋Ÿฌ + +**๋ณ€๊ฒฝ ์ „**: +```typescript +export async function updateColumnInputType( + req: AuthenticatedRequest, + res: Response +): Promise { + const { tableName, columnName } = req.params; + const { inputType, detailSettings } = req.body; + + // โŒ companyCode ์—†์Œ + await tableManagementService.updateColumnInputType( + tableName, + columnName, + inputType, + detailSettings + ); +} +``` + +**๋ณ€๊ฒฝ ํ›„**: +```typescript +export async function updateColumnInputType( + req: AuthenticatedRequest, + res: Response +): Promise { + const { tableName, columnName } = req.params; + const { inputType, detailSettings } = req.body; + const companyCode = req.user?.companyCode; // โœ… ์ธ์ฆ ์ •๋ณด์—์„œ ์ถ”์ถœ + + if (!companyCode) { + return res.status(401).json({ + success: false, + message: "ํšŒ์‚ฌ ์ฝ”๋“œ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + error: { code: "MISSING_COMPANY_CODE" } + }); + } + + await tableManagementService.updateColumnInputType( + tableName, + columnName, + inputType, + companyCode, // โœ… ์ „๋‹ฌ + detailSettings + ); +} +``` + +--- + +### 4. ํ”„๋ก ํŠธ์—”๋“œ (์ˆ˜์ • ๋ถˆํ•„์š”) + +#### ํŒŒ์ผ: `frontend/lib/api/tableManagement.ts` + +**ํ˜„์žฌ ์ฝ”๋“œ** (์ˆ˜์ • ๋ถˆํ•„์š”): +```typescript +async getColumnWebTypes(tableName: string): Promise> { + try { + // โœ… apiClient๊ฐ€ ์ž๋™์œผ๋กœ Authorization ํ—ค๋”์— JWT ํ† ํฐ ์ถ”๊ฐ€ + // โœ… ๋ฐฑ์—”๋“œ์—์„œ req.user.companyCode๋กœ ์ž๋™ ์ถ”์ถœ + const response = await apiClient.get(`${this.basePath}/tables/${tableName}/web-types`); + return response.data; + } catch (error: any) { + console.error(`โŒ ํ…Œ์ด๋ธ” '${tableName}' ์›นํƒ€์ž… ์ •๋ณด ์กฐํšŒ ์‹คํŒจ:`, error); + return { + success: false, + message: error.response?.data?.message || "์›นํƒ€์ž… ์ •๋ณด๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + }; + } +} +``` + +**์™œ ์ˆ˜์ •์ด ๋ถˆํ•„์š”ํ•œ๊ฐ€?** +- `apiClient`๋Š” ์ด๋ฏธ ์ธ์ฆ ํ† ํฐ์„ ์ž๋™์œผ๋กœ ํ—ค๋”์— ์ถ”๊ฐ€ +- ๋ฐฑ์—”๋“œ `authMiddleware`๊ฐ€ JWT์—์„œ `companyCode`๋ฅผ ์ถ”์ถœํ•˜์—ฌ `req.user`์— ์ €์žฅ +- ์ปจํŠธ๋กค๋Ÿฌ์—์„œ `req.user.companyCode`๋กœ ์ ‘๊ทผ + +--- + +## ๐Ÿ“Š ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฒฐ๊ณผ + +### Before (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „) + +```sql +SELECT * FROM table_type_columns LIMIT 3; + + id | table_name | column_name | input_type | company_code +----|-------------|-------------|------------|------------- + 1 | item_info | material | text | NULL + 2 | projects | type | category | NULL + 3 | contracts | status | code | NULL +``` + +**๋ฌธ์ œ**: +- `company_code`๊ฐ€ NULL +- ๋ชจ๋“  ํšŒ์‚ฌ๊ฐ€ ๊ฐ™์€ ํƒ€์ž… ์ •์˜๋ฅผ ๊ณต์œ  +- ์œ ๋‹ˆํฌ ์ œ์•ฝ์กฐ๊ฑด์— `company_code` ์—†์Œ + +--- + +### After (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ›„) + +```sql +SELECT * FROM table_type_columns WHERE table_name = 'item_info' AND column_name = 'material'; + + id | table_name | column_name | input_type | company_code +----|------------|-------------|------------|------------- + 1 | item_info | material | text | * +511 | item_info | material | text | COMPANY_7 +``` + +**๊ฐœ์„ ์‚ฌํ•ญ**: +- โœ… ๊ฐ ํšŒ์‚ฌ๋ณ„๋กœ ๋…๋ฆฝ์ ์ธ ๋ ˆ์ฝ”๋“œ +- โœ… `company_code NOT NULL` +- โœ… ์œ ๋‹ˆํฌ ์ œ์•ฝ์กฐ๊ฑด: `(table_name, column_name, company_code)` + +--- + +## โœ… ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค + +### ์‹œ๋‚˜๋ฆฌ์˜ค 1: ํšŒ์‚ฌ๋ณ„ ๋‹ค๋ฅธ ํƒ€์ž… ์„ค์ • + +```sql +-- ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: material์„ ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ๋ณ€๊ฒฝ +UPDATE table_type_columns +SET input_type = 'category', + updated_date = now() +WHERE table_name = 'item_info' + AND column_name = 'material' + AND company_code = '*'; + +-- COMPANY_7: material์„ ํ…์ŠคํŠธ๋กœ ์œ ์ง€ +-- (๋ณ€๊ฒฝ ์—†์Œ) + +-- ํ™•์ธ +SELECT table_name, column_name, input_type, company_code +FROM table_type_columns +WHERE table_name = 'item_info' AND column_name = 'material' + AND company_code IN ('*', 'COMPANY_7') +ORDER BY company_code; + +-- ์˜ˆ์ƒ ๊ฒฐ๊ณผ: +-- item_info | material | category | * โœ… ๋‹ค๋ฆ„! +-- item_info | material | text | COMPANY_7 โœ… ๋‹ค๋ฆ„! +``` + +### ์‹œ๋‚˜๋ฆฌ์˜ค 2: API ํ˜ธ์ถœ ํ…Œ์ŠคํŠธ + +```typescript +// ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋กœ ๋กœ๊ทธ์ธ +// JWT ํ† ํฐ: { userId: "admin", companyCode: "*" } + +const response = await fetch('/api/tables/item_info/web-types', { + headers: { + 'Authorization': `Bearer ${token}`, + } +}); + +const data = await response.json(); +console.log(data); + +// ์˜ˆ์ƒ ๊ฒฐ๊ณผ: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋Š” ๋ชจ๋“  ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๊ฐ€๋Šฅ +// { +// success: true, +// data: [ +// { columnName: 'material', inputType: 'category', companyCode: '*', ... } +// { columnName: 'material', inputType: 'text', companyCode: 'COMPANY_7', ... } +// ] +// } +``` + +```typescript +// COMPANY_7 ๊ด€๋ฆฌ์ž๋กœ ๋กœ๊ทธ์ธ +// JWT ํ† ํฐ: { userId: "user7", companyCode: "COMPANY_7" } + +const response = await fetch('/api/tables/item_info/web-types', { + headers: { + 'Authorization': `Bearer ${token}`, + } +}); + +const data = await response.json(); +console.log(data); + +// ์˜ˆ์ƒ ๊ฒฐ๊ณผ: COMPANY_7์˜ ์ปฌ๋Ÿผ ํƒ€์ž…๋งŒ ๋ฐ˜ํ™˜ +// { +// success: true, +// data: [ +// { columnName: 'material', inputType: 'text', ... } // COMPANY_7 ์ „์šฉ +// ] +// } +``` + +--- + +## ๐Ÿ” ์ตœ๊ณ  ๊ด€๋ฆฌ์ž (SUPER_ADMIN) ์˜ˆ์™ธ ์ฒ˜๋ฆฌ + +### company_code = "*" ์˜๋ฏธ + +**์ค‘์š”**: `company_code = "*"`๋Š” **์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ ๋ฐ์ดํ„ฐ**์ž…๋‹ˆ๋‹ค. + +```sql +-- ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ๋ฐ์ดํ„ฐ +SELECT * FROM table_type_columns WHERE company_code = '*'; + +-- โŒ ์ž˜๋ชป๋œ ์ดํ•ด: ๋ชจ๋“  ํšŒ์‚ฌ๊ฐ€ ๊ณต์œ ํ•˜๋Š” ๊ณตํ†ต ๋ฐ์ดํ„ฐ +-- โœ… ์˜ฌ๋ฐ”๋ฅธ ์ดํ•ด: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ๊ด€๋ฆฌํ•˜๋Š” ์ „์šฉ ๋ฐ์ดํ„ฐ +``` + +### ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ ‘๊ทผ ๊ถŒํ•œ + +```typescript +// ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค (์˜ˆ: getColumnInputTypes) + +if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๊ฐ€๋Šฅ + query = ` + SELECT * FROM table_type_columns + WHERE table_name = $1 + ORDER BY company_code, column_name + `; + params = [tableName]; + logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์ฒด ์ปฌ๋Ÿผ ํƒ€์ž… ์กฐํšŒ"); +} else { + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ๋งŒ ์กฐํšŒ (company_code="*" ์ œ์™ธ!) + query = ` + SELECT * FROM table_type_columns + WHERE table_name = $1 + AND company_code = $2 + ORDER BY column_name + `; + params = [tableName, companyCode]; + logger.info("ํšŒ์‚ฌ๋ณ„ ์ปฌ๋Ÿผ ํƒ€์ž… ์กฐํšŒ", { companyCode }); +} +``` + +**ํ•ต์‹ฌ**: ์ผ๋ฐ˜ ํšŒ์‚ฌ ์‚ฌ์šฉ์ž๋Š” `company_code = "*"` ๋ฐ์ดํ„ฐ๋ฅผ **์ ˆ๋Œ€ ๋ณผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค**! + +--- + +## ๐Ÿ“ ์ˆ˜์ •๋œ ํŒŒ์ผ ๋ชฉ๋ก + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค +- โœ… `db/migrations/044_add_company_code_to_table_type_columns.sql` (์‹ ๊ทœ) +- โœ… `db/migrations/RUN_044_MIGRATION.md` (์‹ ๊ทœ) +- โœ… `db/migrations/EXECUTE_044_MIGRATION_NOW.txt` (์‹ ๊ทœ) + +### ๋ฐฑ์—”๋“œ +- โœ… `backend-node/src/services/tableManagementService.ts` + - `getColumnInputTypes()` - company_code ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ + - `updateColumnInputType()` - company_code ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ +- โœ… `backend-node/src/controllers/tableManagementController.ts` + - `getColumnWebTypes()` - req.user.companyCode ์ถ”์ถœ ๋ฐ ์ „๋‹ฌ + - `updateColumnInputType()` - req.user.companyCode ์ถ”์ถœ ๋ฐ ์ „๋‹ฌ + +### ํ”„๋ก ํŠธ์—”๋“œ +- โšช ์ˆ˜์ • ๋ถˆํ•„์š” (apiClient๊ฐ€ ์ž๋™์œผ๋กœ ์ธ์ฆ ํ—ค๋” ์ถ”๊ฐ€) + +### ๋ฌธ์„œ +- โœ… `docs/ํ…Œ์ด๋ธ”_์ปฌ๋Ÿผ_ํƒ€์ž…_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๊ตฌ์กฐ์ _๋ฌธ์ œ_๋ถ„์„.md` (๊ธฐ์กด) +- โœ… `docs/ํ…Œ์ด๋ธ”_์ปฌ๋Ÿผ_ํƒ€์ž…_๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_์ˆ˜์ •_์™„๋ฃŒ.md` (๋ณธ ๋ฌธ์„œ) + +--- + +## ๐ŸŽฏ ๋‹ค์Œ ๋‹จ๊ณ„ + +### 1. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ (ํ•„์ˆ˜) + +```bash +docker exec -i erp-node-db-1 psql -U postgres -d ilshin < db/migrations/044_add_company_code_to_table_type_columns.sql +``` + +### 2. ๊ฒ€์ฆ + +```sql +-- 1. ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ํ™•์ธ +SELECT column_name FROM information_schema.columns +WHERE table_name = 'table_type_columns' AND column_name = 'company_code'; + +-- 2. ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ ํ™•์ธ +SELECT COUNT(*) as total FROM table_type_columns; +-- ์˜ˆ์ƒ: 1020 (510 ร— 2) + +-- 3. NULL ํ™•์ธ +SELECT COUNT(*) FROM table_type_columns WHERE company_code IS NULL; +-- ์˜ˆ์ƒ: 0 +``` + +### 3. ๋ฐฑ์—”๋“œ ์žฌ์‹œ์ž‘ + +```bash +# Docker ํ™˜๊ฒฝ +docker-compose restart backend + +# ๋กœ์ปฌ ํ™˜๊ฒฝ +npm run dev +``` + +### 4. ํ”„๋ก ํŠธ์—”๋“œ ํ…Œ์ŠคํŠธ + +1. ์ตœ๊ณ  ๊ด€๋ฆฌ์ž(*) ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ +2. ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ โ†’ item_info ํ…Œ์ด๋ธ” ์„ ํƒ +3. material ์ปฌ๋Ÿผ ํƒ€์ž…์„ **category**๋กœ ๋ณ€๊ฒฝ +4. ์ €์žฅ ํ™•์ธ + +5. COMPANY_7(ํƒ‘์”ฐ) ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ +6. ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ โ†’ item_info ํ…Œ์ด๋ธ” ์„ ํƒ +7. material ์ปฌ๋Ÿผ ํƒ€์ž…์ด ์—ฌ์ „ํžˆ **text**์ธ์ง€ ํ™•์ธ โœ… + +--- + +## ๐Ÿšจ ์ฃผ์˜์‚ฌํ•ญ + +### 1. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „ ๋ฐฑ์—… ํ•„์ˆ˜ + +```bash +# PostgreSQL ๋ฐฑ์—… +docker exec erp-node-db-1 pg_dump -U postgres ilshin > backup_before_044.sql +``` + +### 2. ๋ฐ์ดํ„ฐ ์ฆ๊ฐ€ + +- ๊ธฐ์กด: 510๊ฑด +- ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ›„: 1,020๊ฑด (2๊ฐœ ํšŒ์‚ฌ ร— 510๊ฑด) +- ๋””์Šคํฌ ๊ณต๊ฐ„: ์•ฝ 2๋ฐฐ ์ฆ๊ฐ€ (์˜ํ–ฅ ๋ฏธ๋ฏธ) + +### 3. ๊ธฐ์กด ์ฝ”๋“œ ํ˜ธํ™˜์„ฑ + +**์ด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์€ Breaking Change์ž…๋‹ˆ๋‹ค!** + +`getColumnInputTypes()`๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ชจ๋“  ์ฝ”๋“œ๋Š” `companyCode`๋ฅผ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + +```typescript +// โŒ ์ด์ „ ์ฝ”๋“œ (๋” ์ด์ƒ ์ž‘๋™ํ•˜์ง€ ์•Š์Œ) +const types = await tableManagementService.getColumnInputTypes(tableName); + +// โœ… ์ˆ˜์ •๋œ ์ฝ”๋“œ +const companyCode = req.user?.companyCode; +const types = await tableManagementService.getColumnInputTypes(tableName, companyCode); +``` + +### 4. ๋กค๋ฐฑ ๋ฐฉ๋ฒ• + +๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ๋กค๋ฐฑ: + +```sql +BEGIN; + +-- 1. ์™ธ๋ž˜ํ‚ค ์ œ๊ฑฐ +ALTER TABLE table_type_columns +DROP CONSTRAINT IF EXISTS fk_table_type_columns_company; + +-- 2. ์ธ๋ฑ์Šค ์ œ๊ฑฐ +DROP INDEX IF EXISTS idx_table_column_type_company; +DROP INDEX IF EXISTS idx_table_type_columns_company; + +-- 3. company_code ์ปฌ๋Ÿผ ์ œ๊ฑฐ +ALTER TABLE table_type_columns ALTER COLUMN company_code DROP NOT NULL; +ALTER TABLE table_type_columns DROP COLUMN IF EXISTS company_code; + +COMMIT; +``` + +--- + +## ๐Ÿ“ˆ ์„ฑ๋Šฅ ์˜ํ–ฅ + +### ์ธ๋ฑ์Šค ์ตœ์ ํ™” + +```sql +-- ๋ณตํ•ฉ ์œ ๋‹ˆํฌ ์ธ๋ฑ์Šค (ํ•„์ˆ˜) +CREATE UNIQUE INDEX idx_table_column_type_company +ON table_type_columns(table_name, column_name, company_code); + +-- company_code ์ธ๋ฑ์Šค (์กฐํšŒ ์„ฑ๋Šฅ ํ–ฅ์ƒ) +CREATE INDEX idx_table_type_columns_company +ON table_type_columns(company_code); +``` + +### ์ฟผ๋ฆฌ ์„ฑ๋Šฅ + +- **์ด์ „**: `WHERE table_name = $1` (510๊ฑด ์Šค์บ”) +- **ํ˜„์žฌ**: `WHERE table_name = $1 AND company_code = $2` (255๊ฑด ์Šค์บ”) +- **๊ฒฐ๊ณผ**: ์•ฝ 2๋ฐฐ ์„ฑ๋Šฅ ํ–ฅ์ƒ โœ… + +--- + +## ๐ŸŽ‰ ๊ฒฐ๋ก  + +### ํ•ด๊ฒฐ๋œ ๋ฌธ์ œ + +- โœ… ํšŒ์‚ฌ๋ณ„๋กœ ๊ฐ™์€ ์ปฌ๋Ÿผ์— ๋‹ค๋ฅธ ์ž…๋ ฅ ํƒ€์ž… ์„ค์ • ๊ฐ€๋Šฅ +- โœ… ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์›์น™ ์ค€์ˆ˜ (๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ) +- โœ… ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”(`numbering_rules`, `table_column_category_values`)๊ณผ ์ผ๊ด€๋œ ๊ตฌ์กฐ +- โœ… ์ตœ๊ณ  ๊ด€๋ฆฌ์ž์™€ ์ผ๋ฐ˜ ํšŒ์‚ฌ ๊ถŒํ•œ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ + +### ๊ธฐ๋Œ€ ํšจ๊ณผ + +- **์œ ์—ฐ์„ฑ**: ๊ฐ ํšŒ์‚ฌ๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ด๋ธ” ์„ค์ • ๊ฐ€๋Šฅ +- **๋ณด์•ˆ**: ํšŒ์‚ฌ ๊ฐ„ ๋ฐ์ดํ„ฐ ์™„์ „ ๊ฒฉ๋ฆฌ +- **ํ™•์žฅ์„ฑ**: ์ƒˆ๋กœ์šด ํšŒ์‚ฌ ์ถ”๊ฐ€ ์‹œ ์ž๋™ ๋ฐ์ดํ„ฐ ๋ณต์ œ +- **์ผ๊ด€์„ฑ**: ์ „์ฒด ์‹œ์Šคํ…œ์˜ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํŒจํ„ด ํ†ต์ผ + +--- + +**์ž‘์„ฑ์ผ**: 2025-11-06 +**์ƒํƒœ**: ๐ŸŸข ์™„๋ฃŒ (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ๋Œ€๊ธฐ ์ค‘) +**๋‹ค์Œ ์ž‘์—…**: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ๋ฐ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ + diff --git a/frontend/components/admin/ExternalDbConnectionModal.tsx b/frontend/components/admin/ExternalDbConnectionModal.tsx index 540d8947..1d0c046f 100644 --- a/frontend/components/admin/ExternalDbConnectionModal.tsx +++ b/frontend/components/admin/ExternalDbConnectionModal.tsx @@ -607,7 +607,7 @@ export const ExternalDbConnectionModal: React.FC - +