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:
parent
d0ebb82f90
commit
f35ba75966
|
|
@ -1290,6 +1290,11 @@ export class DynamicFormService {
|
||||||
return res.rows;
|
return res.rows;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 삭제된 행이 없으면 레코드를 찾을 수 없는 것
|
||||||
|
if (!result || !Array.isArray(result) || result.length === 0) {
|
||||||
|
throw new Error(`테이블 ${tableName}에서 ID '${id}'에 해당하는 레코드를 찾을 수 없습니다.`);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("✅ 서비스: 실제 테이블 삭제 성공:", result);
|
console.log("✅ 서비스: 실제 테이블 삭제 성공:", result);
|
||||||
|
|
||||||
// 🔥 조건부 연결 실행 (DELETE 트리거)
|
// 🔥 조건부 연결 실행 (DELETE 트리거)
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,8 @@ export function TabsWidget({
|
||||||
const [screenLayouts, setScreenLayouts] = useState<Record<string, ComponentData[]>>({});
|
const [screenLayouts, setScreenLayouts] = useState<Record<string, ComponentData[]>>({});
|
||||||
const [screenLoadingStates, setScreenLoadingStates] = useState<Record<string, boolean>>({});
|
const [screenLoadingStates, setScreenLoadingStates] = useState<Record<string, boolean>>({});
|
||||||
const [screenErrors, setScreenErrors] = useState<Record<string, string>>({});
|
const [screenErrors, setScreenErrors] = useState<Record<string, string>>({});
|
||||||
|
// 탭별 화면 정보 (screenId, tableName) 저장
|
||||||
|
const [screenInfoMap, setScreenInfoMap] = useState<Record<string, { id: number; tableName?: string }>>({});
|
||||||
|
|
||||||
// 컴포넌트 탭 목록 변경 시 동기화
|
// 컴포넌트 탭 목록 변경 시 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -155,10 +157,21 @@ export function TabsWidget({
|
||||||
) {
|
) {
|
||||||
setScreenLoadingStates((prev) => ({ ...prev, [tab.id]: true }));
|
setScreenLoadingStates((prev) => ({ ...prev, [tab.id]: true }));
|
||||||
try {
|
try {
|
||||||
const layoutData = await screenApi.getLayout(extTab.screenId);
|
// 레이아웃과 화면 정보를 병렬로 로드
|
||||||
|
const [layoutData, screenDef] = await Promise.all([
|
||||||
|
screenApi.getLayout(extTab.screenId),
|
||||||
|
screenApi.getScreen(extTab.screenId),
|
||||||
|
]);
|
||||||
if (layoutData && layoutData.components) {
|
if (layoutData && layoutData.components) {
|
||||||
setScreenLayouts((prev) => ({ ...prev, [tab.id]: layoutData.components }));
|
setScreenLayouts((prev) => ({ ...prev, [tab.id]: layoutData.components }));
|
||||||
}
|
}
|
||||||
|
// 탭의 화면 정보 저장 (tableName 포함)
|
||||||
|
if (screenDef) {
|
||||||
|
setScreenInfoMap((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[tab.id]: { id: extTab.screenId!, tableName: screenDef.tableName },
|
||||||
|
}));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`탭 "${tab.label}" 화면 로드 실패:`, error);
|
console.error(`탭 "${tab.label}" 화면 로드 실패:`, error);
|
||||||
setScreenErrors((prev) => ({ ...prev, [tab.id]: "화면을 불러올 수 없습니다." }));
|
setScreenErrors((prev) => ({ ...prev, [tab.id]: "화면을 불러올 수 없습니다." }));
|
||||||
|
|
@ -172,6 +185,31 @@ export function TabsWidget({
|
||||||
loadScreenLayouts();
|
loadScreenLayouts();
|
||||||
}, [visibleTabs, screenLayouts, screenLoadingStates]);
|
}, [visibleTabs, screenLayouts, screenLoadingStates]);
|
||||||
|
|
||||||
|
// screenInfoMap이 없는 탭의 화면 정보 보충 로드
|
||||||
|
// screenId가 있지만 screenInfoMap에 아직 없는 탭의 화면 정보를 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadMissingScreenInfo = async () => {
|
||||||
|
for (const tab of visibleTabs) {
|
||||||
|
const extTab = tab as ExtendedTabItem;
|
||||||
|
// screenId가 있고 screenInfoMap에 아직 없는 경우 로드
|
||||||
|
if (extTab.screenId && !screenInfoMap[tab.id]) {
|
||||||
|
try {
|
||||||
|
const screenDef = await screenApi.getScreen(extTab.screenId);
|
||||||
|
if (screenDef) {
|
||||||
|
setScreenInfoMap((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[tab.id]: { id: extTab.screenId!, tableName: screenDef.tableName },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`탭 "${tab.label}" 화면 정보 로드 실패:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadMissingScreenInfo();
|
||||||
|
}, [visibleTabs, screenInfoMap]);
|
||||||
|
|
||||||
// 선택된 탭 변경 시 localStorage에 저장 + ActiveTab Context 업데이트
|
// 선택된 탭 변경 시 localStorage에 저장 + ActiveTab Context 업데이트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (persistSelection && typeof window !== "undefined") {
|
if (persistSelection && typeof window !== "undefined") {
|
||||||
|
|
@ -256,7 +294,7 @@ export function TabsWidget({
|
||||||
// 화면 레이아웃이 로드된 경우
|
// 화면 레이아웃이 로드된 경우
|
||||||
const loadedComponents = screenLayouts[tab.id];
|
const loadedComponents = screenLayouts[tab.id];
|
||||||
if (loadedComponents && loadedComponents.length > 0) {
|
if (loadedComponents && loadedComponents.length > 0) {
|
||||||
return renderScreenComponents(loadedComponents);
|
return renderScreenComponents(tab, loadedComponents);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 아직 로드되지 않은 경우
|
// 아직 로드되지 않은 경우
|
||||||
|
|
@ -283,7 +321,7 @@ export function TabsWidget({
|
||||||
};
|
};
|
||||||
|
|
||||||
// screenId로 로드한 화면 컴포넌트 렌더링
|
// screenId로 로드한 화면 컴포넌트 렌더링
|
||||||
const renderScreenComponents = (components: ComponentData[]) => {
|
const renderScreenComponents = (tab: ExtendedTabItem, components: ComponentData[]) => {
|
||||||
// InteractiveScreenViewerDynamic 동적 로드
|
// InteractiveScreenViewerDynamic 동적 로드
|
||||||
const InteractiveScreenViewerDynamic =
|
const InteractiveScreenViewerDynamic =
|
||||||
require("@/components/screen/InteractiveScreenViewerDynamic").InteractiveScreenViewerDynamic;
|
require("@/components/screen/InteractiveScreenViewerDynamic").InteractiveScreenViewerDynamic;
|
||||||
|
|
@ -316,7 +354,10 @@ export function TabsWidget({
|
||||||
allComponents={components}
|
allComponents={components}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onFormDataChange={onFormDataChange}
|
onFormDataChange={onFormDataChange}
|
||||||
|
screenInfo={screenInfoMap[tab.id]}
|
||||||
menuObjid={menuObjid}
|
menuObjid={menuObjid}
|
||||||
|
parentTabId={tab.id}
|
||||||
|
parentTabsComponentId={component.id}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -325,7 +366,7 @@ export function TabsWidget({
|
||||||
};
|
};
|
||||||
|
|
||||||
// 인라인 컴포넌트 렌더링 (v2 방식)
|
// 인라인 컴포넌트 렌더링 (v2 방식)
|
||||||
const renderInlineComponents = (tab: TabItem, components: TabInlineComponent[]) => {
|
const renderInlineComponents = (tab: ExtendedTabItem, components: TabInlineComponent[]) => {
|
||||||
// 컴포넌트들의 최대 위치를 계산하여 스크롤 가능한 영역 확보
|
// 컴포넌트들의 최대 위치를 계산하여 스크롤 가능한 영역 확보
|
||||||
const maxBottom = Math.max(
|
const maxBottom = Math.max(
|
||||||
...components.map((c) => (c.position?.y || 0) + (c.size?.height || 100)),
|
...components.map((c) => (c.position?.y || 0) + (c.size?.height || 100)),
|
||||||
|
|
@ -386,6 +427,15 @@ export function TabsWidget({
|
||||||
isInteractive={!isDesignMode}
|
isInteractive={!isDesignMode}
|
||||||
selectedRowsData={localSelectedRowsData}
|
selectedRowsData={localSelectedRowsData}
|
||||||
onSelectedRowsChange={handleSelectedRowsChange}
|
onSelectedRowsChange={handleSelectedRowsChange}
|
||||||
|
parentTabId={tab.id}
|
||||||
|
parentTabsComponentId={component.id}
|
||||||
|
// 탭에 screenId가 있으면 해당 화면의 tableName/screenId로 오버라이드
|
||||||
|
{...(screenInfoMap[tab.id]
|
||||||
|
? {
|
||||||
|
tableName: screenInfoMap[tab.id].tableName,
|
||||||
|
screenId: screenInfoMap[tab.id].id,
|
||||||
|
}
|
||||||
|
: {})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -154,15 +154,23 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
|
|
||||||
// 대상 패널의 첫 번째 테이블 자동 선택
|
// 대상 패널의 첫 번째 테이블 자동 선택
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!autoSelectFirstTable || tableList.length === 0) {
|
if (!autoSelectFirstTable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 탭 전환 감지: 활성 탭이 변경되었는지 확인
|
// 탭 전환 감지: 활성 탭이 변경되었는지 확인
|
||||||
const tabChanged = prevActiveTabIdsRef.current !== activeTabIdsStr;
|
const tabChanged = prevActiveTabIdsRef.current !== activeTabIdsStr;
|
||||||
if (tabChanged) {
|
if (tabChanged) {
|
||||||
|
// 탭이 변경되면 항상 ref를 갱신 (tableList가 비어 있어도)
|
||||||
|
// 이렇게 해야 비동기로 tableList가 나중에 채워질 때 중복 감지하지 않음
|
||||||
prevActiveTabIdsRef.current = activeTabIdsStr;
|
prevActiveTabIdsRef.current = activeTabIdsStr;
|
||||||
|
|
||||||
|
if (tableList.length === 0) {
|
||||||
|
// 테이블이 아직 등록되지 않은 상태 (비동기 로드 중)
|
||||||
|
// tableList가 나중에 채워지면 아래 폴백 로직에서 처리됨
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 탭 전환 시: 해당 탭에 속한 테이블 중 첫 번째 강제 선택
|
// 탭 전환 시: 해당 탭에 속한 테이블 중 첫 번째 강제 선택
|
||||||
const activeTabTable = tableList.find((t) => t.parentTabId && activeTabIds.includes(t.parentTabId));
|
const activeTabTable = tableList.find((t) => t.parentTabId && activeTabIds.includes(t.parentTabId));
|
||||||
const targetTable = activeTabTable || tableList[0];
|
const targetTable = activeTabTable || tableList[0];
|
||||||
|
|
@ -173,11 +181,21 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
return; // 탭 전환 시에는 여기서 종료
|
return; // 탭 전환 시에는 여기서 종료
|
||||||
}
|
}
|
||||||
|
|
||||||
// 현재 선택된 테이블이 대상 패널에 있는지 확인
|
// tableList가 비어있으면 아래 로직 스킵
|
||||||
const isCurrentTableInTarget = selectedTableId && tableList.some((t) => t.tableId === selectedTableId);
|
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 activeTabTable = tableList.find((t) => t.parentTabId && activeTabIds.includes(t.parentTabId));
|
||||||
const targetTable = activeTabTable || tableList[0];
|
const targetTable = activeTabTable || tableList[0];
|
||||||
|
|
||||||
|
|
@ -223,6 +241,102 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
}
|
}
|
||||||
}, [currentTableTabId, currentTable?.tableName]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!currentTable?.tableName) return;
|
if (!currentTable?.tableName) return;
|
||||||
|
|
@ -246,8 +360,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
try {
|
try {
|
||||||
const parsedValues = JSON.parse(savedValues);
|
const parsedValues = JSON.parse(savedValues);
|
||||||
setFilterValues(parsedValues);
|
setFilterValues(parsedValues);
|
||||||
// 즉시 필터 적용
|
// 다음 렌더 사이클에서 필터 적용 (activeFilters 업데이트 후)
|
||||||
setTimeout(() => applyFilters(parsedValues), 100);
|
pendingFilterApplyRef.current = { values: parsedValues, tableName: currentTable.tableName };
|
||||||
} catch {
|
} catch {
|
||||||
setFilterValues({});
|
setFilterValues({});
|
||||||
}
|
}
|
||||||
|
|
@ -297,8 +411,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
try {
|
try {
|
||||||
const parsedValues = JSON.parse(savedValues);
|
const parsedValues = JSON.parse(savedValues);
|
||||||
setFilterValues(parsedValues);
|
setFilterValues(parsedValues);
|
||||||
// 즉시 필터 적용
|
// 다음 렌더 사이클에서 필터 적용 (activeFilters 업데이트 후)
|
||||||
setTimeout(() => applyFilters(parsedValues), 100);
|
pendingFilterApplyRef.current = { values: parsedValues, tableName: currentTable.tableName };
|
||||||
} catch {
|
} catch {
|
||||||
setFilterValues({});
|
setFilterValues({});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue