Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node
This commit is contained in:
@@ -71,6 +71,7 @@ import codeMergeRoutes from "./routes/codeMergeRoutes"; // 코드 병합
|
||||
import numberingRuleRoutes from "./routes/numberingRuleRoutes"; // 채번 규칙 관리
|
||||
import entitySearchRoutes from "./routes/entitySearchRoutes"; // 엔티티 검색
|
||||
import orderRoutes from "./routes/orderRoutes"; // 수주 관리
|
||||
import screenEmbeddingRoutes from "./routes/screenEmbeddingRoutes"; // 화면 임베딩 및 데이터 전달
|
||||
import { BatchSchedulerService } from "./services/batchSchedulerService";
|
||||
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
||||
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석
|
||||
@@ -236,6 +237,7 @@ app.use("/api/code-merge", codeMergeRoutes); // 코드 병합
|
||||
app.use("/api/numbering-rules", numberingRuleRoutes); // 채번 규칙 관리
|
||||
app.use("/api/entity-search", entitySearchRoutes); // 엔티티 검색
|
||||
app.use("/api/orders", orderRoutes); // 수주 관리
|
||||
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
||||
// app.use("/api/collections", collectionRoutes); // 임시 주석
|
||||
// app.use("/api/batch", batchRoutes); // 임시 주석
|
||||
// app.use('/api/users', userRoutes);
|
||||
|
||||
@@ -419,3 +419,66 @@ export const getTableColumns = async (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 특정 필드만 업데이트 (다른 테이블 지원)
|
||||
export const updateFieldValue = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<Response | void> => {
|
||||
try {
|
||||
const { companyCode, userId } = req.user as any;
|
||||
const { tableName, keyField, keyValue, updateField, updateValue } = req.body;
|
||||
|
||||
console.log("🔄 [updateFieldValue] 요청:", {
|
||||
tableName,
|
||||
keyField,
|
||||
keyValue,
|
||||
updateField,
|
||||
updateValue,
|
||||
userId,
|
||||
companyCode,
|
||||
});
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!tableName || !keyField || keyValue === undefined || !updateField || updateValue === undefined) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "필수 필드가 누락되었습니다. (tableName, keyField, keyValue, updateField, updateValue)",
|
||||
});
|
||||
}
|
||||
|
||||
// SQL 인젝션 방지를 위한 테이블명/컬럼명 검증
|
||||
const validNamePattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||
if (!validNamePattern.test(tableName) || !validNamePattern.test(keyField) || !validNamePattern.test(updateField)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 테이블명 또는 컬럼명입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 업데이트 쿼리 실행
|
||||
const result = await dynamicFormService.updateFieldValue(
|
||||
tableName,
|
||||
keyField,
|
||||
keyValue,
|
||||
updateField,
|
||||
updateValue,
|
||||
companyCode,
|
||||
userId
|
||||
);
|
||||
|
||||
console.log("✅ [updateFieldValue] 성공:", result);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: "필드 값이 업데이트되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("❌ [updateFieldValue] 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "필드 업데이트에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
924
backend-node/src/controllers/screenEmbeddingController.ts
Normal file
924
backend-node/src/controllers/screenEmbeddingController.ts
Normal file
@@ -0,0 +1,924 @@
|
||||
/**
|
||||
* 화면 임베딩 및 데이터 전달 시스템 컨트롤러
|
||||
*/
|
||||
|
||||
import { Request, Response } from "express";
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const pool = getPool();
|
||||
|
||||
// ============================================
|
||||
// 1. 화면 임베딩 API
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 화면 임베딩 목록 조회
|
||||
* GET /api/screen-embedding?parentScreenId=1
|
||||
*/
|
||||
export async function getScreenEmbeddings(req: Request, res: Response) {
|
||||
try {
|
||||
const { parentScreenId } = req.query;
|
||||
const companyCode = req.user!.companyCode;
|
||||
|
||||
if (!parentScreenId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "부모 화면 ID가 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
se.*,
|
||||
ps.screen_name as parent_screen_name,
|
||||
cs.screen_name as child_screen_name
|
||||
FROM screen_embedding se
|
||||
LEFT JOIN screen_definitions ps ON se.parent_screen_id = ps.screen_id
|
||||
LEFT JOIN screen_definitions cs ON se.child_screen_id = cs.screen_id
|
||||
WHERE se.parent_screen_id = $1
|
||||
AND se.company_code = $2
|
||||
ORDER BY se.position, se.created_at
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [parentScreenId, companyCode]);
|
||||
|
||||
logger.info("화면 임베딩 목록 조회", {
|
||||
companyCode,
|
||||
parentScreenId,
|
||||
count: result.rowCount,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: result.rows,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("화면 임베딩 목록 조회 실패", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "화면 임베딩 목록 조회 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면 임베딩 상세 조회
|
||||
* GET /api/screen-embedding/:id
|
||||
*/
|
||||
export async function getScreenEmbeddingById(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user!.companyCode;
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
se.*,
|
||||
ps.screen_name as parent_screen_name,
|
||||
cs.screen_name as child_screen_name
|
||||
FROM screen_embedding se
|
||||
LEFT JOIN screen_definitions ps ON se.parent_screen_id = ps.screen_id
|
||||
LEFT JOIN screen_definitions cs ON se.child_screen_id = cs.screen_id
|
||||
WHERE se.id = $1
|
||||
AND se.company_code = $2
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [id, companyCode]);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "화면 임베딩 설정을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("화면 임베딩 상세 조회", { companyCode, id });
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("화면 임베딩 상세 조회 실패", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "화면 임베딩 상세 조회 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면 임베딩 생성
|
||||
* POST /api/screen-embedding
|
||||
*/
|
||||
export async function createScreenEmbedding(req: Request, res: Response) {
|
||||
try {
|
||||
const {
|
||||
parentScreenId,
|
||||
childScreenId,
|
||||
position,
|
||||
mode,
|
||||
config = {},
|
||||
} = req.body;
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!parentScreenId || !childScreenId || !position || !mode) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "필수 필드가 누락되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const query = `
|
||||
INSERT INTO screen_embedding (
|
||||
parent_screen_id, child_screen_id, position, mode,
|
||||
config, company_code, created_by, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [
|
||||
parentScreenId,
|
||||
childScreenId,
|
||||
position,
|
||||
mode,
|
||||
JSON.stringify(config),
|
||||
companyCode,
|
||||
userId,
|
||||
]);
|
||||
|
||||
logger.info("화면 임베딩 생성", {
|
||||
companyCode,
|
||||
userId,
|
||||
id: result.rows[0].id,
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("화면 임베딩 생성 실패", error);
|
||||
|
||||
// 유니크 제약조건 위반
|
||||
if (error.code === "23505") {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: "이미 동일한 임베딩 설정이 존재합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "화면 임베딩 생성 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면 임베딩 수정
|
||||
* PUT /api/screen-embedding/:id
|
||||
*/
|
||||
export async function updateScreenEmbedding(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { position, mode, config } = req.body;
|
||||
const companyCode = req.user!.companyCode;
|
||||
|
||||
const updates: string[] = [];
|
||||
const values: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (position) {
|
||||
updates.push(`position = $${paramIndex++}`);
|
||||
values.push(position);
|
||||
}
|
||||
|
||||
if (mode) {
|
||||
updates.push(`mode = $${paramIndex++}`);
|
||||
values.push(mode);
|
||||
}
|
||||
|
||||
if (config) {
|
||||
updates.push(`config = $${paramIndex++}`);
|
||||
values.push(JSON.stringify(config));
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "수정할 내용이 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
updates.push(`updated_at = NOW()`);
|
||||
|
||||
values.push(id, companyCode);
|
||||
|
||||
const query = `
|
||||
UPDATE screen_embedding
|
||||
SET ${updates.join(", ")}
|
||||
WHERE id = $${paramIndex++}
|
||||
AND company_code = $${paramIndex++}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, values);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "화면 임베딩 설정을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("화면 임베딩 수정", { companyCode, id });
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("화면 임베딩 수정 실패", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "화면 임베딩 수정 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면 임베딩 삭제
|
||||
* DELETE /api/screen-embedding/:id
|
||||
*/
|
||||
export async function deleteScreenEmbedding(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user!.companyCode;
|
||||
|
||||
const query = `
|
||||
DELETE FROM screen_embedding
|
||||
WHERE id = $1 AND company_code = $2
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [id, companyCode]);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "화면 임베딩 설정을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("화면 임베딩 삭제", { companyCode, id });
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "화면 임베딩이 삭제되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("화면 임베딩 삭제 실패", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "화면 임베딩 삭제 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 2. 데이터 전달 API
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 데이터 전달 설정 조회
|
||||
* GET /api/screen-data-transfer?sourceScreenId=1&targetScreenId=2
|
||||
*/
|
||||
export async function getScreenDataTransfer(req: Request, res: Response) {
|
||||
try {
|
||||
const { sourceScreenId, targetScreenId } = req.query;
|
||||
const companyCode = req.user!.companyCode;
|
||||
|
||||
if (!sourceScreenId || !targetScreenId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "소스 화면 ID와 타겟 화면 ID가 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
sdt.*,
|
||||
ss.screen_name as source_screen_name,
|
||||
ts.screen_name as target_screen_name
|
||||
FROM screen_data_transfer sdt
|
||||
LEFT JOIN screen_definitions ss ON sdt.source_screen_id = ss.screen_id
|
||||
LEFT JOIN screen_definitions ts ON sdt.target_screen_id = ts.screen_id
|
||||
WHERE sdt.source_screen_id = $1
|
||||
AND sdt.target_screen_id = $2
|
||||
AND sdt.company_code = $3
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [
|
||||
sourceScreenId,
|
||||
targetScreenId,
|
||||
companyCode,
|
||||
]);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "데이터 전달 설정을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("데이터 전달 설정 조회", {
|
||||
companyCode,
|
||||
sourceScreenId,
|
||||
targetScreenId,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("데이터 전달 설정 조회 실패", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "데이터 전달 설정 조회 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 전달 설정 생성
|
||||
* POST /api/screen-data-transfer
|
||||
*/
|
||||
export async function createScreenDataTransfer(req: Request, res: Response) {
|
||||
try {
|
||||
const {
|
||||
sourceScreenId,
|
||||
targetScreenId,
|
||||
sourceComponentId,
|
||||
sourceComponentType,
|
||||
dataReceivers,
|
||||
buttonConfig,
|
||||
} = req.body;
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!sourceScreenId || !targetScreenId || !dataReceivers) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "필수 필드가 누락되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const query = `
|
||||
INSERT INTO screen_data_transfer (
|
||||
source_screen_id, target_screen_id, source_component_id, source_component_type,
|
||||
data_receivers, button_config, company_code, created_by, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [
|
||||
sourceScreenId,
|
||||
targetScreenId,
|
||||
sourceComponentId,
|
||||
sourceComponentType,
|
||||
JSON.stringify(dataReceivers),
|
||||
JSON.stringify(buttonConfig || {}),
|
||||
companyCode,
|
||||
userId,
|
||||
]);
|
||||
|
||||
logger.info("데이터 전달 설정 생성", {
|
||||
companyCode,
|
||||
userId,
|
||||
id: result.rows[0].id,
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("데이터 전달 설정 생성 실패", error);
|
||||
|
||||
// 유니크 제약조건 위반
|
||||
if (error.code === "23505") {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: "이미 동일한 데이터 전달 설정이 존재합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "데이터 전달 설정 생성 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 전달 설정 수정
|
||||
* PUT /api/screen-data-transfer/:id
|
||||
*/
|
||||
export async function updateScreenDataTransfer(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { dataReceivers, buttonConfig } = req.body;
|
||||
const companyCode = req.user!.companyCode;
|
||||
|
||||
const updates: string[] = [];
|
||||
const values: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (dataReceivers) {
|
||||
updates.push(`data_receivers = $${paramIndex++}`);
|
||||
values.push(JSON.stringify(dataReceivers));
|
||||
}
|
||||
|
||||
if (buttonConfig) {
|
||||
updates.push(`button_config = $${paramIndex++}`);
|
||||
values.push(JSON.stringify(buttonConfig));
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "수정할 내용이 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
updates.push(`updated_at = NOW()`);
|
||||
|
||||
values.push(id, companyCode);
|
||||
|
||||
const query = `
|
||||
UPDATE screen_data_transfer
|
||||
SET ${updates.join(", ")}
|
||||
WHERE id = $${paramIndex++}
|
||||
AND company_code = $${paramIndex++}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, values);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "데이터 전달 설정을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("데이터 전달 설정 수정", { companyCode, id });
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("데이터 전달 설정 수정 실패", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "데이터 전달 설정 수정 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 전달 설정 삭제
|
||||
* DELETE /api/screen-data-transfer/:id
|
||||
*/
|
||||
export async function deleteScreenDataTransfer(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user!.companyCode;
|
||||
|
||||
const query = `
|
||||
DELETE FROM screen_data_transfer
|
||||
WHERE id = $1 AND company_code = $2
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [id, companyCode]);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "데이터 전달 설정을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("데이터 전달 설정 삭제", { companyCode, id });
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "데이터 전달 설정이 삭제되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("데이터 전달 설정 삭제 실패", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "데이터 전달 설정 삭제 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 3. 분할 패널 API
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 분할 패널 설정 조회
|
||||
* GET /api/screen-split-panel/:screenId
|
||||
*/
|
||||
export async function getScreenSplitPanel(req: Request, res: Response) {
|
||||
try {
|
||||
const { screenId } = req.params;
|
||||
const companyCode = req.user!.companyCode;
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
ssp.*,
|
||||
le.parent_screen_id as le_parent_screen_id,
|
||||
le.child_screen_id as le_child_screen_id,
|
||||
le.position as le_position,
|
||||
le.mode as le_mode,
|
||||
le.config as le_config,
|
||||
re.parent_screen_id as re_parent_screen_id,
|
||||
re.child_screen_id as re_child_screen_id,
|
||||
re.position as re_position,
|
||||
re.mode as re_mode,
|
||||
re.config as re_config,
|
||||
sdt.source_screen_id,
|
||||
sdt.target_screen_id,
|
||||
sdt.source_component_id,
|
||||
sdt.source_component_type,
|
||||
sdt.data_receivers,
|
||||
sdt.button_config
|
||||
FROM screen_split_panel ssp
|
||||
LEFT JOIN screen_embedding le ON ssp.left_embedding_id = le.id
|
||||
LEFT JOIN screen_embedding re ON ssp.right_embedding_id = re.id
|
||||
LEFT JOIN screen_data_transfer sdt ON ssp.data_transfer_id = sdt.id
|
||||
WHERE ssp.screen_id = $1
|
||||
AND ssp.company_code = $2
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [screenId, companyCode]);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "분할 패널 설정을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const row = result.rows[0];
|
||||
|
||||
// 데이터 구조화
|
||||
const data = {
|
||||
id: row.id,
|
||||
screenId: row.screen_id,
|
||||
leftEmbeddingId: row.left_embedding_id,
|
||||
rightEmbeddingId: row.right_embedding_id,
|
||||
dataTransferId: row.data_transfer_id,
|
||||
layoutConfig: row.layout_config,
|
||||
companyCode: row.company_code,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
leftEmbedding: row.le_child_screen_id
|
||||
? {
|
||||
id: row.left_embedding_id,
|
||||
parentScreenId: row.le_parent_screen_id,
|
||||
childScreenId: row.le_child_screen_id,
|
||||
position: row.le_position,
|
||||
mode: row.le_mode,
|
||||
config: row.le_config,
|
||||
}
|
||||
: null,
|
||||
rightEmbedding: row.re_child_screen_id
|
||||
? {
|
||||
id: row.right_embedding_id,
|
||||
parentScreenId: row.re_parent_screen_id,
|
||||
childScreenId: row.re_child_screen_id,
|
||||
position: row.re_position,
|
||||
mode: row.re_mode,
|
||||
config: row.re_config,
|
||||
}
|
||||
: null,
|
||||
dataTransfer: row.source_screen_id
|
||||
? {
|
||||
id: row.data_transfer_id,
|
||||
sourceScreenId: row.source_screen_id,
|
||||
targetScreenId: row.target_screen_id,
|
||||
sourceComponentId: row.source_component_id,
|
||||
sourceComponentType: row.source_component_type,
|
||||
dataReceivers: row.data_receivers,
|
||||
buttonConfig: row.button_config,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
logger.info("분할 패널 설정 조회", { companyCode, screenId });
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("분할 패널 설정 조회 실패", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "분할 패널 설정 조회 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 분할 패널 설정 생성
|
||||
* POST /api/screen-split-panel
|
||||
*/
|
||||
export async function createScreenSplitPanel(req: Request, res: Response) {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const {
|
||||
screenId,
|
||||
leftEmbedding,
|
||||
rightEmbedding,
|
||||
dataTransfer,
|
||||
layoutConfig,
|
||||
} = req.body;
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!screenId || !leftEmbedding || !rightEmbedding || !dataTransfer) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "필수 필드가 누락되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1. 좌측 임베딩 생성
|
||||
const leftEmbeddingQuery = `
|
||||
INSERT INTO screen_embedding (
|
||||
parent_screen_id, child_screen_id, position, mode,
|
||||
config, company_code, created_by, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const leftResult = await client.query(leftEmbeddingQuery, [
|
||||
screenId,
|
||||
leftEmbedding.childScreenId,
|
||||
leftEmbedding.position,
|
||||
leftEmbedding.mode,
|
||||
JSON.stringify(leftEmbedding.config || {}),
|
||||
companyCode,
|
||||
userId,
|
||||
]);
|
||||
|
||||
const leftEmbeddingId = leftResult.rows[0].id;
|
||||
|
||||
// 2. 우측 임베딩 생성
|
||||
const rightEmbeddingQuery = `
|
||||
INSERT INTO screen_embedding (
|
||||
parent_screen_id, child_screen_id, position, mode,
|
||||
config, company_code, created_by, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const rightResult = await client.query(rightEmbeddingQuery, [
|
||||
screenId,
|
||||
rightEmbedding.childScreenId,
|
||||
rightEmbedding.position,
|
||||
rightEmbedding.mode,
|
||||
JSON.stringify(rightEmbedding.config || {}),
|
||||
companyCode,
|
||||
userId,
|
||||
]);
|
||||
|
||||
const rightEmbeddingId = rightResult.rows[0].id;
|
||||
|
||||
// 3. 데이터 전달 설정 생성
|
||||
const dataTransferQuery = `
|
||||
INSERT INTO screen_data_transfer (
|
||||
source_screen_id, target_screen_id, source_component_id, source_component_type,
|
||||
data_receivers, button_config, company_code, created_by, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const dataTransferResult = await client.query(dataTransferQuery, [
|
||||
dataTransfer.sourceScreenId,
|
||||
dataTransfer.targetScreenId,
|
||||
dataTransfer.sourceComponentId,
|
||||
dataTransfer.sourceComponentType,
|
||||
JSON.stringify(dataTransfer.dataReceivers),
|
||||
JSON.stringify(dataTransfer.buttonConfig || {}),
|
||||
companyCode,
|
||||
userId,
|
||||
]);
|
||||
|
||||
const dataTransferId = dataTransferResult.rows[0].id;
|
||||
|
||||
// 4. 분할 패널 생성
|
||||
const splitPanelQuery = `
|
||||
INSERT INTO screen_split_panel (
|
||||
screen_id, left_embedding_id, right_embedding_id, data_transfer_id,
|
||||
layout_config, company_code, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const splitPanelResult = await client.query(splitPanelQuery, [
|
||||
screenId,
|
||||
leftEmbeddingId,
|
||||
rightEmbeddingId,
|
||||
dataTransferId,
|
||||
JSON.stringify(layoutConfig || {}),
|
||||
companyCode,
|
||||
]);
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
logger.info("분할 패널 설정 생성", {
|
||||
companyCode,
|
||||
userId,
|
||||
screenId,
|
||||
id: splitPanelResult.rows[0].id,
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
data: splitPanelResult.rows[0],
|
||||
});
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("분할 패널 설정 생성 실패", error);
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "분할 패널 설정 생성 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 분할 패널 설정 수정
|
||||
* PUT /api/screen-split-panel/:id
|
||||
*/
|
||||
export async function updateScreenSplitPanel(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { layoutConfig } = req.body;
|
||||
const companyCode = req.user!.companyCode;
|
||||
|
||||
if (!layoutConfig) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "수정할 내용이 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const query = `
|
||||
UPDATE screen_split_panel
|
||||
SET layout_config = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND company_code = $3
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [
|
||||
JSON.stringify(layoutConfig),
|
||||
id,
|
||||
companyCode,
|
||||
]);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "분할 패널 설정을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("분할 패널 설정 수정", { companyCode, id });
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("분할 패널 설정 수정 실패", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "분할 패널 설정 수정 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 분할 패널 설정 삭제
|
||||
* DELETE /api/screen-split-panel/:id
|
||||
*/
|
||||
export async function deleteScreenSplitPanel(req: Request, res: Response) {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyCode = req.user!.companyCode;
|
||||
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1. 분할 패널 조회
|
||||
const selectQuery = `
|
||||
SELECT left_embedding_id, right_embedding_id, data_transfer_id
|
||||
FROM screen_split_panel
|
||||
WHERE id = $1 AND company_code = $2
|
||||
`;
|
||||
|
||||
const selectResult = await client.query(selectQuery, [id, companyCode]);
|
||||
|
||||
if (selectResult.rowCount === 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "분할 패널 설정을 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const { left_embedding_id, right_embedding_id, data_transfer_id } =
|
||||
selectResult.rows[0];
|
||||
|
||||
// 2. 분할 패널 삭제
|
||||
await client.query(
|
||||
"DELETE FROM screen_split_panel WHERE id = $1 AND company_code = $2",
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
// 3. 관련 임베딩 및 데이터 전달 설정 삭제 (CASCADE로 자동 삭제되지만 명시적으로)
|
||||
if (left_embedding_id) {
|
||||
await client.query(
|
||||
"DELETE FROM screen_embedding WHERE id = $1 AND company_code = $2",
|
||||
[left_embedding_id, companyCode]
|
||||
);
|
||||
}
|
||||
|
||||
if (right_embedding_id) {
|
||||
await client.query(
|
||||
"DELETE FROM screen_embedding WHERE id = $1 AND company_code = $2",
|
||||
[right_embedding_id, companyCode]
|
||||
);
|
||||
}
|
||||
|
||||
if (data_transfer_id) {
|
||||
await client.query(
|
||||
"DELETE FROM screen_data_transfer WHERE id = $1 AND company_code = $2",
|
||||
[data_transfer_id, companyCode]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
logger.info("분할 패널 설정 삭제", { companyCode, id });
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "분할 패널 설정이 삭제되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("분할 패널 설정 삭제 실패", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "분할 패널 설정 삭제 중 오류가 발생했습니다.",
|
||||
error: error.message,
|
||||
});
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -481,6 +481,52 @@ export const deleteColumnMapping = async (req: AuthenticatedRequest, res: Respon
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 테이블+컬럼 기준으로 모든 매핑 삭제
|
||||
*
|
||||
* DELETE /api/categories/column-mapping/:tableName/:columnName
|
||||
*
|
||||
* 메뉴 선택 변경 시 기존 매핑을 모두 삭제하고 새로운 매핑만 추가하기 위해 사용
|
||||
*/
|
||||
export const deleteColumnMappingsByColumn = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { tableName, columnName } = req.params;
|
||||
|
||||
if (!tableName || !columnName) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "tableName과 columnName은 필수입니다",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("테이블+컬럼 기준 매핑 삭제", {
|
||||
tableName,
|
||||
columnName,
|
||||
companyCode,
|
||||
});
|
||||
|
||||
const deletedCount = await tableCategoryValueService.deleteColumnMappingsByColumn(
|
||||
tableName,
|
||||
columnName,
|
||||
companyCode
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `${deletedCount}개의 컬럼 매핑이 삭제되었습니다`,
|
||||
deletedCount,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error(`테이블+컬럼 기준 매핑 삭제 실패: ${error.message}`);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "컬럼 매핑 삭제 중 오류가 발생했습니다",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 2레벨 메뉴 목록 조회
|
||||
*
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
saveFormDataEnhanced,
|
||||
updateFormData,
|
||||
updateFormDataPartial,
|
||||
updateFieldValue,
|
||||
deleteFormData,
|
||||
getFormData,
|
||||
getFormDataList,
|
||||
@@ -23,6 +24,7 @@ router.post("/save", saveFormData); // 기존 버전 (레거시 지원)
|
||||
router.post("/save-enhanced", saveFormDataEnhanced); // 개선된 버전
|
||||
router.put("/:id", updateFormData);
|
||||
router.patch("/:id/partial", updateFormDataPartial); // 부분 업데이트
|
||||
router.put("/update-field", updateFieldValue); // 특정 필드만 업데이트 (다른 테이블 지원)
|
||||
router.delete("/:id", deleteFormData);
|
||||
router.get("/:id", getFormData);
|
||||
|
||||
|
||||
80
backend-node/src/routes/screenEmbeddingRoutes.ts
Normal file
80
backend-node/src/routes/screenEmbeddingRoutes.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 화면 임베딩 및 데이터 전달 시스템 라우트
|
||||
*/
|
||||
|
||||
import express from "express";
|
||||
import {
|
||||
// 화면 임베딩
|
||||
getScreenEmbeddings,
|
||||
getScreenEmbeddingById,
|
||||
createScreenEmbedding,
|
||||
updateScreenEmbedding,
|
||||
deleteScreenEmbedding,
|
||||
// 데이터 전달
|
||||
getScreenDataTransfer,
|
||||
createScreenDataTransfer,
|
||||
updateScreenDataTransfer,
|
||||
deleteScreenDataTransfer,
|
||||
// 분할 패널
|
||||
getScreenSplitPanel,
|
||||
createScreenSplitPanel,
|
||||
updateScreenSplitPanel,
|
||||
deleteScreenSplitPanel,
|
||||
} from "../controllers/screenEmbeddingController";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// ============================================
|
||||
// 화면 임베딩 라우트
|
||||
// ============================================
|
||||
|
||||
// 화면 임베딩 목록 조회
|
||||
router.get("/screen-embedding", authenticateToken, getScreenEmbeddings);
|
||||
|
||||
// 화면 임베딩 상세 조회
|
||||
router.get("/screen-embedding/:id", authenticateToken, getScreenEmbeddingById);
|
||||
|
||||
// 화면 임베딩 생성
|
||||
router.post("/screen-embedding", authenticateToken, createScreenEmbedding);
|
||||
|
||||
// 화면 임베딩 수정
|
||||
router.put("/screen-embedding/:id", authenticateToken, updateScreenEmbedding);
|
||||
|
||||
// 화면 임베딩 삭제
|
||||
router.delete("/screen-embedding/:id", authenticateToken, deleteScreenEmbedding);
|
||||
|
||||
// ============================================
|
||||
// 데이터 전달 라우트
|
||||
// ============================================
|
||||
|
||||
// 데이터 전달 설정 조회
|
||||
router.get("/screen-data-transfer", authenticateToken, getScreenDataTransfer);
|
||||
|
||||
// 데이터 전달 설정 생성
|
||||
router.post("/screen-data-transfer", authenticateToken, createScreenDataTransfer);
|
||||
|
||||
// 데이터 전달 설정 수정
|
||||
router.put("/screen-data-transfer/:id", authenticateToken, updateScreenDataTransfer);
|
||||
|
||||
// 데이터 전달 설정 삭제
|
||||
router.delete("/screen-data-transfer/:id", authenticateToken, deleteScreenDataTransfer);
|
||||
|
||||
// ============================================
|
||||
// 분할 패널 라우트
|
||||
// ============================================
|
||||
|
||||
// 분할 패널 설정 조회
|
||||
router.get("/screen-split-panel/:screenId", authenticateToken, getScreenSplitPanel);
|
||||
|
||||
// 분할 패널 설정 생성
|
||||
router.post("/screen-split-panel", authenticateToken, createScreenSplitPanel);
|
||||
|
||||
// 분할 패널 설정 수정
|
||||
router.put("/screen-split-panel/:id", authenticateToken, updateScreenSplitPanel);
|
||||
|
||||
// 분할 패널 설정 삭제
|
||||
router.delete("/screen-split-panel/:id", authenticateToken, deleteScreenSplitPanel);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
createColumnMapping,
|
||||
getLogicalColumns,
|
||||
deleteColumnMapping,
|
||||
deleteColumnMappingsByColumn,
|
||||
getSecondLevelMenus,
|
||||
} from "../controllers/tableCategoryValueController";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
@@ -57,7 +58,11 @@ router.get("/logical-columns/:tableName/:menuObjid", getLogicalColumns);
|
||||
// 컬럼 매핑 생성/수정
|
||||
router.post("/column-mapping", createColumnMapping);
|
||||
|
||||
// 컬럼 매핑 삭제
|
||||
// 테이블+컬럼 기준 매핑 삭제 (메뉴 선택 변경 시 기존 매핑 모두 삭제용)
|
||||
// 주의: 더 구체적인 라우트가 먼저 와야 함 (3개 세그먼트 > 1개 세그먼트)
|
||||
router.delete("/column-mapping/:tableName/:columnName/all", deleteColumnMappingsByColumn);
|
||||
|
||||
// 컬럼 매핑 삭제 (단일)
|
||||
router.delete("/column-mapping/:mappingId", deleteColumnMapping);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { query, queryOne, transaction } from "../database/db";
|
||||
import { query, queryOne, transaction, getPool } from "../database/db";
|
||||
import { EventTriggerService } from "./eventTriggerService";
|
||||
import { DataflowControlService } from "./dataflowControlService";
|
||||
|
||||
@@ -1635,6 +1635,69 @@ export class DynamicFormService {
|
||||
// 에러를 다시 던지지 않음 - 메인 저장 프로세스에 영향 주지 않기 위해
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 테이블의 특정 필드 값만 업데이트
|
||||
* (다른 테이블의 레코드 업데이트 지원)
|
||||
*/
|
||||
async updateFieldValue(
|
||||
tableName: string,
|
||||
keyField: string,
|
||||
keyValue: any,
|
||||
updateField: string,
|
||||
updateValue: any,
|
||||
companyCode: string,
|
||||
userId: string
|
||||
): Promise<{ affectedRows: number }> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
console.log("🔄 [updateFieldValue] 업데이트 실행:", {
|
||||
tableName,
|
||||
keyField,
|
||||
keyValue,
|
||||
updateField,
|
||||
updateValue,
|
||||
companyCode,
|
||||
});
|
||||
|
||||
// 멀티테넌시: company_code 조건 추가 (최고관리자는 제외)
|
||||
let whereClause = `"${keyField}" = $1`;
|
||||
const params: any[] = [keyValue, updateValue, userId];
|
||||
let paramIndex = 4;
|
||||
|
||||
if (companyCode && companyCode !== "*") {
|
||||
whereClause += ` AND company_code = $${paramIndex}`;
|
||||
params.push(companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const sqlQuery = `
|
||||
UPDATE "${tableName}"
|
||||
SET "${updateField}" = $2,
|
||||
updated_by = $3,
|
||||
updated_at = NOW()
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
console.log("🔍 [updateFieldValue] 쿼리:", sqlQuery);
|
||||
console.log("🔍 [updateFieldValue] 파라미터:", params);
|
||||
|
||||
const result = await client.query(sqlQuery, params);
|
||||
|
||||
console.log("✅ [updateFieldValue] 결과:", {
|
||||
affectedRows: result.rowCount,
|
||||
});
|
||||
|
||||
return { affectedRows: result.rowCount || 0 };
|
||||
} catch (error) {
|
||||
console.error("❌ [updateFieldValue] 오류:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 싱글톤 인스턴스 생성 및 export
|
||||
|
||||
@@ -10,10 +10,6 @@ export interface MenuCopyResult {
|
||||
copiedMenus: number;
|
||||
copiedScreens: number;
|
||||
copiedFlows: number;
|
||||
copiedCategories: number;
|
||||
copiedCodes: number;
|
||||
copiedCategorySettings: number;
|
||||
copiedNumberingRules: number;
|
||||
menuIdMap: Record<number, number>;
|
||||
screenIdMap: Record<number, number>;
|
||||
flowIdMap: Record<number, number>;
|
||||
@@ -129,35 +125,6 @@ interface FlowStepConnection {
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 카테고리
|
||||
*/
|
||||
interface CodeCategory {
|
||||
category_code: string;
|
||||
category_name: string;
|
||||
category_name_eng: string | null;
|
||||
description: string | null;
|
||||
sort_order: number | null;
|
||||
is_active: string;
|
||||
company_code: string;
|
||||
menu_objid: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 정보
|
||||
*/
|
||||
interface CodeInfo {
|
||||
code_category: string;
|
||||
code_value: string;
|
||||
code_name: string;
|
||||
code_name_eng: string | null;
|
||||
description: string | null;
|
||||
sort_order: number | null;
|
||||
is_active: string;
|
||||
company_code: string;
|
||||
menu_objid: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 복사 서비스
|
||||
*/
|
||||
@@ -249,6 +216,24 @@ export class MenuCopyService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 탭 컴포넌트 (tabs 배열 내부의 screenId)
|
||||
if (
|
||||
props?.componentConfig?.tabs &&
|
||||
Array.isArray(props.componentConfig.tabs)
|
||||
) {
|
||||
for (const tab of props.componentConfig.tabs) {
|
||||
if (tab.screenId) {
|
||||
const screenId = tab.screenId;
|
||||
const numId =
|
||||
typeof screenId === "number" ? screenId : parseInt(screenId);
|
||||
if (!isNaN(numId)) {
|
||||
referenced.push(numId);
|
||||
logger.debug(` 📑 탭 컴포넌트에서 화면 참조 발견: ${numId} (탭: ${tab.label || tab.id})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return referenced;
|
||||
@@ -355,127 +340,6 @@ export class MenuCopyService {
|
||||
return flowIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 수집
|
||||
*/
|
||||
private async collectCodes(
|
||||
menuObjids: number[],
|
||||
sourceCompanyCode: string,
|
||||
client: PoolClient
|
||||
): Promise<{ categories: CodeCategory[]; codes: CodeInfo[] }> {
|
||||
logger.info(`📋 코드 수집 시작: ${menuObjids.length}개 메뉴`);
|
||||
|
||||
const categories: CodeCategory[] = [];
|
||||
const codes: CodeInfo[] = [];
|
||||
|
||||
for (const menuObjid of menuObjids) {
|
||||
// 코드 카테고리
|
||||
const catsResult = await client.query<CodeCategory>(
|
||||
`SELECT * FROM code_category
|
||||
WHERE menu_objid = $1 AND company_code = $2`,
|
||||
[menuObjid, sourceCompanyCode]
|
||||
);
|
||||
categories.push(...catsResult.rows);
|
||||
|
||||
// 각 카테고리의 코드 정보
|
||||
for (const cat of catsResult.rows) {
|
||||
const codesResult = await client.query<CodeInfo>(
|
||||
`SELECT * FROM code_info
|
||||
WHERE code_category = $1 AND menu_objid = $2 AND company_code = $3`,
|
||||
[cat.category_code, menuObjid, sourceCompanyCode]
|
||||
);
|
||||
codes.push(...codesResult.rows);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`✅ 코드 수집 완료: 카테고리 ${categories.length}개, 코드 ${codes.length}개`
|
||||
);
|
||||
return { categories, codes };
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 설정 수집
|
||||
*/
|
||||
private async collectCategorySettings(
|
||||
menuObjids: number[],
|
||||
sourceCompanyCode: string,
|
||||
client: PoolClient
|
||||
): Promise<{
|
||||
columnMappings: any[];
|
||||
categoryValues: any[];
|
||||
}> {
|
||||
logger.info(`📂 카테고리 설정 수집 시작: ${menuObjids.length}개 메뉴`);
|
||||
|
||||
const columnMappings: any[] = [];
|
||||
const categoryValues: any[] = [];
|
||||
|
||||
// 카테고리 컬럼 매핑 (메뉴별 + 공통)
|
||||
const mappingsResult = await client.query(
|
||||
`SELECT * FROM category_column_mapping
|
||||
WHERE (menu_objid = ANY($1) OR menu_objid = 0)
|
||||
AND company_code = $2`,
|
||||
[menuObjids, sourceCompanyCode]
|
||||
);
|
||||
columnMappings.push(...mappingsResult.rows);
|
||||
|
||||
// 테이블 컬럼 카테고리 값 (메뉴별 + 공통)
|
||||
const valuesResult = await client.query(
|
||||
`SELECT * FROM table_column_category_values
|
||||
WHERE (menu_objid = ANY($1) OR menu_objid = 0)
|
||||
AND company_code = $2`,
|
||||
[menuObjids, sourceCompanyCode]
|
||||
);
|
||||
categoryValues.push(...valuesResult.rows);
|
||||
|
||||
logger.info(
|
||||
`✅ 카테고리 설정 수집 완료: 컬럼 매핑 ${columnMappings.length}개 (공통 포함), 카테고리 값 ${categoryValues.length}개 (공통 포함)`
|
||||
);
|
||||
return { columnMappings, categoryValues };
|
||||
}
|
||||
|
||||
/**
|
||||
* 채번 규칙 수집
|
||||
*/
|
||||
private async collectNumberingRules(
|
||||
menuObjids: number[],
|
||||
sourceCompanyCode: string,
|
||||
client: PoolClient
|
||||
): Promise<{
|
||||
rules: any[];
|
||||
parts: any[];
|
||||
}> {
|
||||
logger.info(`📋 채번 규칙 수집 시작: ${menuObjids.length}개 메뉴`);
|
||||
|
||||
const rules: any[] = [];
|
||||
const parts: any[] = [];
|
||||
|
||||
for (const menuObjid of menuObjids) {
|
||||
// 채번 규칙
|
||||
const rulesResult = await client.query(
|
||||
`SELECT * FROM numbering_rules
|
||||
WHERE menu_objid = $1 AND company_code = $2`,
|
||||
[menuObjid, sourceCompanyCode]
|
||||
);
|
||||
rules.push(...rulesResult.rows);
|
||||
|
||||
// 각 규칙의 파트
|
||||
for (const rule of rulesResult.rows) {
|
||||
const partsResult = await client.query(
|
||||
`SELECT * FROM numbering_rule_parts
|
||||
WHERE rule_id = $1 AND company_code = $2`,
|
||||
[rule.rule_id, sourceCompanyCode]
|
||||
);
|
||||
parts.push(...partsResult.rows);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`✅ 채번 규칙 수집 완료: 규칙 ${rules.length}개, 파트 ${parts.length}개`
|
||||
);
|
||||
return { rules, parts };
|
||||
}
|
||||
|
||||
/**
|
||||
* 다음 메뉴 objid 생성
|
||||
*/
|
||||
@@ -709,42 +573,8 @@ export class MenuCopyService {
|
||||
]);
|
||||
logger.info(` ✅ 메뉴 권한 삭제 완료`);
|
||||
|
||||
// 5-5. 채번 규칙 파트 삭제
|
||||
await client.query(
|
||||
`DELETE FROM numbering_rule_parts
|
||||
WHERE rule_id IN (
|
||||
SELECT rule_id FROM numbering_rules
|
||||
WHERE menu_objid = ANY($1) AND company_code = $2
|
||||
)`,
|
||||
[existingMenuIds, targetCompanyCode]
|
||||
);
|
||||
logger.info(` ✅ 채번 규칙 파트 삭제 완료`);
|
||||
|
||||
// 5-6. 채번 규칙 삭제
|
||||
await client.query(
|
||||
`DELETE FROM numbering_rules
|
||||
WHERE menu_objid = ANY($1) AND company_code = $2`,
|
||||
[existingMenuIds, targetCompanyCode]
|
||||
);
|
||||
logger.info(` ✅ 채번 규칙 삭제 완료`);
|
||||
|
||||
// 5-7. 테이블 컬럼 카테고리 값 삭제
|
||||
await client.query(
|
||||
`DELETE FROM table_column_category_values
|
||||
WHERE menu_objid = ANY($1) AND company_code = $2`,
|
||||
[existingMenuIds, targetCompanyCode]
|
||||
);
|
||||
logger.info(` ✅ 카테고리 값 삭제 완료`);
|
||||
|
||||
// 5-8. 카테고리 컬럼 매핑 삭제
|
||||
await client.query(
|
||||
`DELETE FROM category_column_mapping
|
||||
WHERE menu_objid = ANY($1) AND company_code = $2`,
|
||||
[existingMenuIds, targetCompanyCode]
|
||||
);
|
||||
logger.info(` ✅ 카테고리 매핑 삭제 완료`);
|
||||
|
||||
// 5-9. 메뉴 삭제 (역순: 하위 메뉴부터)
|
||||
// 5-5. 메뉴 삭제 (역순: 하위 메뉴부터)
|
||||
// 주의: 채번 규칙과 카테고리 설정은 회사마다 고유하므로 삭제하지 않음
|
||||
for (let i = existingMenus.length - 1; i >= 0; i--) {
|
||||
await client.query(`DELETE FROM menu_info WHERE objid = $1`, [
|
||||
existingMenus[i].objid,
|
||||
@@ -801,33 +631,11 @@ export class MenuCopyService {
|
||||
|
||||
const flowIds = await this.collectFlows(screenIds, client);
|
||||
|
||||
const codes = await this.collectCodes(
|
||||
menus.map((m) => m.objid),
|
||||
sourceCompanyCode,
|
||||
client
|
||||
);
|
||||
|
||||
const categorySettings = await this.collectCategorySettings(
|
||||
menus.map((m) => m.objid),
|
||||
sourceCompanyCode,
|
||||
client
|
||||
);
|
||||
|
||||
const numberingRules = await this.collectNumberingRules(
|
||||
menus.map((m) => m.objid),
|
||||
sourceCompanyCode,
|
||||
client
|
||||
);
|
||||
|
||||
logger.info(`
|
||||
📊 수집 완료:
|
||||
- 메뉴: ${menus.length}개
|
||||
- 화면: ${screenIds.size}개
|
||||
- 플로우: ${flowIds.size}개
|
||||
- 코드 카테고리: ${codes.categories.length}개
|
||||
- 코드: ${codes.codes.length}개
|
||||
- 카테고리 설정: 컬럼 매핑 ${categorySettings.columnMappings.length}개, 카테고리 값 ${categorySettings.categoryValues.length}개
|
||||
- 채번 규칙: 규칙 ${numberingRules.rules.length}개, 파트 ${numberingRules.parts.length}개
|
||||
`);
|
||||
|
||||
// === 2단계: 플로우 복사 ===
|
||||
@@ -871,30 +679,6 @@ export class MenuCopyService {
|
||||
client
|
||||
);
|
||||
|
||||
// === 6단계: 코드 복사 ===
|
||||
logger.info("\n📋 [6단계] 코드 복사");
|
||||
await this.copyCodes(codes, menuIdMap, targetCompanyCode, userId, client);
|
||||
|
||||
// === 7단계: 카테고리 설정 복사 ===
|
||||
logger.info("\n📂 [7단계] 카테고리 설정 복사");
|
||||
await this.copyCategorySettings(
|
||||
categorySettings,
|
||||
menuIdMap,
|
||||
targetCompanyCode,
|
||||
userId,
|
||||
client
|
||||
);
|
||||
|
||||
// === 8단계: 채번 규칙 복사 ===
|
||||
logger.info("\n📋 [8단계] 채번 규칙 복사");
|
||||
await this.copyNumberingRules(
|
||||
numberingRules,
|
||||
menuIdMap,
|
||||
targetCompanyCode,
|
||||
userId,
|
||||
client
|
||||
);
|
||||
|
||||
// 커밋
|
||||
await client.query("COMMIT");
|
||||
logger.info("✅ 트랜잭션 커밋 완료");
|
||||
@@ -904,13 +688,6 @@ export class MenuCopyService {
|
||||
copiedMenus: menuIdMap.size,
|
||||
copiedScreens: screenIdMap.size,
|
||||
copiedFlows: flowIdMap.size,
|
||||
copiedCategories: codes.categories.length,
|
||||
copiedCodes: codes.codes.length,
|
||||
copiedCategorySettings:
|
||||
categorySettings.columnMappings.length +
|
||||
categorySettings.categoryValues.length,
|
||||
copiedNumberingRules:
|
||||
numberingRules.rules.length + numberingRules.parts.length,
|
||||
menuIdMap: Object.fromEntries(menuIdMap),
|
||||
screenIdMap: Object.fromEntries(screenIdMap),
|
||||
flowIdMap: Object.fromEntries(flowIdMap),
|
||||
@@ -923,10 +700,8 @@ export class MenuCopyService {
|
||||
- 메뉴: ${result.copiedMenus}개
|
||||
- 화면: ${result.copiedScreens}개
|
||||
- 플로우: ${result.copiedFlows}개
|
||||
- 코드 카테고리: ${result.copiedCategories}개
|
||||
- 코드: ${result.copiedCodes}개
|
||||
- 카테고리 설정: ${result.copiedCategorySettings}개
|
||||
- 채번 규칙: ${result.copiedNumberingRules}개
|
||||
|
||||
⚠️ 주의: 코드, 카테고리 설정, 채번 규칙은 복사되지 않습니다.
|
||||
============================================
|
||||
`);
|
||||
|
||||
@@ -1125,13 +900,31 @@ export class MenuCopyService {
|
||||
|
||||
const screenDef = screenDefResult.rows[0];
|
||||
|
||||
// 2) 새 screen_code 생성
|
||||
// 2) 중복 체크: 같은 screen_code가 대상 회사에 이미 있는지 확인
|
||||
const existingScreenResult = await client.query<{ screen_id: number }>(
|
||||
`SELECT screen_id FROM screen_definitions
|
||||
WHERE screen_code = $1 AND company_code = $2 AND deleted_date IS NULL
|
||||
LIMIT 1`,
|
||||
[screenDef.screen_code, targetCompanyCode]
|
||||
);
|
||||
|
||||
if (existingScreenResult.rows.length > 0) {
|
||||
// 이미 존재하는 화면 - 복사하지 않고 기존 ID 매핑
|
||||
const existingScreenId = existingScreenResult.rows[0].screen_id;
|
||||
screenIdMap.set(originalScreenId, existingScreenId);
|
||||
logger.info(
|
||||
` ⏭️ 화면 이미 존재 (스킵): ${originalScreenId} → ${existingScreenId} (${screenDef.screen_code})`
|
||||
);
|
||||
continue; // 레이아웃 복사도 스킵
|
||||
}
|
||||
|
||||
// 3) 새 screen_code 생성
|
||||
const newScreenCode = await this.generateUniqueScreenCode(
|
||||
targetCompanyCode,
|
||||
client
|
||||
);
|
||||
|
||||
// 2-1) 화면명 변환 적용
|
||||
// 4) 화면명 변환 적용
|
||||
let transformedScreenName = screenDef.screen_name;
|
||||
if (screenNameConfig) {
|
||||
// 1. 제거할 텍스트 제거
|
||||
@@ -1150,7 +943,7 @@ export class MenuCopyService {
|
||||
}
|
||||
}
|
||||
|
||||
// 3) screen_definitions 복사 (deleted 필드는 NULL로 설정, 삭제된 화면도 활성화)
|
||||
// 5) screen_definitions 복사 (deleted 필드는 NULL로 설정, 삭제된 화면도 활성화)
|
||||
const newScreenResult = await client.query<{ screen_id: number }>(
|
||||
`INSERT INTO screen_definitions (
|
||||
screen_name, screen_code, table_name, company_code,
|
||||
@@ -1479,383 +1272,4 @@ export class MenuCopyService {
|
||||
logger.info(`✅ 화면-메뉴 할당 완료: ${assignmentCount}개`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 카테고리 중복 체크
|
||||
*/
|
||||
private async checkCodeCategoryExists(
|
||||
categoryCode: string,
|
||||
companyCode: string,
|
||||
menuObjid: number,
|
||||
client: PoolClient
|
||||
): Promise<boolean> {
|
||||
const result = await client.query<{ exists: boolean }>(
|
||||
`SELECT EXISTS(
|
||||
SELECT 1 FROM code_category
|
||||
WHERE category_code = $1 AND company_code = $2 AND menu_objid = $3
|
||||
) as exists`,
|
||||
[categoryCode, companyCode, menuObjid]
|
||||
);
|
||||
return result.rows[0].exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 정보 중복 체크
|
||||
*/
|
||||
private async checkCodeInfoExists(
|
||||
categoryCode: string,
|
||||
codeValue: string,
|
||||
companyCode: string,
|
||||
menuObjid: number,
|
||||
client: PoolClient
|
||||
): Promise<boolean> {
|
||||
const result = await client.query<{ exists: boolean }>(
|
||||
`SELECT EXISTS(
|
||||
SELECT 1 FROM code_info
|
||||
WHERE code_category = $1 AND code_value = $2
|
||||
AND company_code = $3 AND menu_objid = $4
|
||||
) as exists`,
|
||||
[categoryCode, codeValue, companyCode, menuObjid]
|
||||
);
|
||||
return result.rows[0].exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 복사
|
||||
*/
|
||||
private async copyCodes(
|
||||
codes: { categories: CodeCategory[]; codes: CodeInfo[] },
|
||||
menuIdMap: Map<number, number>,
|
||||
targetCompanyCode: string,
|
||||
userId: string,
|
||||
client: PoolClient
|
||||
): Promise<void> {
|
||||
logger.info(`📋 코드 복사 중...`);
|
||||
|
||||
let categoryCount = 0;
|
||||
let codeCount = 0;
|
||||
let skippedCategories = 0;
|
||||
let skippedCodes = 0;
|
||||
|
||||
// 1) 코드 카테고리 복사 (중복 체크)
|
||||
for (const category of codes.categories) {
|
||||
const newMenuObjid = menuIdMap.get(category.menu_objid);
|
||||
if (!newMenuObjid) continue;
|
||||
|
||||
// 중복 체크
|
||||
const exists = await this.checkCodeCategoryExists(
|
||||
category.category_code,
|
||||
targetCompanyCode,
|
||||
newMenuObjid,
|
||||
client
|
||||
);
|
||||
|
||||
if (exists) {
|
||||
skippedCategories++;
|
||||
logger.debug(
|
||||
` ⏭️ 카테고리 이미 존재: ${category.category_code} (menu_objid=${newMenuObjid})`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 카테고리 복사
|
||||
await client.query(
|
||||
`INSERT INTO code_category (
|
||||
category_code, category_name, category_name_eng, description,
|
||||
sort_order, is_active, company_code, menu_objid, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[
|
||||
category.category_code,
|
||||
category.category_name,
|
||||
category.category_name_eng,
|
||||
category.description,
|
||||
category.sort_order,
|
||||
category.is_active,
|
||||
targetCompanyCode, // 새 회사 코드
|
||||
newMenuObjid, // 재매핑
|
||||
userId,
|
||||
]
|
||||
);
|
||||
|
||||
categoryCount++;
|
||||
}
|
||||
|
||||
// 2) 코드 정보 복사 (중복 체크)
|
||||
for (const code of codes.codes) {
|
||||
const newMenuObjid = menuIdMap.get(code.menu_objid);
|
||||
if (!newMenuObjid) continue;
|
||||
|
||||
// 중복 체크
|
||||
const exists = await this.checkCodeInfoExists(
|
||||
code.code_category,
|
||||
code.code_value,
|
||||
targetCompanyCode,
|
||||
newMenuObjid,
|
||||
client
|
||||
);
|
||||
|
||||
if (exists) {
|
||||
skippedCodes++;
|
||||
logger.debug(
|
||||
` ⏭️ 코드 이미 존재: ${code.code_category}.${code.code_value} (menu_objid=${newMenuObjid})`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 코드 복사
|
||||
await client.query(
|
||||
`INSERT INTO code_info (
|
||||
code_category, code_value, code_name, code_name_eng, description,
|
||||
sort_order, is_active, company_code, menu_objid, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||
[
|
||||
code.code_category,
|
||||
code.code_value,
|
||||
code.code_name,
|
||||
code.code_name_eng,
|
||||
code.description,
|
||||
code.sort_order,
|
||||
code.is_active,
|
||||
targetCompanyCode, // 새 회사 코드
|
||||
newMenuObjid, // 재매핑
|
||||
userId,
|
||||
]
|
||||
);
|
||||
|
||||
codeCount++;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`✅ 코드 복사 완료: 카테고리 ${categoryCount}개 (${skippedCategories}개 스킵), 코드 ${codeCount}개 (${skippedCodes}개 스킵)`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 설정 복사
|
||||
*/
|
||||
private async copyCategorySettings(
|
||||
settings: { columnMappings: any[]; categoryValues: any[] },
|
||||
menuIdMap: Map<number, number>,
|
||||
targetCompanyCode: string,
|
||||
userId: string,
|
||||
client: PoolClient
|
||||
): Promise<void> {
|
||||
logger.info(`📂 카테고리 설정 복사 중...`);
|
||||
|
||||
const valueIdMap = new Map<number, number>(); // 원본 value_id → 새 value_id
|
||||
let mappingCount = 0;
|
||||
let valueCount = 0;
|
||||
|
||||
// 1) 카테고리 컬럼 매핑 복사 (덮어쓰기 모드)
|
||||
for (const mapping of settings.columnMappings) {
|
||||
// menu_objid = 0인 공통 설정은 그대로 0으로 유지
|
||||
let newMenuObjid: number | undefined;
|
||||
|
||||
if (
|
||||
mapping.menu_objid === 0 ||
|
||||
mapping.menu_objid === "0" ||
|
||||
mapping.menu_objid == 0
|
||||
) {
|
||||
newMenuObjid = 0; // 공통 설정
|
||||
} else {
|
||||
newMenuObjid = menuIdMap.get(mapping.menu_objid);
|
||||
if (newMenuObjid === undefined) {
|
||||
logger.debug(
|
||||
` ⏭️ 매핑할 메뉴가 없음: menu_objid=${mapping.menu_objid}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 기존 매핑 삭제 (덮어쓰기)
|
||||
await client.query(
|
||||
`DELETE FROM category_column_mapping
|
||||
WHERE table_name = $1 AND physical_column_name = $2 AND company_code = $3`,
|
||||
[mapping.table_name, mapping.physical_column_name, targetCompanyCode]
|
||||
);
|
||||
|
||||
// 새 매핑 추가
|
||||
await client.query(
|
||||
`INSERT INTO category_column_mapping (
|
||||
table_name, logical_column_name, physical_column_name,
|
||||
menu_objid, company_code, description, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
mapping.table_name,
|
||||
mapping.logical_column_name,
|
||||
mapping.physical_column_name,
|
||||
newMenuObjid,
|
||||
targetCompanyCode,
|
||||
mapping.description,
|
||||
userId,
|
||||
]
|
||||
);
|
||||
|
||||
mappingCount++;
|
||||
}
|
||||
|
||||
// 2) 테이블 컬럼 카테고리 값 복사 (덮어쓰기 모드, 부모-자식 관계 유지)
|
||||
const sortedValues = settings.categoryValues.sort(
|
||||
(a, b) => a.depth - b.depth
|
||||
);
|
||||
|
||||
// 먼저 기존 값들을 모두 삭제 (테이블+컬럼 단위)
|
||||
const uniqueTableColumns = new Set<string>();
|
||||
for (const value of sortedValues) {
|
||||
uniqueTableColumns.add(`${value.table_name}:${value.column_name}`);
|
||||
}
|
||||
|
||||
for (const tableColumn of uniqueTableColumns) {
|
||||
const [tableName, columnName] = tableColumn.split(":");
|
||||
await client.query(
|
||||
`DELETE FROM table_column_category_values
|
||||
WHERE table_name = $1 AND column_name = $2 AND company_code = $3`,
|
||||
[tableName, columnName, targetCompanyCode]
|
||||
);
|
||||
logger.debug(` 🗑️ 기존 카테고리 값 삭제: ${tableName}.${columnName}`);
|
||||
}
|
||||
|
||||
// 새 값 추가
|
||||
for (const value of sortedValues) {
|
||||
// menu_objid = 0인 공통 설정은 그대로 0으로 유지
|
||||
let newMenuObjid: number | undefined;
|
||||
|
||||
if (
|
||||
value.menu_objid === 0 ||
|
||||
value.menu_objid === "0" ||
|
||||
value.menu_objid == 0
|
||||
) {
|
||||
newMenuObjid = 0; // 공통 설정
|
||||
} else {
|
||||
newMenuObjid = menuIdMap.get(value.menu_objid);
|
||||
if (newMenuObjid === undefined) {
|
||||
logger.debug(
|
||||
` ⏭️ 매핑할 메뉴가 없음: menu_objid=${value.menu_objid}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 부모 ID 재매핑
|
||||
let newParentValueId = null;
|
||||
if (value.parent_value_id) {
|
||||
newParentValueId = valueIdMap.get(value.parent_value_id) || null;
|
||||
}
|
||||
|
||||
const result = await client.query(
|
||||
`INSERT INTO table_column_category_values (
|
||||
table_name, column_name, value_code, value_label,
|
||||
value_order, parent_value_id, depth, description,
|
||||
color, icon, is_active, is_default,
|
||||
company_code, menu_objid, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
RETURNING value_id`,
|
||||
[
|
||||
value.table_name,
|
||||
value.column_name,
|
||||
value.value_code,
|
||||
value.value_label,
|
||||
value.value_order,
|
||||
newParentValueId,
|
||||
value.depth,
|
||||
value.description,
|
||||
value.color,
|
||||
value.icon,
|
||||
value.is_active,
|
||||
value.is_default,
|
||||
targetCompanyCode,
|
||||
newMenuObjid,
|
||||
userId,
|
||||
]
|
||||
);
|
||||
|
||||
// ID 매핑 저장
|
||||
const newValueId = result.rows[0].value_id;
|
||||
valueIdMap.set(value.value_id, newValueId);
|
||||
|
||||
valueCount++;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`✅ 카테고리 설정 복사 완료: 컬럼 매핑 ${mappingCount}개, 카테고리 값 ${valueCount}개 (덮어쓰기)`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 채번 규칙 복사
|
||||
*/
|
||||
private async copyNumberingRules(
|
||||
rules: { rules: any[]; parts: any[] },
|
||||
menuIdMap: Map<number, number>,
|
||||
targetCompanyCode: string,
|
||||
userId: string,
|
||||
client: PoolClient
|
||||
): Promise<void> {
|
||||
logger.info(`📋 채번 규칙 복사 중...`);
|
||||
|
||||
const ruleIdMap = new Map<string, string>(); // 원본 rule_id → 새 rule_id
|
||||
let ruleCount = 0;
|
||||
let partCount = 0;
|
||||
|
||||
// 1) 채번 규칙 복사
|
||||
for (const rule of rules.rules) {
|
||||
const newMenuObjid = menuIdMap.get(rule.menu_objid);
|
||||
if (!newMenuObjid) continue;
|
||||
|
||||
// 새 rule_id 생성 (타임스탬프 기반)
|
||||
const newRuleId = `rule-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
ruleIdMap.set(rule.rule_id, newRuleId);
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO numbering_rules (
|
||||
rule_id, rule_name, description, separator,
|
||||
reset_period, current_sequence, table_name, column_name,
|
||||
company_code, menu_objid, created_by, scope_type
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
|
||||
[
|
||||
newRuleId,
|
||||
rule.rule_name,
|
||||
rule.description,
|
||||
rule.separator,
|
||||
rule.reset_period,
|
||||
1, // 시퀀스 초기화
|
||||
rule.table_name,
|
||||
rule.column_name,
|
||||
targetCompanyCode,
|
||||
newMenuObjid,
|
||||
userId,
|
||||
rule.scope_type,
|
||||
]
|
||||
);
|
||||
|
||||
ruleCount++;
|
||||
}
|
||||
|
||||
// 2) 채번 규칙 파트 복사
|
||||
for (const part of rules.parts) {
|
||||
const newRuleId = ruleIdMap.get(part.rule_id);
|
||||
if (!newRuleId) continue;
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO numbering_rule_parts (
|
||||
rule_id, part_order, part_type, generation_method,
|
||||
auto_config, manual_config, company_code
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
newRuleId,
|
||||
part.part_order,
|
||||
part.part_type,
|
||||
part.generation_method,
|
||||
part.auto_config,
|
||||
part.manual_config,
|
||||
targetCompanyCode,
|
||||
]
|
||||
);
|
||||
|
||||
partCount++;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`✅ 채번 규칙 복사 완료: 규칙 ${ruleCount}개, 파트 ${partCount}개`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,72 @@ export async function getSiblingMenuObjids(menuObjid: number): Promise<number[]>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택한 메뉴와 그 하위 메뉴들의 OBJID 조회
|
||||
*
|
||||
* 형제 메뉴는 포함하지 않고, 선택한 메뉴와 그 자식 메뉴들만 반환합니다.
|
||||
* 채번 규칙 필터링 등 특정 메뉴 계층만 필요할 때 사용합니다.
|
||||
*
|
||||
* @param menuObjid 메뉴 OBJID
|
||||
* @returns 선택한 메뉴 + 모든 하위 메뉴 OBJID 배열 (재귀적)
|
||||
*
|
||||
* @example
|
||||
* // 메뉴 구조:
|
||||
* // └── 구매관리 (100)
|
||||
* // ├── 공급업체관리 (101)
|
||||
* // ├── 발주관리 (102)
|
||||
* // └── 입고관리 (103)
|
||||
* // └── 입고상세 (104)
|
||||
*
|
||||
* await getMenuAndChildObjids(100);
|
||||
* // 결과: [100, 101, 102, 103, 104]
|
||||
*/
|
||||
export async function getMenuAndChildObjids(menuObjid: number): Promise<number[]> {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
logger.debug("메뉴 및 하위 메뉴 조회 시작", { menuObjid });
|
||||
|
||||
// 재귀 CTE를 사용하여 선택한 메뉴와 모든 하위 메뉴 조회
|
||||
const query = `
|
||||
WITH RECURSIVE menu_tree AS (
|
||||
-- 시작점: 선택한 메뉴
|
||||
SELECT objid, parent_obj_id, 1 AS depth
|
||||
FROM menu_info
|
||||
WHERE objid = $1
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- 재귀: 하위 메뉴들
|
||||
SELECT m.objid, m.parent_obj_id, mt.depth + 1
|
||||
FROM menu_info m
|
||||
INNER JOIN menu_tree mt ON m.parent_obj_id = mt.objid
|
||||
WHERE mt.depth < 10 -- 무한 루프 방지
|
||||
)
|
||||
SELECT objid FROM menu_tree ORDER BY depth, objid
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [menuObjid]);
|
||||
const objids = result.rows.map((row) => Number(row.objid));
|
||||
|
||||
logger.debug("메뉴 및 하위 메뉴 조회 완료", {
|
||||
menuObjid,
|
||||
totalCount: objids.length,
|
||||
objids
|
||||
});
|
||||
|
||||
return objids;
|
||||
} catch (error: any) {
|
||||
logger.error("메뉴 및 하위 메뉴 조회 실패", {
|
||||
menuObjid,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
// 에러 발생 시 안전하게 자기 자신만 반환
|
||||
return [menuObjid];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 여러 메뉴의 형제 메뉴 OBJID 합집합 조회
|
||||
*
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
import { getSiblingMenuObjids } from "./menuService";
|
||||
import { getMenuAndChildObjids } from "./menuService";
|
||||
|
||||
interface NumberingRulePart {
|
||||
id?: number;
|
||||
@@ -161,7 +161,7 @@ class NumberingRuleService {
|
||||
companyCode: string,
|
||||
menuObjid?: number
|
||||
): Promise<NumberingRuleConfig[]> {
|
||||
let siblingObjids: number[] = []; // catch 블록에서 접근 가능하도록 함수 최상단에 선언
|
||||
let menuAndChildObjids: number[] = []; // catch 블록에서 접근 가능하도록 함수 최상단에 선언
|
||||
|
||||
try {
|
||||
logger.info("메뉴별 사용 가능한 채번 규칙 조회 시작 (메뉴 스코프)", {
|
||||
@@ -171,14 +171,14 @@ class NumberingRuleService {
|
||||
|
||||
const pool = getPool();
|
||||
|
||||
// 1. 형제 메뉴 OBJID 조회
|
||||
// 1. 선택한 메뉴와 하위 메뉴 OBJID 조회 (형제 메뉴 제외)
|
||||
if (menuObjid) {
|
||||
siblingObjids = await getSiblingMenuObjids(menuObjid);
|
||||
logger.info("형제 메뉴 OBJID 목록", { menuObjid, siblingObjids });
|
||||
menuAndChildObjids = await getMenuAndChildObjids(menuObjid);
|
||||
logger.info("선택한 메뉴 및 하위 메뉴 OBJID 목록", { menuObjid, menuAndChildObjids });
|
||||
}
|
||||
|
||||
// menuObjid가 없으면 global 규칙만 반환
|
||||
if (!menuObjid || siblingObjids.length === 0) {
|
||||
if (!menuObjid || menuAndChildObjids.length === 0) {
|
||||
let query: string;
|
||||
let params: any[];
|
||||
|
||||
@@ -280,7 +280,7 @@ class NumberingRuleService {
|
||||
let params: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 모든 규칙 조회 (형제 메뉴 포함)
|
||||
// 최고 관리자: 모든 규칙 조회 (선택한 메뉴 + 하위 메뉴)
|
||||
query = `
|
||||
SELECT
|
||||
rule_id AS "ruleId",
|
||||
@@ -301,8 +301,7 @@ class NumberingRuleService {
|
||||
WHERE
|
||||
scope_type = 'global'
|
||||
OR (scope_type = 'menu' AND menu_objid = ANY($1))
|
||||
OR (scope_type = 'table' AND menu_objid = ANY($1)) -- ✅ 메뉴별로 필터링
|
||||
OR (scope_type = 'table' AND menu_objid IS NULL) -- ✅ 기존 규칙(menu_objid NULL) 포함 (하위 호환성)
|
||||
OR (scope_type = 'table' AND menu_objid = ANY($1))
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN scope_type = 'menu' OR (scope_type = 'table' AND menu_objid = ANY($1)) THEN 1
|
||||
@@ -311,10 +310,10 @@ class NumberingRuleService {
|
||||
END,
|
||||
created_at DESC
|
||||
`;
|
||||
params = [siblingObjids];
|
||||
logger.info("최고 관리자: 형제 메뉴 기반 채번 규칙 조회 (메뉴별 필터링)", { siblingObjids });
|
||||
params = [menuAndChildObjids];
|
||||
logger.info("최고 관리자: 메뉴 및 하위 메뉴 기반 채번 규칙 조회", { menuAndChildObjids });
|
||||
} else {
|
||||
// 일반 회사: 자신의 규칙만 조회 (형제 메뉴 포함, 메뉴별 필터링)
|
||||
// 일반 회사: 자신의 규칙만 조회 (선택한 메뉴 + 하위 메뉴)
|
||||
query = `
|
||||
SELECT
|
||||
rule_id AS "ruleId",
|
||||
@@ -336,8 +335,7 @@ class NumberingRuleService {
|
||||
AND (
|
||||
scope_type = 'global'
|
||||
OR (scope_type = 'menu' AND menu_objid = ANY($2))
|
||||
OR (scope_type = 'table' AND menu_objid = ANY($2)) -- ✅ 메뉴별로 필터링
|
||||
OR (scope_type = 'table' AND menu_objid IS NULL) -- ✅ 기존 규칙(menu_objid NULL) 포함 (하위 호환성)
|
||||
OR (scope_type = 'table' AND menu_objid = ANY($2))
|
||||
)
|
||||
ORDER BY
|
||||
CASE
|
||||
@@ -347,8 +345,8 @@ class NumberingRuleService {
|
||||
END,
|
||||
created_at DESC
|
||||
`;
|
||||
params = [companyCode, siblingObjids];
|
||||
logger.info("회사별: 형제 메뉴 기반 채번 규칙 조회 (메뉴별 필터링)", { companyCode, siblingObjids });
|
||||
params = [companyCode, menuAndChildObjids];
|
||||
logger.info("회사별: 메뉴 및 하위 메뉴 기반 채번 규칙 조회", { companyCode, menuAndChildObjids });
|
||||
}
|
||||
|
||||
logger.info("🔍 채번 규칙 쿼리 실행", {
|
||||
@@ -420,7 +418,7 @@ class NumberingRuleService {
|
||||
logger.info("메뉴별 사용 가능한 채번 규칙 조회 완료", {
|
||||
companyCode,
|
||||
menuObjid,
|
||||
siblingCount: siblingObjids.length,
|
||||
menuAndChildCount: menuAndChildObjids.length,
|
||||
count: result.rowCount,
|
||||
});
|
||||
|
||||
@@ -432,7 +430,7 @@ class NumberingRuleService {
|
||||
errorStack: error.stack,
|
||||
companyCode,
|
||||
menuObjid,
|
||||
siblingObjids: siblingObjids || [],
|
||||
menuAndChildObjids: menuAndChildObjids || [],
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -1066,6 +1066,66 @@ class TableCategoryValueService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블+컬럼 기준으로 모든 매핑 삭제
|
||||
*
|
||||
* 메뉴 선택 변경 시 기존 매핑을 모두 삭제하고 새로운 매핑만 추가하기 위해 사용
|
||||
*
|
||||
* @param tableName - 테이블명
|
||||
* @param columnName - 컬럼명
|
||||
* @param companyCode - 회사 코드
|
||||
* @returns 삭제된 매핑 수
|
||||
*/
|
||||
async deleteColumnMappingsByColumn(
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
companyCode: string
|
||||
): Promise<number> {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
logger.info("테이블+컬럼 기준 매핑 삭제", { tableName, columnName, companyCode });
|
||||
|
||||
// 멀티테넌시 적용
|
||||
let deleteQuery: string;
|
||||
let deleteParams: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 해당 테이블+컬럼의 모든 매핑 삭제
|
||||
deleteQuery = `
|
||||
DELETE FROM category_column_mapping
|
||||
WHERE table_name = $1
|
||||
AND logical_column_name = $2
|
||||
`;
|
||||
deleteParams = [tableName, columnName];
|
||||
} else {
|
||||
// 일반 회사: 자신의 매핑만 삭제
|
||||
deleteQuery = `
|
||||
DELETE FROM category_column_mapping
|
||||
WHERE table_name = $1
|
||||
AND logical_column_name = $2
|
||||
AND company_code = $3
|
||||
`;
|
||||
deleteParams = [tableName, columnName, companyCode];
|
||||
}
|
||||
|
||||
const result = await pool.query(deleteQuery, deleteParams);
|
||||
const deletedCount = result.rowCount || 0;
|
||||
|
||||
logger.info("테이블+컬럼 기준 매핑 삭제 완료", {
|
||||
tableName,
|
||||
columnName,
|
||||
companyCode,
|
||||
deletedCount
|
||||
});
|
||||
|
||||
return deletedCount;
|
||||
} catch (error: any) {
|
||||
logger.error(`테이블+컬럼 기준 매핑 삭제 실패: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 논리적 컬럼명을 물리적 컬럼명으로 변환
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user