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>
|
</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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue