diff --git a/backend-node/src/services/ddlExecutionService.ts b/backend-node/src/services/ddlExecutionService.ts index 2ed01231..c7a611d3 100644 --- a/backend-node/src/services/ddlExecutionService.ts +++ b/backend-node/src/services/ddlExecutionService.ts @@ -104,7 +104,7 @@ export class DDLExecutionService { await this.saveTableMetadata(client, tableName, description); // 5-3. 컬럼 메타데이터 저장 - await this.saveColumnMetadata(client, tableName, columns); + await this.saveColumnMetadata(client, tableName, columns, userCompanyCode); }); // 6. 성공 로그 기록 @@ -272,7 +272,7 @@ export class DDLExecutionService { await client.query(ddlQuery); // 6-2. 컬럼 메타데이터 저장 - await this.saveColumnMetadata(client, tableName, [column]); + await this.saveColumnMetadata(client, tableName, [column], userCompanyCode); }); // 7. 성공 로그 기록 @@ -446,7 +446,8 @@ CREATE TABLE "${tableName}" (${baseColumns}, private async saveColumnMetadata( client: any, tableName: string, - columns: CreateColumnDefinition[] + columns: CreateColumnDefinition[], + companyCode: string ): Promise { // 먼저 table_labels에 테이블 정보가 있는지 확인하고 없으면 생성 await client.query( @@ -508,19 +509,19 @@ CREATE TABLE "${tableName}" (${baseColumns}, await client.query( ` INSERT INTO table_type_columns ( - table_name, column_name, input_type, detail_settings, + table_name, column_name, company_code, input_type, detail_settings, is_nullable, display_order, created_date, updated_date ) VALUES ( - $1, $2, $3, '{}', - 'Y', $4, now(), now() + $1, $2, $3, $4, '{}', + 'Y', $5, now(), now() ) - ON CONFLICT (table_name, column_name) + ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET - input_type = $3, - display_order = $4, + input_type = $4, + display_order = $5, updated_date = now() `, - [tableName, defaultCol.name, defaultCol.inputType, defaultCol.order] + [tableName, defaultCol.name, companyCode, defaultCol.inputType, defaultCol.order] ); } @@ -535,20 +536,20 @@ CREATE TABLE "${tableName}" (${baseColumns}, await client.query( ` INSERT INTO table_type_columns ( - table_name, column_name, input_type, detail_settings, + table_name, column_name, company_code, input_type, detail_settings, is_nullable, display_order, created_date, updated_date ) VALUES ( - $1, $2, $3, $4, - 'Y', $5, now(), now() + $1, $2, $3, $4, $5, + 'Y', $6, now(), now() ) - ON CONFLICT (table_name, column_name) + ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET - input_type = $3, - detail_settings = $4, - display_order = $5, + input_type = $4, + detail_settings = $5, + display_order = $6, updated_date = now() `, - [tableName, column.name, inputType, detailSettings, i] + [tableName, column.name, companyCode, inputType, detailSettings, i] ); } diff --git a/backend-node/src/services/menuService.ts b/backend-node/src/services/menuService.ts index b22beb88..86df579c 100644 --- a/backend-node/src/services/menuService.ts +++ b/backend-node/src/services/menuService.ts @@ -36,29 +36,61 @@ export async function getSiblingMenuObjids(menuObjid: number): Promise try { logger.debug("메뉴 스코프 조회 시작", { menuObjid }); - // 1. 현재 메뉴 자신을 포함 - const menuObjids = [menuObjid]; + // 1. 현재 메뉴 정보 조회 (부모 ID 확인) + const currentMenuQuery = ` + SELECT parent_obj_id FROM menu_info + WHERE objid = $1 + `; + const currentMenuResult = await pool.query(currentMenuQuery, [menuObjid]); - // 2. 현재 메뉴의 자식 메뉴들 조회 - const childrenQuery = ` + if (currentMenuResult.rows.length === 0) { + logger.warn("메뉴를 찾을 수 없음, 자기 자신만 반환", { menuObjid }); + return [menuObjid]; + } + + const parentObjId = Number(currentMenuResult.rows[0].parent_obj_id); + + // 2. 최상위 메뉴(parent_obj_id = 0)는 자기 자신만 반환 + if (parentObjId === 0) { + logger.debug("최상위 메뉴, 자기 자신만 반환", { menuObjid }); + return [menuObjid]; + } + + // 3. 형제 메뉴들 조회 (같은 부모를 가진 메뉴들) + const siblingsQuery = ` SELECT objid FROM menu_info WHERE parent_obj_id = $1 ORDER BY objid `; - const childrenResult = await pool.query(childrenQuery, [menuObjid]); + const siblingsResult = await pool.query(siblingsQuery, [parentObjId]); - const childObjids = childrenResult.rows.map((row) => Number(row.objid)); + const siblingObjids = siblingsResult.rows.map((row) => Number(row.objid)); - // 3. 자신 + 자식을 합쳐서 정렬 - const allObjids = Array.from(new Set([...menuObjids, ...childObjids])).sort((a, b) => a - b); + // 4. 각 형제 메뉴(자기 자신 포함)의 자식 메뉴들도 조회 + const allObjids = [...siblingObjids]; + + for (const siblingObjid of siblingObjids) { + const childrenQuery = ` + SELECT objid FROM menu_info + WHERE parent_obj_id = $1 + ORDER BY objid + `; + const childrenResult = await pool.query(childrenQuery, [siblingObjid]); + const childObjids = childrenResult.rows.map((row) => Number(row.objid)); + allObjids.push(...childObjids); + } + + // 5. 중복 제거 및 정렬 + const uniqueObjids = Array.from(new Set(allObjids)).sort((a, b) => a - b); logger.debug("메뉴 스코프 조회 완료", { - menuObjid, - childCount: childObjids.length, - totalCount: allObjids.length + menuObjid, + parentObjId, + siblingCount: siblingObjids.length, + totalCount: uniqueObjids.length }); - return allObjids; + return uniqueObjids; } catch (error: any) { logger.error("메뉴 스코프 조회 실패", { menuObjid, diff --git a/backend-node/src/services/tableCategoryValueService.ts b/backend-node/src/services/tableCategoryValueService.ts index 29cad453..c5d51db5 100644 --- a/backend-node/src/services/tableCategoryValueService.ts +++ b/backend-node/src/services/tableCategoryValueService.ts @@ -179,7 +179,8 @@ class TableCategoryValueService { } else { // 일반 회사: 자신의 카테고리 값만 조회 if (menuObjid && siblingObjids.length > 0) { - // 메뉴 스코프 적용 + // 메뉴 스코프 적용 + created_menu_objid 필터링 + // 현재 메뉴 스코프(형제 메뉴)에서 생성된 값만 표시 query = ` SELECT value_id AS "valueId", @@ -197,6 +198,7 @@ class TableCategoryValueService { is_default AS "isDefault", company_code AS "companyCode", menu_objid AS "menuObjid", + created_menu_objid AS "createdMenuObjid", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy", @@ -206,6 +208,10 @@ class TableCategoryValueService { AND column_name = $2 AND menu_objid = ANY($3) AND company_code = $4 + AND ( + created_menu_objid = ANY($3) -- 형제 메뉴에서 생성된 값만 + OR created_menu_objid IS NULL -- 레거시 데이터 (모든 메뉴에서 보임) + ) `; params = [tableName, columnName, siblingObjids, companyCode]; } else { @@ -331,8 +337,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, menu_objid, created_by - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) + is_active, is_default, company_code, menu_objid, created_menu_objid, created_by + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING value_id AS "valueId", table_name AS "tableName", @@ -349,6 +355,7 @@ class TableCategoryValueService { is_default AS "isDefault", company_code AS "companyCode", menu_objid AS "menuObjid", + created_menu_objid AS "createdMenuObjid", created_at AS "createdAt", created_by AS "createdBy" `; @@ -368,6 +375,7 @@ class TableCategoryValueService { value.isDefault || false, companyCode, menuObjid, // ← 메뉴 OBJID 저장 + menuObjid, // ← 🆕 생성 메뉴 OBJID 저장 (같은 값) userId, ]); diff --git a/frontend/components/screen/widgets/CategoryWidget.tsx b/frontend/components/screen/widgets/CategoryWidget.tsx index 2974ed60..a4e93256 100644 --- a/frontend/components/screen/widgets/CategoryWidget.tsx +++ b/frontend/components/screen/widgets/CategoryWidget.tsx @@ -49,6 +49,7 @@ export function CategoryWidget({ widgetId, tableName, menuObjid, component, ...p const effectiveMenuObjid = menuObjid || props.menuObjid; const [selectedColumn, setSelectedColumn] = useState<{ + uniqueKey: string; // 테이블명.컬럼명 형식 columnName: string; columnLabel: string; tableName: string; @@ -98,10 +99,12 @@ export function CategoryWidget({ widgetId, tableName, menuObjid, component, ...p
- setSelectedColumn({ columnName, columnLabel, tableName }) - } + selectedColumn={selectedColumn?.uniqueKey || null} + onColumnSelect={(uniqueKey, columnLabel, tableName) => { + // uniqueKey는 "테이블명.컬럼명" 형식 + const columnName = uniqueKey.split('.')[1]; + setSelectedColumn({ uniqueKey, columnName, columnLabel, tableName }); + }} menuObjid={effectiveMenuObjid} />
@@ -118,6 +121,7 @@ export function CategoryWidget({ widgetId, tableName, menuObjid, component, ...p
{selectedColumn ? ( {columns.map((column) => { const uniqueKey = `${column.tableName}.${column.columnName}`; + const isSelected = selectedColumn === uniqueKey; // 테이블명.컬럼명으로 비교 return (
onColumnSelect(column.columnName, column.columnLabel || column.columnName, column.tableName)} + onClick={() => onColumnSelect(uniqueKey, column.columnLabel || column.columnName, column.tableName)} className={`cursor-pointer rounded-lg border px-4 py-2 transition-all ${ - selectedColumn === column.columnName ? "border-primary bg-primary/10 shadow-sm" : "hover:bg-muted/50" + isSelected ? "border-primary bg-primary/10 shadow-sm" : "hover:bg-muted/50" }`} >

{column.columnLabel || column.columnName}