feat: enhance category value retrieval with company code filtering

- Updated the `getCategoryValues` function to allow filtering based on a specified company code when requested by a super admin.
- Modified the service layer to ensure that super admins can retrieve common category values while preventing data mixing from different companies.
- Adjusted the frontend component to include the filter parameter in API requests, ensuring that the correct company-specific categories are displayed.

Made-with: Cursor
This commit is contained in:
kjs 2026-03-11 17:53:41 +09:00
parent d890155354
commit 62a5ae5f4b
3 changed files with 35 additions and 24 deletions

View File

@ -62,24 +62,31 @@ export const getAllCategoryColumns = async (req: AuthenticatedRequest, res: Resp
*/ */
export const getCategoryValues = async (req: AuthenticatedRequest, res: Response) => { export const getCategoryValues = async (req: AuthenticatedRequest, res: Response) => {
try { try {
const companyCode = req.user!.companyCode; const userCompanyCode = req.user!.companyCode;
const { tableName, columnName } = req.params; const { tableName, columnName } = req.params;
const includeInactive = req.query.includeInactive === "true"; const includeInactive = req.query.includeInactive === "true";
const menuObjid = req.query.menuObjid ? Number(req.query.menuObjid) : undefined; const menuObjid = req.query.menuObjid ? Number(req.query.menuObjid) : undefined;
const filterCompanyCode = req.query.filterCompanyCode as string | undefined;
// 최고관리자가 특정 회사 기준 필터링을 요청한 경우 해당 회사 코드 사용
const effectiveCompanyCode = (userCompanyCode === "*" && filterCompanyCode)
? filterCompanyCode
: userCompanyCode;
logger.info("카테고리 값 조회 요청", { logger.info("카테고리 값 조회 요청", {
tableName, tableName,
columnName, columnName,
menuObjid, menuObjid,
companyCode, companyCode: effectiveCompanyCode,
filterCompanyCode,
}); });
const values = await tableCategoryValueService.getCategoryValues( const values = await tableCategoryValueService.getCategoryValues(
tableName, tableName,
columnName, columnName,
companyCode, effectiveCompanyCode,
includeInactive, includeInactive,
menuObjid // ← menuObjid 전달 menuObjid
); );
return res.json({ return res.json({

View File

@ -217,12 +217,12 @@ class TableCategoryValueService {
AND column_name = $2 AND column_name = $2
`; `;
// category_values 테이블 사용 (menu_objid 없음) // company_code 기반 필터링
if (companyCode === "*") { if (companyCode === "*") {
// 최고 관리자: 모든 값 조회 // 최고 관리자: 공통(*) 카테고리만 조회 (모든 회사 카테고리 혼합 방지)
query = baseSelect; query = baseSelect + ` AND company_code = '*'`;
params = [tableName, columnName]; params = [tableName, columnName];
logger.info("최고 관리자 전체 카테고리 값 조회 (category_values)"); logger.info("최고 관리자: 공통 카테고리만 조회 (category_values)");
} else { } else {
// 일반 회사: 자신의 회사 또는 공통(*) 카테고리 조회 // 일반 회사: 자신의 회사 또는 공통(*) 카테고리 조회
query = baseSelect + ` AND (company_code = $3 OR company_code = '*')`; query = baseSelect + ` AND (company_code = $3 OR company_code = '*')`;

View File

@ -713,7 +713,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const [categoryMappings, setCategoryMappings] = useState< const [categoryMappings, setCategoryMappings] = useState<
Record<string, Record<string, { label: string; color?: string }>> Record<string, Record<string, { label: string; color?: string }>>
>({}); >({});
const [categoryMappingsKey, setCategoryMappingsKey] = useState(0); // 강제 리렌더링용 const [categoryMappingsKey, setCategoryMappingsKey] = useState(0);
const [searchValues, setSearchValues] = useState<Record<string, any>>({}); const [searchValues, setSearchValues] = useState<Record<string, any>>({});
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set()); const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({}); const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
@ -1047,9 +1047,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const getColumnUniqueValues = async (columnName: string) => { const getColumnUniqueValues = async (columnName: string) => {
const { apiClient } = await import("@/lib/api/client"); const { apiClient } = await import("@/lib/api/client");
// 최고관리자가 특정 회사 프리뷰 시 해당 회사 카테고리만 필터링
const filterParam = companyCode && companyCode !== "*"
? `?filterCompanyCode=${encodeURIComponent(companyCode)}`
: "";
// 1단계: 카테고리 API 시도 (columnMeta 무관하게 항상 시도) // 1단계: 카테고리 API 시도 (columnMeta 무관하게 항상 시도)
try { try {
const response = await apiClient.get(`/table-categories/${tableConfig.selectedTable}/${columnName}/values`); const response = await apiClient.get(`/table-categories/${tableConfig.selectedTable}/${columnName}/values${filterParam}`);
if (response.data.success && response.data.data && response.data.data.length > 0) { if (response.data.success && response.data.data && response.data.data.length > 0) {
return response.data.data.map((item: any) => ({ return response.data.data.map((item: any) => ({
value: item.valueCode, value: item.valueCode,
@ -1154,15 +1159,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
tableConfig.selectedTable, tableConfig.selectedTable,
tableConfig.columns, tableConfig.columns,
columnLabels, columnLabels,
columnMeta, // columnMeta가 변경되면 재등록 (inputType 정보 필요) columnMeta,
categoryMappings, // 카테고리 매핑 변경 시 재등록 (필터 라벨 변환용) categoryMappings,
columnWidths, columnWidths,
tableLabel, tableLabel,
data, // 데이터 자체가 변경되면 재등록 (고유 값 조회용) data,
totalItems, // 전체 항목 수가 변경되면 재등록 totalItems,
registerTable, registerTable,
// unregisterTable은 의존성에서 제외 - 무한 루프 방지
// unregisterTable 함수는 의존성이 없어 안정적임
]); ]);
// 🎯 초기 로드 시 localStorage에서 정렬 상태 불러오기 (없으면 defaultSort 적용) // 🎯 초기 로드 시 localStorage에서 정렬 상태 불러오기 (없으면 defaultSort 적용)
@ -1406,7 +1409,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const mappings: Record<string, Record<string, { label: string; color?: string }>> = {}; const mappings: Record<string, Record<string, { label: string; color?: string }>> = {};
const apiClient = (await import("@/lib/api/client")).apiClient; const apiClient = (await import("@/lib/api/client")).apiClient;
// 최고관리자가 특정 회사 프리뷰 시 해당 회사 카테고리만 필터링
const filterCompanyParam = companyCode && companyCode !== "*"
? `&filterCompanyCode=${encodeURIComponent(companyCode)}`
: "";
// 트리 구조를 평탄화하는 헬퍼 함수 (메인 테이블 + 엔티티 조인 공통 사용) // 트리 구조를 평탄화하는 헬퍼 함수 (메인 테이블 + 엔티티 조인 공통 사용)
// valueCode만 키로 사용 (valueId까지 넣으면 같은 라벨이 2번 나옴)
const flattenTree = (items: any[], mapping: Record<string, { label: string; color?: string }>) => { const flattenTree = (items: any[], mapping: Record<string, { label: string; color?: string }>) => {
items.forEach((item: any) => { items.forEach((item: any) => {
if (item.valueCode) { if (item.valueCode) {
@ -1415,12 +1424,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
color: item.color, color: item.color,
}; };
} }
if (item.valueId !== undefined && item.valueId !== null) {
mapping[String(item.valueId)] = {
label: item.valueLabel,
color: item.color,
};
}
if (item.children && Array.isArray(item.children) && item.children.length > 0) { if (item.children && Array.isArray(item.children) && item.children.length > 0) {
flattenTree(item.children, mapping); flattenTree(item.children, mapping);
} }
@ -1448,7 +1451,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
} }
// 비활성화된 카테고리도 라벨로 표시하기 위해 includeInactive=true // 비활성화된 카테고리도 라벨로 표시하기 위해 includeInactive=true
const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values?includeInactive=true`); const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values?includeInactive=true${filterCompanyParam}`);
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 }> = {};
@ -1531,7 +1534,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?includeInactive=true`); const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values?includeInactive=true${filterCompanyParam}`);
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 }> = {};
@ -1601,6 +1604,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
JSON.stringify(categoryColumns), JSON.stringify(categoryColumns),
JSON.stringify(tableConfig.columns), JSON.stringify(tableConfig.columns),
columnMeta, columnMeta,
companyCode,
]); ]);
// ======================================== // ========================================