feat: Enhance dynamic form service and tabs widget functionality

- Added error handling in DynamicFormService to throw an error when a record is not found during deletion, improving robustness.
- Updated TabsWidget to load screen information in parallel with layout data, enhancing performance and user experience.
- Implemented logic to supplement missing screen information for tabs, ensuring all relevant data is available for rendering.
- Enhanced component rendering functions to pass additional screen information, improving data flow and interaction within the widget.
This commit is contained in:
kjs
2026-02-13 09:58:36 +09:00
parent d0ebb82f90
commit f35ba75966
3 changed files with 182 additions and 13 deletions

View File

@@ -154,15 +154,23 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
// 대상 패널의 첫 번째 테이블 자동 선택
useEffect(() => {
if (!autoSelectFirstTable || tableList.length === 0) {
if (!autoSelectFirstTable) {
return;
}
// 탭 전환 감지: 활성 탭이 변경되었는지 확인
const tabChanged = prevActiveTabIdsRef.current !== activeTabIdsStr;
if (tabChanged) {
// 탭이 변경되면 항상 ref를 갱신 (tableList가 비어 있어도)
// 이렇게 해야 비동기로 tableList가 나중에 채워질 때 중복 감지하지 않음
prevActiveTabIdsRef.current = activeTabIdsStr;
if (tableList.length === 0) {
// 테이블이 아직 등록되지 않은 상태 (비동기 로드 중)
// tableList가 나중에 채워지면 아래 폴백 로직에서 처리됨
return;
}
// 탭 전환 시: 해당 탭에 속한 테이블 중 첫 번째 강제 선택
const activeTabTable = tableList.find((t) => t.parentTabId && activeTabIds.includes(t.parentTabId));
const targetTable = activeTabTable || tableList[0];
@@ -173,11 +181,21 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
return; // 탭 전환 시에는 여기서 종료
}
// 현재 선택된 테이블이 대상 패널에 있는지 확인
const isCurrentTableInTarget = selectedTableId && tableList.some((t) => t.tableId === selectedTableId);
// tableList가 비어있으면 아래 로직 스킵
if (tableList.length === 0) {
return;
}
// 현재 선택된 테이블이 대상 패널에 없으면 첫 번째 테이블 선택
if (!selectedTableId || !isCurrentTableInTarget) {
// 현재 선택된 테이블이 활성 탭에 속하는지 확인
const isCurrentTableInActiveTab = selectedTableId && tableList.some((t) => {
if (t.tableId !== selectedTableId) return false;
// parentTabId가 있는 테이블이면 활성 탭에 속하는지 확인
if (t.parentTabId) return activeTabIds.includes(t.parentTabId);
return true; // parentTabId 없는 전역 테이블은 항상 유효
});
// 현재 선택된 테이블이 활성 탭에 없거나 미선택이면 첫 번째 테이블 선택
if (!selectedTableId || !isCurrentTableInActiveTab) {
const activeTabTable = tableList.find((t) => t.parentTabId && activeTabIds.includes(t.parentTabId));
const targetTable = activeTabTable || tableList[0];
@@ -223,6 +241,102 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
}
}, [currentTableTabId, currentTable?.tableName]);
// 탭 전환 플래그 (탭 복귀 시 필터 재적용을 위해)
const needsFilterReapplyRef = useRef(false);
const prevActiveTabIdsForReapplyRef = useRef<string>(activeTabIdsStr);
// 탭 전환 감지: 플래그만 설정 (실제 적용은 currentTable이 준비된 후)
useEffect(() => {
if (prevActiveTabIdsForReapplyRef.current !== activeTabIdsStr) {
prevActiveTabIdsForReapplyRef.current = activeTabIdsStr;
needsFilterReapplyRef.current = true;
}
}, [activeTabIdsStr]);
// 탭 복귀 시 기존 필터값 재적용
// currentTable이 준비되고 필터값이 있을 때 실행
useEffect(() => {
if (!needsFilterReapplyRef.current) return;
if (!currentTable?.onFilterChange) return;
// 플래그 즉시 해제 (중복 실행 방지)
needsFilterReapplyRef.current = false;
// activeFilters와 filterValues가 있으면 직접 onFilterChange 호출
// applyFilters 클로저 의존성을 피하고 직접 계산
if (activeFilters.length === 0) return;
const hasValues = Object.values(filterValues).some(
(v) => v !== "" && v !== undefined && v !== null,
);
if (!hasValues) return;
const filtersWithValues = activeFilters
.map((filter) => {
let filterValue = filterValues[filter.columnName];
// 날짜 범위 객체 처리
if (
filter.filterType === "date" &&
filterValue &&
typeof filterValue === "object" &&
(filterValue.from || filterValue.to)
) {
const formatDate = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
const fromStr = filterValue.from ? formatDate(filterValue.from) : "";
const toStr = filterValue.to ? formatDate(filterValue.to) : "";
if (fromStr && toStr) filterValue = `${fromStr}|${toStr}`;
else if (fromStr) filterValue = `${fromStr}|`;
else if (toStr) filterValue = `|${toStr}`;
else filterValue = "";
}
// 배열 처리
if (Array.isArray(filterValue)) {
filterValue = filterValue.join("|");
}
let operator = "contains";
if (filter.filterType === "select") operator = "equals";
else if (filter.filterType === "number") operator = "equals";
return {
...filter,
value: filterValue || "",
operator,
};
})
.filter((f) => {
if (!f.value) return false;
if (typeof f.value === "string" && f.value === "") return false;
return true;
});
// 직접 onFilterChange 호출 (applyFilters 클로저 우회)
currentTable.onFilterChange(filtersWithValues);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentTable?.onFilterChange, currentTable?.tableName, activeFilters, filterValues]);
// 필터 적용을 다음 렌더 사이클로 지연 (activeFilters 업데이트 후 적용 보장)
const pendingFilterApplyRef = useRef<{ values: Record<string, any>; tableName: string } | null>(null);
useEffect(() => {
if (pendingFilterApplyRef.current) {
const { values, tableName } = pendingFilterApplyRef.current;
// 현재 테이블이 요청된 테이블과 일치하는지 확인 (탭이 빠르게 전환된 경우 방지)
if (currentTable?.tableName === tableName) {
applyFilters(values);
}
pendingFilterApplyRef.current = null;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeFilters, currentTable?.tableName]);
// 현재 테이블의 저장된 필터 불러오기 (동적 모드) 또는 고정 필터 적용 (고정 모드)
useEffect(() => {
if (!currentTable?.tableName) return;
@@ -246,8 +360,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
try {
const parsedValues = JSON.parse(savedValues);
setFilterValues(parsedValues);
// 즉시 필터 적용
setTimeout(() => applyFilters(parsedValues), 100);
// 다음 렌더 사이클에서 필터 적용 (activeFilters 업데이트 후)
pendingFilterApplyRef.current = { values: parsedValues, tableName: currentTable.tableName };
} catch {
setFilterValues({});
}
@@ -297,8 +411,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
try {
const parsedValues = JSON.parse(savedValues);
setFilterValues(parsedValues);
// 즉시 필터 적용
setTimeout(() => applyFilters(parsedValues), 100);
// 다음 렌더 사이클에서 필터 적용 (activeFilters 업데이트 후)
pendingFilterApplyRef.current = { values: parsedValues, tableName: currentTable.tableName };
} catch {
setFilterValues({});
}