Compare commits
8 Commits
8a9285f13e
...
f8ef029e10
| Author | SHA1 | Date | |
|---|---|---|---|
| f8ef029e10 | |||
| 5cd8e72bf0 | |||
| 387a1ae611 | |||
| eeb130e3a8 | |||
| 3ffa5c8ff5 | |||
| acbab68a12 | |||
| db63ba6901 | |||
| ff95c1950e |
@@ -34,7 +34,7 @@ public class DdlController {
|
||||
@RequestBody Map<String, Object> body) {
|
||||
|
||||
if (!isSuperAdmin(companyCode, role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
String tableName = (String) body.get("table_name");
|
||||
@@ -71,7 +71,7 @@ public class DdlController {
|
||||
@RequestBody Map<String, Object> body) {
|
||||
|
||||
if (!isSuperAdmin(companyCode, role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -105,7 +105,7 @@ public class DdlController {
|
||||
@RequestAttribute("user_id") String userId) {
|
||||
|
||||
if (!isSuperAdmin(companyCode, role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
Map<String, Object> result = ddlService.dropColumn(tableName, columnName, companyCode, userId);
|
||||
@@ -131,7 +131,7 @@ public class DdlController {
|
||||
@RequestAttribute("user_id") String userId) {
|
||||
|
||||
if (!isSuperAdmin(companyCode, role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
Map<String, Object> result = ddlService.dropTable(tableName, companyCode, userId);
|
||||
@@ -155,7 +155,7 @@ public class DdlController {
|
||||
@RequestBody Map<String, Object> body) {
|
||||
|
||||
if (!isSuperAdmin(companyCode, role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
String tableName = (String) body.get("table_name");
|
||||
@@ -187,7 +187,7 @@ public class DdlController {
|
||||
@RequestParam(required = false) String ddlType) {
|
||||
|
||||
if (!isSuperAdmin(companyCode, role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
List<Map<String, Object>> logs = ddlService.getDdlLogs(limit, userId, ddlType);
|
||||
@@ -206,7 +206,7 @@ public class DdlController {
|
||||
@RequestParam(required = false) String toDate) {
|
||||
|
||||
if (!isSuperAdmin(companyCode, role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
Map<String, Object> statistics = ddlService.getDdlStatistics(fromDate, toDate);
|
||||
@@ -223,7 +223,7 @@ public class DdlController {
|
||||
@RequestAttribute(value = "role", required = false) String role) {
|
||||
|
||||
if (!isSuperAdmin(companyCode, role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
List<Map<String, Object>> history = ddlService.getTableDdlHistory(tableName);
|
||||
@@ -242,7 +242,7 @@ public class DdlController {
|
||||
@RequestAttribute(value = "role", required = false) String role) {
|
||||
|
||||
if (!isSuperAdmin(companyCode, role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
Map<String, Object> tableInfo = ddlService.getTableInfo(tableName);
|
||||
@@ -268,7 +268,7 @@ public class DdlController {
|
||||
@RequestParam(required = false, defaultValue = "90") int retentionDays) {
|
||||
|
||||
if (!isSuperAdmin(companyCode, role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
int deletedCount = ddlService.cleanupOldLogs(retentionDays);
|
||||
@@ -286,7 +286,7 @@ public class DdlController {
|
||||
@RequestAttribute(value = "role", required = false) String role) {
|
||||
|
||||
if (!isSuperAdmin(companyCode, role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
||||
|
||||
@@ -187,7 +187,8 @@ public class TableManagementController {
|
||||
@PathVariable String tableName,
|
||||
@PathVariable String columnName,
|
||||
@RequestBody Map<String, Object> body,
|
||||
@RequestAttribute("role") String role) {
|
||||
@RequestAttribute("role") String role,
|
||||
@RequestAttribute("company_code") String companyCode) {
|
||||
if (!isAdmin(role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("관리자 권한이 필요합니다."));
|
||||
}
|
||||
@@ -197,7 +198,8 @@ public class TableManagementController {
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> detailSettings = (Map<String, Object>) body.get("detail_settings");
|
||||
tableManagementService.updateColumnWebType(tableName, columnName, webType, detailSettings);
|
||||
// 멀티테넌트 격리: SUPER_ADMIN(company_code='*') 가 아니면 자기 회사 코드로 저장
|
||||
tableManagementService.updateColumnWebType(tableName, columnName, webType, detailSettings, companyCode);
|
||||
return ResponseEntity.ok(ApiResponse.success(null, "컬럼 웹타입이 설정되었습니다."));
|
||||
}
|
||||
|
||||
@@ -272,7 +274,7 @@ public class TableManagementController {
|
||||
@RequestBody Map<String, Object> body,
|
||||
@RequestAttribute("role") String role) {
|
||||
if (!isSuperAdmin(role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> columns = (List<String>) body.get("columns");
|
||||
@@ -291,7 +293,7 @@ public class TableManagementController {
|
||||
@RequestBody Map<String, Object> body,
|
||||
@RequestAttribute("role") String role) {
|
||||
if (!isSuperAdmin(role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
String columnName = (String) body.get("column_name");
|
||||
String indexType = (String) body.get("index_type");
|
||||
@@ -320,7 +322,7 @@ public class TableManagementController {
|
||||
@RequestAttribute("role") String role,
|
||||
@RequestAttribute("company_code") String companyCode) {
|
||||
if (!isSuperAdmin(role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
Object nullableObj = body.get("nullable");
|
||||
if (tableName == null || columnName == null || !(nullableObj instanceof Boolean)) {
|
||||
@@ -342,7 +344,7 @@ public class TableManagementController {
|
||||
@RequestAttribute("role") String role,
|
||||
@RequestAttribute("company_code") String companyCode) {
|
||||
if (!isSuperAdmin(role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
Object uniqueObj = body.get("unique");
|
||||
if (tableName == null || columnName == null || !(uniqueObj instanceof Boolean)) {
|
||||
@@ -567,7 +569,7 @@ public class TableManagementController {
|
||||
@RequestBody Map<String, Object> body,
|
||||
@RequestAttribute("role") String role) {
|
||||
if (!isSuperAdmin(role)) {
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자 권한이 필요합니다."));
|
||||
return ResponseEntity.status(403).body(ApiResponse.error("최고 관리자(SUPER_ADMIN) 권한이 필요합니다."));
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> logColumns = (List<String>) body.get("log_columns");
|
||||
|
||||
@@ -215,19 +215,21 @@ public class TableManagementService extends BaseService {
|
||||
|
||||
@Transactional
|
||||
public void updateColumnWebType(String tableName, String columnName,
|
||||
String webType, Map<String, Object> detailSettings) {
|
||||
String webType, Map<String, Object> detailSettings,
|
||||
String companyCode) {
|
||||
String finalType = normalizeInputType(webType);
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("table_name", tableName);
|
||||
params.put("column_name", columnName);
|
||||
params.put("input_type", finalType);
|
||||
params.put("detail_settings", detailSettings != null ? toJsonString(detailSettings) : "{}");
|
||||
params.put("company_code", "*");
|
||||
// 멀티테넌트 격리: SUPER_ADMIN("*") 은 공통 설정, 그 외는 회사별 설정
|
||||
params.put("company_code", companyCode != null ? companyCode : "*");
|
||||
params.put("clear_entity", false);
|
||||
params.put("clear_code", false);
|
||||
params.put("clear_category", false);
|
||||
sqlSession.update(NS + "upsertColumnInputType", params);
|
||||
log.info("컬럼 웹타입 설정: {}.{} = {}", tableName, columnName, finalType);
|
||||
log.info("컬럼 웹타입 설정: {}.{} = {} (company={})", tableName, columnName, finalType, companyCode);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -398,12 +400,14 @@ public class TableManagementService extends BaseService {
|
||||
String safeTable = sanitize(tableName);
|
||||
List<String> violations = new ArrayList<>();
|
||||
|
||||
// N+N → N+1 최적화: hasColumn 은 information_schema 조회라 비싸. 루프 밖에서 한 번만 수행.
|
||||
boolean hasCompanyCode = hasColumn(safeTable, "company_code");
|
||||
|
||||
for (Map<String, Object> col : uniqueCols) {
|
||||
String colName = (String) col.get("column_name");
|
||||
Object val = data.get(colName);
|
||||
if (val == null) continue;
|
||||
|
||||
boolean hasCompanyCode = hasColumn(safeTable, "company_code");
|
||||
String sql;
|
||||
List<Object> sqlParams = new ArrayList<>();
|
||||
|
||||
@@ -1267,9 +1271,40 @@ public class TableManagementService extends BaseService {
|
||||
}
|
||||
|
||||
/** SQL injection 방지용 식별자 정리 */
|
||||
/**
|
||||
* SQL 식별자(테이블/컬럼명) 살균.
|
||||
* - 영숫자/언더스코어만 허용 (PostgreSQL identifier 규칙)
|
||||
* - 빈 문자열, 숫자로 시작, 63자 초과, SQL 예약어 거부 → IllegalArgumentException
|
||||
*
|
||||
* 이렇게 가드해두지 않으면 동적 SQL 에 빈 식별자가 들어가거나 예약어가 통과해
|
||||
* 의도치 않은 컬럼에 접근하거나 SQL 문법 깨짐(500) 이 생김.
|
||||
*/
|
||||
private static final java.util.Set<String> SQL_RESERVED_WORDS = java.util.Set.of(
|
||||
"user", "order", "group", "table", "column", "index", "select", "insert",
|
||||
"update", "delete", "from", "where", "join", "on", "as", "and", "or", "not",
|
||||
"null", "true", "false", "create", "alter", "drop", "primary", "key",
|
||||
"foreign", "references", "constraint", "default", "unique", "check",
|
||||
"view", "procedure", "function"
|
||||
);
|
||||
|
||||
private String sanitize(String name) {
|
||||
if (name == null) return "";
|
||||
return name.replaceAll("[^a-zA-Z0-9_]", "");
|
||||
if (name == null) {
|
||||
throw new IllegalArgumentException("식별자가 null 입니다.");
|
||||
}
|
||||
String cleaned = name.replaceAll("[^a-zA-Z0-9_]", "");
|
||||
if (cleaned.isEmpty()) {
|
||||
throw new IllegalArgumentException("식별자가 비어있거나 유효하지 않습니다: " + name);
|
||||
}
|
||||
if (cleaned.length() > 63) {
|
||||
throw new IllegalArgumentException("식별자가 63자를 초과합니다: " + cleaned);
|
||||
}
|
||||
if (Character.isDigit(cleaned.charAt(0))) {
|
||||
throw new IllegalArgumentException("식별자는 숫자로 시작할 수 없습니다: " + cleaned);
|
||||
}
|
||||
if (SQL_RESERVED_WORDS.contains(cleaned.toLowerCase())) {
|
||||
throw new IllegalArgumentException("'" + cleaned + "' 은 SQL 예약어라 식별자로 사용할 수 없습니다.");
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/** "direct" / "auto" → "text" 변환 (legacy 호출처 보호 — system-normalize 동작) */
|
||||
|
||||
@@ -279,11 +279,9 @@ export default function TableManagementPage() {
|
||||
if (response.success && response.data) {
|
||||
setSecondLevelMenus(response.data);
|
||||
} else {
|
||||
console.warn("⚠️ 2레벨 메뉴 로드 실패:", response);
|
||||
setSecondLevelMenus([]); // 빈 배열로 설정하여 로딩 상태 해제
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 2레벨 메뉴 로드 에러:", error);
|
||||
setSecondLevelMenus([]); // 에러 발생 시에도 빈 배열로 설정
|
||||
}
|
||||
};
|
||||
@@ -326,12 +324,6 @@ export default function TableManagementPage() {
|
||||
if (response.data.success) {
|
||||
const data = response.data.data;
|
||||
|
||||
console.log("📥 원본 API 응답:", {
|
||||
hasColumns: !!(data.columns || data),
|
||||
firstColumn: (data.columns || data)[0],
|
||||
statusColumn: (data.columns || data).find((col: any) => col.column_name === "status"),
|
||||
});
|
||||
|
||||
// 컬럼 데이터에 기본값 설정
|
||||
const processedColumns = (data.columns || data).map((col: any) => {
|
||||
let hierarchyRole: "large" | "medium" | "small" | undefined = undefined;
|
||||
@@ -648,7 +640,6 @@ export default function TableManagementPage() {
|
||||
};
|
||||
|
||||
finalDetailSettings = JSON.stringify(entitySettings);
|
||||
console.log("🔧 Entity 설정 JSON 생성:", entitySettings);
|
||||
}
|
||||
|
||||
// 🎯 Code 타입인 경우 hierarchyRole을 detailSettings에 포함
|
||||
@@ -668,7 +659,6 @@ export default function TableManagementPage() {
|
||||
};
|
||||
|
||||
finalDetailSettings = JSON.stringify(codeSettings);
|
||||
console.log("🔧 Code 계층 역할 설정 JSON 생성:", codeSettings);
|
||||
}
|
||||
|
||||
const columnSetting = {
|
||||
@@ -686,49 +676,22 @@ export default function TableManagementPage() {
|
||||
|
||||
// console.log("저장할 컬럼 설정:", columnSetting);
|
||||
|
||||
console.log("💾 저장할 컬럼 정보:", {
|
||||
columnName: column.column_name,
|
||||
inputType: column.input_type,
|
||||
categoryMenus: column.category_menus,
|
||||
hasCategoryMenus: !!column.category_menus,
|
||||
categoryMenusLength: column.category_menus?.length || 0,
|
||||
});
|
||||
|
||||
const response = await apiClient.post(`/table-management/tables/${selectedTable}/columns/settings`, [
|
||||
columnSetting,
|
||||
]);
|
||||
|
||||
if (response.data.success) {
|
||||
console.log("✅ 컬럼 설정 저장 성공");
|
||||
|
||||
// 🆕 Category 타입인 경우 컬럼 매핑 처리
|
||||
console.log("🔍 카테고리 조건 체크:", {
|
||||
isCategory: column.input_type === "category",
|
||||
hasCategoryMenus: !!column.category_menus,
|
||||
length: column.category_menus?.length || 0,
|
||||
});
|
||||
|
||||
if (column.input_type === "category" && !column.category_ref) {
|
||||
// 참조가 아닌 자체 카테고리만 메뉴 매핑 처리
|
||||
console.log("기존 카테고리 메뉴 매핑 삭제 시작:", {
|
||||
tableName: selectedTable,
|
||||
columnName: column.column_name,
|
||||
});
|
||||
|
||||
try {
|
||||
const deleteResponse = await deleteColumnMappingsByColumn(selectedTable, column.column_name);
|
||||
console.log("🗑️ 기존 매핑 삭제 결과:", deleteResponse);
|
||||
await deleteColumnMappingsByColumn(selectedTable, column.column_name);
|
||||
} catch (error) {
|
||||
console.error("❌ 기존 매핑 삭제 실패:", error);
|
||||
}
|
||||
|
||||
// 2. 새로운 매핑 추가 (선택된 메뉴가 있는 경우만)
|
||||
if (column.category_menus && column.category_menus.length > 0) {
|
||||
console.log("📥 카테고리 메뉴 매핑 시작:", {
|
||||
columnName: column.column_name,
|
||||
categoryMenus: column.category_menus,
|
||||
count: column.category_menus.length,
|
||||
});
|
||||
|
||||
// 직렬 await 대신 Promise.allSettled 로 병렬 호출 (메뉴가 많으면 직렬은 수십 초 멈춤)
|
||||
const mappingResults = await Promise.allSettled(
|
||||
@@ -890,29 +853,14 @@ export default function TableManagementPage() {
|
||||
// 자체 카테고리 컬럼만 메뉴 매핑 처리 (참조 컬럼 제외)
|
||||
const categoryColumns = columns.filter((col) => col.input_type === "category" && !col.category_ref);
|
||||
|
||||
console.log("📥 전체 저장: 카테고리 컬럼 확인", {
|
||||
totalColumns: columns.length,
|
||||
categoryColumns: categoryColumns.length,
|
||||
categoryColumnsData: categoryColumns.map((col) => ({
|
||||
columnName: col.column_name,
|
||||
categoryMenus: col.category_menus,
|
||||
})),
|
||||
});
|
||||
|
||||
if (categoryColumns.length > 0) {
|
||||
let totalSuccessCount = 0;
|
||||
let totalFailCount = 0;
|
||||
|
||||
for (const column of categoryColumns) {
|
||||
// 1. 먼저 기존 매핑 모두 삭제
|
||||
console.log("🗑️ 기존 카테고리 메뉴 매핑 삭제:", {
|
||||
tableName: selectedTable,
|
||||
columnName: column.column_name,
|
||||
});
|
||||
|
||||
try {
|
||||
const deleteResponse = await deleteColumnMappingsByColumn(selectedTable, column.column_name);
|
||||
console.log("🗑️ 기존 매핑 삭제 결과:", deleteResponse);
|
||||
await deleteColumnMappingsByColumn(selectedTable, column.column_name);
|
||||
} catch (error) {
|
||||
console.error("❌ 기존 매핑 삭제 실패:", error);
|
||||
}
|
||||
@@ -938,8 +886,6 @@ export default function TableManagementPage() {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("📊 전체 매핑 결과:", { totalSuccessCount, totalFailCount });
|
||||
|
||||
if (totalSuccessCount > 0) {
|
||||
toast.success(`테이블 설정 및 ${totalSuccessCount}개 카테고리 메뉴 매핑이 저장되었습니다.`);
|
||||
} else if (totalFailCount > 0) {
|
||||
@@ -1714,13 +1660,21 @@ export default function TableManagementPage() {
|
||||
</div>
|
||||
) : (
|
||||
<Tabs defaultValue="columns" className="flex min-h-0 flex-1 flex-col">
|
||||
<TabsList className="h-auto w-full shrink-0 justify-start gap-1 rounded-none border-b bg-transparent p-0">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"h-9 w-full shrink-0 justify-start gap-1 rounded-none bg-transparent p-0 px-2 pt-1",
|
||||
"border-b border-border",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger
|
||||
value="columns"
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-none border-b-2 border-transparent bg-transparent px-4 py-2.5 text-sm font-medium text-muted-foreground transition-colors",
|
||||
"hover:text-foreground",
|
||||
"data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:text-primary data-[state=active]:shadow-none",
|
||||
"flex flex-none items-center gap-2 rounded-t-md rounded-b-none border border-border bg-transparent -mb-px",
|
||||
"px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors",
|
||||
"hover:bg-muted/40 hover:text-foreground",
|
||||
"data-[state=active]:bg-card data-[state=active]:text-primary data-[state=active]:font-semibold",
|
||||
"data-[state=active]:border-b-card",
|
||||
"data-[state=active]:shadow-[inset_0_2px_0_hsl(var(--primary))]",
|
||||
)}
|
||||
>
|
||||
<Columns3 className="h-4 w-4" />
|
||||
@@ -1729,9 +1683,12 @@ export default function TableManagementPage() {
|
||||
<TabsTrigger
|
||||
value="references"
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-none border-b-2 border-transparent bg-transparent px-4 py-2.5 text-sm font-medium text-muted-foreground transition-colors",
|
||||
"hover:text-foreground",
|
||||
"data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:text-primary data-[state=active]:shadow-none",
|
||||
"flex flex-none items-center gap-2 rounded-t-md rounded-b-none border border-border bg-transparent -mb-px",
|
||||
"px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors",
|
||||
"hover:bg-muted/40 hover:text-foreground",
|
||||
"data-[state=active]:bg-card data-[state=active]:text-primary data-[state=active]:font-semibold",
|
||||
"data-[state=active]:border-b-card",
|
||||
"data-[state=active]:shadow-[inset_0_2px_0_hsl(var(--primary))]",
|
||||
)}
|
||||
>
|
||||
<Link2 className="h-4 w-4" />
|
||||
|
||||
@@ -19,8 +19,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Loader2, Info, AlertCircle, CheckCircle2, Plus, Activity } from "lucide-react";
|
||||
import { Loader2, Info, AlertCircle, CheckCircle2, Plus } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { ColumnDefinitionTable } from "./ColumnDefinitionTable";
|
||||
import { ddlApi } from "../../lib/api/ddl";
|
||||
@@ -57,8 +56,6 @@ export function CreateTableModal({
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [tableNameError, setTableNameError] = useState("");
|
||||
const [validationResult, setValidationResult] = useState<any>(null);
|
||||
const [useLogTable, setUseLogTable] = useState(false);
|
||||
|
||||
/**
|
||||
* 모달 리셋
|
||||
*/
|
||||
@@ -76,7 +73,6 @@ export function CreateTableModal({
|
||||
]);
|
||||
setTableNameError("");
|
||||
setValidationResult(null);
|
||||
setUseLogTable(false);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -107,15 +103,11 @@ export function CreateTableModal({
|
||||
try {
|
||||
// 1. 테이블 컬럼 정보 조회
|
||||
const columnsResponse = await tableManagementApi.getColumnList(tableName);
|
||||
|
||||
console.log("🔍 컬럼 조회 응답:", columnsResponse);
|
||||
|
||||
|
||||
if (columnsResponse.success && columnsResponse.data) {
|
||||
// API는 { columns, total, page, size } 형태로 반환
|
||||
const columnsList = columnsResponse.data.columns;
|
||||
|
||||
console.log("🔍 컬럼 리스트:", columnsList);
|
||||
|
||||
|
||||
if (columnsList && columnsList.length > 0) {
|
||||
// 첫 번째 컬럼에서 테이블 설명 가져오기 (모든 컬럼이 같은 테이블 설명을 가짐)
|
||||
const firstColumn = columnsList[0];
|
||||
@@ -285,23 +277,6 @@ export function CreateTableModal({
|
||||
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
|
||||
// 로그 테이블 생성 옵션이 선택되었다면 로그 테이블 생성
|
||||
if (useLogTable) {
|
||||
try {
|
||||
const pkColumn = { columnName: "id", dataType: "integer" };
|
||||
const logResult = await tableManagementApi.createLogTable(tableName, pkColumn);
|
||||
|
||||
if (logResult.success) {
|
||||
toast.success(`${tableName}_log 테이블이 생성되었습니다.`);
|
||||
} else {
|
||||
toast.warning(`테이블은 생성되었으나 로그 테이블 생성 실패: ${logResult.message}`);
|
||||
}
|
||||
} catch (logError) {
|
||||
toast.warning("테이블은 생성되었으나 로그 테이블 생성 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
onSuccess(result);
|
||||
onClose();
|
||||
} else {
|
||||
@@ -380,29 +355,6 @@ export function CreateTableModal({
|
||||
<ColumnDefinitionTable columns={columns} onChange={setColumns} disabled={loading} />
|
||||
</div>
|
||||
|
||||
{/* 로그 테이블 생성 옵션 - 통합 변경 이력 시스템으로 대체됨 (숨김 처리) */}
|
||||
{/* <div className="flex items-start space-x-3 rounded-lg border p-4">
|
||||
<Checkbox
|
||||
id="useLogTable"
|
||||
checked={useLogTable}
|
||||
onCheckedChange={(checked) => setUseLogTable(checked as boolean)}
|
||||
disabled={loading}
|
||||
/>
|
||||
<div className="grid gap-1.5 leading-none">
|
||||
<label
|
||||
htmlFor="useLogTable"
|
||||
className="flex cursor-pointer items-center gap-2 text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
<Activity className="h-4 w-4" />
|
||||
변경 이력 로그 테이블 생성
|
||||
</label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
선택 시 <code className="bg-muted rounded px-1 py-0.5">{tableName || "table"}_log</code> 테이블이
|
||||
자동으로 생성되어 INSERT/UPDATE/DELETE 변경 이력을 기록합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* 자동 추가 컬럼 안내 */}
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
|
||||
@@ -108,9 +108,23 @@ export const USER_SELECTABLE_INPUT_TYPE_COLORS = USER_SELECTABLE_INPUT_TYPE_ORDE
|
||||
{} as Record<string, TypeColorConfig>,
|
||||
);
|
||||
|
||||
/** 컬럼 그룹 판별 */
|
||||
/** 컬럼 그룹 판별 — 시스템 자동 생성 컬럼은 meta 로 분류 (사용자가 거의 수정하지 않으므로 시각 분리) */
|
||||
export function getColumnGroup(col: ColumnTypeInfo): ColumnGroup {
|
||||
const metaCols = ["id", "created_date", "updated_date", "writer", "company_code"];
|
||||
// 시스템 컬럼: invyone 자동 생성 (id/날짜/작성자/회사) 외에 VEX 계승 (objid), 멀티테넌트 (tenant_id),
|
||||
// 수정자/생성자 변형 (creator/modifier/created_at/updated_at) 까지 모두 포함
|
||||
const metaCols = [
|
||||
"id",
|
||||
"objid",
|
||||
"tenant_id",
|
||||
"created_date",
|
||||
"updated_date",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"writer",
|
||||
"creator",
|
||||
"modifier",
|
||||
"company_code",
|
||||
];
|
||||
if (metaCols.includes(col.column_name)) return "meta";
|
||||
if (["entity", "code", "category"].includes(col.input_type)) return "reference";
|
||||
return "basic";
|
||||
|
||||
Reference in New Issue
Block a user