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) => {
try {
const companyCode = req.user!.companyCode;
const userCompanyCode = req.user!.companyCode;
const { tableName, columnName } = req.params;
const includeInactive = req.query.includeInactive === "true";
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("카테고리 값 조회 요청", {
tableName,
columnName,
menuObjid,
companyCode,
companyCode: effectiveCompanyCode,
filterCompanyCode,
});
const values = await tableCategoryValueService.getCategoryValues(
tableName,
columnName,
companyCode,
effectiveCompanyCode,
includeInactive,
menuObjid // ← menuObjid 전달
menuObjid
);
return res.json({

View File

@ -217,12 +217,12 @@ class TableCategoryValueService {
AND column_name = $2
`;
// category_values 테이블 사용 (menu_objid 없음)
// company_code 기반 필터링
if (companyCode === "*") {
// 최고 관리자: 모든 값 조회
query = baseSelect;
// 최고 관리자: 공통(*) 카테고리만 조회 (모든 회사 카테고리 혼합 방지)
query = baseSelect + ` AND company_code = '*'`;
params = [tableName, columnName];
logger.info("최고 관리자 전체 카테고리 값 조회 (category_values)");
logger.info("최고 관리자: 공통 카테고리만 조회 (category_values)");
} else {
// 일반 회사: 자신의 회사 또는 공통(*) 카테고리 조회
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<
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 [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
@ -1047,9 +1047,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const getColumnUniqueValues = async (columnName: string) => {
const { apiClient } = await import("@/lib/api/client");
// 최고관리자가 특정 회사 프리뷰 시 해당 회사 카테고리만 필터링
const filterParam = companyCode && companyCode !== "*"
? `?filterCompanyCode=${encodeURIComponent(companyCode)}`
: "";
// 1단계: 카테고리 API 시도 (columnMeta 무관하게 항상 시도)
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) {
return response.data.data.map((item: any) => ({
value: item.valueCode,
@ -1154,15 +1159,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
tableConfig.selectedTable,
tableConfig.columns,
columnLabels,
columnMeta, // columnMeta가 변경되면 재등록 (inputType 정보 필요)
categoryMappings, // 카테고리 매핑 변경 시 재등록 (필터 라벨 변환용)
columnMeta,
categoryMappings,
columnWidths,
tableLabel,
data, // 데이터 자체가 변경되면 재등록 (고유 값 조회용)
totalItems, // 전체 항목 수가 변경되면 재등록
data,
totalItems,
registerTable,
// unregisterTable은 의존성에서 제외 - 무한 루프 방지
// unregisterTable 함수는 의존성이 없어 안정적임
]);
// 🎯 초기 로드 시 localStorage에서 정렬 상태 불러오기 (없으면 defaultSort 적용)
@ -1406,7 +1409,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const mappings: Record<string, Record<string, { label: string; color?: string }>> = {};
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 }>) => {
items.forEach((item: any) => {
if (item.valueCode) {
@ -1415,12 +1424,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
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) {
flattenTree(item.children, mapping);
}
@ -1448,7 +1451,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}
// 비활성화된 카테고리도 라벨로 표시하기 위해 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)) {
const mapping: Record<string, { label: string; color?: string }> = {};
@ -1531,7 +1534,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// inputType이 category인 경우 카테고리 매핑 로드
if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) {
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)) {
const mapping: Record<string, { label: string; color?: string }> = {};
@ -1601,6 +1604,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
JSON.stringify(categoryColumns),
JSON.stringify(tableConfig.columns),
columnMeta,
companyCode,
]);
// ========================================