fix(RepeaterTable): 조회 컬럼 헤더 라벨 개선 및 코드 정리
헤더에 "컬럼명 - 옵션라벨" 형식으로 전체 정보 표시 옵션 변경 시 컬럼 너비 자동 재계산 API 검색 시 정확한 일치 검색(equals) 적용 디버그 로그 제거 설정 UI 라벨 사용자 친화적으로 변경
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -76,6 +76,7 @@ export interface DynamicDataSourceConfig {
|
||||
export interface DynamicDataSourceOption {
|
||||
id: string;
|
||||
label: string; // 표시 라벨 (예: "거래처별 단가")
|
||||
headerLabel?: string; // 헤더에 표시될 전체 라벨 (예: "단가 - 거래처별 단가")
|
||||
|
||||
// 조회 방식
|
||||
sourceType: "table" | "multiTable" | "api";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user