feat(universal-form-modal): 테이블 컬럼 저장 설정 및 참조 표시 기능 구현

컬럼별 저장 여부 설정 (saveToTarget: true/false)
저장 안 함 컬럼: 참조 ID로 소스 테이블 조회하여 표시만 함
수정 모드에서 참조 컬럼 값 자동 조회 (loadReferenceColumnValues)
Select 컴포넌트 빈 값 필터링으로 안정성 개선
조건 탭 변경 시 소스 데이터 즉시 로드
컬럼 필드 선택 안 함 옵션 추가 (표시 전용 컬럼)
This commit is contained in:
SeongHyun Kim 2025-12-29 17:42:30 +09:00
parent 00376202fd
commit ef991b3b26
9 changed files with 439 additions and 76 deletions

View File

@ -1636,7 +1636,7 @@ export function ModalRepeaterTableConfigPanel({
</SelectValue> </SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{(localConfig.columns || []).map((col) => ( {(localConfig.columns || []).filter((col) => col.field && col.field !== "").map((col) => (
<SelectItem key={col.field} value={col.field}> <SelectItem key={col.field} value={col.field}>
{col.label} ({col.field}) {col.label} ({col.field})
</SelectItem> </SelectItem>
@ -1900,7 +1900,7 @@ export function ModalRepeaterTableConfigPanel({
<SelectValue placeholder="현재 행 필드" /> <SelectValue placeholder="현재 행 필드" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{(localConfig.columns || []).map((col) => ( {(localConfig.columns || []).filter((col) => col.field && col.field !== "").map((col) => (
<SelectItem key={col.field} value={col.field}> <SelectItem key={col.field} value={col.field}>
{col.label} {col.label}
</SelectItem> </SelectItem>
@ -2056,7 +2056,7 @@ export function ModalRepeaterTableConfigPanel({
<SelectValue placeholder="필드 선택" /> <SelectValue placeholder="필드 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{(localConfig.columns || []).map((col) => ( {(localConfig.columns || []).filter((col) => col.field && col.field !== "").map((col) => (
<SelectItem key={col.field} value={col.field}> <SelectItem key={col.field} value={col.field}>
{col.label} ({col.field}) {col.label} ({col.field})
</SelectItem> </SelectItem>
@ -2303,7 +2303,7 @@ export function ModalRepeaterTableConfigPanel({
<SelectValue placeholder="현재 행 필드" /> <SelectValue placeholder="현재 행 필드" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{(localConfig.columns || []).map((col) => ( {(localConfig.columns || []).filter((col) => col.field && col.field !== "").map((col) => (
<SelectItem key={col.field} value={col.field}> <SelectItem key={col.field} value={col.field}>
{col.label} {col.label}
</SelectItem> </SelectItem>

View File

@ -481,7 +481,7 @@ export function RepeaterTable({
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{column.selectOptions?.map((option) => ( {column.selectOptions?.filter((option) => option.value && option.value !== "").map((option) => (
<SelectItem key={option.value} value={option.value}> <SelectItem key={option.value} value={option.value}>
{option.label} {option.label}
</SelectItem> </SelectItem>

View File

@ -561,7 +561,7 @@ export function SimpleRepeaterTableComponent({
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{column.selectOptions?.map((option) => ( {column.selectOptions?.filter((option) => option.value && option.value !== "").map((option) => (
<SelectItem key={option.value} value={option.value}> <SelectItem key={option.value} value={option.value}>
{option.label} {option.label}
</SelectItem> </SelectItem>

View File

@ -1539,7 +1539,7 @@ export function SimpleRepeaterTableConfigPanel({
<SelectValue placeholder="컬럼 선택" /> <SelectValue placeholder="컬럼 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{(localConfig.columns || []).filter(c => c.type === "number").map((col) => ( {(localConfig.columns || []).filter(c => c.type === "number" && c.field && c.field !== "").map((col) => (
<SelectItem key={col.field} value={col.field}> <SelectItem key={col.field} value={col.field}>
{col.label} {col.label}
</SelectItem> </SelectItem>

View File

@ -498,11 +498,43 @@ export function TableSectionRenderer({
if (!hasDynamicSelectColumns) return; if (!hasDynamicSelectColumns) return;
if (!conditionalConfig?.sourceFilter?.enabled) return; if (!conditionalConfig?.sourceFilter?.enabled) return;
if (!activeConditionTab) return; if (!activeConditionTab) return;
if (!tableConfig.source?.tableName) return;
// 조건 변경 시 캐시 리셋하고 다시 로드 // 조건 변경 시 캐시 리셋하고 즉시 다시 로드
sourceDataLoadedRef.current = false; sourceDataLoadedRef.current = false;
setSourceDataCache([]); setSourceDataCache([]);
}, [activeConditionTab, hasDynamicSelectColumns, conditionalConfig?.sourceFilter?.enabled]);
// 즉시 데이터 다시 로드 (기존 useEffect에 의존하지 않고 직접 호출)
const loadSourceData = async () => {
try {
const filterCondition: Record<string, any> = {};
filterCondition[conditionalConfig.sourceFilter!.filterColumn] = activeConditionTab;
const response = await apiClient.post(
`/table-management/tables/${tableConfig.source!.tableName}/data`,
{
search: filterCondition,
size: 1000,
page: 1,
}
);
if (response.data.success && response.data.data?.data) {
setSourceDataCache(response.data.data.data);
sourceDataLoadedRef.current = true;
console.log("[TableSectionRenderer] 조건 탭 변경 - 소스 데이터 로드 완료:", {
tableName: tableConfig.source!.tableName,
rowCount: response.data.data.data.length,
filter: filterCondition,
});
}
} catch (error) {
console.error("[TableSectionRenderer] 소스 데이터 로드 실패:", error);
}
};
loadSourceData();
}, [activeConditionTab, hasDynamicSelectColumns, conditionalConfig?.sourceFilter?.enabled, conditionalConfig?.sourceFilter?.filterColumn, tableConfig.source?.tableName]);
// 컬럼별 동적 Select 옵션 생성 // 컬럼별 동적 Select 옵션 생성
const dynamicSelectOptionsMap = useMemo(() => { const dynamicSelectOptionsMap = useMemo(() => {
@ -540,6 +572,45 @@ export function TableSectionRenderer({
return optionsMap; return optionsMap;
}, [sourceDataCache, tableConfig.columns]); }, [sourceDataCache, tableConfig.columns]);
// 데이터 변경 핸들러 (날짜 일괄 적용 로직 포함) - 다른 함수에서 참조하므로 먼저 정의
const handleDataChange = useCallback(
(newData: any[]) => {
let processedData = newData;
// 날짜 일괄 적용 로직: batchApply가 활성화된 날짜 컬럼 처리
const batchApplyColumns = tableConfig.columns.filter(
(col) => col.type === "date" && col.batchApply === true
);
for (const dateCol of batchApplyColumns) {
// 이미 일괄 적용된 컬럼은 건너뜀
if (batchAppliedFields.has(dateCol.field)) continue;
// 해당 컬럼에 값이 있는 행과 없는 행 분류
const itemsWithDate = processedData.filter((item) => item[dateCol.field]);
const itemsWithoutDate = processedData.filter((item) => !item[dateCol.field]);
// 조건: 정확히 1개만 날짜가 있고, 나머지는 모두 비어있을 때
if (itemsWithDate.length === 1 && itemsWithoutDate.length > 0) {
const selectedDate = itemsWithDate[0][dateCol.field];
// 모든 행에 동일한 날짜 적용
processedData = processedData.map((item) => ({
...item,
[dateCol.field]: selectedDate,
}));
// 플래그 활성화 (이후 개별 수정 가능)
setBatchAppliedFields((prev) => new Set([...prev, dateCol.field]));
}
}
setTableData(processedData);
onTableDataChange(processedData);
},
[onTableDataChange, tableConfig.columns, batchAppliedFields]
);
// 행 선택 모드: 드롭다운 값 변경 시 같은 소스 행의 다른 컬럼들 자동 채움 // 행 선택 모드: 드롭다운 값 변경 시 같은 소스 행의 다른 컬럼들 자동 채움
const handleDynamicSelectChange = useCallback( const handleDynamicSelectChange = useCallback(
(rowIndex: number, columnField: string, selectedValue: string, conditionValue?: string) => { (rowIndex: number, columnField: string, selectedValue: string, conditionValue?: string) => {
@ -617,6 +688,91 @@ export function TableSectionRenderer({
[tableConfig.columns, sourceDataCache, tableData, conditionalTableData, isConditionalMode, handleDataChange, onConditionalTableDataChange] [tableConfig.columns, sourceDataCache, tableData, conditionalTableData, isConditionalMode, handleDataChange, onConditionalTableDataChange]
); );
// 참조 컬럼 값 조회 함수 (saveToTarget: false인 컬럼에 대해 소스 테이블 조회)
const loadReferenceColumnValues = useCallback(async (data: any[]) => {
// saveToTarget: false이고 referenceDisplay가 설정된 컬럼 찾기
const referenceColumns = (tableConfig.columns || []).filter(
(col) => col.saveConfig?.saveToTarget === false && col.saveConfig?.referenceDisplay
);
if (referenceColumns.length === 0) return;
const sourceTableName = tableConfig.source?.tableName;
if (!sourceTableName) {
console.warn("[TableSectionRenderer] 참조 조회를 위한 소스 테이블이 설정되지 않았습니다.");
return;
}
// 참조 ID들 수집 (중복 제거)
const referenceIdSet = new Set<string>();
for (const col of referenceColumns) {
const refDisplay = col.saveConfig!.referenceDisplay!;
for (const row of data) {
const refId = row[refDisplay.referenceIdField];
if (refId !== undefined && refId !== null && refId !== "") {
referenceIdSet.add(String(refId));
}
}
}
if (referenceIdSet.size === 0) return;
try {
// 소스 테이블에서 참조 ID에 해당하는 데이터 조회
const response = await apiClient.post(
`/table-management/tables/${sourceTableName}/data`,
{
search: { id: Array.from(referenceIdSet) }, // ID 배열로 조회
size: 1000,
page: 1,
}
);
if (!response.data?.success || !response.data?.data?.data) {
console.warn("[TableSectionRenderer] 참조 데이터 조회 실패");
return;
}
const sourceData: any[] = response.data.data.data;
// ID를 키로 하는 맵 생성
const sourceDataMap: Record<string, any> = {};
for (const sourceRow of sourceData) {
sourceDataMap[String(sourceRow.id)] = sourceRow;
}
// 각 행에 참조 컬럼 값 채우기
const updatedData = data.map((row) => {
const newRow = { ...row };
for (const col of referenceColumns) {
const refDisplay = col.saveConfig!.referenceDisplay!;
const refId = row[refDisplay.referenceIdField];
if (refId !== undefined && refId !== null && refId !== "") {
const sourceRow = sourceDataMap[String(refId)];
if (sourceRow) {
newRow[col.field] = sourceRow[refDisplay.sourceColumn];
}
}
}
return newRow;
});
console.log("[TableSectionRenderer] 참조 컬럼 값 조회 완료:", {
referenceColumns: referenceColumns.map((c) => c.field),
updatedRowCount: updatedData.length,
});
setTableData(updatedData);
} catch (error) {
console.error("[TableSectionRenderer] 참조 데이터 조회 실패:", error);
}
}, [tableConfig.columns, tableConfig.source?.tableName]);
// formData에서 초기 테이블 데이터 로드 (수정 모드에서 _groupedData 표시) // formData에서 초기 테이블 데이터 로드 (수정 모드에서 _groupedData 표시)
useEffect(() => { useEffect(() => {
// 이미 초기화되었으면 스킵 // 이미 초기화되었으면 스킵
@ -632,8 +788,11 @@ export function TableSectionRenderer({
}); });
setTableData(initialData); setTableData(initialData);
initialDataLoadedRef.current = true; initialDataLoadedRef.current = true;
// 참조 컬럼 값 조회 (saveToTarget: false인 컬럼)
loadReferenceColumnValues(initialData);
} }
}, [sectionId, formData]); }, [sectionId, formData, loadReferenceColumnValues]);
// RepeaterColumnConfig로 변환 (동적 Select 옵션 반영) // RepeaterColumnConfig로 변환 (동적 Select 옵션 반영)
const columns: RepeaterColumnConfig[] = useMemo(() => { const columns: RepeaterColumnConfig[] = useMemo(() => {
@ -691,45 +850,6 @@ export function TableSectionRenderer({
[calculateRow] [calculateRow]
); );
// 데이터 변경 핸들러 (날짜 일괄 적용 로직 포함)
const handleDataChange = useCallback(
(newData: any[]) => {
let processedData = newData;
// 날짜 일괄 적용 로직: batchApply가 활성화된 날짜 컬럼 처리
const batchApplyColumns = tableConfig.columns.filter(
(col) => col.type === "date" && col.batchApply === true
);
for (const dateCol of batchApplyColumns) {
// 이미 일괄 적용된 컬럼은 건너뜀
if (batchAppliedFields.has(dateCol.field)) continue;
// 해당 컬럼에 값이 있는 행과 없는 행 분류
const itemsWithDate = processedData.filter((item) => item[dateCol.field]);
const itemsWithoutDate = processedData.filter((item) => !item[dateCol.field]);
// 조건: 정확히 1개만 날짜가 있고, 나머지는 모두 비어있을 때
if (itemsWithDate.length === 1 && itemsWithoutDate.length > 0) {
const selectedDate = itemsWithDate[0][dateCol.field];
// 모든 행에 동일한 날짜 적용
processedData = processedData.map((item) => ({
...item,
[dateCol.field]: selectedDate,
}));
// 플래그 활성화 (이후 개별 수정 가능)
setBatchAppliedFields((prev) => new Set([...prev, dateCol.field]));
}
}
setTableData(processedData);
onTableDataChange(processedData);
},
[onTableDataChange, tableConfig.columns, batchAppliedFields]
);
// 행 변경 핸들러 (동적 Select 행 선택 모드 지원) // 행 변경 핸들러 (동적 Select 행 선택 모드 지원)
const handleRowChange = useCallback( const handleRowChange = useCallback(
(index: number, newRow: any, conditionValue?: string) => { (index: number, newRow: any, conditionValue?: string) => {
@ -1377,9 +1497,10 @@ export function TableSectionRenderer({
const { triggerType } = conditionalConfig; const { triggerType } = conditionalConfig;
// 정적 옵션과 동적 옵션 병합 (동적 옵션이 있으면 우선 사용) // 정적 옵션과 동적 옵션 병합 (동적 옵션이 있으면 우선 사용)
const effectiveOptions = conditionalConfig.optionSource?.enabled && dynamicOptions.length > 0 // 빈 value를 가진 옵션은 제외 (Select.Item은 빈 문자열 value를 허용하지 않음)
const effectiveOptions = (conditionalConfig.optionSource?.enabled && dynamicOptions.length > 0
? dynamicOptions ? dynamicOptions
: conditionalConfig.options || []; : conditionalConfig.options || []).filter(opt => opt.value && opt.value.trim() !== "");
// 로딩 중이면 로딩 표시 // 로딩 중이면 로딩 표시
if (dynamicOptionsLoading) { if (dynamicOptionsLoading) {

View File

@ -102,11 +102,13 @@ const CascadingSelectField: React.FC<CascadingSelectFieldProps> = ({
: config.noOptionsMessage || "선택 가능한 항목이 없습니다"} : config.noOptionsMessage || "선택 가능한 항목이 없습니다"}
</div> </div>
) : ( ) : (
options.map((option) => ( options
<SelectItem key={option.value} value={option.value}> .filter((option) => option.value && option.value !== "")
{option.label} .map((option) => (
</SelectItem> <SelectItem key={option.value} value={option.value}>
)) {option.label}
</SelectItem>
))
)} )}
</SelectContent> </SelectContent>
</Select> </Select>
@ -1081,6 +1083,14 @@ export function UniversalFormModalComponent({
// 공통 필드 병합 + 개별 품목 데이터 // 공통 필드 병합 + 개별 품목 데이터
const itemToSave = { ...commonFieldsData, ...item }; const itemToSave = { ...commonFieldsData, ...item };
// saveToTarget: false인 컬럼은 저장에서 제외
const columns = section.tableConfig?.columns || [];
for (const col of columns) {
if (col.saveConfig?.saveToTarget === false && col.field in itemToSave) {
delete itemToSave[col.field];
}
}
// 메인 레코드와 연결이 필요한 경우 // 메인 레코드와 연결이 필요한 경우
if (mainRecordId && config.saveConfig.primaryKeyColumn) { if (mainRecordId && config.saveConfig.primaryKeyColumn) {
itemToSave[config.saveConfig.primaryKeyColumn] = mainRecordId; itemToSave[config.saveConfig.primaryKeyColumn] = mainRecordId;
@ -1680,11 +1690,13 @@ export function UniversalFormModalComponent({
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{sourceData.length > 0 ? ( {sourceData.length > 0 ? (
sourceData.map((row, index) => ( sourceData
<SelectItem key={`${row[valueColumn] || index}_${index}`} value={String(row[valueColumn] || "")}> .filter((row) => row[valueColumn] !== null && row[valueColumn] !== undefined && String(row[valueColumn]) !== "")
{getDisplayText(row)} .map((row, index) => (
</SelectItem> <SelectItem key={`${row[valueColumn]}_${index}`} value={String(row[valueColumn])}>
)) {getDisplayText(row)}
</SelectItem>
))
) : ( ) : (
<SelectItem value="_empty" disabled> <SelectItem value="_empty" disabled>
{cachedData === undefined ? "데이터를 불러오는 중..." : "데이터가 없습니다"} {cachedData === undefined ? "데이터를 불러오는 중..." : "데이터가 없습니다"}
@ -2345,11 +2357,13 @@ function SelectField({ fieldId, value, onChange, optionConfig, placeholder, disa
<SelectValue placeholder={loading ? "로딩 중..." : placeholder} /> <SelectValue placeholder={loading ? "로딩 중..." : placeholder} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{options.map((option) => ( {options
<SelectItem key={option.value} value={option.value}> .filter((option) => option.value && option.value !== "")
{option.label} .map((option) => (
</SelectItem> <SelectItem key={option.value} value={option.value}>
))} {option.label}
</SelectItem>
))}
</SelectContent> </SelectContent>
</Select> </Select>
); );

View File

@ -728,13 +728,13 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents
{/* 테이블 컬럼 목록 (테이블 타입만) */} {/* 테이블 컬럼 목록 (테이블 타입만) */}
{section.type === "table" && section.tableConfig?.columns && section.tableConfig.columns.length > 0 && ( {section.type === "table" && section.tableConfig?.columns && section.tableConfig.columns.length > 0 && (
<div className="flex flex-wrap gap-1.5 max-w-full overflow-hidden pt-1"> <div className="flex flex-wrap gap-1.5 max-w-full overflow-hidden pt-1">
{section.tableConfig.columns.slice(0, 4).map((col) => ( {section.tableConfig.columns.slice(0, 4).map((col, idx) => (
<Badge <Badge
key={col.field} key={col.field || `col_${idx}`}
variant="outline" variant="outline"
className="text-xs px-2 py-0.5 shrink-0 text-purple-600 bg-purple-50 border-purple-200" className="text-xs px-2 py-0.5 shrink-0 text-purple-600 bg-purple-50 border-purple-200"
> >
{col.label} {col.label || col.field || `컬럼 ${idx + 1}`}
</Badge> </Badge>
))} ))}
{section.tableConfig.columns.length > 4 && ( {section.tableConfig.columns.length > 4 && (

View File

@ -450,10 +450,13 @@ function ColumnSettingItem({
variant="outline" variant="outline"
role="combobox" role="combobox"
aria-expanded={fieldSearchOpen} aria-expanded={fieldSearchOpen}
className="h-8 w-full justify-between text-xs mt-1" className={cn(
"h-8 w-full justify-between text-xs mt-1",
!col.field && "text-muted-foreground"
)}
> >
<span className="truncate"> <span className="truncate">
{col.field || "필드 선택..."} {col.field || "(저장 안 함)"}
</span> </span>
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
</Button> </Button>
@ -466,6 +469,25 @@ function ColumnSettingItem({
. .
</CommandEmpty> </CommandEmpty>
<CommandGroup> <CommandGroup>
{/* 선택 안 함 옵션 */}
<CommandItem
key="__none__"
value="__none__"
onSelect={() => {
onUpdate({ field: "" });
setFieldSearchOpen(false);
}}
className="text-xs text-muted-foreground"
>
<Check
className={cn(
"mr-2 h-3 w-3",
!col.field ? "opacity-100" : "opacity-0"
)}
/>
<span className="italic">( - )</span>
</CommandItem>
{/* 실제 컬럼 목록 */}
{saveTableColumns.map((column) => ( {saveTableColumns.map((column) => (
<CommandItem <CommandItem
key={column.column_name} key={column.column_name}
@ -1786,6 +1808,185 @@ function ColumnSettingItem({
)} )}
</div> </div>
)} )}
{/* ============================================ */}
{/* 저장 설정 섹션 */}
{/* ============================================ */}
<div className="space-y-2 border-t pt-3">
<Label className="text-xs font-semibold flex items-center gap-2">
</Label>
<p className="text-[10px] text-muted-foreground">
DB에 .
</p>
{/* 저장 여부 라디오 버튼 */}
<div className="space-y-2 pl-2">
{/* 저장함 옵션 */}
<label className="flex items-start gap-3 p-2 border rounded-lg cursor-pointer hover:bg-muted/50 transition-colors">
<input
type="radio"
name={`saveConfig_${col.field}`}
checked={col.saveConfig?.saveToTarget !== false}
onChange={() => {
onUpdate({
saveConfig: {
saveToTarget: true,
},
});
}}
className="mt-0.5"
/>
<div className="flex-1">
<span className="text-xs font-medium"> ()</span>
<p className="text-[10px] text-muted-foreground mt-0.5">
/ DB에 .
</p>
</div>
</label>
{/* 저장 안 함 옵션 */}
<label className="flex items-start gap-3 p-2 border rounded-lg cursor-pointer hover:bg-muted/50 transition-colors">
<input
type="radio"
name={`saveConfig_${col.field}`}
checked={col.saveConfig?.saveToTarget === false}
onChange={() => {
onUpdate({
saveConfig: {
saveToTarget: false,
referenceDisplay: {
referenceIdField: "",
sourceColumn: "",
},
},
});
}}
className="mt-0.5"
/>
<div className="flex-1">
<span className="text-xs font-medium"> - </span>
<p className="text-[10px] text-muted-foreground mt-0.5">
ID로 .
</p>
</div>
</label>
</div>
{/* 참조 설정 패널 (저장 안 함 선택 시) */}
{col.saveConfig?.saveToTarget === false && (
<div className="ml-6 p-3 border-2 border-dashed border-amber-300 rounded-lg bg-amber-50/50 space-y-3">
<div className="flex items-center gap-2">
<Search className="h-4 w-4 text-amber-600" />
<span className="text-xs font-semibold text-amber-700"> </span>
</div>
{/* Step 1: ID 컬럼 선택 */}
<div className="space-y-1">
<Label className="text-[10px] font-medium">
1. ID ?
</Label>
<Select
value={col.saveConfig?.referenceDisplay?.referenceIdField || ""}
onValueChange={(value) => {
onUpdate({
saveConfig: {
...col.saveConfig!,
referenceDisplay: {
...col.saveConfig!.referenceDisplay!,
referenceIdField: value,
},
},
});
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="ID 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{(tableConfig.columns || [])
.filter((c) => c.field !== col.field) // 현재 컬럼 제외
.map((c) => (
<SelectItem key={c.field} value={c.field} className="text-xs">
{c.label || c.field}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-[9px] text-muted-foreground">
ID로 .
</p>
</div>
{/* Step 2: 소스 컬럼 선택 */}
<div className="space-y-1">
<Label className="text-[10px] font-medium">
2. ?
</Label>
{sourceTableColumns.length > 0 ? (
<Select
value={col.saveConfig?.referenceDisplay?.sourceColumn || ""}
onValueChange={(value) => {
onUpdate({
saveConfig: {
...col.saveConfig!,
referenceDisplay: {
...col.saveConfig!.referenceDisplay!,
sourceColumn: value,
},
},
});
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="소스 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{sourceTableColumns.map((c) => (
<SelectItem key={c.column_name} value={c.column_name} className="text-xs">
{c.column_name} {c.comment && `(${c.comment})`}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={col.saveConfig?.referenceDisplay?.sourceColumn || ""}
onChange={(e) => {
onUpdate({
saveConfig: {
...col.saveConfig!,
referenceDisplay: {
...col.saveConfig!.referenceDisplay!,
sourceColumn: e.target.value,
},
},
});
}}
placeholder="소스 컬럼명 입력"
className="h-7 text-xs"
/>
)}
<p className="text-[9px] text-muted-foreground">
.
</p>
</div>
{/* 설정 요약 */}
{col.saveConfig?.referenceDisplay?.referenceIdField && col.saveConfig?.referenceDisplay?.sourceColumn && (
<div className="text-[10px] text-amber-700 bg-amber-100 rounded p-2 mt-2">
<strong> :</strong>
<br />
- ({col.label || col.field}) .
<br />
- <strong>{col.saveConfig.referenceDisplay.referenceIdField}</strong>{" "}
<strong>{sourceTableName}</strong> {" "}
<strong>{col.saveConfig.referenceDisplay.sourceColumn}</strong> .
</div>
)}
</div>
)}
</div>
</div> </div>
); );
} }
@ -2826,11 +3027,13 @@ export function TableSectionSettingsModal({
</SelectItem> </SelectItem>
) : ( ) : (
(tableConfig.columns || []).map((col) => ( (tableConfig.columns || [])
<SelectItem key={col.field} value={col.field}> .filter((col) => col.field) // 빈 필드명 제외
{col.label || col.field} .map((col, idx) => (
</SelectItem> <SelectItem key={col.field || `col_${idx}`} value={col.field}>
)) {col.label || col.field}
</SelectItem>
))
)} )}
</SelectContent> </SelectContent>
</Select> </Select>

View File

@ -426,6 +426,31 @@ export interface TableColumnConfig {
// 부모에서 값 받기 (모든 행에 동일한 값 적용) // 부모에서 값 받기 (모든 행에 동일한 값 적용)
receiveFromParent?: boolean; // 부모에서 값 받기 활성화 receiveFromParent?: boolean; // 부모에서 값 받기 활성화
parentFieldName?: string; // 부모 필드명 (미지정 시 field와 동일) parentFieldName?: string; // 부모 필드명 (미지정 시 field와 동일)
// 저장 설정 (컬럼별 저장 여부 및 참조 표시)
saveConfig?: TableColumnSaveConfig;
}
/**
*
* - , ID로
*/
export interface TableColumnSaveConfig {
// 저장 여부 (기본값: true)
// true: 사용자가 입력/선택한 값을 DB에 저장
// false: 저장하지 않고, 참조 ID로 소스 테이블을 조회하여 표시만 함
saveToTarget: boolean;
// 참조 표시 설정 (saveToTarget이 false일 때 사용)
referenceDisplay?: {
// 참조할 ID 컬럼 (같은 테이블 내의 다른 컬럼)
// 예: "inspection_standard_id"
referenceIdField: string;
// 소스 테이블에서 가져올 컬럼
// 예: "inspection_item" → 소스 테이블의 inspection_item 값을 표시
sourceColumn: string;
};
} }
// ============================================ // ============================================