diff --git a/backend-spring/src/main/java/com/erp/controller/DdlController.java b/backend-spring/src/main/java/com/erp/controller/DdlController.java index 3ad8a194..e627ca48 100644 --- a/backend-spring/src/main/java/com/erp/controller/DdlController.java +++ b/backend-spring/src/main/java/com/erp/controller/DdlController.java @@ -29,10 +29,11 @@ public class DdlController { @PostMapping("/tables") public ResponseEntity> createTable( @RequestAttribute("company_code") String companyCode, + @RequestAttribute(value = "role", required = false) String role, @RequestAttribute("user_id") String userId, @RequestBody Map body) { - if (!isSuperAdmin(companyCode)) { + if (!isSuperAdmin(companyCode, role)) { return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); } @@ -65,10 +66,11 @@ public class DdlController { public ResponseEntity> addColumn( @PathVariable String tableName, @RequestAttribute("company_code") String companyCode, + @RequestAttribute(value = "role", required = false) String role, @RequestAttribute("user_id") String userId, @RequestBody Map body) { - if (!isSuperAdmin(companyCode)) { + if (!isSuperAdmin(companyCode, role)) { return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); } @@ -99,9 +101,10 @@ public class DdlController { @PathVariable String tableName, @PathVariable String columnName, @RequestAttribute("company_code") String companyCode, + @RequestAttribute(value = "role", required = false) String role, @RequestAttribute("user_id") String userId) { - if (!isSuperAdmin(companyCode)) { + if (!isSuperAdmin(companyCode, role)) { return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); } @@ -124,9 +127,10 @@ public class DdlController { public ResponseEntity> dropTable( @PathVariable String tableName, @RequestAttribute("company_code") String companyCode, + @RequestAttribute(value = "role", required = false) String role, @RequestAttribute("user_id") String userId) { - if (!isSuperAdmin(companyCode)) { + if (!isSuperAdmin(companyCode, role)) { return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); } @@ -147,9 +151,10 @@ public class DdlController { @PostMapping("/validate/table") public ResponseEntity> validateTableCreation( @RequestAttribute("company_code") String companyCode, + @RequestAttribute(value = "role", required = false) String role, @RequestBody Map body) { - if (!isSuperAdmin(companyCode)) { + if (!isSuperAdmin(companyCode, role)) { return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); } @@ -176,11 +181,12 @@ public class DdlController { @GetMapping("/logs") public ResponseEntity> getDdlLogs( @RequestAttribute("company_code") String companyCode, + @RequestAttribute(value = "role", required = false) String role, @RequestParam(required = false, defaultValue = "50") int limit, @RequestParam(required = false) String userId, @RequestParam(required = false) String ddlType) { - if (!isSuperAdmin(companyCode)) { + if (!isSuperAdmin(companyCode, role)) { return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); } @@ -195,10 +201,11 @@ public class DdlController { @GetMapping("/statistics") public ResponseEntity> getDdlStatistics( @RequestAttribute("company_code") String companyCode, + @RequestAttribute(value = "role", required = false) String role, @RequestParam(required = false) String fromDate, @RequestParam(required = false) String toDate) { - if (!isSuperAdmin(companyCode)) { + if (!isSuperAdmin(companyCode, role)) { return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); } @@ -212,9 +219,10 @@ public class DdlController { @GetMapping("/tables/{tableName}/history") public ResponseEntity> getTableDdlHistory( @PathVariable String tableName, - @RequestAttribute("company_code") String companyCode) { + @RequestAttribute("company_code") String companyCode, + @RequestAttribute(value = "role", required = false) String role) { - if (!isSuperAdmin(companyCode)) { + if (!isSuperAdmin(companyCode, role)) { return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); } @@ -230,9 +238,10 @@ public class DdlController { @GetMapping("/tables/{tableName}/info") public ResponseEntity> getTableInfo( @PathVariable String tableName, - @RequestAttribute("company_code") String companyCode) { + @RequestAttribute("company_code") String companyCode, + @RequestAttribute(value = "role", required = false) String role) { - if (!isSuperAdmin(companyCode)) { + if (!isSuperAdmin(companyCode, role)) { return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); } @@ -255,9 +264,10 @@ public class DdlController { @DeleteMapping("/logs/cleanup") public ResponseEntity> cleanupOldLogs( @RequestAttribute("company_code") String companyCode, + @RequestAttribute(value = "role", required = false) String role, @RequestParam(required = false, defaultValue = "90") int retentionDays) { - if (!isSuperAdmin(companyCode)) { + if (!isSuperAdmin(companyCode, role)) { return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); } @@ -272,9 +282,10 @@ public class DdlController { */ @GetMapping("/info") public ResponseEntity> getInfo( - @RequestAttribute("company_code") String companyCode) { + @RequestAttribute("company_code") String companyCode, + @RequestAttribute(value = "role", required = false) String role) { - if (!isSuperAdmin(companyCode)) { + if (!isSuperAdmin(companyCode, role)) { return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다.")); } @@ -318,7 +329,9 @@ public class DdlController { // 내부 유틸 // ───────────────────────────────────────────────────────────────────────── - private boolean isSuperAdmin(String companyCode) { - return "*".equals(companyCode); + private boolean isSuperAdmin(String companyCode, String role) { + // company_code 가 '*' 이고 role 이 SUPER_ADMIN 둘 다 충족해야 통과 (이중 체크). + // 토큰 변조 또는 회사코드만으로 super 권한이 발급되는 사고 방지. + return "*".equals(companyCode) && "SUPER_ADMIN".equals(role); } } diff --git a/backend-spring/src/main/java/com/erp/service/TableManagementService.java b/backend-spring/src/main/java/com/erp/service/TableManagementService.java index 22067b6e..99207fee 100644 --- a/backend-spring/src/main/java/com/erp/service/TableManagementService.java +++ b/backend-spring/src/main/java/com/erp/service/TableManagementService.java @@ -176,6 +176,21 @@ public class TableManagementService extends BaseService { params.put("display_column", "entity".equals(inputType) ? settings.get("display_column") : null); params.put("display_order", settings.getOrDefault("display_order", 0)); params.put("is_visible", settings.getOrDefault("is_visible", true)); + // is_nullable: 'Y'/'N' 또는 null. null 이면 mapper 의 COALESCE 로 기존 값 유지. + Object rawIsNullable = settings.get("is_nullable"); + if (rawIsNullable != null) { + String s = rawIsNullable.toString(); + // 프론트가 'YES'/'NO' 또는 'Y'/'N' 어느 쪽이든 보낼 수 있어 정규화 + if ("NO".equalsIgnoreCase(s) || "N".equalsIgnoreCase(s) || "FALSE".equalsIgnoreCase(s)) { + params.put("is_nullable", "N"); + } else if ("YES".equalsIgnoreCase(s) || "Y".equalsIgnoreCase(s) || "TRUE".equalsIgnoreCase(s)) { + params.put("is_nullable", "Y"); + } else { + params.put("is_nullable", null); + } + } else { + params.put("is_nullable", null); + } params.put("company_code", companyCode); params.put("category_ref", "category".equals(inputType) ? settings.get("category_ref") : null); sqlSession.update(NS + "upsertColumnSettings", params); diff --git a/backend-spring/src/main/resources/mapper/tableManagement.xml b/backend-spring/src/main/resources/mapper/tableManagement.xml index 4e576fa3..55fc8b9c 100644 --- a/backend-spring/src/main/resources/mapper/tableManagement.xml +++ b/backend-spring/src/main/resources/mapper/tableManagement.xml @@ -300,7 +300,7 @@ , #{display_column} , #{display_order} , #{is_visible} - , 'Y' + , COALESCE(#{is_nullable}, 'Y') , #{company_code} , #{category_ref} , NOW() @@ -318,6 +318,7 @@ , DISPLAY_COLUMN = EXCLUDED.DISPLAY_COLUMN , DISPLAY_ORDER = COALESCE(EXCLUDED.DISPLAY_ORDER, TABLE_TYPE_COLUMNS.DISPLAY_ORDER) , IS_VISIBLE = COALESCE(EXCLUDED.IS_VISIBLE, TABLE_TYPE_COLUMNS.IS_VISIBLE) + , IS_NULLABLE = COALESCE(EXCLUDED.IS_NULLABLE, TABLE_TYPE_COLUMNS.IS_NULLABLE) , CATEGORY_REF = EXCLUDED.CATEGORY_REF , UPDATED_DATE = NOW() diff --git a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx index c1612719..2d4e9b74 100644 --- a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx @@ -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} /> setAddColumnModalOpen(false)} - tableName={selectedTable || ""} + table_name={selectedTable || ""} onSuccess={async (result) => { toast.success("컬럼이 성공적으로 추가되었습니다!"); // 테이블 목록 새로고침 (컬럼 수 업데이트)