diff --git a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx index 2d4e9b74..3087601a 100644 --- a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx @@ -135,6 +135,8 @@ export default function TableManagementPage() { indexes: Array<{ name: string; columns: string[]; is_unique: boolean }>; }>({ primaryKey: { name: "", columns: [] }, indexes: [] }); const [pkDialogOpen, setPkDialogOpen] = useState(false); + // 이번 세션 동안 PK 변경 확인 다이얼로그 건너뛰기 (composite PK 만들 때 매번 다이얼로그 뜨는 답답함 해소) + const [pkSkipConfirmSession, setPkSkipConfirmSession] = useState(false); const [pendingPkColumns, setPendingPkColumns] = useState([]); // 선택된 테이블 목록 (체크박스) @@ -399,6 +401,16 @@ export default function TableManagementPage() { } }, []); + // ESC 키로 우측 상세 패널 닫기 (좁은 화면에서 stuck 방지) + useEffect(() => { + if (!selectedColumn) return; + const handler = (e: KeyboardEvent) => { + if (e.key === "Escape") setSelectedColumn(null); + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [selectedColumn]); + // 저장 안 한 변경 사항이 있는지 — columns 와 originalColumns 의 reference 비교 (immutable update 패턴 의존) const hasUnsavedChanges = useMemo(() => { if (columns.length === 0 || originalColumns.length === 0) return false; @@ -441,6 +453,11 @@ export default function TableManagementPage() { // 입력 타입 변경 - 이전 타입의 설정값 초기화 포함 const handleInputTypeChange = useCallback( (columnName: string, newInputType: string) => { + // typeFilter 가 활성화된 상태에서 변경된 input_type 이 필터와 불일치하면 자동으로 필터 해제 + // (그렇지 않으면 사용자가 방금 편집한 행이 그리드에서 갑자기 사라져 혼란) + if (typeFilter && typeFilter !== newInputType) { + setTypeFilter(null); + } setColumns((prev) => prev.map((col) => { if (col.column_name === columnName) { @@ -713,30 +730,22 @@ export default function TableManagementPage() { count: column.category_menus.length, }); - let successCount = 0; - let failCount = 0; - - for (const menuObjid of column.category_menus) { - try { - const mappingResponse = await createColumnMapping({ + // 직렬 await 대신 Promise.allSettled 로 병렬 호출 (메뉴가 많으면 직렬은 수십 초 멈춤) + const mappingResults = await Promise.allSettled( + column.category_menus.map((menuObjid) => + createColumnMapping({ tableName: selectedTable, logicalColumnName: column.column_name, physicalColumnName: column.column_name, menuObjid, description: `${column.display_name} (메뉴별 카테고리)`, - }); - - if (mappingResponse.success) { - successCount++; - } else { - console.error("❌ 매핑 생성 실패:", mappingResponse); - failCount++; - } - } catch (error) { - console.error(`❌ 메뉴 ${menuObjid}에 대한 매핑 생성 실패:`, error); - failCount++; - } - } + }), + ), + ); + const successCount = mappingResults.filter( + (r) => r.status === "fulfilled" && r.value.success, + ).length; + const failCount = mappingResults.length - successCount; if (successCount > 0 && failCount === 0) { toast.success(`컬럼 설정 및 ${successCount}개 메뉴 매핑이 저장되었습니다.`); @@ -755,10 +764,8 @@ export default function TableManagementPage() { // 원본 데이터 업데이트 setOriginalColumns((prev) => prev.map((col) => (col.column_name === column.column_name ? column : col))); - // 저장 후 데이터 확인을 위해 다시 로드 - setTimeout(() => { - loadColumnTypes(selectedTable); - }, 1000); + // 저장 후 데이터 확인을 위해 다시 로드 (await 로 즉시 reload — race + 깜빡임 회피) + await loadColumnTypes(selectedTable); } else { showErrorToast("컬럼 설정 저장에 실패했습니다", response.data.message, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요.", @@ -910,37 +917,24 @@ export default function TableManagementPage() { console.error("❌ 기존 매핑 삭제 실패:", error); } - // 2. 새로운 매핑 추가 (선택된 메뉴가 있는 경우만) + // 2. 새로운 매핑 추가 (선택된 메뉴가 있는 경우만) — 직렬 await 대신 Promise.allSettled 병렬 호출 if (column.category_menus && column.category_menus.length > 0) { - for (const menuObjid of column.category_menus) { - try { - console.log("🔄 매핑 API 호출:", { - tableName: selectedTable, - columnName: column.column_name, - menuObjid, - }); - - const mappingResponse = await createColumnMapping({ + const mappingResults = await Promise.allSettled( + column.category_menus.map((menuObjid) => + createColumnMapping({ tableName: selectedTable, logicalColumnName: column.column_name, physicalColumnName: column.column_name, menuObjid, description: `${column.display_name} (메뉴별 카테고리)`, - }); - - console.log("✅ 매핑 API 응답:", mappingResponse); - - if (mappingResponse.success) { - totalSuccessCount++; - } else { - console.error("❌ 매핑 생성 실패:", mappingResponse); - totalFailCount++; - } - } catch (error) { - console.error(`❌ 메뉴 ${menuObjid}에 대한 매핑 생성 실패:`, error); - totalFailCount++; - } - } + }), + ), + ); + const colSuccess = mappingResults.filter( + (r) => r.status === "fulfilled" && r.value.success, + ).length; + totalSuccessCount += colSuccess; + totalFailCount += mappingResults.length - colSuccess; } } @@ -963,10 +957,8 @@ export default function TableManagementPage() { // 테이블 목록 새로고침 (라벨 변경 반영) loadTables(); - // 저장 후 데이터 다시 로드 - setTimeout(() => { - loadColumnTypes(selectedTable, 1, pageSize); - }, 1000); + // 저장 후 데이터 다시 로드 (await 로 즉시 reload — race + 깜빡임 회피) + await loadColumnTypes(selectedTable, 1, pageSize); } else { showErrorToast("설정 저장에 실패했습니다", response.data.message, { guidance: "잠시 후 다시 시도해 주세요.", @@ -1077,24 +1069,28 @@ export default function TableManagementPage() { } else { newPkCols = currentPkCols.filter((c) => c !== columnName); } + // 이번 세션 동안 묻지 않기로 한 경우 즉시 적용 + if (pkSkipConfirmSession) { + applyPkChange(newPkCols); + return; + } // PK 변경은 확인 다이얼로그 표시 setPendingPkColumns(newPkCols); setPkDialogOpen(true); }, - [constraints.primaryKey?.columns], + [constraints.primaryKey?.columns, pkSkipConfirmSession], ); - // PK 변경 확인 - const handlePkConfirm = async () => { + // PK 변경 실제 적용 (다이얼로그 거치지 않거나 거친 후 호출) + const applyPkChange = async (newPkCols: string[]) => { if (!selectedTable) return; try { - if (pendingPkColumns.length === 0) { + if (newPkCols.length === 0) { toast.error("PK 컬럼을 최소 1개 이상 선택해야 합니다."); - setPkDialogOpen(false); return; } const response = await apiClient.put(`/table-management/tables/${selectedTable}/primary-key`, { - columns: pendingPkColumns, + columns: newPkCols, }); if (response.data.success) { toast.success(response.data.message); @@ -1106,11 +1102,15 @@ export default function TableManagementPage() { showErrorToast("PK 설정에 실패했습니다", error, { guidance: "컬럼 정보를 확인하고 다시 시도해 주세요.", }); - } finally { - setPkDialogOpen(false); } }; + // PK 변경 확인 (다이얼로그에서 호출) + const handlePkConfirm = async () => { + setPkDialogOpen(false); + await applyPkChange(pendingPkColumns); + }; + // 인덱스 토글 핸들러 (일반 인덱스만 DB 레벨 - 유니크는 앱 레벨 소프트 제약조건으로 분리됨) const handleIndexToggle = useCallback( async (columnName: string, indexType: "index", checked: boolean) => { @@ -2089,6 +2089,14 @@ export default function TableManagementPage() {

PK가 모두 제거됩니다

)} + + diff --git a/frontend/components/admin/table-type/ColumnDetailPanel.tsx b/frontend/components/admin/table-type/ColumnDetailPanel.tsx index c03e960a..b347ada7 100644 --- a/frontend/components/admin/table-type/ColumnDetailPanel.tsx +++ b/frontend/components/admin/table-type/ColumnDetailPanel.tsx @@ -116,14 +116,23 @@ export function ColumnDetailPanel({ // 컬럼 선택 안 한 상태에서도 패널이 항상 보이는 와이드 레이아웃 대응 — 빈 상태 안내 UI 표시. if (!column) { return ( -
-
- +
+ {/* 좁은 화면에서 패널이 슬라이드 in 된 상태로 column=null 이 되면 닫을 수단이 없어 + stuck 되는 문제 방지 — 빈 상태에도 X 버튼 유지 */} +
+ +
+
+
+ +
+

컬럼을 선택해주세요

+

+ 좌측 그리드에서 컬럼을 선택하면 여기에 상세 설정이 표시됩니다. +

-

컬럼을 선택해주세요

-

- 좌측 그리드에서 컬럼을 선택하면 여기에 상세 설정이 표시됩니다. -

); }