diff --git a/backend-node/src/controllers/numberingRuleController.ts b/backend-node/src/controllers/numberingRuleController.ts index 556d09df..1b2e2197 100644 --- a/backend-node/src/controllers/numberingRuleController.ts +++ b/backend-node/src/controllers/numberingRuleController.ts @@ -27,12 +27,24 @@ router.get("/available/:menuObjid?", authenticateToken, async (req: Authenticate const companyCode = req.user!.companyCode; const menuObjid = req.params.menuObjid ? parseInt(req.params.menuObjid) : undefined; + logger.info("๐Ÿ“ฅ ๋ฉ”๋‰ด๋ณ„ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์š”์ฒญ", { companyCode, menuObjid }); + try { const rules = await numberingRuleService.getAvailableRulesForMenu(companyCode, menuObjid); + + logger.info("โœ… ๋ฉ”๋‰ด๋ณ„ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์„ฑ๊ณต (์ปจํŠธ๋กค๋Ÿฌ)", { + companyCode, + menuObjid, + rulesCount: rules.length + }); + return res.json({ success: true, data: rules }); } catch (error: any) { - logger.error("๋ฉ”๋‰ด๋ณ„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ", { + logger.error("โŒ ๋ฉ”๋‰ด๋ณ„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ (์ปจํŠธ๋กค๋Ÿฌ)", { error: error.message, + errorCode: error.code, + errorStack: error.stack, + companyCode, menuObjid, }); return res.status(500).json({ success: false, error: error.message }); diff --git a/backend-node/src/controllers/tableCategoryValueController.ts b/backend-node/src/controllers/tableCategoryValueController.ts index 865f7672..ffb6a5a4 100644 --- a/backend-node/src/controllers/tableCategoryValueController.ts +++ b/backend-node/src/controllers/tableCategoryValueController.ts @@ -32,18 +32,31 @@ export const getCategoryColumns = async (req: AuthenticatedRequest, res: Respons /** * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ชฉ๋ก ์กฐํšŒ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„ ์ ์šฉ) + * + * Query Parameters: + * - menuObjid: ๋ฉ”๋‰ด OBJID (์„ ํƒ์‚ฌํ•ญ, ์ œ๊ณต ์‹œ ํ˜•์ œ ๋ฉ”๋‰ด์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ํฌํ•จ) + * - includeInactive: ๋น„ํ™œ์„ฑ ๊ฐ’ ํฌํ•จ ์—ฌ๋ถ€ */ export const getCategoryValues = async (req: AuthenticatedRequest, res: Response) => { try { const companyCode = req.user!.companyCode; const { tableName, columnName } = req.params; const includeInactive = req.query.includeInactive === "true"; + const menuObjid = req.query.menuObjid ? Number(req.query.menuObjid) : undefined; + + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ ์š”์ฒญ", { + tableName, + columnName, + menuObjid, + companyCode, + }); const values = await tableCategoryValueService.getCategoryValues( tableName, columnName, companyCode, - includeInactive + includeInactive, + menuObjid // โ† menuObjid ์ „๋‹ฌ ); return res.json({ @@ -61,18 +74,37 @@ export const getCategoryValues = async (req: AuthenticatedRequest, res: Response }; /** - * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ + * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) + * + * Body: + * - menuObjid: ๋ฉ”๋‰ด OBJID (ํ•„์ˆ˜) + * - ๋‚˜๋จธ์ง€ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ •๋ณด */ export const addCategoryValue = async (req: AuthenticatedRequest, res: Response) => { try { const companyCode = req.user!.companyCode; const userId = req.user!.userId; - const value = req.body; + const { menuObjid, ...value } = req.body; + + if (!menuObjid) { + return res.status(400).json({ + success: false, + message: "menuObjid๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค", + }); + } + + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ ์š”์ฒญ", { + tableName: value.tableName, + columnName: value.columnName, + menuObjid, + companyCode, + }); const newValue = await tableCategoryValueService.addCategoryValue( value, companyCode, - userId + userId, + Number(menuObjid) // โ† menuObjid ์ „๋‹ฌ ); return res.status(201).json({ diff --git a/backend-node/src/services/menuService.ts b/backend-node/src/services/menuService.ts new file mode 100644 index 00000000..9a9be99c --- /dev/null +++ b/backend-node/src/services/menuService.ts @@ -0,0 +1,159 @@ +import { getPool } from "../database/db"; +import { logger } from "../utils/logger"; + +/** + * ๋ฉ”๋‰ด ๊ด€๋ จ ์œ ํ‹ธ๋ฆฌํ‹ฐ ์„œ๋น„์Šค + * + * ๋ฉ”๋‰ด ์Šค์ฝ”ํ”„ ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ๊ณต์œ ๋ฅผ ์œ„ํ•œ ํ˜•์ œ ๋ฉ”๋‰ด ์กฐํšŒ ๊ธฐ๋Šฅ ์ œ๊ณต + */ + +/** + * ๋ฉ”๋‰ด์˜ ํ˜•์ œ ๋ฉ”๋‰ด OBJID ๋ชฉ๋ก ์กฐํšŒ + * (๊ฐ™์€ ๋ถ€๋ชจ๋ฅผ ๊ฐ€์ง„ ๋ฉ”๋‰ด๋“ค) + * + * ๋ฉ”๋‰ด ์Šค์ฝ”ํ”„ ๊ทœ์น™: + * - ๊ฐ™์€ ๋ถ€๋ชจ๋ฅผ ๊ฐ€์ง„ ํ˜•์ œ ๋ฉ”๋‰ด๋“ค์€ ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ๊ทœ์น™์„ ๊ณต์œ  + * - ์ตœ์ƒ์œ„ ๋ฉ”๋‰ด(parent_obj_id = 0)๋Š” ์ž๊ธฐ ์ž์‹ ๋งŒ ๋ฐ˜ํ™˜ + * - ๋ฉ”๋‰ด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์œผ๋ฉด ์•ˆ์ „ํ•˜๊ฒŒ ์ž๊ธฐ ์ž์‹ ๋งŒ ๋ฐ˜ํ™˜ + * + * @param menuObjid ํ˜„์žฌ ๋ฉ”๋‰ด์˜ OBJID + * @returns ํ˜•์ œ ๋ฉ”๋‰ด OBJID ๋ฐฐ์—ด (์ž๊ธฐ ์ž์‹  ํฌํ•จ, ์ •๋ ฌ๋จ) + * + * @example + * // ์˜์—…๊ด€๋ฆฌ (200) + * // โ”œโ”€โ”€ ๊ณ ๊ฐ๊ด€๋ฆฌ (201) + * // โ”œโ”€โ”€ ๊ณ„์•ฝ๊ด€๋ฆฌ (202) + * // โ””โ”€โ”€ ์ฃผ๋ฌธ๊ด€๋ฆฌ (203) + * + * await getSiblingMenuObjids(201); + * // ๊ฒฐ๊ณผ: [201, 202, 203] - ๋ชจ๋‘ ๊ฐ™์€ ๋ถ€๋ชจ(200)๋ฅผ ๊ฐ€์ง„ ํ˜•์ œ + */ +export async function getSiblingMenuObjids(menuObjid: number): Promise { + const pool = getPool(); + + try { + logger.info("ํ˜•์ œ ๋ฉ”๋‰ด ์กฐํšŒ ์‹œ์ž‘", { menuObjid }); + + // 1. ํ˜„์žฌ ๋ฉ”๋‰ด์˜ ๋ถ€๋ชจ ์ฐพ๊ธฐ + const parentQuery = ` + SELECT parent_obj_id FROM menu_info WHERE objid = $1 + `; + const parentResult = await pool.query(parentQuery, [menuObjid]); + + if (parentResult.rows.length === 0) { + logger.warn("๋ฉ”๋‰ด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ, ์ž๊ธฐ ์ž์‹ ๋งŒ ๋ฐ˜ํ™˜", { menuObjid }); + return [menuObjid]; // ๋ฉ”๋‰ด๊ฐ€ ์—†์œผ๋ฉด ์•ˆ์ „ํ•˜๊ฒŒ ์ž๊ธฐ ์ž์‹ ๋งŒ ๋ฐ˜ํ™˜ + } + + const parentObjId = parentResult.rows[0].parent_obj_id; + + if (!parentObjId || parentObjId === 0) { + // ์ตœ์ƒ์œ„ ๋ฉ”๋‰ด์ธ ๊ฒฝ์šฐ ์ž๊ธฐ ์ž์‹ ๋งŒ ๋ฐ˜ํ™˜ + logger.info("์ตœ์ƒ์œ„ ๋ฉ”๋‰ด (ํ˜•์ œ ์—†์Œ)", { menuObjid, parentObjId }); + return [menuObjid]; + } + + // 2. ๊ฐ™์€ ๋ถ€๋ชจ๋ฅผ ๊ฐ€์ง„ ํ˜•์ œ ๋ฉ”๋‰ด๋“ค ์กฐํšŒ + const siblingsQuery = ` + SELECT objid FROM menu_info + WHERE parent_obj_id = $1 + ORDER BY objid + `; + const siblingsResult = await pool.query(siblingsQuery, [parentObjId]); + + const siblingObjids = siblingsResult.rows.map((row) => Number(row.objid)); + + logger.info("ํ˜•์ œ ๋ฉ”๋‰ด ์กฐํšŒ ์™„๋ฃŒ", { + menuObjid, + parentObjId, + siblingCount: siblingObjids.length, + siblings: siblingObjids, + }); + + return siblingObjids; + } catch (error: any) { + logger.error("ํ˜•์ œ ๋ฉ”๋‰ด ์กฐํšŒ ์‹คํŒจ", { + menuObjid, + error: error.message, + stack: error.stack + }); + // ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์•ˆ์ „ํ•˜๊ฒŒ ์ž๊ธฐ ์ž์‹ ๋งŒ ๋ฐ˜ํ™˜ + return [menuObjid]; + } +} + +/** + * ์—ฌ๋Ÿฌ ๋ฉ”๋‰ด์˜ ํ˜•์ œ ๋ฉ”๋‰ด OBJID ํ•ฉ์ง‘ํ•ฉ ์กฐํšŒ + * + * ์—ฌ๋Ÿฌ ๋ฉ”๋‰ด์— ์†ํ•œ ๋ชจ๋“  ํ˜•์ œ ๋ฉ”๋‰ด๋ฅผ ์ค‘๋ณต ์ œ๊ฑฐํ•˜์—ฌ ๋ฐ˜ํ™˜ + * + * @param menuObjids ๋ฉ”๋‰ด OBJID ๋ฐฐ์—ด + * @returns ๋ชจ๋“  ํ˜•์ œ ๋ฉ”๋‰ด OBJID ๋ฐฐ์—ด (์ค‘๋ณต ์ œ๊ฑฐ, ์ •๋ ฌ๋จ) + * + * @example + * // ์„œ๋กœ ๋‹ค๋ฅธ ๋ถ€๋ชจ๋ฅผ ๊ฐ€์ง„ ๋ฉ”๋‰ด๋“ค์˜ ํ˜•์ œ๋ฅผ ๋ชจ๋‘ ์กฐํšŒ + * await getAllSiblingMenuObjids([201, 301]); + * // 201์˜ ํ˜•์ œ: [201, 202, 203] + * // 301์˜ ํ˜•์ œ: [301, 302] + * // ๊ฒฐ๊ณผ: [201, 202, 203, 301, 302] + */ +export async function getAllSiblingMenuObjids( + menuObjids: number[] +): Promise { + if (!menuObjids || menuObjids.length === 0) { + logger.warn("getAllSiblingMenuObjids: ๋นˆ ๋ฐฐ์—ด ์ž…๋ ฅ"); + return []; + } + + const allSiblings = new Set(); + + for (const objid of menuObjids) { + const siblings = await getSiblingMenuObjids(objid); + siblings.forEach((s) => allSiblings.add(s)); + } + + const result = Array.from(allSiblings).sort((a, b) => a - b); + + logger.info("์—ฌ๋Ÿฌ ๋ฉ”๋‰ด์˜ ํ˜•์ œ ์กฐํšŒ ์™„๋ฃŒ", { + inputMenus: menuObjids, + resultCount: result.length, + result, + }); + + return result; +} + +/** + * ๋ฉ”๋‰ด ์ •๋ณด ์กฐํšŒ + * + * @param menuObjid ๋ฉ”๋‰ด OBJID + * @returns ๋ฉ”๋‰ด ์ •๋ณด (์—†์œผ๋ฉด null) + */ +export async function getMenuInfo(menuObjid: number): Promise { + const pool = getPool(); + + try { + const query = ` + SELECT + objid, + parent_obj_id AS "parentObjId", + menu_name_kor AS "menuNameKor", + menu_name_eng AS "menuNameEng", + menu_url AS "menuUrl", + company_code AS "companyCode" + FROM menu_info + WHERE objid = $1 + `; + const result = await pool.query(query, [menuObjid]); + + if (result.rows.length === 0) { + return null; + } + + return result.rows[0]; + } catch (error: any) { + logger.error("๋ฉ”๋‰ด ์ •๋ณด ์กฐํšŒ ์‹คํŒจ", { menuObjid, error: error.message }); + return null; + } +} + diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index 98230b65..2c89f188 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -4,6 +4,7 @@ import { getPool } from "../database/db"; import { logger } from "../utils/logger"; +import { getSiblingMenuObjids } from "./menuService"; interface NumberingRulePart { id?: number; @@ -150,22 +151,33 @@ class NumberingRuleService { } /** - * ํ˜„์žฌ ๋ฉ”๋‰ด์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ + * ํ˜„์žฌ ๋ฉ”๋‰ด์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ทœ์น™ ๋ชฉ๋ก ์กฐํšŒ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) + * + * ๋ฉ”๋‰ด ์Šค์ฝ”ํ”„ ๊ทœ์น™: + * - menuObjid๊ฐ€ ์ œ๊ณต๋˜๋ฉด ํ˜•์ œ ๋ฉ”๋‰ด์˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ํฌํ•จ + * - ์šฐ์„ ์ˆœ์œ„: menu (ํ˜•์ œ ๋ฉ”๋‰ด) > table > global */ async getAvailableRulesForMenu( companyCode: string, menuObjid?: number ): Promise { try { - logger.info("๋ฉ”๋‰ด๋ณ„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹œ์ž‘", { + logger.info("๋ฉ”๋‰ด๋ณ„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹œ์ž‘ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„)", { companyCode, menuObjid, }); const pool = getPool(); + // 1. ํ˜•์ œ ๋ฉ”๋‰ด OBJID ์กฐํšŒ + let siblingObjids: number[] = []; + if (menuObjid) { + siblingObjids = await getSiblingMenuObjids(menuObjid); + logger.info("ํ˜•์ œ ๋ฉ”๋‰ด OBJID ๋ชฉ๋ก", { menuObjid, siblingObjids }); + } + // menuObjid๊ฐ€ ์—†์œผ๋ฉด global ๊ทœ์น™๋งŒ ๋ฐ˜ํ™˜ - if (!menuObjid) { + if (!menuObjid || siblingObjids.length === 0) { let query: string; let params: any[]; @@ -261,35 +273,13 @@ class NumberingRuleService { return result.rows; } - // ํ˜„์žฌ ๋ฉ”๋‰ด์˜ ์ƒ์œ„ ๊ณ„์ธต ์กฐํšŒ (2๋ ˆ๋ฒจ ๋ฉ”๋‰ด ์ฐพ๊ธฐ) - const menuHierarchyQuery = ` - WITH RECURSIVE menu_path AS ( - SELECT objid, objid_parent, menu_level - FROM menu_info - WHERE objid = $1 - - UNION ALL - - SELECT mi.objid, mi.objid_parent, mi.menu_level - FROM menu_info mi - INNER JOIN menu_path mp ON mi.objid = mp.objid_parent - ) - SELECT objid, menu_level - FROM menu_path - WHERE menu_level = 2 - LIMIT 1 - `; - - const hierarchyResult = await pool.query(menuHierarchyQuery, [menuObjid]); - const level2MenuObjid = - hierarchyResult.rowCount > 0 ? hierarchyResult.rows[0].objid : null; - - // ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ทœ์น™ ์กฐํšŒ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ ์šฉ) + // 2. ๋ฉ”๋‰ด ์Šค์ฝ”ํ”„: ํ˜•์ œ ๋ฉ”๋‰ด์˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ + // ์šฐ์„ ์ˆœ์œ„: menu (ํ˜•์ œ ๋ฉ”๋‰ด) > table > global let query: string; let params: any[]; if (companyCode === "*") { - // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ๊ทœ์น™ ์กฐํšŒ + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ๊ทœ์น™ ์กฐํšŒ (ํ˜•์ œ ๋ฉ”๋‰ด ํฌํ•จ) query = ` SELECT rule_id AS "ruleId", @@ -309,12 +299,20 @@ class NumberingRuleService { FROM numbering_rules WHERE scope_type = 'global' - OR (scope_type = 'menu' AND menu_objid = $1) - ORDER BY scope_type DESC, created_at DESC + OR scope_type = 'table' + OR (scope_type = 'menu' AND menu_objid = ANY($1)) + ORDER BY + CASE scope_type + WHEN 'menu' THEN 1 + WHEN 'table' THEN 2 + WHEN 'global' THEN 3 + END, + created_at DESC `; - params = [level2MenuObjid]; + params = [siblingObjids]; + logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ํ˜•์ œ ๋ฉ”๋‰ด ํฌํ•จ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ", { siblingObjids }); } else { - // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ๊ทœ์น™๋งŒ ์กฐํšŒ + // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ๊ทœ์น™๋งŒ ์กฐํšŒ (ํ˜•์ œ ๋ฉ”๋‰ด ํฌํ•จ) query = ` SELECT rule_id AS "ruleId", @@ -335,58 +333,91 @@ class NumberingRuleService { WHERE company_code = $1 AND ( scope_type = 'global' - OR (scope_type = 'menu' AND menu_objid = $2) + OR scope_type = 'table' + OR (scope_type = 'menu' AND menu_objid = ANY($2)) ) - ORDER BY scope_type DESC, created_at DESC + ORDER BY + CASE scope_type + WHEN 'menu' THEN 1 + WHEN 'table' THEN 2 + WHEN 'global' THEN 3 + END, + created_at DESC `; - params = [companyCode, level2MenuObjid]; + params = [companyCode, siblingObjids]; + logger.info("ํšŒ์‚ฌ๋ณ„: ํ˜•์ œ ๋ฉ”๋‰ด ํฌํ•จ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ", { companyCode, siblingObjids }); } + logger.info("๐Ÿ” ์ฑ„๋ฒˆ ๊ทœ์น™ ์ฟผ๋ฆฌ ์‹คํ–‰", { + queryPreview: query.substring(0, 200), + paramsTypes: params.map(p => typeof p), + paramsValues: params, + }); + const result = await pool.query(query, params); + logger.info("โœ… ์ฑ„๋ฒˆ ๊ทœ์น™ ์ฟผ๋ฆฌ ์„ฑ๊ณต", { rowCount: result.rows.length }); + // ํŒŒํŠธ ์ •๋ณด ์ถ”๊ฐ€ for (const rule of result.rows) { - 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]; - } + try { + 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, partsParams); - rule.parts = partsResult.rows; + const partsResult = await pool.query(partsQuery, partsParams); + rule.parts = partsResult.rows; + + logger.info("โœ… ๊ทœ์น™ ํŒŒํŠธ ์กฐํšŒ ์„ฑ๊ณต", { + ruleId: rule.ruleId, + ruleName: rule.ruleName, + partsCount: partsResult.rows.length, + }); + } catch (partError: any) { + logger.error("โŒ ๊ทœ์น™ ํŒŒํŠธ ์กฐํšŒ ์‹คํŒจ", { + ruleId: rule.ruleId, + ruleName: rule.ruleName, + error: partError.message, + errorCode: partError.code, + errorStack: partError.stack, + }); + throw partError; + } } logger.info("๋ฉ”๋‰ด๋ณ„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์™„๋ฃŒ", { companyCode, menuObjid, - level2MenuObjid, + siblingCount: siblingObjids.length, count: result.rowCount, }); @@ -394,8 +425,11 @@ class NumberingRuleService { } catch (error: any) { logger.error("๋ฉ”๋‰ด๋ณ„ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹คํŒจ", { error: error.message, + errorCode: error.code, + errorStack: error.stack, companyCode, menuObjid, + siblingObjids: siblingObjids || [], }); throw error; } diff --git a/backend-node/src/services/tableCategoryValueService.ts b/backend-node/src/services/tableCategoryValueService.ts index 7646dead..29cad453 100644 --- a/backend-node/src/services/tableCategoryValueService.ts +++ b/backend-node/src/services/tableCategoryValueService.ts @@ -1,5 +1,6 @@ import { getPool } from "../database/db"; import { logger } from "../utils/logger"; +import { getSiblingMenuObjids } from "./menuService"; import { TableCategoryValue, CategoryColumn, @@ -79,84 +80,164 @@ class TableCategoryValueService { } /** - * ํŠน์ • ์ปฌ๋Ÿผ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ชฉ๋ก ์กฐํšŒ (ํ…Œ์ด๋ธ” ์Šค์ฝ”ํ”„) + * ํŠน์ • ์ปฌ๋Ÿผ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ชฉ๋ก ์กฐํšŒ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) + * + * ๋ฉ”๋‰ด ์Šค์ฝ”ํ”„ ๊ทœ์น™: + * - menuObjid๊ฐ€ ์ œ๊ณต๋˜๋ฉด ํ•ด๋‹น ๋ฉ”๋‰ด์™€ ํ˜•์ œ ๋ฉ”๋‰ด์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์„ ์กฐํšŒ + * - menuObjid๊ฐ€ ์—†์œผ๋ฉด ํ…Œ์ด๋ธ” ์Šค์ฝ”ํ”„๋กœ ๋™์ž‘ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ) */ async getCategoryValues( tableName: string, columnName: string, companyCode: string, - includeInactive: boolean = false + includeInactive: boolean = false, + menuObjid?: number ): Promise { try { - logger.info("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ชฉ๋ก ์กฐํšŒ", { + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ชฉ๋ก ์กฐํšŒ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„)", { tableName, columnName, companyCode, includeInactive, + menuObjid, }); const pool = getPool(); - // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ company_code="*" ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Œ + // 1. ๋ฉ”๋‰ด ์Šค์ฝ”ํ”„: ํ˜•์ œ ๋ฉ”๋‰ด OBJID ์กฐํšŒ + let siblingObjids: number[] = []; + if (menuObjid) { + siblingObjids = await getSiblingMenuObjids(menuObjid); + logger.info("ํ˜•์ œ ๋ฉ”๋‰ด OBJID ๋ชฉ๋ก", { menuObjid, siblingObjids }); + } + + // 2. ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ (ํ˜•์ œ ๋ฉ”๋‰ด ํฌํ•จ) 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]; + if (menuObjid && siblingObjids.length > 0) { + // ๋ฉ”๋‰ด ์Šค์ฝ”ํ”„ ์ ์šฉ + 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", + menu_objid AS "menuObjid", + 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 menu_objid = ANY($3) + `; + params = [tableName, columnName, siblingObjids]; + } 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", + menu_objid AS "menuObjid", + 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]; + if (menuObjid && siblingObjids.length > 0) { + // ๋ฉ”๋‰ด ์Šค์ฝ”ํ”„ ์ ์šฉ + 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", + menu_objid AS "menuObjid", + 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 menu_objid = ANY($3) + AND company_code = $4 + `; + params = [tableName, columnName, siblingObjids, companyCode]; + } 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", + menu_objid AS "menuObjid", + 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 }); } @@ -175,6 +256,8 @@ class TableCategoryValueService { tableName, columnName, companyCode, + menuObjid, + scopeType: menuObjid ? "menu" : "table", }); return values; @@ -185,17 +268,31 @@ class TableCategoryValueService { } /** - * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ + * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) + * + * @param value ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ •๋ณด + * @param companyCode ํšŒ์‚ฌ ์ฝ”๋“œ + * @param userId ์ƒ์„ฑ์ž ID + * @param menuObjid ๋ฉ”๋‰ด OBJID (ํ•„์ˆ˜) */ async addCategoryValue( value: TableCategoryValue, companyCode: string, - userId: string + userId: string, + menuObjid: number ): Promise { const pool = getPool(); try { - // ์ค‘๋ณต ์ฝ”๋“œ ์ฒดํฌ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ ์šฉ) + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„)", { + tableName: value.tableName, + columnName: value.columnName, + valueCode: value.valueCode, + menuObjid, + companyCode, + }); + + // ์ค‘๋ณต ์ฝ”๋“œ ์ฒดํฌ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ + ๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) let duplicateQuery: string; let duplicateParams: any[]; @@ -207,8 +304,9 @@ class TableCategoryValueService { WHERE table_name = $1 AND column_name = $2 AND value_code = $3 + AND menu_objid = $4 `; - duplicateParams = [value.tableName, value.columnName, value.valueCode]; + duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid]; } else { // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ํšŒ์‚ฌ์—์„œ๋งŒ ์ค‘๋ณต ์ฒดํฌ duplicateQuery = ` @@ -217,9 +315,10 @@ class TableCategoryValueService { WHERE table_name = $1 AND column_name = $2 AND value_code = $3 - AND company_code = $4 + AND menu_objid = $4 + AND company_code = $5 `; - duplicateParams = [value.tableName, value.columnName, value.valueCode, companyCode]; + duplicateParams = [value.tableName, value.columnName, value.valueCode, menuObjid, companyCode]; } const duplicateResult = await pool.query(duplicateQuery, duplicateParams); @@ -232,8 +331,8 @@ class TableCategoryValueService { INSERT INTO table_column_category_values ( table_name, column_name, value_code, value_label, value_order, parent_value_id, depth, description, color, icon, - is_active, is_default, company_code, created_by - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) + is_active, is_default, company_code, menu_objid, created_by + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING value_id AS "valueId", table_name AS "tableName", @@ -249,6 +348,7 @@ class TableCategoryValueService { is_active AS "isActive", is_default AS "isDefault", company_code AS "companyCode", + menu_objid AS "menuObjid", created_at AS "createdAt", created_by AS "createdBy" `; @@ -267,6 +367,7 @@ class TableCategoryValueService { value.isActive !== false, value.isDefault || false, companyCode, + menuObjid, // โ† ๋ฉ”๋‰ด OBJID ์ €์žฅ userId, ]); @@ -274,6 +375,7 @@ class TableCategoryValueService { valueId: result.rows[0].valueId, tableName: value.tableName, columnName: value.columnName, + menuObjid, }); return result.rows[0]; diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index d7ea2039..5c2c587b 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useEffect, useState } from "react"; -import { useParams } from "next/navigation"; +import { useParams, useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Loader2 } from "lucide-react"; import { screenApi } from "@/lib/api/screen"; @@ -21,8 +21,12 @@ import { useResponsive } from "@/lib/hooks/useResponsive"; // ๐Ÿ†• ๋ฐ˜์‘ํ˜• ๊ฐ export default function ScreenViewPage() { const params = useParams(); + const searchParams = useSearchParams(); const router = useRouter(); const screenId = parseInt(params.screenId as string); + + // URL ์ฟผ๋ฆฌ์—์„œ menuObjid ๊ฐ€์ ธ์˜ค๊ธฐ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) + const menuObjid = searchParams.get("menuObjid") ? parseInt(searchParams.get("menuObjid")!) : undefined; // ๐Ÿ†• ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ์ •๋ณด const { user, userName, companyCode } = useAuth(); @@ -404,6 +408,7 @@ export default function ScreenViewPage() { userId={user?.userId} userName={userName} companyCode={companyCode} + menuObjid={menuObjid} selectedRowsData={selectedRowsData} sortBy={tableSortBy} sortOrder={tableSortOrder} diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx index 738aad79..252f5403 100644 --- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx +++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx @@ -26,6 +26,7 @@ interface NumberingRuleDesignerProps { isPreview?: boolean; className?: string; currentTableName?: string; // ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… (์ž๋™ ๊ฐ์ง€์šฉ) + menuObjid?: number; // ํ˜„์žฌ ๋ฉ”๋‰ด OBJID (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) } export const NumberingRuleDesigner: React.FC = ({ @@ -36,6 +37,7 @@ export const NumberingRuleDesigner: React.FC = ({ isPreview = false, className = "", currentTableName, + menuObjid, }) => { const [savedRules, setSavedRules] = useState([]); const [selectedRuleId, setSelectedRuleId] = useState(null); @@ -53,7 +55,7 @@ export const NumberingRuleDesigner: React.FC = ({ const loadRules = useCallback(async () => { setLoading(true); try { - const response = await getNumberingRules(); + const response = await getNumberingRules(menuObjid); if (response.success && response.data) { setSavedRules(response.data); } else { @@ -64,7 +66,7 @@ export const NumberingRuleDesigner: React.FC = ({ } finally { setLoading(false); } - }, []); + }, [menuObjid]); useEffect(() => { if (currentRule) { @@ -145,7 +147,7 @@ export const NumberingRuleDesigner: React.FC = ({ "currentRule.tableName": currentRule.tableName, "ruleToSave.tableName": ruleToSave.tableName, "ruleToSave.scopeType": ruleToSave.scopeType, - ruleToSave + ruleToSave, }); let response; @@ -214,7 +216,7 @@ export const NumberingRuleDesigner: React.FC = ({ const handleNewRule = useCallback(() => { console.log("๐Ÿ“‹ ์ƒˆ ๊ทœ์น™ ์ƒ์„ฑ - currentTableName:", currentTableName); - + const newRule: NumberingRuleConfig = { ruleId: `rule-${Date.now()}`, ruleName: "์ƒˆ ์ฑ„๋ฒˆ ๊ทœ์น™", @@ -227,7 +229,7 @@ export const NumberingRuleDesigner: React.FC = ({ }; console.log("๐Ÿ“‹ ์ƒ์„ฑ๋œ ๊ทœ์น™ ์ •๋ณด:", newRule); - + setSelectedRuleId(newRule.ruleId); setCurrentRule(newRule); @@ -273,7 +275,7 @@ export const NumberingRuleDesigner: React.FC = ({ savedRules.map((rule) => ( handleSelectRule(rule)} @@ -356,7 +358,7 @@ export const NumberingRuleDesigner: React.FC = ({ {currentTableName && (
-
+
{currentTableName}

diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index fa5dc755..1f220586 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -41,6 +41,7 @@ interface RealtimePreviewProps { userId?: string; // ๐Ÿ†• ํ˜„์žฌ ์‚ฌ์šฉ์ž ID userName?: string; // ๐Ÿ†• ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ด๋ฆ„ companyCode?: string; // ๐Ÿ†• ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ํšŒ์‚ฌ ์ฝ”๋“œ + menuObjid?: number; // ๐Ÿ†• ํ˜„์žฌ ๋ฉ”๋‰ด OBJID (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) selectedRowsData?: any[]; onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void; flowSelectedData?: any[]; @@ -107,6 +108,7 @@ export const RealtimePreviewDynamic: React.FC = ({ userId, // ๐Ÿ†• ์‚ฌ์šฉ์ž ID userName, // ๐Ÿ†• ์‚ฌ์šฉ์ž ์ด๋ฆ„ companyCode, // ๐Ÿ†• ํšŒ์‚ฌ ์ฝ”๋“œ + menuObjid, // ๐Ÿ†• ๋ฉ”๋‰ด OBJID selectedRowsData, onSelectedRowsChange, flowSelectedData, @@ -344,6 +346,7 @@ export const RealtimePreviewDynamic: React.FC = ({ userId={userId} userName={userName} companyCode={companyCode} + menuObjid={menuObjid} selectedRowsData={selectedRowsData} onSelectedRowsChange={onSelectedRowsChange} flowSelectedData={flowSelectedData} diff --git a/frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx b/frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx index 2e1f5087..cef462b9 100644 --- a/frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx +++ b/frontend/components/screen/panels/webtype-configs/TextTypeConfigPanel.tsx @@ -14,7 +14,7 @@ interface TextTypeConfigPanelProps { config: TextTypeConfig; onConfigChange: (config: TextTypeConfig) => void; tableName?: string; // ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… (์„ ํƒ) - menuObjid?: number; // ๋ฉ”๋‰ด objid (์„ ํƒ) + menuObjid?: number; // ๋ฉ”๋‰ด objid (์„ ํƒ) - ์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒํ•œ ๋ถ€๋ชจ ๋ฉ”๋‰ด } export const TextTypeConfigPanel: React.FC = ({ @@ -44,6 +44,10 @@ export const TextTypeConfigPanel: React.FC = ({ // ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์ƒํƒœ const [numberingRules, setNumberingRules] = useState([]); const [loadingRules, setLoadingRules] = useState(false); + + // ๋ถ€๋ชจ ๋ฉ”๋‰ด ๋ชฉ๋ก ์ƒํƒœ (์ฑ„๋ฒˆ๊ทœ์น™ ์‚ฌ์šฉ์„ ์œ„ํ•œ ์„ ํƒ) + const [parentMenus, setParentMenus] = useState([]); + const [selectedMenuObjid, setSelectedMenuObjid] = useState(menuObjid); // ๋กœ์ปฌ ์ƒํƒœ๋กœ ์‹ค์‹œ๊ฐ„ ์ž…๋ ฅ ๊ด€๋ฆฌ const [localValues, setLocalValues] = useState({ @@ -60,31 +64,61 @@ export const TextTypeConfigPanel: React.FC = ({ numberingRuleId: safeConfig.numberingRuleId, }); - // ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ๋กœ๋“œ + // ๋ถ€๋ชจ ๋ฉ”๋‰ด ๋ชฉ๋ก ๋กœ๋“œ (์ตœ์ƒ์œ„ ๋ฉ”๋‰ด ๋˜๋Š” ๋ ˆ๋ฒจ 2 ๋ฉ”๋‰ด) + useEffect(() => { + const loadParentMenus = async () => { + try { + const { apiClient } = await import("@/lib/api/client"); + + // ๊ด€๋ฆฌ์ž ๋ฉ”๋‰ด์™€ ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด ๋ชจ๋‘ ๊ฐ€์ ธ์˜ค๊ธฐ + const [adminResponse, userResponse] = await Promise.all([ + apiClient.get("/admin/menus", { params: { menuType: "0" } }), + apiClient.get("/admin/menus", { params: { menuType: "1" } }) + ]); + + const allMenus = [ + ...(adminResponse.data?.data || []), + ...(userResponse.data?.data || []) + ]; + + // ๋ ˆ๋ฒจ 2 ์ดํ•˜ ๋ฉ”๋‰ด๋งŒ ์„ ํƒ ๊ฐ€๋Šฅ (๋ถ€๋ชจ๊ฐ€ ์žˆ๋Š” ๋ฉ”๋‰ด) + const parentMenuList = allMenus.filter((menu: any) => { + const level = menu.lev || menu.LEV || 0; + return level >= 2; // ๋ ˆ๋ฒจ 2 ์ด์ƒ๋งŒ ํ‘œ์‹œ (ํ˜•์ œ ๋ฉ”๋‰ด๊ฐ€ ์žˆ์„ ๊ฐ€๋Šฅ์„ฑ) + }); + + setParentMenus(parentMenuList); + console.log("โœ… ๋ถ€๋ชจ ๋ฉ”๋‰ด ๋ชฉ๋ก ๋กœ๋“œ:", parentMenuList.length); + } catch (error) { + console.error("โŒ ๋ถ€๋ชจ ๋ฉ”๋‰ด ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:", error); + } + }; + + loadParentMenus(); + }, []); + + // ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ๋กœ๋“œ (์„ ํƒ๋œ ๋ฉ”๋‰ด ๊ธฐ์ค€) useEffect(() => { const loadRules = async () => { console.log("๐Ÿ”„ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋กœ๋“œ ์‹œ์ž‘:", { autoValueType: localValues.autoValueType, + selectedMenuObjid, tableName, - hasTableName: !!tableName, }); + // ๋ฉ”๋‰ด๋ฅผ ์„ ํƒํ•˜์ง€ ์•Š์œผ๋ฉด ๋กœ๋“œํ•˜์ง€ ์•Š์Œ + if (!selectedMenuObjid) { + console.warn("โš ๏ธ ๋ฉ”๋‰ด๋ฅผ ์„ ํƒํ•ด์•ผ ์ฑ„๋ฒˆ ๊ทœ์น™์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค"); + setNumberingRules([]); + return; + } + setLoadingRules(true); try { - let response; - - // ํ…Œ์ด๋ธ”๋ช…์ด ์žˆ์œผ๋ฉด ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง ์‚ฌ์šฉ - if (tableName) { - console.log("๐Ÿ“‹ ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ API ํ˜ธ์ถœ:", { tableName }); - response = await getAvailableNumberingRulesForScreen(tableName); - console.log("๐Ÿ“‹ API ์‘๋‹ต:", response); - } else { - // ํ…Œ์ด๋ธ”๋ช…์ด ์—†์œผ๋ฉด ๋นˆ ๋ฐฐ์—ด (ํ…Œ์ด๋ธ” ํ•„์ˆ˜) - console.warn("โš ๏ธ ํ…Œ์ด๋ธ”๋ช…์ด ์—†์–ด ์ฑ„๋ฒˆ ๊ทœ์น™์„ ์กฐํšŒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"); - setNumberingRules([]); - setLoadingRules(false); - return; - } + // ์„ ํƒ๋œ ๋ฉ”๋‰ด์˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) + console.log("๐Ÿ“‹ ๋ฉ”๋‰ด ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ API ํ˜ธ์ถœ:", { menuObjid: selectedMenuObjid }); + const response = await getAvailableNumberingRules(selectedMenuObjid); + console.log("๐Ÿ“‹ API ์‘๋‹ต:", response); if (response.success && response.data) { setNumberingRules(response.data); @@ -93,7 +127,7 @@ export const TextTypeConfigPanel: React.FC = ({ rules: response.data.map((r: any) => ({ ruleId: r.ruleId, ruleName: r.ruleName, - tableName: r.tableName, + menuObjid: selectedMenuObjid, })), }); } else { @@ -115,7 +149,7 @@ export const TextTypeConfigPanel: React.FC = ({ } else { console.log("โญ๏ธ autoValueType !== 'numbering_rule', ๊ทœ์น™ ๋กœ๋“œ ์Šคํ‚ต:", localValues.autoValueType); } - }, [localValues.autoValueType, tableName]); + }, [localValues.autoValueType, selectedMenuObjid]); // config๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ๋กœ์ปฌ ์ƒํƒœ ๋™๊ธฐํ™” useEffect(() => { @@ -314,37 +348,95 @@ export const TextTypeConfigPanel: React.FC = ({

+ {(() => { + console.log("๐Ÿ” ๋ฉ”๋‰ด ์„ ํƒ UI ๋ Œ๋”๋ง ์ฒดํฌ:", { + autoValueType: localValues.autoValueType, + isNumberingRule: localValues.autoValueType === "numbering_rule", + parentMenusCount: parentMenus.length, + selectedMenuObjid, + }); + return null; + })()} + {localValues.autoValueType === "numbering_rule" && ( -
- - setSelectedMenuObjid(parseInt(value))} + > + + + + + {parentMenus.length === 0 ? ( + + ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฉ”๋‰ด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค - )) - )} - - -

- ํ˜„์žฌ ๋ฉ”๋‰ด์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ฑ„๋ฒˆ ๊ทœ์น™๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค -

-
+ ) : ( + parentMenus.map((menu) => { + const objid = menu.objid || menu.OBJID; + const menuName = menu.menu_name_kor || menu.MENU_NAME_KOR; + return ( + + {menuName} + + ); + }) + )} + + +

+ ์ด ํ•„๋“œ๊ฐ€ ์–ด๋А ๋ฉ”๋‰ด์—์„œ ์‚ฌ์šฉ๋  ๊ฒƒ์ธ์ง€ ์„ ํƒํ•˜์„ธ์š” +

+
+ + {/* ์ฑ„๋ฒˆ ๊ทœ์น™ ์„ ํƒ */} +
+ + +

+ ์„ ํƒํ•œ ๋ฉ”๋‰ด์™€ ํ˜•์ œ ๋ฉ”๋‰ด์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ฑ„๋ฒˆ ๊ทœ์น™๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค +

+
+ )} {localValues.autoValueType === "custom" && ( diff --git a/frontend/components/screen/widgets/CategoryWidget.tsx b/frontend/components/screen/widgets/CategoryWidget.tsx index 54c8f98b..3e1c7f7b 100644 --- a/frontend/components/screen/widgets/CategoryWidget.tsx +++ b/frontend/components/screen/widgets/CategoryWidget.tsx @@ -8,14 +8,15 @@ import { GripVertical } from "lucide-react"; interface CategoryWidgetProps { widgetId: string; tableName: string; // ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ” + menuObjid?: number; // ํ˜„์žฌ ๋ฉ”๋‰ด OBJID (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) } /** * ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ ์œ„์ ฏ (์ขŒ์šฐ ๋ถ„ํ• ) * - ์ขŒ์ธก: ํ˜„์žฌ ํ…Œ์ด๋ธ”์˜ ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ์ปฌ๋Ÿผ ๋ชฉ๋ก - * - ์šฐ์ธก: ์„ ํƒ๋œ ์ปฌ๋Ÿผ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๊ด€๋ฆฌ (ํ…Œ์ด๋ธ” ์Šค์ฝ”ํ”„) + * - ์šฐ์ธก: ์„ ํƒ๋œ ์ปฌ๋Ÿผ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๊ด€๋ฆฌ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) */ -export function CategoryWidget({ widgetId, tableName }: CategoryWidgetProps) { +export function CategoryWidget({ widgetId, tableName, menuObjid }: CategoryWidgetProps) { const [selectedColumn, setSelectedColumn] = useState<{ columnName: string; columnLabel: string; @@ -69,6 +70,7 @@ export function CategoryWidget({ widgetId, tableName }: CategoryWidgetProps) { onColumnSelect={(columnName, columnLabel) => setSelectedColumn({ columnName, columnLabel }) } + menuObjid={menuObjid} /> @@ -87,6 +89,7 @@ export function CategoryWidget({ widgetId, tableName }: CategoryWidgetProps) { tableName={tableName} columnName={selectedColumn.columnName} columnLabel={selectedColumn.columnLabel} + menuObjid={menuObjid} /> ) : (
diff --git a/frontend/components/table-category/CategoryColumnList.tsx b/frontend/components/table-category/CategoryColumnList.tsx index 7b7ebd32..db6af71c 100644 --- a/frontend/components/table-category/CategoryColumnList.tsx +++ b/frontend/components/table-category/CategoryColumnList.tsx @@ -16,13 +16,14 @@ interface CategoryColumnListProps { tableName: string; selectedColumn: string | null; onColumnSelect: (columnName: string, columnLabel: string) => void; + menuObjid?: number; // ํ˜„์žฌ ๋ฉ”๋‰ด OBJID (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) } /** * ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ ๋ชฉ๋ก (์ขŒ์ธก ํŒจ๋„) - * - ํ˜„์žฌ ํ…Œ์ด๋ธ”์—์„œ input_type='category'์ธ ์ปฌ๋Ÿผ๋“ค์„ ํ‘œ์‹œ (ํ…Œ์ด๋ธ” ์Šค์ฝ”ํ”„) + * - ํ˜„์žฌ ํ…Œ์ด๋ธ”์—์„œ input_type='category'์ธ ์ปฌ๋Ÿผ๋“ค์„ ํ‘œ์‹œ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) */ -export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect }: CategoryColumnListProps) { +export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect, menuObjid }: CategoryColumnListProps) { const [columns, setColumns] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -89,7 +90,7 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect } // ๊ฐ ์ปฌ๋Ÿผ์˜ ๊ฐ’ ๊ฐœ์ˆ˜ ๊ฐ€์ ธ์˜ค๊ธฐ let valueCount = 0; try { - const valuesResult = await getCategoryValues(tableName, colName, false); + const valuesResult = await getCategoryValues(tableName, colName, false, menuObjid); if (valuesResult.success && valuesResult.data) { valueCount = valuesResult.data.length; } diff --git a/frontend/components/table-category/CategoryValueManager.tsx b/frontend/components/table-category/CategoryValueManager.tsx index 0b7eecc3..dfcbd045 100644 --- a/frontend/components/table-category/CategoryValueManager.tsx +++ b/frontend/components/table-category/CategoryValueManager.tsx @@ -29,6 +29,7 @@ interface CategoryValueManagerProps { columnName: string; columnLabel: string; onValueCountChange?: (count: number) => void; + menuObjid?: number; // ํ˜„์žฌ ๋ฉ”๋‰ด OBJID (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) } export const CategoryValueManager: React.FC = ({ @@ -36,6 +37,7 @@ export const CategoryValueManager: React.FC = ({ columnName, columnLabel, onValueCountChange, + menuObjid, }) => { const { toast } = useToast(); const [values, setValues] = useState([]); @@ -81,7 +83,7 @@ export const CategoryValueManager: React.FC = ({ setIsLoading(true); try { // includeInactive: true๋กœ ๋น„ํ™œ์„ฑ ๊ฐ’๋„ ํฌํ•จ - const response = await getCategoryValues(tableName, columnName, true); + const response = await getCategoryValues(tableName, columnName, true, menuObjid); if (response.success && response.data) { setValues(response.data); setFilteredValues(response.data); @@ -101,11 +103,23 @@ export const CategoryValueManager: React.FC = ({ const handleAddValue = async (newValue: TableCategoryValue) => { try { - const response = await addCategoryValue({ - ...newValue, - tableName, - columnName, - }); + if (!menuObjid) { + toast({ + title: "์˜ค๋ฅ˜", + description: "๋ฉ”๋‰ด ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + }); + return; + } + + const response = await addCategoryValue( + { + ...newValue, + tableName, + columnName, + }, + menuObjid + ); if (response.success && response.data) { await loadCategoryValues(); @@ -128,7 +142,7 @@ export const CategoryValueManager: React.FC = ({ title: "์˜ค๋ฅ˜", description: error.message || "์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค", variant: "destructive", - }); + }); } }; diff --git a/frontend/lib/api/tableCategoryValue.ts b/frontend/lib/api/tableCategoryValue.ts index ec927ac9..ee42c859 100644 --- a/frontend/lib/api/tableCategoryValue.ts +++ b/frontend/lib/api/tableCategoryValue.ts @@ -21,19 +21,30 @@ export async function getCategoryColumns(tableName: string) { } /** - * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ชฉ๋ก ์กฐํšŒ (ํ…Œ์ด๋ธ” ์Šค์ฝ”ํ”„) + * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ชฉ๋ก ์กฐํšŒ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) + * + * @param tableName ํ…Œ์ด๋ธ”๋ช… + * @param columnName ์ปฌ๋Ÿผ๋ช… + * @param includeInactive ๋น„ํ™œ์„ฑ ๊ฐ’ ํฌํ•จ ์—ฌ๋ถ€ + * @param menuObjid ๋ฉ”๋‰ด OBJID (์„ ํƒ์‚ฌํ•ญ, ์ œ๊ณต ์‹œ ํ˜•์ œ ๋ฉ”๋‰ด์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ํฌํ•จ) */ export async function getCategoryValues( tableName: string, columnName: string, - includeInactive: boolean = false + includeInactive: boolean = false, + menuObjid?: number ) { try { + const params: any = { includeInactive }; + if (menuObjid) { + params.menuObjid = menuObjid; + } + const response = await apiClient.get<{ success: boolean; data: TableCategoryValue[]; }>(`/table-categories/${tableName}/${columnName}/values`, { - params: { includeInactive }, + params, }); return response.data; } catch (error: any) { @@ -43,14 +54,23 @@ export async function getCategoryValues( } /** - * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ + * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) + * + * @param value ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ •๋ณด + * @param menuObjid ๋ฉ”๋‰ด OBJID (ํ•„์ˆ˜) */ -export async function addCategoryValue(value: TableCategoryValue) { +export async function addCategoryValue( + value: TableCategoryValue, + menuObjid: number +) { try { const response = await apiClient.post<{ success: boolean; data: TableCategoryValue; - }>("/table-categories/values", value); + }>("/table-categories/values", { + ...value, + menuObjid, // โ† menuObjid ํฌํ•จ + }); return response.data; } catch (error: any) { console.error("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ ์‹คํŒจ:", error); diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 19d61cb0..1791e9b0 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -98,6 +98,7 @@ export interface DynamicComponentRendererProps { screenId?: number; tableName?: string; menuId?: number; // ๐Ÿ†• ๋ฉ”๋‰ด ID (์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ ๋“ฑ์— ํ•„์š”) + menuObjid?: number; // ๐Ÿ†• ๋ฉ”๋‰ด OBJID (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„ - ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ) selectedScreen?: any; // ๐Ÿ†• ํ™”๋ฉด ์ •๋ณด ์ „์ฒด (menuId ๋“ฑ ์ถ”์ถœ์šฉ) userId?: string; // ๐Ÿ†• ํ˜„์žฌ ์‚ฌ์šฉ์ž ID userName?: string; // ๐Ÿ†• ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ด๋ฆ„ @@ -224,6 +225,7 @@ export const DynamicComponentRenderer: React.FC = onFormDataChange, tableName, menuId, // ๐Ÿ†• ๋ฉ”๋‰ด ID + menuObjid, // ๐Ÿ†• ๋ฉ”๋‰ด OBJID (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) selectedScreen, // ๐Ÿ†• ํ™”๋ฉด ์ •๋ณด onRefresh, onClose, @@ -319,6 +321,7 @@ export const DynamicComponentRenderer: React.FC = onChange: handleChange, // ๊ฐœ์„ ๋œ onChange ํ•ธ๋“ค๋Ÿฌ ์ „๋‹ฌ tableName, menuId, // ๐Ÿ†• ๋ฉ”๋‰ด ID + menuObjid, // ๐Ÿ†• ๋ฉ”๋‰ด OBJID (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) selectedScreen, // ๐Ÿ†• ํ™”๋ฉด ์ •๋ณด onRefresh, onClose, diff --git a/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx b/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx index f487b320..817baf57 100644 --- a/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx +++ b/frontend/lib/registry/components/text-input/TextInputConfigPanel.tsx @@ -15,32 +15,65 @@ export interface TextInputConfigPanelProps { config: TextInputConfig; onChange: (config: Partial) => void; screenTableName?: string; // ๐Ÿ†• ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… + menuObjid?: number; // ๐Ÿ†• ๋ฉ”๋‰ด OBJID (์‚ฌ์šฉ์ž ์„ ํƒ) } /** * TextInput ์„ค์ • ํŒจ๋„ * ์ปดํฌ๋„ŒํŠธ์˜ ์„ค์ •๊ฐ’๋“ค์„ ํŽธ์ง‘ํ•  ์ˆ˜ ์žˆ๋Š” UI ์ œ๊ณต */ -export const TextInputConfigPanel: React.FC = ({ config, onChange, screenTableName }) => { +export const TextInputConfigPanel: React.FC = ({ config, onChange, screenTableName, menuObjid }) => { // ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ์ƒํƒœ const [numberingRules, setNumberingRules] = useState([]); const [loadingRules, setLoadingRules] = useState(false); + + // ๋ถ€๋ชจ ๋ฉ”๋‰ด ๋ชฉ๋ก ์ƒํƒœ (์ฑ„๋ฒˆ๊ทœ์น™ ์‚ฌ์šฉ์„ ์œ„ํ•œ ์„ ํƒ) + const [parentMenus, setParentMenus] = useState([]); + const [selectedMenuObjid, setSelectedMenuObjid] = useState(menuObjid); + const [loadingMenus, setLoadingMenus] = useState(false); - // ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ๋กœ๋“œ + // ๋ถ€๋ชจ ๋ฉ”๋‰ด ๋ชฉ๋ก ๋กœ๋“œ (์‚ฌ์šฉ์ž ๋ฉ”๋‰ด์˜ ๋ ˆ๋ฒจ 2๋งŒ) + useEffect(() => { + const loadMenus = async () => { + setLoadingMenus(true); + try { + const { apiClient } = await import("@/lib/api/client"); + const response = await apiClient.get("/admin/menus"); + + if (response.data.success && response.data.data) { + const allMenus = response.data.data; + + // ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด(menu_type='1')์˜ ๋ ˆ๋ฒจ 2๋งŒ ํ•„ํ„ฐ๋ง + const level2UserMenus = allMenus.filter((menu: any) => + menu.menu_type === '1' && menu.lev === 2 + ); + + setParentMenus(level2UserMenus); + console.log("โœ… ๋ถ€๋ชจ ๋ฉ”๋‰ด ๋กœ๋“œ ์™„๋ฃŒ:", level2UserMenus.length, "๊ฐœ", level2UserMenus); + } + } catch (error) { + console.error("๋ถ€๋ชจ ๋ฉ”๋‰ด ๋กœ๋“œ ์‹คํŒจ:", error); + } finally { + setLoadingMenus(false); + } + }; + loadMenus(); + }, []); + + // ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ๋กœ๋“œ (์„ ํƒ๋œ ๋ฉ”๋‰ด ๊ธฐ์ค€) useEffect(() => { const loadRules = async () => { + // ๋ฉ”๋‰ด๊ฐ€ ์„ ํƒ๋˜์ง€ ์•Š์•˜์œผ๋ฉด ๋กœ๋“œํ•˜์ง€ ์•Š์Œ + if (!selectedMenuObjid) { + console.log("โš ๏ธ ๋ฉ”๋‰ด๊ฐ€ ์„ ํƒ๋˜์ง€ ์•Š์•„ ์ฑ„๋ฒˆ ๊ทœ์น™์„ ๋กœ๋“œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"); + setNumberingRules([]); + return; + } + setLoadingRules(true); try { - let response; - - // ๐Ÿ†• ํ…Œ์ด๋ธ”๋ช…์ด ์žˆ์œผ๋ฉด ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง, ์—†์œผ๋ฉด ์ „์ฒด ์กฐํšŒ - if (screenTableName) { - console.log("๐Ÿ” TextInputConfigPanel: ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋กœ๋“œ", { screenTableName }); - response = await getAvailableNumberingRulesForScreen(screenTableName); - } else { - console.log("๐Ÿ” TextInputConfigPanel: ์ „์ฒด ์ฑ„๋ฒˆ ๊ทœ์น™ ๋กœ๋“œ (ํ…Œ์ด๋ธ”๋ช… ์—†์Œ)"); - response = await getAvailableNumberingRules(); - } + console.log("๐Ÿ” ์„ ํƒ๋œ ๋ฉ”๋‰ด ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋กœ๋“œ", { selectedMenuObjid }); + const response = await getAvailableNumberingRules(selectedMenuObjid); if (response.success && response.data) { setNumberingRules(response.data); @@ -48,6 +81,7 @@ export const TextInputConfigPanel: React.FC = ({ conf } } catch (error) { console.error("์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:", error); + setNumberingRules([]); } finally { setLoadingRules(false); } @@ -57,7 +91,7 @@ export const TextInputConfigPanel: React.FC = ({ conf if (config.autoGeneration?.type === "numbering_rule") { loadRules(); } - }, [config.autoGeneration?.type, screenTableName]); + }, [config.autoGeneration?.type, selectedMenuObjid]); const handleChange = (key: keyof TextInputConfig, value: any) => { onChange({ [key]: value }); @@ -157,50 +191,100 @@ export const TextInputConfigPanel: React.FC = ({ conf {/* ์ฑ„๋ฒˆ ๊ทœ์น™ ์„ ํƒ */} {config.autoGeneration?.type === "numbering_rule" && ( -
- - { + const menuObjid = parseInt(value); + setSelectedMenuObjid(menuObjid); + console.log("โœ… ๋ฉ”๋‰ด ์„ ํƒ๋จ:", menuObjid); + }} + disabled={loadingMenus} + > + + + + + {parentMenus.length === 0 ? ( + + ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฉ”๋‰ด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค - )) - )} - - -

- ํ˜„์žฌ ๋ฉ”๋‰ด์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ฑ„๋ฒˆ ๊ทœ์น™๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค -

-
+ ) : ( + parentMenus.map((menu) => ( + + {menu.menu_name_kor} + {menu.menu_name_eng && ( + + ({menu.menu_name_eng}) + + )} + + )) + )} + + +

+ ์ด ์ž…๋ ฅ ํ•„๋“œ๊ฐ€ ์–ด๋А ๋ฉ”๋‰ด์— ์†ํ• ์ง€ ์„ ํƒํ•˜์„ธ์š” (ํ•ด๋‹น ๋ฉ”๋‰ด์˜ ์ฑ„๋ฒˆ๊ทœ์น™์ด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค) +

+
+ + {/* ์ฑ„๋ฒˆ ๊ทœ์น™ ์„ ํƒ (๋ฉ”๋‰ด ์„ ํƒ ํ›„) */} + {selectedMenuObjid ? ( +
+ + +

+ ์„ ํƒ๋œ ๋ฉ”๋‰ด ๋ฐ ํ˜•์ œ ๋ฉ”๋‰ด์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ฑ„๋ฒˆ ๊ทœ์น™๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค +

+
+ ) : ( +
+ ๋จผ์ € ๋Œ€์ƒ ๋ฉ”๋‰ด๋ฅผ ์„ ํƒํ•˜์„ธ์š” +
+ )} + )} )} diff --git a/์นดํ…Œ๊ณ ๋ฆฌ_์ฑ„๋ฒˆ_๋ฉ”๋‰ด์Šค์ฝ”ํ”„_์ „ํ™˜_ํ†ตํ•ฉ_๊ณ„ํš์„œ.md b/์นดํ…Œ๊ณ ๋ฆฌ_์ฑ„๋ฒˆ_๋ฉ”๋‰ด์Šค์ฝ”ํ”„_์ „ํ™˜_ํ†ตํ•ฉ_๊ณ„ํš์„œ.md new file mode 100644 index 00000000..ac8b0d79 --- /dev/null +++ b/์นดํ…Œ๊ณ ๋ฆฌ_์ฑ„๋ฒˆ_๋ฉ”๋‰ด์Šค์ฝ”ํ”„_์ „ํ™˜_ํ†ตํ•ฉ_๊ณ„ํš์„œ.md @@ -0,0 +1,1004 @@ +# ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ฉ”๋‰ด ์Šค์ฝ”ํ”„ ์ „ํ™˜ ํ†ตํ•ฉ ๊ณ„ํš์„œ + +## ๐Ÿ“‹ ํ˜„์žฌ ๋ฌธ์ œ์  ๋ถ„์„ + +### ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ์Šค์ฝ”ํ”„์˜ ๊ทผ๋ณธ์  ํ•œ๊ณ„ + +**ํ˜„์žฌ ์ƒํ™ฉ**: +- ์นดํ…Œ๊ณ ๋ฆฌ ์‹œ์Šคํ…œ: `table_column_category_values` ํ…Œ์ด๋ธ”์—์„œ `table_name + column_name`์œผ๋กœ ๋ฐ์ดํ„ฐ ์กฐํšŒ +- ์ฑ„๋ฒˆ๊ทœ์น™ ์‹œ์Šคํ…œ: `numbering_rules` ํ…Œ์ด๋ธ”์—์„œ `table_name`์œผ๋กœ ๋ฐ์ดํ„ฐ ์กฐํšŒ + +**๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ**: + +``` +์˜์—…๊ด€๋ฆฌ (menu_objid: 200) +โ”œโ”€โ”€ ๊ณ ๊ฐ๊ด€๋ฆฌ (menu_objid: 201) - ํ…Œ์ด๋ธ”: customer_info +โ”œโ”€โ”€ ๊ณ„์•ฝ๊ด€๋ฆฌ (menu_objid: 202) - ํ…Œ์ด๋ธ”: contract_info +โ”œโ”€โ”€ ์ฃผ๋ฌธ๊ด€๋ฆฌ (menu_objid: 203) - ํ…Œ์ด๋ธ”: order_info +โ””โ”€โ”€ ๊ณตํ†ต์ฝ”๋“œ ๊ด€๋ฆฌ (menu_objid: 204) - ์–ด๋–ค ํ…Œ์ด๋ธ” ์„ ํƒ? +``` + +**๋ฌธ์ œ 1**: ํ˜•์ œ ๋ฉ”๋‰ด ๊ฐ„ ์ฝ”๋“œ ๊ณต์œ  ๋ถˆ๊ฐ€ +- ๊ณ ๊ฐ๊ด€๋ฆฌ, ๊ณ„์•ฝ๊ด€๋ฆฌ, ์ฃผ๋ฌธ๊ด€๋ฆฌ๊ฐ€ ๋ชจ๋‘ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ +- ๊ฐ ํ™”๋ฉด๋งˆ๋‹ค **๋™์ผํ•œ ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ๊ทœ์น™์„ ์ค‘๋ณต ์ƒ์„ฑ**ํ•ด์•ผ ํ•จ +- "๊ณ ๊ฐ ์œ ํ˜•" ๊ฐ™์€ ๊ณตํ†ต ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ 3๋ฒˆ ๋งŒ๋“ค์–ด์•ผ ํ•จ + +**๋ฌธ์ œ 2**: ๊ณตํ†ต์ฝ”๋“œ ๊ด€๋ฆฌ ํ™”๋ฉด ๋ถˆ๊ฐ€๋Šฅ +- ์˜์—…๊ด€๋ฆฌ ์ „์ฒด์—์„œ ์‚ฌ์šฉํ•  ๊ณตํ†ต์ฝ”๋“œ๋ฅผ ๊ด€๋ฆฌํ•˜๋ ค๋ฉด +- ํŠน์ • ํ…Œ์ด๋ธ” ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•ด์•ผ ํ•˜๋Š”๋ฐ +- ๊ทธ๋Ÿฌ๋ฉด ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•˜๋Š” ํ˜•์ œ ๋ฉ”๋‰ด์—์„œ ์ ‘๊ทผ ๋ถˆ๊ฐ€ + +**๋ฌธ์ œ 3**: ๋น„ํšจ์œจ์ ์ธ ์œ ์ง€๋ณด์ˆ˜ +- ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ์—ฌ๋Ÿฌ ํ…Œ์ด๋ธ”์— ์ค‘๋ณต ๊ด€๋ฆฌ +- ํ•˜๋‚˜์˜ ๊ฐ’์„ ์ˆ˜์ •ํ•˜๋ ค๋ฉด ๋ชจ๋“  ํ…Œ์ด๋ธ”์—์„œ ์ˆ˜์ • ํ•„์š” +- ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜ ๋ฐœ์ƒ ๊ฐ€๋Šฅ + +--- + +## โœ… ํ•ด๊ฒฐ ๋ฐฉ์•ˆ: ๋ฉ”๋‰ด ๊ธฐ๋ฐ˜ ์Šค์ฝ”ํ”„ + +### ํ•ต์‹ฌ ๊ฐœ๋… + +**๋ฉ”๋‰ด ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ๋ฐ์ดํ„ฐ ์Šค์ฝ”ํ”„๋กœ ์‚ฌ์šฉ**: +- ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ๊ทœ์น™ ์ƒ์„ฑ ์‹œ `menu_objid`๋ฅผ ๊ธฐ๋ก +- ๊ฐ™์€ ๋ถ€๋ชจ ๋ฉ”๋‰ด๋ฅผ ๊ฐ€์ง„ **ํ˜•์ œ ๋ฉ”๋‰ด๋“ค**์ด ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ  +- ํ…Œ์ด๋ธ”๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋ฉ”๋‰ด ๊ตฌ์กฐ์— ๋”ฐ๋ผ ์Šค์ฝ”ํ”„ ๊ฒฐ์ • + +### ๋ฉ”๋‰ด ์Šค์ฝ”ํ”„ ๊ทœ์น™ + +``` +์˜์—…๊ด€๋ฆฌ (parent_id: 0, menu_objid: 200) +โ”œโ”€โ”€ ๊ณ ๊ฐ๊ด€๋ฆฌ (parent_id: 200, menu_objid: 201) +โ”œโ”€โ”€ ๊ณ„์•ฝ๊ด€๋ฆฌ (parent_id: 200, menu_objid: 202) +โ”œโ”€โ”€ ์ฃผ๋ฌธ๊ด€๋ฆฌ (parent_id: 200, menu_objid: 203) +โ””โ”€โ”€ ๊ณตํ†ต์ฝ”๋“œ ๊ด€๋ฆฌ (parent_id: 200, menu_objid: 204) โ† ์—ฌ๊ธฐ์„œ ์ƒ์„ฑ +``` + +**์Šค์ฝ”ํ”„ ๊ทœ์น™**: +1. 204๋ฒˆ ๋ฉ”๋‰ด์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ์ƒ์„ฑ โ†’ `menu_objid = 204`๋กœ ์ €์žฅ +2. ํ˜•์ œ ๋ฉ”๋‰ด (201, 202, 203, 204)์—์„œ **๋ชจ๋‘ ์‚ฌ์šฉ ๊ฐ€๋Šฅ** +3. ๋‹ค๋ฅธ ๋ถ€๋ชจ์˜ ๋ฉ”๋‰ด (์˜ˆ: ๊ตฌ๋งค๊ด€๋ฆฌ)์—์„œ๋Š” ์‚ฌ์šฉ ๋ถˆ๊ฐ€ + +### ์ด์  + +โœ… **ํ˜•์ œ ๋ฉ”๋‰ด ๊ฐ„ ์ฝ”๋“œ ๊ณต์œ **: ํ•œ ๋ฒˆ ์ƒ์„ฑํ•˜๋ฉด ๋ชจ๋“  ํ˜•์ œ ๋ฉ”๋‰ด์—์„œ ์‚ฌ์šฉ +โœ… **๊ณตํ†ต์ฝ”๋“œ ๊ด€๋ฆฌ ํ™”๋ฉด ๊ฐ€๋Šฅ**: ์ „์šฉ ๋ฉ”๋‰ด์—์„œ ์ผ๊ด„ ๊ด€๋ฆฌ +โœ… **ํ…Œ์ด๋ธ” ๋…๋ฆฝ์„ฑ**: ํ…Œ์ด๋ธ”์ด ๋‹ฌ๋ผ๋„ ๊ฐ™์€ ์นดํ…Œ๊ณ ๋ฆฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅ +โœ… **์ง๊ด€์ ์ธ ๊ด€๋ฆฌ**: ๋ฉ”๋‰ด ๊ตฌ์กฐ๊ฐ€ ๊ณง ๋ฐ์ดํ„ฐ ์Šค์ฝ”ํ”„ +โœ… **์œ ์ง€๋ณด์ˆ˜ ์šฉ์ด**: ํ•œ ๊ณณ์—์„œ ์ˆ˜์ •ํ•˜๋ฉด ๋ชจ๋“  ํ˜•์ œ ๋ฉ”๋‰ด์— ๋ฐ˜์˜ + +--- + +## ๐Ÿ“ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ + +### 1. ์นดํ…Œ๊ณ ๋ฆฌ ์‹œ์Šคํ…œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +#### ๊ธฐ์กด ์ƒํƒœ +```sql +-- table_column_category_values ํ…Œ์ด๋ธ” +table_name | column_name | value_code | company_code +customer_info | customer_type | REGULAR | COMPANY_A +customer_info | customer_type | VIP | COMPANY_A +``` + +**๋ฌธ์ œ**: `contract_info` ํ…Œ์ด๋ธ”์—์„œ๋Š” ์ด ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Œ + +#### ๋ณ€๊ฒฝ ํ›„ +```sql +-- table_column_category_values ํ…Œ์ด๋ธ”์— menu_objid ์ถ”๊ฐ€ +table_name | column_name | value_code | menu_objid | company_code +customer_info | customer_type | REGULAR | 204 | COMPANY_A +customer_info | customer_type | VIP | 204 | COMPANY_A +``` + +**ํ•ด๊ฒฐ**: menu_objid=204์˜ ํ˜•์ œ ๋ฉ”๋‰ด(201,202,203,204)์—์„œ ๋ชจ๋‘ ์‚ฌ์šฉ ๊ฐ€๋Šฅ + +#### ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ SQL + +```sql +-- db/migrations/048_convert_category_to_menu_scope.sql + +-- 1. menu_objid ์ปฌ๋Ÿผ ์ถ”๊ฐ€ (NULL ํ—ˆ์šฉ) +ALTER TABLE table_column_category_values +ADD COLUMN IF NOT EXISTS menu_objid NUMERIC; + +COMMENT ON COLUMN table_column_category_values.menu_objid +IS '์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•œ ๋ฉ”๋‰ด OBJID (ํ˜•์ œ ๋ฉ”๋‰ด์—์„œ ๊ณต์œ )'; + +-- 2. ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— ์ž„์‹œ menu_objid ์„ค์ • +-- ์ฒซ ๋ฒˆ์งธ ๋ฉ”๋‰ด์˜ objid๋ฅผ ๊ฐ€์ ธ์™€์„œ ์„ค์ • +DO $$ +DECLARE + first_menu_objid NUMERIC; +BEGIN + SELECT objid INTO first_menu_objid FROM menu_info LIMIT 1; + + IF first_menu_objid IS NOT NULL THEN + UPDATE table_column_category_values + SET menu_objid = first_menu_objid + WHERE menu_objid IS NULL; + + RAISE NOTICE '๊ธฐ์กด ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ์ดํ„ฐ์˜ menu_objid๋ฅผ %๋กœ ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค', first_menu_objid; + RAISE NOTICE '๊ด€๋ฆฌ์ž๊ฐ€ ์ˆ˜๋™์œผ๋กœ ์˜ฌ๋ฐ”๋ฅธ menu_objid๋กœ ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค'; + END IF; +END $$; + +-- 3. menu_objid๋ฅผ NOT NULL๋กœ ๋ณ€๊ฒฝ +ALTER TABLE table_column_category_values +ALTER COLUMN menu_objid SET NOT NULL; + +-- 4. ์™ธ๋ž˜ํ‚ค ์ถ”๊ฐ€ +ALTER TABLE table_column_category_values +ADD CONSTRAINT fk_category_value_menu +FOREIGN KEY (menu_objid) REFERENCES menu_info(objid) +ON DELETE CASCADE; + +-- 5. ๊ธฐ์กด UNIQUE ์ œ์•ฝ์กฐ๊ฑด ์‚ญ์ œ +ALTER TABLE table_column_category_values +DROP CONSTRAINT IF EXISTS unique_category_value; + +ALTER TABLE table_column_category_values +DROP CONSTRAINT IF EXISTS table_column_category_values_table_name_column_name_value_key; + +-- 6. ์ƒˆ๋กœ์šด UNIQUE ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ (menu_objid ํฌํ•จ) +ALTER TABLE table_column_category_values +ADD CONSTRAINT unique_category_value +UNIQUE (table_name, column_name, value_code, menu_objid, company_code); + +-- 7. ์ธ๋ฑ์Šค ์ถ”๊ฐ€ (์„ฑ๋Šฅ ์ตœ์ ํ™”) +CREATE INDEX IF NOT EXISTS idx_category_value_menu +ON table_column_category_values(menu_objid, table_name, column_name, company_code); + +CREATE INDEX IF NOT EXISTS idx_category_value_company +ON table_column_category_values(company_code, table_name, column_name); +``` + +### 2. ์ฑ„๋ฒˆ๊ทœ์น™ ์‹œ์Šคํ…œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +#### ๊ธฐ์กด ์ƒํƒœ +```sql +-- numbering_rules ํ…Œ์ด๋ธ” +rule_id | table_name | scope_type | company_code +ITEM_CODE | item_info | table | COMPANY_A +``` + +**๋ฌธ์ œ**: `item_info` ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•˜๋Š” ํ™”๋ฉด์—์„œ๋งŒ ์ด ๊ทœ์น™ ์‚ฌ์šฉ ๊ฐ€๋Šฅ + +#### ๋ณ€๊ฒฝ ํ›„ +```sql +-- numbering_rules ํ…Œ์ด๋ธ” (menu_objid ์ถ”๊ฐ€) +rule_id | table_name | scope_type | menu_objid | company_code +ITEM_CODE | item_info | menu | 204 | COMPANY_A +``` + +**ํ•ด๊ฒฐ**: menu_objid=204์˜ ํ˜•์ œ ๋ฉ”๋‰ด์—์„œ ๋ชจ๋‘ ์‚ฌ์šฉ ๊ฐ€๋Šฅ + +#### ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ SQL + +```sql +-- db/migrations/049_convert_numbering_to_menu_scope.sql + +-- 1. menu_objid ์ปฌ๋Ÿผ ์ถ”๊ฐ€ (์ด๋ฏธ ์กด์žฌํ•˜๋ฉด ์Šคํ‚ต) +ALTER TABLE numbering_rules +ADD COLUMN IF NOT EXISTS menu_objid NUMERIC; + +COMMENT ON COLUMN numbering_rules.menu_objid +IS '์ฑ„๋ฒˆ๊ทœ์น™์„ ์ƒ์„ฑํ•œ ๋ฉ”๋‰ด OBJID (ํ˜•์ œ ๋ฉ”๋‰ด์—์„œ ๊ณต์œ )'; + +-- 2. ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ +DO $$ +DECLARE + first_menu_objid NUMERIC; +BEGIN + SELECT objid INTO first_menu_objid FROM menu_info LIMIT 1; + + IF first_menu_objid IS NOT NULL THEN + -- scope_type='table'์ด๊ณ  menu_objid๊ฐ€ NULL์ธ ๊ทœ์น™๋“ค์„ + -- scope_type='menu'๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ์ž„์‹œ menu_objid ์„ค์ • + UPDATE numbering_rules + SET scope_type = 'menu', + menu_objid = first_menu_objid + WHERE scope_type = 'table' + AND menu_objid IS NULL; + + RAISE NOTICE '๊ธฐ์กด ์ฑ„๋ฒˆ๊ทœ์น™์˜ scope_type์„ menu๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  menu_objid๋ฅผ %๋กœ ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค', first_menu_objid; + RAISE NOTICE '๊ด€๋ฆฌ์ž๊ฐ€ ์ˆ˜๋™์œผ๋กœ ์˜ฌ๋ฐ”๋ฅธ menu_objid๋กœ ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค'; + END IF; +END $$; + +-- 3. ์ œ์•ฝ์กฐ๊ฑด ์ˆ˜์ • +-- menu ํƒ€์ž…์€ menu_objid ํ•„์ˆ˜ +ALTER TABLE numbering_rules +DROP CONSTRAINT IF EXISTS check_menu_scope_requires_menu_objid; + +ALTER TABLE numbering_rules +ADD CONSTRAINT check_menu_scope_requires_menu_objid +CHECK ( + (scope_type != 'menu') OR + (scope_type = 'menu' AND menu_objid IS NOT NULL) +); + +-- 4. ์™ธ๋ž˜ํ‚ค ์ถ”๊ฐ€ (menu_objid โ†’ menu_info.objid) +ALTER TABLE numbering_rules +DROP CONSTRAINT IF EXISTS fk_numbering_rule_menu; + +ALTER TABLE numbering_rules +ADD CONSTRAINT fk_numbering_rule_menu +FOREIGN KEY (menu_objid) REFERENCES menu_info(objid) +ON DELETE CASCADE; + +-- 5. ์ธ๋ฑ์Šค ์ถ”๊ฐ€ (์„ฑ๋Šฅ ์ตœ์ ํ™”) +CREATE INDEX IF NOT EXISTS idx_numbering_rules_menu +ON numbering_rules(menu_objid, company_code); +``` + +--- + +## ๐Ÿ”ง ๋ฐฑ์—”๋“œ ๊ตฌํ˜„ + +### 1. ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ: ํ˜•์ œ ๋ฉ”๋‰ด ์กฐํšŒ + +```typescript +// backend-node/src/services/menuService.ts (์‹ ๊ทœ ํŒŒ์ผ) + +import { getPool } from "../database/db"; +import { logger } from "../utils/logger"; + +/** + * ๋ฉ”๋‰ด์˜ ํ˜•์ œ ๋ฉ”๋‰ด OBJID ๋ชฉ๋ก ์กฐํšŒ + * (๊ฐ™์€ ๋ถ€๋ชจ๋ฅผ ๊ฐ€์ง„ ๋ฉ”๋‰ด๋“ค) + * + * @param menuObjid ํ˜„์žฌ ๋ฉ”๋‰ด์˜ OBJID + * @returns ํ˜•์ œ ๋ฉ”๋‰ด OBJID ๋ฐฐ์—ด (์ž๊ธฐ ์ž์‹  ํฌํ•จ) + */ +export async function getSiblingMenuObjids(menuObjid: number): Promise { + const pool = getPool(); + + try { + logger.info("ํ˜•์ œ ๋ฉ”๋‰ด ์กฐํšŒ ์‹œ์ž‘", { menuObjid }); + + // 1. ํ˜„์žฌ ๋ฉ”๋‰ด์˜ ๋ถ€๋ชจ ์ฐพ๊ธฐ + const parentQuery = ` + SELECT parent_id FROM menu_info WHERE objid = $1 + `; + const parentResult = await pool.query(parentQuery, [menuObjid]); + + if (parentResult.rows.length === 0) { + logger.warn("๋ฉ”๋‰ด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ", { menuObjid }); + return [menuObjid]; // ๋ฉ”๋‰ด๊ฐ€ ์—†์œผ๋ฉด ์ž๊ธฐ ์ž์‹ ๋งŒ + } + + const parentId = parentResult.rows[0].parent_id; + + if (!parentId || parentId === 0) { + // ์ตœ์ƒ์œ„ ๋ฉ”๋‰ด์ธ ๊ฒฝ์šฐ ์ž๊ธฐ ์ž์‹ ๋งŒ + logger.info("์ตœ์ƒ์œ„ ๋ฉ”๋‰ด (ํ˜•์ œ ์—†์Œ)", { menuObjid }); + return [menuObjid]; + } + + // 2. ๊ฐ™์€ ๋ถ€๋ชจ๋ฅผ ๊ฐ€์ง„ ํ˜•์ œ ๋ฉ”๋‰ด๋“ค ์กฐํšŒ + const siblingsQuery = ` + SELECT objid FROM menu_info WHERE parent_id = $1 ORDER BY objid + `; + const siblingsResult = await pool.query(siblingsQuery, [parentId]); + + const siblingObjids = siblingsResult.rows.map((row) => row.objid); + + logger.info("ํ˜•์ œ ๋ฉ”๋‰ด ์กฐํšŒ ์™„๋ฃŒ", { + menuObjid, + parentId, + siblingCount: siblingObjids.length, + siblings: siblingObjids, + }); + + return siblingObjids; + } catch (error: any) { + logger.error("ํ˜•์ œ ๋ฉ”๋‰ด ์กฐํšŒ ์‹คํŒจ", { menuObjid, error: error.message }); + // ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์•ˆ์ „ํ•˜๊ฒŒ ์ž๊ธฐ ์ž์‹ ๋งŒ ๋ฐ˜ํ™˜ + return [menuObjid]; + } +} + +/** + * ์—ฌ๋Ÿฌ ๋ฉ”๋‰ด์˜ ํ˜•์ œ ๋ฉ”๋‰ด OBJID ํ•ฉ์ง‘ํ•ฉ ์กฐํšŒ + * + * @param menuObjids ๋ฉ”๋‰ด OBJID ๋ฐฐ์—ด + * @returns ๋ชจ๋“  ํ˜•์ œ ๋ฉ”๋‰ด OBJID ๋ฐฐ์—ด (์ค‘๋ณต ์ œ๊ฑฐ) + */ +export async function getAllSiblingMenuObjids( + menuObjids: number[] +): Promise { + if (!menuObjids || menuObjids.length === 0) { + return []; + } + + const allSiblings = new Set(); + + for (const objid of menuObjids) { + const siblings = await getSiblingMenuObjids(objid); + siblings.forEach((s) => allSiblings.add(s)); + } + + return Array.from(allSiblings).sort((a, b) => a - b); +} +``` + +### 2. ์นดํ…Œ๊ณ ๋ฆฌ ์„œ๋น„์Šค ์ˆ˜์ • + +```typescript +// backend-node/src/services/tableCategoryValueService.ts + +import { getSiblingMenuObjids } from "./menuService"; + +class TableCategoryValueService { + /** + * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ชฉ๋ก ์กฐํšŒ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„ ์ ์šฉ) + */ + async getCategoryValues( + tableName: string, + columnName: string, + menuObjid: number, // โ† ์ถ”๊ฐ€ + companyCode: string, + includeInactive: boolean = false + ): Promise { + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„)", { + tableName, + columnName, + menuObjid, + companyCode, + }); + + const pool = getPool(); + + // 1. ํ˜•์ œ ๋ฉ”๋‰ด OBJID ์กฐํšŒ + const siblingObjids = await getSiblingMenuObjids(menuObjid); + + logger.info("ํ˜•์ œ ๋ฉ”๋‰ด OBJID ๋ชฉ๋ก", { menuObjid, siblingObjids }); + + // 2. ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ (ํ˜•์ œ ๋ฉ”๋‰ด ํฌํ•จ) + 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", + menu_objid AS "menuObjid", + created_at AS "createdAt", + created_by AS "createdBy" + FROM table_column_category_values + WHERE table_name = $1 + AND column_name = $2 + AND menu_objid = ANY($3) -- โ† ํ˜•์ œ ๋ฉ”๋‰ด ํฌํ•จ + ${!includeInactive ? 'AND is_active = true' : ''} + ORDER BY value_order, value_label + `; + params = [tableName, columnName, siblingObjids]; + } 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", + menu_objid AS "menuObjid", + created_at AS "createdAt", + created_by AS "createdBy" + FROM table_column_category_values + WHERE table_name = $1 + AND column_name = $2 + AND menu_objid = ANY($3) -- โ† ํ˜•์ œ ๋ฉ”๋‰ด ํฌํ•จ + AND company_code = $4 -- โ† ํšŒ์‚ฌ๋ณ„ ํ•„ํ„ฐ๋ง + ${!includeInactive ? 'AND is_active = true' : ''} + ORDER BY value_order, value_label + `; + params = [tableName, columnName, siblingObjids, companyCode]; + } + + const result = await pool.query(query, params); + + logger.info(`์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ${result.rows.length}๊ฐœ ์กฐํšŒ ์™„๋ฃŒ`); + + return result.rows; + } + + /** + * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ (menu_objid ์ €์žฅ) + */ + async addCategoryValue( + value: TableCategoryValue, + menuObjid: number, // โ† ์ถ”๊ฐ€ + companyCode: string, + userId: string + ): Promise { + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„)", { + tableName: value.tableName, + columnName: value.columnName, + valueCode: value.valueCode, + menuObjid, + companyCode, + }); + + const pool = getPool(); + + const query = ` + INSERT INTO table_column_category_values ( + table_name, column_name, + value_code, value_label, value_order, + parent_value_id, depth, + description, color, icon, + is_active, is_default, + company_code, menu_objid, -- โ† menu_objid ์ถ”๊ฐ€ + created_by + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) + 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", + parent_value_id AS "parentValueId", + depth, + description, + color, + icon, + is_active AS "isActive", + is_default AS "isDefault", + company_code AS "companyCode", + menu_objid AS "menuObjid", + created_at AS "createdAt", + created_by AS "createdBy" + `; + + const result = await pool.query(query, [ + value.tableName, + value.columnName, + value.valueCode, + value.valueLabel, + value.valueOrder || 0, + value.parentValueId || null, + value.depth || 1, + value.description || null, + value.color || null, + value.icon || null, + value.isActive !== false, + value.isDefault || false, + companyCode, + menuObjid, // โ† ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ ํ™”๋ฉด์˜ menu_objid + userId, + ]); + + logger.info("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ ์„ฑ๊ณต", { + valueId: result.rows[0].valueId, + menuObjid, + }); + + return result.rows[0]; + } + + // ์ˆ˜์ •, ์‚ญ์ œ ๋ฉ”์„œ๋“œ๋„ ๋™์ผํ•˜๊ฒŒ menuObjid ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ +} + +export default TableCategoryValueService; +``` + +### 3. ์ฑ„๋ฒˆ๊ทœ์น™ ์„œ๋น„์Šค ์ˆ˜์ • + +```typescript +// backend-node/src/services/numberingRuleService.ts + +import { getSiblingMenuObjids } from "./menuService"; + +class NumberingRuleService { + /** + * ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„ ์ ์šฉ) + */ + async getAvailableRulesForScreen( + companyCode: string, + tableName: string, + menuObjid?: number + ): Promise { + logger.info("ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„)", { + companyCode, + tableName, + menuObjid, + }); + + const pool = getPool(); + + // 1. ํ˜•์ œ ๋ฉ”๋‰ด OBJID ์กฐํšŒ + let siblingObjids: number[] = []; + if (menuObjid) { + siblingObjids = await getSiblingMenuObjids(menuObjid); + logger.info("ํ˜•์ œ ๋ฉ”๋‰ด OBJID ๋ชฉ๋ก", { menuObjid, siblingObjids }); + } + + // 2. ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (์šฐ์„ ์ˆœ์œ„: menu > table > global) + let query: string; + let params: any[]; + + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ์กฐํšŒ (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 != '*' + AND ( + ${ + siblingObjids.length > 0 + ? `(scope_type = 'menu' AND menu_objid = ANY($1)) OR` + : "" + } + (scope_type = 'table' AND table_name = $${siblingObjids.length > 0 ? 2 : 1}) + OR (scope_type = 'global' AND table_name IS NULL) + ) + ORDER BY + CASE scope_type + WHEN 'menu' THEN 1 + WHEN 'table' THEN 2 + WHEN 'global' THEN 3 + END, + created_at DESC + `; + params = siblingObjids.length > 0 ? [siblingObjids, tableName] : [tableName]; + } 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 ( + ${ + siblingObjids.length > 0 + ? `(scope_type = 'menu' AND menu_objid = ANY($2)) OR` + : "" + } + (scope_type = 'table' AND table_name = $${siblingObjids.length > 0 ? 3 : 2}) + OR (scope_type = 'global' AND table_name IS NULL) + ) + ORDER BY + CASE scope_type + WHEN 'menu' THEN 1 + WHEN 'table' THEN 2 + WHEN 'global' THEN 3 + END, + created_at DESC + `; + params = siblingObjids.length > 0 + ? [companyCode, siblingObjids, tableName] + : [companyCode, tableName]; + } + + 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 + ORDER BY part_order + `; + + const partsResult = await pool.query(partsQuery, [ + rule.ruleId, + companyCode === "*" ? rule.companyCode : companyCode, + ]); + + rule.parts = partsResult.rows; + } + + logger.info(`ํ™”๋ฉด์šฉ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์™„๋ฃŒ: ${result.rows.length}๊ฐœ`); + + return result.rows; + } +} + +export default NumberingRuleService; +``` + +### 4. ์ปจํŠธ๋กค๋Ÿฌ ์ˆ˜์ • + +```typescript +// backend-node/src/controllers/tableCategoryValueController.ts + +/** + * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ชฉ๋ก ์กฐํšŒ + */ +export async function getCategoryValues( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { tableName, columnName } = req.params; + const { menuObjid, includeInactive } = req.query; // โ† menuObjid ์ถ”๊ฐ€ + const companyCode = req.user!.companyCode; + + if (!menuObjid) { + res.status(400).json({ + success: false, + message: "menuObjid๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค", + }); + return; + } + + const service = new TableCategoryValueService(); + const values = await service.getCategoryValues( + tableName, + columnName, + Number(menuObjid), // โ† menuObjid ์ „๋‹ฌ + companyCode, + includeInactive === "true" + ); + + res.json({ + success: true, + data: values, + }); + } catch (error: any) { + logger.error("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ ์‹คํŒจ:", error); + res.status(500).json({ + success: false, + message: "์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ", + error: error.message, + }); + } +} + +/** + * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ + */ +export async function addCategoryValue( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { menuObjid, ...value } = req.body; // โ† menuObjid ์ถ”๊ฐ€ + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + + if (!menuObjid) { + res.status(400).json({ + success: false, + message: "menuObjid๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค", + }); + return; + } + + const service = new TableCategoryValueService(); + const newValue = await service.addCategoryValue( + value, + menuObjid, // โ† menuObjid ์ „๋‹ฌ + companyCode, + userId + ); + + res.json({ + success: true, + data: newValue, + }); + } catch (error: any) { + logger.error("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ ์‹คํŒจ:", error); + res.status(500).json({ + success: false, + message: "์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ", + error: error.message, + }); + } +} +``` + +--- + +## ๐ŸŽจ ํ”„๋ก ํŠธ์—”๋“œ ๊ตฌํ˜„ + +### 1. API ํด๋ผ์ด์–ธํŠธ ์ˆ˜์ • + +```typescript +// frontend/lib/api/tableCategoryValue.ts + +/** + * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ชฉ๋ก ์กฐํšŒ (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) + */ +export async function getCategoryValues( + tableName: string, + columnName: string, + menuObjid: number, // โ† ์ถ”๊ฐ€ + includeInactive: boolean = false +) { + try { + const response = await apiClient.get<{ + success: boolean; + data: TableCategoryValue[]; + }>(`/table-categories/${tableName}/${columnName}/values`, { + params: { + menuObjid, // โ† menuObjid ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ + includeInactive, + }, + }); + return response.data; + } catch (error: any) { + console.error("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ ์‹คํŒจ:", error); + return { success: false, error: error.message }; + } +} + +/** + * ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ + */ +export async function addCategoryValue( + value: TableCategoryValue, + menuObjid: number // โ† ์ถ”๊ฐ€ +) { + try { + const response = await apiClient.post<{ + success: boolean; + data: TableCategoryValue; + }>("/table-categories/values", { + ...value, + menuObjid, // โ† menuObjid ํฌํ•จ + }); + return response.data; + } catch (error: any) { + console.error("์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ ์‹คํŒจ:", error); + return { success: false, error: error.message }; + } +} +``` + +### 2. ํ™”๋ฉด๊ด€๋ฆฌ ์‹œ์Šคํ…œ์—์„œ menuObjid ์ „๋‹ฌ + +```typescript +// frontend/components/screen/ScreenDesigner.tsx + +export function ScreenDesigner() { + const [selectedScreen, setSelectedScreen] = useState(null); + + // ์„ ํƒ๋œ ํ™”๋ฉด์˜ menuObjid ์ถ”์ถœ + const currentMenuObjid = selectedScreen?.menuObjid; + + return ( +
+ {/* ๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ์— menuObjid ์ „๋‹ฌ */} + +
+ ); +} +``` + +### 3. ์ปดํฌ๋„ŒํŠธ props ์ˆ˜์ • + +๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ์— `menuObjid: number` prop ์ถ”๊ฐ€: + +- `CategoryColumnList` +- `CategoryValueManager` +- `NumberingRuleSelector` +- `TextTypeConfigPanel` + +--- + +## ๐Ÿ“Š ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค + +### ์‹œ๋‚˜๋ฆฌ์˜ค: ์˜์—…๊ด€๋ฆฌ ๊ณตํ†ต์ฝ”๋“œ ๊ด€๋ฆฌ + +#### 1๋‹จ๊ณ„: ๋ฉ”๋‰ด ๊ตฌ์กฐ + +``` +์˜์—…๊ด€๋ฆฌ (parent_id: 0, menu_objid: 200) +โ”œโ”€โ”€ ๊ณ ๊ฐ๊ด€๋ฆฌ (parent_id: 200, menu_objid: 201) - customer_info ํ…Œ์ด๋ธ” +โ”œโ”€โ”€ ๊ณ„์•ฝ๊ด€๋ฆฌ (parent_id: 200, menu_objid: 202) - contract_info ํ…Œ์ด๋ธ” +โ”œโ”€โ”€ ์ฃผ๋ฌธ๊ด€๋ฆฌ (parent_id: 200, menu_objid: 203) - order_info ํ…Œ์ด๋ธ” +โ””โ”€โ”€ ๊ณตํ†ต์ฝ”๋“œ ๊ด€๋ฆฌ (parent_id: 200, menu_objid: 204) - ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ ์ „์šฉ +``` + +#### 2๋‹จ๊ณ„: ์นดํ…Œ๊ณ ๋ฆฌ ์ƒ์„ฑ + +1. **๋ฉ”๋‰ด ๋“ฑ๋ก**: ์˜์—…๊ด€๋ฆฌ > ๊ณตํ†ต์ฝ”๋“œ ๊ด€๋ฆฌ (menu_objid: 204) +2. **ํ™”๋ฉด ์ƒ์„ฑ**: ํ™”๋ฉด๊ด€๋ฆฌ ์‹œ์Šคํ…œ์—์„œ ํ™”๋ฉด ์ƒ์„ฑ +3. **ํ…Œ์ด๋ธ” ์„ ํƒ**: `customer_info` (์–ด๋–ค ํ…Œ์ด๋ธ”์ด๋“  ์ƒ๊ด€์—†์Œ) +4. **์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€**: + - ์ปฌ๋Ÿผ: `customer_type` + - ๊ฐ’: `REGULAR (์ผ๋ฐ˜ ๊ณ ๊ฐ)`, `VIP (VIP ๊ณ ๊ฐ)` + - **์ €์žฅ ์‹œ `menu_objid = 204`๋กœ ์ž๋™ ์ €์žฅ** + +#### 3๋‹จ๊ณ„: ํ˜•์ œ ๋ฉ”๋‰ด์—์„œ ์‚ฌ์šฉ + +**๊ณ ๊ฐ๊ด€๋ฆฌ ํ™”๋ฉด** (menu_objid: 201): +- โœ… `customer_type` ๋“œ๋กญ๋‹ค์šด์— `์ผ๋ฐ˜ ๊ณ ๊ฐ`, `VIP ๊ณ ๊ฐ` ํ‘œ์‹œ +- **์ด์œ **: 201๊ณผ 204๋Š” ๊ฐ™์€ ๋ถ€๋ชจ(200)๋ฅผ ๊ฐ€์ง„ ํ˜•์ œ ๋ฉ”๋‰ด + +**๊ณ„์•ฝ๊ด€๋ฆฌ ํ™”๋ฉด** (menu_objid: 202): +- โœ… `customer_type` ์ปฌ๋Ÿผ์— ๋™์ผํ•œ ์นดํ…Œ๊ณ ๋ฆฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅ +- **์ด์œ **: 202์™€ 204๋„ ํ˜•์ œ ๋ฉ”๋‰ด + +**๊ตฌ๋งค๊ด€๋ฆฌ > ๋ฐœ์ฃผ๊ด€๋ฆฌ** (parent_id: 300): +- โŒ ์˜์—…๊ด€๋ฆฌ์˜ ์นดํ…Œ๊ณ ๋ฆฌ๋Š” ํ‘œ์‹œ๋˜์ง€ ์•Š์Œ +- **์ด์œ **: ๋‹ค๋ฅธ ๋ถ€๋ชจ ๋ฉ”๋‰ด์ด๋ฏ€๋กœ ์Šค์ฝ”ํ”„๊ฐ€ ๋‹ค๋ฆ„ + +--- + +## ๐Ÿ“ ๊ตฌํ˜„ ์ˆœ์„œ + +### Phase 1: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (1์‹œ๊ฐ„) + +- [ ] `048_convert_category_to_menu_scope.sql` ์ž‘์„ฑ ๋ฐ ์‹คํ–‰ +- [ ] `049_convert_numbering_to_menu_scope.sql` ์ž‘์„ฑ ๋ฐ ์‹คํ–‰ +- [ ] ๊ธฐ์กด ๋ฐ์ดํ„ฐ ํ™•์ธ ๋ฐ ์ž„์‹œ menu_objid ์ •๋ฆฌ ๊ณ„ํš ์ˆ˜๋ฆฝ + +### Phase 2: ๋ฐฑ์—”๋“œ ๊ตฌํ˜„ (3-4์‹œ๊ฐ„) + +- [ ] `menuService.ts` ์‹ ๊ทœ ํŒŒ์ผ ์ƒ์„ฑ (`getSiblingMenuObjids()` ํ•จ์ˆ˜) +- [ ] `tableCategoryValueService.ts` ์ˆ˜์ • (menuObjid ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€) +- [ ] `numberingRuleService.ts` ์ˆ˜์ • (menuObjid ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€) +- [ ] ์ปจํŠธ๋กค๋Ÿฌ ์ˆ˜์ • (์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ menuObjid ์ถ”์ถœ) +- [ ] ๋ฐฑ์—”๋“œ ํ…Œ์ŠคํŠธ + +### Phase 3: ํ”„๋ก ํŠธ์—”๋“œ API ํด๋ผ์ด์–ธํŠธ (1์‹œ๊ฐ„) + +- [ ] `tableCategoryValue.ts` API ํด๋ผ์ด์–ธํŠธ ์ˆ˜์ • +- [ ] `numberingRule.ts` API ํด๋ผ์ด์–ธํŠธ ์ˆ˜์ • + +### Phase 4: ํ”„๋ก ํŠธ์—”๋“œ ์ปดํฌ๋„ŒํŠธ (3-4์‹œ๊ฐ„) + +- [ ] `CategoryColumnList.tsx` ์ˆ˜์ • (menuObjid prop ์ถ”๊ฐ€) +- [ ] `CategoryValueManager.tsx` ์ˆ˜์ • (menuObjid prop ์ถ”๊ฐ€) +- [ ] `NumberingRuleSelector.tsx` ์ˆ˜์ • (menuObjid prop ์ถ”๊ฐ€) +- [ ] `TextTypeConfigPanel.tsx` ์ˆ˜์ • (menuObjid prop ์ถ”๊ฐ€) +- [ ] ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์—์„œ API ํ˜ธ์ถœ ์‹œ menuObjid ์ „๋‹ฌ + +### Phase 5: ํ™”๋ฉด๊ด€๋ฆฌ ์‹œ์Šคํ…œ ํ†ตํ•ฉ (2์‹œ๊ฐ„) + +- [ ] `ScreenDesigner.tsx`์—์„œ menuObjid ์ถ”์ถœ ๋ฐ ์ „๋‹ฌ +- [ ] ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] ์ฑ„๋ฒˆ๊ทœ์น™ ์„ค์ • ํ™”๋ฉด ํ…Œ์ŠคํŠธ + +### Phase 6: ํ…Œ์ŠคํŠธ ๋ฐ ๋ฌธ์„œํ™” (2์‹œ๊ฐ„) + +- [ ] ์ „์ฒด ํ”Œ๋กœ์šฐ ํ…Œ์ŠคํŠธ +- [ ] ๋ฉ”๋‰ด ์Šค์ฝ”ํ”„ ๋™์ž‘ ๊ฒ€์ฆ +- [ ] ์‚ฌ์šฉ ๊ฐ€์ด๋“œ ์ž‘์„ฑ + +**์ด ์˜ˆ์ƒ ์‹œ๊ฐ„**: 12-15์‹œ๊ฐ„ + +--- + +## ๐Ÿงช ํ…Œ์ŠคํŠธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ŠคํŠธ + +- [ ] ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ •์ƒ ์‹คํ–‰ ํ™•์ธ +- [ ] menu_objid ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ์กฐ๊ฑด ํ™•์ธ +- [ ] UNIQUE ์ œ์•ฝ์กฐ๊ฑด ํ™•์ธ (menu_objid ํฌํ•จ) +- [ ] ์ธ๋ฑ์Šค ์ƒ์„ฑ ํ™•์ธ + +### ๋ฐฑ์—”๋“œ ํ…Œ์ŠคํŠธ + +- [ ] `getSiblingMenuObjids()` ํ•จ์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ํ˜•์ œ ๋ฉ”๋‰ด ๋ฐ˜ํ™˜ +- [ ] ์ตœ์ƒ์œ„ ๋ฉ”๋‰ด์˜ ๊ฒฝ์šฐ ์ž๊ธฐ ์ž์‹ ๋งŒ ๋ฐ˜ํ™˜ +- [ ] ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ ์‹œ ํ˜•์ œ ๋ฉ”๋‰ด์˜ ๊ฐ’๋„ ํฌํ•จ +- [ ] ๋‹ค๋ฅธ ๋ถ€๋ชจ ๋ฉ”๋‰ด์˜ ์นดํ…Œ๊ณ ๋ฆฌ๋Š” ์กฐํšŒ๋˜์ง€ ์•Š์Œ +- [ ] ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„ํ„ฐ๋ง ์ •์ƒ ์ž‘๋™ + +### ํ”„๋ก ํŠธ์—”๋“œ ํ…Œ์ŠคํŠธ + +- [ ] ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ ๋ชฉ๋ก ์ •์ƒ ํ‘œ์‹œ +- [ ] ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ชฉ๋ก ์ •์ƒ ํ‘œ์‹œ (ํ˜•์ œ ๋ฉ”๋‰ด ํฌํ•จ) +- [ ] ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ถ”๊ฐ€ ์‹œ menuObjid ํฌํ•จ +- [ ] ์ฑ„๋ฒˆ๊ทœ์น™ ๋ชฉ๋ก ์ •์ƒ ํ‘œ์‹œ (ํ˜•์ œ ๋ฉ”๋‰ด ํฌํ•จ) +- [ ] ๋ชจ๋“  CRUD ์ž‘์—… ์ •์ƒ ์ž‘๋™ + +### ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ + +- [ ] ์˜์—…๊ด€๋ฆฌ > ๊ณตํ†ต์ฝ”๋“œ ๊ด€๋ฆฌ์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ์ƒ์„ฑ +- [ ] ์˜์—…๊ด€๋ฆฌ > ๊ณ ๊ฐ๊ด€๋ฆฌ์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅ +- [ ] ์˜์—…๊ด€๋ฆฌ > ๊ณ„์•ฝ๊ด€๋ฆฌ์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅ +- [ ] ๊ตฌ๋งค๊ด€๋ฆฌ์—์„œ๋Š” ์˜์—…๊ด€๋ฆฌ ์นดํ…Œ๊ณ ๋ฆฌ ์‚ฌ์šฉ ๋ถˆ๊ฐ€ +- [ ] ์ฑ„๋ฒˆ๊ทœ์น™๋„ ๋™์ผํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ + +--- + +## ๐Ÿ’ก ์ด์  ์š”์•ฝ + +### 1. ํ˜•์ œ ๋ฉ”๋‰ด ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ณต์œ  +- ๊ฐ™์€ ๋ถ€์„œ์˜ ํ™”๋ฉด๋“ค์ด ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ๊ทœ์น™ ๊ณต์œ  +- ์ค‘๋ณต ์ƒ์„ฑ ๋ถˆํ•„์š” + +### 2. ๊ณตํ†ต์ฝ”๋“œ ๊ด€๋ฆฌ ํ™”๋ฉด ๊ฐ€๋Šฅ +- ์ „์šฉ ๋ฉ”๋‰ด์—์„œ ์ผ๊ด„ ๊ด€๋ฆฌ +- ํ•œ ๊ณณ์—์„œ ์ˆ˜์ •ํ•˜๋ฉด ๋ชจ๋“  ํ˜•์ œ ๋ฉ”๋‰ด์— ๋ฐ˜์˜ + +### 3. ํ…Œ์ด๋ธ” ๋…๋ฆฝ์„ฑ +- ํ…Œ์ด๋ธ”์ด ๋‹ฌ๋ผ๋„ ๊ฐ™์€ ์นดํ…Œ๊ณ ๋ฆฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅ +- ํ…Œ์ด๋ธ” ๊ตฌ์กฐ ๋ณ€๊ฒฝ์— ์˜ํ–ฅ ์—†์Œ + +### 4. ์ง๊ด€์ ์ธ ๊ด€๋ฆฌ +- ๋ฉ”๋‰ด ๊ตฌ์กฐ๊ฐ€ ๊ณง ๋ฐ์ดํ„ฐ ์Šค์ฝ”ํ”„ +- ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์šด ๊ถŒํ•œ ์ฒด๊ณ„ + +### 5. ์œ ์ง€๋ณด์ˆ˜ ์šฉ์ด +- ํ•œ ๊ณณ์—์„œ ์ˆ˜์ •ํ•˜๋ฉด ์ž๋™ ๋ฐ˜์˜ +- ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜ ๋ฐฉ์ง€ + +--- + +## ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„ + +### 1. ๊ณ„ํš ์Šน์ธ +์ด ๊ณ„ํš์„œ๋ฅผ ๊ฒ€ํ† ํ•˜๊ณ  ์Šน์ธ๋ฐ›์œผ๋ฉด ๋ฐ”๋กœ ๊ตฌํ˜„์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + +### 2. ๋‹จ๊ณ„๋ณ„ ๊ตฌํ˜„ +Phase 1๋ถ€ํ„ฐ ์ˆœ์ฐจ์ ์œผ๋กœ ๊ตฌํ˜„ํ•˜์—ฌ ์•ˆ์ •์„ฑ ํ™•๋ณด + +### 3. ์ ์ง„์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ +๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ์ ์ง„์ ์œผ๋กœ ์˜ฌ๋ฐ”๋ฅธ menu_objid๋กœ ์ •๋ฆฌ + +--- + +**์ด ๊ณ„ํš์„œ๋Œ€๋กœ ๊ตฌํ˜„ํ•˜๋ฉด ํ…Œ์ด๋ธ” ๊ธฐ๋ฐ˜ ์Šค์ฝ”ํ”„์˜ ํ•œ๊ณ„๋ฅผ ์™„์ „ํžˆ ๊ทน๋ณตํ•˜๊ณ , ๋ฉ”๋‰ด ๊ตฌ์กฐ ๊ธฐ๋ฐ˜์˜ ์ง๊ด€์ ์ธ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.** + +๊ตฌํ˜„์„ ์‹œ์ž‘ํ• ๊นŒ์š”? +