테이블관리
This commit is contained in:
parent
ce130ee225
commit
eb1a6aa206
|
|
@ -0,0 +1,37 @@
|
||||||
|
const { Client } = require("pg");
|
||||||
|
require("dotenv/config");
|
||||||
|
|
||||||
|
async function checkActualPassword() {
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log("✅ 데이터베이스 연결 성공");
|
||||||
|
|
||||||
|
// 실제 저장된 비밀번호 확인 (암호화된 상태)
|
||||||
|
const passwordResult = await client.query(`
|
||||||
|
SELECT user_id, user_name, user_password, status
|
||||||
|
FROM user_info
|
||||||
|
WHERE user_id = 'kkh'
|
||||||
|
`);
|
||||||
|
console.log("🔐 사용자 비밀번호 정보:", passwordResult.rows);
|
||||||
|
|
||||||
|
// 다른 사용자도 확인
|
||||||
|
const otherUsersResult = await client.query(`
|
||||||
|
SELECT user_id, user_name, user_password, status
|
||||||
|
FROM user_info
|
||||||
|
WHERE user_password IS NOT NULL
|
||||||
|
AND user_password != ''
|
||||||
|
LIMIT 3
|
||||||
|
`);
|
||||||
|
console.log("👥 다른 사용자 비밀번호 정보:", otherUsersResult.rows);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 오류 발생:", error);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkActualPassword();
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
const { Client } = require("pg");
|
||||||
|
require("dotenv/config");
|
||||||
|
|
||||||
|
async function checkPasswordField() {
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log("✅ 데이터베이스 연결 성공");
|
||||||
|
|
||||||
|
// user_info 테이블의 컬럼 정보 확인
|
||||||
|
const columnsResult = await client.query(`
|
||||||
|
SELECT column_name, data_type, is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'user_info'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
`);
|
||||||
|
console.log("📋 user_info 테이블 컬럼:", columnsResult.rows);
|
||||||
|
|
||||||
|
// 비밀번호 관련 컬럼 확인
|
||||||
|
const passwordResult = await client.query(`
|
||||||
|
SELECT user_id, user_name, user_password, password, status
|
||||||
|
FROM user_info
|
||||||
|
WHERE user_id = 'kkh'
|
||||||
|
`);
|
||||||
|
console.log("🔐 사용자 비밀번호 정보:", passwordResult.rows);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 오류 발생:", error);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPasswordField();
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
const { Client } = require("pg");
|
||||||
|
require("dotenv/config");
|
||||||
|
|
||||||
|
async function createTestUser() {
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log("✅ 데이터베이스 연결 성공");
|
||||||
|
|
||||||
|
// 테스트용 사용자 생성 (MD5 해시: admin123)
|
||||||
|
const testUser = {
|
||||||
|
user_id: "admin",
|
||||||
|
user_name: "테스트 관리자",
|
||||||
|
user_password: "f21b1ce8b08dc955bd4afff71b3db1fc", // admin123의 MD5 해시
|
||||||
|
status: "active",
|
||||||
|
company_code: "ILSHIN",
|
||||||
|
data_type: "PLM",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 기존 사용자 확인
|
||||||
|
const existingUser = await client.query(
|
||||||
|
"SELECT user_id FROM user_info WHERE user_id = $1",
|
||||||
|
[testUser.user_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingUser.rows.length > 0) {
|
||||||
|
console.log("⚠️ 테스트 사용자가 이미 존재합니다:", testUser.user_id);
|
||||||
|
|
||||||
|
// 기존 사용자 정보 업데이트
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE user_info
|
||||||
|
SET user_name = $1, user_password = $2, status = $3
|
||||||
|
WHERE user_id = $4
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
testUser.user_name,
|
||||||
|
testUser.user_password,
|
||||||
|
testUser.status,
|
||||||
|
testUser.user_id,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("✅ 테스트 사용자 정보 업데이트 완료");
|
||||||
|
} else {
|
||||||
|
// 새 사용자 생성
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
INSERT INTO user_info (user_id, user_name, user_password, status, company_code, data_type)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
testUser.user_id,
|
||||||
|
testUser.user_name,
|
||||||
|
testUser.user_password,
|
||||||
|
testUser.status,
|
||||||
|
testUser.company_code,
|
||||||
|
testUser.data_type,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("✅ 테스트 사용자 생성 완료");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 생성된 사용자 확인
|
||||||
|
const createdUser = await client.query(
|
||||||
|
"SELECT user_id, user_name, status FROM user_info WHERE user_id = $1",
|
||||||
|
[testUser.user_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("👤 생성된 사용자:", createdUser.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 오류 발생:", error);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTestUser();
|
||||||
|
|
@ -8,6 +8,8 @@ datasource db {
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 테이블 타입관리 관련 모델은 이미 정의되어 있음 (line 11, 717)
|
||||||
|
|
||||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||||
model admin_supply_mng {
|
model admin_supply_mng {
|
||||||
objid Decimal @id @default(0) @db.Decimal
|
objid Decimal @id @default(0) @db.Decimal
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
const { Client } = require("pg");
|
||||||
|
|
||||||
|
async function createTestUser() {
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log("✅ 데이터베이스 연결 성공");
|
||||||
|
|
||||||
|
// 테스트용 사용자 생성
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO user_info (user_id, user_name, user_password, status, company_code, data_type)
|
||||||
|
VALUES ('admin', '테스트 관리자', 'f21b1ce8b08dc955bd4afff71b3db1fc', 'active', 'ILSHIN', 'PLM')
|
||||||
|
ON CONFLICT (user_id) DO UPDATE SET
|
||||||
|
user_name = EXCLUDED.user_name,
|
||||||
|
user_password = EXCLUDED.user_password,
|
||||||
|
status = EXCLUDED.status
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log("✅ 테스트 사용자 생성/업데이트 완료");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 오류 발생:", error);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTestUser();
|
||||||
|
|
@ -12,6 +12,7 @@ import { errorHandler } from "./middleware/errorHandler";
|
||||||
import authRoutes from "./routes/authRoutes";
|
import authRoutes from "./routes/authRoutes";
|
||||||
import adminRoutes from "./routes/adminRoutes";
|
import adminRoutes from "./routes/adminRoutes";
|
||||||
import multilangRoutes from "./routes/multilangRoutes";
|
import multilangRoutes from "./routes/multilangRoutes";
|
||||||
|
import tableManagementRoutes from "./routes/tableManagementRoutes";
|
||||||
// import userRoutes from './routes/userRoutes';
|
// import userRoutes from './routes/userRoutes';
|
||||||
// import menuRoutes from './routes/menuRoutes';
|
// import menuRoutes from './routes/menuRoutes';
|
||||||
|
|
||||||
|
|
@ -61,6 +62,7 @@ app.get("/health", (req, res) => {
|
||||||
app.use("/api/auth", authRoutes);
|
app.use("/api/auth", authRoutes);
|
||||||
app.use("/api/admin", adminRoutes);
|
app.use("/api/admin", adminRoutes);
|
||||||
app.use("/api/multilang", multilangRoutes);
|
app.use("/api/multilang", multilangRoutes);
|
||||||
|
app.use("/api/table-management", tableManagementRoutes);
|
||||||
// app.use('/api/users', userRoutes);
|
// app.use('/api/users', userRoutes);
|
||||||
// app.use('/api/menus', menuRoutes);
|
// app.use('/api/menus', menuRoutes);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,445 @@
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
|
import { ApiResponse } from "../types/common";
|
||||||
|
import { Client } from "pg";
|
||||||
|
import { TableManagementService } from "../services/tableManagementService";
|
||||||
|
import {
|
||||||
|
TableInfo,
|
||||||
|
ColumnTypeInfo,
|
||||||
|
ColumnSettings,
|
||||||
|
TableListResponse,
|
||||||
|
ColumnListResponse,
|
||||||
|
ColumnSettingsResponse,
|
||||||
|
} from "../types/tableManagement";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 목록 조회
|
||||||
|
*/
|
||||||
|
export async function getTableList(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info("=== 테이블 목록 조회 시작 ===");
|
||||||
|
|
||||||
|
// PostgreSQL 클라이언트 생성
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableManagementService = new TableManagementService(client);
|
||||||
|
const tableList = await tableManagementService.getTableList();
|
||||||
|
|
||||||
|
logger.info(`테이블 목록 조회 결과: ${tableList.length}개`);
|
||||||
|
|
||||||
|
const response: ApiResponse<TableInfo[]> = {
|
||||||
|
success: true,
|
||||||
|
message: "테이블 목록을 성공적으로 조회했습니다.",
|
||||||
|
data: tableList,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("테이블 목록 조회 중 오류 발생:", error);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블 목록 조회 중 오류가 발생했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "TABLE_LIST_ERROR",
|
||||||
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 컬럼 정보 조회
|
||||||
|
*/
|
||||||
|
export async function getColumnList(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { tableName } = req.params;
|
||||||
|
logger.info(`=== 컬럼 정보 조회 시작: ${tableName} ===`);
|
||||||
|
|
||||||
|
if (!tableName) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블명이 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_TABLE_NAME",
|
||||||
|
details: "테이블명 파라미터가 누락되었습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQL 클라이언트 생성
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableManagementService = new TableManagementService(client);
|
||||||
|
const columnList = await tableManagementService.getColumnList(tableName);
|
||||||
|
|
||||||
|
logger.info(`컬럼 정보 조회 결과: ${tableName}, ${columnList.length}개`);
|
||||||
|
|
||||||
|
const response: ApiResponse<ColumnTypeInfo[]> = {
|
||||||
|
success: true,
|
||||||
|
message: "컬럼 목록을 성공적으로 조회했습니다.",
|
||||||
|
data: columnList,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("컬럼 정보 조회 중 오류 발생:", error);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "컬럼 목록 조회 중 오류가 발생했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "COLUMN_LIST_ERROR",
|
||||||
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개별 컬럼 설정 업데이트
|
||||||
|
*/
|
||||||
|
export async function updateColumnSettings(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { tableName, columnName } = req.params;
|
||||||
|
const settings: ColumnSettings = req.body;
|
||||||
|
|
||||||
|
logger.info(`=== 컬럼 설정 업데이트 시작: ${tableName}.${columnName} ===`);
|
||||||
|
|
||||||
|
if (!tableName || !columnName) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블명과 컬럼명이 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_PARAMETERS",
|
||||||
|
details: "테이블명 또는 컬럼명 파라미터가 누락되었습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "컬럼 설정 정보가 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_SETTINGS",
|
||||||
|
details: "요청 본문에 컬럼 설정 정보가 누락되었습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQL 클라이언트 생성
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableManagementService = new TableManagementService(client);
|
||||||
|
await tableManagementService.updateColumnSettings(
|
||||||
|
tableName,
|
||||||
|
columnName,
|
||||||
|
settings
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`컬럼 설정 업데이트 완료: ${tableName}.${columnName}`);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: true,
|
||||||
|
message: "컬럼 설정을 성공적으로 저장했습니다.",
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("컬럼 설정 업데이트 중 오류 발생:", error);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "컬럼 설정 저장 중 오류가 발생했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "COLUMN_SETTINGS_UPDATE_ERROR",
|
||||||
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 컬럼 설정 일괄 업데이트
|
||||||
|
*/
|
||||||
|
export async function updateAllColumnSettings(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { tableName } = req.params;
|
||||||
|
const columnSettings: ColumnSettings[] = req.body;
|
||||||
|
|
||||||
|
logger.info(`=== 전체 컬럼 설정 일괄 업데이트 시작: ${tableName} ===`);
|
||||||
|
|
||||||
|
if (!tableName) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블명이 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_TABLE_NAME",
|
||||||
|
details: "테이블명 파라미터가 누락되었습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(columnSettings) || columnSettings.length === 0) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "컬럼 설정 목록이 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_COLUMN_SETTINGS",
|
||||||
|
details: "요청 본문에 컬럼 설정 목록이 누락되었습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQL 클라이언트 생성
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableManagementService = new TableManagementService(client);
|
||||||
|
await tableManagementService.updateAllColumnSettings(
|
||||||
|
tableName,
|
||||||
|
columnSettings
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}, ${columnSettings.length}개`
|
||||||
|
);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: true,
|
||||||
|
message: "모든 컬럼 설정을 성공적으로 저장했습니다.",
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("전체 컬럼 설정 일괄 업데이트 중 오류 발생:", error);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "컬럼 설정 저장 중 오류가 발생했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "ALL_COLUMN_SETTINGS_UPDATE_ERROR",
|
||||||
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 라벨 정보 조회
|
||||||
|
*/
|
||||||
|
export async function getTableLabels(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { tableName } = req.params;
|
||||||
|
logger.info(`=== 테이블 라벨 정보 조회 시작: ${tableName} ===`);
|
||||||
|
|
||||||
|
if (!tableName) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블명이 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_TABLE_NAME",
|
||||||
|
details: "테이블명 파라미터가 누락되었습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQL 클라이언트 생성
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableManagementService = new TableManagementService(client);
|
||||||
|
const tableLabels =
|
||||||
|
await tableManagementService.getTableLabels(tableName);
|
||||||
|
|
||||||
|
if (!tableLabels) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블 라벨 정보를 찾을 수 없습니다.",
|
||||||
|
error: {
|
||||||
|
code: "TABLE_LABELS_NOT_FOUND",
|
||||||
|
details: `테이블 ${tableName}의 라벨 정보가 존재하지 않습니다.`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(404).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`테이블 라벨 정보 조회 완료: ${tableName}`);
|
||||||
|
|
||||||
|
const response: ApiResponse<any> = {
|
||||||
|
success: true,
|
||||||
|
message: "테이블 라벨 정보를 성공적으로 조회했습니다.",
|
||||||
|
data: tableLabels,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("테이블 라벨 정보 조회 중 오류 발생:", error);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블 라벨 정보 조회 중 오류가 발생했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "TABLE_LABELS_ERROR",
|
||||||
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컬럼 라벨 정보 조회
|
||||||
|
*/
|
||||||
|
export async function getColumnLabels(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { tableName, columnName } = req.params;
|
||||||
|
logger.info(`=== 컬럼 라벨 정보 조회 시작: ${tableName}.${columnName} ===`);
|
||||||
|
|
||||||
|
if (!tableName || !columnName) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "테이블명과 컬럼명이 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_PARAMETERS",
|
||||||
|
details: "테이블명 또는 컬럼명 파라미터가 누락되었습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQL 클라이언트 생성
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tableManagementService = new TableManagementService(client);
|
||||||
|
const columnLabels = await tableManagementService.getColumnLabels(
|
||||||
|
tableName,
|
||||||
|
columnName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!columnLabels) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "컬럼 라벨 정보를 찾을 수 없습니다.",
|
||||||
|
error: {
|
||||||
|
code: "COLUMN_LABELS_NOT_FOUND",
|
||||||
|
details: `컬럼 ${tableName}.${columnName}의 라벨 정보가 존재하지 않습니다.`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(404).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`컬럼 라벨 정보 조회 완료: ${tableName}.${columnName}`);
|
||||||
|
|
||||||
|
const response: ApiResponse<any> = {
|
||||||
|
success: true,
|
||||||
|
message: "컬럼 라벨 정보를 성공적으로 조회했습니다.",
|
||||||
|
data: columnLabels,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("컬럼 라벨 정보 조회 중 오류 발생:", error);
|
||||||
|
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "컬럼 라벨 정보 조회 중 오류가 발생했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "COLUMN_LABELS_ERROR",
|
||||||
|
details: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import express from "express";
|
||||||
|
import { authenticateToken } from "../middleware/authMiddleware";
|
||||||
|
import {
|
||||||
|
getTableList,
|
||||||
|
getColumnList,
|
||||||
|
updateColumnSettings,
|
||||||
|
updateAllColumnSettings,
|
||||||
|
getTableLabels,
|
||||||
|
getColumnLabels,
|
||||||
|
} from "../controllers/tableManagementController";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// 모든 라우트에 인증 미들웨어 적용 (테스트 시에는 주석 처리)
|
||||||
|
// router.use(authenticateToken);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 목록 조회
|
||||||
|
* GET /api/table-management/tables
|
||||||
|
*/
|
||||||
|
router.get("/tables", getTableList);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 컬럼 정보 조회
|
||||||
|
* GET /api/table-management/tables/:tableName/columns
|
||||||
|
*/
|
||||||
|
router.get("/tables/:tableName/columns", getColumnList);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개별 컬럼 설정 업데이트
|
||||||
|
* POST /api/table-management/tables/:tableName/columns/:columnName/settings
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
"/tables/:tableName/columns/:columnName/settings",
|
||||||
|
updateColumnSettings
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 컬럼 설정 일괄 업데이트
|
||||||
|
* POST /api/table-management/tables/:tableName/columns/settings
|
||||||
|
*/
|
||||||
|
router.post("/tables/:tableName/columns/settings", updateAllColumnSettings);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 라벨 정보 조회
|
||||||
|
* GET /api/table-management/tables/:tableName/labels
|
||||||
|
*/
|
||||||
|
router.get("/tables/:tableName/labels", getTableLabels);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컬럼 라벨 정보 조회
|
||||||
|
* GET /api/table-management/tables/:tableName/columns/:columnName/labels
|
||||||
|
*/
|
||||||
|
router.get("/tables/:tableName/columns/:columnName/labels", getColumnLabels);
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
@ -0,0 +1,313 @@
|
||||||
|
import { Client } from "pg";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
import {
|
||||||
|
TableInfo,
|
||||||
|
ColumnTypeInfo,
|
||||||
|
ColumnSettings,
|
||||||
|
TableLabels,
|
||||||
|
ColumnLabels,
|
||||||
|
} from "../types/tableManagement";
|
||||||
|
|
||||||
|
export class TableManagementService {
|
||||||
|
private client: Client;
|
||||||
|
|
||||||
|
constructor(client: Client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 목록 조회 (PostgreSQL information_schema 활용)
|
||||||
|
*/
|
||||||
|
async getTableList(): Promise<TableInfo[]> {
|
||||||
|
try {
|
||||||
|
logger.info("테이블 목록 조회 시작");
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
t.table_name as "tableName",
|
||||||
|
COALESCE(tl.table_label, t.table_name) as "displayName",
|
||||||
|
COALESCE(tl.description, '') as "description",
|
||||||
|
(SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_name = t.table_name AND table_schema = 'public') as "columnCount"
|
||||||
|
FROM information_schema.tables t
|
||||||
|
LEFT JOIN table_labels tl ON t.table_name = tl.table_name
|
||||||
|
WHERE t.table_schema = 'public'
|
||||||
|
AND t.table_type = 'BASE TABLE'
|
||||||
|
AND t.table_name NOT LIKE 'pg_%'
|
||||||
|
AND t.table_name NOT LIKE 'sql_%'
|
||||||
|
ORDER BY t.table_name
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await this.client.query(query);
|
||||||
|
logger.info(`테이블 목록 조회 완료: ${result.rows.length}개`);
|
||||||
|
|
||||||
|
return result.rows;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("테이블 목록 조회 중 오류 발생:", error);
|
||||||
|
throw new Error(
|
||||||
|
`테이블 목록 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 컬럼 정보 조회
|
||||||
|
*/
|
||||||
|
async getColumnList(tableName: string): Promise<ColumnTypeInfo[]> {
|
||||||
|
try {
|
||||||
|
logger.info(`컬럼 정보 조회 시작: ${tableName}`);
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
c.column_name as "columnName",
|
||||||
|
COALESCE(cl.column_label, c.column_name) as "displayName",
|
||||||
|
c.data_type as "dbType",
|
||||||
|
COALESCE(cl.web_type, 'text') as "webType",
|
||||||
|
COALESCE(cl.detail_settings, '') as "detailSettings",
|
||||||
|
COALESCE(cl.description, '') as "description",
|
||||||
|
c.is_nullable as "isNullable",
|
||||||
|
c.column_default as "defaultValue",
|
||||||
|
c.character_maximum_length as "maxLength",
|
||||||
|
c.numeric_precision as "numericPrecision",
|
||||||
|
c.numeric_scale as "numericScale",
|
||||||
|
cl.code_category as "codeCategory",
|
||||||
|
cl.code_value as "codeValue",
|
||||||
|
cl.reference_table as "referenceTable",
|
||||||
|
cl.reference_column as "referenceColumn",
|
||||||
|
cl.display_order as "displayOrder",
|
||||||
|
cl.is_visible as "isVisible"
|
||||||
|
FROM information_schema.columns c
|
||||||
|
LEFT JOIN column_labels cl ON c.table_name = cl.table_name AND c.column_name = cl.column_name
|
||||||
|
WHERE c.table_name = $1
|
||||||
|
ORDER BY c.ordinal_position
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await this.client.query(query, [tableName]);
|
||||||
|
logger.info(`컬럼 정보 조회 완료: ${tableName}, ${result.rows.length}개`);
|
||||||
|
|
||||||
|
return result.rows;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`컬럼 정보 조회 중 오류 발생: ${tableName}`, error);
|
||||||
|
throw new Error(
|
||||||
|
`컬럼 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블이 table_labels에 없으면 자동 추가
|
||||||
|
*/
|
||||||
|
async insertTableIfNotExists(tableName: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info(`테이블 라벨 자동 추가 시작: ${tableName}`);
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
INSERT INTO table_labels (table_name, table_label, description)
|
||||||
|
VALUES ($1, $1, '')
|
||||||
|
ON CONFLICT (table_name) DO NOTHING
|
||||||
|
`;
|
||||||
|
|
||||||
|
await this.client.query(query, [tableName]);
|
||||||
|
logger.info(`테이블 라벨 자동 추가 완료: ${tableName}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`테이블 라벨 자동 추가 중 오류 발생: ${tableName}`, error);
|
||||||
|
throw new Error(
|
||||||
|
`테이블 라벨 자동 추가 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컬럼 설정 업데이트 (UPSERT 방식)
|
||||||
|
*/
|
||||||
|
async updateColumnSettings(
|
||||||
|
tableName: string,
|
||||||
|
columnName: string,
|
||||||
|
settings: ColumnSettings
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info(`컬럼 설정 업데이트 시작: ${tableName}.${columnName}`);
|
||||||
|
|
||||||
|
// 테이블이 table_labels에 없으면 자동 추가
|
||||||
|
await this.insertTableIfNotExists(tableName);
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
INSERT INTO column_labels (
|
||||||
|
table_name, column_name, column_label, web_type,
|
||||||
|
detail_settings, code_category, code_value,
|
||||||
|
reference_table, reference_column, display_order, is_visible
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||||
|
ON CONFLICT (table_name, column_name) DO UPDATE SET
|
||||||
|
column_label = EXCLUDED.column_label,
|
||||||
|
web_type = EXCLUDED.web_type,
|
||||||
|
detail_settings = EXCLUDED.detail_settings,
|
||||||
|
code_category = EXCLUDED.code_category,
|
||||||
|
code_value = EXCLUDED.code_value,
|
||||||
|
reference_table = EXCLUDED.reference_table,
|
||||||
|
reference_column = EXCLUDED.reference_column,
|
||||||
|
display_order = EXCLUDED.display_order,
|
||||||
|
is_visible = EXCLUDED.is_visible,
|
||||||
|
updated_date = now()
|
||||||
|
`;
|
||||||
|
|
||||||
|
await this.client.query(query, [
|
||||||
|
tableName,
|
||||||
|
columnName,
|
||||||
|
settings.columnLabel,
|
||||||
|
settings.webType,
|
||||||
|
settings.detailSettings,
|
||||||
|
settings.codeCategory,
|
||||||
|
settings.codeValue,
|
||||||
|
settings.referenceTable,
|
||||||
|
settings.referenceColumn,
|
||||||
|
settings.displayOrder || 0,
|
||||||
|
settings.isVisible !== undefined ? settings.isVisible : true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
logger.info(`컬럼 설정 업데이트 완료: ${tableName}.${columnName}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`컬럼 설정 업데이트 중 오류 발생: ${tableName}.${columnName}`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`컬럼 설정 업데이트 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 컬럼 설정 일괄 업데이트
|
||||||
|
*/
|
||||||
|
async updateAllColumnSettings(
|
||||||
|
tableName: string,
|
||||||
|
columnSettings: ColumnSettings[]
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info(
|
||||||
|
`전체 컬럼 설정 일괄 업데이트 시작: ${tableName}, ${columnSettings.length}개`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 트랜잭션 시작
|
||||||
|
await this.client.query("BEGIN");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 테이블이 table_labels에 없으면 자동 추가
|
||||||
|
await this.insertTableIfNotExists(tableName);
|
||||||
|
|
||||||
|
// 각 컬럼 설정을 순차적으로 업데이트
|
||||||
|
for (const columnSetting of columnSettings) {
|
||||||
|
const columnName =
|
||||||
|
columnSetting.columnLabel || columnSetting.columnName;
|
||||||
|
if (columnName) {
|
||||||
|
await this.updateColumnSettings(
|
||||||
|
tableName,
|
||||||
|
columnName,
|
||||||
|
columnSetting
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 트랜잭션 커밋
|
||||||
|
await this.client.query("COMMIT");
|
||||||
|
logger.info(`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}`);
|
||||||
|
} catch (error) {
|
||||||
|
// 트랜잭션 롤백
|
||||||
|
await this.client.query("ROLLBACK");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`전체 컬럼 설정 일괄 업데이트 중 오류 발생: ${tableName}`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`전체 컬럼 설정 일괄 업데이트 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테이블 라벨 정보 조회
|
||||||
|
*/
|
||||||
|
async getTableLabels(tableName: string): Promise<TableLabels | null> {
|
||||||
|
try {
|
||||||
|
logger.info(`테이블 라벨 정보 조회 시작: ${tableName}`);
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
table_name as "tableName",
|
||||||
|
table_label as "tableLabel",
|
||||||
|
description,
|
||||||
|
created_date as "createdDate",
|
||||||
|
updated_date as "updatedDate"
|
||||||
|
FROM table_labels
|
||||||
|
WHERE table_name = $1
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await this.client.query(query, [tableName]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`테이블 라벨 정보 조회 완료: ${tableName}`);
|
||||||
|
return result.rows[0];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`테이블 라벨 정보 조회 중 오류 발생: ${tableName}`, error);
|
||||||
|
throw new Error(
|
||||||
|
`테이블 라벨 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컬럼 라벨 정보 조회
|
||||||
|
*/
|
||||||
|
async getColumnLabels(
|
||||||
|
tableName: string,
|
||||||
|
columnName: string
|
||||||
|
): Promise<ColumnLabels | null> {
|
||||||
|
try {
|
||||||
|
logger.info(`컬럼 라벨 정보 조회 시작: ${tableName}.${columnName}`);
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
table_name as "tableName",
|
||||||
|
column_name as "columnName",
|
||||||
|
column_label as "columnLabel",
|
||||||
|
web_type as "webType",
|
||||||
|
detail_settings as "detailSettings",
|
||||||
|
description,
|
||||||
|
display_order as "displayOrder",
|
||||||
|
is_visible as "isVisible",
|
||||||
|
code_category as "codeCategory",
|
||||||
|
code_value as "codeValue",
|
||||||
|
reference_table as "referenceTable",
|
||||||
|
reference_column as "referenceColumn",
|
||||||
|
created_date as "createdDate",
|
||||||
|
updated_date as "updatedDate"
|
||||||
|
FROM column_labels
|
||||||
|
WHERE table_name = $1 AND column_name = $2
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await this.client.query(query, [tableName, columnName]);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`컬럼 라벨 정보 조회 완료: ${tableName}.${columnName}`);
|
||||||
|
return result.rows[0];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`컬럼 라벨 정보 조회 중 오류 발생: ${tableName}.${columnName}`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`컬럼 라벨 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
// 테이블 타입관리 관련 타입 정의
|
||||||
|
|
||||||
|
export interface TableInfo {
|
||||||
|
tableName: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
columnCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnTypeInfo {
|
||||||
|
columnName: string;
|
||||||
|
displayName: string;
|
||||||
|
dbType: string;
|
||||||
|
webType: string;
|
||||||
|
detailSettings: string;
|
||||||
|
description: string;
|
||||||
|
isNullable: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
maxLength?: number;
|
||||||
|
numericPrecision?: number;
|
||||||
|
numericScale?: number;
|
||||||
|
codeCategory?: string;
|
||||||
|
codeValue?: string;
|
||||||
|
referenceTable?: string;
|
||||||
|
referenceColumn?: string;
|
||||||
|
displayOrder?: number;
|
||||||
|
isVisible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnSettings {
|
||||||
|
columnName?: string; // 컬럼명 (업데이트 시 필요)
|
||||||
|
columnLabel: string; // 컬럼 표시명
|
||||||
|
webType: string; // 웹 입력 타입 (text, number, date, code, entity)
|
||||||
|
detailSettings: string; // 상세 설정
|
||||||
|
codeCategory: string; // 코드 카테고리
|
||||||
|
codeValue: string; // 코드 값
|
||||||
|
referenceTable: string; // 참조 테이블
|
||||||
|
referenceColumn: string; // 참조 컬럼
|
||||||
|
displayOrder?: number; // 표시 순서
|
||||||
|
isVisible?: boolean; // 표시 여부
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableLabels {
|
||||||
|
tableName: string;
|
||||||
|
tableLabel?: string;
|
||||||
|
description?: string;
|
||||||
|
createdDate?: Date;
|
||||||
|
updatedDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnLabels {
|
||||||
|
id?: number;
|
||||||
|
tableName: string;
|
||||||
|
columnName: string;
|
||||||
|
columnLabel?: string;
|
||||||
|
webType?: string;
|
||||||
|
detailSettings?: string;
|
||||||
|
description?: string;
|
||||||
|
displayOrder?: number;
|
||||||
|
isVisible?: boolean;
|
||||||
|
codeCategory?: string;
|
||||||
|
codeValue?: string;
|
||||||
|
referenceTable?: string;
|
||||||
|
referenceColumn?: string;
|
||||||
|
createdDate?: Date;
|
||||||
|
updatedDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 응답 타입
|
||||||
|
export interface TableListResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: TableInfo[];
|
||||||
|
message?: string;
|
||||||
|
error?: {
|
||||||
|
code: string;
|
||||||
|
details?: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnListResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: ColumnTypeInfo[];
|
||||||
|
message?: string;
|
||||||
|
error?: {
|
||||||
|
code: string;
|
||||||
|
details?: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnSettingsResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: {
|
||||||
|
code: string;
|
||||||
|
details?: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 웹 타입 옵션
|
||||||
|
export const WEB_TYPE_OPTIONS = [
|
||||||
|
{ value: "text", label: "text", description: "일반 텍스트 입력" },
|
||||||
|
{ value: "number", label: "number", description: "숫자 입력" },
|
||||||
|
{ value: "date", label: "date", description: "날짜 선택기" },
|
||||||
|
{ value: "code", label: "code", description: "코드 선택 (공통코드 지정)" },
|
||||||
|
{
|
||||||
|
value: "entity",
|
||||||
|
label: "entity",
|
||||||
|
description: "엔티티 참조 (참조테이블 지정)",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type WebType = (typeof WEB_TYPE_OPTIONS)[number]["value"];
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
const { Client } = require("pg");
|
||||||
|
require("dotenv/config");
|
||||||
|
|
||||||
|
async function testDatabase() {
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log("✅ 데이터베이스 연결 성공");
|
||||||
|
|
||||||
|
// 사용자 정보 조회
|
||||||
|
const userResult = await client.query(
|
||||||
|
"SELECT user_id, user_name, status FROM user_info LIMIT 5"
|
||||||
|
);
|
||||||
|
console.log("👥 사용자 정보:", userResult.rows);
|
||||||
|
|
||||||
|
// 테이블 라벨 정보 조회
|
||||||
|
const tableLabelsResult = await client.query(
|
||||||
|
"SELECT * FROM table_labels LIMIT 5"
|
||||||
|
);
|
||||||
|
console.log("🏷️ 테이블 라벨 정보:", tableLabelsResult.rows);
|
||||||
|
|
||||||
|
// 컬럼 라벨 정보 조회
|
||||||
|
const columnLabelsResult = await client.query(
|
||||||
|
"SELECT * FROM column_labels LIMIT 5"
|
||||||
|
);
|
||||||
|
console.log("📋 컬럼 라벨 정보:", columnLabelsResult.rows);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 오류 발생:", error);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testDatabase();
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
const { Client } = require("pg");
|
||||||
|
|
||||||
|
async function updatePassword() {
|
||||||
|
const client = new Client({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log("✅ 데이터베이스 연결 성공");
|
||||||
|
|
||||||
|
// kkh 사용자의 비밀번호를 admin123으로 변경
|
||||||
|
await client.query(`
|
||||||
|
UPDATE user_info
|
||||||
|
SET user_password = 'f21b1ce8b08dc955bd4afff71b3db1fc'
|
||||||
|
WHERE user_id = 'kkh'
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log("✅ 비밀번호 변경 완료: kkh -> admin123");
|
||||||
|
|
||||||
|
// 변경 확인
|
||||||
|
const result = await client.query(`
|
||||||
|
SELECT user_id, user_name, user_password
|
||||||
|
FROM user_info
|
||||||
|
WHERE user_id = 'kkh'
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log("👤 변경된 사용자:", result.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 오류 발생:", error);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePassword();
|
||||||
|
|
@ -952,6 +952,294 @@ public Map<String, Object> changeUserStatus(Map<String, Object> paramMap)
|
||||||
|
|
||||||
**시작 지점**: 사용자 목록 조회 API부터 실제 데이터베이스 연동으로 구현
|
**시작 지점**: 사용자 목록 조회 API부터 실제 데이터베이스 연동으로 구현
|
||||||
|
|
||||||
|
## 🗄️ 테이블 타입관리 백엔드 Node.js 리팩토링 계획
|
||||||
|
|
||||||
|
### 📋 테이블 타입관리 기능 Node.js 리팩토링 개요
|
||||||
|
|
||||||
|
**목표**: 기존 Java Spring Boot의 테이블 타입관리 기능을 Node.js + TypeScript로 완전 리팩토링
|
||||||
|
|
||||||
|
**기존 Java 백엔드 (`@backend/`) 분석**
|
||||||
|
|
||||||
|
- Spring Framework 기반의 `TableManagementController`와 `TableManagementService`
|
||||||
|
- MyBatis를 사용한 PostgreSQL 메타데이터 조회
|
||||||
|
- `table_labels`, `column_labels` 테이블을 통한 테이블/컬럼 설정 관리
|
||||||
|
- `information_schema` 활용한 데이터베이스 구조 자동 조회
|
||||||
|
|
||||||
|
**현재 Node.js 백엔드 (`@backend-node/`) 상황**
|
||||||
|
|
||||||
|
- 테이블 타입관리 기능 미구현
|
||||||
|
- PostgreSQL 메타데이터 조회 기능 부재
|
||||||
|
- 컬럼 설정 관리 기능 부재
|
||||||
|
|
||||||
|
**🎯 리팩토링 목표**
|
||||||
|
|
||||||
|
1. **기존 Java 백엔드의 테이블 타입관리 기능을 Node.js로 완전 이전**
|
||||||
|
2. **PostgreSQL `information_schema` 활용한 메타데이터 조회**
|
||||||
|
3. **`table_labels`, `column_labels` 테이블을 통한 설정 관리**
|
||||||
|
4. **기존 API 응답 형식과 호환성 유지**
|
||||||
|
|
||||||
|
**🛠️ 단계별 구현 계획**
|
||||||
|
|
||||||
|
**Phase 2-5-1: 데이터베이스 스키마 및 모델 정리 (1일)**
|
||||||
|
|
||||||
|
- [ ] Prisma 스키마에 `table_labels`, `column_labels` 테이블 정의
|
||||||
|
- [ ] PostgreSQL 메타데이터 조회를 위한 권한 설정 확인
|
||||||
|
- [ ] 데이터 타입 및 관계 정의
|
||||||
|
|
||||||
|
**Phase 2-5-2: 핵심 테이블 타입관리 API 구현 (3일)**
|
||||||
|
|
||||||
|
**테이블 메타데이터 API**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 기존 Java TableManagementController의 핵심 메서드들
|
||||||
|
- GET /api/table-management/tables - 테이블 목록 조회 (information_schema 기반)
|
||||||
|
- GET /api/table-management/tables/:tableName/columns - 컬럼 정보 조회
|
||||||
|
- POST /api/table-management/tables/:tableName/columns/:columnName/settings - 개별 컬럼 설정 업데이트
|
||||||
|
- POST /api/table-management/tables/:tableName/columns/settings - 전체 컬럼 설정 일괄 업데이트
|
||||||
|
```
|
||||||
|
|
||||||
|
**PostgreSQL 메타데이터 조회**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// information_schema를 활용한 테이블 목록 조회
|
||||||
|
const getTableList = async (): Promise<TableInfo[]> => {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
t.table_name as "tableName",
|
||||||
|
COALESCE(tl.table_label, t.table_name) as "displayName",
|
||||||
|
COALESCE(tl.description, '') as "description",
|
||||||
|
(SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_name = t.table_name AND table_schema = 'public') as "columnCount"
|
||||||
|
FROM information_schema.tables t
|
||||||
|
LEFT JOIN table_labels tl ON t.table_name = tl.table_name
|
||||||
|
WHERE t.table_schema = 'public'
|
||||||
|
AND t.table_type = 'BASE TABLE'
|
||||||
|
AND t.table_name NOT LIKE 'pg_%'
|
||||||
|
AND t.table_name NOT LIKE 'sql_%'
|
||||||
|
ORDER BY t.table_name
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await client.query(query);
|
||||||
|
return result.rows;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Phase 2-5-3: 서비스 레이어 구현 (2일)**
|
||||||
|
|
||||||
|
**TableManagementService 확장**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 기존 Java TableManagementService의 핵심 메서드들
|
||||||
|
- getTableList() - 테이블 목록 조회 (information_schema 활용)
|
||||||
|
- getColumnList(tableName) - 컬럼 정보 조회
|
||||||
|
- updateColumnSettings() - 개별 컬럼 설정 업데이트
|
||||||
|
- updateAllColumnSettings() - 전체 컬럼 설정 일괄 업데이트
|
||||||
|
```
|
||||||
|
|
||||||
|
**데이터베이스 연동**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
- PostgreSQL 클라이언트를 사용한 직접 쿼리 실행
|
||||||
|
- 트랜잭션 처리
|
||||||
|
- 에러 핸들링 및 로깅
|
||||||
|
```
|
||||||
|
|
||||||
|
**Phase 2-5-4: 컬럼 설정 관리 기능 (1일)**
|
||||||
|
|
||||||
|
**컬럼 설정 데이터 구조**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ColumnSettings {
|
||||||
|
columnLabel: string; // 컬럼 표시명
|
||||||
|
webType: string; // 웹 입력 타입 (text, number, date, code, entity)
|
||||||
|
detailSettings: string; // 상세 설정
|
||||||
|
codeCategory: string; // 코드 카테고리
|
||||||
|
codeValue: string; // 코드 값
|
||||||
|
referenceTable: string; // 참조 테이블
|
||||||
|
referenceColumn: string; // 참조 컬럼
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**UPSERT 방식으로 컬럼 설정 저장**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const updateColumnSettings = async (
|
||||||
|
tableName: string,
|
||||||
|
columnName: string,
|
||||||
|
settings: ColumnSettings
|
||||||
|
): Promise<void> => {
|
||||||
|
const query = `
|
||||||
|
INSERT INTO column_labels (
|
||||||
|
table_name, column_name, column_label, web_type,
|
||||||
|
detail_settings, code_category, code_value,
|
||||||
|
reference_table, reference_column
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
ON CONFLICT (table_name, column_name) DO UPDATE SET
|
||||||
|
column_label = EXCLUDED.column_label,
|
||||||
|
web_type = EXCLUDED.web_type,
|
||||||
|
detail_settings = EXCLUDED.detail_settings,
|
||||||
|
code_category = EXCLUDED.code_category,
|
||||||
|
code_value = EXCLUDED.code_value,
|
||||||
|
reference_table = EXCLUDED.reference_table,
|
||||||
|
reference_column = EXCLUDED.reference_column,
|
||||||
|
updated_date = now()
|
||||||
|
`;
|
||||||
|
|
||||||
|
await client.query(query, [
|
||||||
|
tableName,
|
||||||
|
columnName,
|
||||||
|
settings.columnLabel,
|
||||||
|
settings.webType,
|
||||||
|
settings.detailSettings,
|
||||||
|
settings.codeCategory,
|
||||||
|
settings.codeValue,
|
||||||
|
settings.referenceTable,
|
||||||
|
settings.referenceColumn,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**🔄 구현 우선순위**
|
||||||
|
|
||||||
|
**High Priority (1-2일차)**
|
||||||
|
|
||||||
|
1. 테이블 목록 조회 API (information_schema 활용)
|
||||||
|
2. 컬럼 정보 조회 API
|
||||||
|
3. 기본적인 에러 핸들링
|
||||||
|
|
||||||
|
**Medium Priority (3-4일차)**
|
||||||
|
|
||||||
|
1. 개별 컬럼 설정 업데이트 API
|
||||||
|
2. 전체 컬럼 설정 일괄 업데이트 API
|
||||||
|
3. 테이블/컬럼 라벨 자동 생성 기능
|
||||||
|
|
||||||
|
**Low Priority (5일차)**
|
||||||
|
|
||||||
|
1. 컬럼 설정 검증 로직
|
||||||
|
2. 메타데이터 캐싱 기능
|
||||||
|
3. 고급 검색 및 필터링
|
||||||
|
|
||||||
|
**🔧 기술적 고려사항**
|
||||||
|
|
||||||
|
**PostgreSQL 메타데이터 조회**
|
||||||
|
|
||||||
|
- `information_schema` 접근 권한 확인
|
||||||
|
- 시스템 테이블 제외 로직 (`pg_*`, `sql_*` 테이블 제외)
|
||||||
|
- 성능 최적화를 위한 인덱스 활용
|
||||||
|
|
||||||
|
**데이터베이스 연동**
|
||||||
|
|
||||||
|
- Prisma ORM 대신 PostgreSQL 클라이언트 직접 사용
|
||||||
|
- 메타데이터 조회를 위한 특수 쿼리 처리
|
||||||
|
- 트랜잭션 관리 및 롤백 처리
|
||||||
|
|
||||||
|
**API 호환성**
|
||||||
|
|
||||||
|
- 기존 Java 백엔드와 동일한 응답 형식 유지
|
||||||
|
- 프론트엔드 변경 최소화
|
||||||
|
- 점진적 마이그레이션 지원
|
||||||
|
|
||||||
|
**📊 테스트 계획**
|
||||||
|
|
||||||
|
1. **단위 테스트**: 각 서비스 메서드별 테스트
|
||||||
|
2. **통합 테스트**: API 엔드포인트별 테스트
|
||||||
|
3. **데이터베이스 테스트**: 실제 PostgreSQL 메타데이터 조회 테스트
|
||||||
|
4. **성능 테스트**: 대용량 테이블/컬럼 조회 테스트
|
||||||
|
|
||||||
|
**📝 기존 Java 코드 분석 결과**
|
||||||
|
|
||||||
|
**TableManagementController 주요 메서드**
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 테이블 목록 조회
|
||||||
|
@GetMapping("/tables")
|
||||||
|
public ResponseEntity<Map<String, Object>> getTableList(HttpServletRequest request)
|
||||||
|
|
||||||
|
// 컬럼 정보 조회
|
||||||
|
@GetMapping("/tables/{tableName}/columns")
|
||||||
|
public ResponseEntity<Map<String, Object>> getColumnList(
|
||||||
|
HttpServletRequest request, @PathVariable String tableName)
|
||||||
|
|
||||||
|
// 컬럼 설정 업데이트
|
||||||
|
@PostMapping("/tables/{tableName}/columns/{columnName}/settings")
|
||||||
|
public ResponseEntity<Map<String, Object>> updateColumnSettings(
|
||||||
|
HttpServletRequest request, @PathVariable String tableName,
|
||||||
|
@PathVariable String columnName, @RequestBody Map<String, Object> settings)
|
||||||
|
|
||||||
|
// 전체 컬럼 설정 일괄 업데이트
|
||||||
|
@PostMapping("/tables/{tableName}/columns/settings")
|
||||||
|
public ResponseEntity<Map<String, Object>> updateAllColumnSettings(
|
||||||
|
HttpServletRequest request, @PathVariable String tableName,
|
||||||
|
@RequestBody List<Map<String, Object>> columnSettings)
|
||||||
|
```
|
||||||
|
|
||||||
|
**TableManagementService 주요 메서드**
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 테이블 목록 조회
|
||||||
|
public List<Map<String, Object>> getTableList()
|
||||||
|
|
||||||
|
// 컬럼 정보 조회
|
||||||
|
public List<Map<String, Object>> getColumnList(String tableName)
|
||||||
|
|
||||||
|
// 컬럼 설정 업데이트
|
||||||
|
public void updateColumnSettings(String tableName, String columnName, Map<String, Object> settings)
|
||||||
|
|
||||||
|
// 전체 컬럼 설정 일괄 업데이트
|
||||||
|
public void updateAllColumnSettings(String tableName, List<Map<String, Object>> columnSettings)
|
||||||
|
```
|
||||||
|
|
||||||
|
**MyBatis Mapper 주요 쿼리**
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- 테이블 목록 조회 -->
|
||||||
|
<select id="selectTableList" resultType="map">
|
||||||
|
SELECT
|
||||||
|
t.table_name as "tableName",
|
||||||
|
COALESCE(tl.table_label, t.table_name) as "displayName",
|
||||||
|
COALESCE(tl.description, '') as "description",
|
||||||
|
(SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_name = t.table_name AND table_schema = 'public') as "columnCount"
|
||||||
|
FROM information_schema.tables t
|
||||||
|
LEFT JOIN table_labels tl ON t.table_name = tl.table_name
|
||||||
|
WHERE t.table_schema = 'public'
|
||||||
|
AND t.table_type = 'BASE TABLE'
|
||||||
|
AND t.table_name NOT LIKE 'pg_%'
|
||||||
|
AND t.table_name NOT LIKE 'sql_%'
|
||||||
|
ORDER BY t.table_name
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 컬럼 정보 조회 -->
|
||||||
|
<select id="selectColumnList" parameterType="map" resultType="map">
|
||||||
|
SELECT
|
||||||
|
c.column_name as "columnName",
|
||||||
|
COALESCE(cl.column_label, c.column_name) as "displayName",
|
||||||
|
c.data_type as "dbType",
|
||||||
|
COALESCE(cl.web_type, 'text') as "webType",
|
||||||
|
COALESCE(cl.detail_settings, '') as "detailSettings",
|
||||||
|
COALESCE(cl.description, '') as "description",
|
||||||
|
c.is_nullable as "isNullable",
|
||||||
|
c.column_default as "defaultValue",
|
||||||
|
c.character_maximum_length as "maxLength",
|
||||||
|
c.numeric_precision as "numericPrecision",
|
||||||
|
c.numeric_scale as "numericScale",
|
||||||
|
cl.code_category as "codeCategory",
|
||||||
|
cl.code_value as "codeValue",
|
||||||
|
cl.reference_table as "referenceTable",
|
||||||
|
cl.reference_column as "referenceColumn"
|
||||||
|
FROM information_schema.columns c
|
||||||
|
LEFT JOIN column_labels cl ON c.table_name = cl.table_name AND c.column_name = cl.column_name
|
||||||
|
WHERE c.table_name = #{tableName}
|
||||||
|
ORDER BY c.ordinal_position
|
||||||
|
</select>
|
||||||
|
```
|
||||||
|
|
||||||
|
**📋 다음 단계**
|
||||||
|
|
||||||
|
이 계획에 따라 **Phase 2-5**를 시작하여 단계적으로 테이블 타입관리 기능을 구현하겠습니다.
|
||||||
|
|
||||||
|
**시작 지점**: 테이블 목록 조회 API부터 PostgreSQL 메타데이터 조회로 구현
|
||||||
|
|
||||||
#### **Phase 2-2A: 메뉴 관리 API (완료 ✅)**
|
#### **Phase 2-2A: 메뉴 관리 API (완료 ✅)**
|
||||||
|
|
||||||
- [x] 관리자 메뉴 조회 API (`GET /api/admin/menus`) - **완료: 기존 `AdminController.getAdminMenuList()` 포팅**
|
- [x] 관리자 메뉴 조회 API (`GET /api/admin/menus`) - **완료: 기존 `AdminController.getAdminMenuList()` 포팅**
|
||||||
|
|
@ -975,7 +1263,13 @@ public Map<String, Object> changeUserStatus(Map<String, Object> paramMap)
|
||||||
- [ ] 권한 관리 API (`authority_master`, `rel_menu_auth` 테이블 기반)
|
- [ ] 권한 관리 API (`authority_master`, `rel_menu_auth` 테이블 기반)
|
||||||
- [ ] 사용자별 메뉴 권한 조회 API
|
- [ ] 사용자별 메뉴 권한 조회 API
|
||||||
|
|
||||||
#### **Phase 2-5: 다국어 및 공통 관리 API (1주)**
|
#### **Phase 2-5: 테이블 타입관리 API (1주)**
|
||||||
|
|
||||||
|
- [ ] 테이블 타입관리 API (`table_labels`, `column_labels` 테이블 기반)
|
||||||
|
- [ ] PostgreSQL 메타데이터 조회 API (`information_schema` 활용)
|
||||||
|
- [ ] 컬럼 설정 관리 API (웹 타입, 참조 테이블, 코드 카테고리 등)
|
||||||
|
|
||||||
|
#### **Phase 2-6: 다국어 및 공통 관리 API (1주)**
|
||||||
|
|
||||||
- [ ] 다국어 관리 API (`multi_lang_key_master`, `multi_lang_text` 테이블 기반)
|
- [ ] 다국어 관리 API (`multi_lang_key_master`, `multi_lang_text` 테이블 기반)
|
||||||
- [ ] 공통 코드 관리 API (`comm_code` 테이블 기반)
|
- [ ] 공통 코드 관리 API (`comm_code` 테이블 기반)
|
||||||
|
|
@ -1414,6 +1708,6 @@ export const authenticateToken = (
|
||||||
---
|
---
|
||||||
|
|
||||||
**마지막 업데이트**: 2024년 12월 20일
|
**마지막 업데이트**: 2024년 12월 20일
|
||||||
**버전**: 1.9.0
|
**버전**: 2.0.0
|
||||||
**작성자**: AI Assistant
|
**작성자**: AI Assistant
|
||||||
**현재 상태**: Phase 1 완료, Phase 2-1A 완료, Phase 2-1B 완료, Phase 2-2A 완료 ✅ (메뉴 API 구현 완료, 어드민 메뉴 인증 문제 해결, 토큰 인증 문제 완전 해결)
|
**현재 상태**: Phase 1 완료, Phase 2-1A 완료, Phase 2-1B 완료, Phase 2-2A 완료 ✅ (메뉴 API 구현 완료, 어드민 메뉴 인증 문제 해결, 토큰 인증 문제 완전 해결), Phase 2-5 계획 수립 ✅ (테이블 타입관리 백엔드 Node.js 리팩토링 계획 완성)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue