Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map
This commit is contained in:
118
backend-node/src/controllers/entitySearchController.ts
Normal file
118
backend-node/src/controllers/entitySearchController.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Request, Response } from "express";
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* 엔티티 검색 API
|
||||
* GET /api/entity-search/:tableName
|
||||
*/
|
||||
export async function searchEntity(req: Request, res: Response) {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
const {
|
||||
searchText = "",
|
||||
searchFields = "",
|
||||
filterCondition = "{}",
|
||||
page = "1",
|
||||
limit = "20",
|
||||
} = req.query;
|
||||
|
||||
// tableName 유효성 검증
|
||||
if (!tableName || tableName === "undefined" || tableName === "null") {
|
||||
logger.warn("엔티티 검색 실패: 테이블명이 없음", { tableName });
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "테이블명이 지정되지 않았습니다. 컴포넌트 설정에서 sourceTable을 확인해주세요.",
|
||||
});
|
||||
}
|
||||
|
||||
// 멀티테넌시
|
||||
const companyCode = req.user!.companyCode;
|
||||
|
||||
// 검색 필드 파싱
|
||||
const fields = searchFields
|
||||
? (searchFields as string).split(",").map((f) => f.trim())
|
||||
: [];
|
||||
|
||||
// WHERE 조건 생성
|
||||
const whereConditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 멀티테넌시 필터링
|
||||
if (companyCode !== "*") {
|
||||
whereConditions.push(`company_code = $${paramIndex}`);
|
||||
params.push(companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 검색 조건
|
||||
if (searchText && fields.length > 0) {
|
||||
const searchConditions = fields.map((field) => {
|
||||
const condition = `${field}::text ILIKE $${paramIndex}`;
|
||||
paramIndex++;
|
||||
return condition;
|
||||
});
|
||||
whereConditions.push(`(${searchConditions.join(" OR ")})`);
|
||||
|
||||
// 검색어 파라미터 추가
|
||||
fields.forEach(() => {
|
||||
params.push(`%${searchText}%`);
|
||||
});
|
||||
}
|
||||
|
||||
// 추가 필터 조건
|
||||
const additionalFilter = JSON.parse(filterCondition as string);
|
||||
for (const [key, value] of Object.entries(additionalFilter)) {
|
||||
whereConditions.push(`${key} = $${paramIndex}`);
|
||||
params.push(value);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 페이징
|
||||
const offset = (parseInt(page as string) - 1) * parseInt(limit as string);
|
||||
const whereClause =
|
||||
whereConditions.length > 0
|
||||
? `WHERE ${whereConditions.join(" AND ")}`
|
||||
: "";
|
||||
|
||||
// 쿼리 실행
|
||||
const pool = getPool();
|
||||
const countQuery = `SELECT COUNT(*) FROM ${tableName} ${whereClause}`;
|
||||
const dataQuery = `
|
||||
SELECT * FROM ${tableName} ${whereClause}
|
||||
ORDER BY id DESC
|
||||
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||
`;
|
||||
|
||||
params.push(parseInt(limit as string));
|
||||
params.push(offset);
|
||||
|
||||
const countResult = await pool.query(
|
||||
countQuery,
|
||||
params.slice(0, params.length - 2)
|
||||
);
|
||||
const dataResult = await pool.query(dataQuery, params);
|
||||
|
||||
logger.info("엔티티 검색 성공", {
|
||||
tableName,
|
||||
searchText,
|
||||
companyCode,
|
||||
rowCount: dataResult.rowCount,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: dataResult.rows,
|
||||
pagination: {
|
||||
total: parseInt(countResult.rows[0].count),
|
||||
page: parseInt(page as string),
|
||||
limit: parseInt(limit as string),
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("엔티티 검색 오류", { error: error.message, stack: error.stack });
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
238
backend-node/src/controllers/orderController.ts
Normal file
238
backend-node/src/controllers/orderController.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { Request, Response } from "express";
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* 수주 번호 생성 함수
|
||||
* 형식: ORD + YYMMDD + 4자리 시퀀스
|
||||
* 예: ORD250114001
|
||||
*/
|
||||
async function generateOrderNumber(companyCode: string): Promise<string> {
|
||||
const pool = getPool();
|
||||
const today = new Date();
|
||||
const year = today.getFullYear().toString().slice(2); // 25
|
||||
const month = String(today.getMonth() + 1).padStart(2, "0"); // 01
|
||||
const day = String(today.getDate()).padStart(2, "0"); // 14
|
||||
const dateStr = `${year}${month}${day}`; // 250114
|
||||
|
||||
// 당일 수주 카운트 조회
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM order_mng_master
|
||||
WHERE objid LIKE $1
|
||||
AND writer LIKE $2
|
||||
`;
|
||||
|
||||
const pattern = `ORD${dateStr}%`;
|
||||
const result = await pool.query(countQuery, [pattern, `%${companyCode}%`]);
|
||||
const count = parseInt(result.rows[0]?.count || "0");
|
||||
const seq = count + 1;
|
||||
|
||||
return `ORD${dateStr}${String(seq).padStart(4, "0")}`; // ORD250114001
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 등록 API
|
||||
* POST /api/orders
|
||||
*/
|
||||
export async function createOrder(req: Request, res: Response) {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
const {
|
||||
inputMode, // 입력 방식
|
||||
customerCode, // 거래처 코드
|
||||
deliveryDate, // 납품일
|
||||
items, // 품목 목록
|
||||
memo, // 메모
|
||||
} = req.body;
|
||||
|
||||
// 멀티테넌시
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
// 유효성 검사
|
||||
if (!customerCode) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "거래처 코드는 필수입니다",
|
||||
});
|
||||
}
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "품목은 최소 1개 이상 필요합니다",
|
||||
});
|
||||
}
|
||||
|
||||
// 수주 번호 생성
|
||||
const orderNo = await generateOrderNumber(companyCode);
|
||||
|
||||
// 전체 금액 계산
|
||||
const totalAmount = items.reduce(
|
||||
(sum: number, item: any) => sum + (item.amount || 0),
|
||||
0
|
||||
);
|
||||
|
||||
// 수주 마스터 생성
|
||||
const masterQuery = `
|
||||
INSERT INTO order_mng_master (
|
||||
objid,
|
||||
partner_objid,
|
||||
final_delivery_date,
|
||||
reason,
|
||||
status,
|
||||
reg_date,
|
||||
writer
|
||||
) VALUES ($1, $2, $3, $4, $5, NOW(), $6)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const masterResult = await pool.query(masterQuery, [
|
||||
orderNo,
|
||||
customerCode,
|
||||
deliveryDate || null,
|
||||
memo || null,
|
||||
"진행중",
|
||||
`${userId}|${companyCode}`,
|
||||
]);
|
||||
|
||||
const masterObjid = masterResult.rows[0].objid;
|
||||
|
||||
// 수주 상세 (품목) 생성
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const subObjid = `${orderNo}_${i + 1}`;
|
||||
|
||||
const subQuery = `
|
||||
INSERT INTO order_mng_sub (
|
||||
objid,
|
||||
order_mng_master_objid,
|
||||
part_objid,
|
||||
partner_objid,
|
||||
partner_price,
|
||||
partner_qty,
|
||||
delivery_date,
|
||||
status,
|
||||
regdate,
|
||||
writer
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), $9)
|
||||
`;
|
||||
|
||||
await pool.query(subQuery, [
|
||||
subObjid,
|
||||
masterObjid,
|
||||
item.item_code || item.id, // 품목 코드
|
||||
customerCode,
|
||||
item.unit_price || 0,
|
||||
item.quantity || 0,
|
||||
item.delivery_date || deliveryDate || null,
|
||||
"진행중",
|
||||
`${userId}|${companyCode}`,
|
||||
]);
|
||||
}
|
||||
|
||||
logger.info("수주 등록 성공", {
|
||||
companyCode,
|
||||
orderNo,
|
||||
masterObjid,
|
||||
itemCount: items.length,
|
||||
totalAmount,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
orderNo,
|
||||
masterObjid,
|
||||
itemCount: items.length,
|
||||
totalAmount,
|
||||
},
|
||||
message: "수주가 등록되었습니다",
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("수주 등록 오류", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "수주 등록 중 오류가 발생했습니다",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 목록 조회 API
|
||||
* GET /api/orders
|
||||
*/
|
||||
export async function getOrders(req: Request, res: Response) {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
const { page = "1", limit = "20", searchText = "" } = req.query;
|
||||
const companyCode = req.user!.companyCode;
|
||||
|
||||
const offset = (parseInt(page as string) - 1) * parseInt(limit as string);
|
||||
|
||||
// WHERE 조건
|
||||
const whereConditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 멀티테넌시 (writer 필드에 company_code 포함)
|
||||
if (companyCode !== "*") {
|
||||
whereConditions.push(`writer LIKE $${paramIndex}`);
|
||||
params.push(`%${companyCode}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 검색
|
||||
if (searchText) {
|
||||
whereConditions.push(`objid LIKE $${paramIndex}`);
|
||||
params.push(`%${searchText}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause =
|
||||
whereConditions.length > 0
|
||||
? `WHERE ${whereConditions.join(" AND ")}`
|
||||
: "";
|
||||
|
||||
// 카운트 쿼리
|
||||
const countQuery = `SELECT COUNT(*) as count FROM order_mng_master ${whereClause}`;
|
||||
const countResult = await pool.query(countQuery, params);
|
||||
const total = parseInt(countResult.rows[0]?.count || "0");
|
||||
|
||||
// 데이터 쿼리
|
||||
const dataQuery = `
|
||||
SELECT * FROM order_mng_master
|
||||
${whereClause}
|
||||
ORDER BY reg_date DESC
|
||||
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||
`;
|
||||
|
||||
params.push(parseInt(limit as string));
|
||||
params.push(offset);
|
||||
|
||||
const dataResult = await pool.query(dataQuery, params);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: dataResult.rows,
|
||||
pagination: {
|
||||
total,
|
||||
page: parseInt(page as string),
|
||||
limit: parseInt(limit as string),
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("수주 목록 조회 오류", { error: error.message });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ export const getScreens = async (req: AuthenticatedRequest, res: Response) => {
|
||||
const result = await screenManagementService.getScreensByCompany(
|
||||
targetCompanyCode,
|
||||
parseInt(page as string),
|
||||
parseInt(size as string)
|
||||
parseInt(size as string),
|
||||
searchTerm as string // 검색어 전달
|
||||
);
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -187,6 +187,16 @@ export const deleteCategoryValue = async (req: AuthenticatedRequest, res: Respon
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error(`카테고리 값 삭제 실패: ${error.message}`);
|
||||
|
||||
// 사용 중인 경우 상세 에러 메시지 반환 (400)
|
||||
if (error.message.includes("삭제할 수 없습니다")) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
// 기타 에러 (500)
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "카테고리 값 삭제 중 오류가 발생했습니다",
|
||||
|
||||
@@ -1604,10 +1604,14 @@ export async function toggleLogTable(
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴의 형제 메뉴들이 사용하는 모든 테이블의 카테고리 타입 컬럼 조회
|
||||
* 메뉴의 상위 메뉴들이 설정한 모든 카테고리 타입 컬럼 조회 (계층 구조 상속)
|
||||
*
|
||||
* @route GET /api/table-management/menu/:menuObjid/category-columns
|
||||
* @description 형제 메뉴들의 화면에서 사용하는 테이블의 input_type='category' 컬럼 조회
|
||||
* @description 현재 메뉴와 상위 메뉴들에서 설정한 category_column_mapping의 모든 카테고리 컬럼 조회
|
||||
*
|
||||
* 예시:
|
||||
* - 2레벨 메뉴 "고객사관리"에서 discount_type, rounding_type 설정
|
||||
* - 3레벨 메뉴 "고객등록", "고객조회" 등에서도 동일하게 보임 (상속)
|
||||
*/
|
||||
export async function getCategoryColumnsByMenu(
|
||||
req: AuthenticatedRequest,
|
||||
@@ -1627,40 +1631,10 @@ export async function getCategoryColumnsByMenu(
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 형제 메뉴 조회
|
||||
const { getSiblingMenuObjids } = await import("../services/menuService");
|
||||
const siblingObjids = await getSiblingMenuObjids(parseInt(menuObjid));
|
||||
|
||||
logger.info("✅ 형제 메뉴 조회 완료", { siblingObjids });
|
||||
|
||||
// 2. 형제 메뉴들이 사용하는 테이블 조회
|
||||
const { getPool } = await import("../database/db");
|
||||
const pool = getPool();
|
||||
|
||||
const tablesQuery = `
|
||||
SELECT DISTINCT sd.table_name
|
||||
FROM screen_menu_assignments sma
|
||||
INNER JOIN screen_definitions sd ON sma.screen_id = sd.screen_id
|
||||
WHERE sma.menu_objid = ANY($1)
|
||||
AND sma.company_code = $2
|
||||
AND sd.table_name IS NOT NULL
|
||||
`;
|
||||
|
||||
const tablesResult = await pool.query(tablesQuery, [siblingObjids, companyCode]);
|
||||
const tableNames = tablesResult.rows.map((row: any) => row.table_name);
|
||||
|
||||
logger.info("✅ 형제 메뉴 테이블 조회 완료", { tableNames, count: tableNames.length });
|
||||
|
||||
if (tableNames.length === 0) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: [],
|
||||
message: "형제 메뉴에 연결된 테이블이 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. category_column_mapping 테이블 존재 여부 확인
|
||||
// 1. category_column_mapping 테이블 존재 여부 확인
|
||||
const tableExistsResult = await pool.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
@@ -1672,33 +1646,42 @@ export async function getCategoryColumnsByMenu(
|
||||
let columnsResult;
|
||||
|
||||
if (mappingTableExists) {
|
||||
// 🆕 category_column_mapping을 사용한 필터링
|
||||
logger.info("🔍 category_column_mapping 기반 카테고리 컬럼 조회", { menuObjid, companyCode });
|
||||
// 🆕 category_column_mapping을 사용한 계층 구조 기반 조회
|
||||
logger.info("🔍 category_column_mapping 기반 카테고리 컬럼 조회 (계층 구조 상속)", { menuObjid, companyCode });
|
||||
|
||||
// 현재 메뉴와 모든 상위 메뉴의 objid 조회 (재귀)
|
||||
const ancestorMenuQuery = `
|
||||
WITH RECURSIVE menu_hierarchy AS (
|
||||
-- 현재 메뉴
|
||||
SELECT objid, parent_obj_id, menu_type
|
||||
SELECT objid, parent_obj_id, menu_type, menu_name_kor
|
||||
FROM menu_info
|
||||
WHERE objid = $1
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- 부모 메뉴 재귀 조회
|
||||
SELECT m.objid, m.parent_obj_id, m.menu_type
|
||||
SELECT m.objid, m.parent_obj_id, m.menu_type, m.menu_name_kor
|
||||
FROM menu_info m
|
||||
INNER JOIN menu_hierarchy mh ON m.objid = mh.parent_obj_id
|
||||
WHERE m.parent_obj_id != 0 -- 최상위 메뉴(parent_obj_id=0) 제외
|
||||
)
|
||||
SELECT ARRAY_AGG(objid) as menu_objids
|
||||
SELECT
|
||||
ARRAY_AGG(objid) as menu_objids,
|
||||
ARRAY_AGG(menu_name_kor) as menu_names
|
||||
FROM menu_hierarchy
|
||||
`;
|
||||
|
||||
const ancestorMenuResult = await pool.query(ancestorMenuQuery, [parseInt(menuObjid)]);
|
||||
const ancestorMenuObjids = ancestorMenuResult.rows[0]?.menu_objids || [parseInt(menuObjid)];
|
||||
const ancestorMenuNames = ancestorMenuResult.rows[0]?.menu_names || [];
|
||||
|
||||
logger.info("✅ 상위 메뉴 계층 조회 완료", {
|
||||
ancestorMenuObjids,
|
||||
ancestorMenuNames,
|
||||
hierarchyDepth: ancestorMenuObjids.length
|
||||
});
|
||||
|
||||
// 상위 메뉴들에 설정된 모든 카테고리 컬럼 조회 (테이블 필터링 제거)
|
||||
const columnsQuery = `
|
||||
SELECT DISTINCT
|
||||
ttc.table_name AS "tableName",
|
||||
@@ -1711,7 +1694,8 @@ export async function getCategoryColumnsByMenu(
|
||||
cl.column_label,
|
||||
initcap(replace(ccm.logical_column_name, '_', ' '))
|
||||
) AS "columnLabel",
|
||||
ttc.input_type AS "inputType"
|
||||
ttc.input_type AS "inputType",
|
||||
ccm.menu_objid AS "definedAtMenuObjid"
|
||||
FROM category_column_mapping ccm
|
||||
INNER JOIN table_type_columns ttc
|
||||
ON ccm.table_name = ttc.table_name
|
||||
@@ -1721,18 +1705,48 @@ export async function getCategoryColumnsByMenu(
|
||||
AND ttc.column_name = cl.column_name
|
||||
LEFT JOIN table_labels tl
|
||||
ON ttc.table_name = tl.table_name
|
||||
WHERE ccm.table_name = ANY($1)
|
||||
AND ccm.company_code = $2
|
||||
AND ccm.menu_objid = ANY($3)
|
||||
WHERE ccm.company_code = $1
|
||||
AND ccm.menu_objid = ANY($2)
|
||||
AND ttc.input_type = 'category'
|
||||
ORDER BY ttc.table_name, ccm.logical_column_name
|
||||
`;
|
||||
|
||||
columnsResult = await pool.query(columnsQuery, [tableNames, companyCode, ancestorMenuObjids]);
|
||||
logger.info("✅ category_column_mapping 기반 조회 완료", { rowCount: columnsResult.rows.length });
|
||||
columnsResult = await pool.query(columnsQuery, [companyCode, ancestorMenuObjids]);
|
||||
logger.info("✅ category_column_mapping 기반 조회 완료 (계층 구조 상속)", {
|
||||
rowCount: columnsResult.rows.length,
|
||||
columns: columnsResult.rows.map((r: any) => `${r.tableName}.${r.columnName}`)
|
||||
});
|
||||
} else {
|
||||
// 🔄 기존 방식: table_type_columns에서 모든 카테고리 컬럼 조회
|
||||
logger.info("🔍 레거시 방식: table_type_columns 기반 카테고리 컬럼 조회", { tableNames, companyCode });
|
||||
// 🔄 레거시 방식: 형제 메뉴들의 테이블에서 모든 카테고리 컬럼 조회
|
||||
logger.info("🔍 레거시 방식: 형제 메뉴 테이블 기반 카테고리 컬럼 조회", { menuObjid, companyCode });
|
||||
|
||||
// 형제 메뉴 조회
|
||||
const { getSiblingMenuObjids } = await import("../services/menuService");
|
||||
const siblingObjids = await getSiblingMenuObjids(parseInt(menuObjid));
|
||||
|
||||
// 형제 메뉴들이 사용하는 테이블 조회
|
||||
const tablesQuery = `
|
||||
SELECT DISTINCT sd.table_name
|
||||
FROM screen_menu_assignments sma
|
||||
INNER JOIN screen_definitions sd ON sma.screen_id = sd.screen_id
|
||||
WHERE sma.menu_objid = ANY($1)
|
||||
AND sma.company_code = $2
|
||||
AND sd.table_name IS NOT NULL
|
||||
`;
|
||||
|
||||
const tablesResult = await pool.query(tablesQuery, [siblingObjids, companyCode]);
|
||||
const tableNames = tablesResult.rows.map((row: any) => row.table_name);
|
||||
|
||||
logger.info("✅ 형제 메뉴 테이블 조회 완료", { tableNames, count: tableNames.length });
|
||||
|
||||
if (tableNames.length === 0) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: [],
|
||||
message: "형제 메뉴에 연결된 테이블이 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const columnsQuery = `
|
||||
SELECT
|
||||
|
||||
Reference in New Issue
Block a user