refactor: Update references from table_column_category_values to category_values

- Changed all occurrences of `table_column_category_values` to `category_values` in the backend services and controllers to standardize the terminology.
- Updated SQL queries to reflect the new table name, ensuring proper data retrieval and management.
- Adjusted comments and documentation to clarify the purpose of the `category_values` table in the context of category management.

These changes enhance code clarity and maintain consistency across the application.
This commit is contained in:
kjs 2026-03-09 13:46:38 +09:00
parent 13506912d9
commit f6a02b5182
21 changed files with 144 additions and 107 deletions

View File

@ -818,13 +818,13 @@ export const getCategoryValueCascadingParentOptions = async (
const group = groupResult.rows[0]; const group = groupResult.rows[0];
// 부모 카테고리 값 조회 (table_column_category_values에서) // 부모 카테고리 값 조회 (category_values에서)
let optionsQuery = ` let optionsQuery = `
SELECT SELECT
value_code as value, value_code as value,
value_label as label, value_label as label,
value_order as display_order value_order as display_order
FROM table_column_category_values FROM category_values
WHERE table_name = $1 WHERE table_name = $1
AND column_name = $2 AND column_name = $2
AND is_active = true AND is_active = true
@ -916,13 +916,13 @@ export const getCategoryValueCascadingChildOptions = async (
const group = groupResult.rows[0]; const group = groupResult.rows[0];
// 자식 카테고리 값 조회 (table_column_category_values에서) // 자식 카테고리 값 조회 (category_values에서)
let optionsQuery = ` let optionsQuery = `
SELECT SELECT
value_code as value, value_code as value,
value_label as label, value_label as label,
value_order as display_order value_order as display_order
FROM table_column_category_values FROM category_values
WHERE table_name = $1 WHERE table_name = $1
AND column_name = $2 AND column_name = $2
AND is_active = true AND is_active = true

View File

@ -417,10 +417,10 @@ export class EntityJoinController {
// 1. 현재 테이블의 Entity 조인 설정 조회 // 1. 현재 테이블의 Entity 조인 설정 조회
const allJoinConfigs = await entityJoinService.detectEntityJoins(tableName, undefined, companyCode); const allJoinConfigs = await entityJoinService.detectEntityJoins(tableName, undefined, companyCode);
// 🆕 화면 디자이너용: table_column_category_values는 카테고리 드롭다운용이므로 제외 // 🆕 화면 디자이너용: category_values는 카테고리 드롭다운용이므로 제외
// 카테고리 값은 엔티티 조인 컬럼이 아니라 셀렉트박스 옵션으로 사용됨 // 카테고리 값은 엔티티 조인 컬럼이 아니라 셀렉트박스 옵션으로 사용됨
const joinConfigs = allJoinConfigs.filter( const joinConfigs = allJoinConfigs.filter(
(config) => config.referenceTable !== "table_column_category_values" (config) => config.referenceTable !== "category_values"
); );
if (joinConfigs.length === 0) { if (joinConfigs.length === 0) {

View File

@ -92,7 +92,7 @@ export class EntityJoinService {
if (column.input_type === "category") { if (column.input_type === "category") {
// 카테고리 타입: reference 정보가 비어있어도 자동 설정 // 카테고리 타입: reference 정보가 비어있어도 자동 설정
referenceTable = referenceTable || "table_column_category_values"; referenceTable = referenceTable || "category_values";
referenceColumn = referenceColumn || "value_code"; referenceColumn = referenceColumn || "value_code";
displayColumn = displayColumn || "value_label"; displayColumn = displayColumn || "value_label";
@ -308,7 +308,7 @@ export class EntityJoinService {
const usedAliasesForColumns = new Set<string>(); const usedAliasesForColumns = new Set<string>();
// joinConfigs를 참조 테이블 + 소스 컬럼별로 중복 제거하여 별칭 생성 // joinConfigs를 참조 테이블 + 소스 컬럼별로 중복 제거하여 별칭 생성
// (table_column_category_values는 같은 테이블이라도 sourceColumn마다 별도 JOIN 필요) // (category_values는 같은 테이블이라도 sourceColumn마다 별도 JOIN 필요)
const uniqueReferenceTableConfigs = joinConfigs.reduce((acc, config) => { const uniqueReferenceTableConfigs = joinConfigs.reduce((acc, config) => {
if ( if (
!acc.some( !acc.some(
@ -336,7 +336,7 @@ export class EntityJoinService {
counter++; counter++;
} }
usedAliasesForColumns.add(alias); usedAliasesForColumns.add(alias);
// 같은 테이블이라도 sourceColumn이 다르면 별도 별칭 생성 (table_column_category_values 대응) // 같은 테이블이라도 sourceColumn이 다르면 별도 별칭 생성 (category_values 대응)
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`; const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
aliasMap.set(aliasKey, alias); aliasMap.set(aliasKey, alias);
logger.info( logger.info(
@ -455,9 +455,10 @@ export class EntityJoinService {
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`; const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
const alias = aliasMap.get(aliasKey); const alias = aliasMap.get(aliasKey);
// table_column_category_values는 특별한 조인 조건 필요 (회사별 필터링) // category_values는 특별한 조인 조건 필요 (회사별 필터링)
if (config.referenceTable === "table_column_category_values") { // is_active 필터 제거: 비활성화된 카테고리도 라벨로 표시되어야 함
return `LEFT JOIN ${config.referenceTable} ${alias} ON main."${config.sourceColumn}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}' AND ${alias}.company_code = main.company_code AND ${alias}.is_active = true`; if (config.referenceTable === "category_values") {
return `LEFT JOIN ${config.referenceTable} ${alias} ON main."${config.sourceColumn}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}' AND ${alias}.company_code = main.company_code`;
} }
// user_info는 전역 테이블이므로 company_code 조건 없이 조인 // user_info는 전역 테이블이므로 company_code 조건 없이 조인
@ -528,10 +529,10 @@ export class EntityJoinService {
return "join"; return "join";
} }
// table_column_category_values는 특수 조인 조건이 필요하므로 캐시 불가 // category_values는 특수 조인 조건이 필요하므로 캐시 불가
if (config.referenceTable === "table_column_category_values") { if (config.referenceTable === "category_values") {
logger.info( logger.info(
`🎯 table_column_category_values는 캐시 전략 불가: ${config.sourceColumn}` `🎯 category_values는 캐시 전략 불가: ${config.sourceColumn}`
); );
return "join"; return "join";
} }
@ -723,10 +724,10 @@ export class EntityJoinService {
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`; const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
const alias = aliasMap.get(aliasKey); const alias = aliasMap.get(aliasKey);
// table_column_category_values는 특별한 조인 조건 필요 (회사별 필터링만) // category_values는 특별한 조인 조건 필요 (회사별 필터링만)
if (config.referenceTable === "table_column_category_values") { // is_active 필터 제거: 비활성화된 카테고리도 라벨로 표시되어야 함
// 멀티테넌시: 회사 데이터만 사용 (공통 데이터 제외) if (config.referenceTable === "category_values") {
return `LEFT JOIN ${config.referenceTable} ${alias} ON main."${config.sourceColumn}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}' AND ${alias}.company_code = main.company_code AND ${alias}.is_active = true`; return `LEFT JOIN ${config.referenceTable} ${alias} ON main."${config.sourceColumn}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}' AND ${alias}.company_code = main.company_code`;
} }
return `LEFT JOIN ${config.referenceTable} ${alias} ON main."${config.sourceColumn}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT`; return `LEFT JOIN ${config.referenceTable} ${alias} ON main."${config.sourceColumn}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT`;

View File

@ -3098,7 +3098,7 @@ export class MenuCopyService {
} }
const allValuesResult = await client.query( const allValuesResult = await client.query(
`SELECT * FROM table_column_category_values `SELECT * FROM category_values
WHERE company_code = $1 WHERE company_code = $1
AND (${columnConditions.join(" OR ")}) AND (${columnConditions.join(" OR ")})
ORDER BY depth NULLS FIRST, parent_value_id NULLS FIRST, value_order`, ORDER BY depth NULLS FIRST, parent_value_id NULLS FIRST, value_order`,
@ -3115,7 +3115,7 @@ export class MenuCopyService {
// 5. 대상 회사에 이미 존재하는 값 한 번에 조회 // 5. 대상 회사에 이미 존재하는 값 한 번에 조회
const existingValuesResult = await client.query( const existingValuesResult = await client.query(
`SELECT value_id, table_name, column_name, value_code `SELECT value_id, table_name, column_name, value_code
FROM table_column_category_values WHERE company_code = $1`, FROM category_values WHERE company_code = $1`,
[targetCompanyCode] [targetCompanyCode]
); );
const existingValueKeys = new Map( const existingValueKeys = new Map(
@ -3194,7 +3194,7 @@ export class MenuCopyService {
}); });
const insertResult = await client.query( const insertResult = await client.query(
`INSERT INTO table_column_category_values ( `INSERT INTO category_values (
table_name, column_name, value_code, value_label, value_order, table_name, column_name, value_code, value_label, value_order,
parent_value_id, depth, description, color, icon, parent_value_id, depth, description, color, icon,
is_active, is_default, created_at, created_by, company_code, menu_objid is_active, is_default, created_at, created_by, company_code, menu_objid

View File

@ -31,7 +31,7 @@ class TableCategoryValueService {
tc.column_name AS "columnLabel", tc.column_name AS "columnLabel",
COUNT(cv.value_id) AS "valueCount" COUNT(cv.value_id) AS "valueCount"
FROM table_type_columns tc FROM table_type_columns tc
LEFT JOIN table_column_category_values cv LEFT JOIN category_values cv
ON tc.table_name = cv.table_name ON tc.table_name = cv.table_name
AND tc.column_name = cv.column_name AND tc.column_name = cv.column_name
AND cv.is_active = true AND cv.is_active = true
@ -50,7 +50,7 @@ class TableCategoryValueService {
tc.column_name AS "columnLabel", tc.column_name AS "columnLabel",
COUNT(cv.value_id) AS "valueCount" COUNT(cv.value_id) AS "valueCount"
FROM table_type_columns tc FROM table_type_columns tc
LEFT JOIN table_column_category_values cv LEFT JOIN category_values cv
ON tc.table_name = cv.table_name ON tc.table_name = cv.table_name
AND tc.column_name = cv.column_name AND tc.column_name = cv.column_name
AND cv.is_active = true AND cv.is_active = true
@ -110,7 +110,7 @@ class TableCategoryValueService {
) tc ) tc
LEFT JOIN ( LEFT JOIN (
SELECT table_name, column_name, COUNT(*) as cnt SELECT table_name, column_name, COUNT(*) as cnt
FROM table_column_category_values FROM category_values
WHERE is_active = true WHERE is_active = true
GROUP BY table_name, column_name GROUP BY table_name, column_name
) cv_count ON tc.table_name = cv_count.table_name AND tc.column_name = cv_count.column_name ) cv_count ON tc.table_name = cv_count.table_name AND tc.column_name = cv_count.column_name
@ -133,7 +133,7 @@ class TableCategoryValueService {
) tc ) tc
LEFT JOIN ( LEFT JOIN (
SELECT table_name, column_name, COUNT(*) as cnt SELECT table_name, column_name, COUNT(*) as cnt
FROM table_column_category_values FROM category_values
WHERE is_active = true AND company_code = $1 WHERE is_active = true AND company_code = $1
GROUP BY table_name, column_name GROUP BY table_name, column_name
) cv_count ON tc.table_name = cv_count.table_name AND tc.column_name = cv_count.column_name ) cv_count ON tc.table_name = cv_count.table_name AND tc.column_name = cv_count.column_name
@ -207,7 +207,7 @@ class TableCategoryValueService {
is_active AS "isActive", is_active AS "isActive",
is_default AS "isDefault", is_default AS "isDefault",
company_code AS "companyCode", company_code AS "companyCode",
NULL::numeric AS "menuObjid", menu_objid AS "menuObjid",
created_at AS "createdAt", created_at AS "createdAt",
updated_at AS "updatedAt", updated_at AS "updatedAt",
created_by AS "createdBy", created_by AS "createdBy",
@ -289,7 +289,7 @@ class TableCategoryValueService {
// 최고 관리자: 모든 회사에서 중복 체크 // 최고 관리자: 모든 회사에서 중복 체크
duplicateQuery = ` duplicateQuery = `
SELECT value_id SELECT value_id
FROM table_column_category_values FROM category_values
WHERE table_name = $1 WHERE table_name = $1
AND column_name = $2 AND column_name = $2
AND value_code = $3 AND value_code = $3
@ -300,7 +300,7 @@ class TableCategoryValueService {
// 일반 회사: 자신의 회사에서만 중복 체크 // 일반 회사: 자신의 회사에서만 중복 체크
duplicateQuery = ` duplicateQuery = `
SELECT value_id SELECT value_id
FROM table_column_category_values FROM category_values
WHERE table_name = $1 WHERE table_name = $1
AND column_name = $2 AND column_name = $2
AND value_code = $3 AND value_code = $3
@ -316,8 +316,41 @@ class TableCategoryValueService {
throw new Error("이미 존재하는 코드입니다"); throw new Error("이미 존재하는 코드입니다");
} }
// 라벨 중복 체크 (같은 테이블+컬럼+회사에서 동일한 라벨명 방지)
let labelDupQuery: string;
let labelDupParams: any[];
if (companyCode === "*") {
labelDupQuery = `
SELECT value_id
FROM category_values
WHERE table_name = $1
AND column_name = $2
AND value_label = $3
AND is_active = true
`;
labelDupParams = [value.tableName, value.columnName, value.valueLabel];
} else {
labelDupQuery = `
SELECT value_id
FROM category_values
WHERE table_name = $1
AND column_name = $2
AND value_label = $3
AND company_code = $4
AND is_active = true
`;
labelDupParams = [value.tableName, value.columnName, value.valueLabel, companyCode];
}
const labelDupResult = await pool.query(labelDupQuery, labelDupParams);
if (labelDupResult.rows.length > 0) {
throw new Error(`이미 동일한 이름의 카테고리 값이 존재합니다: "${value.valueLabel}"`);
}
const insertQuery = ` const insertQuery = `
INSERT INTO table_column_category_values ( INSERT INTO category_values (
table_name, column_name, value_code, value_label, value_order, table_name, column_name, value_code, value_label, value_order,
parent_value_id, depth, description, color, icon, parent_value_id, depth, description, color, icon,
is_active, is_default, company_code, menu_objid, created_by is_active, is_default, company_code, menu_objid, created_by
@ -425,6 +458,32 @@ class TableCategoryValueService {
values.push(updates.isDefault); values.push(updates.isDefault);
} }
// 라벨 수정 시 중복 체크 (자기 자신 제외)
if (updates.valueLabel !== undefined) {
const currentRow = await pool.query(
`SELECT table_name, column_name, company_code FROM category_values WHERE value_id = $1`,
[valueId]
);
if (currentRow.rows.length > 0) {
const { table_name, column_name, company_code } = currentRow.rows[0];
const labelDupResult = await pool.query(
`SELECT value_id FROM category_values
WHERE table_name = $1
AND column_name = $2
AND value_label = $3
AND company_code = $4
AND is_active = true
AND value_id != $5`,
[table_name, column_name, updates.valueLabel, company_code, valueId]
);
if (labelDupResult.rows.length > 0) {
throw new Error(`이미 동일한 이름의 카테고리 값이 존재합니다: "${updates.valueLabel}"`);
}
}
}
setClauses.push(`updated_at = NOW()`); setClauses.push(`updated_at = NOW()`);
setClauses.push(`updated_by = $${paramIndex++}`); setClauses.push(`updated_by = $${paramIndex++}`);
values.push(userId); values.push(userId);
@ -436,7 +495,7 @@ class TableCategoryValueService {
// 최고 관리자: 모든 카테고리 값 수정 가능 // 최고 관리자: 모든 카테고리 값 수정 가능
values.push(valueId); values.push(valueId);
updateQuery = ` updateQuery = `
UPDATE table_column_category_values UPDATE category_values
SET ${setClauses.join(", ")} SET ${setClauses.join(", ")}
WHERE value_id = $${paramIndex++} WHERE value_id = $${paramIndex++}
RETURNING RETURNING
@ -459,7 +518,7 @@ class TableCategoryValueService {
// 일반 회사: 자신의 카테고리 값만 수정 가능 // 일반 회사: 자신의 카테고리 값만 수정 가능
values.push(valueId, companyCode); values.push(valueId, companyCode);
updateQuery = ` updateQuery = `
UPDATE table_column_category_values UPDATE category_values
SET ${setClauses.join(", ")} SET ${setClauses.join(", ")}
WHERE value_id = $${paramIndex++} WHERE value_id = $${paramIndex++}
AND company_code = $${paramIndex++} AND company_code = $${paramIndex++}
@ -516,14 +575,14 @@ class TableCategoryValueService {
if (companyCode === "*") { if (companyCode === "*") {
valueQuery = ` valueQuery = `
SELECT table_name, column_name, value_code SELECT table_name, column_name, value_code
FROM table_column_category_values FROM category_values
WHERE value_id = $1 WHERE value_id = $1
`; `;
valueParams = [valueId]; valueParams = [valueId];
} else { } else {
valueQuery = ` valueQuery = `
SELECT table_name, column_name, value_code SELECT table_name, column_name, value_code
FROM table_column_category_values FROM category_values
WHERE value_id = $1 WHERE value_id = $1
AND company_code = $2 AND company_code = $2
`; `;
@ -635,10 +694,10 @@ class TableCategoryValueService {
if (companyCode === "*") { if (companyCode === "*") {
query = ` query = `
WITH RECURSIVE category_tree AS ( WITH RECURSIVE category_tree AS (
SELECT value_id FROM table_column_category_values WHERE parent_value_id = $1 SELECT value_id FROM category_values WHERE parent_value_id = $1
UNION ALL UNION ALL
SELECT cv.value_id SELECT cv.value_id
FROM table_column_category_values cv FROM category_values cv
INNER JOIN category_tree ct ON cv.parent_value_id = ct.value_id INNER JOIN category_tree ct ON cv.parent_value_id = ct.value_id
) )
SELECT value_id FROM category_tree SELECT value_id FROM category_tree
@ -647,11 +706,11 @@ class TableCategoryValueService {
} else { } else {
query = ` query = `
WITH RECURSIVE category_tree AS ( WITH RECURSIVE category_tree AS (
SELECT value_id FROM table_column_category_values SELECT value_id FROM category_values
WHERE parent_value_id = $1 AND company_code = $2 WHERE parent_value_id = $1 AND company_code = $2
UNION ALL UNION ALL
SELECT cv.value_id SELECT cv.value_id
FROM table_column_category_values cv FROM category_values cv
INNER JOIN category_tree ct ON cv.parent_value_id = ct.value_id INNER JOIN category_tree ct ON cv.parent_value_id = ct.value_id
WHERE cv.company_code = $2 WHERE cv.company_code = $2
) )
@ -697,10 +756,10 @@ class TableCategoryValueService {
let labelParams: any[]; let labelParams: any[];
if (companyCode === "*") { if (companyCode === "*") {
labelQuery = `SELECT value_label FROM table_column_category_values WHERE value_id = $1`; labelQuery = `SELECT value_label FROM category_values WHERE value_id = $1`;
labelParams = [id]; labelParams = [id];
} else { } else {
labelQuery = `SELECT value_label FROM table_column_category_values WHERE value_id = $1 AND company_code = $2`; labelQuery = `SELECT value_label FROM category_values WHERE value_id = $1 AND company_code = $2`;
labelParams = [id, companyCode]; labelParams = [id, companyCode];
} }
@ -730,10 +789,10 @@ class TableCategoryValueService {
let deleteParams: any[]; let deleteParams: any[];
if (companyCode === "*") { if (companyCode === "*") {
deleteQuery = `DELETE FROM table_column_category_values WHERE value_id = $1`; deleteQuery = `DELETE FROM category_values WHERE value_id = $1`;
deleteParams = [id]; deleteParams = [id];
} else { } else {
deleteQuery = `DELETE FROM table_column_category_values WHERE value_id = $1 AND company_code = $2`; deleteQuery = `DELETE FROM category_values WHERE value_id = $1 AND company_code = $2`;
deleteParams = [id, companyCode]; deleteParams = [id, companyCode];
} }
@ -770,7 +829,7 @@ class TableCategoryValueService {
if (companyCode === "*") { if (companyCode === "*") {
// 최고 관리자: 모든 카테고리 값 일괄 삭제 가능 // 최고 관리자: 모든 카테고리 값 일괄 삭제 가능
deleteQuery = ` deleteQuery = `
UPDATE table_column_category_values UPDATE category_values
SET is_active = false, updated_at = NOW(), updated_by = $2 SET is_active = false, updated_at = NOW(), updated_by = $2
WHERE value_id = ANY($1::int[]) WHERE value_id = ANY($1::int[])
`; `;
@ -778,7 +837,7 @@ class TableCategoryValueService {
} else { } else {
// 일반 회사: 자신의 카테고리 값만 일괄 삭제 가능 // 일반 회사: 자신의 카테고리 값만 일괄 삭제 가능
deleteQuery = ` deleteQuery = `
UPDATE table_column_category_values UPDATE category_values
SET is_active = false, updated_at = NOW(), updated_by = $3 SET is_active = false, updated_at = NOW(), updated_by = $3
WHERE value_id = ANY($1::int[]) WHERE value_id = ANY($1::int[])
AND company_code = $2 AND company_code = $2
@ -819,7 +878,7 @@ class TableCategoryValueService {
if (companyCode === "*") { if (companyCode === "*") {
// 최고 관리자: 모든 카테고리 값 순서 변경 가능 // 최고 관리자: 모든 카테고리 값 순서 변경 가능
updateQuery = ` updateQuery = `
UPDATE table_column_category_values UPDATE category_values
SET value_order = $1, updated_at = NOW() SET value_order = $1, updated_at = NOW()
WHERE value_id = $2 WHERE value_id = $2
`; `;
@ -827,7 +886,7 @@ class TableCategoryValueService {
} else { } else {
// 일반 회사: 자신의 카테고리 값만 순서 변경 가능 // 일반 회사: 자신의 카테고리 값만 순서 변경 가능
updateQuery = ` updateQuery = `
UPDATE table_column_category_values UPDATE category_values
SET value_order = $1, updated_at = NOW() SET value_order = $1, updated_at = NOW()
WHERE value_id = $2 WHERE value_id = $2
AND company_code = $3 AND company_code = $3
@ -1379,48 +1438,23 @@ class TableCategoryValueService {
let query: string; let query: string;
let params: any[]; let params: any[];
// is_active 필터 제거: 비활성화된 카테고리도 라벨로 표시되어야 함
if (companyCode === "*") { if (companyCode === "*") {
// 최고 관리자: 두 테이블 모두에서 조회 (UNION으로 병합)
// 두 번째 쿼리용 플레이스홀더: $n+1 ~ $2n
const placeholders2 = valueCodes.map((_, i) => `$${n + i + 1}`).join(", ");
query = ` query = `
SELECT value_code, value_label FROM ( SELECT DISTINCT value_code, value_label
SELECT value_code, value_label FROM category_values
FROM table_column_category_values WHERE value_code IN (${placeholders1})
WHERE value_code IN (${placeholders1})
AND is_active = true
UNION ALL
SELECT value_code, value_label
FROM category_values
WHERE value_code IN (${placeholders2})
AND is_active = true
) combined
`; `;
params = [...valueCodes, ...valueCodes]; params = [...valueCodes];
} else { } else {
// 일반 회사: 두 테이블에서 자신의 카테고리 값 + 공통 카테고리 값 조회 const companyIdx = n + 1;
// 첫 번째: $1~$n (valueCodes), $n+1 (companyCode)
// 두 번째: $n+2~$2n+1 (valueCodes), $2n+2 (companyCode)
const companyIdx1 = n + 1;
const placeholders2 = valueCodes.map((_, i) => `$${n + 1 + i + 1}`).join(", ");
const companyIdx2 = 2 * n + 2;
query = ` query = `
SELECT value_code, value_label FROM ( SELECT DISTINCT value_code, value_label
SELECT value_code, value_label FROM category_values
FROM table_column_category_values WHERE value_code IN (${placeholders1})
WHERE value_code IN (${placeholders1}) AND (company_code = $${companyIdx} OR company_code = '*')
AND is_active = true
AND (company_code = $${companyIdx1} OR company_code = '*')
UNION ALL
SELECT value_code, value_label
FROM category_values
WHERE value_code IN (${placeholders2})
AND is_active = true
AND (company_code = $${companyIdx2} OR company_code = '*')
) combined
`; `;
params = [...valueCodes, companyCode, ...valueCodes, companyCode]; params = [...valueCodes, companyCode];
} }
const result = await pool.query(query, params); const result = await pool.query(query, params);
@ -1488,7 +1522,7 @@ class TableCategoryValueService {
// 최고 관리자: 모든 카테고리 값 조회 // 최고 관리자: 모든 카테고리 값 조회
query = ` query = `
SELECT value_code, value_label SELECT value_code, value_label
FROM table_column_category_values FROM category_values
WHERE table_name = $1 WHERE table_name = $1
AND column_name = $2 AND column_name = $2
AND is_active = true AND is_active = true
@ -1498,7 +1532,7 @@ class TableCategoryValueService {
// 일반 회사: 자신의 카테고리 값 + 공통 카테고리 값 조회 // 일반 회사: 자신의 카테고리 값 + 공통 카테고리 값 조회
query = ` query = `
SELECT value_code, value_label SELECT value_code, value_label
FROM table_column_category_values FROM category_values
WHERE table_name = $1 WHERE table_name = $1
AND column_name = $2 AND column_name = $2
AND is_active = true AND is_active = true

View File

@ -3505,7 +3505,7 @@ export class TableManagementService {
const referenceTableColumns = new Map<string, string[]>(); const referenceTableColumns = new Map<string, string[]>();
const uniqueRefTables = new Set( const uniqueRefTables = new Set(
joinConfigs joinConfigs
.filter((c) => c.referenceTable !== "table_column_category_values") // 카테고리는 제외 .filter((c) => c.referenceTable !== "category_values") // 카테고리는 제외
.map((c) => `${c.referenceTable}:${c.sourceColumn}`) .map((c) => `${c.referenceTable}:${c.sourceColumn}`)
); );
@ -4310,8 +4310,8 @@ export class TableManagementService {
]; ];
for (const config of joinConfigs) { for (const config of joinConfigs) {
// table_column_category_values는 특수 조인 조건이 필요하므로 항상 DB 조인 // category_values는 특수 조인 조건이 필요하므로 항상 DB 조인
if (config.referenceTable === "table_column_category_values") { if (config.referenceTable === "category_values") {
dbJoins.push(config); dbJoins.push(config);
console.log(`🔗 DB 조인 (특수 조건): ${config.referenceTable}`); console.log(`🔗 DB 조인 (특수 조건): ${config.referenceTable}`);
continue; continue;

View File

@ -378,7 +378,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
for (const col of categoryColumns) { for (const col of categoryColumns) {
try { try {
// menuObjid가 있으면 쿼리 파라미터로 전달 (메뉴별 카테고리 색상 적용) // menuObjid가 있으면 쿼리 파라미터로 전달 (메뉴별 카테고리 색상 적용)
const queryParams = menuObjid ? `?menuObjid=${menuObjid}` : ""; const queryParams = menuObjid ? `?menuObjid=${menuObjid}&includeInactive=true` : "?includeInactive=true";
const response = await apiClient.get( const response = await apiClient.get(
`/table-categories/${component.tableName}/${col.columnName}/values${queryParams}`, `/table-categories/${component.tableName}/${col.columnName}/values${queryParams}`,
); );

View File

@ -796,7 +796,7 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
console.log(`📡 [RepeaterInput] 카테고리 매핑 로드: ${tableName}/${columnName}`); console.log(`📡 [RepeaterInput] 카테고리 매핑 로드: ${tableName}/${columnName}`);
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values`); const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values?includeInactive=true`);
if (response.data.success && response.data.data && Array.isArray(response.data.data)) { if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
const mapping: Record<string, { label: string; color: string }> = {}; const mapping: Record<string, { label: string; color: string }> = {};
@ -838,7 +838,7 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
try { try {
console.log(`📡 [RepeaterInput] 조인 테이블 카테고리 매핑 로드: ${joinedTableName}/${columnName}`); console.log(`📡 [RepeaterInput] 조인 테이블 카테고리 매핑 로드: ${joinedTableName}/${columnName}`);
const response = await apiClient.get(`/table-categories/${joinedTableName}/${columnName}/values`); const response = await apiClient.get(`/table-categories/${joinedTableName}/${columnName}/values?includeInactive=true`);
if (response.data.success && response.data.data && Array.isArray(response.data.data)) { if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
const mapping: Record<string, { label: string; color: string }> = {}; const mapping: Record<string, { label: string; color: string }> = {};

View File

@ -367,7 +367,7 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
for (const columnName of categoryColumns) { for (const columnName of categoryColumns) {
try { try {
const response = await apiClient.get(`/table-categories/${tableNameToUse}/${columnName}/values`); const response = await apiClient.get(`/table-categories/${tableNameToUse}/${columnName}/values?includeInactive=true`);
if (response.data.success && response.data.data) { if (response.data.success && response.data.data) {

View File

@ -133,7 +133,7 @@ export function RepeaterTable({
continue; continue;
} }
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values`); const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values?includeInactive=true`);
if (response.data?.success && response.data.data) { if (response.data?.success && response.data.data) {
const options = response.data.data.map((item: any) => ({ const options = response.data.data.map((item: any) => ({

View File

@ -1588,7 +1588,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
for (const col of categoryColumns) { for (const col of categoryColumns) {
const columnName = col.columnName || col.column_name; const columnName = col.columnName || col.column_name;
try { try {
const response = await apiClient.get(`/table-categories/${leftTableName}/${columnName}/values`); const response = await apiClient.get(`/table-categories/${leftTableName}/${columnName}/values?includeInactive=true`);
if (response.data.success && response.data.data) { if (response.data.success && response.data.data) {
const valueMap: Record<string, { label: string; color?: string }> = {}; const valueMap: Record<string, { label: string; color?: string }> = {};
@ -1650,7 +1650,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
for (const col of categoryColumns) { for (const col of categoryColumns) {
const columnName = col.columnName || col.column_name; const columnName = col.columnName || col.column_name;
try { try {
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values`); const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values?includeInactive=true`);
if (response.data.success && response.data.data) { if (response.data.success && response.data.data) {
const valueMap: Record<string, { label: string; color?: string }> = {}; const valueMap: Record<string, { label: string; color?: string }> = {};

View File

@ -1298,7 +1298,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
targetColumn = parts[1]; targetColumn = parts[1];
} }
const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values`); // 비활성화된 카테고리도 라벨로 표시하기 위해 includeInactive=true
const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values?includeInactive=true`);
if (response.data.success && response.data.data && Array.isArray(response.data.data)) { if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
const mapping: Record<string, { label: string; color?: string }> = {}; const mapping: Record<string, { label: string; color?: string }> = {};
@ -1381,7 +1382,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// inputType이 category인 경우 카테고리 매핑 로드 // inputType이 category인 경우 카테고리 매핑 로드
if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) { if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) {
try { try {
const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values`); const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values?includeInactive=true`);
if (response.data.success && response.data.data && Array.isArray(response.data.data)) { if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
const mapping: Record<string, { label: string; color?: string }> = {}; const mapping: Record<string, { label: string; color?: string }> = {};

View File

@ -1362,7 +1362,7 @@ export function UniversalFormModalComponent({
label: String(row[optionConfig.labelColumn || "name"]), label: String(row[optionConfig.labelColumn || "name"]),
})); }));
} else if (optionConfig.type === "code" && optionConfig.categoryKey) { } else if (optionConfig.type === "code" && optionConfig.categoryKey) {
// 공통코드(카테고리 컬럼): table_column_category_values 테이블에서 조회 // 공통코드(카테고리 컬럼): category_values 테이블에서 조회
// categoryKey 형식: "tableName.columnName" // categoryKey 형식: "tableName.columnName"
const [categoryTable, categoryColumn] = optionConfig.categoryKey.split("."); const [categoryTable, categoryColumn] = optionConfig.categoryKey.split(".");
if (categoryTable && categoryColumn) { if (categoryTable && categoryColumn) {

View File

@ -31,7 +31,7 @@ import {
import { apiClient } from "@/lib/api/client"; import { apiClient } from "@/lib/api/client";
import { getCascadingRelations, getCascadingRelationByCode, CascadingRelation } from "@/lib/api/cascadingRelation"; import { getCascadingRelations, getCascadingRelationByCode, CascadingRelation } from "@/lib/api/cascadingRelation";
// 카테고리 컬럼 타입 (table_column_category_values 용) // 카테고리 컬럼 타입 (category_values 용)
interface CategoryColumnOption { interface CategoryColumnOption {
tableName: string; tableName: string;
columnName: string; columnName: string;

View File

@ -2526,7 +2526,7 @@ interface TableSectionSettingsModalProps {
tables: { table_name: string; comment?: string }[]; tables: { table_name: string; comment?: string }[];
tableColumns: Record<string, { column_name: string; data_type: string; is_nullable: string; comment?: string; input_type?: string }[]>; tableColumns: Record<string, { column_name: string; data_type: string; is_nullable: string; comment?: string; input_type?: string }[]>;
onLoadTableColumns: (tableName: string) => void; onLoadTableColumns: (tableName: string) => void;
// 카테고리 목록 (table_column_category_values에서 가져옴) // 카테고리 목록 (category_values에서 가져옴)
categoryList?: { tableName: string; columnName: string; displayName?: string }[]; categoryList?: { tableName: string; columnName: string; displayName?: string }[];
onLoadCategoryList?: () => void; onLoadCategoryList?: () => void;
// 전체 섹션 목록 (다른 섹션 필드 참조용) // 전체 섹션 목록 (다른 섹션 필드 참조용)

View File

@ -16,7 +16,7 @@ export interface SelectOptionConfig {
labelColumn?: string; // 표시할 컬럼 (화면에 보여줄 텍스트) labelColumn?: string; // 표시할 컬럼 (화면에 보여줄 텍스트)
saveColumn?: string; // 저장할 컬럼 (실제로 DB에 저장할 값, 미지정 시 valueColumn 사용) saveColumn?: string; // 저장할 컬럼 (실제로 DB에 저장할 값, 미지정 시 valueColumn 사용)
filterCondition?: string; filterCondition?: string;
// 카테고리 컬럼 기반 옵션 (table_column_category_values 테이블) // 카테고리 컬럼 기반 옵션 (category_values 테이블)
// 형식: "tableName.columnName" (예: "sales_order_mng.incoterms") // 형식: "tableName.columnName" (예: "sales_order_mng.incoterms")
categoryKey?: string; categoryKey?: string;

View File

@ -540,7 +540,7 @@ export function BomItemEditorComponent({
if (alreadyLoaded) continue; if (alreadyLoaded) continue;
try { try {
const response = await apiClient.get(`/table-categories/${mainTableName}/${col.key}/values`); const response = await apiClient.get(`/table-categories/${mainTableName}/${col.key}/values?includeInactive=true`);
if (response.data?.success && response.data.data) { if (response.data?.success && response.data.data) {
const options = response.data.data.map((item: any) => ({ const options = response.data.data.map((item: any) => ({
value: item.valueCode || item.value_code, value: item.valueCode || item.value_code,

View File

@ -146,7 +146,7 @@ export function BomTreeComponent({
useEffect(() => { useEffect(() => {
const loadLabels = async () => { const loadLabels = async () => {
try { try {
const res = await apiClient.get(`/table-categories/${detailTable}/process_type/values`); const res = await apiClient.get(`/table-categories/${detailTable}/process_type/values?includeInactive=true`);
const vals = res.data?.data || []; const vals = res.data?.data || [];
if (vals.length > 0) { if (vals.length > 0) {
const map: Record<string, string> = {}; const map: Record<string, string> = {};

View File

@ -367,7 +367,7 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
for (const columnName of categoryColumns) { for (const columnName of categoryColumns) {
try { try {
const response = await apiClient.get(`/table-categories/${tableNameToUse}/${columnName}/values`); const response = await apiClient.get(`/table-categories/${tableNameToUse}/${columnName}/values?includeInactive=true`);
if (response.data.success && response.data.data) { if (response.data.success && response.data.data) {

View File

@ -1894,7 +1894,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
for (const col of categoryColumns) { for (const col of categoryColumns) {
const columnName = col.columnName || col.column_name; const columnName = col.columnName || col.column_name;
try { try {
const response = await apiClient.get(`/table-categories/${leftTableName}/${columnName}/values`); const response = await apiClient.get(`/table-categories/${leftTableName}/${columnName}/values?includeInactive=true`);
if (response.data.success && response.data.data) { if (response.data.success && response.data.data) {
const valueMap: Record<string, { label: string; color?: string }> = {}; const valueMap: Record<string, { label: string; color?: string }> = {};
@ -1972,7 +1972,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
for (const col of categoryColumns) { for (const col of categoryColumns) {
const columnName = col.columnName || col.column_name; const columnName = col.columnName || col.column_name;
try { try {
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values`); const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values?includeInactive=true`);
if (response.data.success && response.data.data) { if (response.data.success && response.data.data) {
const valueMap: Record<string, { label: string; color?: string }> = {}; const valueMap: Record<string, { label: string; color?: string }> = {};

View File

@ -1441,7 +1441,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
targetColumn = parts[1]; targetColumn = parts[1];
} }
const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values`); // 비활성화된 카테고리도 라벨로 표시하기 위해 includeInactive=true
const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values?includeInactive=true`);
if (response.data.success && response.data.data && Array.isArray(response.data.data)) { if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
const mapping: Record<string, { label: string; color?: string }> = {}; const mapping: Record<string, { label: string; color?: string }> = {};
@ -1524,7 +1525,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// inputType이 category인 경우 카테고리 매핑 로드 // inputType이 category인 경우 카테고리 매핑 로드
if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) { if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) {
try { try {
const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values`); const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values?includeInactive=true`);
if (response.data.success && response.data.data && Array.isArray(response.data.data)) { if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
const mapping: Record<string, { label: string; color?: string }> = {}; const mapping: Record<string, { label: string; color?: string }> = {};