2026-01-05 10:05:31 +09:00
|
|
|
import { Request, Response } from "express";
|
|
|
|
|
import { getPool } from "../database/db";
|
|
|
|
|
import { logger } from "../utils/logger";
|
|
|
|
|
|
|
|
|
|
// pool 인스턴스 가져오기
|
|
|
|
|
const pool = getPool();
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 화면 그룹 (screen_groups) CRUD
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
// 화면 그룹 목록 조회
|
|
|
|
|
export const getScreenGroups = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
const { page = 1, size = 20, searchTerm } = req.query;
|
|
|
|
|
const offset = (parseInt(page as string) - 1) * parseInt(size as string);
|
|
|
|
|
|
|
|
|
|
let whereClause = "WHERE 1=1";
|
|
|
|
|
const params: any[] = [];
|
|
|
|
|
let paramIndex = 1;
|
|
|
|
|
|
|
|
|
|
// 회사 코드 필터링 (멀티테넌시)
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
whereClause += ` AND company_code = $${paramIndex}`;
|
|
|
|
|
params.push(companyCode);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 검색어 필터링
|
|
|
|
|
if (searchTerm) {
|
|
|
|
|
whereClause += ` AND (group_name ILIKE $${paramIndex} OR group_code ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`;
|
|
|
|
|
params.push(`%${searchTerm}%`);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 전체 개수 조회
|
|
|
|
|
const countQuery = `SELECT COUNT(*) as total FROM screen_groups ${whereClause}`;
|
|
|
|
|
const countResult = await pool.query(countQuery, params);
|
|
|
|
|
const total = parseInt(countResult.rows[0].total);
|
|
|
|
|
|
2026-01-05 18:18:26 +09:00
|
|
|
// 데이터 조회 (screens 배열 포함)
|
2026-01-05 10:05:31 +09:00
|
|
|
const dataQuery = `
|
|
|
|
|
SELECT
|
|
|
|
|
sg.*,
|
2026-01-05 18:18:26 +09:00
|
|
|
(SELECT COUNT(*) FROM screen_group_screens sgs WHERE sgs.group_id = sg.id) as screen_count,
|
|
|
|
|
(SELECT json_agg(
|
|
|
|
|
json_build_object(
|
|
|
|
|
'id', sgs.id,
|
|
|
|
|
'screen_id', sgs.screen_id,
|
|
|
|
|
'screen_name', sd.screen_name,
|
|
|
|
|
'screen_role', sgs.screen_role,
|
|
|
|
|
'display_order', sgs.display_order,
|
|
|
|
|
'is_default', sgs.is_default,
|
|
|
|
|
'table_name', sd.table_name
|
|
|
|
|
) ORDER BY sgs.display_order
|
|
|
|
|
) FROM screen_group_screens sgs
|
|
|
|
|
LEFT JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id
|
|
|
|
|
WHERE sgs.group_id = sg.id
|
|
|
|
|
) as screens
|
2026-01-05 10:05:31 +09:00
|
|
|
FROM screen_groups sg
|
|
|
|
|
${whereClause}
|
|
|
|
|
ORDER BY sg.display_order ASC, sg.created_date DESC
|
|
|
|
|
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
|
|
|
|
`;
|
|
|
|
|
params.push(parseInt(size as string), offset);
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(dataQuery, params);
|
|
|
|
|
|
|
|
|
|
logger.info("화면 그룹 목록 조회", { companyCode, total, count: result.rows.length });
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: result.rows,
|
|
|
|
|
total,
|
|
|
|
|
page: parseInt(page as string),
|
|
|
|
|
size: parseInt(size as string),
|
|
|
|
|
totalPages: Math.ceil(total / parseInt(size as string)),
|
|
|
|
|
});
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("화면 그룹 목록 조회 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "화면 그룹 목록 조회에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 화면 그룹 상세 조회
|
|
|
|
|
export const getScreenGroup = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
|
|
|
|
|
let query = `
|
|
|
|
|
SELECT sg.*,
|
|
|
|
|
(SELECT json_agg(
|
|
|
|
|
json_build_object(
|
|
|
|
|
'id', sgs.id,
|
|
|
|
|
'screen_id', sgs.screen_id,
|
|
|
|
|
'screen_name', sd.screen_name,
|
|
|
|
|
'screen_role', sgs.screen_role,
|
|
|
|
|
'display_order', sgs.display_order,
|
2026-01-05 18:18:26 +09:00
|
|
|
'is_default', sgs.is_default,
|
|
|
|
|
'table_name', sd.table_name
|
2026-01-05 10:05:31 +09:00
|
|
|
) ORDER BY sgs.display_order
|
|
|
|
|
) FROM screen_group_screens sgs
|
|
|
|
|
LEFT JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id
|
|
|
|
|
WHERE sgs.group_id = sg.id
|
|
|
|
|
) as screens
|
|
|
|
|
FROM screen_groups sg
|
|
|
|
|
WHERE sg.id = $1
|
|
|
|
|
`;
|
|
|
|
|
const params: any[] = [id];
|
|
|
|
|
|
|
|
|
|
// 멀티테넌시 필터링
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
query += ` AND sg.company_code = $2`;
|
|
|
|
|
params.push(companyCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
if (result.rows.length === 0) {
|
|
|
|
|
return res.status(404).json({ success: false, message: "화면 그룹을 찾을 수 없습니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, data: result.rows[0] });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("화면 그룹 상세 조회 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "화면 그룹 조회에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 화면 그룹 생성
|
|
|
|
|
export const createScreenGroup = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
2026-01-07 14:49:49 +09:00
|
|
|
const userCompanyCode = (req.user as any).companyCode;
|
2026-01-05 10:05:31 +09:00
|
|
|
const userId = (req.user as any).userId;
|
2026-01-07 14:49:49 +09:00
|
|
|
const { group_name, group_code, main_table_name, description, icon, display_order, is_active, parent_group_id, target_company_code } = req.body;
|
2026-01-05 10:05:31 +09:00
|
|
|
|
|
|
|
|
if (!group_name || !group_code) {
|
|
|
|
|
return res.status(400).json({ success: false, message: "그룹명과 그룹코드는 필수입니다." });
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 14:49:49 +09:00
|
|
|
// 회사 코드 결정: 최고 관리자가 특정 회사를 선택한 경우 해당 회사로, 아니면 사용자 회사
|
|
|
|
|
let finalCompanyCode = userCompanyCode;
|
|
|
|
|
if (userCompanyCode === "*" && target_company_code) {
|
|
|
|
|
// 최고 관리자가 특정 회사를 선택한 경우
|
|
|
|
|
finalCompanyCode = target_company_code;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 부모 그룹이 있으면 group_level과 hierarchy_path 계산
|
|
|
|
|
let groupLevel = 0;
|
|
|
|
|
let parentHierarchyPath = "";
|
|
|
|
|
|
|
|
|
|
if (parent_group_id) {
|
|
|
|
|
const parentQuery = `SELECT id, group_level, hierarchy_path FROM screen_groups WHERE id = $1`;
|
|
|
|
|
const parentResult = await pool.query(parentQuery, [parent_group_id]);
|
|
|
|
|
if (parentResult.rows.length > 0) {
|
|
|
|
|
groupLevel = (parentResult.rows[0].group_level || 0) + 1;
|
|
|
|
|
parentHierarchyPath = parentResult.rows[0].hierarchy_path || `/${parent_group_id}/`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 10:05:31 +09:00
|
|
|
const query = `
|
2026-01-07 14:49:49 +09:00
|
|
|
INSERT INTO screen_groups (group_name, group_code, main_table_name, description, icon, display_order, is_active, company_code, writer, parent_group_id, group_level)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
2026-01-05 10:05:31 +09:00
|
|
|
RETURNING *
|
|
|
|
|
`;
|
|
|
|
|
const params = [
|
|
|
|
|
group_name,
|
|
|
|
|
group_code,
|
|
|
|
|
main_table_name || null,
|
|
|
|
|
description || null,
|
|
|
|
|
icon || null,
|
|
|
|
|
display_order || 0,
|
|
|
|
|
is_active || 'Y',
|
2026-01-07 14:49:49 +09:00
|
|
|
finalCompanyCode,
|
|
|
|
|
userId,
|
|
|
|
|
parent_group_id || null,
|
|
|
|
|
groupLevel
|
2026-01-05 10:05:31 +09:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
2026-01-07 14:49:49 +09:00
|
|
|
const newGroupId = result.rows[0].id;
|
2026-01-05 10:05:31 +09:00
|
|
|
|
2026-01-07 14:49:49 +09:00
|
|
|
// hierarchy_path 업데이트
|
|
|
|
|
const hierarchyPath = parent_group_id
|
|
|
|
|
? `${parentHierarchyPath}${newGroupId}/`.replace('//', '/')
|
|
|
|
|
: `/${newGroupId}/`;
|
|
|
|
|
await pool.query(`UPDATE screen_groups SET hierarchy_path = $1 WHERE id = $2`, [hierarchyPath, newGroupId]);
|
2026-01-05 10:05:31 +09:00
|
|
|
|
2026-01-07 14:49:49 +09:00
|
|
|
// 업데이트된 데이터 반환
|
|
|
|
|
const updatedResult = await pool.query(`SELECT * FROM screen_groups WHERE id = $1`, [newGroupId]);
|
|
|
|
|
|
|
|
|
|
logger.info("화면 그룹 생성", { userCompanyCode, finalCompanyCode, groupId: newGroupId, groupName: group_name, parentGroupId: parent_group_id });
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, data: updatedResult.rows[0], message: "화면 그룹이 생성되었습니다." });
|
2026-01-05 10:05:31 +09:00
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("화면 그룹 생성 실패:", error);
|
|
|
|
|
if (error.code === '23505') {
|
|
|
|
|
return res.status(400).json({ success: false, message: "이미 존재하는 그룹 코드입니다." });
|
|
|
|
|
}
|
|
|
|
|
res.status(500).json({ success: false, message: "화면 그룹 생성에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 화면 그룹 수정
|
|
|
|
|
export const updateScreenGroup = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
2026-01-07 14:49:49 +09:00
|
|
|
const userCompanyCode = (req.user as any).companyCode;
|
|
|
|
|
const { group_name, group_code, main_table_name, description, icon, display_order, is_active, parent_group_id, target_company_code } = req.body;
|
2026-01-05 10:05:31 +09:00
|
|
|
|
2026-01-07 14:49:49 +09:00
|
|
|
// 회사 코드 결정: 최고 관리자가 특정 회사를 선택한 경우 해당 회사로, 아니면 현재 그룹의 회사 유지
|
|
|
|
|
let finalCompanyCode = target_company_code || null;
|
2026-01-05 10:05:31 +09:00
|
|
|
|
2026-01-07 14:49:49 +09:00
|
|
|
// 부모 그룹이 변경되면 group_level과 hierarchy_path 재계산
|
|
|
|
|
let groupLevel = 0;
|
|
|
|
|
let hierarchyPath = `/${id}/`;
|
|
|
|
|
|
|
|
|
|
if (parent_group_id !== undefined && parent_group_id !== null) {
|
|
|
|
|
// 자기 자신을 부모로 지정하는 것 방지
|
|
|
|
|
if (Number(parent_group_id) === Number(id)) {
|
|
|
|
|
return res.status(400).json({ success: false, message: "자기 자신을 상위 그룹으로 지정할 수 없습니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parentQuery = `SELECT id, group_level, hierarchy_path FROM screen_groups WHERE id = $1`;
|
|
|
|
|
const parentResult = await pool.query(parentQuery, [parent_group_id]);
|
|
|
|
|
if (parentResult.rows.length > 0) {
|
|
|
|
|
// 순환 참조 방지: 부모의 hierarchy_path에 현재 그룹 ID가 포함되어 있으면 오류
|
|
|
|
|
if (parentResult.rows[0].hierarchy_path && parentResult.rows[0].hierarchy_path.includes(`/${id}/`)) {
|
|
|
|
|
return res.status(400).json({ success: false, message: "하위 그룹을 상위 그룹으로 지정할 수 없습니다." });
|
|
|
|
|
}
|
|
|
|
|
groupLevel = (parentResult.rows[0].group_level || 0) + 1;
|
|
|
|
|
const parentPath = parentResult.rows[0].hierarchy_path || `/${parent_group_id}/`;
|
|
|
|
|
hierarchyPath = `${parentPath}${id}/`.replace('//', '/');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 쿼리 구성: 회사 코드 변경 포함 여부
|
|
|
|
|
let query: string;
|
|
|
|
|
let params: any[];
|
|
|
|
|
|
|
|
|
|
if (userCompanyCode === "*" && finalCompanyCode) {
|
|
|
|
|
// 최고 관리자가 회사를 변경하는 경우
|
|
|
|
|
query = `
|
|
|
|
|
UPDATE screen_groups
|
|
|
|
|
SET group_name = $1, group_code = $2, main_table_name = $3, description = $4,
|
|
|
|
|
icon = $5, display_order = $6, is_active = $7, updated_date = NOW(),
|
|
|
|
|
parent_group_id = $8, group_level = $9, hierarchy_path = $10, company_code = $11
|
|
|
|
|
WHERE id = $12
|
|
|
|
|
`;
|
|
|
|
|
params = [
|
|
|
|
|
group_name, group_code, main_table_name, description, icon, display_order, is_active,
|
|
|
|
|
parent_group_id || null, groupLevel, hierarchyPath, finalCompanyCode, id
|
|
|
|
|
];
|
|
|
|
|
} else {
|
|
|
|
|
// 회사 코드 변경 없음
|
|
|
|
|
query = `
|
|
|
|
|
UPDATE screen_groups
|
|
|
|
|
SET group_name = $1, group_code = $2, main_table_name = $3, description = $4,
|
|
|
|
|
icon = $5, display_order = $6, is_active = $7, updated_date = NOW(),
|
|
|
|
|
parent_group_id = $8, group_level = $9, hierarchy_path = $10
|
|
|
|
|
WHERE id = $11
|
|
|
|
|
`;
|
|
|
|
|
params = [
|
|
|
|
|
group_name, group_code, main_table_name, description, icon, display_order, is_active,
|
|
|
|
|
parent_group_id || null, groupLevel, hierarchyPath, id
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 멀티테넌시 필터링 (최고 관리자가 아닌 경우)
|
|
|
|
|
if (userCompanyCode !== "*") {
|
|
|
|
|
const paramIndex = params.length + 1;
|
|
|
|
|
query += ` AND company_code = $${paramIndex}`;
|
|
|
|
|
params.push(userCompanyCode);
|
2026-01-05 10:05:31 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query += " RETURNING *";
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
if (result.rows.length === 0) {
|
|
|
|
|
return res.status(404).json({ success: false, message: "화면 그룹을 찾을 수 없거나 권한이 없습니다." });
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 14:49:49 +09:00
|
|
|
logger.info("화면 그룹 수정", { userCompanyCode, groupId: id, parentGroupId: parent_group_id, targetCompanyCode: finalCompanyCode });
|
2026-01-05 10:05:31 +09:00
|
|
|
|
|
|
|
|
res.json({ success: true, data: result.rows[0], message: "화면 그룹이 수정되었습니다." });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("화면 그룹 수정 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "화면 그룹 수정에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 화면 그룹 삭제
|
|
|
|
|
export const deleteScreenGroup = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
|
|
|
|
|
let query = `DELETE FROM screen_groups WHERE id = $1`;
|
|
|
|
|
const params: any[] = [id];
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
query += ` AND company_code = $2`;
|
|
|
|
|
params.push(companyCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query += " RETURNING id";
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
if (result.rows.length === 0) {
|
|
|
|
|
return res.status(404).json({ success: false, message: "화면 그룹을 찾을 수 없거나 권한이 없습니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info("화면 그룹 삭제", { companyCode, groupId: id });
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, message: "화면 그룹이 삭제되었습니다." });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("화면 그룹 삭제 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "화면 그룹 삭제에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 화면-그룹 연결 (screen_group_screens) CRUD
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
// 그룹에 화면 추가
|
|
|
|
|
export const addScreenToGroup = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
const userId = (req.user as any).userId;
|
|
|
|
|
const { group_id, screen_id, screen_role, display_order, is_default } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!group_id || !screen_id) {
|
|
|
|
|
return res.status(400).json({ success: false, message: "그룹 ID와 화면 ID는 필수입니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const query = `
|
|
|
|
|
INSERT INTO screen_group_screens (group_id, screen_id, screen_role, display_order, is_default, company_code, writer)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
|
|
|
RETURNING *
|
|
|
|
|
`;
|
|
|
|
|
const params = [
|
|
|
|
|
group_id,
|
|
|
|
|
screen_id,
|
|
|
|
|
screen_role || 'main',
|
|
|
|
|
display_order || 0,
|
|
|
|
|
is_default || 'N',
|
|
|
|
|
companyCode === "*" ? "*" : companyCode,
|
|
|
|
|
userId
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
logger.info("화면-그룹 연결 추가", { companyCode, groupId: group_id, screenId: screen_id });
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, data: result.rows[0], message: "화면이 그룹에 추가되었습니다." });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("화면-그룹 연결 추가 실패:", error);
|
|
|
|
|
if (error.code === '23505') {
|
|
|
|
|
return res.status(400).json({ success: false, message: "이미 그룹에 추가된 화면입니다." });
|
|
|
|
|
}
|
|
|
|
|
res.status(500).json({ success: false, message: "화면 추가에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 그룹에서 화면 제거
|
|
|
|
|
export const removeScreenFromGroup = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
|
|
|
|
|
let query = `DELETE FROM screen_group_screens WHERE id = $1`;
|
|
|
|
|
const params: any[] = [id];
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
query += ` AND company_code = $2`;
|
|
|
|
|
params.push(companyCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query += " RETURNING id";
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
if (result.rows.length === 0) {
|
|
|
|
|
return res.status(404).json({ success: false, message: "연결을 찾을 수 없거나 권한이 없습니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info("화면-그룹 연결 제거", { companyCode, id });
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, message: "화면이 그룹에서 제거되었습니다." });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("화면-그룹 연결 제거 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "화면 제거에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 그룹 내 화면 순서/역할 수정
|
|
|
|
|
export const updateScreenInGroup = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
const { screen_role, display_order, is_default } = req.body;
|
|
|
|
|
|
|
|
|
|
let query = `
|
|
|
|
|
UPDATE screen_group_screens
|
|
|
|
|
SET screen_role = $1, display_order = $2, is_default = $3, updated_date = NOW()
|
|
|
|
|
WHERE id = $4
|
|
|
|
|
`;
|
|
|
|
|
const params: any[] = [screen_role, display_order, is_default, id];
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
query += ` AND company_code = $5`;
|
|
|
|
|
params.push(companyCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query += " RETURNING *";
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
if (result.rows.length === 0) {
|
|
|
|
|
return res.status(404).json({ success: false, message: "연결을 찾을 수 없거나 권한이 없습니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, data: result.rows[0], message: "화면 정보가 수정되었습니다." });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("화면-그룹 연결 수정 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "화면 정보 수정에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 화면 필드 조인 설정 (screen_field_joins) CRUD
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
// 화면 필드 조인 목록 조회
|
|
|
|
|
export const getFieldJoins = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
const { screen_id } = req.query;
|
|
|
|
|
|
|
|
|
|
let query = `
|
|
|
|
|
SELECT sfj.*,
|
|
|
|
|
tl1.table_label as save_table_label,
|
|
|
|
|
tl2.table_label as join_table_label
|
|
|
|
|
FROM screen_field_joins sfj
|
|
|
|
|
LEFT JOIN table_labels tl1 ON sfj.save_table = tl1.table_name
|
|
|
|
|
LEFT JOIN table_labels tl2 ON sfj.join_table = tl2.table_name
|
|
|
|
|
WHERE 1=1
|
|
|
|
|
`;
|
|
|
|
|
const params: any[] = [];
|
|
|
|
|
let paramIndex = 1;
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
query += ` AND sfj.company_code = $${paramIndex}`;
|
|
|
|
|
params.push(companyCode);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (screen_id) {
|
|
|
|
|
query += ` AND sfj.screen_id = $${paramIndex}`;
|
|
|
|
|
params.push(screen_id);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query += " ORDER BY sfj.id ASC";
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, data: result.rows });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("필드 조인 목록 조회 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "필드 조인 목록 조회에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 화면 필드 조인 생성
|
|
|
|
|
export const createFieldJoin = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
const userId = (req.user as any).userId;
|
|
|
|
|
const {
|
|
|
|
|
screen_id, layout_id, component_id, field_name,
|
|
|
|
|
save_table, save_column, join_table, join_column, display_column,
|
|
|
|
|
join_type, filter_condition, sort_column, sort_direction, is_active
|
|
|
|
|
} = req.body;
|
|
|
|
|
|
|
|
|
|
if (!screen_id || !save_table || !save_column || !join_table || !join_column || !display_column) {
|
|
|
|
|
return res.status(400).json({ success: false, message: "필수 필드가 누락되었습니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const query = `
|
|
|
|
|
INSERT INTO screen_field_joins (
|
|
|
|
|
screen_id, layout_id, component_id, field_name,
|
|
|
|
|
save_table, save_column, join_table, join_column, display_column,
|
|
|
|
|
join_type, filter_condition, sort_column, sort_direction, is_active, company_code, writer
|
|
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
|
|
|
|
RETURNING *
|
|
|
|
|
`;
|
|
|
|
|
const params = [
|
|
|
|
|
screen_id, layout_id || null, component_id || null, field_name || null,
|
|
|
|
|
save_table, save_column, join_table, join_column, display_column,
|
|
|
|
|
join_type || 'LEFT', filter_condition || null, sort_column || null, sort_direction || 'ASC',
|
|
|
|
|
is_active || 'Y', companyCode === "*" ? "*" : companyCode, userId
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
logger.info("필드 조인 생성", { companyCode, screenId: screen_id, id: result.rows[0].id });
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, data: result.rows[0], message: "필드 조인이 생성되었습니다." });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("필드 조인 생성 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "필드 조인 생성에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 화면 필드 조인 수정
|
|
|
|
|
export const updateFieldJoin = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
const {
|
|
|
|
|
layout_id, component_id, field_name,
|
|
|
|
|
save_table, save_column, join_table, join_column, display_column,
|
|
|
|
|
join_type, filter_condition, sort_column, sort_direction, is_active
|
|
|
|
|
} = req.body;
|
|
|
|
|
|
|
|
|
|
let query = `
|
|
|
|
|
UPDATE screen_field_joins SET
|
|
|
|
|
layout_id = $1, component_id = $2, field_name = $3,
|
|
|
|
|
save_table = $4, save_column = $5, join_table = $6, join_column = $7, display_column = $8,
|
|
|
|
|
join_type = $9, filter_condition = $10, sort_column = $11, sort_direction = $12,
|
|
|
|
|
is_active = $13, updated_date = NOW()
|
|
|
|
|
WHERE id = $14
|
|
|
|
|
`;
|
|
|
|
|
const params: any[] = [
|
|
|
|
|
layout_id, component_id, field_name,
|
|
|
|
|
save_table, save_column, join_table, join_column, display_column,
|
|
|
|
|
join_type, filter_condition, sort_column, sort_direction, is_active, id
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
query += ` AND company_code = $15`;
|
|
|
|
|
params.push(companyCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query += " RETURNING *";
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
if (result.rows.length === 0) {
|
|
|
|
|
return res.status(404).json({ success: false, message: "필드 조인을 찾을 수 없거나 권한이 없습니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, data: result.rows[0], message: "필드 조인이 수정되었습니다." });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("필드 조인 수정 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "필드 조인 수정에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 화면 필드 조인 삭제
|
|
|
|
|
export const deleteFieldJoin = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
|
|
|
|
|
let query = `DELETE FROM screen_field_joins WHERE id = $1`;
|
|
|
|
|
const params: any[] = [id];
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
query += ` AND company_code = $2`;
|
|
|
|
|
params.push(companyCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query += " RETURNING id";
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
if (result.rows.length === 0) {
|
|
|
|
|
return res.status(404).json({ success: false, message: "필드 조인을 찾을 수 없거나 권한이 없습니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, message: "필드 조인이 삭제되었습니다." });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("필드 조인 삭제 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "필드 조인 삭제에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 데이터 흐름 (screen_data_flows) CRUD
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
// 데이터 흐름 목록 조회
|
|
|
|
|
export const getDataFlows = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
const { group_id } = req.query;
|
|
|
|
|
|
|
|
|
|
let query = `
|
|
|
|
|
SELECT sdf.*,
|
|
|
|
|
sd1.screen_name as source_screen_name,
|
|
|
|
|
sd2.screen_name as target_screen_name,
|
|
|
|
|
sg.group_name
|
|
|
|
|
FROM screen_data_flows sdf
|
|
|
|
|
LEFT JOIN screen_definitions sd1 ON sdf.source_screen_id = sd1.screen_id
|
|
|
|
|
LEFT JOIN screen_definitions sd2 ON sdf.target_screen_id = sd2.screen_id
|
|
|
|
|
LEFT JOIN screen_groups sg ON sdf.group_id = sg.id
|
|
|
|
|
WHERE 1=1
|
|
|
|
|
`;
|
|
|
|
|
const params: any[] = [];
|
|
|
|
|
let paramIndex = 1;
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
query += ` AND sdf.company_code = $${paramIndex}`;
|
|
|
|
|
params.push(companyCode);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (group_id) {
|
|
|
|
|
query += ` AND sdf.group_id = $${paramIndex}`;
|
|
|
|
|
params.push(group_id);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query += " ORDER BY sdf.id ASC";
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, data: result.rows });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("데이터 흐름 목록 조회 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "데이터 흐름 목록 조회에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 데이터 흐름 생성
|
|
|
|
|
export const createDataFlow = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
const userId = (req.user as any).userId;
|
|
|
|
|
const {
|
|
|
|
|
group_id, source_screen_id, source_action, target_screen_id, target_action,
|
|
|
|
|
data_mapping, flow_type, flow_label, condition_expression, is_active
|
|
|
|
|
} = req.body;
|
|
|
|
|
|
|
|
|
|
if (!source_screen_id || !target_screen_id) {
|
|
|
|
|
return res.status(400).json({ success: false, message: "소스 화면과 타겟 화면은 필수입니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const query = `
|
|
|
|
|
INSERT INTO screen_data_flows (
|
|
|
|
|
group_id, source_screen_id, source_action, target_screen_id, target_action,
|
|
|
|
|
data_mapping, flow_type, flow_label, condition_expression, is_active, company_code, writer
|
|
|
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
|
|
|
RETURNING *
|
|
|
|
|
`;
|
|
|
|
|
const params = [
|
|
|
|
|
group_id || null, source_screen_id, source_action || null, target_screen_id, target_action || null,
|
|
|
|
|
data_mapping ? JSON.stringify(data_mapping) : null, flow_type || 'unidirectional',
|
|
|
|
|
flow_label || null, condition_expression || null, is_active || 'Y',
|
|
|
|
|
companyCode === "*" ? "*" : companyCode, userId
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
logger.info("데이터 흐름 생성", { companyCode, id: result.rows[0].id });
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, data: result.rows[0], message: "데이터 흐름이 생성되었습니다." });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("데이터 흐름 생성 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "데이터 흐름 생성에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 데이터 흐름 수정
|
|
|
|
|
export const updateDataFlow = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
const {
|
|
|
|
|
group_id, source_screen_id, source_action, target_screen_id, target_action,
|
|
|
|
|
data_mapping, flow_type, flow_label, condition_expression, is_active
|
|
|
|
|
} = req.body;
|
|
|
|
|
|
|
|
|
|
let query = `
|
|
|
|
|
UPDATE screen_data_flows SET
|
|
|
|
|
group_id = $1, source_screen_id = $2, source_action = $3,
|
|
|
|
|
target_screen_id = $4, target_action = $5, data_mapping = $6,
|
|
|
|
|
flow_type = $7, flow_label = $8, condition_expression = $9,
|
|
|
|
|
is_active = $10, updated_date = NOW()
|
|
|
|
|
WHERE id = $11
|
|
|
|
|
`;
|
|
|
|
|
const params: any[] = [
|
|
|
|
|
group_id, source_screen_id, source_action, target_screen_id, target_action,
|
|
|
|
|
data_mapping ? JSON.stringify(data_mapping) : null, flow_type, flow_label, condition_expression, is_active, id
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
query += ` AND company_code = $12`;
|
|
|
|
|
params.push(companyCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query += " RETURNING *";
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
if (result.rows.length === 0) {
|
|
|
|
|
return res.status(404).json({ success: false, message: "데이터 흐름을 찾을 수 없거나 권한이 없습니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, data: result.rows[0], message: "데이터 흐름이 수정되었습니다." });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("데이터 흐름 수정 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "데이터 흐름 수정에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 데이터 흐름 삭제
|
|
|
|
|
export const deleteDataFlow = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
|
|
|
|
|
let query = `DELETE FROM screen_data_flows WHERE id = $1`;
|
|
|
|
|
const params: any[] = [id];
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
query += ` AND company_code = $2`;
|
|
|
|
|
params.push(companyCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query += " RETURNING id";
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
if (result.rows.length === 0) {
|
|
|
|
|
return res.status(404).json({ success: false, message: "데이터 흐름을 찾을 수 없거나 권한이 없습니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, message: "데이터 흐름이 삭제되었습니다." });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("데이터 흐름 삭제 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "데이터 흐름 삭제에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 화면-테이블 관계 (screen_table_relations) CRUD
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
// 화면-테이블 관계 목록 조회
|
|
|
|
|
export const getTableRelations = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
const { screen_id, group_id } = req.query;
|
|
|
|
|
|
|
|
|
|
let query = `
|
|
|
|
|
SELECT str.*,
|
|
|
|
|
sd.screen_name,
|
|
|
|
|
sg.group_name,
|
|
|
|
|
tl.table_label
|
|
|
|
|
FROM screen_table_relations str
|
|
|
|
|
LEFT JOIN screen_definitions sd ON str.screen_id = sd.screen_id
|
|
|
|
|
LEFT JOIN screen_groups sg ON str.group_id = sg.id
|
|
|
|
|
LEFT JOIN table_labels tl ON str.table_name = tl.table_name
|
|
|
|
|
WHERE 1=1
|
|
|
|
|
`;
|
|
|
|
|
const params: any[] = [];
|
|
|
|
|
let paramIndex = 1;
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
query += ` AND str.company_code = $${paramIndex}`;
|
|
|
|
|
params.push(companyCode);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (screen_id) {
|
|
|
|
|
query += ` AND str.screen_id = $${paramIndex}`;
|
|
|
|
|
params.push(screen_id);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (group_id) {
|
|
|
|
|
query += ` AND str.group_id = $${paramIndex}`;
|
|
|
|
|
params.push(group_id);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query += " ORDER BY str.id ASC";
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, data: result.rows });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("화면-테이블 관계 목록 조회 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "화면-테이블 관계 목록 조회에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 화면-테이블 관계 생성
|
|
|
|
|
export const createTableRelation = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
const userId = (req.user as any).userId;
|
|
|
|
|
const { group_id, screen_id, table_name, relation_type, crud_operations, description, is_active } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!screen_id || !table_name) {
|
|
|
|
|
return res.status(400).json({ success: false, message: "화면 ID와 테이블명은 필수입니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const query = `
|
|
|
|
|
INSERT INTO screen_table_relations (group_id, screen_id, table_name, relation_type, crud_operations, description, is_active, company_code, writer)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
|
|
|
RETURNING *
|
|
|
|
|
`;
|
|
|
|
|
const params = [
|
|
|
|
|
group_id || null, screen_id, table_name, relation_type || 'main',
|
|
|
|
|
crud_operations || 'CRUD', description || null, is_active || 'Y',
|
|
|
|
|
companyCode === "*" ? "*" : companyCode, userId
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
logger.info("화면-테이블 관계 생성", { companyCode, screenId: screen_id, tableName: table_name });
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, data: result.rows[0], message: "화면-테이블 관계가 생성되었습니다." });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("화면-테이블 관계 생성 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "화면-테이블 관계 생성에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 화면-테이블 관계 수정
|
|
|
|
|
export const updateTableRelation = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
const { group_id, table_name, relation_type, crud_operations, description, is_active } = req.body;
|
|
|
|
|
|
|
|
|
|
let query = `
|
|
|
|
|
UPDATE screen_table_relations SET
|
|
|
|
|
group_id = $1, table_name = $2, relation_type = $3, crud_operations = $4,
|
|
|
|
|
description = $5, is_active = $6, updated_date = NOW()
|
|
|
|
|
WHERE id = $7
|
|
|
|
|
`;
|
|
|
|
|
const params: any[] = [group_id, table_name, relation_type, crud_operations, description, is_active, id];
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
query += ` AND company_code = $8`;
|
|
|
|
|
params.push(companyCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query += " RETURNING *";
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
if (result.rows.length === 0) {
|
|
|
|
|
return res.status(404).json({ success: false, message: "화면-테이블 관계를 찾을 수 없거나 권한이 없습니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, data: result.rows[0], message: "화면-테이블 관계가 수정되었습니다." });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("화면-테이블 관계 수정 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "화면-테이블 관계 수정에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 화면-테이블 관계 삭제
|
|
|
|
|
export const deleteTableRelation = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
const companyCode = (req.user as any).companyCode;
|
|
|
|
|
|
|
|
|
|
let query = `DELETE FROM screen_table_relations WHERE id = $1`;
|
|
|
|
|
const params: any[] = [id];
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
query += ` AND company_code = $2`;
|
|
|
|
|
params.push(companyCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query += " RETURNING id";
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
|
|
|
|
|
|
if (result.rows.length === 0) {
|
|
|
|
|
return res.status(404).json({ success: false, message: "화면-테이블 관계를 찾을 수 없거나 권한이 없습니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json({ success: true, message: "화면-테이블 관계가 삭제되었습니다." });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("화면-테이블 관계 삭제 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "화면-테이블 관계 삭제에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 화면 레이아웃 요약 정보 (미리보기용)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
// 화면 레이아웃 요약 조회 (위젯 타입별 개수, 라벨 목록)
|
|
|
|
|
export const getScreenLayoutSummary = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const { screenId } = req.params;
|
|
|
|
|
|
|
|
|
|
// 화면의 컴포넌트 정보 조회
|
|
|
|
|
const query = `
|
|
|
|
|
SELECT
|
|
|
|
|
properties->>'widgetType' as widget_type,
|
|
|
|
|
properties->>'label' as label,
|
|
|
|
|
properties->>'fieldName' as field_name,
|
|
|
|
|
properties->>'tableName' as table_name
|
|
|
|
|
FROM screen_layouts
|
|
|
|
|
WHERE screen_id = $1
|
|
|
|
|
AND component_type = 'component'
|
|
|
|
|
ORDER BY display_order ASC
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, [screenId]);
|
|
|
|
|
|
|
|
|
|
// 위젯 타입별 집계
|
|
|
|
|
const widgetCounts: Record<string, number> = {};
|
|
|
|
|
const labels: string[] = [];
|
|
|
|
|
const fields: Array<{ label: string; widgetType: string; fieldName?: string }> = [];
|
|
|
|
|
|
|
|
|
|
result.rows.forEach((row: any) => {
|
|
|
|
|
const widgetType = row.widget_type || 'text';
|
|
|
|
|
widgetCounts[widgetType] = (widgetCounts[widgetType] || 0) + 1;
|
|
|
|
|
|
|
|
|
|
if (row.label && row.label !== '기본 버튼') {
|
|
|
|
|
labels.push(row.label);
|
|
|
|
|
fields.push({
|
|
|
|
|
label: row.label,
|
|
|
|
|
widgetType: widgetType,
|
|
|
|
|
fieldName: row.field_name,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 화면 타입 추론 (가장 많은 컴포넌트 기준)
|
|
|
|
|
let screenType = 'form'; // 기본값
|
|
|
|
|
if (widgetCounts['table'] > 0) {
|
|
|
|
|
screenType = 'grid';
|
|
|
|
|
} else if (widgetCounts['custom'] > 2) {
|
|
|
|
|
screenType = 'dashboard';
|
|
|
|
|
} else if (Object.keys(widgetCounts).length <= 2 && widgetCounts['button'] > 0) {
|
|
|
|
|
screenType = 'action';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info("화면 레이아웃 요약 조회", { screenId, widgetCounts, fieldCount: fields.length });
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: {
|
|
|
|
|
screenId: parseInt(screenId),
|
|
|
|
|
screenType,
|
|
|
|
|
widgetCounts,
|
|
|
|
|
totalComponents: result.rows.length,
|
|
|
|
|
fields: fields.slice(0, 10), // 최대 10개
|
|
|
|
|
labels: labels.slice(0, 8), // 최대 8개
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("화면 레이아웃 요약 조회 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "화면 레이아웃 요약 조회에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 여러 화면의 레이아웃 요약 일괄 조회 (미니어처 렌더링용 좌표 포함)
|
|
|
|
|
export const getMultipleScreenLayoutSummary = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const { screenIds } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!screenIds || !Array.isArray(screenIds) || screenIds.length === 0) {
|
|
|
|
|
return res.status(400).json({ success: false, message: "screenIds 배열이 필요합니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 여러 화면의 컴포넌트 정보 (좌표 포함) 한번에 조회
|
|
|
|
|
// componentType이 더 정확한 위젯 종류 (table-list, button-primary 등)
|
2026-01-07 14:49:49 +09:00
|
|
|
// 다양한 컴포넌트 타입에서 사용 컬럼 추출
|
2026-01-05 10:05:31 +09:00
|
|
|
const query = `
|
|
|
|
|
SELECT
|
|
|
|
|
screen_id,
|
|
|
|
|
component_type,
|
|
|
|
|
position_x,
|
|
|
|
|
position_y,
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
properties->>'componentType' as component_kind,
|
|
|
|
|
properties->>'widgetType' as widget_type,
|
2026-01-07 14:49:49 +09:00
|
|
|
properties->>'label' as label,
|
|
|
|
|
COALESCE(
|
|
|
|
|
properties->'componentConfig'->>'bindField',
|
|
|
|
|
properties->>'bindField',
|
|
|
|
|
properties->'componentConfig'->>'field',
|
|
|
|
|
properties->>'field'
|
|
|
|
|
) as bind_field,
|
|
|
|
|
-- componentConfig 전체 (JavaScript에서 다양한 패턴 파싱용)
|
|
|
|
|
properties->'componentConfig' as component_config
|
2026-01-05 10:05:31 +09:00
|
|
|
FROM screen_layouts
|
|
|
|
|
WHERE screen_id = ANY($1)
|
|
|
|
|
AND component_type = 'component'
|
|
|
|
|
ORDER BY screen_id, display_order ASC
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, [screenIds]);
|
|
|
|
|
|
|
|
|
|
// 화면별로 그룹핑
|
|
|
|
|
const summaryMap: Record<number, any> = {};
|
|
|
|
|
|
|
|
|
|
screenIds.forEach((id: number) => {
|
|
|
|
|
summaryMap[id] = {
|
|
|
|
|
screenId: id,
|
|
|
|
|
screenType: 'form',
|
|
|
|
|
widgetCounts: {},
|
|
|
|
|
totalComponents: 0,
|
|
|
|
|
// 미니어처 렌더링용 레이아웃 데이터
|
|
|
|
|
layoutItems: [],
|
|
|
|
|
canvasWidth: 0,
|
|
|
|
|
canvasHeight: 0,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
result.rows.forEach((row: any) => {
|
|
|
|
|
const screenId = row.screen_id;
|
|
|
|
|
// componentKind가 더 정확한 타입 (table-list, button-primary, table-search-widget 등)
|
|
|
|
|
const componentKind = row.component_kind || row.widget_type || 'text';
|
|
|
|
|
const widgetType = row.widget_type || 'text';
|
2026-01-07 14:49:49 +09:00
|
|
|
const componentConfig = row.component_config || {};
|
|
|
|
|
|
|
|
|
|
// 다양한 컴포넌트 타입에서 usedColumns, joinColumns 추출
|
|
|
|
|
let usedColumns: string[] = [];
|
|
|
|
|
let joinColumns: string[] = [];
|
|
|
|
|
|
|
|
|
|
// 1. 기본 columns 배열에서 추출 (table-list 등)
|
|
|
|
|
if (Array.isArray(componentConfig.columns)) {
|
|
|
|
|
componentConfig.columns.forEach((col: any) => {
|
|
|
|
|
const colName = col.columnName || col.field || col.name;
|
|
|
|
|
if (colName && !usedColumns.includes(colName)) {
|
|
|
|
|
usedColumns.push(colName);
|
|
|
|
|
}
|
|
|
|
|
if (col.isEntityJoin === true && colName && !joinColumns.includes(colName)) {
|
|
|
|
|
joinColumns.push(colName);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. split-panel-layout의 leftPanel.columns, rightPanel.columns 추출
|
|
|
|
|
if (componentKind === 'split-panel-layout') {
|
|
|
|
|
if (componentConfig.leftPanel?.columns && Array.isArray(componentConfig.leftPanel.columns)) {
|
|
|
|
|
componentConfig.leftPanel.columns.forEach((col: any) => {
|
|
|
|
|
const colName = col.name || col.columnName || col.field;
|
|
|
|
|
if (colName && !usedColumns.includes(colName)) {
|
|
|
|
|
usedColumns.push(colName);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (componentConfig.rightPanel?.columns && Array.isArray(componentConfig.rightPanel.columns)) {
|
|
|
|
|
componentConfig.rightPanel.columns.forEach((col: any) => {
|
|
|
|
|
const colName = col.name || col.columnName || col.field;
|
|
|
|
|
if (colName) {
|
|
|
|
|
// customer_mng.customer_name 같은 경우 조인 컬럼으로 처리
|
|
|
|
|
if (colName.includes('.')) {
|
|
|
|
|
if (!joinColumns.includes(colName)) {
|
|
|
|
|
joinColumns.push(colName);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (!usedColumns.includes(colName)) {
|
|
|
|
|
usedColumns.push(colName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. selected-items-detail-input의 additionalFields, displayColumns 추출
|
|
|
|
|
if (componentKind === 'selected-items-detail-input') {
|
|
|
|
|
if (componentConfig.additionalFields && Array.isArray(componentConfig.additionalFields)) {
|
|
|
|
|
componentConfig.additionalFields.forEach((field: any) => {
|
|
|
|
|
const fieldName = field.name || field.field;
|
|
|
|
|
if (fieldName && !usedColumns.includes(fieldName)) {
|
|
|
|
|
usedColumns.push(fieldName);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// displayColumns는 연관 테이블에서 가져오는 표시용 컬럼이므로
|
|
|
|
|
// 메인 테이블의 joinColumns가 아님 (parentDataMapping에서 별도 추출됨)
|
|
|
|
|
// 단, 참조용으로 usedColumns에는 추가 가능
|
|
|
|
|
if (componentConfig.displayColumns && Array.isArray(componentConfig.displayColumns)) {
|
|
|
|
|
componentConfig.displayColumns.forEach((col: any) => {
|
|
|
|
|
const colName = col.name || col.columnName || col.field;
|
|
|
|
|
// displayColumns는 연관 테이블 컬럼이므로 메인 테이블 usedColumns에 추가하지 않음
|
|
|
|
|
// 조인 컬럼은 parentDataMapping.targetField에서 추출됨
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-05 10:05:31 +09:00
|
|
|
|
|
|
|
|
if (summaryMap[screenId]) {
|
|
|
|
|
summaryMap[screenId].widgetCounts[componentKind] =
|
|
|
|
|
(summaryMap[screenId].widgetCounts[componentKind] || 0) + 1;
|
|
|
|
|
summaryMap[screenId].totalComponents++;
|
|
|
|
|
|
|
|
|
|
// 레이아웃 아이템 추가 (미니어처 렌더링용)
|
|
|
|
|
summaryMap[screenId].layoutItems.push({
|
|
|
|
|
x: row.position_x || 0,
|
|
|
|
|
y: row.position_y || 0,
|
|
|
|
|
width: row.width || 100,
|
|
|
|
|
height: row.height || 30,
|
|
|
|
|
componentKind: componentKind, // 정확한 컴포넌트 종류
|
|
|
|
|
widgetType: widgetType,
|
|
|
|
|
label: row.label,
|
2026-01-07 14:49:49 +09:00
|
|
|
bindField: row.bind_field || null, // 바인딩된 컬럼명
|
|
|
|
|
usedColumns: usedColumns, // 이 컴포넌트에서 사용하는 컬럼 목록
|
|
|
|
|
joinColumns: joinColumns, // 이 컴포넌트에서 조인 컬럼 목록
|
2026-01-05 10:05:31 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 캔버스 크기 계산 (최대 좌표 기준)
|
|
|
|
|
const rightEdge = (row.position_x || 0) + (row.width || 100);
|
|
|
|
|
const bottomEdge = (row.position_y || 0) + (row.height || 30);
|
|
|
|
|
if (rightEdge > summaryMap[screenId].canvasWidth) {
|
|
|
|
|
summaryMap[screenId].canvasWidth = rightEdge;
|
|
|
|
|
}
|
|
|
|
|
if (bottomEdge > summaryMap[screenId].canvasHeight) {
|
|
|
|
|
summaryMap[screenId].canvasHeight = bottomEdge;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 화면 타입 추론 (componentKind 기준)
|
|
|
|
|
Object.values(summaryMap).forEach((summary: any) => {
|
|
|
|
|
if (summary.widgetCounts['table-list'] > 0) {
|
|
|
|
|
summary.screenType = 'grid';
|
|
|
|
|
} else if (summary.widgetCounts['table-search-widget'] > 1) {
|
|
|
|
|
summary.screenType = 'dashboard';
|
|
|
|
|
} else if (summary.totalComponents <= 5 && summary.widgetCounts['button-primary'] > 0) {
|
|
|
|
|
summary.screenType = 'action';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
logger.info("여러 화면 레이아웃 요약 조회", { screenIds, count: Object.keys(summaryMap).length });
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: summaryMap,
|
|
|
|
|
});
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("여러 화면 레이아웃 요약 조회 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "여러 화면 레이아웃 요약 조회에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-05 18:18:26 +09:00
|
|
|
// ============================================================
|
|
|
|
|
// 화면 서브 테이블 관계 조회 (조인/참조 테이블)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
// 여러 화면의 서브 테이블 정보 조회 (메인 테이블 → 서브 테이블 관계)
|
|
|
|
|
export const getScreenSubTables = async (req: Request, res: Response) => {
|
|
|
|
|
try {
|
|
|
|
|
const { screenIds } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!screenIds || !Array.isArray(screenIds) || screenIds.length === 0) {
|
|
|
|
|
return res.status(400).json({ success: false, message: "screenIds 배열이 필요합니다." });
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 14:49:49 +09:00
|
|
|
// 화면별 서브 테이블 그룹화
|
|
|
|
|
const screenSubTables: Record<number, {
|
|
|
|
|
screenId: number;
|
|
|
|
|
screenName: string;
|
|
|
|
|
mainTable: string;
|
|
|
|
|
subTables: Array<{
|
|
|
|
|
tableName: string;
|
|
|
|
|
componentType: string;
|
|
|
|
|
relationType: string; // 'join' | 'lookup' | 'source' | 'reference'
|
|
|
|
|
fieldMappings?: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }>;
|
|
|
|
|
}>;
|
|
|
|
|
}> = {};
|
|
|
|
|
|
|
|
|
|
// 1. 기존 방식: componentConfig에서 tableName, sourceTable, fieldMappings 추출
|
|
|
|
|
const componentQuery = `
|
2026-01-05 18:18:26 +09:00
|
|
|
SELECT DISTINCT
|
|
|
|
|
sd.screen_id,
|
|
|
|
|
sd.screen_name,
|
|
|
|
|
sd.table_name as main_table,
|
|
|
|
|
COALESCE(
|
|
|
|
|
sl.properties->'componentConfig'->>'tableName',
|
|
|
|
|
sl.properties->'componentConfig'->>'sourceTable'
|
|
|
|
|
) as sub_table,
|
|
|
|
|
sl.properties->>'componentType' as component_type,
|
2026-01-07 14:49:49 +09:00
|
|
|
sl.properties->'componentConfig'->>'targetTable' as target_table,
|
|
|
|
|
sl.properties->'componentConfig'->'fieldMappings' as field_mappings,
|
|
|
|
|
sl.properties->'componentConfig'->'columns' as columns_config
|
2026-01-05 18:18:26 +09:00
|
|
|
FROM screen_definitions sd
|
|
|
|
|
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
|
|
|
|
|
WHERE sd.screen_id = ANY($1)
|
|
|
|
|
AND (
|
|
|
|
|
sl.properties->'componentConfig'->>'tableName' IS NOT NULL
|
|
|
|
|
OR sl.properties->'componentConfig'->>'sourceTable' IS NOT NULL
|
|
|
|
|
)
|
|
|
|
|
ORDER BY sd.screen_id
|
|
|
|
|
`;
|
|
|
|
|
|
2026-01-07 14:49:49 +09:00
|
|
|
const componentResult = await pool.query(componentQuery, [screenIds]);
|
|
|
|
|
|
|
|
|
|
// fieldMappings의 한글 컬럼명을 조회하기 위한 테이블-컬럼 쌍 수집
|
|
|
|
|
const columnLabelLookups: Array<{ table: string; column: string }> = [];
|
|
|
|
|
componentResult.rows.forEach((row: any) => {
|
|
|
|
|
if (row.field_mappings && Array.isArray(row.field_mappings)) {
|
|
|
|
|
row.field_mappings.forEach((fm: any) => {
|
|
|
|
|
const mainTable = row.main_table;
|
|
|
|
|
const subTable = row.sub_table;
|
|
|
|
|
if (fm.sourceField && subTable) {
|
|
|
|
|
columnLabelLookups.push({ table: subTable, column: fm.sourceField });
|
|
|
|
|
}
|
|
|
|
|
if (fm.targetField && mainTable) {
|
|
|
|
|
columnLabelLookups.push({ table: mainTable, column: fm.targetField });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-01-05 18:18:26 +09:00
|
|
|
|
2026-01-07 14:49:49 +09:00
|
|
|
// 한글 컬럼명 조회
|
|
|
|
|
const columnLabelMap = new Map<string, string>(); // "table.column" -> "한글명"
|
|
|
|
|
if (columnLabelLookups.length > 0) {
|
|
|
|
|
const uniqueLookups = [...new Set(columnLabelLookups.map(l => `${l.table}|${l.column}`))];
|
|
|
|
|
const conditions = uniqueLookups.map((lookup, i) => {
|
|
|
|
|
const [table, column] = lookup.split('|');
|
|
|
|
|
return `(table_name = $${i * 2 + 1} AND column_name = $${i * 2 + 2})`;
|
|
|
|
|
});
|
|
|
|
|
const params = uniqueLookups.flatMap(lookup => lookup.split('|'));
|
|
|
|
|
|
|
|
|
|
if (conditions.length > 0) {
|
|
|
|
|
const labelQuery = `
|
|
|
|
|
SELECT table_name, column_name, column_label
|
|
|
|
|
FROM column_labels
|
|
|
|
|
WHERE ${conditions.join(' OR ')}
|
|
|
|
|
`;
|
|
|
|
|
const labelResult = await pool.query(labelQuery, params);
|
|
|
|
|
labelResult.rows.forEach((row: any) => {
|
|
|
|
|
const key = `${row.table_name}.${row.column_name}`;
|
|
|
|
|
columnLabelMap.set(key, row.column_label || row.column_name);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-05 18:18:26 +09:00
|
|
|
|
2026-01-07 14:49:49 +09:00
|
|
|
componentResult.rows.forEach((row: any) => {
|
2026-01-05 18:18:26 +09:00
|
|
|
const screenId = row.screen_id;
|
|
|
|
|
const mainTable = row.main_table;
|
|
|
|
|
const subTable = row.sub_table;
|
|
|
|
|
|
|
|
|
|
// 메인 테이블과 동일한 경우 제외
|
|
|
|
|
if (!subTable || subTable === mainTable) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!screenSubTables[screenId]) {
|
|
|
|
|
screenSubTables[screenId] = {
|
|
|
|
|
screenId,
|
|
|
|
|
screenName: row.screen_name,
|
|
|
|
|
mainTable: mainTable || '',
|
|
|
|
|
subTables: [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 중복 체크
|
|
|
|
|
const exists = screenSubTables[screenId].subTables.some(
|
|
|
|
|
(st) => st.tableName === subTable
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!exists) {
|
|
|
|
|
// 관계 타입 추론
|
|
|
|
|
let relationType = 'lookup';
|
|
|
|
|
const componentType = row.component_type || '';
|
|
|
|
|
if (componentType.includes('autocomplete') || componentType.includes('entity-search')) {
|
|
|
|
|
relationType = 'lookup';
|
|
|
|
|
} else if (componentType.includes('modal-repeater') || componentType.includes('selected-items')) {
|
|
|
|
|
relationType = 'source';
|
|
|
|
|
} else if (componentType.includes('table')) {
|
|
|
|
|
relationType = 'join';
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 14:49:49 +09:00
|
|
|
// fieldMappings 파싱 (JSON 배열 또는 null)
|
|
|
|
|
let fieldMappings: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }> | undefined;
|
|
|
|
|
|
|
|
|
|
if (row.field_mappings && Array.isArray(row.field_mappings)) {
|
|
|
|
|
// 1. 직접 fieldMappings가 있는 경우
|
|
|
|
|
fieldMappings = row.field_mappings.map((fm: any) => {
|
|
|
|
|
const sourceField = fm.sourceField || fm.source_field || '';
|
|
|
|
|
const targetField = fm.targetField || fm.target_field || '';
|
|
|
|
|
|
|
|
|
|
// 한글 컬럼명 조회 (sourceField는 서브테이블 컬럼, targetField는 메인테이블 컬럼)
|
|
|
|
|
const sourceKey = `${subTable}.${sourceField}`;
|
|
|
|
|
const targetKey = `${mainTable}.${targetField}`;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
sourceField,
|
|
|
|
|
targetField,
|
|
|
|
|
// sourceField(서브테이블 컬럼)의 한글명
|
|
|
|
|
sourceDisplayName: columnLabelMap.get(sourceKey) || sourceField,
|
|
|
|
|
// targetField(메인테이블 컬럼)의 한글명
|
|
|
|
|
targetDisplayName: columnLabelMap.get(targetKey) || targetField,
|
|
|
|
|
};
|
|
|
|
|
}).filter((fm: any) => fm.sourceField || fm.targetField);
|
|
|
|
|
} else if (row.columns_config && Array.isArray(row.columns_config)) {
|
|
|
|
|
// 2. columns_config.mapping에서 추출 (item_info 같은 경우)
|
|
|
|
|
// mapping.type === 'source'인 경우: sourceField(서브테이블) → field(메인테이블)
|
|
|
|
|
fieldMappings = [];
|
|
|
|
|
row.columns_config.forEach((col: any) => {
|
|
|
|
|
if (col.mapping && col.mapping.type === 'source' && col.mapping.sourceField) {
|
|
|
|
|
fieldMappings!.push({
|
|
|
|
|
sourceField: col.field || '', // 메인 테이블 컬럼
|
|
|
|
|
targetField: col.mapping.sourceField || '', // 서브 테이블 컬럼
|
|
|
|
|
sourceDisplayName: col.label || col.field || '', // 한글 라벨
|
|
|
|
|
targetDisplayName: col.mapping.sourceField || '', // 서브 테이블은 영문만
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (fieldMappings.length === 0) {
|
|
|
|
|
fieldMappings = undefined;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 18:18:26 +09:00
|
|
|
screenSubTables[screenId].subTables.push({
|
|
|
|
|
tableName: subTable,
|
|
|
|
|
componentType: componentType,
|
|
|
|
|
relationType: relationType,
|
2026-01-07 14:49:49 +09:00
|
|
|
fieldMappings: fieldMappings,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 2. 추가 방식: 화면에서 사용하는 컬럼 중 column_labels.reference_table이 설정된 경우
|
|
|
|
|
// 화면의 usedColumns/joinColumns에서 reference_table 조회
|
|
|
|
|
const referenceQuery = `
|
|
|
|
|
WITH screen_used_columns AS (
|
|
|
|
|
-- 화면별 사용 컬럼 추출 (componentConfig.columns에서)
|
|
|
|
|
SELECT
|
|
|
|
|
sd.screen_id,
|
|
|
|
|
sd.screen_name,
|
|
|
|
|
sd.table_name as main_table,
|
|
|
|
|
jsonb_array_elements_text(
|
|
|
|
|
COALESCE(
|
|
|
|
|
sl.properties->'componentConfig'->'columns',
|
|
|
|
|
'[]'::jsonb
|
|
|
|
|
)
|
|
|
|
|
)::jsonb->>'columnName' as column_name
|
|
|
|
|
FROM screen_definitions sd
|
|
|
|
|
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
|
|
|
|
|
WHERE sd.screen_id = ANY($1)
|
|
|
|
|
AND sl.properties->'componentConfig'->'columns' IS NOT NULL
|
|
|
|
|
AND jsonb_array_length(sl.properties->'componentConfig'->'columns') > 0
|
|
|
|
|
|
|
|
|
|
UNION
|
|
|
|
|
|
|
|
|
|
-- bindField도 포함
|
|
|
|
|
SELECT
|
|
|
|
|
sd.screen_id,
|
|
|
|
|
sd.screen_name,
|
|
|
|
|
sd.table_name as main_table,
|
|
|
|
|
COALESCE(
|
|
|
|
|
sl.properties->'componentConfig'->>'bindField',
|
|
|
|
|
sl.properties->>'bindField',
|
|
|
|
|
sl.properties->'componentConfig'->>'field',
|
|
|
|
|
sl.properties->>'field'
|
|
|
|
|
) as column_name
|
|
|
|
|
FROM screen_definitions sd
|
|
|
|
|
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
|
|
|
|
|
WHERE sd.screen_id = ANY($1)
|
|
|
|
|
AND (
|
|
|
|
|
sl.properties->'componentConfig'->>'bindField' IS NOT NULL
|
|
|
|
|
OR sl.properties->>'bindField' IS NOT NULL
|
|
|
|
|
OR sl.properties->'componentConfig'->>'field' IS NOT NULL
|
|
|
|
|
OR sl.properties->>'field' IS NOT NULL
|
|
|
|
|
)
|
2026-01-08 14:24:33 +09:00
|
|
|
|
|
|
|
|
UNION
|
|
|
|
|
|
|
|
|
|
-- valueField 추출 (entity-search-input, autocomplete-search-input 등에서 사용)
|
|
|
|
|
SELECT
|
|
|
|
|
sd.screen_id,
|
|
|
|
|
sd.screen_name,
|
|
|
|
|
sd.table_name as main_table,
|
|
|
|
|
sl.properties->'componentConfig'->>'valueField' as column_name
|
|
|
|
|
FROM screen_definitions sd
|
|
|
|
|
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
|
|
|
|
|
WHERE sd.screen_id = ANY($1)
|
|
|
|
|
AND sl.properties->'componentConfig'->>'valueField' IS NOT NULL
|
|
|
|
|
|
|
|
|
|
UNION
|
|
|
|
|
|
|
|
|
|
-- parentFieldId 추출 (캐스케이딩 관계에서 사용)
|
|
|
|
|
SELECT
|
|
|
|
|
sd.screen_id,
|
|
|
|
|
sd.screen_name,
|
|
|
|
|
sd.table_name as main_table,
|
|
|
|
|
sl.properties->'componentConfig'->>'parentFieldId' as column_name
|
|
|
|
|
FROM screen_definitions sd
|
|
|
|
|
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
|
|
|
|
|
WHERE sd.screen_id = ANY($1)
|
|
|
|
|
AND sl.properties->'componentConfig'->>'parentFieldId' IS NOT NULL
|
|
|
|
|
|
|
|
|
|
UNION
|
|
|
|
|
|
|
|
|
|
-- cascadingParentField 추출 (캐스케이딩 부모 필드)
|
|
|
|
|
SELECT
|
|
|
|
|
sd.screen_id,
|
|
|
|
|
sd.screen_name,
|
|
|
|
|
sd.table_name as main_table,
|
|
|
|
|
sl.properties->'componentConfig'->>'cascadingParentField' as column_name
|
|
|
|
|
FROM screen_definitions sd
|
|
|
|
|
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
|
|
|
|
|
WHERE sd.screen_id = ANY($1)
|
|
|
|
|
AND sl.properties->'componentConfig'->>'cascadingParentField' IS NOT NULL
|
|
|
|
|
|
|
|
|
|
UNION
|
|
|
|
|
|
|
|
|
|
-- controlField 추출 (conditional-container에서 사용)
|
|
|
|
|
SELECT
|
|
|
|
|
sd.screen_id,
|
|
|
|
|
sd.screen_name,
|
|
|
|
|
sd.table_name as main_table,
|
|
|
|
|
sl.properties->'componentConfig'->>'controlField' as column_name
|
|
|
|
|
FROM screen_definitions sd
|
|
|
|
|
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
|
|
|
|
|
WHERE sd.screen_id = ANY($1)
|
|
|
|
|
AND sl.properties->'componentConfig'->>'controlField' IS NOT NULL
|
2026-01-07 14:49:49 +09:00
|
|
|
)
|
|
|
|
|
SELECT DISTINCT
|
|
|
|
|
suc.screen_id,
|
|
|
|
|
suc.screen_name,
|
|
|
|
|
suc.main_table,
|
|
|
|
|
suc.column_name,
|
|
|
|
|
cl.column_label as source_display_name,
|
|
|
|
|
cl.reference_table,
|
|
|
|
|
cl.reference_column,
|
|
|
|
|
ref_cl.column_label as target_display_name
|
|
|
|
|
FROM screen_used_columns suc
|
|
|
|
|
JOIN column_labels cl ON cl.table_name = suc.main_table AND cl.column_name = suc.column_name
|
|
|
|
|
LEFT JOIN column_labels ref_cl ON ref_cl.table_name = cl.reference_table AND ref_cl.column_name = cl.reference_column
|
|
|
|
|
WHERE cl.reference_table IS NOT NULL
|
|
|
|
|
AND cl.reference_table != ''
|
|
|
|
|
AND cl.reference_table != suc.main_table
|
2026-01-08 14:24:33 +09:00
|
|
|
AND cl.input_type = 'entity'
|
2026-01-07 14:49:49 +09:00
|
|
|
ORDER BY suc.screen_id
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const referenceResult = await pool.query(referenceQuery, [screenIds]);
|
|
|
|
|
|
|
|
|
|
logger.info("column_labels reference_table 조회 결과", {
|
|
|
|
|
screenIds,
|
|
|
|
|
referenceCount: referenceResult.rows.length,
|
|
|
|
|
references: referenceResult.rows.map((r: any) => ({
|
|
|
|
|
screenId: r.screen_id,
|
|
|
|
|
column: r.column_name,
|
|
|
|
|
refTable: r.reference_table
|
|
|
|
|
}))
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
referenceResult.rows.forEach((row: any) => {
|
|
|
|
|
const screenId = row.screen_id;
|
|
|
|
|
const mainTable = row.main_table;
|
|
|
|
|
const referenceTable = row.reference_table;
|
|
|
|
|
|
|
|
|
|
if (!referenceTable || referenceTable === mainTable) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!screenSubTables[screenId]) {
|
|
|
|
|
screenSubTables[screenId] = {
|
|
|
|
|
screenId,
|
|
|
|
|
screenName: row.screen_name,
|
|
|
|
|
mainTable: mainTable || '',
|
|
|
|
|
subTables: [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 중복 체크
|
|
|
|
|
const exists = screenSubTables[screenId].subTables.some(
|
|
|
|
|
(st) => st.tableName === referenceTable
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!exists) {
|
|
|
|
|
screenSubTables[screenId].subTables.push({
|
|
|
|
|
tableName: referenceTable,
|
|
|
|
|
componentType: 'column_reference',
|
|
|
|
|
relationType: 'reference',
|
|
|
|
|
fieldMappings: [{
|
|
|
|
|
sourceField: row.column_name,
|
|
|
|
|
targetField: row.reference_column || 'id',
|
|
|
|
|
sourceDisplayName: row.source_display_name || row.column_name,
|
|
|
|
|
targetDisplayName: row.target_display_name || row.reference_column || 'id',
|
|
|
|
|
}],
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// 이미 존재하면 fieldMappings에 추가
|
|
|
|
|
const existingSubTable = screenSubTables[screenId].subTables.find(
|
|
|
|
|
(st) => st.tableName === referenceTable
|
|
|
|
|
);
|
|
|
|
|
if (existingSubTable && existingSubTable.fieldMappings) {
|
|
|
|
|
const mappingExists = existingSubTable.fieldMappings.some(
|
|
|
|
|
(fm) => fm.sourceField === row.column_name
|
|
|
|
|
);
|
|
|
|
|
if (!mappingExists) {
|
|
|
|
|
existingSubTable.fieldMappings.push({
|
|
|
|
|
sourceField: row.column_name,
|
|
|
|
|
targetField: row.reference_column || 'id',
|
|
|
|
|
sourceDisplayName: row.source_display_name || row.column_name,
|
|
|
|
|
targetDisplayName: row.target_display_name || row.reference_column || 'id',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 3. parentDataMapping 파싱 (selected-items-detail-input 등에서 사용)
|
|
|
|
|
const parentMappingQuery = `
|
|
|
|
|
SELECT
|
|
|
|
|
sd.screen_id,
|
|
|
|
|
sd.screen_name,
|
|
|
|
|
sd.table_name as main_table,
|
|
|
|
|
sl.properties->>'componentType' as component_type,
|
|
|
|
|
sl.properties->'componentConfig'->'parentDataMapping' as parent_data_mapping
|
|
|
|
|
FROM screen_definitions sd
|
|
|
|
|
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
|
|
|
|
|
WHERE sd.screen_id = ANY($1)
|
|
|
|
|
AND sl.properties->'componentConfig'->'parentDataMapping' IS NOT NULL
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const parentMappingResult = await pool.query(parentMappingQuery, [screenIds]);
|
|
|
|
|
|
|
|
|
|
parentMappingResult.rows.forEach((row: any) => {
|
|
|
|
|
const screenId = row.screen_id;
|
|
|
|
|
const mainTable = row.main_table;
|
|
|
|
|
const componentType = row.component_type || 'parentDataMapping';
|
|
|
|
|
const parentDataMapping = row.parent_data_mapping;
|
|
|
|
|
|
|
|
|
|
if (!Array.isArray(parentDataMapping)) return;
|
|
|
|
|
|
|
|
|
|
if (!screenSubTables[screenId]) {
|
|
|
|
|
screenSubTables[screenId] = {
|
|
|
|
|
screenId,
|
|
|
|
|
screenName: row.screen_name,
|
|
|
|
|
mainTable: mainTable || '',
|
|
|
|
|
subTables: [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parentDataMapping.forEach((mapping: any) => {
|
|
|
|
|
const sourceTable = mapping.sourceTable;
|
|
|
|
|
if (!sourceTable || sourceTable === mainTable) return;
|
|
|
|
|
|
|
|
|
|
// 중복 체크
|
|
|
|
|
const existingSubTable = screenSubTables[screenId].subTables.find(
|
|
|
|
|
(st) => st.tableName === sourceTable
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const newMapping = {
|
|
|
|
|
sourceTable: sourceTable, // 연관 테이블 정보 추가
|
|
|
|
|
sourceField: mapping.sourceField || '',
|
|
|
|
|
targetField: mapping.targetField || '',
|
|
|
|
|
sourceDisplayName: mapping.sourceField || '',
|
|
|
|
|
targetDisplayName: mapping.targetField || '',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (existingSubTable) {
|
|
|
|
|
// 이미 존재하면 fieldMappings에 추가
|
|
|
|
|
if (!existingSubTable.fieldMappings) {
|
|
|
|
|
existingSubTable.fieldMappings = [];
|
|
|
|
|
}
|
|
|
|
|
const mappingExists = existingSubTable.fieldMappings.some(
|
|
|
|
|
(fm: any) => fm.sourceField === newMapping.sourceField && fm.targetField === newMapping.targetField
|
|
|
|
|
);
|
|
|
|
|
if (!mappingExists) {
|
|
|
|
|
existingSubTable.fieldMappings.push(newMapping);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
screenSubTables[screenId].subTables.push({
|
|
|
|
|
tableName: sourceTable,
|
|
|
|
|
componentType: componentType,
|
|
|
|
|
relationType: 'parentMapping',
|
|
|
|
|
fieldMappings: [newMapping],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
logger.info("parentDataMapping 파싱 완료", {
|
|
|
|
|
screenIds,
|
|
|
|
|
parentMappingCount: parentMappingResult.rows.length
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 4. rightPanel.relation 파싱 (split-panel-layout 등에서 사용)
|
|
|
|
|
const rightPanelQuery = `
|
|
|
|
|
SELECT
|
|
|
|
|
sd.screen_id,
|
|
|
|
|
sd.screen_name,
|
|
|
|
|
sd.table_name as main_table,
|
|
|
|
|
sl.properties->>'componentType' as component_type,
|
|
|
|
|
sl.properties->'componentConfig'->'rightPanel'->'relation' as right_panel_relation,
|
|
|
|
|
sl.properties->'componentConfig'->'rightPanel'->'tableName' as right_panel_table
|
|
|
|
|
FROM screen_definitions sd
|
|
|
|
|
JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
|
|
|
|
|
WHERE sd.screen_id = ANY($1)
|
|
|
|
|
AND sl.properties->'componentConfig'->'rightPanel'->'relation' IS NOT NULL
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const rightPanelResult = await pool.query(rightPanelQuery, [screenIds]);
|
|
|
|
|
|
|
|
|
|
rightPanelResult.rows.forEach((row: any) => {
|
|
|
|
|
const screenId = row.screen_id;
|
|
|
|
|
const mainTable = row.main_table;
|
|
|
|
|
const componentType = row.component_type || 'split-panel-layout';
|
|
|
|
|
const relation = row.right_panel_relation;
|
|
|
|
|
const rightPanelTable = row.right_panel_table;
|
|
|
|
|
|
|
|
|
|
// relation 객체에서 테이블 및 필드 매핑 추출
|
|
|
|
|
const subTable = rightPanelTable || relation?.targetTable || relation?.tableName;
|
|
|
|
|
if (!subTable || subTable === mainTable) return;
|
|
|
|
|
|
|
|
|
|
if (!screenSubTables[screenId]) {
|
|
|
|
|
screenSubTables[screenId] = {
|
|
|
|
|
screenId,
|
|
|
|
|
screenName: row.screen_name,
|
|
|
|
|
mainTable: mainTable || '',
|
|
|
|
|
subTables: [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 중복 체크
|
|
|
|
|
const existingSubTable = screenSubTables[screenId].subTables.find(
|
|
|
|
|
(st) => st.tableName === subTable
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// relation에서 필드 매핑 추출
|
|
|
|
|
const fieldMappings: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }> = [];
|
|
|
|
|
|
|
|
|
|
if (relation?.sourceField && relation?.targetField) {
|
|
|
|
|
fieldMappings.push({
|
|
|
|
|
sourceField: relation.sourceField,
|
|
|
|
|
targetField: relation.targetField,
|
|
|
|
|
sourceDisplayName: relation.sourceField,
|
|
|
|
|
targetDisplayName: relation.targetField,
|
2026-01-05 18:18:26 +09:00
|
|
|
});
|
|
|
|
|
}
|
2026-01-07 14:49:49 +09:00
|
|
|
|
|
|
|
|
// fieldMappings 배열이 있는 경우
|
|
|
|
|
if (relation?.fieldMappings && Array.isArray(relation.fieldMappings)) {
|
|
|
|
|
relation.fieldMappings.forEach((fm: any) => {
|
|
|
|
|
fieldMappings.push({
|
|
|
|
|
sourceField: fm.sourceField || fm.source_field || '',
|
|
|
|
|
targetField: fm.targetField || fm.target_field || '',
|
|
|
|
|
sourceDisplayName: fm.sourceField || fm.source_field || '',
|
|
|
|
|
targetDisplayName: fm.targetField || fm.target_field || '',
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (existingSubTable) {
|
|
|
|
|
// 이미 존재하면 fieldMappings에 추가
|
|
|
|
|
if (!existingSubTable.fieldMappings) {
|
|
|
|
|
existingSubTable.fieldMappings = [];
|
|
|
|
|
}
|
|
|
|
|
fieldMappings.forEach((newMapping) => {
|
|
|
|
|
const mappingExists = existingSubTable.fieldMappings!.some(
|
|
|
|
|
(fm) => fm.sourceField === newMapping.sourceField && fm.targetField === newMapping.targetField
|
|
|
|
|
);
|
|
|
|
|
if (!mappingExists) {
|
|
|
|
|
existingSubTable.fieldMappings!.push(newMapping);
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-01-08 14:24:33 +09:00
|
|
|
// 추가 정보도 업데이트
|
|
|
|
|
if (relation?.type) {
|
|
|
|
|
(existingSubTable as any).originalRelationType = relation.type;
|
|
|
|
|
}
|
|
|
|
|
if (relation?.foreignKey) {
|
|
|
|
|
(existingSubTable as any).foreignKey = relation.foreignKey;
|
|
|
|
|
}
|
|
|
|
|
if (relation?.leftColumn) {
|
|
|
|
|
(existingSubTable as any).leftColumn = relation.leftColumn;
|
|
|
|
|
}
|
2026-01-07 14:49:49 +09:00
|
|
|
} else {
|
|
|
|
|
screenSubTables[screenId].subTables.push({
|
|
|
|
|
tableName: subTable,
|
|
|
|
|
componentType: componentType,
|
|
|
|
|
relationType: 'rightPanelRelation',
|
2026-01-08 14:24:33 +09:00
|
|
|
// 관계 유형 추론을 위한 추가 정보
|
|
|
|
|
originalRelationType: relation?.type || 'join', // 원본 relation.type ("join" | "detail")
|
|
|
|
|
foreignKey: relation?.foreignKey, // 디테일 테이블의 FK 컬럼
|
|
|
|
|
leftColumn: relation?.leftColumn, // 마스터 테이블의 선택 기준 컬럼
|
2026-01-07 14:49:49 +09:00
|
|
|
fieldMappings: fieldMappings.length > 0 ? fieldMappings : undefined,
|
2026-01-08 14:24:33 +09:00
|
|
|
} as any);
|
2026-01-07 14:49:49 +09:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
logger.info("rightPanel.relation 파싱 완료", {
|
|
|
|
|
screenIds,
|
|
|
|
|
rightPanelCount: rightPanelResult.rows.length
|
2026-01-05 18:18:26 +09:00
|
|
|
});
|
|
|
|
|
|
2026-01-07 14:49:49 +09:00
|
|
|
// 5. 모든 fieldMappings의 한글명을 column_labels에서 가져와서 적용
|
|
|
|
|
// 모든 테이블/컬럼 조합을 수집
|
|
|
|
|
const columnLookups: Array<{ tableName: string; columnName: string }> = [];
|
|
|
|
|
Object.values(screenSubTables).forEach((screenData: any) => {
|
|
|
|
|
screenData.subTables.forEach((subTable: any) => {
|
|
|
|
|
if (subTable.fieldMappings) {
|
|
|
|
|
subTable.fieldMappings.forEach((mapping: any) => {
|
|
|
|
|
// sourceTable + sourceField (연관 테이블의 컬럼)
|
|
|
|
|
if (mapping.sourceTable && mapping.sourceField) {
|
|
|
|
|
columnLookups.push({ tableName: mapping.sourceTable, columnName: mapping.sourceField });
|
|
|
|
|
}
|
|
|
|
|
// mainTable + targetField (메인 테이블의 컬럼)
|
|
|
|
|
if (screenData.mainTable && mapping.targetField) {
|
|
|
|
|
columnLookups.push({ tableName: screenData.mainTable, columnName: mapping.targetField });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 중복 제거
|
|
|
|
|
const uniqueColumnLookups = columnLookups.filter((item, index, self) =>
|
|
|
|
|
index === self.findIndex((t) => t.tableName === item.tableName && t.columnName === item.columnName)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// column_labels에서 한글명 조회
|
|
|
|
|
const columnLabelsMap: { [key: string]: string } = {};
|
|
|
|
|
if (uniqueColumnLookups.length > 0) {
|
|
|
|
|
const columnLabelsQuery = `
|
|
|
|
|
SELECT
|
|
|
|
|
table_name,
|
|
|
|
|
column_name,
|
|
|
|
|
column_label
|
|
|
|
|
FROM column_labels
|
|
|
|
|
WHERE (table_name, column_name) IN (
|
|
|
|
|
${uniqueColumnLookups.map((_, i) => `($${i * 2 + 1}, $${i * 2 + 2})`).join(', ')}
|
|
|
|
|
)
|
|
|
|
|
`;
|
|
|
|
|
const columnLabelsParams = uniqueColumnLookups.flatMap(item => [item.tableName, item.columnName]);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const columnLabelsResult = await pool.query(columnLabelsQuery, columnLabelsParams);
|
|
|
|
|
columnLabelsResult.rows.forEach((row: any) => {
|
|
|
|
|
const key = `${row.table_name}.${row.column_name}`;
|
|
|
|
|
columnLabelsMap[key] = row.column_label;
|
|
|
|
|
});
|
|
|
|
|
logger.info("column_labels 한글명 조회 완료", { count: columnLabelsResult.rows.length });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.warn("column_labels 한글명 조회 실패 (무시하고 계속 진행):", error.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 각 fieldMappings에 한글명 적용
|
|
|
|
|
Object.values(screenSubTables).forEach((screenData: any) => {
|
|
|
|
|
screenData.subTables.forEach((subTable: any) => {
|
|
|
|
|
if (subTable.fieldMappings) {
|
|
|
|
|
subTable.fieldMappings.forEach((mapping: any) => {
|
|
|
|
|
// sourceDisplayName: 연관 테이블의 컬럼 한글명
|
|
|
|
|
if (mapping.sourceTable && mapping.sourceField) {
|
|
|
|
|
const sourceKey = `${mapping.sourceTable}.${mapping.sourceField}`;
|
|
|
|
|
if (columnLabelsMap[sourceKey]) {
|
|
|
|
|
mapping.sourceDisplayName = columnLabelsMap[sourceKey];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// targetDisplayName: 메인 테이블의 컬럼 한글명
|
|
|
|
|
if (screenData.mainTable && mapping.targetField) {
|
|
|
|
|
const targetKey = `${screenData.mainTable}.${mapping.targetField}`;
|
|
|
|
|
if (columnLabelsMap[targetKey]) {
|
|
|
|
|
mapping.targetDisplayName = columnLabelsMap[targetKey];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
logger.info("화면 서브 테이블 정보 조회 완료", {
|
|
|
|
|
screenIds,
|
|
|
|
|
resultCount: Object.keys(screenSubTables).length,
|
|
|
|
|
details: Object.values(screenSubTables).map(s => ({
|
|
|
|
|
screenId: s.screenId,
|
|
|
|
|
mainTable: s.mainTable,
|
|
|
|
|
subTables: s.subTables.map(st => st.tableName)
|
|
|
|
|
}))
|
|
|
|
|
});
|
2026-01-05 18:18:26 +09:00
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: screenSubTables,
|
|
|
|
|
});
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("화면 서브 테이블 정보 조회 실패:", error);
|
|
|
|
|
res.status(500).json({ success: false, message: "화면 서브 테이블 정보 조회에 실패했습니다.", error: error.message });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|