fix(테이블관리): 블로커 5건 (PR-A) (#28)
Build & Deploy to K8s / build-and-deploy (push) Successful in 8m26s

PR-A: 블로커 5건 일괄 수정
This commit was merged in pull request #28.
This commit is contained in:
2026-05-22 05:44:32 +00:00
4 changed files with 83 additions and 20 deletions
@@ -29,10 +29,11 @@ public class DdlController {
@PostMapping("/tables")
public ResponseEntity<ApiResponse<?>> createTable(
@RequestAttribute("company_code") String companyCode,
@RequestAttribute(value = "role", required = false) String role,
@RequestAttribute("user_id") String userId,
@RequestBody Map<String, Object> body) {
if (!isSuperAdmin(companyCode)) {
if (!isSuperAdmin(companyCode, role)) {
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
}
@@ -65,10 +66,11 @@ public class DdlController {
public ResponseEntity<ApiResponse<?>> addColumn(
@PathVariable String tableName,
@RequestAttribute("company_code") String companyCode,
@RequestAttribute(value = "role", required = false) String role,
@RequestAttribute("user_id") String userId,
@RequestBody Map<String, Object> 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<ApiResponse<?>> 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<ApiResponse<?>> validateTableCreation(
@RequestAttribute("company_code") String companyCode,
@RequestAttribute(value = "role", required = false) String role,
@RequestBody Map<String, Object> 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<ApiResponse<?>> 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<ApiResponse<?>> 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<ApiResponse<?>> 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<ApiResponse<?>> 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<ApiResponse<?>> 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<ApiResponse<?>> 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);
}
}
@@ -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);
@@ -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()
</insert>
@@ -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("컬럼이 성공적으로 추가되었습니다!");
// 테이블 목록 새로고침 (컬럼 수 업데이트)