화면관리 테이블 타입관리 연계
This commit is contained in:
parent
42dbfd98f8
commit
ca56cff114
|
|
@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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" />
|
||||
미리보기
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
// 웹 타입 변경
|
||||
|
|
|
|||
|
|
@ -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}`}>
|
||||
{/* 헤더 */}
|
||||
|
|
|
|||
|
|
@ -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; // 라벨 추가
|
||||
}
|
||||
|
||||
// 컨테이너 컴포넌트
|
||||
|
|
|
|||
Loading…
Reference in New Issue