화면관리 테이블 타입관리 연계

This commit is contained in:
kjs 2025-09-01 14:00:31 +09:00
parent 42dbfd98f8
commit ca56cff114
9 changed files with 1330 additions and 1434 deletions

View File

@ -1,455 +1,152 @@
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";
import { Response } from "express";
import { screenManagementService } from "../services/screenManagementService";
import { AuthenticatedRequest } from "../types/auth";
export class ScreenManagementController {
private screenService: ScreenManagementService;
constructor() {
this.screenService = new ScreenManagementService();
// 화면 목록 조회
export const getScreens = async (req: AuthenticatedRequest, res: Response) => {
try {
const { companyCode } = req.user as any;
const screens = await screenManagementService.getScreens(companyCode);
res.json({ success: true, data: screens });
} catch (error) {
console.error("화면 목록 조회 실패:", error);
res
.status(500)
.json({ success: false, message: "화면 목록 조회에 실패했습니다." });
}
};
// ========================================
// 화면 정의 관리
// ========================================
/**
* ()
*/
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",
});
}
// 화면 생성
export const createScreen = async (
req: AuthenticatedRequest,
res: Response
) => {
try {
const { companyCode } = req.user as any;
const screenData = { ...req.body, companyCode };
const newScreen = await screenManagementService.createScreen(
screenData,
companyCode
);
res.status(201).json({ success: true, data: newScreen });
} catch (error) {
console.error("화면 생성 실패:", error);
res
.status(500)
.json({ success: false, message: "화면 생성에 실패했습니다." });
}
};
/**
*
*/
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 : "화면 생성에 실패했습니다.",
});
}
// 화면 수정
export const updateScreen = async (
req: AuthenticatedRequest,
res: Response
) => {
try {
const { id } = req.params;
const { companyCode } = req.user as any;
const updateData = { ...req.body, companyCode };
const updatedScreen = await screenManagementService.updateScreen(
parseInt(id),
updateData,
companyCode
);
res.json({ success: true, data: updatedScreen });
} catch (error) {
console.error("화면 수정 실패:", error);
res
.status(500)
.json({ success: false, 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",
});
}
// 화면 삭제
export const deleteScreen = async (
req: AuthenticatedRequest,
res: Response
) => {
try {
const { id } = req.params;
const { companyCode } = req.user as any;
await screenManagementService.deleteScreen(parseInt(id), companyCode);
res.json({ success: true, message: "화면이 삭제되었습니다." });
} catch (error) {
console.error("화면 삭제 실패:", error);
res
.status(500)
.json({ success: false, message: "화면 삭제에 실패했습니다." });
}
};
/**
*
*/
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 : "화면 수정에 실패했습니다.",
});
}
// 테이블 목록 조회
export const getTables = async (req: AuthenticatedRequest, res: Response) => {
try {
const { companyCode } = req.user as any;
const tables = await screenManagementService.getTables(companyCode);
res.json({ success: true, data: tables });
} catch (error) {
console.error("테이블 목록 조회 실패:", error);
res
.status(500)
.json({ success: false, 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 : "화면 삭제에 실패했습니다.",
});
}
// 테이블 컬럼 정보 조회
export const getTableColumns = async (
req: AuthenticatedRequest,
res: Response
) => {
try {
const { tableName } = req.params;
const { companyCode } = req.user as any;
const columns = await screenManagementService.getTableColumns(
tableName,
companyCode
);
res.json({ success: true, data: columns });
} catch (error) {
console.error("테이블 컬럼 조회 실패:", error);
res
.status(500)
.json({ success: false, 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",
});
}
// 레이아웃 저장
export const saveLayout = async (req: AuthenticatedRequest, res: Response) => {
try {
const { screenId } = req.params;
const { companyCode } = req.user as any;
const layoutData = req.body;
const savedLayout = await screenManagementService.saveLayout(
parseInt(screenId),
layoutData,
companyCode
);
res.json({ success: true, data: savedLayout });
} catch (error) {
console.error("레이아웃 저장 실패:", error);
res
.status(500)
.json({ success: false, message: "레이아웃 저장에 실패했습니다." });
}
};
/**
*
*/
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
: "레이아웃 저장에 실패했습니다.",
});
}
// 레이아웃 조회
export const getLayout = async (req: AuthenticatedRequest, res: Response) => {
try {
const { screenId } = req.params;
const { companyCode } = req.user as any;
const layout = await screenManagementService.getLayout(
parseInt(screenId),
companyCode
);
res.json({ success: true, data: layout });
} catch (error) {
console.error("레이아웃 조회 실패:", error);
res
.status(500)
.json({ success: false, 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",
});
}
}
}
};

View File

@ -1,159 +1,33 @@
import express from "express";
import { ScreenManagementController } from "../controllers/screenManagementController";
import { authenticateToken } from "../middleware/authMiddleware";
import {
getScreens,
createScreen,
updateScreen,
deleteScreen,
getTables,
getTableColumns,
saveLayout,
getLayout,
} from "../controllers/screenManagementController";
const router = express.Router();
const screenController = new ScreenManagementController();
// 모든 라우트에 인증 미들웨어 적용
router.use(authenticateToken);
// ========================================
// 화면 정의 관리
// ========================================
// 화면 관리
router.get("/screens", getScreens);
router.post("/screens", createScreen);
router.put("/screens/:id", updateScreen);
router.delete("/screens/:id", deleteScreen);
/**
* @route POST /screens
* @desc
* @access Private
*/
router.post("/screens", screenController.createScreen.bind(screenController));
// 테이블 관리
router.get("/tables", getTables);
router.get("/tables/:tableName/columns", getTableColumns);
/**
* @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)
);
router.post("/screens/:screenId/layout", saveLayout);
router.get("/screens/:screenId/layout", getLayout);
export default router;

View File

@ -16,6 +16,13 @@ import {
} from "../types/screen";
import { generateId } from "../utils/generateId";
// 백엔드에서 사용할 테이블 정보 타입
interface TableInfo {
tableName: string;
tableLabel: string;
columns: ColumnInfo[];
}
export class ScreenManagementService {
// ========================================
// 화면 정의 관리
@ -83,6 +90,21 @@ export class ScreenManagementService {
};
}
/**
* ( )
*/
async getScreens(companyCode: string): Promise<ScreenDefinition[]> {
const whereClause =
companyCode === "*" ? {} : { company_code: companyCode };
const screens = await prisma.screen_definitions.findMany({
where: whereClause,
orderBy: { created_date: "desc" },
});
return screens.map((screen) => this.mapToScreenDefinition(screen));
}
/**
*
*/
@ -102,56 +124,225 @@ export class ScreenManagementService {
updateData: UpdateScreenRequest,
userCompanyCode: string
): Promise<ScreenDefinition> {
// 권한 검증
const screen = await prisma.screen_definitions.findUnique({
// 권한 확인
const existingScreen = await prisma.screen_definitions.findUnique({
where: { screen_id: screenId },
});
if (!screen) {
if (!existingScreen) {
throw new Error("화면을 찾을 수 없습니다.");
}
if (userCompanyCode !== "*" && userCompanyCode !== screen.company_code) {
throw new Error("해당 화면을 수정할 권한이 없습니다.");
if (
userCompanyCode !== "*" &&
existingScreen.company_code !== userCompanyCode
) {
throw new Error("이 화면을 수정할 권한이 없습니다.");
}
const updatedScreen = await prisma.screen_definitions.update({
const screen = await prisma.screen_definitions.update({
where: { screen_id: screenId },
data: {
screen_name: updateData.screenName,
description: updateData.description,
is_active: updateData.isActive,
is_active: updateData.isActive ? "Y" : "N",
updated_by: updateData.updatedBy,
updated_date: new Date(),
},
});
return this.mapToScreenDefinition(updatedScreen);
return this.mapToScreenDefinition(screen);
}
/**
*
*/
async deleteScreen(screenId: number, userCompanyCode: string): Promise<void> {
// 권한 검증
const screen = await prisma.screen_definitions.findUnique({
// 권한 확인
const existingScreen = await prisma.screen_definitions.findUnique({
where: { screen_id: screenId },
});
if (!screen) {
if (!existingScreen) {
throw new Error("화면을 찾을 수 없습니다.");
}
if (userCompanyCode !== "*" && userCompanyCode !== screen.company_code) {
throw new Error("해당 화면을 삭제할 권한이 없습니다.");
if (
userCompanyCode !== "*" &&
existingScreen.company_code !== userCompanyCode
) {
throw new Error("이 화면을 삭제할 권한이 없습니다.");
}
// CASCADE로 인해 관련 레이아웃과 위젯도 자동 삭제됨
await prisma.screen_definitions.delete({
where: { screen_id: screenId },
});
}
// ========================================
// 테이블 관리
// ========================================
/**
*
*/
async getTables(companyCode: string): Promise<TableInfo[]> {
try {
// PostgreSQL에서 사용 가능한 테이블 목록 조회
const tables = await prisma.$queryRaw<Array<{ table_name: string }>>`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name
`;
// 각 테이블의 컬럼 정보도 함께 조회
const tableInfos: TableInfo[] = [];
for (const table of tables) {
const columns = await this.getTableColumns(
table.table_name,
companyCode
);
if (columns.length > 0) {
tableInfos.push({
tableName: table.table_name,
tableLabel: this.getTableLabel(table.table_name),
columns: columns,
});
}
}
return tableInfos;
} catch (error) {
console.error("테이블 목록 조회 실패:", error);
throw new Error("테이블 목록을 조회할 수 없습니다.");
}
}
/**
*
*/
async getTableColumns(
tableName: string,
companyCode: string
): Promise<ColumnInfo[]> {
try {
// 테이블 컬럼 정보 조회
const columns = await prisma.$queryRaw<
Array<{
column_name: string;
data_type: string;
is_nullable: string;
column_default: string | null;
character_maximum_length: number | null;
numeric_precision: number | null;
numeric_scale: number | null;
}>
>`
SELECT
column_name,
data_type,
is_nullable,
column_default,
character_maximum_length,
numeric_precision,
numeric_scale
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = ${tableName}
ORDER BY ordinal_position
`;
// column_labels 테이블에서 웹타입 정보 조회 (있는 경우)
const webTypeInfo = await prisma.column_labels.findMany({
where: { table_name: tableName },
select: {
column_name: true,
web_type: true,
column_label: true,
detail_settings: true,
},
});
// 컬럼 정보 매핑
return columns.map((column) => {
const webTypeData = webTypeInfo.find(
(wt) => wt.column_name === column.column_name
);
return {
tableName: tableName,
columnName: column.column_name,
columnLabel:
webTypeData?.column_label ||
this.getColumnLabel(column.column_name),
dataType: column.data_type,
webType:
(webTypeData?.web_type as WebType) ||
this.inferWebType(column.data_type),
isNullable: column.is_nullable,
columnDefault: column.column_default || undefined,
characterMaximumLength: column.character_maximum_length || undefined,
numericPrecision: column.numeric_precision || undefined,
numericScale: column.numeric_scale || undefined,
detailSettings: webTypeData?.detail_settings || undefined,
};
});
} catch (error) {
console.error("테이블 컬럼 조회 실패:", error);
throw new Error("테이블 컬럼 정보를 조회할 수 없습니다.");
}
}
/**
*
*/
private getTableLabel(tableName: string): string {
// snake_case를 읽기 쉬운 형태로 변환
return tableName
.replace(/_/g, " ")
.replace(/\b\w/g, (l) => l.toUpperCase())
.replace(/\s+/g, " ")
.trim();
}
/**
*
*/
private getColumnLabel(columnName: string): string {
// snake_case를 읽기 쉬운 형태로 변환
return columnName
.replace(/_/g, " ")
.replace(/\b\w/g, (l) => l.toUpperCase())
.replace(/\s+/g, " ")
.trim();
}
/**
*
*/
private inferWebType(dataType: string): WebType {
const lowerType = dataType.toLowerCase();
if (lowerType.includes("char") || lowerType.includes("text")) {
return "text";
} else if (
lowerType.includes("int") ||
lowerType.includes("numeric") ||
lowerType.includes("decimal")
) {
return "number";
} else if (lowerType.includes("date") || lowerType.includes("time")) {
return "date";
} else if (lowerType.includes("bool")) {
return "checkbox";
} else {
return "text";
}
}
// ========================================
// 레이아웃 관리
// ========================================
@ -161,109 +352,107 @@ export class ScreenManagementService {
*/
async saveLayout(
screenId: number,
layoutData: SaveLayoutRequest
layoutData: LayoutData,
companyCode: string
): Promise<void> {
// 화면 존재 확인
const screen = await prisma.screen_definitions.findUnique({
// 권한 확인
const existingScreen = await prisma.screen_definitions.findUnique({
where: { screen_id: screenId },
});
if (!screen) {
if (!existingScreen) {
throw new Error("화면을 찾을 수 없습니다.");
}
if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
throw new Error("이 화면의 레이아웃을 저장할 권한이 없습니다.");
}
// 기존 레이아웃 삭제
await prisma.screen_layouts.deleteMany({
where: { screen_id: screenId },
});
// 새 레이아웃 저장
const layoutPromises = layoutData.components.map((component) =>
prisma.screen_layouts.create({
for (const component of layoutData.components) {
const { id, ...componentData } = component;
// Prisma JSON 필드에 맞는 타입으로 변환
const properties: any = {
...componentData,
position: {
x: component.position.x,
y: component.position.y,
},
size: {
width: component.size.width,
height: component.size.height,
},
};
await prisma.screen_layouts.create({
data: {
screen_id: screenId,
component_type: component.type,
component_id: component.id,
parent_id: component.parentId,
parent_id: component.parentId || null,
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,
properties: properties,
},
})
);
await Promise.all(layoutPromises);
});
}
}
/**
*
*/
async getLayout(screenId: number): Promise<LayoutData | null> {
async getLayout(
screenId: number,
companyCode: string
): Promise<LayoutData | null> {
// 권한 확인
const existingScreen = await prisma.screen_definitions.findUnique({
where: { screen_id: screenId },
});
if (!existingScreen) {
return null;
}
if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
throw new Error("이 화면의 레이아웃을 조회할 권한이 없습니다.");
}
const layouts = await prisma.screen_layouts.findMany({
where: { screen_id: screenId },
orderBy: { display_order: "asc" },
});
if (layouts.length === 0) {
return null;
return {
components: [],
gridSettings: { columns: 12, gap: 16, padding: 16 },
};
}
const components: ComponentData[] = layouts.map((layout) => {
const baseComponent = {
const properties = layout.properties as any;
return {
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,
parentId: layout.parent_id,
...properties,
};
// 컴포넌트 타입별 추가 속성 처리
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,
},
gridSettings: { columns: 12, gap: 16, padding: 16 },
};
}
@ -616,3 +805,6 @@ export class ScreenManagementService {
};
}
}
// 서비스 인스턴스 export
export const screenManagementService = new ScreenManagementService();

View File

@ -1,122 +1,178 @@
"use client";
import { useState, useEffect } from "react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Plus, Grid3X3, Palette, Settings, FileText } from "lucide-react";
import { Plus, ArrowLeft, ArrowRight, CheckCircle, Circle } from "lucide-react";
import ScreenList from "@/components/screen/ScreenList";
import ScreenDesigner from "@/components/screen/ScreenDesigner";
import TemplateManager from "@/components/screen/TemplateManager";
import { ScreenDefinition } from "@/types/screen";
// 단계별 진행을 위한 타입 정의
type Step = "list" | "design" | "template";
export default function ScreenManagementPage() {
const [currentStep, setCurrentStep] = useState<Step>("list");
const [selectedScreen, setSelectedScreen] = useState<ScreenDefinition | null>(null);
const [activeTab, setActiveTab] = useState("screens");
const [stepHistory, setStepHistory] = useState<Step[]>(["list"]);
// 단계별 제목과 설명
const stepConfig = {
list: {
title: "화면 목록 관리",
description: "생성된 화면들을 확인하고 관리하세요",
icon: "📋",
},
design: {
title: "화면 설계",
description: "드래그앤드롭으로 화면을 설계하세요",
icon: "🎨",
},
template: {
title: "템플릿 관리",
description: "화면 템플릿을 관리하고 재사용하세요",
icon: "📝",
},
};
// 다음 단계로 이동
const goToNextStep = (nextStep: Step) => {
setStepHistory((prev) => [...prev, nextStep]);
setCurrentStep(nextStep);
};
// 이전 단계로 이동
const goToPreviousStep = () => {
if (stepHistory.length > 1) {
const newHistory = stepHistory.slice(0, -1);
const previousStep = newHistory[newHistory.length - 1];
setStepHistory(newHistory);
setCurrentStep(previousStep);
}
};
// 특정 단계로 이동
const goToStep = (step: Step) => {
setCurrentStep(step);
// 해당 단계까지의 히스토리만 유지
const stepIndex = stepHistory.findIndex((s) => s === step);
if (stepIndex !== -1) {
setStepHistory(stepHistory.slice(0, stepIndex + 1));
}
};
// 단계별 진행 상태 확인
const isStepCompleted = (step: Step) => {
return stepHistory.includes(step);
};
// 현재 단계가 마지막 단계인지 확인
const isLastStep = currentStep === "template";
return (
<div className="container mx-auto space-y-6 p-6">
<div className="flex h-full w-full flex-col">
{/* 페이지 헤더 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600"> </p>
<p className="mt-2 text-gray-600"> </p>
</div>
<Button className="bg-blue-600 hover:bg-blue-700">
<Plus className="mr-2 h-4 w-4" />
</Button>
<div className="text-sm text-gray-500">{stepConfig[currentStep].description}</div>
</div>
{/* 메인 컨텐츠 */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="screens" className="flex items-center gap-2">
<Grid3X3 className="h-4 w-4" />
</TabsTrigger>
<TabsTrigger value="designer" className="flex items-center gap-2">
<Palette className="h-4 w-4" />
</TabsTrigger>
<TabsTrigger value="templates" className="flex items-center gap-2">
<FileText className="h-4 w-4" />
릿
</TabsTrigger>
<TabsTrigger value="settings" className="flex items-center gap-2">
<Settings className="h-4 w-4" />
</TabsTrigger>
</TabsList>
{/* 화면 관리 탭 */}
<TabsContent value="screens" className="space-y-4">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ScreenList onScreenSelect={setSelectedScreen} selectedScreen={selectedScreen} />
</CardContent>
</Card>
</TabsContent>
{/* 화면 설계기 탭 */}
<TabsContent value="designer" className="space-y-4">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
{selectedScreen ? (
<ScreenDesigner screen={selectedScreen} />
) : (
<div className="py-12 text-center text-gray-500">
<Palette className="mx-auto mb-4 h-16 w-16 text-gray-300" />
<p> </p>
<p className="text-sm"> </p>
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* 템플릿 관리 탭 */}
<TabsContent value="templates" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>릿 </CardTitle>
</CardHeader>
<CardContent>
<TemplateManager />
</CardContent>
</Card>
</TabsContent>
{/* 설정 탭 */}
<TabsContent value="settings" className="space-y-4">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<h3 className="mb-2 text-lg font-medium"> </h3>
<p className="text-sm text-gray-600"> .</p>
</div>
<div>
<h3 className="mb-2 text-lg font-medium"> </h3>
<p className="text-sm text-gray-600"> .</p>
</div>
<div>
<h3 className="mb-2 text-lg font-medium"> </h3>
<p className="text-sm text-gray-600"> , .</p>
{/* 단계별 진행 표시 */}
<div className="border-b bg-white p-4">
<div className="flex items-center justify-between">
{Object.entries(stepConfig).map(([step, config], index) => (
<div key={step} className="flex items-center">
<div className="flex flex-col items-center">
<button
onClick={() => goToStep(step as Step)}
className={`flex h-12 w-12 items-center justify-center rounded-full border-2 transition-all ${
currentStep === step
? "border-blue-600 bg-blue-600 text-white"
: isStepCompleted(step as Step)
? "border-green-500 bg-green-500 text-white"
: "border-gray-300 bg-white text-gray-400"
} ${isStepCompleted(step as Step) ? "cursor-pointer hover:bg-green-600" : ""}`}
>
{isStepCompleted(step as Step) && currentStep !== step ? (
<CheckCircle className="h-6 w-6" />
) : (
<span className="text-lg">{config.icon}</span>
)}
</button>
<div className="mt-2 text-center">
<div
className={`text-sm font-medium ${
currentStep === step
? "text-blue-600"
: isStepCompleted(step as Step)
? "text-green-600"
: "text-gray-500"
}`}
>
{config.title}
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{index < Object.keys(stepConfig).length - 1 && (
<div className={`mx-4 h-0.5 w-16 ${isStepCompleted(step as Step) ? "bg-green-500" : "bg-gray-300"}`} />
)}
</div>
))}
</div>
</div>
{/* 단계별 내용 */}
<div className="flex-1 overflow-hidden">
{/* 화면 목록 단계 */}
{currentStep === "list" && (
<div className="h-full p-6">
<div className="mb-6 flex items-center justify-between">
<h2 className="text-2xl font-bold">{stepConfig.list.title}</h2>
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => goToNextStep("design")}>
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
<ScreenList
onScreenSelect={setSelectedScreen}
selectedScreen={selectedScreen}
onDesignScreen={(screen) => {
setSelectedScreen(screen);
goToNextStep("design");
}}
/>
</div>
)}
{/* 화면 설계 단계 */}
{currentStep === "design" && (
<div className="h-full">
<ScreenDesigner selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
</div>
)}
{/* 템플릿 관리 단계 */}
{currentStep === "template" && (
<div className="h-full p-6">
<div className="mb-6 flex items-center justify-between">
<h2 className="text-2xl font-bold">{stepConfig.template.title}</h2>
<div className="flex gap-2">
<Button variant="outline" onClick={goToPreviousStep}>
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => goToStep("list")}>
</Button>
</div>
</div>
<TemplateManager selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
</div>
)}
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -12,15 +12,16 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search } from "lucide-react";
import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette } from "lucide-react";
import { ScreenDefinition } from "@/types/screen";
interface ScreenListProps {
onScreenSelect: (screen: ScreenDefinition) => void;
selectedScreen: ScreenDefinition | null;
onDesignScreen: (screen: ScreenDefinition) => void;
}
export default function ScreenList({ onScreenSelect, selectedScreen }: ScreenListProps) {
export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScreen }: ScreenListProps) {
const [screens, setScreens] = useState<ScreenDefinition[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
@ -198,6 +199,10 @@ export default function ScreenList({ onScreenSelect, selectedScreen }: ScreenLis
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onDesignScreen(screen)}>
<Palette className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleView(screen)}>
<Eye className="mr-2 h-4 w-4" />

View File

@ -13,12 +13,18 @@ import { tableTypeApi } from "@/lib/api/screen";
import { useAuth } from "@/hooks/useAuth";
interface TableTypeSelectorProps {
onTableSelect?: (tableName: string) => void;
onColumnSelect?: (column: ColumnInfo) => void;
selectedTable?: string;
onTableChange?: (tableName: string) => void;
onColumnWebTypeChange?: (columnInfo: ColumnInfo) => void;
className?: string;
}
export default function TableTypeSelector({ onTableSelect, onColumnSelect, className }: TableTypeSelectorProps) {
export default function TableTypeSelector({
selectedTable: propSelectedTable,
onTableChange,
onColumnWebTypeChange,
className,
}: TableTypeSelectorProps) {
const { user } = useAuth();
const [tables, setTables] = useState<
Array<{ tableName: string; displayName: string; description: string; columnCount: string }>
@ -140,12 +146,16 @@ export default function TableTypeSelector({ onTableSelect, onColumnSelect, class
// 테이블 선택
const handleTableSelect = (tableName: string) => {
setSelectedTable(tableName);
onTableSelect?.(tableName);
if (onTableChange) {
onTableChange(tableName);
}
};
// 컬럼 선택
const handleColumnSelect = (column: ColumnInfo) => {
onColumnSelect?.(column);
if (onColumnWebTypeChange) {
onColumnWebTypeChange(column);
}
};
// 웹 타입 변경

View File

@ -7,18 +7,26 @@ import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Separator } from "@/components/ui/separator";
import { Search, Plus, Download, Upload, Trash2, Eye, Edit } from "lucide-react";
import { ScreenTemplate, LayoutData } from "@/types/screen";
import { Search, Plus, Download, Upload, Trash2, Eye, Edit, FileText } from "lucide-react";
import { ScreenTemplate, LayoutData, ScreenDefinition } from "@/types/screen";
import { templateApi } from "@/lib/api/screen";
import { useAuth } from "@/hooks/useAuth";
interface TemplateManagerProps {
selectedScreen: ScreenDefinition | null;
onBackToList: () => void;
onTemplateSelect?: (template: ScreenTemplate) => void;
onTemplateApply?: (template: ScreenTemplate) => void;
className?: string;
}
export default function TemplateManager({ onTemplateSelect, onTemplateApply, className }: TemplateManagerProps) {
export default function TemplateManager({
selectedScreen,
onBackToList,
onTemplateSelect,
onTemplateApply,
className,
}: TemplateManagerProps) {
const { user } = useAuth();
const [templates, setTemplates] = useState<ScreenTemplate[]>([]);
const [selectedTemplate, setSelectedTemplate] = useState<ScreenTemplate | null>(null);
@ -211,6 +219,20 @@ export default function TemplateManager({ onTemplateSelect, onTemplateApply, cla
URL.revokeObjectURL(url);
};
// 화면이 선택되지 않았을 때 처리
if (!selectedScreen) {
return (
<div className="py-12 text-center text-gray-500">
<FileText className="mx-auto mb-4 h-16 w-16 text-gray-300" />
<p className="mb-4 text-lg">릿 </p>
<p className="mb-6 text-sm"> 릿 </p>
<Button onClick={onBackToList} variant="outline">
</Button>
</div>
);
}
return (
<div className={`space-y-4 ${className}`}>
{/* 헤더 */}

View File

@ -14,7 +14,14 @@ export type WebType =
| "select"
| "checkbox"
| "radio"
| "file";
| "file"
| "email"
| "tel"
| "datetime"
| "dropdown"
| "text_area"
| "boolean"
| "decimal";
// 위치 정보
export interface Position {
@ -28,6 +35,13 @@ export interface Size {
height: number; // 픽셀
}
// 테이블 정보
export interface TableInfo {
tableName: string;
tableLabel: string;
columns: ColumnInfo[];
}
// 스타일 관련 타입
export interface ComponentStyle {
// 레이아웃
@ -109,6 +123,8 @@ export interface BaseComponent {
size: { width: number; height: number };
parentId?: string;
style?: ComponentStyle; // 스타일 속성 추가
tableName?: string; // 테이블명 추가
label?: string; // 라벨 추가
}
// 컨테이너 컴포넌트