feat(universal-form-modal): 조건부 테이블, 동적 Select 옵션, 서브 테이블 수정 로드 기능 구현
조건부 테이블: 체크박스/탭으로 조건 선택 시 다른 테이블 데이터 관리 동적 Select 옵션: 소스 테이블에서 드롭다운 옵션 동적 로드 행 선택 모드: Select 값 변경 시 같은 소스 행의 연관 컬럼 자동 채움 수정 모드 서브 테이블 로드: loadOnEdit 옵션으로 반복 섹션 데이터 자동 로드 SplitPanelLayout2 메인 테이블 병합: 서브 테이블 수정 시 메인 데이터 함께 조회 연결 필드 그룹 표시 형식: subDisplayColumn 추가로 메인/서브 컬럼 분리 설정 UX 개선: 컬럼 선택 UI를 검색 가능한 Combobox로 전환 saveMainAsFirst 로직 개선: items 없어도 메인 데이터 저장 가능
This commit is contained in:
parent
486e5ee29b
commit
47b23d1aa3
|
|
@ -1973,15 +1973,21 @@ export async function multiTableSave(
|
|||
for (const subTableConfig of subTables || []) {
|
||||
const { tableName, linkColumn, items, options } = subTableConfig;
|
||||
|
||||
if (!tableName || !items || items.length === 0) {
|
||||
logger.info(`서브 테이블 ${tableName} 스킵: 데이터 없음`);
|
||||
// saveMainAsFirst가 활성화된 경우, items가 비어있어도 메인 데이터를 서브 테이블에 저장해야 함
|
||||
const hasSaveMainAsFirst = options?.saveMainAsFirst &&
|
||||
options?.mainFieldMappings &&
|
||||
options.mainFieldMappings.length > 0;
|
||||
|
||||
if (!tableName || (!items?.length && !hasSaveMainAsFirst)) {
|
||||
logger.info(`서브 테이블 ${tableName} 스킵: 데이터 없음 (saveMainAsFirst: ${hasSaveMainAsFirst})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.info(`서브 테이블 ${tableName} 저장 시작:`, {
|
||||
itemsCount: items.length,
|
||||
itemsCount: items?.length || 0,
|
||||
linkColumn,
|
||||
options,
|
||||
hasSaveMainAsFirst,
|
||||
});
|
||||
|
||||
// 기존 데이터 삭제 옵션
|
||||
|
|
@ -1999,7 +2005,15 @@ export async function multiTableSave(
|
|||
}
|
||||
|
||||
// 메인 데이터도 서브 테이블에 저장 (옵션)
|
||||
if (options?.saveMainAsFirst && options?.mainFieldMappings && linkColumn?.subColumn) {
|
||||
// mainFieldMappings가 비어 있으면 건너뜀 (필수 컬럼 누락 방지)
|
||||
logger.info(`saveMainAsFirst 옵션 확인:`, {
|
||||
saveMainAsFirst: options?.saveMainAsFirst,
|
||||
mainFieldMappings: options?.mainFieldMappings,
|
||||
mainFieldMappingsLength: options?.mainFieldMappings?.length,
|
||||
linkColumn,
|
||||
mainDataKeys: Object.keys(mainData),
|
||||
});
|
||||
if (options?.saveMainAsFirst && options?.mainFieldMappings && options.mainFieldMappings.length > 0 && linkColumn?.subColumn) {
|
||||
const mainSubItem: Record<string, any> = {
|
||||
[linkColumn.subColumn]: savedPkValue,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -996,14 +996,6 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
|||
screenId: modalState.screenId, // 화면 ID 추가
|
||||
};
|
||||
|
||||
// 🔍 디버깅: enrichedFormData 확인
|
||||
console.log("🔑 [EditModal] enrichedFormData 생성:", {
|
||||
"screenData.screenInfo": screenData.screenInfo,
|
||||
"screenData.screenInfo?.tableName": screenData.screenInfo?.tableName,
|
||||
"enrichedFormData.tableName": enrichedFormData.tableName,
|
||||
"enrichedFormData.id": enrichedFormData.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<InteractiveScreenViewerDynamic
|
||||
key={component.id}
|
||||
|
|
|
|||
|
|
@ -675,7 +675,7 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
|||
|
||||
// 우측 패널 수정 버튼 클릭
|
||||
const handleEditItem = useCallback(
|
||||
(item: any) => {
|
||||
async (item: any) => {
|
||||
// 수정용 모달 화면 ID 결정 (editModalScreenId 우선, 없으면 addModalScreenId 사용)
|
||||
const modalScreenId = config.rightPanel?.editModalScreenId || config.rightPanel?.addModalScreenId;
|
||||
|
||||
|
|
@ -684,13 +684,42 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
|||
return;
|
||||
}
|
||||
|
||||
// 메인 테이블 데이터 조회 (우측 패널이 서브 테이블인 경우)
|
||||
let editData = { ...item };
|
||||
|
||||
// 연결 설정이 있고, 메인 테이블이 설정되어 있으면 메인 테이블 데이터도 조회
|
||||
if (config.rightPanel?.mainTableForEdit) {
|
||||
const { tableName, linkColumn } = config.rightPanel.mainTableForEdit;
|
||||
const linkValue = item[linkColumn?.subColumn || ""];
|
||||
|
||||
if (tableName && linkValue) {
|
||||
try {
|
||||
const response = await apiClient.get(`/table-management/tables/${tableName}/data`, {
|
||||
params: {
|
||||
filters: JSON.stringify({ [linkColumn?.mainColumn || linkColumn?.subColumn || ""]: linkValue }),
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data?.success && response.data?.data?.items?.[0]) {
|
||||
// 메인 테이블 데이터를 editData에 병합 (서브 테이블 데이터 우선)
|
||||
editData = { ...response.data.data.items[0], ...item };
|
||||
console.log("[SplitPanelLayout2] 메인 테이블 데이터 병합:", editData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[SplitPanelLayout2] 메인 테이블 데이터 조회 실패:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EditModal 열기 이벤트 발생 (수정 모드)
|
||||
const event = new CustomEvent("openEditModal", {
|
||||
detail: {
|
||||
screenId: modalScreenId,
|
||||
title: "수정",
|
||||
modalSize: "lg",
|
||||
editData: item, // 기존 데이터 전달
|
||||
editData: editData, // 병합된 데이터 전달
|
||||
isCreateMode: false, // 수정 모드
|
||||
onSave: () => {
|
||||
if (selectedLeftItem) {
|
||||
|
|
@ -700,9 +729,9 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
|||
},
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
console.log("[SplitPanelLayout2] 우측 수정 모달 열기:", item);
|
||||
console.log("[SplitPanelLayout2] 우측 수정 모달 열기:", editData);
|
||||
},
|
||||
[config.rightPanel?.editModalScreenId, config.rightPanel?.addModalScreenId, selectedLeftItem, loadRightData],
|
||||
[config.rightPanel?.editModalScreenId, config.rightPanel?.addModalScreenId, config.rightPanel?.mainTableForEdit, selectedLeftItem, loadRightData],
|
||||
);
|
||||
|
||||
// 좌측 패널 수정 버튼 클릭
|
||||
|
|
@ -791,11 +820,11 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
|||
toast.success(`${itemsToDelete.length}개 항목이 삭제되었습니다.`);
|
||||
setSelectedRightItems(new Set<string | number>());
|
||||
} else if (itemToDelete) {
|
||||
// 단일 삭제 - 해당 항목 데이터를 body로 전달
|
||||
// 단일 삭제 - 해당 항목 데이터를 배열로 감싸서 body로 전달 (백엔드가 배열을 기대함)
|
||||
console.log(`[SplitPanelLayout2] ${deleteTargetPanel === "left" ? "좌측" : "우측"} 단일 삭제:`, itemToDelete);
|
||||
|
||||
await apiClient.delete(`/table-management/tables/${tableName}/delete`, {
|
||||
data: itemToDelete,
|
||||
data: [itemToDelete],
|
||||
});
|
||||
toast.success("항목이 삭제되었습니다.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1343,6 +1343,65 @@ export const SplitPanelLayout2ConfigPanel: React.FC<SplitPanelLayout2ConfigPanel
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 수정 시 메인 테이블 조회 설정 */}
|
||||
{config.rightPanel?.showEditButton && (
|
||||
<div className="mt-3 border-t pt-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label className="text-xs font-medium">수정 시 메인 테이블 조회</Label>
|
||||
<Switch
|
||||
checked={!!config.rightPanel?.mainTableForEdit?.tableName}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
updateConfig("rightPanel.mainTableForEdit", {
|
||||
tableName: "",
|
||||
linkColumn: { mainColumn: "", subColumn: "" },
|
||||
});
|
||||
} else {
|
||||
updateConfig("rightPanel.mainTableForEdit", undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground mb-2 text-[10px]">
|
||||
우측 패널이 서브 테이블일 때, 수정 모달에 메인 테이블 데이터도 함께 전달
|
||||
</p>
|
||||
|
||||
{config.rightPanel?.mainTableForEdit && (
|
||||
<div className="space-y-2 rounded-lg border p-2 bg-muted/30">
|
||||
<div>
|
||||
<Label className="text-[10px]">메인 테이블</Label>
|
||||
<Input
|
||||
value={config.rightPanel.mainTableForEdit.tableName || ""}
|
||||
onChange={(e) => updateConfig("rightPanel.mainTableForEdit.tableName", e.target.value)}
|
||||
placeholder="예: user_info"
|
||||
className="h-7 text-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label className="text-[10px]">메인 테이블 연결 컬럼</Label>
|
||||
<Input
|
||||
value={config.rightPanel.mainTableForEdit.linkColumn?.mainColumn || ""}
|
||||
onChange={(e) => updateConfig("rightPanel.mainTableForEdit.linkColumn.mainColumn", e.target.value)}
|
||||
placeholder="예: user_id"
|
||||
className="h-7 text-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px]">서브 테이블 연결 컬럼</Label>
|
||||
<Input
|
||||
value={config.rightPanel.mainTableForEdit.linkColumn?.subColumn || ""}
|
||||
onChange={(e) => updateConfig("rightPanel.mainTableForEdit.linkColumn.subColumn", e.target.value)}
|
||||
placeholder="예: user_id"
|
||||
className="h-7 text-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -211,6 +211,20 @@ export interface RightPanelConfig {
|
|||
* - 결과: 부서별 사원 목록에 이메일, 전화번호 등 개인정보 함께 표시
|
||||
*/
|
||||
joinTables?: JoinTableConfig[];
|
||||
|
||||
/**
|
||||
* 수정 시 메인 테이블 데이터 조회 설정
|
||||
* 우측 패널이 서브 테이블(예: user_dept)이고, 수정 모달이 메인 테이블(예: user_info) 기준일 때
|
||||
* 수정 버튼 클릭 시 메인 테이블 데이터를 함께 조회하여 모달에 전달합니다.
|
||||
*/
|
||||
mainTableForEdit?: {
|
||||
tableName: string; // 메인 테이블명 (예: user_info)
|
||||
linkColumn: {
|
||||
mainColumn: string; // 메인 테이블의 연결 컬럼 (예: user_id)
|
||||
subColumn: string; // 서브 테이블의 연결 컬럼 (예: user_id)
|
||||
};
|
||||
};
|
||||
|
||||
// 탭 설정
|
||||
tabConfig?: TabConfig;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -212,15 +212,23 @@ export function UniversalFormModalComponent({
|
|||
// 마지막으로 초기화된 데이터의 ID를 추적 (수정 모달에서 다른 항목 선택 시 재초기화 필요)
|
||||
const lastInitializedId = useRef<string | undefined>(undefined);
|
||||
|
||||
// 초기화 - 최초 마운트 시 또는 initialData의 ID가 변경되었을 때 실행
|
||||
// 초기화 - 최초 마운트 시 또는 initialData가 변경되었을 때 실행
|
||||
useEffect(() => {
|
||||
// initialData에서 ID 값 추출 (id, ID, objid 등)
|
||||
const currentId = initialData?.id || initialData?.ID || initialData?.objid;
|
||||
const currentIdString = currentId !== undefined ? String(currentId) : undefined;
|
||||
|
||||
// 생성 모드에서 부모로부터 전달받은 데이터 해시 (ID가 없을 때만)
|
||||
const createModeDataHash = !currentIdString && initialData && Object.keys(initialData).length > 0
|
||||
? JSON.stringify(initialData)
|
||||
: undefined;
|
||||
|
||||
// 이미 초기화되었고, ID가 동일하면 스킵
|
||||
// 이미 초기화되었고, ID가 동일하고, 생성 모드 데이터도 동일하면 스킵
|
||||
if (hasInitialized.current && lastInitializedId.current === currentIdString) {
|
||||
return;
|
||||
// 생성 모드에서 데이터가 새로 전달된 경우는 재초기화 필요
|
||||
if (!createModeDataHash || capturedInitialData.current) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 수정 모드: initialData에 데이터가 있으면서 ID가 변경된 경우 재초기화
|
||||
|
|
@ -245,7 +253,7 @@ export function UniversalFormModalComponent({
|
|||
hasInitialized.current = true;
|
||||
initializeForm();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialData?.id, initialData?.ID, initialData?.objid]); // ID 값 변경 시 재초기화
|
||||
}, [initialData]); // initialData 전체 변경 시 재초기화
|
||||
|
||||
// config 변경 시에만 재초기화 (initialData 변경은 무시) - 채번규칙 제외
|
||||
useEffect(() => {
|
||||
|
|
@ -478,6 +486,82 @@ export function UniversalFormModalComponent({
|
|||
setActivatedOptionalFieldGroups(newActivatedGroups);
|
||||
setOriginalData(effectiveInitialData || {});
|
||||
|
||||
// 수정 모드에서 서브 테이블 데이터 로드 (겸직 등)
|
||||
const multiTable = config.saveConfig?.customApiSave?.multiTable;
|
||||
if (multiTable && effectiveInitialData) {
|
||||
const pkColumn = multiTable.mainTable?.primaryKeyColumn;
|
||||
const pkValue = effectiveInitialData[pkColumn];
|
||||
|
||||
// PK 값이 있으면 수정 모드로 판단
|
||||
if (pkValue) {
|
||||
console.log("[initializeForm] 수정 모드 - 서브 테이블 데이터 로드 시작");
|
||||
|
||||
for (const subTableConfig of multiTable.subTables || []) {
|
||||
// loadOnEdit 옵션이 활성화된 경우에만 로드
|
||||
if (!subTableConfig.enabled || !subTableConfig.options?.loadOnEdit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { tableName, linkColumn, repeatSectionId, fieldMappings, options } = subTableConfig;
|
||||
if (!tableName || !linkColumn?.subColumn || !repeatSectionId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 서브 테이블에서 데이터 조회
|
||||
const filters: Record<string, any> = {
|
||||
[linkColumn.subColumn]: pkValue,
|
||||
};
|
||||
|
||||
// 서브 항목만 로드 (메인 항목 제외)
|
||||
if (options?.loadOnlySubItems && options?.mainMarkerColumn) {
|
||||
filters[options.mainMarkerColumn] = options.subMarkerValue ?? false;
|
||||
}
|
||||
|
||||
console.log(`[initializeForm] 서브 테이블 ${tableName} 조회:`, filters);
|
||||
|
||||
const response = await apiClient.get(`/table-management/tables/${tableName}/data`, {
|
||||
params: {
|
||||
filters: JSON.stringify(filters),
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data?.success && response.data?.data?.items) {
|
||||
const subItems = response.data.data.items;
|
||||
console.log(`[initializeForm] 서브 테이블 ${tableName} 데이터 ${subItems.length}건 로드됨`);
|
||||
|
||||
// 역매핑: 서브 테이블 데이터 → 반복 섹션 데이터
|
||||
const repeatItems: RepeatSectionItem[] = subItems.map((item: any, index: number) => {
|
||||
const repeatItem: RepeatSectionItem = {
|
||||
_id: generateUniqueId("repeat"),
|
||||
_index: index,
|
||||
_originalData: item, // 원본 데이터 보관 (수정 시 필요)
|
||||
};
|
||||
|
||||
// 필드 매핑 역변환 (targetColumn → formField)
|
||||
for (const mapping of fieldMappings || []) {
|
||||
if (mapping.formField && mapping.targetColumn) {
|
||||
repeatItem[mapping.formField] = item[mapping.targetColumn];
|
||||
}
|
||||
}
|
||||
|
||||
return repeatItem;
|
||||
});
|
||||
|
||||
// 반복 섹션에 데이터 설정
|
||||
newRepeatSections[repeatSectionId] = repeatItems;
|
||||
setRepeatSections({ ...newRepeatSections });
|
||||
console.log(`[initializeForm] 반복 섹션 ${repeatSectionId}에 ${repeatItems.length}건 설정`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[initializeForm] 서브 테이블 ${tableName} 로드 실패:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 채번규칙 자동 생성
|
||||
console.log("[initializeForm] generateNumberingValues 호출");
|
||||
await generateNumberingValues(newFormData);
|
||||
|
|
@ -1142,6 +1226,20 @@ export function UniversalFormModalComponent({
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 1-0. receiveFromParent 필드 값도 mainData에 추가 (서브 테이블 저장용)
|
||||
// 이 필드들은 메인 테이블에는 저장되지 않지만, 서브 테이블 저장 시 필요할 수 있음
|
||||
config.sections.forEach((section) => {
|
||||
if (section.repeatable || section.type === "table") return;
|
||||
(section.fields || []).forEach((field) => {
|
||||
if (field.receiveFromParent && !mainData[field.columnName]) {
|
||||
const value = formData[field.columnName];
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
mainData[field.columnName] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 1-1. 채번규칙 처리 (저장 시점에 실제 순번 할당)
|
||||
for (const section of config.sections) {
|
||||
|
|
@ -1185,36 +1283,42 @@ export function UniversalFormModalComponent({
|
|||
}> = [];
|
||||
|
||||
for (const subTableConfig of multiTable.subTables || []) {
|
||||
if (!subTableConfig.enabled || !subTableConfig.tableName || !subTableConfig.repeatSectionId) {
|
||||
// 서브 테이블이 활성화되어 있고 테이블명이 있어야 함
|
||||
// repeatSectionId는 선택사항 (saveMainAsFirst만 사용하는 경우 없을 수 있음)
|
||||
if (!subTableConfig.enabled || !subTableConfig.tableName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const subItems: Record<string, any>[] = [];
|
||||
const repeatData = repeatSections[subTableConfig.repeatSectionId] || [];
|
||||
|
||||
// 반복 섹션이 있는 경우에만 반복 데이터 처리
|
||||
if (subTableConfig.repeatSectionId) {
|
||||
const repeatData = repeatSections[subTableConfig.repeatSectionId] || [];
|
||||
|
||||
// 반복 섹션 데이터를 필드 매핑에 따라 변환
|
||||
for (const item of repeatData) {
|
||||
const mappedItem: Record<string, any> = {};
|
||||
// 반복 섹션 데이터를 필드 매핑에 따라 변환
|
||||
for (const item of repeatData) {
|
||||
const mappedItem: Record<string, any> = {};
|
||||
|
||||
// 연결 컬럼 값 설정
|
||||
if (subTableConfig.linkColumn?.mainField && subTableConfig.linkColumn?.subColumn) {
|
||||
mappedItem[subTableConfig.linkColumn.subColumn] = mainData[subTableConfig.linkColumn.mainField];
|
||||
}
|
||||
|
||||
// 필드 매핑에 따라 데이터 변환
|
||||
for (const mapping of subTableConfig.fieldMappings || []) {
|
||||
if (mapping.formField && mapping.targetColumn) {
|
||||
mappedItem[mapping.targetColumn] = item[mapping.formField];
|
||||
// 연결 컬럼 값 설정
|
||||
if (subTableConfig.linkColumn?.mainField && subTableConfig.linkColumn?.subColumn) {
|
||||
mappedItem[subTableConfig.linkColumn.subColumn] = mainData[subTableConfig.linkColumn.mainField];
|
||||
}
|
||||
}
|
||||
|
||||
// 메인/서브 구분 컬럼 설정 (서브 데이터는 서브 마커 값)
|
||||
if (subTableConfig.options?.mainMarkerColumn) {
|
||||
mappedItem[subTableConfig.options.mainMarkerColumn] = subTableConfig.options?.subMarkerValue ?? false;
|
||||
}
|
||||
// 필드 매핑에 따라 데이터 변환
|
||||
for (const mapping of subTableConfig.fieldMappings || []) {
|
||||
if (mapping.formField && mapping.targetColumn) {
|
||||
mappedItem[mapping.targetColumn] = item[mapping.formField];
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(mappedItem).length > 0) {
|
||||
subItems.push(mappedItem);
|
||||
// 메인/서브 구분 컬럼 설정 (서브 데이터는 서브 마커 값)
|
||||
if (subTableConfig.options?.mainMarkerColumn) {
|
||||
mappedItem[subTableConfig.options.mainMarkerColumn] = subTableConfig.options?.subMarkerValue ?? false;
|
||||
}
|
||||
|
||||
if (Object.keys(mappedItem).length > 0) {
|
||||
subItems.push(mappedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1226,8 +1330,9 @@ export function UniversalFormModalComponent({
|
|||
// fieldMappings에 정의된 targetColumn만 매핑 (서브 테이블에 존재하는 컬럼만)
|
||||
for (const mapping of subTableConfig.fieldMappings || []) {
|
||||
if (mapping.targetColumn) {
|
||||
// 메인 데이터에서 동일한 컬럼명이 있으면 매핑
|
||||
if (mainData[mapping.targetColumn] !== undefined && mainData[mapping.targetColumn] !== null && mainData[mapping.targetColumn] !== "") {
|
||||
// formData에서 동일한 컬럼명이 있으면 매핑 (receiveFromParent 필드 포함)
|
||||
const formValue = formData[mapping.targetColumn];
|
||||
if (formValue !== undefined && formValue !== null && formValue !== "") {
|
||||
mainFieldMappings.push({
|
||||
formField: mapping.targetColumn,
|
||||
targetColumn: mapping.targetColumn,
|
||||
|
|
@ -1238,11 +1343,14 @@ export function UniversalFormModalComponent({
|
|||
config.sections.forEach((section) => {
|
||||
if (section.repeatable || section.type === "table") return;
|
||||
const matchingField = (section.fields || []).find((f) => f.columnName === mapping.targetColumn);
|
||||
if (matchingField && mainData[matchingField.columnName] !== undefined && mainData[matchingField.columnName] !== null && mainData[matchingField.columnName] !== "") {
|
||||
mainFieldMappings!.push({
|
||||
formField: matchingField.columnName,
|
||||
targetColumn: mapping.targetColumn,
|
||||
});
|
||||
if (matchingField) {
|
||||
const fieldValue = formData[matchingField.columnName];
|
||||
if (fieldValue !== undefined && fieldValue !== null && fieldValue !== "") {
|
||||
mainFieldMappings!.push({
|
||||
formField: matchingField.columnName,
|
||||
targetColumn: mapping.targetColumn,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1255,15 +1363,18 @@ export function UniversalFormModalComponent({
|
|||
);
|
||||
}
|
||||
|
||||
subTablesData.push({
|
||||
tableName: subTableConfig.tableName,
|
||||
linkColumn: subTableConfig.linkColumn,
|
||||
items: subItems,
|
||||
options: {
|
||||
...subTableConfig.options,
|
||||
mainFieldMappings, // 메인 데이터 매핑 추가
|
||||
},
|
||||
});
|
||||
// 서브 테이블 데이터 추가 (반복 데이터가 없어도 saveMainAsFirst가 있으면 추가)
|
||||
if (subItems.length > 0 || subTableConfig.options?.saveMainAsFirst) {
|
||||
subTablesData.push({
|
||||
tableName: subTableConfig.tableName,
|
||||
linkColumn: subTableConfig.linkColumn,
|
||||
items: subItems,
|
||||
options: {
|
||||
...subTableConfig.options,
|
||||
mainFieldMappings, // 메인 데이터 매핑 추가
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 범용 다중 테이블 저장 API 호출
|
||||
|
|
@ -1489,13 +1600,20 @@ export function UniversalFormModalComponent({
|
|||
|
||||
// 표시 텍스트 생성 함수
|
||||
const getDisplayText = (row: Record<string, unknown>): string => {
|
||||
const displayVal = row[lfg.displayColumn || ""] || "";
|
||||
const valueVal = row[valueColumn] || "";
|
||||
// 메인 표시 컬럼 (displayColumn)
|
||||
const mainDisplayVal = row[lfg.displayColumn || ""] || "";
|
||||
// 서브 표시 컬럼 (subDisplayColumn이 있으면 사용, 없으면 valueColumn 사용)
|
||||
const subDisplayVal = lfg.subDisplayColumn
|
||||
? (row[lfg.subDisplayColumn] || "")
|
||||
: (row[valueColumn] || "");
|
||||
|
||||
switch (lfg.displayFormat) {
|
||||
case "code_name":
|
||||
return `${valueVal} - ${displayVal}`;
|
||||
// 서브 - 메인 형식
|
||||
return `${subDisplayVal} - ${mainDisplayVal}`;
|
||||
case "name_code":
|
||||
return `${displayVal} (${valueVal})`;
|
||||
// 메인 (서브) 형식
|
||||
return `${mainDisplayVal} (${subDisplayVal})`;
|
||||
case "custom":
|
||||
// 커스텀 형식: {컬럼명}을 실제 값으로 치환
|
||||
if (lfg.customDisplayFormat) {
|
||||
|
|
@ -1511,10 +1629,10 @@ export function UniversalFormModalComponent({
|
|||
}
|
||||
return result;
|
||||
}
|
||||
return String(displayVal);
|
||||
return String(mainDisplayVal);
|
||||
case "name_only":
|
||||
default:
|
||||
return String(displayVal);
|
||||
return String(mainDisplayVal);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import {
|
|||
TablePreFilter,
|
||||
TableModalFilter,
|
||||
TableCalculationRule,
|
||||
ConditionalTableConfig,
|
||||
ConditionalTableOption,
|
||||
} from "./types";
|
||||
|
||||
// 기본 설정값
|
||||
|
|
@ -133,6 +135,33 @@ export const defaultTableSectionConfig: TableSectionConfig = {
|
|||
multiSelect: true,
|
||||
maxHeight: "400px",
|
||||
},
|
||||
conditionalTable: undefined,
|
||||
};
|
||||
|
||||
// 기본 조건부 테이블 설정
|
||||
export const defaultConditionalTableConfig: ConditionalTableConfig = {
|
||||
enabled: false,
|
||||
triggerType: "checkbox",
|
||||
conditionColumn: "",
|
||||
options: [],
|
||||
optionSource: {
|
||||
enabled: false,
|
||||
tableName: "",
|
||||
valueColumn: "",
|
||||
labelColumn: "",
|
||||
filterCondition: "",
|
||||
},
|
||||
sourceFilter: {
|
||||
enabled: false,
|
||||
filterColumn: "",
|
||||
},
|
||||
};
|
||||
|
||||
// 기본 조건부 테이블 옵션 설정
|
||||
export const defaultConditionalTableOptionConfig: ConditionalTableOption = {
|
||||
id: "",
|
||||
value: "",
|
||||
label: "",
|
||||
};
|
||||
|
||||
// 기본 테이블 컬럼 설정
|
||||
|
|
@ -300,3 +329,8 @@ export const generateColumnModeId = (): string => {
|
|||
export const generateFilterId = (): string => {
|
||||
return generateUniqueId("filter");
|
||||
};
|
||||
|
||||
// 유틸리티: 조건부 테이블 옵션 ID 생성
|
||||
export const generateConditionalOptionId = (): string => {
|
||||
return generateUniqueId("cond");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -98,6 +98,9 @@ export function FieldDetailSettingsModal({
|
|||
// Combobox 열림 상태
|
||||
const [sourceTableOpen, setSourceTableOpen] = useState(false);
|
||||
const [targetColumnOpenMap, setTargetColumnOpenMap] = useState<Record<number, boolean>>({});
|
||||
const [displayColumnOpen, setDisplayColumnOpen] = useState(false);
|
||||
const [subDisplayColumnOpen, setSubDisplayColumnOpen] = useState(false); // 서브 표시 컬럼 Popover 상태
|
||||
const [sourceColumnOpenMap, setSourceColumnOpenMap] = useState<Record<number, boolean>>({});
|
||||
|
||||
// open이 변경될 때마다 필드 데이터 동기화
|
||||
useEffect(() => {
|
||||
|
|
@ -105,6 +108,16 @@ export function FieldDetailSettingsModal({
|
|||
setLocalField(field);
|
||||
}
|
||||
}, [open, field]);
|
||||
|
||||
// 모달이 열릴 때 소스 테이블 컬럼 자동 로드
|
||||
useEffect(() => {
|
||||
if (open && field.linkedFieldGroup?.sourceTable) {
|
||||
// tableColumns에 해당 테이블 컬럼이 없으면 로드
|
||||
if (!tableColumns[field.linkedFieldGroup.sourceTable] || tableColumns[field.linkedFieldGroup.sourceTable].length === 0) {
|
||||
onLoadTableColumns(field.linkedFieldGroup.sourceTable);
|
||||
}
|
||||
}
|
||||
}, [open, field.linkedFieldGroup?.sourceTable, tableColumns, onLoadTableColumns]);
|
||||
|
||||
// 모든 카테고리 컬럼 목록 로드 (모달 열릴 때)
|
||||
useEffect(() => {
|
||||
|
|
@ -735,32 +748,108 @@ export function FieldDetailSettingsModal({
|
|||
<HelpText>값을 가져올 소스 테이블 (예: customer_mng)</HelpText>
|
||||
</div>
|
||||
|
||||
{/* 표시 형식 선택 */}
|
||||
<div>
|
||||
<Label className="text-[10px]">표시 컬럼</Label>
|
||||
<Label className="text-[10px]">표시 형식</Label>
|
||||
<Select
|
||||
value={localField.linkedFieldGroup?.displayFormat || "name_only"}
|
||||
onValueChange={(value) =>
|
||||
updateField({
|
||||
linkedFieldGroup: {
|
||||
...localField.linkedFieldGroup,
|
||||
displayFormat: value as "name_only" | "code_name" | "name_code",
|
||||
// name_only 선택 시 서브 컬럼 초기화
|
||||
...(value === "name_only" ? { subDisplayColumn: undefined } : {}),
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LINKED_FIELD_DISPLAY_FORMAT_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
<div className="flex flex-col">
|
||||
<span>{opt.label}</span>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{opt.value === "name_only" && "메인 컬럼만 표시"}
|
||||
{opt.value === "code_name" && "서브 - 메인 형식"}
|
||||
{opt.value === "name_code" && "메인 (서브) 형식"}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<HelpText>드롭다운에 표시할 형식을 선택합니다</HelpText>
|
||||
</div>
|
||||
|
||||
{/* 메인 표시 컬럼 */}
|
||||
<div>
|
||||
<Label className="text-[10px]">메인 표시 컬럼</Label>
|
||||
{sourceTableColumns.length > 0 ? (
|
||||
<Select
|
||||
value={localField.linkedFieldGroup?.displayColumn || ""}
|
||||
onValueChange={(value) =>
|
||||
updateField({
|
||||
linkedFieldGroup: {
|
||||
...localField.linkedFieldGroup,
|
||||
displayColumn: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs mt-1">
|
||||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sourceTableColumns.map((col) => (
|
||||
<SelectItem key={col.name} value={col.name}>
|
||||
{col.name}
|
||||
{col.label !== col.name && ` (${col.label})`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Popover open={displayColumnOpen} onOpenChange={setDisplayColumnOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={displayColumnOpen}
|
||||
className="h-7 w-full justify-between text-xs mt-1 font-normal"
|
||||
>
|
||||
{localField.linkedFieldGroup?.displayColumn
|
||||
? (() => {
|
||||
const selectedCol = sourceTableColumns.find(
|
||||
(c) => c.name === localField.linkedFieldGroup?.displayColumn
|
||||
);
|
||||
return selectedCol
|
||||
? `${selectedCol.name} (${selectedCol.label})`
|
||||
: localField.linkedFieldGroup?.displayColumn;
|
||||
})()
|
||||
: "컬럼 선택..."}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[280px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="py-2 text-xs text-center">
|
||||
컬럼을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{sourceTableColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={`${col.name} ${col.label}`}
|
||||
onSelect={() => {
|
||||
updateField({
|
||||
linkedFieldGroup: {
|
||||
...localField.linkedFieldGroup,
|
||||
displayColumn: col.name,
|
||||
},
|
||||
});
|
||||
setDisplayColumnOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
localField.linkedFieldGroup?.displayColumn === col.name
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<span className="font-medium">{col.name}</span>
|
||||
<span className="ml-1 text-muted-foreground">({col.label})</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Input
|
||||
value={localField.linkedFieldGroup?.displayColumn || ""}
|
||||
|
|
@ -772,39 +861,133 @@ export function FieldDetailSettingsModal({
|
|||
},
|
||||
})
|
||||
}
|
||||
placeholder="customer_name"
|
||||
placeholder="item_name"
|
||||
className="h-7 text-xs mt-1"
|
||||
/>
|
||||
)}
|
||||
<HelpText>드롭다운에 표시할 컬럼 (예: customer_name)</HelpText>
|
||||
<HelpText>드롭다운에 표시할 메인 컬럼 (예: item_name)</HelpText>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-[10px]">표시 형식</Label>
|
||||
<Select
|
||||
value={localField.linkedFieldGroup?.displayFormat || "name_only"}
|
||||
onValueChange={(value) =>
|
||||
updateField({
|
||||
linkedFieldGroup: {
|
||||
...localField.linkedFieldGroup,
|
||||
displayFormat: value as "name_only" | "code_name" | "name_code",
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LINKED_FIELD_DISPLAY_FORMAT_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<HelpText>드롭다운에 표시될 형식을 선택하세요</HelpText>
|
||||
</div>
|
||||
{/* 서브 표시 컬럼 - 표시 형식이 name_only가 아닌 경우에만 표시 */}
|
||||
{localField.linkedFieldGroup?.displayFormat &&
|
||||
localField.linkedFieldGroup.displayFormat !== "name_only" && (
|
||||
<div>
|
||||
<Label className="text-[10px]">서브 표시 컬럼</Label>
|
||||
{sourceTableColumns.length > 0 ? (
|
||||
<Popover open={subDisplayColumnOpen} onOpenChange={setSubDisplayColumnOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={subDisplayColumnOpen}
|
||||
className="h-7 w-full justify-between text-xs mt-1 font-normal"
|
||||
>
|
||||
{localField.linkedFieldGroup?.subDisplayColumn
|
||||
? (() => {
|
||||
const selectedCol = sourceTableColumns.find(
|
||||
(c) => c.name === localField.linkedFieldGroup?.subDisplayColumn
|
||||
);
|
||||
return selectedCol
|
||||
? `${selectedCol.name} (${selectedCol.label})`
|
||||
: localField.linkedFieldGroup?.subDisplayColumn;
|
||||
})()
|
||||
: "컬럼 선택..."}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[280px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="py-2 text-xs text-center">
|
||||
컬럼을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{sourceTableColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={`${col.name} ${col.label}`}
|
||||
onSelect={() => {
|
||||
updateField({
|
||||
linkedFieldGroup: {
|
||||
...localField.linkedFieldGroup,
|
||||
subDisplayColumn: col.name,
|
||||
},
|
||||
});
|
||||
setSubDisplayColumnOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
localField.linkedFieldGroup?.subDisplayColumn === col.name
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<span className="font-medium">{col.name}</span>
|
||||
<span className="ml-1 text-muted-foreground">({col.label})</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Input
|
||||
value={localField.linkedFieldGroup?.subDisplayColumn || ""}
|
||||
onChange={(e) =>
|
||||
updateField({
|
||||
linkedFieldGroup: {
|
||||
...localField.linkedFieldGroup,
|
||||
subDisplayColumn: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="item_code"
|
||||
className="h-7 text-xs mt-1"
|
||||
/>
|
||||
)}
|
||||
<HelpText>
|
||||
{localField.linkedFieldGroup?.displayFormat === "code_name"
|
||||
? "메인 앞에 표시될 서브 컬럼 (예: 서브 - 메인)"
|
||||
: "메인 뒤에 표시될 서브 컬럼 (예: 메인 (서브))"}
|
||||
</HelpText>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 미리보기 - 메인 컬럼이 선택된 경우에만 표시 */}
|
||||
{localField.linkedFieldGroup?.displayColumn && (
|
||||
<div className="p-3 bg-muted/50 rounded-lg border border-dashed">
|
||||
<p className="text-[10px] text-muted-foreground mb-2">미리보기:</p>
|
||||
{(() => {
|
||||
const mainCol = localField.linkedFieldGroup?.displayColumn || "";
|
||||
const subCol = localField.linkedFieldGroup?.subDisplayColumn || "";
|
||||
const mainLabel = sourceTableColumns.find(c => c.name === mainCol)?.label || mainCol;
|
||||
const subLabel = sourceTableColumns.find(c => c.name === subCol)?.label || subCol;
|
||||
const format = localField.linkedFieldGroup?.displayFormat || "name_only";
|
||||
|
||||
let preview = "";
|
||||
if (format === "name_only") {
|
||||
preview = mainLabel;
|
||||
} else if (format === "code_name" && subCol) {
|
||||
preview = `${subLabel} - ${mainLabel}`;
|
||||
} else if (format === "name_code" && subCol) {
|
||||
preview = `${mainLabel} (${subLabel})`;
|
||||
} else if (format !== "name_only" && !subCol) {
|
||||
preview = `${mainLabel} (서브 컬럼을 선택하세요)`;
|
||||
} else {
|
||||
preview = mainLabel;
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="text-sm font-medium">{preview}</p>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
|
|
@ -846,24 +1029,67 @@ export function FieldDetailSettingsModal({
|
|||
<div>
|
||||
<Label className="text-[9px]">소스 컬럼 (가져올 값)</Label>
|
||||
{sourceTableColumns.length > 0 ? (
|
||||
<Select
|
||||
value={mapping.sourceColumn || ""}
|
||||
onValueChange={(value) =>
|
||||
updateLinkedFieldMapping(index, { sourceColumn: value })
|
||||
<Popover
|
||||
open={sourceColumnOpenMap[index] || false}
|
||||
onOpenChange={(open) =>
|
||||
setSourceColumnOpenMap((prev) => ({ ...prev, [index]: open }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-[9px] mt-0.5">
|
||||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sourceTableColumns.map((col) => (
|
||||
<SelectItem key={col.name} value={col.name}>
|
||||
{col.name}
|
||||
{col.label !== col.name && ` (${col.label})`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={sourceColumnOpenMap[index] || false}
|
||||
className="h-6 w-full justify-between text-[9px] mt-0.5 font-normal"
|
||||
>
|
||||
{mapping.sourceColumn
|
||||
? (() => {
|
||||
const selectedCol = sourceTableColumns.find(
|
||||
(c) => c.name === mapping.sourceColumn
|
||||
);
|
||||
return selectedCol
|
||||
? `${selectedCol.name} (${selectedCol.label})`
|
||||
: mapping.sourceColumn;
|
||||
})()
|
||||
: "컬럼 선택..."}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[250px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼 검색..." className="h-7 text-[9px]" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="py-2 text-[9px] text-center">
|
||||
컬럼을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{sourceTableColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={`${col.name} ${col.label}`}
|
||||
onSelect={() => {
|
||||
updateLinkedFieldMapping(index, { sourceColumn: col.name });
|
||||
setSourceColumnOpenMap((prev) => ({ ...prev, [index]: false }));
|
||||
}}
|
||||
className="text-[9px]"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
mapping.sourceColumn === col.name
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<span className="font-medium">{col.name}</span>
|
||||
<span className="ml-1 text-muted-foreground">({col.label})</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Input
|
||||
value={mapping.sourceColumn || ""}
|
||||
|
|
|
|||
|
|
@ -57,13 +57,34 @@ export function SaveSettingsModal({
|
|||
const [mainTableSearchOpen, setMainTableSearchOpen] = useState(false);
|
||||
const [subTableSearchOpen, setSubTableSearchOpen] = useState<Record<number, boolean>>({});
|
||||
|
||||
// 컬럼 검색 Popover 상태
|
||||
const [mainKeyColumnSearchOpen, setMainKeyColumnSearchOpen] = useState(false);
|
||||
const [mainFieldSearchOpen, setMainFieldSearchOpen] = useState<Record<number, boolean>>({});
|
||||
const [subColumnSearchOpen, setSubColumnSearchOpen] = useState<Record<number, boolean>>({});
|
||||
const [subTableColumnSearchOpen, setSubTableColumnSearchOpen] = useState<Record<string, boolean>>({});
|
||||
const [markerColumnSearchOpen, setMarkerColumnSearchOpen] = useState<Record<number, boolean>>({});
|
||||
|
||||
// open이 변경될 때마다 데이터 동기화
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setLocalSaveConfig(saveConfig);
|
||||
setSaveMode(saveConfig.customApiSave?.enabled && saveConfig.customApiSave?.multiTable?.enabled ? "multi" : "single");
|
||||
|
||||
// 모달이 열릴 때 기존에 설정된 테이블들의 컬럼 정보 로드
|
||||
const mainTableName = saveConfig.customApiSave?.multiTable?.mainTable?.tableName;
|
||||
if (mainTableName && !tableColumns[mainTableName]) {
|
||||
onLoadTableColumns(mainTableName);
|
||||
}
|
||||
|
||||
// 서브 테이블들의 컬럼 정보도 로드
|
||||
const subTables = saveConfig.customApiSave?.multiTable?.subTables || [];
|
||||
subTables.forEach((subTable) => {
|
||||
if (subTable.tableName && !tableColumns[subTable.tableName]) {
|
||||
onLoadTableColumns(subTable.tableName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [open, saveConfig]);
|
||||
}, [open, saveConfig, tableColumns, onLoadTableColumns]);
|
||||
|
||||
// 저장 설정 업데이트 함수
|
||||
const updateSaveConfig = (updates: Partial<SaveConfig>) => {
|
||||
|
|
@ -558,35 +579,76 @@ export function SaveSettingsModal({
|
|||
<div>
|
||||
<Label className="text-[10px]">메인 테이블 키 컬럼</Label>
|
||||
{mainTableColumns.length > 0 ? (
|
||||
<Select
|
||||
value={localSaveConfig.customApiSave?.multiTable?.mainTable?.primaryKeyColumn || ""}
|
||||
onValueChange={(value) =>
|
||||
updateSaveConfig({
|
||||
customApiSave: {
|
||||
...localSaveConfig.customApiSave,
|
||||
multiTable: {
|
||||
...localSaveConfig.customApiSave?.multiTable,
|
||||
mainTable: {
|
||||
...localSaveConfig.customApiSave?.multiTable?.mainTable,
|
||||
primaryKeyColumn: value,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs mt-1">
|
||||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{mainTableColumns.map((col) => (
|
||||
<SelectItem key={col.name} value={col.name}>
|
||||
{col.name}
|
||||
{col.label !== col.name && ` (${col.label})`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Popover open={mainKeyColumnSearchOpen} onOpenChange={setMainKeyColumnSearchOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={mainKeyColumnSearchOpen}
|
||||
className="h-7 w-full justify-between text-xs mt-1 font-normal"
|
||||
>
|
||||
{localSaveConfig.customApiSave?.multiTable?.mainTable?.primaryKeyColumn ? (
|
||||
<>
|
||||
{localSaveConfig.customApiSave.multiTable.mainTable.primaryKeyColumn}
|
||||
{(() => {
|
||||
const col = mainTableColumns.find(c => c.name === localSaveConfig.customApiSave?.multiTable?.mainTable?.primaryKeyColumn);
|
||||
return col?.label && col.label !== col.name ? ` (${col.label})` : "";
|
||||
})()}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-muted-foreground">컬럼 선택</span>
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[280px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼명 또는 라벨로 검색..." className="text-xs h-8" />
|
||||
<CommandList className="max-h-[200px]">
|
||||
<CommandEmpty className="py-3 text-center text-xs">
|
||||
컬럼을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{mainTableColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={`${col.name} ${col.label || ""}`}
|
||||
onSelect={() => {
|
||||
updateSaveConfig({
|
||||
customApiSave: {
|
||||
...localSaveConfig.customApiSave,
|
||||
multiTable: {
|
||||
...localSaveConfig.customApiSave?.multiTable,
|
||||
mainTable: {
|
||||
...localSaveConfig.customApiSave?.multiTable?.mainTable,
|
||||
primaryKeyColumn: col.name,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
setMainKeyColumnSearchOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
localSaveConfig.customApiSave?.multiTable?.mainTable?.primaryKeyColumn === col.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{col.name}</span>
|
||||
{col.label && col.label !== col.name && (
|
||||
<span className="text-[10px] text-muted-foreground">{col.label}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Input
|
||||
value={localSaveConfig.customApiSave?.multiTable?.mainTable?.primaryKeyColumn || ""}
|
||||
|
|
@ -775,25 +837,70 @@ export function SaveSettingsModal({
|
|||
<div>
|
||||
<Label className="text-[9px]">메인 필드</Label>
|
||||
{mainTableColumns.length > 0 ? (
|
||||
<Select
|
||||
value={subTable.linkColumn?.mainField || ""}
|
||||
onValueChange={(value) =>
|
||||
updateSubTable(subIndex, {
|
||||
linkColumn: { ...subTable.linkColumn, mainField: value },
|
||||
})
|
||||
}
|
||||
<Popover
|
||||
open={mainFieldSearchOpen[subIndex] || false}
|
||||
onOpenChange={(open) => setMainFieldSearchOpen(prev => ({ ...prev, [subIndex]: open }))}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-[9px] mt-0.5">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{mainTableColumns.map((col) => (
|
||||
<SelectItem key={col.name} value={col.name}>
|
||||
{col.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={mainFieldSearchOpen[subIndex] || false}
|
||||
className="h-6 w-full justify-between text-[9px] mt-0.5 font-normal"
|
||||
>
|
||||
{subTable.linkColumn?.mainField ? (
|
||||
<>
|
||||
{subTable.linkColumn.mainField}
|
||||
{(() => {
|
||||
const col = mainTableColumns.find(c => c.name === subTable.linkColumn?.mainField);
|
||||
return col?.label && col.label !== col.name ? ` (${col.label})` : "";
|
||||
})()}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-muted-foreground">필드 선택</span>
|
||||
)}
|
||||
<ChevronsUpDown className="ml-1 h-2.5 w-2.5 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[220px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼명/라벨 검색..." className="text-xs h-7" />
|
||||
<CommandList className="max-h-[180px]">
|
||||
<CommandEmpty className="py-2 text-center text-[10px]">
|
||||
컬럼을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{mainTableColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={`${col.name} ${col.label || ""}`}
|
||||
onSelect={() => {
|
||||
updateSubTable(subIndex, {
|
||||
linkColumn: { ...subTable.linkColumn, mainField: col.name },
|
||||
});
|
||||
setMainFieldSearchOpen(prev => ({ ...prev, [subIndex]: false }));
|
||||
}}
|
||||
className="text-[10px]"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
subTable.linkColumn?.mainField === col.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{col.name}</span>
|
||||
{col.label && col.label !== col.name && (
|
||||
<span className="text-[9px] text-muted-foreground">{col.label}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Input
|
||||
value={subTable.linkColumn?.mainField || ""}
|
||||
|
|
@ -811,25 +918,70 @@ export function SaveSettingsModal({
|
|||
<div>
|
||||
<Label className="text-[9px]">서브 컬럼</Label>
|
||||
{subTableColumns.length > 0 ? (
|
||||
<Select
|
||||
value={subTable.linkColumn?.subColumn || ""}
|
||||
onValueChange={(value) =>
|
||||
updateSubTable(subIndex, {
|
||||
linkColumn: { ...subTable.linkColumn, subColumn: value },
|
||||
})
|
||||
}
|
||||
<Popover
|
||||
open={subColumnSearchOpen[subIndex] || false}
|
||||
onOpenChange={(open) => setSubColumnSearchOpen(prev => ({ ...prev, [subIndex]: open }))}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-[9px] mt-0.5">
|
||||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{subTableColumns.map((col) => (
|
||||
<SelectItem key={col.name} value={col.name}>
|
||||
{col.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={subColumnSearchOpen[subIndex] || false}
|
||||
className="h-6 w-full justify-between text-[9px] mt-0.5 font-normal"
|
||||
>
|
||||
{subTable.linkColumn?.subColumn ? (
|
||||
<>
|
||||
{subTable.linkColumn.subColumn}
|
||||
{(() => {
|
||||
const col = subTableColumns.find(c => c.name === subTable.linkColumn?.subColumn);
|
||||
return col?.label && col.label !== col.name ? ` (${col.label})` : "";
|
||||
})()}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-muted-foreground">컬럼 선택</span>
|
||||
)}
|
||||
<ChevronsUpDown className="ml-1 h-2.5 w-2.5 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[220px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼명/라벨 검색..." className="text-xs h-7" />
|
||||
<CommandList className="max-h-[180px]">
|
||||
<CommandEmpty className="py-2 text-center text-[10px]">
|
||||
컬럼을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{subTableColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={`${col.name} ${col.label || ""}`}
|
||||
onSelect={() => {
|
||||
updateSubTable(subIndex, {
|
||||
linkColumn: { ...subTable.linkColumn, subColumn: col.name },
|
||||
});
|
||||
setSubColumnSearchOpen(prev => ({ ...prev, [subIndex]: false }));
|
||||
}}
|
||||
className="text-[10px]"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
subTable.linkColumn?.subColumn === col.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{col.name}</span>
|
||||
{col.label && col.label !== col.name && (
|
||||
<span className="text-[9px] text-muted-foreground">{col.label}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Input
|
||||
value={subTable.linkColumn?.subColumn || ""}
|
||||
|
|
@ -911,23 +1063,68 @@ export function SaveSettingsModal({
|
|||
<div>
|
||||
<Label className="text-[8px]">서브 테이블 컬럼</Label>
|
||||
{subTableColumns.length > 0 ? (
|
||||
<Select
|
||||
value={mapping.targetColumn || ""}
|
||||
onValueChange={(value) =>
|
||||
updateFieldMapping(subIndex, mapIndex, { targetColumn: value })
|
||||
}
|
||||
<Popover
|
||||
open={subTableColumnSearchOpen[`${subIndex}-${mapIndex}`] || false}
|
||||
onOpenChange={(open) => setSubTableColumnSearchOpen(prev => ({ ...prev, [`${subIndex}-${mapIndex}`]: open }))}
|
||||
>
|
||||
<SelectTrigger className="h-5 text-[8px] mt-0.5">
|
||||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{subTableColumns.map((col) => (
|
||||
<SelectItem key={col.name} value={col.name}>
|
||||
{col.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={subTableColumnSearchOpen[`${subIndex}-${mapIndex}`] || false}
|
||||
className="h-5 w-full justify-between text-[8px] mt-0.5 font-normal"
|
||||
>
|
||||
{mapping.targetColumn ? (
|
||||
<>
|
||||
{mapping.targetColumn}
|
||||
{(() => {
|
||||
const col = subTableColumns.find(c => c.name === mapping.targetColumn);
|
||||
return col?.label && col.label !== col.name ? ` (${col.label})` : "";
|
||||
})()}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-muted-foreground">컬럼 선택</span>
|
||||
)}
|
||||
<ChevronsUpDown className="ml-1 h-2 w-2 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼명/라벨 검색..." className="text-xs h-7" />
|
||||
<CommandList className="max-h-[180px]">
|
||||
<CommandEmpty className="py-2 text-center text-[10px]">
|
||||
컬럼을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{subTableColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={`${col.name} ${col.label || ""}`}
|
||||
onSelect={() => {
|
||||
updateFieldMapping(subIndex, mapIndex, { targetColumn: col.name });
|
||||
setSubTableColumnSearchOpen(prev => ({ ...prev, [`${subIndex}-${mapIndex}`]: false }));
|
||||
}}
|
||||
className="text-[10px]"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
mapping.targetColumn === col.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{col.name}</span>
|
||||
{col.label && col.label !== col.name && (
|
||||
<span className="text-[9px] text-muted-foreground">{col.label}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Input
|
||||
value={mapping.targetColumn || ""}
|
||||
|
|
@ -946,6 +1143,289 @@ export function SaveSettingsModal({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 대표 데이터 구분 저장 옵션 */}
|
||||
<div className="space-y-2">
|
||||
{!subTable.options?.saveMainAsFirst ? (
|
||||
// 비활성화 상태: 추가 버튼 표시
|
||||
<div className="border-2 border-dashed border-muted rounded-lg p-3 hover:border-primary/50 hover:bg-muted/30 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[10px] font-medium text-muted-foreground">대표/일반 구분 저장</p>
|
||||
<p className="text-[9px] text-muted-foreground/70 mt-0.5">
|
||||
저장되는 데이터를 대표와 일반으로 구분합니다
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => updateSubTable(subIndex, {
|
||||
options: {
|
||||
...subTable.options,
|
||||
saveMainAsFirst: true,
|
||||
mainMarkerColumn: "",
|
||||
mainMarkerValue: true,
|
||||
subMarkerValue: false,
|
||||
}
|
||||
})}
|
||||
className="h-6 text-[9px] px-2 shrink-0"
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
추가
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// 활성화 상태: 설정 필드 표시
|
||||
<div className="border rounded-lg p-3 bg-amber-50/50 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[10px] font-medium">대표/일반 구분 저장</p>
|
||||
<p className="text-[9px] text-muted-foreground">
|
||||
저장되는 데이터를 대표와 일반으로 구분합니다
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => updateSubTable(subIndex, {
|
||||
options: {
|
||||
...subTable.options,
|
||||
saveMainAsFirst: false,
|
||||
mainMarkerColumn: undefined,
|
||||
mainMarkerValue: undefined,
|
||||
subMarkerValue: undefined,
|
||||
}
|
||||
})}
|
||||
className="h-6 text-[9px] px-2 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3 mr-1" />
|
||||
제거
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<Label className="text-[9px]">구분 컬럼</Label>
|
||||
{subTableColumns.length > 0 ? (
|
||||
<Popover
|
||||
open={markerColumnSearchOpen[subIndex] || false}
|
||||
onOpenChange={(open) => setMarkerColumnSearchOpen(prev => ({ ...prev, [subIndex]: open }))}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={markerColumnSearchOpen[subIndex] || false}
|
||||
className="h-6 w-full justify-between text-[9px] mt-0.5 font-normal"
|
||||
>
|
||||
{subTable.options?.mainMarkerColumn ? (
|
||||
<>
|
||||
{subTable.options.mainMarkerColumn}
|
||||
{(() => {
|
||||
const col = subTableColumns.find(c => c.name === subTable.options?.mainMarkerColumn);
|
||||
return col?.label && col.label !== col.name ? ` (${col.label})` : "";
|
||||
})()}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-muted-foreground">컬럼 선택</span>
|
||||
)}
|
||||
<ChevronsUpDown className="ml-1 h-2.5 w-2.5 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[220px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼명/라벨 검색..." className="text-xs h-7" />
|
||||
<CommandList className="max-h-[180px]">
|
||||
<CommandEmpty className="py-2 text-center text-[10px]">
|
||||
컬럼을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{subTableColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={`${col.name} ${col.label || ""}`}
|
||||
onSelect={() => {
|
||||
updateSubTable(subIndex, {
|
||||
options: {
|
||||
...subTable.options,
|
||||
mainMarkerColumn: col.name,
|
||||
}
|
||||
});
|
||||
setMarkerColumnSearchOpen(prev => ({ ...prev, [subIndex]: false }));
|
||||
}}
|
||||
className="text-[10px]"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
subTable.options?.mainMarkerColumn === col.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{col.name}</span>
|
||||
{col.label && col.label !== col.name && (
|
||||
<span className="text-[9px] text-muted-foreground">{col.label}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Input
|
||||
value={subTable.options?.mainMarkerColumn || ""}
|
||||
onChange={(e) => updateSubTable(subIndex, {
|
||||
options: {
|
||||
...subTable.options,
|
||||
mainMarkerColumn: e.target.value,
|
||||
}
|
||||
})}
|
||||
placeholder="is_primary"
|
||||
className="h-6 text-[9px] mt-0.5"
|
||||
/>
|
||||
)}
|
||||
<HelpText>대표/일반을 구분하는 컬럼</HelpText>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label className="text-[9px]">함께 저장 (대표)</Label>
|
||||
<Input
|
||||
value={String(subTable.options?.mainMarkerValue ?? "true")}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
// true/false 문자열은 boolean으로 변환
|
||||
let parsedValue: any = val;
|
||||
if (val === "true") parsedValue = true;
|
||||
else if (val === "false") parsedValue = false;
|
||||
else if (!isNaN(Number(val)) && val !== "") parsedValue = Number(val);
|
||||
|
||||
updateSubTable(subIndex, {
|
||||
options: {
|
||||
...subTable.options,
|
||||
mainMarkerValue: parsedValue,
|
||||
}
|
||||
});
|
||||
}}
|
||||
placeholder="true, Y, 1 등"
|
||||
className="h-6 text-[9px] mt-0.5"
|
||||
/>
|
||||
<HelpText>기본 정보와 함께 저장될 때 값</HelpText>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[9px]">액션 활성화시 저장 (일반)</Label>
|
||||
<Input
|
||||
value={String(subTable.options?.subMarkerValue ?? "false")}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
let parsedValue: any = val;
|
||||
if (val === "true") parsedValue = true;
|
||||
else if (val === "false") parsedValue = false;
|
||||
else if (!isNaN(Number(val)) && val !== "") parsedValue = Number(val);
|
||||
|
||||
updateSubTable(subIndex, {
|
||||
options: {
|
||||
...subTable.options,
|
||||
subMarkerValue: parsedValue,
|
||||
}
|
||||
});
|
||||
}}
|
||||
placeholder="false, N, 0 등"
|
||||
className="h-6 text-[9px] mt-0.5"
|
||||
/>
|
||||
<HelpText>겸직 추가 시 저장될 때 값</HelpText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 수정 시 데이터 로드 옵션 */}
|
||||
<div className="space-y-2">
|
||||
{!subTable.options?.loadOnEdit ? (
|
||||
// 비활성화 상태: 추가 버튼 표시
|
||||
<div className="border-2 border-dashed border-muted rounded-lg p-3 hover:border-primary/50 hover:bg-muted/30 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[10px] font-medium text-muted-foreground">수정 시 데이터 로드</p>
|
||||
<p className="text-[9px] text-muted-foreground/70 mt-0.5">
|
||||
수정 모드에서 서브 테이블 데이터를 불러옵니다
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => updateSubTable(subIndex, {
|
||||
options: {
|
||||
...subTable.options,
|
||||
loadOnEdit: true,
|
||||
loadOnlySubItems: true,
|
||||
}
|
||||
})}
|
||||
className="h-6 text-[9px] px-2 shrink-0"
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
추가
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// 활성화 상태: 설정 필드 표시
|
||||
<div className="border rounded-lg p-3 bg-blue-50/50 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[10px] font-medium">수정 시 데이터 로드</p>
|
||||
<p className="text-[9px] text-muted-foreground">
|
||||
수정 모드에서 서브 테이블 데이터를 불러옵니다
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => updateSubTable(subIndex, {
|
||||
options: {
|
||||
...subTable.options,
|
||||
loadOnEdit: false,
|
||||
loadOnlySubItems: undefined,
|
||||
}
|
||||
})}
|
||||
className="h-6 text-[9px] px-2 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3 mr-1" />
|
||||
제거
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id={`loadOnlySubItems-${subIndex}`}
|
||||
checked={subTable.options?.loadOnlySubItems ?? true}
|
||||
onCheckedChange={(checked) => updateSubTable(subIndex, {
|
||||
options: {
|
||||
...subTable.options,
|
||||
loadOnlySubItems: checked,
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Label htmlFor={`loadOnlySubItems-${subIndex}`} className="text-[9px]">
|
||||
일반 항목만 로드 (대표 항목 제외)
|
||||
</Label>
|
||||
</div>
|
||||
<HelpText>
|
||||
활성화하면 겸직 데이터만 불러오고, 비활성화하면 모든 데이터를 불러옵니다
|
||||
</HelpText>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
|
|
|||
|
|
@ -699,6 +699,357 @@ export function TableColumnSettingsModal({
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 동적 Select 옵션 (소스 테이블에서 로드) */}
|
||||
<Separator />
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">동적 옵션 (소스 테이블에서 로드)</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
소스 테이블에서 옵션을 동적으로 가져옵니다. 조건부 테이블 필터가 자동 적용됩니다.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={localColumn.dynamicSelectOptions?.enabled ?? false}
|
||||
onCheckedChange={(checked) => {
|
||||
updateColumn({
|
||||
dynamicSelectOptions: checked
|
||||
? {
|
||||
enabled: true,
|
||||
sourceField: "",
|
||||
distinct: true,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{localColumn.dynamicSelectOptions?.enabled && (
|
||||
<div className="space-y-3 pl-2 border-l-2 border-primary/20">
|
||||
{/* 소스 필드 */}
|
||||
<div>
|
||||
<Label className="text-xs">소스 컬럼</Label>
|
||||
<p className="text-[10px] text-muted-foreground mb-1">
|
||||
소스 테이블에서 옵션 값을 가져올 컬럼
|
||||
</p>
|
||||
{sourceTableColumns.length > 0 ? (
|
||||
<Select
|
||||
value={localColumn.dynamicSelectOptions.sourceField || ""}
|
||||
onValueChange={(value) => {
|
||||
updateColumn({
|
||||
dynamicSelectOptions: {
|
||||
...localColumn.dynamicSelectOptions!,
|
||||
sourceField: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sourceTableColumns.map((col) => (
|
||||
<SelectItem key={col.column_name} value={col.column_name}>
|
||||
{col.column_name} {col.comment && `(${col.comment})`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={localColumn.dynamicSelectOptions.sourceField || ""}
|
||||
onChange={(e) => {
|
||||
updateColumn({
|
||||
dynamicSelectOptions: {
|
||||
...localColumn.dynamicSelectOptions!,
|
||||
sourceField: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="inspection_item"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 라벨 필드 */}
|
||||
<div>
|
||||
<Label className="text-xs">라벨 컬럼 (선택)</Label>
|
||||
<p className="text-[10px] text-muted-foreground mb-1">
|
||||
표시할 라벨 컬럼 (없으면 소스 컬럼 값 사용)
|
||||
</p>
|
||||
{sourceTableColumns.length > 0 ? (
|
||||
<Select
|
||||
value={localColumn.dynamicSelectOptions.labelField || ""}
|
||||
onValueChange={(value) => {
|
||||
updateColumn({
|
||||
dynamicSelectOptions: {
|
||||
...localColumn.dynamicSelectOptions!,
|
||||
labelField: value || undefined,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="(소스 컬럼과 동일)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">없음 (소스 컬럼 사용)</SelectItem>
|
||||
{sourceTableColumns.map((col) => (
|
||||
<SelectItem key={col.column_name} value={col.column_name}>
|
||||
{col.column_name} {col.comment && `(${col.comment})`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={localColumn.dynamicSelectOptions.labelField || ""}
|
||||
onChange={(e) => {
|
||||
updateColumn({
|
||||
dynamicSelectOptions: {
|
||||
...localColumn.dynamicSelectOptions!,
|
||||
labelField: e.target.value || undefined,
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="(비워두면 소스 컬럼 사용)"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 행 선택 모드 */}
|
||||
<div className="space-y-2 pt-2 border-t">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={localColumn.dynamicSelectOptions.rowSelectionMode?.enabled ?? false}
|
||||
onCheckedChange={(checked) => {
|
||||
updateColumn({
|
||||
dynamicSelectOptions: {
|
||||
...localColumn.dynamicSelectOptions!,
|
||||
rowSelectionMode: checked
|
||||
? {
|
||||
enabled: true,
|
||||
autoFillMappings: [],
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="scale-75"
|
||||
/>
|
||||
<div>
|
||||
<Label className="text-xs">행 선택 모드</Label>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
이 컬럼 선택 시 같은 소스 행의 다른 컬럼들도 자동 채움
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{localColumn.dynamicSelectOptions.rowSelectionMode?.enabled && (
|
||||
<div className="space-y-3 pl-4">
|
||||
{/* 소스 ID 저장 설정 */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label className="text-[10px]">소스 ID 컬럼</Label>
|
||||
{sourceTableColumns.length > 0 ? (
|
||||
<Select
|
||||
value={localColumn.dynamicSelectOptions.rowSelectionMode.sourceIdColumn || ""}
|
||||
onValueChange={(value) => {
|
||||
updateColumn({
|
||||
dynamicSelectOptions: {
|
||||
...localColumn.dynamicSelectOptions!,
|
||||
rowSelectionMode: {
|
||||
...localColumn.dynamicSelectOptions!.rowSelectionMode!,
|
||||
sourceIdColumn: value || undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs mt-1">
|
||||
<SelectValue placeholder="id" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sourceTableColumns.map((col) => (
|
||||
<SelectItem key={col.column_name} value={col.column_name}>
|
||||
{col.column_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={localColumn.dynamicSelectOptions.rowSelectionMode.sourceIdColumn || ""}
|
||||
onChange={(e) => {
|
||||
updateColumn({
|
||||
dynamicSelectOptions: {
|
||||
...localColumn.dynamicSelectOptions!,
|
||||
rowSelectionMode: {
|
||||
...localColumn.dynamicSelectOptions!.rowSelectionMode!,
|
||||
sourceIdColumn: e.target.value || undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="id"
|
||||
className="h-7 text-xs mt-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px]">저장할 필드</Label>
|
||||
<Input
|
||||
value={localColumn.dynamicSelectOptions.rowSelectionMode.targetIdField || ""}
|
||||
onChange={(e) => {
|
||||
updateColumn({
|
||||
dynamicSelectOptions: {
|
||||
...localColumn.dynamicSelectOptions!,
|
||||
rowSelectionMode: {
|
||||
...localColumn.dynamicSelectOptions!.rowSelectionMode!,
|
||||
targetIdField: e.target.value || undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="inspection_standard_id"
|
||||
className="h-7 text-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 자동 채움 매핑 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label className="text-[10px]">자동 채움 매핑</Label>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const currentMappings = localColumn.dynamicSelectOptions?.rowSelectionMode?.autoFillMappings || [];
|
||||
updateColumn({
|
||||
dynamicSelectOptions: {
|
||||
...localColumn.dynamicSelectOptions!,
|
||||
rowSelectionMode: {
|
||||
...localColumn.dynamicSelectOptions!.rowSelectionMode!,
|
||||
autoFillMappings: [...currentMappings, { sourceColumn: "", targetField: "" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="h-6 text-[10px] px-2"
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
매핑 추가
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{(localColumn.dynamicSelectOptions.rowSelectionMode.autoFillMappings || []).map((mapping, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
{sourceTableColumns.length > 0 ? (
|
||||
<Select
|
||||
value={mapping.sourceColumn}
|
||||
onValueChange={(value) => {
|
||||
const newMappings = [...(localColumn.dynamicSelectOptions?.rowSelectionMode?.autoFillMappings || [])];
|
||||
newMappings[idx] = { ...newMappings[idx], sourceColumn: value };
|
||||
updateColumn({
|
||||
dynamicSelectOptions: {
|
||||
...localColumn.dynamicSelectOptions!,
|
||||
rowSelectionMode: {
|
||||
...localColumn.dynamicSelectOptions!.rowSelectionMode!,
|
||||
autoFillMappings: newMappings,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs flex-1">
|
||||
<SelectValue placeholder="소스 컬럼" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sourceTableColumns.map((col) => (
|
||||
<SelectItem key={col.column_name} value={col.column_name}>
|
||||
{col.column_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={mapping.sourceColumn}
|
||||
onChange={(e) => {
|
||||
const newMappings = [...(localColumn.dynamicSelectOptions?.rowSelectionMode?.autoFillMappings || [])];
|
||||
newMappings[idx] = { ...newMappings[idx], sourceColumn: e.target.value };
|
||||
updateColumn({
|
||||
dynamicSelectOptions: {
|
||||
...localColumn.dynamicSelectOptions!,
|
||||
rowSelectionMode: {
|
||||
...localColumn.dynamicSelectOptions!.rowSelectionMode!,
|
||||
autoFillMappings: newMappings,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="소스 컬럼"
|
||||
className="h-7 text-xs flex-1"
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">→</span>
|
||||
<Input
|
||||
value={mapping.targetField}
|
||||
onChange={(e) => {
|
||||
const newMappings = [...(localColumn.dynamicSelectOptions?.rowSelectionMode?.autoFillMappings || [])];
|
||||
newMappings[idx] = { ...newMappings[idx], targetField: e.target.value };
|
||||
updateColumn({
|
||||
dynamicSelectOptions: {
|
||||
...localColumn.dynamicSelectOptions!,
|
||||
rowSelectionMode: {
|
||||
...localColumn.dynamicSelectOptions!.rowSelectionMode!,
|
||||
autoFillMappings: newMappings,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="타겟 필드"
|
||||
className="h-7 text-xs flex-1"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
const newMappings = (localColumn.dynamicSelectOptions?.rowSelectionMode?.autoFillMappings || []).filter((_, i) => i !== idx);
|
||||
updateColumn({
|
||||
dynamicSelectOptions: {
|
||||
...localColumn.dynamicSelectOptions!,
|
||||
rowSelectionMode: {
|
||||
...localColumn.dynamicSelectOptions!.rowSelectionMode!,
|
||||
autoFillMappings: newMappings,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="h-7 w-7 p-0 text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{(localColumn.dynamicSelectOptions.rowSelectionMode.autoFillMappings || []).length === 0 && (
|
||||
<p className="text-[10px] text-muted-foreground text-center py-2 border border-dashed rounded">
|
||||
매핑을 추가하세요 (예: inspection_criteria → inspection_standard)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -80,7 +80,8 @@ export interface FormFieldConfig {
|
|||
linkedFieldGroup?: {
|
||||
enabled?: boolean; // 사용 여부
|
||||
sourceTable?: string; // 소스 테이블 (예: dept_info)
|
||||
displayColumn?: string; // 표시할 컬럼 (예: dept_name) - 드롭다운에 보여줄 텍스트
|
||||
displayColumn?: string; // 메인 표시 컬럼 (예: item_name) - 드롭다운에 보여줄 메인 텍스트
|
||||
subDisplayColumn?: string; // 서브 표시 컬럼 (예: item_number) - 메인과 함께 표시될 서브 텍스트
|
||||
displayFormat?: "name_only" | "code_name" | "name_code" | "custom"; // 표시 형식
|
||||
// 커스텀 표시 형식 (displayFormat이 "custom"일 때 사용)
|
||||
// 형식: {컬럼명} 으로 치환됨 (예: "{item_name} ({item_number})" → "철판 (ITEM-001)")
|
||||
|
|
@ -256,6 +257,11 @@ export interface TableSectionConfig {
|
|||
modalTitle?: string; // 모달 제목 (기본: "항목 검색 및 선택")
|
||||
multiSelect?: boolean; // 다중 선택 허용 (기본: true)
|
||||
maxHeight?: string; // 테이블 최대 높이 (기본: "400px")
|
||||
|
||||
// 추가 버튼 타입
|
||||
// - search: 검색 모달 열기 (기본값) - 기존 데이터에서 선택
|
||||
// - addRow: 빈 행 직접 추가 - 새 데이터 직접 입력
|
||||
addButtonType?: "search" | "addRow";
|
||||
};
|
||||
|
||||
// 7. 조건부 테이블 설정 (고급)
|
||||
|
|
@ -295,6 +301,13 @@ export interface ConditionalTableConfig {
|
|||
labelColumn: string; // 예: type_name
|
||||
filterCondition?: string; // 예: is_active = 'Y'
|
||||
};
|
||||
|
||||
// 소스 테이블 필터링 설정
|
||||
// 조건 선택 시 소스 테이블(검사기준 등)에서 해당 조건으로 필터링
|
||||
sourceFilter?: {
|
||||
enabled: boolean;
|
||||
filterColumn: string; // 소스 테이블에서 필터링할 컬럼 (예: inspection_type)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -373,6 +386,30 @@ export interface TableColumnConfig {
|
|||
// Select 옵션 (type이 "select"일 때)
|
||||
selectOptions?: { value: string; label: string }[];
|
||||
|
||||
// 동적 Select 옵션 (소스 테이블에서 옵션 로드)
|
||||
// 조건부 테이블의 sourceFilter가 활성화되어 있으면 자동으로 필터 적용
|
||||
dynamicSelectOptions?: {
|
||||
enabled: boolean;
|
||||
sourceField: string; // 소스 테이블에서 가져올 컬럼 (예: inspection_item)
|
||||
labelField?: string; // 표시 라벨 컬럼 (없으면 sourceField 사용)
|
||||
distinct?: boolean; // 중복 제거 (기본: true)
|
||||
|
||||
// 행 선택 모드: 이 컬럼 선택 시 같은 소스 행의 다른 컬럼들도 자동 채움
|
||||
// 활성화하면 이 컬럼이 "대표 컬럼"이 되어 선택 시 연관 컬럼들이 자동으로 채워짐
|
||||
rowSelectionMode?: {
|
||||
enabled: boolean;
|
||||
// 자동 채움할 컬럼 매핑 (소스 컬럼 → 타겟 필드)
|
||||
// 예: [{ sourceColumn: "inspection_criteria", targetField: "inspection_standard" }]
|
||||
autoFillColumns?: {
|
||||
sourceColumn: string; // 소스 테이블의 컬럼
|
||||
targetField: string; // 현재 테이블의 필드
|
||||
}[];
|
||||
// 소스 테이블의 ID 컬럼 (참조 ID 저장용)
|
||||
sourceIdColumn?: string; // 예: "id"
|
||||
targetIdField?: string; // 예: "inspection_standard_id"
|
||||
};
|
||||
};
|
||||
|
||||
// 값 매핑 (핵심 기능) - 고급 설정용
|
||||
valueMapping?: ValueMappingConfig;
|
||||
|
||||
|
|
@ -642,6 +679,10 @@ export interface SubTableSaveConfig {
|
|||
// 저장 전 기존 데이터 삭제
|
||||
deleteExistingBefore?: boolean;
|
||||
deleteOnlySubItems?: boolean; // 메인 항목은 유지하고 서브만 삭제
|
||||
|
||||
// 수정 모드에서 서브 테이블 데이터 로드
|
||||
loadOnEdit?: boolean; // 수정 시 서브 테이블 데이터 로드 여부
|
||||
loadOnlySubItems?: boolean; // 서브 항목만 로드 (메인 항목 제외)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue