phase 2.3 테이블 및 컬럼 동적생성기능 변경
This commit is contained in:
parent
c8c05f1c0d
commit
3c06d35374
|
|
@ -11,9 +11,9 @@ DDLExecutionService는 **4개의 Prisma 호출**이 있으며, DDL(Data Definiti
|
|||
| 파일 위치 | `backend-node/src/services/ddlExecutionService.ts` |
|
||||
| 파일 크기 | 400+ 라인 |
|
||||
| Prisma 호출 | 4개 |
|
||||
| **현재 진행률** | **0/4 (0%)** ⏳ **진행 예정** |
|
||||
| **현재 진행률** | **6/6 (100%)** ✅ **완료** |
|
||||
| 복잡도 | 중간 (DDL 실행 + 로그 관리) |
|
||||
| 우선순위 | 🟢 낮음 (Phase 2.7) |
|
||||
| 우선순위 | 🔴 최우선 (테이블 추가 기능 - Phase 2.3으로 변경) |
|
||||
|
||||
### 🎯 전환 목표
|
||||
|
||||
|
|
|
|||
|
|
@ -1067,35 +1067,43 @@ describe("Performance Benchmarks", () => {
|
|||
|
||||
#### ✅ 완료된 서비스
|
||||
|
||||
- [x] **ScreenManagementService 전환 (46개)** ✅ **완료**
|
||||
- [x] **ScreenManagementService 전환 (46개)** ✅ **완료** (Phase 2.1)
|
||||
|
||||
- [x] 46개 Prisma 호출 전환 완료
|
||||
- [x] 18개 단위 테스트 통과
|
||||
- [x] 6개 통합 테스트 작성 완료
|
||||
- [x] 실제 운영 버그 발견 및 수정 (소수점 좌표)
|
||||
- 📄 **[PHASE2_SCREEN_MANAGEMENT_MIGRATION.md](PHASE2_SCREEN_MANAGEMENT_MIGRATION.md)**
|
||||
|
||||
- [x] **TableManagementService 전환 (33개)** ✅ **완료** (Phase 2.2)
|
||||
|
||||
- [x] 33개 Prisma 호출 전환 완료 ($queryRaw 26개 + ORM 7개)
|
||||
- [x] 단위 테스트 작성 완료
|
||||
- [x] Prisma import 완전 제거
|
||||
- 📄 **[PHASE2.2_TABLE_MANAGEMENT_MIGRATION.md](PHASE2.2_TABLE_MANAGEMENT_MIGRATION.md)**
|
||||
|
||||
- [x] **DDLExecutionService 전환 (6개)** ✅ **완료** (Phase 2.3)
|
||||
- [x] 6개 Prisma 호출 전환 완료 (트랜잭션 2개 + $queryRawUnsafe 2개 + ORM 2개)
|
||||
- [x] **테이블 동적 생성/수정/삭제 기능 완료**
|
||||
- [x] ✅ 단위 테스트 8개 모두 통과
|
||||
- [x] Prisma import 완전 제거
|
||||
- 📄 **[PHASE2.7_DDL_EXECUTION_MIGRATION.md](PHASE2.7_DDL_EXECUTION_MIGRATION.md)**
|
||||
|
||||
#### ⏳ 진행 예정 서비스
|
||||
|
||||
- [ ] **TableManagementService 전환 (33개)** - Phase 2.2 🟡 중간 우선순위
|
||||
- 33개 Prisma 호출 ($queryRaw 26개 + ORM 7개)
|
||||
- SQL은 79% 작성 완료 → `query()` 함수로 교체만 필요
|
||||
- 📄 **[PHASE2.2_TABLE_MANAGEMENT_MIGRATION.md](PHASE2.2_TABLE_MANAGEMENT_MIGRATION.md)**
|
||||
- [ ] **DataflowService 전환 (31개)** - Phase 2.3 🔴 최우선
|
||||
- [ ] **DataflowService 전환 (31개)** - Phase 2.4 🟡 중간 우선순위
|
||||
- 31개 Prisma 호출 (복잡한 관계 관리)
|
||||
- 📄 **[PHASE2.3_DATAFLOW_SERVICE_MIGRATION.md](PHASE2.3_DATAFLOW_SERVICE_MIGRATION.md)**
|
||||
- [ ] **DynamicFormService 전환 (13개)** - Phase 2.4 🟢 낮은 우선순위
|
||||
- [ ] **DynamicFormService 전환 (13개)** - Phase 2.5 🟢 낮은 우선순위
|
||||
- 13개 Prisma 호출 ($queryRaw 11개 + ORM 2개)
|
||||
- SQL은 85% 작성 완료 → `query()` 함수로 교체만 필요
|
||||
- 📄 **[PHASE2.4_DYNAMIC_FORM_MIGRATION.md](PHASE2.4_DYNAMIC_FORM_MIGRATION.md)**
|
||||
- [ ] **ExternalDbConnectionService 전환 (15개)** - Phase 2.5 🟡 중간 우선순위
|
||||
- [ ] **ExternalDbConnectionService 전환 (15개)** - Phase 2.6 🟡 중간 우선순위
|
||||
- 15개 Prisma 호출 (외부 DB 연결 관리)
|
||||
- 📄 **[PHASE2.5_EXTERNAL_DB_CONNECTION_MIGRATION.md](PHASE2.5_EXTERNAL_DB_CONNECTION_MIGRATION.md)**
|
||||
- [ ] **DataflowControlService 전환 (6개)** - Phase 2.6 🟡 중간 우선순위
|
||||
- [ ] **DataflowControlService 전환 (6개)** - Phase 2.7 🟡 중간 우선순위
|
||||
- 6개 Prisma 호출 (복잡한 비즈니스 로직)
|
||||
- 📄 **[PHASE2.6_DATAFLOW_CONTROL_MIGRATION.md](PHASE2.6_DATAFLOW_CONTROL_MIGRATION.md)**
|
||||
- [ ] **DDLExecutionService 전환 (4개)** - Phase 2.7 🟢 낮은 우선순위
|
||||
- 4개 Prisma 호출 (DDL 실행 및 로그)
|
||||
- 📄 **[PHASE2.7_DDL_EXECUTION_MIGRATION.md](PHASE2.7_DDL_EXECUTION_MIGRATION.md)**
|
||||
|
||||
#### ✅ 다른 Phase로 이동
|
||||
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ export const requireSuperAdmin = (
|
|||
return;
|
||||
}
|
||||
|
||||
// 슈퍼관리자 권한 확인 (회사코드가 '*'이고 plm_admin 사용자)
|
||||
if (req.user.companyCode !== "*" || req.user.userId !== "plm_admin") {
|
||||
// 슈퍼관리자 권한 확인 (회사코드가 '*'인 사용자)
|
||||
if (req.user.companyCode !== "*") {
|
||||
logger.warn("DDL 실행 시도 - 권한 부족", {
|
||||
userId: req.user.userId,
|
||||
companyCode: req.user.companyCode,
|
||||
|
|
@ -62,7 +62,7 @@ export const requireSuperAdmin = (
|
|||
error: {
|
||||
code: "SUPER_ADMIN_REQUIRED",
|
||||
details:
|
||||
"최고 관리자 권한이 필요합니다. DDL 실행은 회사코드가 '*'인 plm_admin 사용자만 가능합니다.",
|
||||
"최고 관리자 권한이 필요합니다. DDL 실행은 회사코드가 '*'인 사용자만 가능합니다.",
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
|
@ -167,7 +167,7 @@ export const validateDDLPermission = (
|
|||
* 사용자가 슈퍼관리자인지 확인하는 유틸리티 함수
|
||||
*/
|
||||
export const isSuperAdmin = (user: AuthenticatedRequest["user"]): boolean => {
|
||||
return user?.companyCode === "*" && user?.userId === "plm_admin";
|
||||
return user?.companyCode === "*";
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* 실제 PostgreSQL 테이블 및 컬럼 생성을 담당
|
||||
*/
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { query, queryOne, transaction } from "../database/db";
|
||||
import {
|
||||
CreateColumnDefinition,
|
||||
DDLExecutionResult,
|
||||
|
|
@ -15,8 +15,6 @@ import { DDLAuditLogger } from "./ddlAuditLogger";
|
|||
import { logger } from "../utils/logger";
|
||||
import { cache, CacheKeys } from "../utils/cache";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export class DDLExecutionService {
|
||||
/**
|
||||
* 새 테이블 생성
|
||||
|
|
@ -98,15 +96,15 @@ export class DDLExecutionService {
|
|||
const ddlQuery = this.generateCreateTableQuery(tableName, columns);
|
||||
|
||||
// 5. 트랜잭션으로 안전하게 실행
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await transaction(async (client) => {
|
||||
// 5-1. 테이블 생성
|
||||
await tx.$executeRawUnsafe(ddlQuery);
|
||||
await client.query(ddlQuery);
|
||||
|
||||
// 5-2. 테이블 메타데이터 저장
|
||||
await this.saveTableMetadata(tx, tableName, description);
|
||||
await this.saveTableMetadata(client, tableName, description);
|
||||
|
||||
// 5-3. 컬럼 메타데이터 저장
|
||||
await this.saveColumnMetadata(tx, tableName, columns);
|
||||
await this.saveColumnMetadata(client, tableName, columns);
|
||||
});
|
||||
|
||||
// 6. 성공 로그 기록
|
||||
|
|
@ -269,12 +267,12 @@ export class DDLExecutionService {
|
|||
const ddlQuery = this.generateAddColumnQuery(tableName, column);
|
||||
|
||||
// 6. 트랜잭션으로 안전하게 실행
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await transaction(async (client) => {
|
||||
// 6-1. 컬럼 추가
|
||||
await tx.$executeRawUnsafe(ddlQuery);
|
||||
await client.query(ddlQuery);
|
||||
|
||||
// 6-2. 컬럼 메타데이터 저장
|
||||
await this.saveColumnMetadata(tx, tableName, [column]);
|
||||
await this.saveColumnMetadata(client, tableName, [column]);
|
||||
});
|
||||
|
||||
// 7. 성공 로그 기록
|
||||
|
|
@ -424,51 +422,42 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
|||
* 테이블 메타데이터 저장
|
||||
*/
|
||||
private async saveTableMetadata(
|
||||
tx: any,
|
||||
client: any,
|
||||
tableName: string,
|
||||
description?: string
|
||||
): Promise<void> {
|
||||
await tx.table_labels.upsert({
|
||||
where: { table_name: tableName },
|
||||
update: {
|
||||
table_label: tableName,
|
||||
description: description || `사용자 생성 테이블: ${tableName}`,
|
||||
updated_date: new Date(),
|
||||
},
|
||||
create: {
|
||||
table_name: tableName,
|
||||
table_label: tableName,
|
||||
description: description || `사용자 생성 테이블: ${tableName}`,
|
||||
created_date: new Date(),
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
|
||||
VALUES ($1, $2, $3, now(), now())
|
||||
ON CONFLICT (table_name)
|
||||
DO UPDATE SET
|
||||
table_label = $2,
|
||||
description = $3,
|
||||
updated_date = now()
|
||||
`,
|
||||
[tableName, tableName, description || `사용자 생성 테이블: ${tableName}`]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 컬럼 메타데이터 저장
|
||||
*/
|
||||
private async saveColumnMetadata(
|
||||
tx: any,
|
||||
client: any,
|
||||
tableName: string,
|
||||
columns: CreateColumnDefinition[]
|
||||
): Promise<void> {
|
||||
// 먼저 table_labels에 테이블 정보가 있는지 확인하고 없으면 생성
|
||||
await tx.table_labels.upsert({
|
||||
where: {
|
||||
table_name: tableName,
|
||||
},
|
||||
update: {
|
||||
updated_date: new Date(),
|
||||
},
|
||||
create: {
|
||||
table_name: tableName,
|
||||
table_label: tableName,
|
||||
description: `자동 생성된 테이블 메타데이터: ${tableName}`,
|
||||
created_date: new Date(),
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
|
||||
VALUES ($1, $2, $3, now(), now())
|
||||
ON CONFLICT (table_name)
|
||||
DO UPDATE SET updated_date = now()
|
||||
`,
|
||||
[tableName, tableName, `자동 생성된 테이블 메타데이터: ${tableName}`]
|
||||
);
|
||||
|
||||
// 기본 컬럼들 정의 (모든 테이블에 자동으로 추가되는 시스템 컬럼)
|
||||
const defaultColumns = [
|
||||
|
|
@ -516,20 +505,23 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
|||
|
||||
// 기본 컬럼들을 table_type_columns에 등록
|
||||
for (const defaultCol of defaultColumns) {
|
||||
await tx.$executeRaw`
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO table_type_columns (
|
||||
table_name, column_name, input_type, detail_settings,
|
||||
is_nullable, display_order, created_date, updated_date
|
||||
) VALUES (
|
||||
${tableName}, ${defaultCol.name}, ${defaultCol.inputType}, '{}',
|
||||
'Y', ${defaultCol.order}, now(), now()
|
||||
$1, $2, $3, '{}',
|
||||
'Y', $4, now(), now()
|
||||
)
|
||||
ON CONFLICT (table_name, column_name)
|
||||
DO UPDATE SET
|
||||
input_type = ${defaultCol.inputType},
|
||||
display_order = ${defaultCol.order},
|
||||
updated_date = now();
|
||||
`;
|
||||
input_type = $3,
|
||||
display_order = $4,
|
||||
updated_date = now()
|
||||
`,
|
||||
[tableName, defaultCol.name, defaultCol.inputType, defaultCol.order]
|
||||
);
|
||||
}
|
||||
|
||||
// 사용자 정의 컬럼들을 table_type_columns에 등록
|
||||
|
|
@ -538,89 +530,98 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
|||
const inputType = this.convertWebTypeToInputType(
|
||||
column.webType || "text"
|
||||
);
|
||||
const detailSettings = JSON.stringify(column.detailSettings || {});
|
||||
|
||||
await tx.$executeRaw`
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO table_type_columns (
|
||||
table_name, column_name, input_type, detail_settings,
|
||||
is_nullable, display_order, created_date, updated_date
|
||||
) VALUES (
|
||||
${tableName}, ${column.name}, ${inputType}, ${JSON.stringify(column.detailSettings || {})},
|
||||
'Y', ${i}, now(), now()
|
||||
$1, $2, $3, $4,
|
||||
'Y', $5, now(), now()
|
||||
)
|
||||
ON CONFLICT (table_name, column_name)
|
||||
DO UPDATE SET
|
||||
input_type = ${inputType},
|
||||
detail_settings = ${JSON.stringify(column.detailSettings || {})},
|
||||
display_order = ${i},
|
||||
updated_date = now();
|
||||
`;
|
||||
input_type = $3,
|
||||
detail_settings = $4,
|
||||
display_order = $5,
|
||||
updated_date = now()
|
||||
`,
|
||||
[tableName, column.name, inputType, detailSettings, i]
|
||||
);
|
||||
}
|
||||
|
||||
// 레거시 지원: column_labels 테이블에도 등록 (기존 시스템 호환성)
|
||||
// 1. 기본 컬럼들을 column_labels에 등록
|
||||
for (const defaultCol of defaultColumns) {
|
||||
await tx.column_labels.upsert({
|
||||
where: {
|
||||
table_name_column_name: {
|
||||
table_name: tableName,
|
||||
column_name: defaultCol.name,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
column_label: defaultCol.label,
|
||||
input_type: defaultCol.inputType,
|
||||
detail_settings: JSON.stringify({}),
|
||||
description: defaultCol.description,
|
||||
display_order: defaultCol.order,
|
||||
is_visible: defaultCol.isVisible,
|
||||
updated_date: new Date(),
|
||||
},
|
||||
create: {
|
||||
table_name: tableName,
|
||||
column_name: defaultCol.name,
|
||||
column_label: defaultCol.label,
|
||||
input_type: defaultCol.inputType,
|
||||
detail_settings: JSON.stringify({}),
|
||||
description: defaultCol.description,
|
||||
display_order: defaultCol.order,
|
||||
is_visible: defaultCol.isVisible,
|
||||
created_date: new Date(),
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO column_labels (
|
||||
table_name, column_name, column_label, input_type, detail_settings,
|
||||
description, display_order, is_visible, created_date, updated_date
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, now(), now()
|
||||
)
|
||||
ON CONFLICT (table_name, column_name)
|
||||
DO UPDATE SET
|
||||
column_label = $3,
|
||||
input_type = $4,
|
||||
detail_settings = $5,
|
||||
description = $6,
|
||||
display_order = $7,
|
||||
is_visible = $8,
|
||||
updated_date = now()
|
||||
`,
|
||||
[
|
||||
tableName,
|
||||
defaultCol.name,
|
||||
defaultCol.label,
|
||||
defaultCol.inputType,
|
||||
JSON.stringify({}),
|
||||
defaultCol.description,
|
||||
defaultCol.order,
|
||||
defaultCol.isVisible,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 사용자 정의 컬럼들을 column_labels에 등록
|
||||
for (const column of columns) {
|
||||
await tx.column_labels.upsert({
|
||||
where: {
|
||||
table_name_column_name: {
|
||||
table_name: tableName,
|
||||
column_name: column.name,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
column_label: column.label || column.name,
|
||||
input_type: this.convertWebTypeToInputType(column.webType || "text"),
|
||||
detail_settings: JSON.stringify(column.detailSettings || {}),
|
||||
description: column.description,
|
||||
display_order: column.order || 0,
|
||||
is_visible: true,
|
||||
updated_date: new Date(),
|
||||
},
|
||||
create: {
|
||||
table_name: tableName,
|
||||
column_name: column.name,
|
||||
column_label: column.label || column.name,
|
||||
input_type: this.convertWebTypeToInputType(column.webType || "text"),
|
||||
detail_settings: JSON.stringify(column.detailSettings || {}),
|
||||
description: column.description,
|
||||
display_order: column.order || 0,
|
||||
is_visible: true,
|
||||
created_date: new Date(),
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
const inputType = this.convertWebTypeToInputType(
|
||||
column.webType || "text"
|
||||
);
|
||||
const detailSettings = JSON.stringify(column.detailSettings || {});
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO column_labels (
|
||||
table_name, column_name, column_label, input_type, detail_settings,
|
||||
description, display_order, is_visible, created_date, updated_date
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, now(), now()
|
||||
)
|
||||
ON CONFLICT (table_name, column_name)
|
||||
DO UPDATE SET
|
||||
column_label = $3,
|
||||
input_type = $4,
|
||||
detail_settings = $5,
|
||||
description = $6,
|
||||
display_order = $7,
|
||||
is_visible = $8,
|
||||
updated_date = now()
|
||||
`,
|
||||
[
|
||||
tableName,
|
||||
column.name,
|
||||
column.label || column.name,
|
||||
inputType,
|
||||
detailSettings,
|
||||
column.description,
|
||||
column.order || 0,
|
||||
true,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -679,18 +680,18 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
|||
*/
|
||||
private async checkTableExists(tableName: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
const result = await queryOne<{ exists: boolean }>(
|
||||
`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = $1
|
||||
);
|
||||
)
|
||||
`,
|
||||
tableName
|
||||
[tableName]
|
||||
);
|
||||
|
||||
return (result as any)[0]?.exists || false;
|
||||
return result?.exists || false;
|
||||
} catch (error) {
|
||||
logger.error("테이블 존재 확인 오류:", error);
|
||||
return false;
|
||||
|
|
@ -705,20 +706,19 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
|||
columnName: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
const result = await queryOne<{ exists: boolean }>(
|
||||
`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = $1
|
||||
AND column_name = $2
|
||||
);
|
||||
)
|
||||
`,
|
||||
tableName,
|
||||
columnName
|
||||
[tableName, columnName]
|
||||
);
|
||||
|
||||
return (result as any)[0]?.exists || false;
|
||||
return result?.exists || false;
|
||||
} catch (error) {
|
||||
logger.error("컬럼 존재 확인 오류:", error);
|
||||
return false;
|
||||
|
|
@ -734,15 +734,16 @@ CREATE TABLE "${tableName}" (${baseColumns},
|
|||
} | null> {
|
||||
try {
|
||||
// 테이블 정보 조회
|
||||
const tableInfo = await prisma.table_labels.findUnique({
|
||||
where: { table_name: tableName },
|
||||
});
|
||||
const tableInfo = await queryOne(
|
||||
`SELECT * FROM table_labels WHERE table_name = $1`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
// 컬럼 정보 조회
|
||||
const columns = await prisma.column_labels.findMany({
|
||||
where: { table_name: tableName },
|
||||
orderBy: { display_order: "asc" },
|
||||
});
|
||||
const columns = await query(
|
||||
`SELECT * FROM column_labels WHERE table_name = $1 ORDER BY display_order ASC`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
if (!tableInfo) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -76,8 +76,8 @@ export default function TableManagementPage() {
|
|||
const [addColumnModalOpen, setAddColumnModalOpen] = useState(false);
|
||||
const [ddlLogViewerOpen, setDdlLogViewerOpen] = useState(false);
|
||||
|
||||
// 최고 관리자 여부 확인
|
||||
const isSuperAdmin = user?.companyCode === "*" && user?.userId === "plm_admin";
|
||||
// 최고 관리자 여부 확인 (회사코드가 "*"인 경우)
|
||||
const isSuperAdmin = user?.companyCode === "*";
|
||||
|
||||
// 다국어 텍스트 로드
|
||||
useEffect(() => {
|
||||
|
|
@ -541,9 +541,9 @@ export default function TableManagementPage() {
|
|||
}, [selectedTable, columns.length, totalColumns, columnsLoading, pageSize, loadColumnTypes]);
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
||||
<div className="w-full max-w-none space-y-8 px-4 py-8">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between rounded-lg border bg-white p-6 shadow-sm">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")}
|
||||
|
|
@ -593,7 +593,7 @@ export default function TableManagementPage() {
|
|||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-5">
|
||||
{/* 테이블 목록 */}
|
||||
<Card className="lg:col-span-1 shadow-sm">
|
||||
<Card className="shadow-sm lg:col-span-1">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5 text-gray-600" />
|
||||
|
|
@ -663,7 +663,7 @@ export default function TableManagementPage() {
|
|||
</Card>
|
||||
|
||||
{/* 컬럼 타입 관리 */}
|
||||
<Card className="lg:col-span-4 shadow-sm">
|
||||
<Card className="shadow-sm lg:col-span-4">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Settings className="h-5 w-5 text-gray-600" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue