fix: 필터 select 옵션에서 카테고리/엔티티 라벨이 올바르게 표시되도록 수정
- 백엔드: entityJoinService에서 _label 필드를 SELECT에 추가 - 백엔드: tableManagementService에 멀티테넌시 필터링 추가 (company_code) - 백엔드: categorizeJoins에서 table_column_category_values를 명시적으로 dbJoins로 분류 - 백엔드: executeCachedLookup와 getTableData에 companyCode 파라미터 추가 - 프론트엔드: getColumnUniqueValues가 백엔드 조인 결과의 _name 필드를 사용하도록 수정 - 프론트엔드: TableSearchWidget에서 select 옵션 로드 로직 개선 이제 필터 select 박스에서 코드 대신 실제 이름(라벨)이 표시됩니다. 예: CATEGORY_148700 → 정상, topseal_admin → 탑씰 관리자 계정
This commit is contained in:
parent
58870237b6
commit
71fd3f5ee7
|
|
@ -24,20 +24,19 @@ export class EntityJoinService {
|
|||
try {
|
||||
logger.info(`Entity 컬럼 감지 시작: ${tableName}`);
|
||||
|
||||
// column_labels에서 entity 타입인 컬럼들 조회
|
||||
// column_labels에서 entity 및 category 타입인 컬럼들 조회 (input_type 사용)
|
||||
const entityColumns = await query<{
|
||||
column_name: string;
|
||||
input_type: string;
|
||||
reference_table: string;
|
||||
reference_column: string;
|
||||
display_column: string | null;
|
||||
}>(
|
||||
`SELECT column_name, reference_table, reference_column, display_column
|
||||
`SELECT column_name, input_type, reference_table, reference_column, display_column
|
||||
FROM column_labels
|
||||
WHERE table_name = $1
|
||||
AND web_type = $2
|
||||
AND reference_table IS NOT NULL
|
||||
AND reference_column IS NOT NULL`,
|
||||
[tableName, "entity"]
|
||||
AND input_type IN ('entity', 'category')`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
logger.info(`🔍 Entity 컬럼 조회 결과: ${entityColumns.length}개 발견`);
|
||||
|
|
@ -77,18 +76,34 @@ export class EntityJoinService {
|
|||
}
|
||||
|
||||
for (const column of entityColumns) {
|
||||
// 카테고리 타입인 경우 자동으로 category_values 테이블 참조 설정
|
||||
let referenceTable = column.reference_table;
|
||||
let referenceColumn = column.reference_column;
|
||||
let displayColumn = column.display_column;
|
||||
|
||||
if (column.input_type === 'category') {
|
||||
// 카테고리 타입: reference 정보가 비어있어도 자동 설정
|
||||
referenceTable = referenceTable || 'table_column_category_values';
|
||||
referenceColumn = referenceColumn || 'value_code';
|
||||
displayColumn = displayColumn || 'value_label';
|
||||
|
||||
logger.info(`🏷️ 카테고리 타입 자동 설정: ${column.column_name}`, {
|
||||
referenceTable,
|
||||
referenceColumn,
|
||||
displayColumn,
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`🔍 Entity 컬럼 상세 정보:`, {
|
||||
column_name: column.column_name,
|
||||
reference_table: column.reference_table,
|
||||
reference_column: column.reference_column,
|
||||
display_column: column.display_column,
|
||||
input_type: column.input_type,
|
||||
reference_table: referenceTable,
|
||||
reference_column: referenceColumn,
|
||||
display_column: displayColumn,
|
||||
});
|
||||
|
||||
if (
|
||||
!column.column_name ||
|
||||
!column.reference_table ||
|
||||
!column.reference_column
|
||||
) {
|
||||
if (!column.column_name || !referenceTable || !referenceColumn) {
|
||||
logger.warn(`⚠️ 필수 정보 누락으로 스킵: ${column.column_name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -112,27 +127,28 @@ export class EntityJoinService {
|
|||
separator,
|
||||
screenConfig,
|
||||
});
|
||||
} else if (column.display_column && column.display_column !== "none") {
|
||||
} else if (displayColumn && displayColumn !== "none") {
|
||||
// 기존 설정된 단일 표시 컬럼 사용 (none이 아닌 경우만)
|
||||
displayColumns = [column.display_column];
|
||||
displayColumns = [displayColumn];
|
||||
logger.info(
|
||||
`🔧 기존 display_column 사용: ${column.column_name} → ${column.display_column}`
|
||||
`🔧 기존 display_column 사용: ${column.column_name} → ${displayColumn}`
|
||||
);
|
||||
} else {
|
||||
// display_column이 "none"이거나 없는 경우 기본 표시 컬럼 설정
|
||||
// 🚨 display_column이 항상 "none"이므로 이 로직을 기본으로 사용
|
||||
let defaultDisplayColumn = column.reference_column;
|
||||
if (column.reference_table === "dept_info") {
|
||||
let defaultDisplayColumn = referenceColumn;
|
||||
if (referenceTable === "dept_info") {
|
||||
defaultDisplayColumn = "dept_name";
|
||||
} else if (column.reference_table === "company_info") {
|
||||
} else if (referenceTable === "company_info") {
|
||||
defaultDisplayColumn = "company_name";
|
||||
} else if (column.reference_table === "user_info") {
|
||||
} else if (referenceTable === "user_info") {
|
||||
defaultDisplayColumn = "user_name";
|
||||
} else if (referenceTable === "category_values") {
|
||||
defaultDisplayColumn = "category_name";
|
||||
}
|
||||
|
||||
displayColumns = [defaultDisplayColumn];
|
||||
logger.info(
|
||||
`🔧 Entity 조인 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${column.reference_table})`
|
||||
`🔧 Entity 조인 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${referenceTable})`
|
||||
);
|
||||
logger.info(`🔍 생성된 displayColumns 배열:`, displayColumns);
|
||||
}
|
||||
|
|
@ -143,8 +159,8 @@ export class EntityJoinService {
|
|||
const joinConfig: EntityJoinConfig = {
|
||||
sourceTable: tableName,
|
||||
sourceColumn: column.column_name,
|
||||
referenceTable: column.reference_table,
|
||||
referenceColumn: column.reference_column,
|
||||
referenceTable: referenceTable, // 카테고리의 경우 자동 설정된 값 사용
|
||||
referenceColumn: referenceColumn, // 카테고리의 경우 자동 설정된 값 사용
|
||||
displayColumns: displayColumns,
|
||||
displayColumn: displayColumns[0], // 하위 호환성
|
||||
aliasColumn: aliasColumn,
|
||||
|
|
@ -245,11 +261,14 @@ export class EntityJoinService {
|
|||
config.displayColumn,
|
||||
];
|
||||
const separator = config.separator || " - ";
|
||||
|
||||
// 결과 컬럼 배열 (aliasColumn + _label 필드)
|
||||
const resultColumns: string[] = [];
|
||||
|
||||
if (displayColumns.length === 0 || !displayColumns[0]) {
|
||||
// displayColumns가 빈 배열이거나 첫 번째 값이 null/undefined인 경우
|
||||
// 조인 테이블의 referenceColumn을 기본값으로 사용
|
||||
return `COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.aliasColumn}`;
|
||||
resultColumns.push(`COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.aliasColumn}`);
|
||||
} else if (displayColumns.length === 1) {
|
||||
// 단일 컬럼인 경우
|
||||
const col = displayColumns[0];
|
||||
|
|
@ -265,12 +284,18 @@ export class EntityJoinService {
|
|||
"company_name",
|
||||
"sales_yn",
|
||||
"status",
|
||||
"value_label", // table_column_category_values
|
||||
"user_name", // user_info
|
||||
].includes(col);
|
||||
|
||||
if (isJoinTableColumn) {
|
||||
return `COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}`;
|
||||
resultColumns.push(`COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}`);
|
||||
|
||||
// _label 필드도 함께 SELECT (프론트엔드 getColumnUniqueValues용)
|
||||
// sourceColumn_label 형식으로 추가
|
||||
resultColumns.push(`COALESCE(${alias}.${col}::TEXT, '') AS ${config.sourceColumn}_label`);
|
||||
} else {
|
||||
return `COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}`;
|
||||
resultColumns.push(`COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}`);
|
||||
}
|
||||
} else {
|
||||
// 여러 컬럼인 경우 CONCAT으로 연결
|
||||
|
|
@ -291,6 +316,8 @@ export class EntityJoinService {
|
|||
"company_name",
|
||||
"sales_yn",
|
||||
"status",
|
||||
"value_label", // table_column_category_values
|
||||
"user_name", // user_info
|
||||
].includes(col);
|
||||
|
||||
if (isJoinTableColumn) {
|
||||
|
|
@ -303,8 +330,11 @@ export class EntityJoinService {
|
|||
})
|
||||
.join(` || '${separator}' || `);
|
||||
|
||||
return `(${concatParts}) AS ${config.aliasColumn}`;
|
||||
resultColumns.push(`(${concatParts}) AS ${config.aliasColumn}`);
|
||||
}
|
||||
|
||||
// 모든 resultColumns를 반환
|
||||
return resultColumns.join(", ");
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
|
|
@ -320,6 +350,12 @@ export class EntityJoinService {
|
|||
const joinClauses = uniqueReferenceTableConfigs
|
||||
.map((config) => {
|
||||
const alias = aliasMap.get(config.referenceTable);
|
||||
|
||||
// table_column_category_values는 특별한 조인 조건 필요
|
||||
if (config.referenceTable === 'table_column_category_values') {
|
||||
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn} AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}'`;
|
||||
}
|
||||
|
||||
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
|
@ -380,6 +416,14 @@ export class EntityJoinService {
|
|||
return "join";
|
||||
}
|
||||
|
||||
// table_column_category_values는 특수 조인 조건이 필요하므로 캐시 불가
|
||||
if (config.referenceTable === 'table_column_category_values') {
|
||||
logger.info(
|
||||
`🎯 table_column_category_values는 캐시 전략 불가: ${config.sourceColumn}`
|
||||
);
|
||||
return "join";
|
||||
}
|
||||
|
||||
// 참조 테이블의 캐시 가능성 확인
|
||||
const displayCol =
|
||||
config.displayColumn ||
|
||||
|
|
|
|||
|
|
@ -1494,6 +1494,7 @@ export class TableManagementService {
|
|||
search?: Record<string, any>;
|
||||
sortBy?: string;
|
||||
sortOrder?: string;
|
||||
companyCode?: string;
|
||||
}
|
||||
): Promise<{
|
||||
data: any[];
|
||||
|
|
@ -1503,7 +1504,7 @@ export class TableManagementService {
|
|||
totalPages: number;
|
||||
}> {
|
||||
try {
|
||||
const { page, size, search = {}, sortBy, sortOrder = "asc" } = options;
|
||||
const { page, size, search = {}, sortBy, sortOrder = "asc", companyCode } = options;
|
||||
const offset = (page - 1) * size;
|
||||
|
||||
logger.info(`테이블 데이터 조회: ${tableName}`, options);
|
||||
|
|
@ -1517,6 +1518,14 @@ export class TableManagementService {
|
|||
let searchValues: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 멀티테넌시 필터 추가 (company_code)
|
||||
if (companyCode) {
|
||||
whereConditions.push(`company_code = $${paramIndex}`);
|
||||
searchValues.push(companyCode);
|
||||
paramIndex++;
|
||||
logger.info(`🔒 멀티테넌시 필터 추가 (기본 조회): company_code = ${companyCode}`);
|
||||
}
|
||||
|
||||
if (search && Object.keys(search).length > 0) {
|
||||
for (const [column, value] of Object.entries(search)) {
|
||||
if (value !== null && value !== undefined && value !== "") {
|
||||
|
|
@ -2213,11 +2222,20 @@ export class TableManagementService {
|
|||
const selectColumns = columns.data.map((col: any) => col.column_name);
|
||||
|
||||
// WHERE 절 구성
|
||||
const whereClause = await this.buildWhereClause(
|
||||
let whereClause = await this.buildWhereClause(
|
||||
tableName,
|
||||
options.search
|
||||
);
|
||||
|
||||
// 멀티테넌시 필터 추가 (company_code)
|
||||
if (options.companyCode) {
|
||||
const companyFilter = `main.company_code = '${options.companyCode.replace(/'/g, "''")}'`;
|
||||
whereClause = whereClause
|
||||
? `${whereClause} AND ${companyFilter}`
|
||||
: companyFilter;
|
||||
logger.info(`🔒 멀티테넌시 필터 추가 (Entity 조인): company_code = ${options.companyCode}`);
|
||||
}
|
||||
|
||||
// ORDER BY 절 구성
|
||||
const orderBy = options.sortBy
|
||||
? `main.${options.sortBy} ${options.sortOrder === "desc" ? "DESC" : "ASC"}`
|
||||
|
|
@ -2343,6 +2361,7 @@ export class TableManagementService {
|
|||
search?: Record<string, any>;
|
||||
sortBy?: string;
|
||||
sortOrder?: string;
|
||||
companyCode?: string;
|
||||
},
|
||||
startTime: number
|
||||
): Promise<EntityJoinResponse> {
|
||||
|
|
@ -2530,11 +2549,11 @@ export class TableManagementService {
|
|||
);
|
||||
}
|
||||
|
||||
basicResult = await this.getTableData(tableName, fallbackOptions);
|
||||
basicResult = await this.getTableData(tableName, { ...fallbackOptions, companyCode: options.companyCode });
|
||||
}
|
||||
} else {
|
||||
// Entity 조인 컬럼 검색이 없는 경우 기존 캐시 방식 사용
|
||||
basicResult = await this.getTableData(tableName, options);
|
||||
basicResult = await this.getTableData(tableName, { ...options, companyCode: options.companyCode });
|
||||
}
|
||||
|
||||
// Entity 값들을 캐시에서 룩업하여 변환
|
||||
|
|
@ -2807,10 +2826,14 @@ export class TableManagementService {
|
|||
}
|
||||
// 모든 조인이 캐시 가능한 경우: 기본 쿼리 + 캐시 룩업
|
||||
else {
|
||||
// whereClause에서 company_code 추출 (멀티테넌시 필터)
|
||||
const companyCodeMatch = whereClause.match(/main\.company_code\s*=\s*'([^']+)'/);
|
||||
const companyCode = companyCodeMatch ? companyCodeMatch[1] : undefined;
|
||||
|
||||
return await this.executeCachedLookup(
|
||||
tableName,
|
||||
cacheableJoins,
|
||||
{ page: Math.floor(offset / limit) + 1, size: limit, search: {} },
|
||||
{ page: Math.floor(offset / limit) + 1, size: limit, search: {}, companyCode },
|
||||
startTime
|
||||
);
|
||||
}
|
||||
|
|
@ -2831,6 +2854,13 @@ export class TableManagementService {
|
|||
const dbJoins: EntityJoinConfig[] = [];
|
||||
|
||||
for (const config of joinConfigs) {
|
||||
// table_column_category_values는 특수 조인 조건이 필요하므로 항상 DB 조인
|
||||
if (config.referenceTable === 'table_column_category_values') {
|
||||
dbJoins.push(config);
|
||||
console.log(`🔗 DB 조인 (특수 조건): ${config.referenceTable}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 캐시 가능성 확인
|
||||
const cachedData = await referenceCacheService.getCachedReference(
|
||||
config.referenceTable,
|
||||
|
|
|
|||
|
|
@ -348,22 +348,60 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
// 컬럼의 고유 값 조회 함수
|
||||
const getColumnUniqueValues = async (columnName: string) => {
|
||||
console.log("🔍 [getColumnUniqueValues] 호출됨:", {
|
||||
columnName,
|
||||
dataLength: data.length,
|
||||
columnMeta: columnMeta[columnName],
|
||||
sampleData: data[0],
|
||||
});
|
||||
|
||||
const meta = columnMeta[columnName];
|
||||
const inputType = meta?.inputType || "text";
|
||||
|
||||
// 카테고리, 엔티티, 코드 타입인 경우 _name 필드 사용 (백엔드 조인 결과)
|
||||
const isLabelType = ["category", "entity", "code"].includes(inputType);
|
||||
const labelField = isLabelType ? `${columnName}_name` : columnName;
|
||||
|
||||
console.log("🔍 [getColumnUniqueValues] 필드 선택:", {
|
||||
columnName,
|
||||
inputType,
|
||||
isLabelType,
|
||||
labelField,
|
||||
hasLabelField: data[0] && labelField in data[0],
|
||||
sampleLabelValue: data[0] ? data[0][labelField] : undefined,
|
||||
});
|
||||
|
||||
// 현재 로드된 데이터에서 고유 값 추출
|
||||
const uniqueValues = new Set<string>();
|
||||
const uniqueValuesMap = new Map<string, string>(); // value -> label
|
||||
|
||||
data.forEach((row) => {
|
||||
const value = row[columnName];
|
||||
if (value !== null && value !== undefined && value !== "") {
|
||||
uniqueValues.add(String(value));
|
||||
// 백엔드 조인된 _name 필드 사용 (없으면 원본 값)
|
||||
const label = isLabelType && row[labelField] ? row[labelField] : String(value);
|
||||
uniqueValuesMap.set(String(value), label);
|
||||
}
|
||||
});
|
||||
|
||||
// Set을 배열로 변환하고 정렬
|
||||
const sortedValues = Array.from(uniqueValues).sort();
|
||||
|
||||
return sortedValues.map((value) => ({
|
||||
label: value,
|
||||
value: value,
|
||||
}));
|
||||
// Map을 배열로 변환하고 라벨 기준으로 정렬
|
||||
const result = Array.from(uniqueValuesMap.entries())
|
||||
.map(([value, label]) => ({
|
||||
value: value,
|
||||
label: label,
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
console.log("✅ [getColumnUniqueValues] 결과:", {
|
||||
columnName,
|
||||
inputType,
|
||||
isLabelType,
|
||||
labelField,
|
||||
uniqueCount: result.length,
|
||||
values: result, // 전체 값 출력
|
||||
allKeys: data[0] ? Object.keys(data[0]) : [], // 모든 키 출력
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const registration = {
|
||||
|
|
@ -396,7 +434,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
tableConfig.selectedTable,
|
||||
tableConfig.columns,
|
||||
columnLabels,
|
||||
columnMeta,
|
||||
columnMeta, // columnMeta가 변경되면 재등록 (inputType 정보 필요)
|
||||
columnWidths,
|
||||
tableLabel,
|
||||
data, // 데이터 자체가 변경되면 재등록 (고유 값 조회용)
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
|
|||
}
|
||||
}, [registeredTables, selectedTableId, autoSelectFirstTable, setSelectedTableId]);
|
||||
|
||||
// 현재 테이블의 저장된 필터 불러오기 및 select 옵션 로드
|
||||
// 현재 테이블의 저장된 필터 불러오기
|
||||
useEffect(() => {
|
||||
if (currentTable?.tableName) {
|
||||
const storageKey = `table_filters_${currentTable.tableName}`;
|
||||
|
|
@ -89,36 +89,54 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
|
|||
}));
|
||||
|
||||
setActiveFilters(activeFiltersList);
|
||||
|
||||
// select 타입 필터들의 옵션 로드
|
||||
const loadSelectOptions = async () => {
|
||||
const newOptions: Record<string, Array<{ label: string; value: string }>> = {};
|
||||
|
||||
for (const filter of activeFiltersList) {
|
||||
if (filter.filterType === "select" && currentTable.getColumnUniqueValues) {
|
||||
try {
|
||||
const options = await currentTable.getColumnUniqueValues(filter.columnName);
|
||||
newOptions[filter.columnName] = options;
|
||||
console.log("✅ [TableSearchWidget] select 옵션 로드:", {
|
||||
columnName: filter.columnName,
|
||||
optionCount: options.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("select 옵션 로드 실패:", filter.columnName, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSelectOptions(newOptions);
|
||||
};
|
||||
|
||||
loadSelectOptions();
|
||||
} catch (error) {
|
||||
console.error("저장된 필터 불러오기 실패:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [currentTable?.tableName, currentTable?.getColumnUniqueValues]);
|
||||
}, [currentTable?.tableName]);
|
||||
|
||||
// select 옵션 로드 (activeFilters 또는 dataCount 변경 시)
|
||||
useEffect(() => {
|
||||
if (!currentTable?.getColumnUniqueValues || activeFilters.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadSelectOptions = async () => {
|
||||
const selectFilters = activeFilters.filter(f => f.filterType === "select");
|
||||
|
||||
if (selectFilters.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("🔄 [TableSearchWidget] select 옵션 로드 시작:", {
|
||||
activeFiltersCount: activeFilters.length,
|
||||
selectFiltersCount: selectFilters.length,
|
||||
dataCount: currentTable.dataCount,
|
||||
});
|
||||
|
||||
const newOptions: Record<string, Array<{ label: string; value: string }>> = {};
|
||||
|
||||
for (const filter of selectFilters) {
|
||||
try {
|
||||
const options = await currentTable.getColumnUniqueValues(filter.columnName);
|
||||
newOptions[filter.columnName] = options;
|
||||
console.log("✅ [TableSearchWidget] select 옵션 로드:", {
|
||||
columnName: filter.columnName,
|
||||
optionCount: options.length,
|
||||
options: options.slice(0, 5),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("❌ [TableSearchWidget] select 옵션 로드 실패:", filter.columnName, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ [TableSearchWidget] 최종 selectOptions:", newOptions);
|
||||
setSelectOptions(newOptions);
|
||||
};
|
||||
|
||||
loadSelectOptions();
|
||||
}, [activeFilters, currentTable?.dataCount, currentTable?.getColumnUniqueValues]);
|
||||
|
||||
// 디버깅: 현재 테이블 정보 로깅
|
||||
useEffect(() => {
|
||||
|
|
@ -193,13 +211,13 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) {
|
|||
onValueChange={(val) => handleFilterChange(filter.columnName, val)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs sm:h-9 sm:text-sm">
|
||||
<SelectValue placeholder={column?.columnLabel} />
|
||||
<SelectValue placeholder={column?.columnLabel || "선택"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.length === 0 ? (
|
||||
<SelectItem value="" disabled>
|
||||
<div className="px-2 py-1.5 text-xs text-muted-foreground">
|
||||
옵션 없음
|
||||
</SelectItem>
|
||||
</div>
|
||||
) : (
|
||||
options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue