Merge pull request #13 - fix+security: bug hunt 6 + 인가/SQL 2
Build & Deploy to K8s / build-and-deploy (push) Successful in 8m46s
Build & Deploy to K8s / build-and-deploy (push) Successful in 8m46s
johngreen -> main: 테이블 타입 관리 bug fix 6 + 보안 hardening 2
This commit was merged in pull request #13.
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
package com.erp.constants;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class InputTypeConstants {
|
||||||
|
private InputTypeConstants() {}
|
||||||
|
|
||||||
|
/** 사용자가 직접 선택 가능한 INPUT_TYPE 8종 (INSERT/UPDATE-type 검증용) */
|
||||||
|
public static final Set<String> USER_SELECTABLE_INPUT_TYPES = Set.of(
|
||||||
|
"text", "number", "date", "code", "entity",
|
||||||
|
"numbering", "file", "image"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.erp.constants;
|
||||||
|
|
||||||
|
public enum InputTypeContext {
|
||||||
|
USER_INSERT,
|
||||||
|
USER_UPDATE_TYPE,
|
||||||
|
USER_UPDATE_OTHER,
|
||||||
|
SYSTEM_NORMALIZE
|
||||||
|
}
|
||||||
@@ -75,7 +75,11 @@ public class TableManagementController {
|
|||||||
@PutMapping("/tables/{tableName}/label")
|
@PutMapping("/tables/{tableName}/label")
|
||||||
public ResponseEntity<ApiResponse<Void>> updateTableLabel(
|
public ResponseEntity<ApiResponse<Void>> updateTableLabel(
|
||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@RequestBody Map<String, Object> body) {
|
@RequestBody Map<String, Object> body,
|
||||||
|
@RequestAttribute("role") String role) {
|
||||||
|
if (!isAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
String displayName = (String) body.get("display_name");
|
String displayName = (String) body.get("display_name");
|
||||||
String description = (String) body.get("description");
|
String description = (String) body.get("description");
|
||||||
if (displayName == null || displayName.isBlank()) {
|
if (displayName == null || displayName.isBlank()) {
|
||||||
@@ -105,7 +109,11 @@ public class TableManagementController {
|
|||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@PathVariable String columnName,
|
@PathVariable String columnName,
|
||||||
@RequestBody Map<String, Object> settings,
|
@RequestBody Map<String, Object> settings,
|
||||||
|
@RequestAttribute("role") String role,
|
||||||
@RequestAttribute("company_code") String companyCode) {
|
@RequestAttribute("company_code") String companyCode) {
|
||||||
|
if (!isAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
return doUpdateColumnSettings(tableName, columnName, settings, companyCode);
|
return doUpdateColumnSettings(tableName, columnName, settings, companyCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +123,11 @@ public class TableManagementController {
|
|||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@PathVariable String columnName,
|
@PathVariable String columnName,
|
||||||
@RequestBody Map<String, Object> settings,
|
@RequestBody Map<String, Object> settings,
|
||||||
|
@RequestAttribute("role") String role,
|
||||||
@RequestAttribute("company_code") String companyCode) {
|
@RequestAttribute("company_code") String companyCode) {
|
||||||
|
if (!isAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
return doUpdateColumnSettings(tableName, columnName, settings, companyCode);
|
return doUpdateColumnSettings(tableName, columnName, settings, companyCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +148,11 @@ public class TableManagementController {
|
|||||||
public ResponseEntity<ApiResponse<Void>> updateAllColumnSettingsPost(
|
public ResponseEntity<ApiResponse<Void>> updateAllColumnSettingsPost(
|
||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@RequestBody List<Map<String, Object>> columnSettings,
|
@RequestBody List<Map<String, Object>> columnSettings,
|
||||||
|
@RequestAttribute("role") String role,
|
||||||
@RequestAttribute("company_code") String companyCode) {
|
@RequestAttribute("company_code") String companyCode) {
|
||||||
|
if (!isAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
return doUpdateAllColumnSettings(tableName, columnSettings, companyCode);
|
return doUpdateAllColumnSettings(tableName, columnSettings, companyCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +161,11 @@ public class TableManagementController {
|
|||||||
public ResponseEntity<ApiResponse<Void>> updateAllColumnSettingsBatch(
|
public ResponseEntity<ApiResponse<Void>> updateAllColumnSettingsBatch(
|
||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@RequestBody List<Map<String, Object>> columnSettings,
|
@RequestBody List<Map<String, Object>> columnSettings,
|
||||||
|
@RequestAttribute("role") String role,
|
||||||
@RequestAttribute("company_code") String companyCode) {
|
@RequestAttribute("company_code") String companyCode) {
|
||||||
|
if (!isAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
return doUpdateAllColumnSettings(tableName, columnSettings, companyCode);
|
return doUpdateAllColumnSettings(tableName, columnSettings, companyCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +186,11 @@ public class TableManagementController {
|
|||||||
public ResponseEntity<ApiResponse<Void>> updateColumnWebType(
|
public ResponseEntity<ApiResponse<Void>> updateColumnWebType(
|
||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@PathVariable String columnName,
|
@PathVariable String columnName,
|
||||||
@RequestBody Map<String, Object> body) {
|
@RequestBody Map<String, Object> body,
|
||||||
|
@RequestAttribute("role") String role) {
|
||||||
|
if (!isAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
String webType = (String) body.get("web_type");
|
String webType = (String) body.get("web_type");
|
||||||
if (webType == null || webType.isBlank()) {
|
if (webType == null || webType.isBlank()) {
|
||||||
return ResponseEntity.status(400).body(ApiResponse.error("웹 타입이 필요합니다."));
|
return ResponseEntity.status(400).body(ApiResponse.error("웹 타입이 필요합니다."));
|
||||||
@@ -183,7 +207,11 @@ public class TableManagementController {
|
|||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@PathVariable String columnName,
|
@PathVariable String columnName,
|
||||||
@RequestBody Map<String, Object> body,
|
@RequestBody Map<String, Object> body,
|
||||||
|
@RequestAttribute("role") String role,
|
||||||
@RequestAttribute("company_code") String companyCode) {
|
@RequestAttribute("company_code") String companyCode) {
|
||||||
|
if (!isAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
String inputType = (String) body.get("input_type");
|
String inputType = (String) body.get("input_type");
|
||||||
if (tableName == null || columnName == null || inputType == null || inputType.isBlank()) {
|
if (tableName == null || columnName == null || inputType == null || inputType.isBlank()) {
|
||||||
return ResponseEntity.status(400).body(ApiResponse.error("테이블명, 컬럼명, 입력 타입이 모두 필요합니다."));
|
return ResponseEntity.status(400).body(ApiResponse.error("테이블명, 컬럼명, 입력 타입이 모두 필요합니다."));
|
||||||
@@ -241,7 +269,11 @@ public class TableManagementController {
|
|||||||
@PutMapping("/tables/{tableName}/primary-key")
|
@PutMapping("/tables/{tableName}/primary-key")
|
||||||
public ResponseEntity<ApiResponse<Void>> setTablePrimaryKey(
|
public ResponseEntity<ApiResponse<Void>> setTablePrimaryKey(
|
||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@RequestBody Map<String, Object> body) {
|
@RequestBody Map<String, Object> body,
|
||||||
|
@RequestAttribute("role") String role) {
|
||||||
|
if (!isSuperAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<String> columns = (List<String>) body.get("columns");
|
List<String> columns = (List<String>) body.get("columns");
|
||||||
if (tableName == null || columns == null || columns.isEmpty()) {
|
if (tableName == null || columns == null || columns.isEmpty()) {
|
||||||
@@ -256,7 +288,11 @@ public class TableManagementController {
|
|||||||
@PostMapping("/tables/{tableName}/indexes")
|
@PostMapping("/tables/{tableName}/indexes")
|
||||||
public ResponseEntity<ApiResponse<Void>> toggleTableIndex(
|
public ResponseEntity<ApiResponse<Void>> toggleTableIndex(
|
||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@RequestBody Map<String, Object> body) {
|
@RequestBody Map<String, Object> body,
|
||||||
|
@RequestAttribute("role") String role) {
|
||||||
|
if (!isSuperAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
String columnName = (String) body.get("column_name");
|
String columnName = (String) body.get("column_name");
|
||||||
String indexType = (String) body.get("index_type");
|
String indexType = (String) body.get("index_type");
|
||||||
String action = (String) body.get("action");
|
String action = (String) body.get("action");
|
||||||
@@ -281,7 +317,11 @@ public class TableManagementController {
|
|||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@PathVariable String columnName,
|
@PathVariable String columnName,
|
||||||
@RequestBody Map<String, Object> body,
|
@RequestBody Map<String, Object> body,
|
||||||
|
@RequestAttribute("role") String role,
|
||||||
@RequestAttribute("company_code") String companyCode) {
|
@RequestAttribute("company_code") String companyCode) {
|
||||||
|
if (!isSuperAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
Object nullableObj = body.get("nullable");
|
Object nullableObj = body.get("nullable");
|
||||||
if (tableName == null || columnName == null || !(nullableObj instanceof Boolean)) {
|
if (tableName == null || columnName == null || !(nullableObj instanceof Boolean)) {
|
||||||
return ResponseEntity.status(400).body(ApiResponse.error("tableName, columnName, nullable(boolean)이 필요합니다."));
|
return ResponseEntity.status(400).body(ApiResponse.error("tableName, columnName, nullable(boolean)이 필요합니다."));
|
||||||
@@ -299,7 +339,11 @@ public class TableManagementController {
|
|||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@PathVariable String columnName,
|
@PathVariable String columnName,
|
||||||
@RequestBody Map<String, Object> body,
|
@RequestBody Map<String, Object> body,
|
||||||
|
@RequestAttribute("role") String role,
|
||||||
@RequestAttribute("company_code") String companyCode) {
|
@RequestAttribute("company_code") String companyCode) {
|
||||||
|
if (!isSuperAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
Object uniqueObj = body.get("unique");
|
Object uniqueObj = body.get("unique");
|
||||||
if (tableName == null || columnName == null || !(uniqueObj instanceof Boolean)) {
|
if (tableName == null || columnName == null || !(uniqueObj instanceof Boolean)) {
|
||||||
return ResponseEntity.status(400).body(ApiResponse.error("tableName, columnName, unique(boolean)이 필요합니다."));
|
return ResponseEntity.status(400).body(ApiResponse.error("tableName, columnName, unique(boolean)이 필요합니다."));
|
||||||
@@ -366,7 +410,11 @@ public class TableManagementController {
|
|||||||
public ResponseEntity<ApiResponse<Map<String, Object>>> addTableData(
|
public ResponseEntity<ApiResponse<Map<String, Object>>> addTableData(
|
||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@RequestBody Map<String, Object> data,
|
@RequestBody Map<String, Object> data,
|
||||||
|
@RequestAttribute("role") String role,
|
||||||
@RequestAttribute("company_code") String companyCode) {
|
@RequestAttribute("company_code") String companyCode) {
|
||||||
|
if (!isAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
if (data == null || data.isEmpty()) {
|
if (data == null || data.isEmpty()) {
|
||||||
return ResponseEntity.status(400).body(ApiResponse.error("추가할 데이터가 필요합니다."));
|
return ResponseEntity.status(400).body(ApiResponse.error("추가할 데이터가 필요합니다."));
|
||||||
}
|
}
|
||||||
@@ -399,7 +447,11 @@ public class TableManagementController {
|
|||||||
public ResponseEntity<ApiResponse<Void>> editTableData(
|
public ResponseEntity<ApiResponse<Void>> editTableData(
|
||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@RequestBody Map<String, Object> body,
|
@RequestBody Map<String, Object> body,
|
||||||
|
@RequestAttribute("role") String role,
|
||||||
@RequestAttribute("company_code") String companyCode) {
|
@RequestAttribute("company_code") String companyCode) {
|
||||||
|
if (!isAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Object> originalData = (Map<String, Object>) body.get("original_data");
|
Map<String, Object> originalData = (Map<String, Object>) body.get("original_data");
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@@ -433,7 +485,11 @@ public class TableManagementController {
|
|||||||
@DeleteMapping("/tables/{tableName}/delete")
|
@DeleteMapping("/tables/{tableName}/delete")
|
||||||
public ResponseEntity<ApiResponse<Void>> deleteTableData(
|
public ResponseEntity<ApiResponse<Void>> deleteTableData(
|
||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@RequestBody Object body) {
|
@RequestBody Object body,
|
||||||
|
@RequestAttribute("role") String role) {
|
||||||
|
if (!isAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
List<Map<String, Object>> dataList;
|
List<Map<String, Object>> dataList;
|
||||||
if (body instanceof List) {
|
if (body instanceof List) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@@ -457,7 +513,11 @@ public class TableManagementController {
|
|||||||
@PostMapping("/tables/{tableName}/log")
|
@PostMapping("/tables/{tableName}/log")
|
||||||
public ResponseEntity<ApiResponse<Void>> createLogTable(
|
public ResponseEntity<ApiResponse<Void>> createLogTable(
|
||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@RequestBody Map<String, Object> body) {
|
@RequestBody Map<String, Object> body,
|
||||||
|
@RequestAttribute("role") String role) {
|
||||||
|
if (!isSuperAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<String> logColumns = (List<String>) body.get("log_columns");
|
List<String> logColumns = (List<String>) body.get("log_columns");
|
||||||
boolean isActive = Boolean.TRUE.equals(body.get("is_active"));
|
boolean isActive = Boolean.TRUE.equals(body.get("is_active"));
|
||||||
@@ -487,7 +547,11 @@ public class TableManagementController {
|
|||||||
@PostMapping("/tables/{tableName}/log/toggle")
|
@PostMapping("/tables/{tableName}/log/toggle")
|
||||||
public ResponseEntity<ApiResponse<Void>> toggleLogTable(
|
public ResponseEntity<ApiResponse<Void>> toggleLogTable(
|
||||||
@PathVariable String tableName,
|
@PathVariable String tableName,
|
||||||
@RequestBody Map<String, Object> body) {
|
@RequestBody Map<String, Object> body,
|
||||||
|
@RequestAttribute("role") String role) {
|
||||||
|
if (!isAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
boolean isActive = Boolean.TRUE.equals(body.get("is_active"));
|
boolean isActive = Boolean.TRUE.equals(body.get("is_active"));
|
||||||
tableManagementService.toggleLogTable(tableName, isActive);
|
tableManagementService.toggleLogTable(tableName, isActive);
|
||||||
return ResponseEntity.ok(ApiResponse.success(null,
|
return ResponseEntity.ok(ApiResponse.success(null,
|
||||||
@@ -544,7 +608,11 @@ public class TableManagementController {
|
|||||||
@PostMapping("/multi-table-save")
|
@PostMapping("/multi-table-save")
|
||||||
public ResponseEntity<ApiResponse<Map<String, Object>>> multiTableSave(
|
public ResponseEntity<ApiResponse<Map<String, Object>>> multiTableSave(
|
||||||
@RequestBody Map<String, Object> payload,
|
@RequestBody Map<String, Object> payload,
|
||||||
|
@RequestAttribute("role") String role,
|
||||||
@RequestAttribute("company_code") String companyCode) {
|
@RequestAttribute("company_code") String companyCode) {
|
||||||
|
if (!isAdmin(role)) {
|
||||||
|
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||||
|
}
|
||||||
return ResponseEntity.ok(ApiResponse.success(
|
return ResponseEntity.ok(ApiResponse.success(
|
||||||
tableManagementService.multiTableSave(payload, companyCode),
|
tableManagementService.multiTableSave(payload, companyCode),
|
||||||
"다중 테이블 저장이 완료되었습니다."));
|
"다중 테이블 저장이 완료되었습니다."));
|
||||||
@@ -575,4 +643,16 @@ public class TableManagementController {
|
|||||||
return ResponseEntity.ok(ApiResponse.success(
|
return ResponseEntity.ok(ApiResponse.success(
|
||||||
tableManagementService.checkDatabaseConnection(), "데이터베이스 연결 상태를 확인했습니다."));
|
tableManagementService.checkDatabaseConnection(), "데이터베이스 연결 상태를 확인했습니다."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────
|
||||||
|
// 권한 헬퍼
|
||||||
|
// ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private boolean isAdmin(String role) {
|
||||||
|
return isSuperAdmin(role) || "COMPANY_ADMIN".equals(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSuperAdmin(String roleOrCode) {
|
||||||
|
return "*".equals(roleOrCode) || "SUPER_ADMIN".equals(roleOrCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.erp.service;
|
package com.erp.service;
|
||||||
|
|
||||||
import com.erp.common.BaseService;
|
import com.erp.common.BaseService;
|
||||||
|
import com.erp.constants.InputTypeConstants;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -39,12 +40,6 @@ public class DdlService extends BaseService {
|
|||||||
"id", "created_date", "updated_date", "company_code"
|
"id", "created_date", "updated_date", "company_code"
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 사용자가 신규 추가하는 컬럼에 허용되는 INPUT_TYPE 8종 (백엔드 백스톱) */
|
|
||||||
private static final Set<String> USER_SELECTABLE_INPUT_TYPES = Set.of(
|
|
||||||
"text", "number", "date", "code", "entity",
|
|
||||||
"numbering", "file", "image"
|
|
||||||
);
|
|
||||||
|
|
||||||
public DdlService(JdbcTemplate jdbcTemplate, PlatformTransactionManager transactionManager) {
|
public DdlService(JdbcTemplate jdbcTemplate, PlatformTransactionManager transactionManager) {
|
||||||
this.jdbcTemplate = jdbcTemplate;
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
this.transactionTemplate = new TransactionTemplate(transactionManager);
|
this.transactionTemplate = new TransactionTemplate(transactionManager);
|
||||||
@@ -146,9 +141,9 @@ public class DdlService extends BaseService {
|
|||||||
transactionTemplate.execute(status -> {
|
transactionTemplate.execute(status -> {
|
||||||
jdbcTemplate.execute(ddlQuery);
|
jdbcTemplate.execute(ddlQuery);
|
||||||
String inputType = convertToInputType(column);
|
String inputType = convertToInputType(column);
|
||||||
if (!USER_SELECTABLE_INPUT_TYPES.contains(inputType)) {
|
if (!InputTypeConstants.USER_SELECTABLE_INPUT_TYPES.contains(inputType)) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + USER_SELECTABLE_INPUT_TYPES
|
"INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + InputTypeConstants.USER_SELECTABLE_INPUT_TYPES
|
||||||
+ " (받은 값: " + inputType + ")"
|
+ " (받은 값: " + inputType + ")"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -421,9 +416,9 @@ public class DdlService extends BaseService {
|
|||||||
for (int i = 0; i < columns.size(); i++) {
|
for (int i = 0; i < columns.size(); i++) {
|
||||||
Map<String, Object> col = columns.get(i);
|
Map<String, Object> col = columns.get(i);
|
||||||
String inputType = convertToInputType(col);
|
String inputType = convertToInputType(col);
|
||||||
if (!USER_SELECTABLE_INPUT_TYPES.contains(inputType)) {
|
if (!InputTypeConstants.USER_SELECTABLE_INPUT_TYPES.contains(inputType)) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + USER_SELECTABLE_INPUT_TYPES
|
"INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + InputTypeConstants.USER_SELECTABLE_INPUT_TYPES
|
||||||
+ " (받은 값: " + inputType + ")"
|
+ " (받은 값: " + inputType + ")"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -532,6 +527,9 @@ public class DdlService extends BaseService {
|
|||||||
case "radio" -> "radio";
|
case "radio" -> "radio";
|
||||||
case "code" -> "code";
|
case "code" -> "code";
|
||||||
case "entity" -> "entity";
|
case "entity" -> "entity";
|
||||||
|
case "file" -> "file";
|
||||||
|
case "image" -> "image";
|
||||||
|
case "numbering" -> "numbering";
|
||||||
default -> "text";
|
default -> "text";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.erp.service;
|
package com.erp.service;
|
||||||
|
|
||||||
import com.erp.common.BaseService;
|
import com.erp.common.BaseService;
|
||||||
|
import com.erp.constants.InputTypeConstants;
|
||||||
|
import com.erp.constants.InputTypeContext;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -26,10 +28,14 @@ public class TableManagementService extends BaseService {
|
|||||||
|
|
||||||
private static final String NS = "tableManagement.";
|
private static final String NS = "tableManagement.";
|
||||||
|
|
||||||
/** 사용자가 직접 선택 가능한 INPUT_TYPE 8종 (INSERT/UPDATE-type 검증용) */
|
/** 로그 테이블 컬럼 정의에 허용하는 PostgreSQL data_type 화이트리스트.
|
||||||
private static final Set<String> USER_SELECTABLE_INPUT_TYPES = Set.of(
|
* information_schema.columns.data_type 값과 정확히 일치해야 한다. */
|
||||||
"text", "number", "date", "code", "entity",
|
private static final Set<String> ALLOWED_LOG_COLUMN_TYPES = Set.of(
|
||||||
"numbering", "file", "image"
|
"varchar", "text", "char", "character", "character varying",
|
||||||
|
"integer", "bigint", "smallint", "numeric", "decimal", "real", "double precision",
|
||||||
|
"boolean", "date", "timestamp", "timestamp without time zone", "timestamp with time zone",
|
||||||
|
"time", "time without time zone", "time with time zone",
|
||||||
|
"uuid", "json", "jsonb", "bytea"
|
||||||
);
|
);
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
@@ -151,9 +157,12 @@ public class TableManagementService extends BaseService {
|
|||||||
Map<String, Object> settings, String companyCode) {
|
Map<String, Object> settings, String companyCode) {
|
||||||
ensureTableInLabels(tableName);
|
ensureTableInLabels(tableName);
|
||||||
|
|
||||||
boolean inputTypeChanged = settings.containsKey("input_type");
|
Object rawInputType = settings.get("input_type");
|
||||||
String ctx = inputTypeChanged ? "user-update-type" : "user-update-other";
|
boolean inputTypeChanged = settings.containsKey("input_type") && rawInputType != null;
|
||||||
String inputType = normalizeInputType((String) settings.get("input_type"), ctx);
|
InputTypeContext ctx = inputTypeChanged
|
||||||
|
? InputTypeContext.USER_UPDATE_TYPE
|
||||||
|
: InputTypeContext.USER_UPDATE_OTHER;
|
||||||
|
String inputType = normalizeInputType((String) rawInputType, ctx);
|
||||||
Map<String, Object> params = new HashMap<>();
|
Map<String, Object> params = new HashMap<>();
|
||||||
params.put("table_name", tableName);
|
params.put("table_name", tableName);
|
||||||
params.put("column_name", columnName);
|
params.put("column_name", columnName);
|
||||||
@@ -210,7 +219,7 @@ public class TableManagementService extends BaseService {
|
|||||||
public void updateColumnInputType(String tableName, String columnName,
|
public void updateColumnInputType(String tableName, String columnName,
|
||||||
String inputType, String companyCode,
|
String inputType, String companyCode,
|
||||||
Map<String, Object> detailSettings) {
|
Map<String, Object> detailSettings) {
|
||||||
String finalType = normalizeInputType(inputType, "user-update-type");
|
String finalType = normalizeInputType(inputType, InputTypeContext.USER_UPDATE_TYPE);
|
||||||
Map<String, Object> params = new HashMap<>();
|
Map<String, Object> params = new HashMap<>();
|
||||||
params.put("table_name", tableName);
|
params.put("table_name", tableName);
|
||||||
params.put("column_name", columnName);
|
params.put("column_name", columnName);
|
||||||
@@ -611,9 +620,14 @@ public class TableManagementService extends BaseService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void createLogTable(String tableName, List<String> logColumns, boolean isActive) {
|
public void createLogTable(String tableName, List<String> logColumns, boolean isActive) {
|
||||||
String logTableName = tableName + "_log";
|
|
||||||
String safeLog = sanitize(logTableName);
|
|
||||||
String safeOrig = sanitize(tableName);
|
String safeOrig = sanitize(tableName);
|
||||||
|
if (safeOrig.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("유효하지 않은 테이블명입니다.");
|
||||||
|
}
|
||||||
|
String safeLog = sanitize(safeOrig + "_log");
|
||||||
|
if (safeLog.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("유효하지 않은 로그 테이블명입니다.");
|
||||||
|
}
|
||||||
|
|
||||||
// 원본 테이블 컬럼 정보 조회
|
// 원본 테이블 컬럼 정보 조회
|
||||||
Map<String, String> colTypes = getColumnTypes(safeOrig);
|
Map<String, String> colTypes = getColumnTypes(safeOrig);
|
||||||
@@ -625,13 +639,32 @@ public class TableManagementService extends BaseService {
|
|||||||
colDefs.add("log_date TIMESTAMP DEFAULT NOW()");
|
colDefs.add("log_date TIMESTAMP DEFAULT NOW()");
|
||||||
colDefs.add("log_user VARCHAR(100)");
|
colDefs.add("log_user VARCHAR(100)");
|
||||||
|
|
||||||
List<String> targetCols = (logColumns != null && !logColumns.isEmpty())
|
List<String> requestedCols = (logColumns != null && !logColumns.isEmpty())
|
||||||
? logColumns.stream().map(this::sanitize).filter(c -> !c.isBlank()).collect(Collectors.toList())
|
? logColumns
|
||||||
: new ArrayList<>(colTypes.keySet());
|
: new ArrayList<>(colTypes.keySet());
|
||||||
|
|
||||||
for (String col : targetCols) {
|
// 실제 SQL 에 들어간 컬럼만 메타에 저장 (skip 된 것은 log_columns 설정에서도 빠짐)
|
||||||
String type = colTypes.getOrDefault(col, "TEXT");
|
List<String> persistedCols = new ArrayList<>();
|
||||||
colDefs.add(String.format("\"%s\" %s", col, type));
|
for (String col : requestedCols) {
|
||||||
|
if (col == null) continue;
|
||||||
|
String safeCol = sanitize(col);
|
||||||
|
if (safeCol.isBlank()) continue; // sanitize 결과 빈 식별자 차단
|
||||||
|
if (!colTypes.containsKey(col)) continue; // 원본 테이블에 없는 컬럼 skip
|
||||||
|
|
||||||
|
String rawType = colTypes.get(col);
|
||||||
|
String normalized = (rawType == null ? "" : rawType.toLowerCase(Locale.ROOT).trim());
|
||||||
|
if (!ALLOWED_LOG_COLUMN_TYPES.contains(normalized)) {
|
||||||
|
// 알 수 없는 type 은 text 로 fallback (안전 default)
|
||||||
|
log.warn("로그 테이블 컬럼 타입 화이트리스트 미일치 → text 로 대체: table={}, col={}, type={}",
|
||||||
|
safeOrig, safeCol, rawType);
|
||||||
|
normalized = "text";
|
||||||
|
}
|
||||||
|
colDefs.add(String.format("\"%s\" %s", safeCol, normalized));
|
||||||
|
persistedCols.add(safeCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (persistedCols.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("log 생성할 컬럼이 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
String createSql = String.format(
|
String createSql = String.format(
|
||||||
@@ -642,7 +675,7 @@ public class TableManagementService extends BaseService {
|
|||||||
Map<String, Object> params = new HashMap<>();
|
Map<String, Object> params = new HashMap<>();
|
||||||
params.put("table_name", tableName);
|
params.put("table_name", tableName);
|
||||||
params.put("is_active", isActive);
|
params.put("is_active", isActive);
|
||||||
params.put("log_columns", String.join(",", targetCols));
|
params.put("log_columns", String.join(",", persistedCols));
|
||||||
sqlSession.update(NS + "upsertLogConfig", params);
|
sqlSession.update(NS + "upsertLogConfig", params);
|
||||||
|
|
||||||
log.info("로그 테이블 생성: {}", safeLog);
|
log.info("로그 테이블 생성: {}", safeLog);
|
||||||
@@ -872,19 +905,18 @@ public class TableManagementService extends BaseService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* context 에 따라 INPUT_TYPE 정규화 및 검증.
|
* context 에 따라 INPUT_TYPE 정규화 및 검증.
|
||||||
* @param context "user-insert" | "user-update-type" | "user-update-other" | "system-normalize"
|
|
||||||
*/
|
*/
|
||||||
private String normalizeInputType(String value, String context) {
|
private String normalizeInputType(String value, InputTypeContext context) {
|
||||||
if ("user-insert".equals(context) || "user-update-type".equals(context)) {
|
if (context == InputTypeContext.USER_INSERT || context == InputTypeContext.USER_UPDATE_TYPE) {
|
||||||
if (value == null || !USER_SELECTABLE_INPUT_TYPES.contains(value)) {
|
if (value == null || !InputTypeConstants.USER_SELECTABLE_INPUT_TYPES.contains(value)) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + USER_SELECTABLE_INPUT_TYPES
|
"INPUT_TYPE 은 다음 8개 중 하나여야 합니다: " + InputTypeConstants.USER_SELECTABLE_INPUT_TYPES
|
||||||
+ " (받은 값: " + value + ")"
|
+ " (받은 값: " + value + ")"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
// user-update-other / system-normalize: 기존 동작 그대로
|
// USER_UPDATE_OTHER / SYSTEM_NORMALIZE: 기존 동작 그대로
|
||||||
return normalizeInputType(value);
|
return normalizeInputType(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,24 @@ function countByInputType(columns: ColumnTypeInfo[]): Record<string, number> {
|
|||||||
return counts;
|
return counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 도넛 차트용 비율 (0~1) 배열 및 라벨 순서 (8개 사용자 선택 가능 타입 한정) */
|
/** 도넛 차트용 segment (8개 base + legacy 그룹) */
|
||||||
function getDonutSegments(counts: Record<string, number>, total: number): Array<{ type: string; ratio: number }> {
|
function getDonutSegments(
|
||||||
return (USER_SELECTABLE_INPUT_TYPE_ORDER as readonly string[])
|
counts: Record<string, number>,
|
||||||
|
total: number,
|
||||||
|
): Array<{ type: string; ratio: number; isLegacy: boolean }> {
|
||||||
|
const baseSegments = (USER_SELECTABLE_INPUT_TYPE_ORDER as readonly string[])
|
||||||
.filter((type) => (counts[type] || 0) > 0)
|
.filter((type) => (counts[type] || 0) > 0)
|
||||||
.map((type) => ({ type, ratio: (counts[type] || 0) / total }));
|
.map((type) => ({ type, ratio: (counts[type] || 0) / total, isLegacy: false }));
|
||||||
|
|
||||||
|
const legacyCount = Object.entries(counts)
|
||||||
|
.filter(([type]) => !(USER_SELECTABLE_INPUT_TYPE_ORDER as readonly string[]).includes(type))
|
||||||
|
.reduce((sum, [, count]) => sum + count, 0);
|
||||||
|
|
||||||
|
if (legacyCount > 0) {
|
||||||
|
baseSegments.push({ type: "__legacy__", ratio: legacyCount / total, isLegacy: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseSegments;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TypeOverviewStrip({
|
export function TypeOverviewStrip({
|
||||||
@@ -44,16 +57,25 @@ export function TypeOverviewStrip({
|
|||||||
/** stroke-dasharray: 비율만큼 둘레에 할당 (둘레 100 기준) */
|
/** stroke-dasharray: 비율만큼 둘레에 할당 (둘레 100 기준) */
|
||||||
const circumference = 100;
|
const circumference = 100;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
const segmentPaths = segments.map(({ type, ratio }) => {
|
const LEGACY_CONF = {
|
||||||
|
color: "text-amber-600",
|
||||||
|
bgColor: "bg-amber-50",
|
||||||
|
barColor: "bg-amber-400",
|
||||||
|
label: "Legacy",
|
||||||
|
desc: "구버전 타입",
|
||||||
|
iconChar: "?",
|
||||||
|
};
|
||||||
|
const segmentPaths = segments.map(({ type, ratio, isLegacy }) => {
|
||||||
const length = ratio * circumference;
|
const length = ratio * circumference;
|
||||||
const dashArray = `${length} ${circumference - length}`;
|
const dashArray = `${length} ${circumference - length}`;
|
||||||
const dashOffset = -offset;
|
const dashOffset = -offset;
|
||||||
offset += length;
|
offset += length;
|
||||||
const conf = INPUT_TYPE_COLORS[type] || { color: "text-muted-foreground", bgColor: "bg-muted" };
|
const conf = isLegacy ? LEGACY_CONF : (INPUT_TYPE_COLORS[type] || { color: "text-muted-foreground", bgColor: "bg-muted" });
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
dashArray,
|
dashArray,
|
||||||
dashOffset,
|
dashOffset,
|
||||||
|
isLegacy,
|
||||||
...conf,
|
...conf,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -84,7 +106,7 @@ export function TypeOverviewStrip({
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 타입 칩 목록 (8개 사용자 선택 가능 타입 한정, 클릭 시 필터 토글) */}
|
{/* 타입 칩 목록 (8개 base + legacy 그룹, 클릭 시 필터 토글) */}
|
||||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
||||||
{(USER_SELECTABLE_INPUT_TYPE_ORDER as readonly string[])
|
{(USER_SELECTABLE_INPUT_TYPE_ORDER as readonly string[])
|
||||||
.filter((type) => (counts[type] || 0) > 0)
|
.filter((type) => (counts[type] || 0) > 0)
|
||||||
@@ -109,6 +131,29 @@ export function TypeOverviewStrip({
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{/* Legacy 칩 — 8개 외 구버전 타입 합산 */}
|
||||||
|
{(() => {
|
||||||
|
const legacyCount = Object.entries(counts)
|
||||||
|
.filter(([type]) => !(USER_SELECTABLE_INPUT_TYPE_ORDER as readonly string[]).includes(type))
|
||||||
|
.reduce((sum, [, count]) => sum + count, 0);
|
||||||
|
if (legacyCount === 0) return null;
|
||||||
|
const isActive = activeFilter === null || activeFilter === "__legacy__";
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key="__legacy__"
|
||||||
|
type="button"
|
||||||
|
onClick={() => onFilterChange?.(activeFilter === "__legacy__" ? null : "__legacy__")}
|
||||||
|
className={cn(
|
||||||
|
"rounded-md border px-2 py-1 text-xs font-medium transition-colors",
|
||||||
|
"bg-amber-50 text-amber-600",
|
||||||
|
"border-amber-200",
|
||||||
|
isActive ? "ring-1 ring-ring" : "opacity-70 hover:opacity-100",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Legacy {legacyCount}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ export function TableSettingModal({
|
|||||||
const initialEdits: Record<string, Partial<ColumnTypeInfo>> = {};
|
const initialEdits: Record<string, Partial<ColumnTypeInfo>> = {};
|
||||||
columnsData.forEach((col) => {
|
columnsData.forEach((col) => {
|
||||||
// reference_table이 설정되어 있으면 input_type은 entity여야 함
|
// reference_table이 설정되어 있으면 input_type은 entity여야 함
|
||||||
let effectiveInputType = col.input_type || "direct";
|
let effectiveInputType = col.input_type || "text";
|
||||||
if (col.reference_table && effectiveInputType !== "entity") {
|
if (col.reference_table && effectiveInputType !== "entity") {
|
||||||
effectiveInputType = "entity";
|
effectiveInputType = "entity";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user