fix(RepeaterTable): 조회 컬럼 헤더 라벨 개선 및 코드 정리

헤더에 "컬럼명 - 옵션라벨" 형식으로 전체 정보 표시
옵션 변경 시 컬럼 너비 자동 재계산
API 검색 시 정확한 일치 검색(equals) 적용
디버그 로그 제거
설정 UI 라벨 사용자 친화적으로 변경
This commit is contained in:
SeongHyun Kim
2025-12-19 13:43:26 +09:00
parent c86140fad3
commit 228fd33a2a
4 changed files with 59 additions and 63 deletions

View File

@@ -238,9 +238,17 @@ export function RepeaterTable({
return equalWidth;
}
// 헤더 텍스트 너비
const headerText = column.label || field;
const headerWidth = measureTextWidth(headerText) + 24; // padding
// 헤더 텍스트 너비 (동적 데이터 소스가 있으면 headerLabel 사용)
let headerText = column.label || field;
if (column.dynamicDataSource?.enabled && column.dynamicDataSource.options.length > 0) {
const activeOptionId = activeDataSources[field] || column.dynamicDataSource.defaultOptionId;
const activeOption = column.dynamicDataSource.options.find((opt) => opt.id === activeOptionId)
|| column.dynamicDataSource.options[0];
if (activeOption?.headerLabel) {
headerText = activeOption.headerLabel;
}
}
const headerWidth = measureTextWidth(headerText) + 32; // padding + 드롭다운 아이콘
// 헤더와 데이터 중 큰 값 사용
return Math.max(headerWidth, maxDataWidth);
@@ -554,7 +562,10 @@ export function RepeaterTable({
"-mx-1 rounded px-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500",
)}
>
<span>{col.label}</span>
{/* 컬럼명 - 선택된 옵션라벨 형식으로 표시 */}
<span>
{activeOption?.headerLabel || `${col.label} - ${activeOption?.label || ''}`}
</span>
<ChevronDown className="h-3 w-3 opacity-60" />
</button>
</PopoverTrigger>
@@ -569,6 +580,14 @@ export function RepeaterTable({
onClick={() => {
onDataSourceChange?.(col.field, option.id);
setOpenPopover(null);
// 옵션 변경 시 해당 컬럼 너비 재계산
if (option.headerLabel) {
const newHeaderWidth = measureTextWidth(option.headerLabel) + 32;
setColumnWidths((prev) => ({
...prev,
[col.field]: Math.max(prev[col.field] || 60, newHeaderWidth),
}));
}
}}
className={cn(
"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs",

View File

@@ -76,6 +76,7 @@ export interface DynamicDataSourceConfig {
export interface DynamicDataSourceOption {
id: string;
label: string; // 표시 라벨 (예: "거래처별 단가")
headerLabel?: string; // 헤더에 표시될 전체 라벨 (예: "단가 - 거래처별 단가")
// 조회 방식
sourceType: "table" | "multiTable" | "api";

View File

@@ -53,8 +53,10 @@ function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig {
enabled: true,
options: col.lookup.options.map((option) => ({
id: option.id,
// displayLabel이 있으면 그것을 사용, 없으면 원래 label 사용
// "컬럼명 - 옵션라벨" 형식으로 헤더에 표시
label: option.displayLabel || option.label,
// 헤더에 표시될 전체 라벨 (컬럼명 - 옵션라벨)
headerLabel: `${col.label} - ${option.displayLabel || option.label}`,
sourceType: "table" as const,
tableConfig: {
tableName: option.tableName,
@@ -131,9 +133,19 @@ async function transformValue(
}
try {
// 정확히 일치하는 검색
const response = await apiClient.post(
`/table-management/tables/${transform.tableName}/data`,
{ search: { [transform.matchColumn]: value }, size: 1, page: 1 }
{
search: {
[transform.matchColumn]: {
value: value,
operator: "equals"
}
},
size: 1,
page: 1
}
);
if (response.data.success && response.data.data?.data?.length > 0) {
@@ -181,11 +193,20 @@ async function fetchExternalLookupValue(
return undefined;
}
// 2. 외부 테이블에서 값 조회
// 2. 외부 테이블에서 값 조회 (정확히 일치하는 검색)
try {
const response = await apiClient.post(
`/table-management/tables/${externalLookup.tableName}/data`,
{ search: { [externalLookup.matchColumn]: matchValue }, size: 1, page: 1 }
{
search: {
[externalLookup.matchColumn]: {
value: matchValue,
operator: "equals"
}
},
size: 1,
page: 1
}
);
if (response.data.success && response.data.data?.data?.length > 0) {
@@ -218,10 +239,7 @@ async function fetchExternalValue(
sourceData: any,
formData: FormDataState
): Promise<any> {
console.log("📡 [fetchExternalValue] 시작:", { tableName, valueColumn, joinConditions });
if (joinConditions.length === 0) {
console.warn("📡 [fetchExternalValue] 조인 조건이 없습니다.");
return undefined;
}
@@ -231,40 +249,29 @@ async function fetchExternalValue(
for (const condition of joinConditions) {
let value: any;
console.log("📡 [fetchExternalValue] 조건 처리:", { condition, rowData, sourceData, formData });
// 값 출처에 따라 가져오기 (4가지 소스 타입 지원)
if (condition.sourceType === "row") {
// 현재 행 데이터 (설정된 컬럼 필드)
value = rowData[condition.sourceField];
console.log("📡 [fetchExternalValue] row에서 값 가져옴:", { field: condition.sourceField, value });
} else if (condition.sourceType === "sourceData") {
// 원본 소스 테이블 데이터 (_sourceData)
value = sourceData?.[condition.sourceField];
console.log("📡 [fetchExternalValue] sourceData에서 값 가져옴:", { field: condition.sourceField, value });
} else if (condition.sourceType === "formData") {
// formData에서 가져오기 (다른 섹션)
value = formData[condition.sourceField];
console.log("📡 [fetchExternalValue] formData에서 값 가져옴:", { field: condition.sourceField, value });
} else if (condition.sourceType === "externalTable" && condition.externalLookup) {
// 외부 테이블에서 조회하여 가져오기
console.log("📡 [fetchExternalValue] externalTable 조회 시작:", condition.externalLookup);
value = await fetchExternalLookupValue(condition.externalLookup, rowData, sourceData, formData);
console.log("📡 [fetchExternalValue] externalTable 조회 결과:", { value });
}
if (value === undefined || value === null || value === "") {
console.warn(`📡 [fetchExternalValue] 조인 조건의 필드 "${condition.sourceField}" 값이 없습니다. (sourceType: ${condition.sourceType})`);
return undefined;
}
// 값 변환이 필요한 경우 (예: 이름 → 코드) - 레거시 호환
if (condition.transform) {
console.log("📡 [fetchExternalValue] 값 변환 시작:", { originalValue: value, transform: condition.transform });
value = await transformValue(value, condition.transform);
console.log("📡 [fetchExternalValue] 값 변환 결과:", { transformedValue: value });
if (value === undefined) {
console.warn(`📡 [fetchExternalValue] 값 변환 후 결과가 없습니다. 원본 값: "${formData[condition.sourceField]}"`);
return undefined;
}
}
@@ -283,28 +290,21 @@ async function fetchExternalValue(
value: convertedValue,
operator: "equals"
};
console.log("📡 [fetchExternalValue] WHERE 조건 추가:", { targetColumn: condition.targetColumn, value: convertedValue });
}
// API 호출
console.log("📡 [fetchExternalValue] API 호출:", { tableName, whereConditions });
const response = await apiClient.post(
`/table-management/tables/${tableName}/data`,
{ search: whereConditions, size: 1, page: 1 }
);
console.log("📡 [fetchExternalValue] API 응답:", response.data);
if (response.data.success && response.data.data?.data?.length > 0) {
const result = response.data.data.data[0][valueColumn];
console.log("📡 [fetchExternalValue] 최종 결과:", { valueColumn, result });
return result;
return response.data.data.data[0][valueColumn];
}
console.warn("📡 [fetchExternalValue] 조회 결과 없음");
return undefined;
} catch (error) {
console.error("📡 [fetchExternalValue] 외부 테이블 조회 오류:", error);
console.error("외부 테이블 조회 오류:", error);
return undefined;
}
}
@@ -581,8 +581,6 @@ export function TableSectionRenderer({
// 컬럼 모드/조회 옵션 변경 핸들러
const handleDataSourceChange = useCallback(
async (columnField: string, optionId: string) => {
console.log("🔍 [handleDataSourceChange] 시작:", { columnField, optionId });
setActiveDataSources((prev) => ({
...prev,
[columnField]: optionId,
@@ -590,23 +588,19 @@ export function TableSectionRenderer({
// 해당 컬럼의 모든 행 데이터 재조회
const column = tableConfig.columns.find((col) => col.field === columnField);
console.log("🔍 [handleDataSourceChange] 컬럼 찾기:", { column: column?.field, hasLookup: column?.lookup?.enabled });
// lookup 설정이 있는 경우 (새로운 조회 기능)
if (column?.lookup?.enabled && column.lookup.options) {
const selectedOption = column.lookup.options.find((opt) => opt.id === optionId);
console.log("🔍 [handleDataSourceChange] 선택된 옵션:", { selectedOption, optionId });
if (!selectedOption) return;
// sameTable 타입: 현재 행의 소스 데이터에서 값 복사 (외부 조회 필요 없음)
if (selectedOption.type === "sameTable") {
console.log("🔍 [handleDataSourceChange] sameTable 타입 - 소스 데이터에서 복사");
const updatedData = tableData.map((row) => {
// sourceField에서 값을 가져와 해당 컬럼에 복사
// row에 _sourceData가 있으면 거기서, 없으면 row 자체에서 가져옴
const sourceData = row._sourceData || row;
const newValue = sourceData[selectedOption.valueColumn] ?? row[columnField];
console.log("🔍 [handleDataSourceChange] sameTable 값 복사:", { valueColumn: selectedOption.valueColumn, sourceData, newValue });
return { ...row, [columnField]: newValue };
});
@@ -616,16 +610,8 @@ export function TableSectionRenderer({
}
// 모든 행에 대해 새 값 조회
console.log("🔍 [handleDataSourceChange] 외부 테이블 조회 시작:", {
type: selectedOption.type,
tableName: selectedOption.tableName,
valueColumn: selectedOption.valueColumn,
conditions: selectedOption.conditions,
tableDataLength: tableData.length,
});
const updatedData = await Promise.all(
tableData.map(async (row, rowIndex) => {
tableData.map(async (row) => {
let newValue: any = row[columnField];
// 조인 조건 구성 (4가지 소스 타입 지원)
@@ -657,13 +643,6 @@ export function TableSectionRenderer({
};
});
console.log(`🔍 [handleDataSourceChange] 행 ${rowIndex} 조회:`, {
rowData: row,
sourceData: row._sourceData,
formData,
joinConditions,
});
// 외부 테이블에서 값 조회 (_sourceData 전달)
const sourceData = row._sourceData || row;
const value = await fetchExternalValue(
@@ -675,8 +654,6 @@ export function TableSectionRenderer({
formData
);
console.log(`🔍 [handleDataSourceChange] 행 ${rowIndex} 조회 결과:`, { value });
if (value !== undefined) {
newValue = value;
}
@@ -688,7 +665,6 @@ export function TableSectionRenderer({
// 계산 필드 업데이트
const calculatedData = calculateAll(updatedData);
handleDataChange(calculatedData);
console.log("🔍 [handleDataSourceChange] 완료:", { calculatedData });
return;
}

View File

@@ -807,7 +807,7 @@ function ColumnSettingItem({
</div>
<div>
<p className="text-[9px] text-muted-foreground mb-0.5"> (WHERE)</p>
<p className="text-[9px] text-muted-foreground mb-0.5"> </p>
<Select
value={cond.externalLookup.matchColumn}
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, {
@@ -826,7 +826,7 @@ function ColumnSettingItem({
</div>
<div>
<p className="text-[9px] text-muted-foreground mb-0.5"> (SELECT)</p>
<p className="text-[9px] text-muted-foreground mb-0.5"> </p>
<Select
value={cond.externalLookup.resultColumn}
onValueChange={(value) => {
@@ -850,7 +850,7 @@ function ColumnSettingItem({
{/* 2행: 비교 값 출처 */}
<div className="p-1.5 bg-orange-50/50 rounded">
<p className="text-[9px] text-muted-foreground mb-1"> </p>
<p className="text-[9px] text-muted-foreground mb-1"> ( )</p>
<div className="flex items-center gap-1.5">
<Select
value={cond.externalLookup.matchSourceType}
@@ -935,14 +935,14 @@ function ColumnSettingItem({
{/* 3행: 최종 조회 컬럼 */}
<div className="flex items-center gap-1.5">
<span className="text-[9px] text-muted-foreground"> </span>
<span className="text-[9px] text-muted-foreground"> ( )</span>
<span className="text-[10px] text-muted-foreground">=</span>
<Select
value={cond.targetColumn}
onValueChange={(value) => updateLookupCondition(optIndex, condIndex, { targetColumn: value })}
>
<SelectTrigger className="h-5 text-[9px] flex-1">
<SelectValue placeholder="조회 컬럼" />
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{option.tableName && (
@@ -958,9 +958,9 @@ function ColumnSettingItem({
</div>
{/* 설명 텍스트 */}
{cond.externalLookup.tableName && cond.externalLookup.matchColumn && cond.externalLookup.resultColumn && (
{cond.externalLookup.tableName && cond.externalLookup.matchColumn && cond.externalLookup.resultColumn && cond.targetColumn && (
<p className="text-[9px] text-orange-600 bg-orange-100/50 rounded px-1.5 py-0.5">
{cond.externalLookup.tableName} {cond.externalLookup.matchColumn} = {" "}
{cond.externalLookup.tableName} {cond.externalLookup.matchColumn} = ( ) {" "}
{cond.externalLookup.resultColumn} {option.tableName}.{cond.targetColumn}
</p>
)}