feat(universal-form-modal): 테이블 컬럼 저장 설정 및 참조 표시 기능 구현
컬럼별 저장 여부 설정 (saveToTarget: true/false) 저장 안 함 컬럼: 참조 ID로 소스 테이블 조회하여 표시만 함 수정 모드에서 참조 컬럼 값 자동 조회 (loadReferenceColumnValues) Select 컴포넌트 빈 값 필터링으로 안정성 개선 조건 탭 변경 시 소스 데이터 즉시 로드 컬럼 필드 선택 안 함 옵션 추가 (표시 전용 컬럼)
This commit is contained in:
parent
00376202fd
commit
ef991b3b26
|
|
@ -1636,7 +1636,7 @@ export function ModalRepeaterTableConfigPanel({
|
|||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(localConfig.columns || []).map((col) => (
|
||||
{(localConfig.columns || []).filter((col) => col.field && col.field !== "").map((col) => (
|
||||
<SelectItem key={col.field} value={col.field}>
|
||||
{col.label} ({col.field})
|
||||
</SelectItem>
|
||||
|
|
@ -1900,7 +1900,7 @@ export function ModalRepeaterTableConfigPanel({
|
|||
<SelectValue placeholder="현재 행 필드" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(localConfig.columns || []).map((col) => (
|
||||
{(localConfig.columns || []).filter((col) => col.field && col.field !== "").map((col) => (
|
||||
<SelectItem key={col.field} value={col.field}>
|
||||
{col.label}
|
||||
</SelectItem>
|
||||
|
|
@ -2056,7 +2056,7 @@ export function ModalRepeaterTableConfigPanel({
|
|||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(localConfig.columns || []).map((col) => (
|
||||
{(localConfig.columns || []).filter((col) => col.field && col.field !== "").map((col) => (
|
||||
<SelectItem key={col.field} value={col.field}>
|
||||
{col.label} ({col.field})
|
||||
</SelectItem>
|
||||
|
|
@ -2303,7 +2303,7 @@ export function ModalRepeaterTableConfigPanel({
|
|||
<SelectValue placeholder="현재 행 필드" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(localConfig.columns || []).map((col) => (
|
||||
{(localConfig.columns || []).filter((col) => col.field && col.field !== "").map((col) => (
|
||||
<SelectItem key={col.field} value={col.field}>
|
||||
{col.label}
|
||||
</SelectItem>
|
||||
|
|
|
|||
|
|
@ -481,7 +481,7 @@ export function RepeaterTable({
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{column.selectOptions?.map((option) => (
|
||||
{column.selectOptions?.filter((option) => option.value && option.value !== "").map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
|
|
|
|||
|
|
@ -561,7 +561,7 @@ export function SimpleRepeaterTableComponent({
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{column.selectOptions?.map((option) => (
|
||||
{column.selectOptions?.filter((option) => option.value && option.value !== "").map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
|
|
|
|||
|
|
@ -1539,7 +1539,7 @@ export function SimpleRepeaterTableConfigPanel({
|
|||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<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}>
|
||||
{col.label}
|
||||
</SelectItem>
|
||||
|
|
|
|||
|
|
@ -498,11 +498,43 @@ export function TableSectionRenderer({
|
|||
if (!hasDynamicSelectColumns) return;
|
||||
if (!conditionalConfig?.sourceFilter?.enabled) return;
|
||||
if (!activeConditionTab) return;
|
||||
if (!tableConfig.source?.tableName) return;
|
||||
|
||||
// 조건 변경 시 캐시 리셋하고 다시 로드
|
||||
// 조건 변경 시 캐시 리셋하고 즉시 다시 로드
|
||||
sourceDataLoadedRef.current = false;
|
||||
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 옵션 생성
|
||||
const dynamicSelectOptionsMap = useMemo(() => {
|
||||
|
|
@ -540,6 +572,45 @@ export function TableSectionRenderer({
|
|||
return optionsMap;
|
||||
}, [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(
|
||||
(rowIndex: number, columnField: string, selectedValue: string, conditionValue?: string) => {
|
||||
|
|
@ -617,6 +688,91 @@ export function TableSectionRenderer({
|
|||
[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 표시)
|
||||
useEffect(() => {
|
||||
// 이미 초기화되었으면 스킵
|
||||
|
|
@ -632,8 +788,11 @@ export function TableSectionRenderer({
|
|||
});
|
||||
setTableData(initialData);
|
||||
initialDataLoadedRef.current = true;
|
||||
|
||||
// 참조 컬럼 값 조회 (saveToTarget: false인 컬럼)
|
||||
loadReferenceColumnValues(initialData);
|
||||
}
|
||||
}, [sectionId, formData]);
|
||||
}, [sectionId, formData, loadReferenceColumnValues]);
|
||||
|
||||
// RepeaterColumnConfig로 변환 (동적 Select 옵션 반영)
|
||||
const columns: RepeaterColumnConfig[] = useMemo(() => {
|
||||
|
|
@ -691,45 +850,6 @@ export function TableSectionRenderer({
|
|||
[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 행 선택 모드 지원)
|
||||
const handleRowChange = useCallback(
|
||||
(index: number, newRow: any, conditionValue?: string) => {
|
||||
|
|
@ -1377,9 +1497,10 @@ export function TableSectionRenderer({
|
|||
const { triggerType } = conditionalConfig;
|
||||
|
||||
// 정적 옵션과 동적 옵션 병합 (동적 옵션이 있으면 우선 사용)
|
||||
const effectiveOptions = conditionalConfig.optionSource?.enabled && dynamicOptions.length > 0
|
||||
// 빈 value를 가진 옵션은 제외 (Select.Item은 빈 문자열 value를 허용하지 않음)
|
||||
const effectiveOptions = (conditionalConfig.optionSource?.enabled && dynamicOptions.length > 0
|
||||
? dynamicOptions
|
||||
: conditionalConfig.options || [];
|
||||
: conditionalConfig.options || []).filter(opt => opt.value && opt.value.trim() !== "");
|
||||
|
||||
// 로딩 중이면 로딩 표시
|
||||
if (dynamicOptionsLoading) {
|
||||
|
|
|
|||
|
|
@ -102,11 +102,13 @@ const CascadingSelectField: React.FC<CascadingSelectFieldProps> = ({
|
|||
: config.noOptionsMessage || "선택 가능한 항목이 없습니다"}
|
||||
</div>
|
||||
) : (
|
||||
options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))
|
||||
options
|
||||
.filter((option) => option.value && option.value !== "")
|
||||
.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
|
@ -1081,6 +1083,14 @@ export function UniversalFormModalComponent({
|
|||
// 공통 필드 병합 + 개별 품목 데이터
|
||||
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) {
|
||||
itemToSave[config.saveConfig.primaryKeyColumn] = mainRecordId;
|
||||
|
|
@ -1680,11 +1690,13 @@ export function UniversalFormModalComponent({
|
|||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sourceData.length > 0 ? (
|
||||
sourceData.map((row, index) => (
|
||||
<SelectItem key={`${row[valueColumn] || index}_${index}`} value={String(row[valueColumn] || "")}>
|
||||
{getDisplayText(row)}
|
||||
</SelectItem>
|
||||
))
|
||||
sourceData
|
||||
.filter((row) => row[valueColumn] !== null && row[valueColumn] !== undefined && String(row[valueColumn]) !== "")
|
||||
.map((row, index) => (
|
||||
<SelectItem key={`${row[valueColumn]}_${index}`} value={String(row[valueColumn])}>
|
||||
{getDisplayText(row)}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="_empty" disabled>
|
||||
{cachedData === undefined ? "데이터를 불러오는 중..." : "데이터가 없습니다"}
|
||||
|
|
@ -2345,11 +2357,13 @@ function SelectField({ fieldId, value, onChange, optionConfig, placeholder, disa
|
|||
<SelectValue placeholder={loading ? "로딩 중..." : placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{options
|
||||
.filter((option) => option.value && option.value !== "")
|
||||
.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -728,13 +728,13 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents
|
|||
{/* 테이블 컬럼 목록 (테이블 타입만) */}
|
||||
{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">
|
||||
{section.tableConfig.columns.slice(0, 4).map((col) => (
|
||||
{section.tableConfig.columns.slice(0, 4).map((col, idx) => (
|
||||
<Badge
|
||||
key={col.field}
|
||||
key={col.field || `col_${idx}`}
|
||||
variant="outline"
|
||||
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>
|
||||
))}
|
||||
{section.tableConfig.columns.length > 4 && (
|
||||
|
|
|
|||
|
|
@ -450,10 +450,13 @@ function ColumnSettingItem({
|
|||
variant="outline"
|
||||
role="combobox"
|
||||
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">
|
||||
{col.field || "필드 선택..."}
|
||||
{col.field || "(저장 안 함)"}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
|
|
@ -466,6 +469,25 @@ function ColumnSettingItem({
|
|||
필드를 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<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) => (
|
||||
<CommandItem
|
||||
key={column.column_name}
|
||||
|
|
@ -1786,6 +1808,185 @@ function ColumnSettingItem({
|
|||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
@ -2826,11 +3027,13 @@ export function TableSectionSettingsModal({
|
|||
컬럼 설정에서 먼저 컬럼을 추가하세요
|
||||
</SelectItem>
|
||||
) : (
|
||||
(tableConfig.columns || []).map((col) => (
|
||||
<SelectItem key={col.field} value={col.field}>
|
||||
{col.label || col.field}
|
||||
</SelectItem>
|
||||
))
|
||||
(tableConfig.columns || [])
|
||||
.filter((col) => col.field) // 빈 필드명 제외
|
||||
.map((col, idx) => (
|
||||
<SelectItem key={col.field || `col_${idx}`} value={col.field}>
|
||||
{col.label || col.field}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
|
|
|||
|
|
@ -426,6 +426,31 @@ export interface TableColumnConfig {
|
|||
// 부모에서 값 받기 (모든 행에 동일한 값 적용)
|
||||
receiveFromParent?: boolean; // 부모에서 값 받기 활성화
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
|
|
|||
Loading…
Reference in New Issue