화면관리 중간 커밋
This commit is contained in:
@@ -13,6 +13,7 @@ import authRoutes from "./routes/authRoutes";
|
||||
import adminRoutes from "./routes/adminRoutes";
|
||||
import multilangRoutes from "./routes/multilangRoutes";
|
||||
import tableManagementRoutes from "./routes/tableManagementRoutes";
|
||||
import screenManagementRoutes from "./routes/screenManagementRoutes";
|
||||
// import userRoutes from './routes/userRoutes';
|
||||
// import menuRoutes from './routes/menuRoutes';
|
||||
|
||||
@@ -63,6 +64,7 @@ app.use("/api/auth", authRoutes);
|
||||
app.use("/api/admin", adminRoutes);
|
||||
app.use("/api/multilang", multilangRoutes);
|
||||
app.use("/api/table-management", tableManagementRoutes);
|
||||
app.use("/api/screen-management", screenManagementRoutes);
|
||||
// app.use('/api/users', userRoutes);
|
||||
// app.use('/api/menus', menuRoutes);
|
||||
|
||||
|
||||
455
backend-node/src/controllers/screenManagementController.ts
Normal file
455
backend-node/src/controllers/screenManagementController.ts
Normal file
@@ -0,0 +1,455 @@
|
||||
import { Request, Response } from "express";
|
||||
import { ScreenManagementService } from "../services/screenManagementService";
|
||||
import {
|
||||
CreateScreenRequest,
|
||||
UpdateScreenRequest,
|
||||
SaveLayoutRequest,
|
||||
MenuAssignmentRequest,
|
||||
ColumnWebTypeSetting,
|
||||
WebType,
|
||||
} from "../types/screen";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export class ScreenManagementController {
|
||||
private screenService: ScreenManagementService;
|
||||
|
||||
constructor() {
|
||||
this.screenService = new ScreenManagementService();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 화면 정의 관리
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 화면 목록 조회 (회사별)
|
||||
*/
|
||||
async getScreens(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { page = 1, size = 20 } = req.query;
|
||||
const userCompanyCode = (req as any).user?.company_code || "*";
|
||||
|
||||
const result = await this.screenService.getScreensByCompany(
|
||||
userCompanyCode,
|
||||
Number(page),
|
||||
Number(size)
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.pagination,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("화면 목록 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "화면 목록을 조회하는 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면 생성
|
||||
*/
|
||||
async createScreen(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const screenData: CreateScreenRequest = req.body;
|
||||
const userCompanyCode = (req as any).user?.company_code || "*";
|
||||
const userId = (req as any).user?.user_id || "system";
|
||||
|
||||
// 사용자 회사 코드 자동 설정
|
||||
if (userCompanyCode !== "*") {
|
||||
screenData.companyCode = userCompanyCode;
|
||||
}
|
||||
screenData.createdBy = userId;
|
||||
|
||||
const screen = await this.screenService.createScreen(
|
||||
screenData,
|
||||
userCompanyCode
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: screen,
|
||||
message: "화면이 성공적으로 생성되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("화면 생성 실패:", error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "화면 생성에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면 조회
|
||||
*/
|
||||
async getScreen(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { screenId } = req.params;
|
||||
const screen = await this.screenService.getScreenById(Number(screenId));
|
||||
|
||||
if (!screen) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "화면을 찾을 수 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: screen,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("화면 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "화면을 조회하는 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면 수정
|
||||
*/
|
||||
async updateScreen(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { screenId } = req.params;
|
||||
const updateData: UpdateScreenRequest = req.body;
|
||||
const userCompanyCode = (req as any).user?.company_code || "*";
|
||||
const userId = (req as any).user?.user_id || "system";
|
||||
|
||||
updateData.updatedBy = userId;
|
||||
|
||||
const screen = await this.screenService.updateScreen(
|
||||
Number(screenId),
|
||||
updateData,
|
||||
userCompanyCode
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: screen,
|
||||
message: "화면이 성공적으로 수정되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("화면 수정 실패:", error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "화면 수정에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면 삭제
|
||||
*/
|
||||
async deleteScreen(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { screenId } = req.params;
|
||||
const userCompanyCode = (req as any).user?.company_code || "*";
|
||||
|
||||
await this.screenService.deleteScreen(Number(screenId), userCompanyCode);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "화면이 성공적으로 삭제되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("화면 삭제 실패:", error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "화면 삭제에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 레이아웃 관리
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 레이아웃 조회
|
||||
*/
|
||||
async getLayout(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { screenId } = req.params;
|
||||
const layout = await this.screenService.getLayout(Number(screenId));
|
||||
|
||||
if (!layout) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
components: [],
|
||||
gridSettings: {
|
||||
columns: 12,
|
||||
gap: 16,
|
||||
padding: 16,
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: layout,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("레이아웃 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "레이아웃을 조회하는 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 저장
|
||||
*/
|
||||
async saveLayout(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { screenId } = req.params;
|
||||
const layoutData: SaveLayoutRequest = req.body;
|
||||
|
||||
await this.screenService.saveLayout(Number(screenId), layoutData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "레이아웃이 성공적으로 저장되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("레이아웃 저장 실패:", error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "레이아웃 저장에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 템플릿 관리
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 템플릿 목록 조회
|
||||
*/
|
||||
async getTemplates(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { type, isPublic } = req.query;
|
||||
const userCompanyCode = (req as any).user?.company_code || "*";
|
||||
|
||||
const templates = await this.screenService.getTemplatesByCompany(
|
||||
userCompanyCode,
|
||||
type as string,
|
||||
isPublic === "true"
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: templates,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("템플릿 목록 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "템플릿 목록을 조회하는 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 생성
|
||||
*/
|
||||
async createTemplate(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const templateData = req.body;
|
||||
const userCompanyCode = (req as any).user?.company_code || "*";
|
||||
const userId = (req as any).user?.user_id || "system";
|
||||
|
||||
templateData.company_code = userCompanyCode;
|
||||
templateData.created_by = userId;
|
||||
|
||||
const template = await this.screenService.createTemplate(templateData);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: template,
|
||||
message: "템플릿이 성공적으로 생성되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("템플릿 생성 실패:", error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "템플릿 생성에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 메뉴 할당 관리
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 화면-메뉴 할당
|
||||
*/
|
||||
async assignScreenToMenu(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { screenId } = req.params;
|
||||
const assignmentData: MenuAssignmentRequest = req.body;
|
||||
const userCompanyCode = (req as any).user?.company_code || "*";
|
||||
const userId = (req as any).user?.user_id || "system";
|
||||
|
||||
// 사용자 회사 코드 자동 설정
|
||||
if (userCompanyCode !== "*") {
|
||||
assignmentData.companyCode = userCompanyCode;
|
||||
}
|
||||
assignmentData.createdBy = userId;
|
||||
|
||||
await this.screenService.assignScreenToMenu(
|
||||
Number(screenId),
|
||||
assignmentData
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "화면이 메뉴에 성공적으로 할당되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("메뉴 할당 실패:", error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "메뉴 할당에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴별 화면 목록 조회
|
||||
*/
|
||||
async getScreensByMenu(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { menuObjid } = req.params;
|
||||
const userCompanyCode = (req as any).user?.company_code || "*";
|
||||
|
||||
const screens = await this.screenService.getScreensByMenu(
|
||||
Number(menuObjid),
|
||||
userCompanyCode
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: screens,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("메뉴별 화면 목록 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "메뉴별 화면 목록을 조회하는 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 테이블 타입 연계
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 테이블 컬럼 정보 조회
|
||||
*/
|
||||
async getTableColumns(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
const columns = await this.screenService.getColumnInfo(tableName);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: columns,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("테이블 컬럼 정보 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "테이블 컬럼 정보를 조회하는 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 컬럼 웹 타입 설정
|
||||
*/
|
||||
async setColumnWebType(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { tableName, columnName } = req.params;
|
||||
const { webType, ...additionalSettings } = req.body;
|
||||
|
||||
await this.screenService.setColumnWebType(
|
||||
tableName,
|
||||
columnName,
|
||||
webType as WebType,
|
||||
additionalSettings
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "컬럼 웹 타입이 성공적으로 설정되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("컬럼 웹 타입 설정 실패:", error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "컬럼 웹 타입 설정에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 목록 조회 (화면 생성용)
|
||||
*/
|
||||
async getTables(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
// PostgreSQL에서 사용 가능한 테이블 목록 조회
|
||||
const tables = await (this.screenService as any).prisma.$queryRaw`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE'
|
||||
ORDER BY table_name
|
||||
`;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: tables,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("테이블 목록 조회 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "테이블 목록을 조회하는 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -443,3 +443,77 @@ export async function getColumnLabels(
|
||||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 컬럼 웹 타입 설정
|
||||
*/
|
||||
export async function updateColumnWebType(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { tableName, columnName } = req.params;
|
||||
const { webType, detailSettings } = req.body;
|
||||
|
||||
logger.info(
|
||||
`=== 컬럼 웹 타입 설정 시작: ${tableName}.${columnName} = ${webType} ===`
|
||||
);
|
||||
|
||||
if (!tableName || !columnName || !webType) {
|
||||
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);
|
||||
await tableManagementService.updateColumnWebType(
|
||||
tableName,
|
||||
columnName,
|
||||
webType,
|
||||
detailSettings
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`컬럼 웹 타입 설정 완료: ${tableName}.${columnName} = ${webType}`
|
||||
);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: true,
|
||||
message: "컬럼 웹 타입이 성공적으로 설정되었습니다.",
|
||||
data: null,
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("컬럼 웹 타입 설정 중 오류 발생:", error);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "컬럼 웹 타입 설정 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "WEB_TYPE_UPDATE_ERROR",
|
||||
details: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
|
||||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
||||
159
backend-node/src/routes/screenManagementRoutes.ts
Normal file
159
backend-node/src/routes/screenManagementRoutes.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import express from "express";
|
||||
import { ScreenManagementController } from "../controllers/screenManagementController";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
|
||||
const router = express.Router();
|
||||
const screenController = new ScreenManagementController();
|
||||
|
||||
// 모든 라우트에 인증 미들웨어 적용
|
||||
router.use(authenticateToken);
|
||||
|
||||
// ========================================
|
||||
// 화면 정의 관리
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* @route POST /screens
|
||||
* @desc 새 화면 생성
|
||||
* @access Private
|
||||
*/
|
||||
router.post("/screens", screenController.createScreen.bind(screenController));
|
||||
|
||||
/**
|
||||
* @route GET /screens
|
||||
* @desc 회사별 화면 목록 조회
|
||||
* @access Private
|
||||
*/
|
||||
router.get("/screens", screenController.getScreens.bind(screenController));
|
||||
|
||||
/**
|
||||
* @route GET /screens/:screenId
|
||||
* @desc 특정 화면 조회
|
||||
* @access Private
|
||||
*/
|
||||
router.get(
|
||||
"/screens/:screenId",
|
||||
screenController.getScreen.bind(screenController)
|
||||
);
|
||||
|
||||
/**
|
||||
* @route PUT /screens/:screenId
|
||||
* @desc 화면 정보 수정
|
||||
* @access Private
|
||||
*/
|
||||
router.put(
|
||||
"/screens/:screenId",
|
||||
screenController.updateScreen.bind(screenController)
|
||||
);
|
||||
|
||||
/**
|
||||
* @route DELETE /screens/:screenId
|
||||
* @desc 화면 삭제
|
||||
* @access Private
|
||||
*/
|
||||
router.delete(
|
||||
"/screens/:screenId",
|
||||
screenController.deleteScreen.bind(screenController)
|
||||
);
|
||||
|
||||
// ========================================
|
||||
// 레이아웃 관리
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* @route GET /screens/:screenId/layout
|
||||
* @desc 화면 레이아웃 조회
|
||||
* @access Private
|
||||
*/
|
||||
router.get(
|
||||
"/screens/:screenId/layout",
|
||||
screenController.getLayout.bind(screenController)
|
||||
);
|
||||
|
||||
/**
|
||||
* @route POST /screens/:screenId/layout
|
||||
* @desc 화면 레이아웃 저장
|
||||
* @access Private
|
||||
*/
|
||||
router.post(
|
||||
"/screens/:screenId/layout",
|
||||
screenController.saveLayout.bind(screenController)
|
||||
);
|
||||
|
||||
// ========================================
|
||||
// 템플릿 관리
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* @route GET /templates
|
||||
* @desc 회사별 템플릿 목록 조회
|
||||
* @access Private
|
||||
*/
|
||||
router.get("/templates", screenController.getTemplates.bind(screenController));
|
||||
|
||||
/**
|
||||
* @route POST /templates
|
||||
* @desc 새 템플릿 생성
|
||||
* @access Private
|
||||
*/
|
||||
router.post(
|
||||
"/templates",
|
||||
screenController.createTemplate.bind(screenController)
|
||||
);
|
||||
|
||||
// ========================================
|
||||
// 메뉴 할당 관리
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* @route POST /screens/:screenId/menu-assignments
|
||||
* @desc 화면을 메뉴에 할당
|
||||
* @access Private
|
||||
*/
|
||||
router.post(
|
||||
"/screens/:screenId/menu-assignments",
|
||||
screenController.assignScreenToMenu.bind(screenController)
|
||||
);
|
||||
|
||||
/**
|
||||
* @route GET /menus/:menuObjid/screens
|
||||
* @desc 메뉴별 할당된 화면 목록 조회
|
||||
* @access Private
|
||||
*/
|
||||
router.get(
|
||||
"/menus/:menuObjid/screens",
|
||||
screenController.getScreensByMenu.bind(screenController)
|
||||
);
|
||||
|
||||
// ========================================
|
||||
// 테이블 타입 연계
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* @route GET /tables
|
||||
* @desc 사용 가능한 테이블 목록 조회
|
||||
* @access Private
|
||||
*/
|
||||
router.get("/tables", screenController.getTables.bind(screenController));
|
||||
|
||||
/**
|
||||
* @route GET /tables/:tableName/columns
|
||||
* @desc 테이블의 컬럼 정보 조회
|
||||
* @access Private
|
||||
*/
|
||||
router.get(
|
||||
"/tables/:tableName/columns",
|
||||
screenController.getTableColumns.bind(screenController)
|
||||
);
|
||||
|
||||
/**
|
||||
* @route PUT /tables/:tableName/columns/:columnName/web-type
|
||||
* @desc 컬럼의 웹 타입 설정
|
||||
* @access Private
|
||||
*/
|
||||
router.put(
|
||||
"/tables/:tableName/columns/:columnName/web-type",
|
||||
screenController.setColumnWebType.bind(screenController)
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
updateAllColumnSettings,
|
||||
getTableLabels,
|
||||
getColumnLabels,
|
||||
updateColumnWebType,
|
||||
} from "../controllers/tableManagementController";
|
||||
|
||||
const router = express.Router();
|
||||
@@ -53,4 +54,13 @@ router.get("/tables/:tableName/labels", getTableLabels);
|
||||
*/
|
||||
router.get("/tables/:tableName/columns/:columnName/labels", getColumnLabels);
|
||||
|
||||
/**
|
||||
* 컬럼 웹 타입 설정
|
||||
* PUT /api/table-management/tables/:tableName/columns/:columnName/web-type
|
||||
*/
|
||||
router.put(
|
||||
"/tables/:tableName/columns/:columnName/web-type",
|
||||
updateColumnWebType
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
618
backend-node/src/services/screenManagementService.ts
Normal file
618
backend-node/src/services/screenManagementService.ts
Normal file
@@ -0,0 +1,618 @@
|
||||
import prisma from "../config/database";
|
||||
import {
|
||||
ScreenDefinition,
|
||||
CreateScreenRequest,
|
||||
UpdateScreenRequest,
|
||||
LayoutData,
|
||||
SaveLayoutRequest,
|
||||
ScreenTemplate,
|
||||
MenuAssignmentRequest,
|
||||
PaginatedResponse,
|
||||
ComponentData,
|
||||
ColumnInfo,
|
||||
ColumnWebTypeSetting,
|
||||
WebType,
|
||||
WidgetData,
|
||||
} from "../types/screen";
|
||||
import { generateId } from "../utils/generateId";
|
||||
|
||||
export class ScreenManagementService {
|
||||
// ========================================
|
||||
// 화면 정의 관리
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 화면 정의 생성
|
||||
*/
|
||||
async createScreen(
|
||||
screenData: CreateScreenRequest,
|
||||
userCompanyCode: string
|
||||
): Promise<ScreenDefinition> {
|
||||
// 화면 코드 중복 확인
|
||||
const existingScreen = await prisma.screen_definitions.findUnique({
|
||||
where: { screen_code: screenData.screenCode },
|
||||
});
|
||||
|
||||
if (existingScreen) {
|
||||
throw new Error("이미 존재하는 화면 코드입니다.");
|
||||
}
|
||||
|
||||
const screen = await prisma.screen_definitions.create({
|
||||
data: {
|
||||
screen_name: screenData.screenName,
|
||||
screen_code: screenData.screenCode,
|
||||
table_name: screenData.tableName,
|
||||
company_code: screenData.companyCode,
|
||||
description: screenData.description,
|
||||
created_by: screenData.createdBy,
|
||||
},
|
||||
});
|
||||
|
||||
return this.mapToScreenDefinition(screen);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회사별 화면 목록 조회 (페이징 지원)
|
||||
*/
|
||||
async getScreensByCompany(
|
||||
companyCode: string,
|
||||
page: number = 1,
|
||||
size: number = 20
|
||||
): Promise<PaginatedResponse<ScreenDefinition>> {
|
||||
const whereClause =
|
||||
companyCode === "*" ? {} : { company_code: companyCode };
|
||||
|
||||
const [screens, total] = await Promise.all([
|
||||
prisma.screen_definitions.findMany({
|
||||
where: whereClause,
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
orderBy: { created_date: "desc" },
|
||||
}),
|
||||
prisma.screen_definitions.count({ where: whereClause }),
|
||||
]);
|
||||
|
||||
return {
|
||||
data: screens.map((screen) => this.mapToScreenDefinition(screen)),
|
||||
pagination: {
|
||||
page,
|
||||
size,
|
||||
total,
|
||||
totalPages: Math.ceil(total / size),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면 정의 조회
|
||||
*/
|
||||
async getScreenById(screenId: number): Promise<ScreenDefinition | null> {
|
||||
const screen = await prisma.screen_definitions.findUnique({
|
||||
where: { screen_id: screenId },
|
||||
});
|
||||
|
||||
return screen ? this.mapToScreenDefinition(screen) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면 정의 수정
|
||||
*/
|
||||
async updateScreen(
|
||||
screenId: number,
|
||||
updateData: UpdateScreenRequest,
|
||||
userCompanyCode: string
|
||||
): Promise<ScreenDefinition> {
|
||||
// 권한 검증
|
||||
const screen = await prisma.screen_definitions.findUnique({
|
||||
where: { screen_id: screenId },
|
||||
});
|
||||
|
||||
if (!screen) {
|
||||
throw new Error("화면을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
if (userCompanyCode !== "*" && userCompanyCode !== screen.company_code) {
|
||||
throw new Error("해당 화면을 수정할 권한이 없습니다.");
|
||||
}
|
||||
|
||||
const updatedScreen = await prisma.screen_definitions.update({
|
||||
where: { screen_id: screenId },
|
||||
data: {
|
||||
screen_name: updateData.screenName,
|
||||
description: updateData.description,
|
||||
is_active: updateData.isActive,
|
||||
updated_by: updateData.updatedBy,
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return this.mapToScreenDefinition(updatedScreen);
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면 정의 삭제
|
||||
*/
|
||||
async deleteScreen(screenId: number, userCompanyCode: string): Promise<void> {
|
||||
// 권한 검증
|
||||
const screen = await prisma.screen_definitions.findUnique({
|
||||
where: { screen_id: screenId },
|
||||
});
|
||||
|
||||
if (!screen) {
|
||||
throw new Error("화면을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
if (userCompanyCode !== "*" && userCompanyCode !== screen.company_code) {
|
||||
throw new Error("해당 화면을 삭제할 권한이 없습니다.");
|
||||
}
|
||||
|
||||
// CASCADE로 인해 관련 레이아웃과 위젯도 자동 삭제됨
|
||||
await prisma.screen_definitions.delete({
|
||||
where: { screen_id: screenId },
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 레이아웃 관리
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 레이아웃 저장
|
||||
*/
|
||||
async saveLayout(
|
||||
screenId: number,
|
||||
layoutData: SaveLayoutRequest
|
||||
): Promise<void> {
|
||||
// 화면 존재 확인
|
||||
const screen = await prisma.screen_definitions.findUnique({
|
||||
where: { screen_id: screenId },
|
||||
});
|
||||
|
||||
if (!screen) {
|
||||
throw new Error("화면을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
// 기존 레이아웃 삭제
|
||||
await prisma.screen_layouts.deleteMany({
|
||||
where: { screen_id: screenId },
|
||||
});
|
||||
|
||||
// 새 레이아웃 저장
|
||||
const layoutPromises = layoutData.components.map((component) =>
|
||||
prisma.screen_layouts.create({
|
||||
data: {
|
||||
screen_id: screenId,
|
||||
component_type: component.type,
|
||||
component_id: component.id,
|
||||
parent_id: component.parentId,
|
||||
position_x: component.position.x,
|
||||
position_y: component.position.y,
|
||||
width: component.size.width,
|
||||
height: component.size.height,
|
||||
properties: component.properties,
|
||||
display_order: component.displayOrder || 0,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(layoutPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 조회
|
||||
*/
|
||||
async getLayout(screenId: number): Promise<LayoutData | null> {
|
||||
const layouts = await prisma.screen_layouts.findMany({
|
||||
where: { screen_id: screenId },
|
||||
orderBy: { display_order: "asc" },
|
||||
});
|
||||
|
||||
if (layouts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const components: ComponentData[] = layouts.map((layout) => {
|
||||
const baseComponent = {
|
||||
id: layout.component_id,
|
||||
type: layout.component_type as any,
|
||||
position: { x: layout.position_x, y: layout.position_y },
|
||||
size: { width: layout.width, height: layout.height },
|
||||
properties: layout.properties as Record<string, any>,
|
||||
displayOrder: layout.display_order,
|
||||
};
|
||||
|
||||
// 컴포넌트 타입별 추가 속성 처리
|
||||
switch (layout.component_type) {
|
||||
case "group":
|
||||
return {
|
||||
...baseComponent,
|
||||
type: "group",
|
||||
title: (layout.properties as any)?.title,
|
||||
backgroundColor: (layout.properties as any)?.backgroundColor,
|
||||
border: (layout.properties as any)?.border,
|
||||
borderRadius: (layout.properties as any)?.borderRadius,
|
||||
shadow: (layout.properties as any)?.shadow,
|
||||
padding: (layout.properties as any)?.padding,
|
||||
margin: (layout.properties as any)?.margin,
|
||||
collapsible: (layout.properties as any)?.collapsible,
|
||||
collapsed: (layout.properties as any)?.collapsed,
|
||||
children: (layout.properties as any)?.children || [],
|
||||
};
|
||||
case "widget":
|
||||
return {
|
||||
...baseComponent,
|
||||
type: "widget",
|
||||
tableName: (layout.properties as any)?.tableName,
|
||||
columnName: (layout.properties as any)?.columnName,
|
||||
widgetType: (layout.properties as any)?.widgetType,
|
||||
label: (layout.properties as any)?.label,
|
||||
placeholder: (layout.properties as any)?.placeholder,
|
||||
required: (layout.properties as any)?.required,
|
||||
readonly: (layout.properties as any)?.readonly,
|
||||
validationRules: (layout.properties as any)?.validationRules,
|
||||
displayProperties: (layout.properties as any)?.displayProperties,
|
||||
};
|
||||
default:
|
||||
return baseComponent;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
components,
|
||||
gridSettings: {
|
||||
columns: 12,
|
||||
gap: 16,
|
||||
padding: 16,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 템플릿 관리
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 템플릿 목록 조회 (회사별)
|
||||
*/
|
||||
async getTemplatesByCompany(
|
||||
companyCode: string,
|
||||
type?: string,
|
||||
isPublic?: boolean
|
||||
): Promise<ScreenTemplate[]> {
|
||||
const whereClause: any = {};
|
||||
|
||||
if (companyCode !== "*") {
|
||||
whereClause.company_code = companyCode;
|
||||
}
|
||||
|
||||
if (type) {
|
||||
whereClause.template_type = type;
|
||||
}
|
||||
|
||||
if (isPublic !== undefined) {
|
||||
whereClause.is_public = isPublic;
|
||||
}
|
||||
|
||||
const templates = await prisma.screen_templates.findMany({
|
||||
where: whereClause,
|
||||
orderBy: { created_date: "desc" },
|
||||
});
|
||||
|
||||
return templates.map(this.mapToScreenTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 생성
|
||||
*/
|
||||
async createTemplate(
|
||||
templateData: Partial<ScreenTemplate>
|
||||
): Promise<ScreenTemplate> {
|
||||
const template = await prisma.screen_templates.create({
|
||||
data: {
|
||||
template_name: templateData.templateName!,
|
||||
template_type: templateData.templateType!,
|
||||
company_code: templateData.companyCode!,
|
||||
description: templateData.description,
|
||||
layout_data: templateData.layoutData
|
||||
? JSON.parse(JSON.stringify(templateData.layoutData))
|
||||
: null,
|
||||
is_public: templateData.isPublic || false,
|
||||
created_by: templateData.createdBy,
|
||||
},
|
||||
});
|
||||
|
||||
return this.mapToScreenTemplate(template);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 메뉴 할당 관리
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 화면-메뉴 할당
|
||||
*/
|
||||
async assignScreenToMenu(
|
||||
screenId: number,
|
||||
assignmentData: MenuAssignmentRequest
|
||||
): Promise<void> {
|
||||
// 중복 할당 방지
|
||||
const existingAssignment = await prisma.screen_menu_assignments.findFirst({
|
||||
where: {
|
||||
screen_id: screenId,
|
||||
menu_objid: assignmentData.menuObjid,
|
||||
company_code: assignmentData.companyCode,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingAssignment) {
|
||||
throw new Error("이미 할당된 화면입니다.");
|
||||
}
|
||||
|
||||
await prisma.screen_menu_assignments.create({
|
||||
data: {
|
||||
screen_id: screenId,
|
||||
menu_objid: assignmentData.menuObjid,
|
||||
company_code: assignmentData.companyCode,
|
||||
display_order: assignmentData.displayOrder || 0,
|
||||
created_by: assignmentData.createdBy,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴별 화면 목록 조회
|
||||
*/
|
||||
async getScreensByMenu(
|
||||
menuObjid: number,
|
||||
companyCode: string
|
||||
): Promise<ScreenDefinition[]> {
|
||||
const assignments = await prisma.screen_menu_assignments.findMany({
|
||||
where: {
|
||||
menu_objid: menuObjid,
|
||||
company_code: companyCode,
|
||||
is_active: "Y",
|
||||
},
|
||||
include: {
|
||||
screen: true,
|
||||
},
|
||||
orderBy: { display_order: "asc" },
|
||||
});
|
||||
|
||||
return assignments.map((assignment) =>
|
||||
this.mapToScreenDefinition(assignment.screen)
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 테이블 타입 연계
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 컬럼 정보 조회 (웹 타입 포함)
|
||||
*/
|
||||
async getColumnInfo(tableName: string): Promise<ColumnInfo[]> {
|
||||
const columns = await prisma.$queryRaw`
|
||||
SELECT
|
||||
c.column_name,
|
||||
COALESCE(cl.column_label, c.column_name) as column_label,
|
||||
c.data_type,
|
||||
COALESCE(cl.web_type, 'text') as web_type,
|
||||
c.is_nullable,
|
||||
c.column_default,
|
||||
c.character_maximum_length,
|
||||
c.numeric_precision,
|
||||
c.numeric_scale,
|
||||
cl.detail_settings,
|
||||
cl.code_category,
|
||||
cl.reference_table,
|
||||
cl.reference_column,
|
||||
cl.is_visible,
|
||||
cl.display_order,
|
||||
cl.description
|
||||
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 COALESCE(cl.display_order, c.ordinal_position)
|
||||
`;
|
||||
|
||||
return columns as ColumnInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 웹 타입 설정
|
||||
*/
|
||||
async setColumnWebType(
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
webType: WebType,
|
||||
additionalSettings?: Partial<ColumnWebTypeSetting>
|
||||
): Promise<void> {
|
||||
await prisma.column_labels.upsert({
|
||||
where: {
|
||||
table_name_column_name: {
|
||||
table_name: tableName,
|
||||
column_name: columnName,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
web_type: webType,
|
||||
column_label: additionalSettings?.columnLabel,
|
||||
detail_settings: additionalSettings?.detailSettings
|
||||
? JSON.stringify(additionalSettings.detailSettings)
|
||||
: null,
|
||||
code_category: additionalSettings?.codeCategory,
|
||||
reference_table: additionalSettings?.referenceTable,
|
||||
reference_column: additionalSettings?.referenceColumn,
|
||||
is_visible: additionalSettings?.isVisible ?? true,
|
||||
display_order: additionalSettings?.displayOrder ?? 0,
|
||||
description: additionalSettings?.description,
|
||||
updated_date: new Date(),
|
||||
},
|
||||
create: {
|
||||
table_name: tableName,
|
||||
column_name: columnName,
|
||||
column_label: additionalSettings?.columnLabel,
|
||||
web_type: webType,
|
||||
detail_settings: additionalSettings?.detailSettings
|
||||
? JSON.stringify(additionalSettings.detailSettings)
|
||||
: null,
|
||||
code_category: additionalSettings?.codeCategory,
|
||||
reference_table: additionalSettings?.referenceTable,
|
||||
reference_column: additionalSettings?.referenceColumn,
|
||||
is_visible: additionalSettings?.isVisible ?? true,
|
||||
display_order: additionalSettings?.displayOrder ?? 0,
|
||||
description: additionalSettings?.description,
|
||||
created_date: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 웹 타입별 위젯 생성
|
||||
*/
|
||||
generateWidgetFromColumn(column: ColumnInfo): WidgetData {
|
||||
const baseWidget = {
|
||||
id: generateId(),
|
||||
tableName: column.tableName,
|
||||
columnName: column.columnName,
|
||||
type: column.webType || "text",
|
||||
label: column.columnLabel || column.columnName,
|
||||
required: column.isNullable === "N",
|
||||
readonly: false,
|
||||
};
|
||||
|
||||
// detail_settings JSON 파싱
|
||||
const detailSettings = column.detailSettings
|
||||
? JSON.parse(column.detailSettings)
|
||||
: {};
|
||||
|
||||
switch (column.webType) {
|
||||
case "text":
|
||||
return {
|
||||
...baseWidget,
|
||||
maxLength: detailSettings.maxLength || column.characterMaximumLength,
|
||||
placeholder: `Enter ${column.columnLabel || column.columnName}`,
|
||||
pattern: detailSettings.pattern,
|
||||
};
|
||||
|
||||
case "number":
|
||||
return {
|
||||
...baseWidget,
|
||||
min: detailSettings.min,
|
||||
max:
|
||||
detailSettings.max ||
|
||||
(column.numericPrecision
|
||||
? Math.pow(10, column.numericPrecision) - 1
|
||||
: undefined),
|
||||
step:
|
||||
detailSettings.step ||
|
||||
(column.numericScale && column.numericScale > 0
|
||||
? Math.pow(10, -column.numericScale)
|
||||
: 1),
|
||||
};
|
||||
|
||||
case "date":
|
||||
return {
|
||||
...baseWidget,
|
||||
format: detailSettings.format || "YYYY-MM-DD",
|
||||
minDate: detailSettings.minDate,
|
||||
maxDate: detailSettings.maxDate,
|
||||
};
|
||||
|
||||
case "code":
|
||||
return {
|
||||
...baseWidget,
|
||||
codeCategory: column.codeCategory,
|
||||
multiple: detailSettings.multiple || false,
|
||||
searchable: detailSettings.searchable || false,
|
||||
};
|
||||
|
||||
case "entity":
|
||||
return {
|
||||
...baseWidget,
|
||||
referenceTable: column.referenceTable,
|
||||
referenceColumn: column.referenceColumn,
|
||||
searchable: detailSettings.searchable || true,
|
||||
multiple: detailSettings.multiple || false,
|
||||
};
|
||||
|
||||
case "textarea":
|
||||
return {
|
||||
...baseWidget,
|
||||
rows: detailSettings.rows || 3,
|
||||
maxLength: detailSettings.maxLength || column.characterMaximumLength,
|
||||
};
|
||||
|
||||
case "select":
|
||||
return {
|
||||
...baseWidget,
|
||||
options: detailSettings.options || [],
|
||||
multiple: detailSettings.multiple || false,
|
||||
searchable: detailSettings.searchable || false,
|
||||
};
|
||||
|
||||
case "checkbox":
|
||||
return {
|
||||
...baseWidget,
|
||||
defaultChecked: detailSettings.defaultChecked || false,
|
||||
label: detailSettings.label || column.columnLabel,
|
||||
};
|
||||
|
||||
case "radio":
|
||||
return {
|
||||
...baseWidget,
|
||||
options: detailSettings.options || [],
|
||||
inline: detailSettings.inline || false,
|
||||
};
|
||||
|
||||
case "file":
|
||||
return {
|
||||
...baseWidget,
|
||||
accept: detailSettings.accept || "*/*",
|
||||
maxSize: detailSettings.maxSize || 10485760, // 10MB
|
||||
multiple: detailSettings.multiple || false,
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
...baseWidget,
|
||||
type: "text",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 유틸리티 메서드
|
||||
// ========================================
|
||||
|
||||
private mapToScreenDefinition(data: any): ScreenDefinition {
|
||||
return {
|
||||
screenId: data.screen_id,
|
||||
screenName: data.screen_name,
|
||||
screenCode: data.screen_code,
|
||||
tableName: data.table_name,
|
||||
companyCode: data.company_code,
|
||||
description: data.description,
|
||||
isActive: data.is_active,
|
||||
createdDate: data.created_date,
|
||||
createdBy: data.created_by,
|
||||
updatedDate: data.updated_date,
|
||||
updatedBy: data.updated_by,
|
||||
};
|
||||
}
|
||||
|
||||
private mapToScreenTemplate(data: any): ScreenTemplate {
|
||||
return {
|
||||
templateId: data.template_id,
|
||||
templateName: data.template_name,
|
||||
templateType: data.template_type,
|
||||
companyCode: data.company_code,
|
||||
description: data.description,
|
||||
layoutData: data.layout_data,
|
||||
isPublic: data.is_public,
|
||||
createdBy: data.created_by,
|
||||
createdDate: data.created_date,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -197,14 +197,18 @@ export class TableManagementService {
|
||||
|
||||
// 각 컬럼 설정을 순차적으로 업데이트
|
||||
for (const columnSetting of columnSettings) {
|
||||
const columnName =
|
||||
columnSetting.columnLabel || columnSetting.columnName;
|
||||
// columnName은 실제 DB 컬럼명을 유지해야 함
|
||||
const columnName = columnSetting.columnName;
|
||||
if (columnName) {
|
||||
await this.updateColumnSettings(
|
||||
tableName,
|
||||
columnName,
|
||||
columnSetting
|
||||
);
|
||||
} else {
|
||||
logger.warn(
|
||||
`컬럼명이 누락된 설정: ${JSON.stringify(columnSetting)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,4 +314,166 @@ export class TableManagementService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 컬럼 웹 타입 설정
|
||||
*/
|
||||
async updateColumnWebType(
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
webType: string,
|
||||
detailSettings?: Record<string, any>
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.info(
|
||||
`컬럼 웹 타입 설정 시작: ${tableName}.${columnName} = ${webType}`
|
||||
);
|
||||
|
||||
// 웹 타입별 기본 상세 설정 생성
|
||||
const defaultDetailSettings = this.generateDefaultDetailSettings(webType);
|
||||
|
||||
// 사용자 정의 설정과 기본 설정 병합
|
||||
const finalDetailSettings = {
|
||||
...defaultDetailSettings,
|
||||
...detailSettings,
|
||||
};
|
||||
|
||||
// column_labels 테이블에 해당 컬럼이 있는지 확인
|
||||
const checkQuery = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM column_labels
|
||||
WHERE table_name = $1 AND column_name = $2
|
||||
`;
|
||||
|
||||
const checkResult = await this.client.query(checkQuery, [
|
||||
tableName,
|
||||
columnName,
|
||||
]);
|
||||
|
||||
if (checkResult.rows[0].count > 0) {
|
||||
// 기존 컬럼 라벨 업데이트
|
||||
const updateQuery = `
|
||||
UPDATE column_labels
|
||||
SET web_type = $3, detail_settings = $4, updated_date = NOW()
|
||||
WHERE table_name = $1 AND column_name = $2
|
||||
`;
|
||||
|
||||
await this.client.query(updateQuery, [
|
||||
tableName,
|
||||
columnName,
|
||||
webType,
|
||||
JSON.stringify(finalDetailSettings),
|
||||
]);
|
||||
logger.info(
|
||||
`컬럼 웹 타입 업데이트 완료: ${tableName}.${columnName} = ${webType}`
|
||||
);
|
||||
} else {
|
||||
// 새로운 컬럼 라벨 생성
|
||||
const insertQuery = `
|
||||
INSERT INTO column_labels (
|
||||
table_name, column_name, web_type, detail_settings, created_date, updated_date
|
||||
) VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||
`;
|
||||
|
||||
await this.client.query(insertQuery, [
|
||||
tableName,
|
||||
columnName,
|
||||
webType,
|
||||
JSON.stringify(finalDetailSettings),
|
||||
]);
|
||||
logger.info(
|
||||
`컬럼 라벨 생성 및 웹 타입 설정 완료: ${tableName}.${columnName} = ${webType}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`컬럼 웹 타입 설정 중 오류 발생: ${tableName}.${columnName}`,
|
||||
error
|
||||
);
|
||||
throw new Error(
|
||||
`컬럼 웹 타입 설정 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 웹 타입별 기본 상세 설정 생성
|
||||
*/
|
||||
private generateDefaultDetailSettings(webType: string): Record<string, any> {
|
||||
switch (webType) {
|
||||
case "text":
|
||||
return {
|
||||
maxLength: 255,
|
||||
pattern: null,
|
||||
placeholder: null,
|
||||
};
|
||||
|
||||
case "number":
|
||||
return {
|
||||
min: null,
|
||||
max: null,
|
||||
step: 1,
|
||||
precision: 2,
|
||||
};
|
||||
|
||||
case "date":
|
||||
return {
|
||||
format: "YYYY-MM-DD",
|
||||
minDate: null,
|
||||
maxDate: null,
|
||||
};
|
||||
|
||||
case "code":
|
||||
return {
|
||||
codeCategory: null,
|
||||
displayFormat: "label",
|
||||
searchable: true,
|
||||
multiple: false,
|
||||
};
|
||||
|
||||
case "entity":
|
||||
return {
|
||||
referenceTable: null,
|
||||
referenceColumn: null,
|
||||
searchable: true,
|
||||
multiple: false,
|
||||
};
|
||||
|
||||
case "textarea":
|
||||
return {
|
||||
rows: 3,
|
||||
maxLength: 1000,
|
||||
placeholder: null,
|
||||
};
|
||||
|
||||
case "select":
|
||||
return {
|
||||
options: [],
|
||||
multiple: false,
|
||||
searchable: false,
|
||||
};
|
||||
|
||||
case "checkbox":
|
||||
return {
|
||||
defaultChecked: false,
|
||||
label: null,
|
||||
};
|
||||
|
||||
case "radio":
|
||||
return {
|
||||
options: [],
|
||||
inline: false,
|
||||
};
|
||||
|
||||
case "file":
|
||||
return {
|
||||
accept: "*/*",
|
||||
maxSize: 10485760, // 10MB
|
||||
multiple: false,
|
||||
};
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
284
backend-node/src/types/screen.ts
Normal file
284
backend-node/src/types/screen.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
// 화면관리 시스템 타입 정의
|
||||
|
||||
// 기본 컴포넌트 타입
|
||||
export type ComponentType = "container" | "row" | "column" | "widget" | "group";
|
||||
|
||||
// 웹 타입 정의
|
||||
export type WebType =
|
||||
| "text"
|
||||
| "number"
|
||||
| "date"
|
||||
| "code"
|
||||
| "entity"
|
||||
| "textarea"
|
||||
| "select"
|
||||
| "checkbox"
|
||||
| "radio"
|
||||
| "file";
|
||||
|
||||
// 위치 정보
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
// 크기 정보
|
||||
export interface Size {
|
||||
width: number; // 1-12 그리드
|
||||
height: number; // 픽셀
|
||||
}
|
||||
|
||||
// 기본 컴포넌트 인터페이스
|
||||
export interface BaseComponent {
|
||||
id: string;
|
||||
type: ComponentType;
|
||||
position: Position;
|
||||
size: Size;
|
||||
properties?: Record<string, any>;
|
||||
displayOrder?: number;
|
||||
parentId?: string; // 부모 컴포넌트 ID 추가
|
||||
}
|
||||
|
||||
// 컨테이너 컴포넌트
|
||||
export interface ContainerComponent extends BaseComponent {
|
||||
type: "container";
|
||||
backgroundColor?: string;
|
||||
border?: string;
|
||||
borderRadius?: number;
|
||||
shadow?: string;
|
||||
padding?: number;
|
||||
margin?: number;
|
||||
}
|
||||
|
||||
// 그룹 컴포넌트
|
||||
export interface GroupComponent extends BaseComponent {
|
||||
type: "group";
|
||||
title?: string;
|
||||
backgroundColor?: string;
|
||||
border?: string;
|
||||
borderRadius?: number;
|
||||
shadow?: string;
|
||||
padding?: number;
|
||||
margin?: number;
|
||||
collapsible?: boolean;
|
||||
collapsed?: boolean;
|
||||
children: string[]; // 포함된 컴포넌트 ID 목록
|
||||
}
|
||||
|
||||
// 행 컴포넌트
|
||||
export interface RowComponent extends BaseComponent {
|
||||
type: "row";
|
||||
columns: number; // 1-12
|
||||
gap: number;
|
||||
alignItems: "start" | "center" | "end";
|
||||
justifyContent: "start" | "center" | "end" | "space-between";
|
||||
}
|
||||
|
||||
// 컬럼 컴포넌트
|
||||
export interface ColumnComponent extends BaseComponent {
|
||||
type: "column";
|
||||
offset?: number;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
// 위젯 컴포넌트
|
||||
export interface WidgetComponent extends BaseComponent {
|
||||
type: "widget";
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
widgetType: WebType;
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
required: boolean;
|
||||
readonly: boolean;
|
||||
validationRules?: ValidationRule[];
|
||||
displayProperties?: Record<string, any>;
|
||||
}
|
||||
|
||||
// 컴포넌트 유니온 타입
|
||||
export type ComponentData =
|
||||
| ContainerComponent
|
||||
| GroupComponent
|
||||
| RowComponent
|
||||
| ColumnComponent
|
||||
| WidgetComponent;
|
||||
|
||||
// 레이아웃 데이터
|
||||
export interface LayoutData {
|
||||
components: ComponentData[];
|
||||
gridSettings?: GridSettings;
|
||||
}
|
||||
|
||||
// 그리드 설정
|
||||
export interface GridSettings {
|
||||
columns: number; // 기본값: 12
|
||||
gap: number; // 기본값: 16px
|
||||
padding: number; // 기본값: 16px
|
||||
}
|
||||
|
||||
// 유효성 검증 규칙
|
||||
export interface ValidationRule {
|
||||
type:
|
||||
| "required"
|
||||
| "minLength"
|
||||
| "maxLength"
|
||||
| "pattern"
|
||||
| "min"
|
||||
| "max"
|
||||
| "email"
|
||||
| "url";
|
||||
value?: any;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 화면 정의
|
||||
export interface ScreenDefinition {
|
||||
screenId: number;
|
||||
screenName: string;
|
||||
screenCode: string;
|
||||
tableName: string;
|
||||
companyCode: string;
|
||||
description?: string;
|
||||
isActive: string;
|
||||
createdDate: Date;
|
||||
createdBy?: string;
|
||||
updatedDate: Date;
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
// 화면 생성 요청
|
||||
export interface CreateScreenRequest {
|
||||
screenName: string;
|
||||
screenCode: string;
|
||||
tableName: string;
|
||||
companyCode: string;
|
||||
description?: string;
|
||||
createdBy?: string;
|
||||
}
|
||||
|
||||
// 화면 수정 요청
|
||||
export interface UpdateScreenRequest {
|
||||
screenName?: string;
|
||||
description?: string;
|
||||
isActive?: string;
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
// 레이아웃 저장 요청
|
||||
export interface SaveLayoutRequest {
|
||||
components: ComponentData[];
|
||||
gridSettings?: GridSettings;
|
||||
}
|
||||
|
||||
// 화면 템플릿
|
||||
export interface ScreenTemplate {
|
||||
templateId: number;
|
||||
templateName: string;
|
||||
templateType: string;
|
||||
companyCode: string;
|
||||
description?: string;
|
||||
layoutData?: LayoutData;
|
||||
isPublic: boolean;
|
||||
createdBy?: string;
|
||||
createdDate: Date;
|
||||
}
|
||||
|
||||
// 메뉴 할당 요청
|
||||
export interface MenuAssignmentRequest {
|
||||
menuObjid: number;
|
||||
companyCode: string;
|
||||
displayOrder?: number;
|
||||
createdBy?: string;
|
||||
}
|
||||
|
||||
// 드래그 상태
|
||||
export interface DragState {
|
||||
isDragging: boolean;
|
||||
draggedItem: ComponentData | null;
|
||||
dragSource: "toolbox" | "canvas";
|
||||
dropTarget: string | null;
|
||||
dropZone?: DropZone;
|
||||
}
|
||||
|
||||
// 드롭 영역
|
||||
export interface DropZone {
|
||||
id: string;
|
||||
accepts: ComponentType[];
|
||||
position: Position;
|
||||
size: Size;
|
||||
}
|
||||
|
||||
// 그룹화 상태
|
||||
export interface GroupState {
|
||||
isGrouping: boolean;
|
||||
selectedComponents: string[];
|
||||
groupTarget: string | null;
|
||||
groupMode: "create" | "add" | "remove";
|
||||
}
|
||||
|
||||
// 컬럼 정보 (테이블 타입관리 연계용)
|
||||
export interface ColumnInfo {
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
columnLabel?: string;
|
||||
dataType: string;
|
||||
webType?: WebType;
|
||||
isNullable: string;
|
||||
columnDefault?: string;
|
||||
characterMaximumLength?: number;
|
||||
numericPrecision?: number;
|
||||
numericScale?: number;
|
||||
detailSettings?: string; // JSON 문자열
|
||||
codeCategory?: string;
|
||||
referenceTable?: string;
|
||||
referenceColumn?: string;
|
||||
isVisible?: boolean;
|
||||
displayOrder?: number;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// 웹 타입 설정
|
||||
export interface ColumnWebTypeSetting {
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
webType: WebType;
|
||||
columnLabel?: string;
|
||||
detailSettings?: Record<string, any>;
|
||||
codeCategory?: string;
|
||||
referenceTable?: string;
|
||||
referenceColumn?: string;
|
||||
isVisible?: boolean;
|
||||
displayOrder?: number;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// 위젯 데이터
|
||||
export interface WidgetData {
|
||||
id: string;
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
type: WebType;
|
||||
label: string;
|
||||
required: boolean;
|
||||
readonly: boolean;
|
||||
[key: string]: any; // 추가 속성들
|
||||
}
|
||||
|
||||
// API 응답 타입
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
errorCode?: string;
|
||||
}
|
||||
|
||||
// 페이지네이션 응답
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
pagination: {
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
@@ -107,6 +107,11 @@ export const WEB_TYPE_OPTIONS = [
|
||||
label: "entity",
|
||||
description: "엔티티 참조 (참조테이블 지정)",
|
||||
},
|
||||
{ value: "textarea", label: "textarea", description: "여러 줄 텍스트" },
|
||||
{ value: "select", label: "select", description: "드롭다운 선택" },
|
||||
{ value: "checkbox", label: "checkbox", description: "체크박스" },
|
||||
{ value: "radio", label: "radio", description: "라디오 버튼" },
|
||||
{ value: "file", label: "file", description: "파일 업로드" },
|
||||
] as const;
|
||||
|
||||
export type WebType = (typeof WEB_TYPE_OPTIONS)[number]["value"];
|
||||
|
||||
56
backend-node/src/utils/generateId.ts
Normal file
56
backend-node/src/utils/generateId.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 고유 ID 생성 유틸리티
|
||||
*/
|
||||
|
||||
/**
|
||||
* UUID v4 생성
|
||||
*/
|
||||
export function generateUUID(): string {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 짧은 고유 ID 생성 (8자리)
|
||||
*/
|
||||
export function generateShortId(): string {
|
||||
return Math.random().toString(36).substring(2, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 긴 고유 ID 생성 (16자리)
|
||||
*/
|
||||
export function generateLongId(): string {
|
||||
return Math.random().toString(36).substring(2, 18);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 ID 생성 (UUID v4)
|
||||
*/
|
||||
export function generateId(): string {
|
||||
return generateUUID();
|
||||
}
|
||||
|
||||
/**
|
||||
* 타임스탬프 기반 ID 생성
|
||||
*/
|
||||
export function generateTimestampId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substring(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 컴포넌트 ID 생성 (화면관리 시스템용)
|
||||
*/
|
||||
export function generateComponentId(prefix: string = "comp"): string {
|
||||
return `${prefix}_${generateShortId()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면 ID 생성 (화면관리 시스템용)
|
||||
*/
|
||||
export function generateScreenId(prefix: string = "screen"): string {
|
||||
return `${prefix}_${generateShortId()}`;
|
||||
}
|
||||
Reference in New Issue
Block a user