fix(테이블관리): 블로커 5건 일괄 수정 (PR-A)
종합 감사 결과 발견된 블로커 5건:
1. AddColumnModal / CreateTableModal prop 미스매치 — 호출부 tableName/sourceTableName(camelCase)
vs 인터페이스 table_name/source_table_name(snake_case). 결과적으로 컬럼 추가 기능이 실제로
동작하지 않았음 (/api/ddl/tables/undefined/columns). 호출부를 인터페이스에 맞춰 통일.
2. IS_NULLABLE 강제 'Y' 덮어쓰기 (backend mapper) — upsertColumnSettings 의 VALUES 절이
모든 케이스에서 'Y' 리터럴을 박음. NN 토글 후 "컬럼 설정 저장" 누르면 필수 입력 제약이
조용히 사라짐. COALESCE(#{is_nullable}, 'Y') 로 변경 + ON CONFLICT 절에 IS_NULLABLE
COALESCE 추가. Service 도 settings 에서 is_nullable 을 읽어 'Y'/'N' 으로 정규화 후 전달.
3. 저장 안 한 편집 침묵 손실 — 우측 패널에서 편집 후 저장 안 누르고 다른 테이블로 이동 시
변경 사항 소실. hasUnsavedChanges memo 추가 (columns vs originalColumns JSON 비교).
handleTableSelect 에서 dirty 면 confirm 다이얼로그.
4. PK / NN / IDX / UQ 위치별 비대칭 저장 — 그리드 행 칩은 즉시 저장하는데 우측 상세 패널
토글은 메모리만 변경하고 저장 버튼 필요. 두 위치 모두 즉시 저장으로 통일 (상세 패널의
onColumnChange 가 is_nullable / is_unique 필드를 받으면 handleNullableToggle /
handleUniqueToggle 호출하도록).
5. SUPER_ADMIN role 검증 누락 (backend 보안) — DdlController 의 isSuperAdmin 이 company_code
== "*" 만 보고 role 클레임 무시. 토큰 변조 또는 "*" 회사 소속만으로 운영 DB DROP / ALTER
가능. company_code "*" AND role "SUPER_ADMIN" 둘 다 충족 시에만 통과로 변경. 11개 엔드포인트
에 @RequestAttribute("role") 추가.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -399,9 +399,28 @@ export default function TableManagementPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 저장 안 한 변경 사항이 있는지 — columns 와 originalColumns 의 reference 비교 (immutable update 패턴 의존)
|
||||
const hasUnsavedChanges = useMemo(() => {
|
||||
if (columns.length === 0 || originalColumns.length === 0) return false;
|
||||
if (columns.length !== originalColumns.length) return true;
|
||||
// 직렬화 비교 (얕은 ref 만으론 부족 — handleColumnChange 가 새 객체를 만들지만 다른 필드는 같은 ref 일 수 있어서)
|
||||
try {
|
||||
return JSON.stringify(columns) !== JSON.stringify(originalColumns);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, [columns, originalColumns]);
|
||||
|
||||
// 테이블 선택
|
||||
const handleTableSelect = useCallback(
|
||||
(tableName: string) => {
|
||||
if (tableName === selectedTable) return;
|
||||
if (hasUnsavedChanges) {
|
||||
const ok = typeof window !== "undefined"
|
||||
? window.confirm("저장하지 않은 컬럼 변경 사항이 있습니다. 이동하면 변경 내용이 사라집니다. 계속할까요?")
|
||||
: true;
|
||||
if (!ok) return;
|
||||
}
|
||||
setSelectedTable(tableName);
|
||||
setCurrentPage(1);
|
||||
setColumns([]);
|
||||
@@ -416,7 +435,7 @@ export default function TableManagementPage() {
|
||||
loadColumnTypes(tableName, 1, pageSize);
|
||||
loadConstraints(tableName);
|
||||
},
|
||||
[loadColumnTypes, loadConstraints, pageSize, tables],
|
||||
[hasUnsavedChanges, loadColumnTypes, loadConstraints, pageSize, selectedTable, tables],
|
||||
);
|
||||
|
||||
// 입력 타입 변경 - 이전 타입의 설정값 초기화 포함
|
||||
@@ -1808,6 +1827,21 @@ export default function TableManagementPage() {
|
||||
handleInputTypeChange(selectedColumn, value as string);
|
||||
return;
|
||||
}
|
||||
// 그리드 칩과 동일하게 is_nullable/is_unique 는 즉시 저장
|
||||
if (field === "is_nullable") {
|
||||
const currentColumn = columns.find((c) => c.column_name === selectedColumn);
|
||||
if (currentColumn) {
|
||||
handleNullableToggle(selectedColumn, currentColumn.is_nullable || "YES");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (field === "is_unique") {
|
||||
const currentColumn = columns.find((c) => c.column_name === selectedColumn);
|
||||
if (currentColumn) {
|
||||
handleUniqueToggle(selectedColumn, currentColumn.is_unique || "NO");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (field === "reference_table" && value) {
|
||||
loadReferenceTableColumns(value as string);
|
||||
}
|
||||
@@ -1857,13 +1891,13 @@ export default function TableManagementPage() {
|
||||
setDuplicateSourceTable(null);
|
||||
}}
|
||||
mode={duplicateModalMode}
|
||||
sourceTableName={duplicateSourceTable || undefined}
|
||||
source_table_name={duplicateSourceTable || undefined}
|
||||
/>
|
||||
|
||||
<AddColumnModal
|
||||
isOpen={addColumnModalOpen}
|
||||
onClose={() => setAddColumnModalOpen(false)}
|
||||
tableName={selectedTable || ""}
|
||||
table_name={selectedTable || ""}
|
||||
onSuccess={async (result) => {
|
||||
toast.success("컬럼이 성공적으로 추가되었습니다!");
|
||||
// 테이블 목록 새로고침 (컬럼 수 업데이트)
|
||||
|
||||
Reference in New Issue
Block a user