Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feat/dashboard

This commit is contained in:
dohyeons
2025-11-05 15:39:11 +09:00
38 changed files with 6529 additions and 441 deletions

View File

@@ -3084,3 +3084,84 @@ export const resetUserPassword = async (
});
}
};
/**
* 테이블 스키마 조회 (엑셀 업로드 컬럼 매핑용)
*/
export async function getTableSchema(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const companyCode = req.user?.companyCode;
if (!tableName) {
res.status(400).json({
success: false,
message: "테이블명이 필요합니다.",
});
return;
}
logger.info("테이블 스키마 조회", { tableName, companyCode });
// information_schema에서 컬럼 정보 가져오기
const schemaQuery = `
SELECT
column_name,
data_type,
is_nullable,
column_default,
character_maximum_length,
numeric_precision,
numeric_scale
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = $1
ORDER BY ordinal_position
`;
const columns = await query<any>(schemaQuery, [tableName]);
if (columns.length === 0) {
res.status(404).json({
success: false,
message: `테이블 '${tableName}'을 찾을 수 없습니다.`,
});
return;
}
// 컬럼 정보를 간단한 형태로 변환
const columnList = columns.map((col: any) => ({
name: col.column_name,
type: col.data_type,
nullable: col.is_nullable === "YES",
default: col.column_default,
maxLength: col.character_maximum_length,
precision: col.numeric_precision,
scale: col.numeric_scale,
}));
logger.info(`테이블 스키마 조회 성공: ${columnList.length}개 컬럼`);
res.json({
success: true,
message: "테이블 스키마 조회 성공",
data: {
tableName,
columns: columnList,
},
});
} catch (error) {
logger.error("테이블 스키마 조회 중 오류 발생:", error);
res.status(500).json({
success: false,
message: "테이블 스키마 조회 중 오류가 발생했습니다.",
error: {
code: "TABLE_SCHEMA_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
},
});
}
}

View File

@@ -0,0 +1,282 @@
import { Request, Response } from "express";
import pool from "../database/db";
import { logger } from "../utils/logger";
interface AuthenticatedRequest extends Request {
user?: {
userId: string;
userName: string;
companyCode: string;
};
}
/**
* 코드 병합 - 모든 관련 테이블에 적용
* 데이터(레코드)는 삭제하지 않고, 컬럼 값만 변경
*/
export async function mergeCodeAllTables(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
const { columnName, oldValue, newValue } = req.body;
const companyCode = req.user?.companyCode;
try {
// 입력값 검증
if (!columnName || !oldValue || !newValue) {
res.status(400).json({
success: false,
message: "필수 필드가 누락되었습니다. (columnName, oldValue, newValue)",
});
return;
}
if (!companyCode) {
res.status(401).json({
success: false,
message: "인증 정보가 없습니다.",
});
return;
}
// 같은 값으로 병합 시도 방지
if (oldValue === newValue) {
res.status(400).json({
success: false,
message: "기존 값과 새 값이 동일합니다.",
});
return;
}
logger.info("코드 병합 시작", {
columnName,
oldValue,
newValue,
companyCode,
userId: req.user?.userId,
});
// PostgreSQL 함수 호출
const result = await pool.query(
"SELECT * FROM merge_code_all_tables($1, $2, $3, $4)",
[columnName, oldValue, newValue, companyCode]
);
// 결과 처리 (pool.query 반환 타입 처리)
const affectedTables = Array.isArray(result) ? result : (result.rows || []);
const totalRows = affectedTables.reduce(
(sum, row) => sum + parseInt(row.rows_updated || 0),
0
);
logger.info("코드 병합 완료", {
columnName,
oldValue,
newValue,
affectedTablesCount: affectedTables.length,
totalRowsUpdated: totalRows,
});
res.json({
success: true,
message: `코드 병합 완료: ${oldValue}${newValue}`,
data: {
columnName,
oldValue,
newValue,
affectedTables: affectedTables.map((row) => ({
tableName: row.table_name,
rowsUpdated: parseInt(row.rows_updated),
})),
totalRowsUpdated: totalRows,
},
});
} catch (error: any) {
logger.error("코드 병합 실패:", {
error: error.message,
stack: error.stack,
columnName,
oldValue,
newValue,
});
res.status(500).json({
success: false,
message: "코드 병합 중 오류가 발생했습니다.",
error: {
code: "CODE_MERGE_ERROR",
details: error.message,
},
});
}
}
/**
* 특정 컬럼을 가진 테이블 목록 조회
*/
export async function getTablesWithColumn(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
const { columnName } = req.params;
try {
if (!columnName) {
res.status(400).json({
success: false,
message: "컬럼명이 필요합니다.",
});
return;
}
logger.info("컬럼을 가진 테이블 목록 조회", { columnName });
const query = `
SELECT DISTINCT t.table_name
FROM information_schema.columns c
JOIN information_schema.tables t
ON c.table_name = t.table_name
WHERE c.column_name = $1
AND t.table_schema = 'public'
AND t.table_type = 'BASE TABLE'
AND EXISTS (
SELECT 1 FROM information_schema.columns c2
WHERE c2.table_name = t.table_name
AND c2.column_name = 'company_code'
)
ORDER BY t.table_name
`;
const result = await pool.query(query, [columnName]);
logger.info(`컬럼을 가진 테이블 조회 완료: ${result.rows.length}`);
res.json({
success: true,
message: "테이블 목록 조회 성공",
data: {
columnName,
tables: result.rows.map((row) => row.table_name),
count: result.rows.length,
},
});
} catch (error: any) {
logger.error("테이블 목록 조회 실패:", error);
res.status(500).json({
success: false,
message: "테이블 목록 조회 중 오류가 발생했습니다.",
error: {
code: "TABLE_LIST_ERROR",
details: error.message,
},
});
}
}
/**
* 코드 병합 미리보기 (실제 실행 없이 영향받을 데이터 확인)
*/
export async function previewCodeMerge(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
const { columnName, oldValue } = req.body;
const companyCode = req.user?.companyCode;
try {
if (!columnName || !oldValue) {
res.status(400).json({
success: false,
message: "필수 필드가 누락되었습니다. (columnName, oldValue)",
});
return;
}
if (!companyCode) {
res.status(401).json({
success: false,
message: "인증 정보가 없습니다.",
});
return;
}
logger.info("코드 병합 미리보기", { columnName, oldValue, companyCode });
// 해당 컬럼을 가진 테이블 찾기
const tablesQuery = `
SELECT DISTINCT t.table_name
FROM information_schema.columns c
JOIN information_schema.tables t
ON c.table_name = t.table_name
WHERE c.column_name = $1
AND t.table_schema = 'public'
AND t.table_type = 'BASE TABLE'
AND EXISTS (
SELECT 1 FROM information_schema.columns c2
WHERE c2.table_name = t.table_name
AND c2.column_name = 'company_code'
)
`;
const tablesResult = await pool.query(tablesQuery, [columnName]);
// 각 테이블에서 영향받을 행 수 계산
const preview = [];
const tableRows = Array.isArray(tablesResult) ? tablesResult : (tablesResult.rows || []);
for (const row of tableRows) {
const tableName = row.table_name;
// 동적 SQL 생성 (테이블명과 컬럼명은 파라미터 바인딩 불가)
// SQL 인젝션 방지: 테이블명과 컬럼명은 information_schema에서 검증된 값
const countQuery = `SELECT COUNT(*) as count FROM "${tableName}" WHERE "${columnName}" = $1 AND company_code = $2`;
try {
const countResult = await pool.query(countQuery, [oldValue, companyCode]);
const count = parseInt(countResult.rows[0].count);
if (count > 0) {
preview.push({
tableName,
affectedRows: count,
});
}
} catch (error: any) {
logger.warn(`테이블 ${tableName} 조회 실패:`, error.message);
// 테이블 접근 실패 시 건너뛰기
continue;
}
}
const totalRows = preview.reduce((sum, item) => sum + item.affectedRows, 0);
logger.info("코드 병합 미리보기 완료", {
tablesCount: preview.length,
totalRows,
});
res.json({
success: true,
message: "코드 병합 미리보기 완료",
data: {
columnName,
oldValue,
preview,
totalAffectedRows: totalRows,
},
});
} catch (error: any) {
logger.error("코드 병합 미리보기 실패:", error);
res.status(500).json({
success: false,
message: "코드 병합 미리보기 중 오류가 발생했습니다.",
error: {
code: "PREVIEW_ERROR",
details: error.message,
},
});
}
}

View File

@@ -2,15 +2,15 @@
* 채번 규칙 관리 컨트롤러
*/
import { Router, Request, Response } from "express";
import { authenticateToken } from "../middleware/authMiddleware";
import { Router, Response } from "express";
import { authenticateToken, AuthenticatedRequest } from "../middleware/authMiddleware";
import { numberingRuleService } from "../services/numberingRuleService";
import { logger } from "../utils/logger";
const router = Router();
// 규칙 목록 조회
router.get("/", authenticateToken, async (req: Request, res: Response) => {
// 규칙 목록 조회 (전체)
router.get("/", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const companyCode = req.user!.companyCode;
try {
@@ -22,8 +22,25 @@ router.get("/", authenticateToken, async (req: Request, res: Response) => {
}
});
// 메뉴별 사용 가능한 규칙 조회
router.get("/available/:menuObjid?", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const companyCode = req.user!.companyCode;
const menuObjid = req.params.menuObjid ? parseInt(req.params.menuObjid) : undefined;
try {
const rules = await numberingRuleService.getAvailableRulesForMenu(companyCode, menuObjid);
return res.json({ success: true, data: rules });
} catch (error: any) {
logger.error("메뉴별 사용 가능한 규칙 조회 실패", {
error: error.message,
menuObjid,
});
return res.status(500).json({ success: false, error: error.message });
}
});
// 특정 규칙 조회
router.get("/:ruleId", authenticateToken, async (req: Request, res: Response) => {
router.get("/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const companyCode = req.user!.companyCode;
const { ruleId } = req.params;
@@ -40,7 +57,7 @@ router.get("/:ruleId", authenticateToken, async (req: Request, res: Response) =>
});
// 규칙 생성
router.post("/", authenticateToken, async (req: Request, res: Response) => {
router.post("/", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const companyCode = req.user!.companyCode;
const userId = req.user!.userId;
const ruleConfig = req.body;
@@ -66,7 +83,7 @@ router.post("/", authenticateToken, async (req: Request, res: Response) => {
});
// 규칙 수정
router.put("/:ruleId", authenticateToken, async (req: Request, res: Response) => {
router.put("/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const companyCode = req.user!.companyCode;
const { ruleId } = req.params;
const updates = req.body;
@@ -84,7 +101,7 @@ router.put("/:ruleId", authenticateToken, async (req: Request, res: Response) =>
});
// 규칙 삭제
router.delete("/:ruleId", authenticateToken, async (req: Request, res: Response) => {
router.delete("/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const companyCode = req.user!.companyCode;
const { ruleId } = req.params;
@@ -100,14 +117,42 @@ router.delete("/:ruleId", authenticateToken, async (req: Request, res: Response)
}
});
// 코드 생성
router.post("/:ruleId/generate", authenticateToken, async (req: Request, res: Response) => {
// 코드 미리보기 (순번 증가 없음)
router.post("/:ruleId/preview", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const companyCode = req.user!.companyCode;
const { ruleId } = req.params;
try {
const previewCode = await numberingRuleService.previewCode(ruleId, companyCode);
return res.json({ success: true, data: { generatedCode: previewCode } });
} catch (error: any) {
logger.error("코드 미리보기 실패", { error: error.message });
return res.status(500).json({ success: false, error: error.message });
}
});
// 코드 할당 (저장 시점에 실제 순번 증가)
router.post("/:ruleId/allocate", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const companyCode = req.user!.companyCode;
const { ruleId } = req.params;
try {
const allocatedCode = await numberingRuleService.allocateCode(ruleId, companyCode);
return res.json({ success: true, data: { generatedCode: allocatedCode } });
} catch (error: any) {
logger.error("코드 할당 실패", { error: error.message });
return res.status(500).json({ success: false, error: error.message });
}
});
// 코드 생성 (기존 호환성 유지, deprecated)
router.post("/:ruleId/generate", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const companyCode = req.user!.companyCode;
const { ruleId } = req.params;
try {
const generatedCode = await numberingRuleService.generateCode(ruleId, companyCode);
return res.json({ success: true, data: { code: generatedCode } });
return res.json({ success: true, data: { generatedCode } });
} catch (error: any) {
logger.error("코드 생성 실패", { error: error.message });
return res.status(500).json({ success: false, error: error.message });
@@ -115,7 +160,7 @@ router.post("/:ruleId/generate", authenticateToken, async (req: Request, res: Re
});
// 시퀀스 초기화
router.post("/:ruleId/reset", authenticateToken, async (req: Request, res: Response) => {
router.post("/:ruleId/reset", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
const companyCode = req.user!.companyCode;
const { ruleId } = req.params;