Compare commits

..

9 Commits

10 changed files with 388 additions and 158 deletions

View File

@ -3,7 +3,8 @@
* *
*/ */
import { Request, Response } from "express"; import { Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import { query, queryOne } from "../database/db"; import { query, queryOne } from "../database/db";
import logger from "../utils/logger"; import logger from "../utils/logger";
@ -14,7 +15,10 @@ import logger from "../utils/logger";
/** /**
* *
*/ */
export const getAutoFillGroups = async (req: Request, res: Response) => { export const getAutoFillGroups = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
const { isActive } = req.query; const { isActive } = req.query;
@ -47,7 +51,10 @@ export const getAutoFillGroups = async (req: Request, res: Response) => {
const result = await query(sql, params); const result = await query(sql, params);
logger.info("자동 입력 그룹 목록 조회", { count: result.length, companyCode }); logger.info("자동 입력 그룹 목록 조회", {
count: result.length,
companyCode,
});
res.json({ res.json({
success: true, success: true,
@ -66,7 +73,10 @@ export const getAutoFillGroups = async (req: Request, res: Response) => {
/** /**
* ( ) * ( )
*/ */
export const getAutoFillGroupDetail = async (req: Request, res: Response) => { export const getAutoFillGroupDetail = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { groupCode } = req.params; const { groupCode } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -98,7 +108,10 @@ export const getAutoFillGroupDetail = async (req: Request, res: Response) => {
WHERE group_code = $1 AND company_code = $2 WHERE group_code = $1 AND company_code = $2
ORDER BY sort_order, mapping_id ORDER BY sort_order, mapping_id
`; `;
const mappingResult = await query(mappingSql, [groupCode, groupResult.company_code]); const mappingResult = await query(mappingSql, [
groupCode,
groupResult.company_code,
]);
logger.info("자동 입력 그룹 상세 조회", { groupCode, companyCode }); logger.info("자동 입력 그룹 상세 조회", { groupCode, companyCode });
@ -122,7 +135,9 @@ export const getAutoFillGroupDetail = async (req: Request, res: Response) => {
/** /**
* *
*/ */
const generateAutoFillGroupCode = async (companyCode: string): Promise<string> => { const generateAutoFillGroupCode = async (
companyCode: string
): Promise<string> => {
const prefix = "AF"; const prefix = "AF";
const result = await queryOne( const result = await queryOne(
`SELECT COUNT(*) as cnt FROM cascading_auto_fill_group WHERE company_code = $1`, `SELECT COUNT(*) as cnt FROM cascading_auto_fill_group WHERE company_code = $1`,
@ -136,7 +151,10 @@ const generateAutoFillGroupCode = async (companyCode: string): Promise<string> =
/** /**
* *
*/ */
export const createAutoFillGroup = async (req: Request, res: Response) => { export const createAutoFillGroup = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
const userId = req.user?.userId || "system"; const userId = req.user?.userId || "system";
@ -153,7 +171,8 @@ export const createAutoFillGroup = async (req: Request, res: Response) => {
if (!groupName || !masterTable || !masterValueColumn) { if (!groupName || !masterTable || !masterValueColumn) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "필수 필드가 누락되었습니다. (groupName, masterTable, masterValueColumn)", message:
"필수 필드가 누락되었습니다. (groupName, masterTable, masterValueColumn)",
}); });
} }
@ -224,7 +243,10 @@ export const createAutoFillGroup = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const updateAutoFillGroup = async (req: Request, res: Response) => { export const updateAutoFillGroup = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { groupCode } = req.params; const { groupCode } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -333,7 +355,10 @@ export const updateAutoFillGroup = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const deleteAutoFillGroup = async (req: Request, res: Response) => { export const deleteAutoFillGroup = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { groupCode } = req.params; const { groupCode } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -382,7 +407,10 @@ export const deleteAutoFillGroup = async (req: Request, res: Response) => {
* *
* *
*/ */
export const getAutoFillMasterOptions = async (req: Request, res: Response) => { export const getAutoFillMasterOptions = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { groupCode } = req.params; const { groupCode } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -436,7 +464,10 @@ export const getAutoFillMasterOptions = async (req: Request, res: Response) => {
const optionsResult = await query(optionsSql, optionsParams); const optionsResult = await query(optionsSql, optionsParams);
logger.info("자동 입력 마스터 옵션 조회", { groupCode, count: optionsResult.length }); logger.info("자동 입력 마스터 옵션 조회", {
groupCode,
count: optionsResult.length,
});
res.json({ res.json({
success: true, success: true,
@ -456,7 +487,10 @@ export const getAutoFillMasterOptions = async (req: Request, res: Response) => {
* *
* *
*/ */
export const getAutoFillData = async (req: Request, res: Response) => { export const getAutoFillData = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { groupCode } = req.params; const { groupCode } = req.params;
const { masterValue } = req.query; const { masterValue } = req.query;
@ -535,9 +569,10 @@ export const getAutoFillData = async (req: Request, res: Response) => {
for (const mapping of mappings) { for (const mapping of mappings) {
const sourceValue = dataResult?.[mapping.source_column]; const sourceValue = dataResult?.[mapping.source_column];
const finalValue = sourceValue !== null && sourceValue !== undefined const finalValue =
? sourceValue sourceValue !== null && sourceValue !== undefined
: mapping.default_value; ? sourceValue
: mapping.default_value;
autoFillData[mapping.target_field] = finalValue; autoFillData[mapping.target_field] = finalValue;
mappingInfo.push({ mappingInfo.push({
@ -549,7 +584,11 @@ export const getAutoFillData = async (req: Request, res: Response) => {
}); });
} }
logger.info("자동 입력 데이터 조회", { groupCode, masterValue, fieldCount: mappingInfo.length }); logger.info("자동 입력 데이터 조회", {
groupCode,
masterValue,
fieldCount: mappingInfo.length,
});
res.json({ res.json({
success: true, success: true,
@ -565,4 +604,3 @@ export const getAutoFillData = async (req: Request, res: Response) => {
}); });
} }
}; };

View File

@ -3,7 +3,8 @@
* *
*/ */
import { Request, Response } from "express"; import { Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import { query, queryOne } from "../database/db"; import { query, queryOne } from "../database/db";
import logger from "../utils/logger"; import logger from "../utils/logger";
@ -14,7 +15,10 @@ import logger from "../utils/logger";
/** /**
* *
*/ */
export const getConditions = async (req: Request, res: Response) => { export const getConditions = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
const { isActive, relationCode, relationType } = req.query; const { isActive, relationCode, relationType } = req.query;
@ -54,7 +58,10 @@ export const getConditions = async (req: Request, res: Response) => {
const result = await query(sql, params); const result = await query(sql, params);
logger.info("조건부 연쇄 규칙 목록 조회", { count: result.length, companyCode }); logger.info("조건부 연쇄 규칙 목록 조회", {
count: result.length,
companyCode,
});
res.json({ res.json({
success: true, success: true,
@ -77,7 +84,10 @@ export const getConditions = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const getConditionDetail = async (req: Request, res: Response) => { export const getConditionDetail = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { conditionId } = req.params; const { conditionId } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -118,7 +128,10 @@ export const getConditionDetail = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const createCondition = async (req: Request, res: Response) => { export const createCondition = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
const { const {
@ -134,10 +147,18 @@ export const createCondition = async (req: Request, res: Response) => {
} = req.body; } = req.body;
// 필수 필드 검증 // 필수 필드 검증
if (!relationCode || !conditionName || !conditionField || !conditionValue || !filterColumn || !filterValues) { if (
!relationCode ||
!conditionName ||
!conditionField ||
!conditionValue ||
!filterColumn ||
!filterValues
) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "필수 필드가 누락되었습니다. (relationCode, conditionName, conditionField, conditionValue, filterColumn, filterValues)", message:
"필수 필드가 누락되었습니다. (relationCode, conditionName, conditionField, conditionValue, filterColumn, filterValues)",
}); });
} }
@ -164,7 +185,11 @@ export const createCondition = async (req: Request, res: Response) => {
companyCode, companyCode,
]); ]);
logger.info("조건부 연쇄 규칙 생성", { conditionId: result?.condition_id, relationCode, companyCode }); logger.info("조건부 연쇄 규칙 생성", {
conditionId: result?.condition_id,
relationCode,
companyCode,
});
res.status(201).json({ res.status(201).json({
success: true, success: true,
@ -184,7 +209,10 @@ export const createCondition = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const updateCondition = async (req: Request, res: Response) => { export const updateCondition = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { conditionId } = req.params; const { conditionId } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -264,7 +292,10 @@ export const updateCondition = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const deleteCondition = async (req: Request, res: Response) => { export const deleteCondition = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { conditionId } = req.params; const { conditionId } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -312,7 +343,10 @@ export const deleteCondition = async (req: Request, res: Response) => {
* *
* *
*/ */
export const getFilteredOptions = async (req: Request, res: Response) => { export const getFilteredOptions = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { relationCode } = req.params; const { relationCode } = req.params;
const { conditionFieldValue, parentValue } = req.query; const { conditionFieldValue, parentValue } = req.query;
@ -390,8 +424,12 @@ export const getFilteredOptions = async (req: Request, res: Response) => {
// 조건부 필터 적용 // 조건부 필터 적용
if (matchedCondition) { if (matchedCondition) {
const filterValues = matchedCondition.filter_values.split(",").map((v: string) => v.trim()); const filterValues = matchedCondition.filter_values
const placeholders = filterValues.map((_: any, i: number) => `$${optionsParamIndex + i}`).join(","); .split(",")
.map((v: string) => v.trim());
const placeholders = filterValues
.map((_: any, i: number) => `$${optionsParamIndex + i}`)
.join(",");
optionsSql += ` AND ${matchedCondition.filter_column} IN (${placeholders})`; optionsSql += ` AND ${matchedCondition.filter_column} IN (${placeholders})`;
optionsParams.push(...filterValues); optionsParams.push(...filterValues);
optionsParamIndex += filterValues.length; optionsParamIndex += filterValues.length;
@ -522,4 +560,3 @@ function evaluateCondition(
return false; return false;
} }
} }

View File

@ -3,7 +3,8 @@
* > > / * > > /
*/ */
import { Request, Response } from "express"; import { Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import { query, queryOne } from "../database/db"; import { query, queryOne } from "../database/db";
import logger from "../utils/logger"; import logger from "../utils/logger";
@ -14,7 +15,10 @@ import logger from "../utils/logger";
/** /**
* *
*/ */
export const getHierarchyGroups = async (req: Request, res: Response) => { export const getHierarchyGroups = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
const { isActive, hierarchyType } = req.query; const { isActive, hierarchyType } = req.query;
@ -66,7 +70,10 @@ export const getHierarchyGroups = async (req: Request, res: Response) => {
/** /**
* ( ) * ( )
*/ */
export const getHierarchyGroupDetail = async (req: Request, res: Response) => { export const getHierarchyGroupDetail = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { groupCode } = req.params; const { groupCode } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -124,7 +131,9 @@ export const getHierarchyGroupDetail = async (req: Request, res: Response) => {
/** /**
* *
*/ */
const generateHierarchyGroupCode = async (companyCode: string): Promise<string> => { const generateHierarchyGroupCode = async (
companyCode: string
): Promise<string> => {
const prefix = "HG"; const prefix = "HG";
const result = await queryOne( const result = await queryOne(
`SELECT COUNT(*) as cnt FROM cascading_hierarchy_group WHERE company_code = $1`, `SELECT COUNT(*) as cnt FROM cascading_hierarchy_group WHERE company_code = $1`,
@ -138,7 +147,10 @@ const generateHierarchyGroupCode = async (companyCode: string): Promise<string>
/** /**
* *
*/ */
export const createHierarchyGroup = async (req: Request, res: Response) => { export const createHierarchyGroup = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
const userId = req.user?.userId || "system"; const userId = req.user?.userId || "system";
@ -280,7 +292,10 @@ export const createHierarchyGroup = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const updateHierarchyGroup = async (req: Request, res: Response) => { export const updateHierarchyGroup = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { groupCode } = req.params; const { groupCode } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -364,7 +379,10 @@ export const updateHierarchyGroup = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const deleteHierarchyGroup = async (req: Request, res: Response) => { export const deleteHierarchyGroup = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { groupCode } = req.params; const { groupCode } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -423,7 +441,7 @@ export const deleteHierarchyGroup = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const addLevel = async (req: Request, res: Response) => { export const addLevel = async (req: AuthenticatedRequest, res: Response) => {
try { try {
const { groupCode } = req.params; const { groupCode } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -506,7 +524,7 @@ export const addLevel = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const updateLevel = async (req: Request, res: Response) => { export const updateLevel = async (req: AuthenticatedRequest, res: Response) => {
try { try {
const { levelId } = req.params; const { levelId } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -600,7 +618,7 @@ export const updateLevel = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const deleteLevel = async (req: Request, res: Response) => { export const deleteLevel = async (req: AuthenticatedRequest, res: Response) => {
try { try {
const { levelId } = req.params; const { levelId } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -647,7 +665,10 @@ export const deleteLevel = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const getLevelOptions = async (req: Request, res: Response) => { export const getLevelOptions = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { groupCode, levelOrder } = req.params; const { groupCode, levelOrder } = req.params;
const { parentValue } = req.query; const { parentValue } = req.query;
@ -749,4 +770,3 @@ export const getLevelOptions = async (req: Request, res: Response) => {
}); });
} }
}; };

View File

@ -3,7 +3,8 @@
* *
*/ */
import { Request, Response } from "express"; import { Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import { query, queryOne } from "../database/db"; import { query, queryOne } from "../database/db";
import logger from "../utils/logger"; import logger from "../utils/logger";
@ -14,7 +15,10 @@ import logger from "../utils/logger";
/** /**
* *
*/ */
export const getExclusions = async (req: Request, res: Response) => { export const getExclusions = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
const { isActive } = req.query; const { isActive } = req.query;
@ -42,7 +46,10 @@ export const getExclusions = async (req: Request, res: Response) => {
const result = await query(sql, params); const result = await query(sql, params);
logger.info("상호 배제 규칙 목록 조회", { count: result.length, companyCode }); logger.info("상호 배제 규칙 목록 조회", {
count: result.length,
companyCode,
});
res.json({ res.json({
success: true, success: true,
@ -61,7 +68,10 @@ export const getExclusions = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const getExclusionDetail = async (req: Request, res: Response) => { export const getExclusionDetail = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { exclusionId } = req.params; const { exclusionId } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -116,7 +126,10 @@ const generateExclusionCode = async (companyCode: string): Promise<string> => {
/** /**
* *
*/ */
export const createExclusion = async (req: Request, res: Response) => { export const createExclusion = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
const { const {
@ -133,7 +146,8 @@ export const createExclusion = async (req: Request, res: Response) => {
if (!exclusionName || !fieldNames || !sourceTable || !valueColumn) { if (!exclusionName || !fieldNames || !sourceTable || !valueColumn) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "필수 필드가 누락되었습니다. (exclusionName, fieldNames, sourceTable, valueColumn)", message:
"필수 필드가 누락되었습니다. (exclusionName, fieldNames, sourceTable, valueColumn)",
}); });
} }
@ -195,7 +209,10 @@ export const createExclusion = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const updateExclusion = async (req: Request, res: Response) => { export const updateExclusion = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { exclusionId } = req.params; const { exclusionId } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -274,7 +291,10 @@ export const updateExclusion = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const deleteExclusion = async (req: Request, res: Response) => { export const deleteExclusion = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { exclusionId } = req.params; const { exclusionId } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -322,7 +342,10 @@ export const deleteExclusion = async (req: Request, res: Response) => {
* *
* *
*/ */
export const validateExclusion = async (req: Request, res: Response) => { export const validateExclusion = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { exclusionCode } = req.params; const { exclusionCode } = req.params;
const { fieldValues } = req.body; // { "source_warehouse": "WH001", "target_warehouse": "WH002" } const { fieldValues } = req.body; // { "source_warehouse": "WH001", "target_warehouse": "WH002" }
@ -347,7 +370,9 @@ export const validateExclusion = async (req: Request, res: Response) => {
} }
// 필드명 파싱 // 필드명 파싱
const fields = exclusion.field_names.split(",").map((f: string) => f.trim()); const fields = exclusion.field_names
.split(",")
.map((f: string) => f.trim());
// 필드 값 수집 // 필드 값 수집
const values: string[] = []; const values: string[] = [];
@ -418,7 +443,10 @@ export const validateExclusion = async (req: Request, res: Response) => {
* *
* *
*/ */
export const getExcludedOptions = async (req: Request, res: Response) => { export const getExcludedOptions = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { exclusionCode } = req.params; const { exclusionCode } = req.params;
const { currentField, selectedValues } = req.query; // selectedValues: 이미 선택된 값들 (콤마 구분) const { currentField, selectedValues } = req.query; // selectedValues: 이미 선택된 값들 (콤마 구분)
@ -470,9 +498,14 @@ export const getExcludedOptions = async (req: Request, res: Response) => {
// 이미 선택된 값 제외 // 이미 선택된 값 제외
if (selectedValues) { if (selectedValues) {
const excludeValues = (selectedValues as string).split(",").map((v) => v.trim()).filter((v) => v); const excludeValues = (selectedValues as string)
.split(",")
.map((v) => v.trim())
.filter((v) => v);
if (excludeValues.length > 0) { if (excludeValues.length > 0) {
const placeholders = excludeValues.map((_, i) => `$${optionsParamIndex + i}`).join(","); const placeholders = excludeValues
.map((_, i) => `$${optionsParamIndex + i}`)
.join(",");
optionsSql += ` AND ${exclusion.value_column} NOT IN (${placeholders})`; optionsSql += ` AND ${exclusion.value_column} NOT IN (${placeholders})`;
optionsParams.push(...excludeValues); optionsParams.push(...excludeValues);
} }
@ -502,4 +535,3 @@ export const getExcludedOptions = async (req: Request, res: Response) => {
}); });
} }
}; };

View File

@ -1,4 +1,5 @@
import { Request, Response } from "express"; import { Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import { getPool } from "../database/db"; import { getPool } from "../database/db";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
@ -7,7 +8,10 @@ const pool = getPool();
/** /**
* *
*/ */
export const getCascadingRelations = async (req: Request, res: Response) => { export const getCascadingRelations = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
const { isActive } = req.query; const { isActive } = req.query;
@ -86,7 +90,10 @@ export const getCascadingRelations = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const getCascadingRelationById = async (req: Request, res: Response) => { export const getCascadingRelationById = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { id } = req.params; const { id } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -155,7 +162,7 @@ export const getCascadingRelationById = async (req: Request, res: Response) => {
* *
*/ */
export const getCascadingRelationByCode = async ( export const getCascadingRelationByCode = async (
req: Request, req: AuthenticatedRequest,
res: Response res: Response
) => { ) => {
try { try {
@ -223,7 +230,10 @@ export const getCascadingRelationByCode = async (
/** /**
* *
*/ */
export const createCascadingRelation = async (req: Request, res: Response) => { export const createCascadingRelation = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
const userId = req.user?.userId || "system"; const userId = req.user?.userId || "system";
@ -350,7 +360,10 @@ export const createCascadingRelation = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const updateCascadingRelation = async (req: Request, res: Response) => { export const updateCascadingRelation = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { id } = req.params; const { id } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -474,7 +487,10 @@ export const updateCascadingRelation = async (req: Request, res: Response) => {
/** /**
* *
*/ */
export const deleteCascadingRelation = async (req: Request, res: Response) => { export const deleteCascadingRelation = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { id } = req.params; const { id } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -536,7 +552,10 @@ export const deleteCascadingRelation = async (req: Request, res: Response) => {
* 🆕 ( ) * 🆕 ( )
* parent_table에서 . * parent_table에서 .
*/ */
export const getParentOptions = async (req: Request, res: Response) => { export const getParentOptions = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { code } = req.params; const { code } = req.params;
const companyCode = req.user?.companyCode || "*"; const companyCode = req.user?.companyCode || "*";
@ -644,7 +663,10 @@ export const getParentOptions = async (req: Request, res: Response) => {
* *
* API * API
*/ */
export const getCascadingOptions = async (req: Request, res: Response) => { export const getCascadingOptions = async (
req: AuthenticatedRequest,
res: Response
) => {
try { try {
const { code } = req.params; const { code } = req.params;
const { parentValue } = req.query; const { parentValue } = req.query;

View File

@ -1,43 +1,25 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { pool, queryOne } from "../database/db";
import logger from "../utils/logger"; import logger from "../utils/logger";
import { PasswordEncryption } from "../utils/passwordEncryption"; import { ExternalDbConnectionPoolService } from "../services/externalDbConnectionPoolService";
import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory";
// 외부 DB 커넥터를 가져오는 헬퍼 함수 // 외부 DB 커넥터를 가져오는 헬퍼 함수 (연결 풀 사용)
export async function getExternalDbConnector(connectionId: number) { export async function getExternalDbConnector(connectionId: number) {
// 외부 DB 연결 정보 조회 const poolService = ExternalDbConnectionPoolService.getInstance();
const connection = await queryOne<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[connectionId]
);
if (!connection) { // 연결 풀 래퍼를 반환 (executeQuery 메서드를 가진 객체)
throw new Error(`외부 DB 연결 정보를 찾을 수 없습니다. ID: ${connectionId}`); return {
} executeQuery: async (sql: string, params?: any[]) => {
const result = await poolService.executeQuery(connectionId, sql, params);
// 패스워드 복호화 return { rows: result };
const decryptedPassword = PasswordEncryption.decrypt(connection.password); },
// DB 연결 설정
const config = {
host: connection.host,
port: connection.port,
user: connection.username,
password: decryptedPassword,
database: connection.database_name,
}; };
// DB 커넥터 생성
return await DatabaseConnectorFactory.createConnector(
connection.db_type || "mariadb",
config,
connectionId
);
} }
// 동적 계층 구조 데이터 조회 (범용) // 동적 계층 구조 데이터 조회 (범용)
export const getHierarchyData = async (req: Request, res: Response): Promise<Response> => { export const getHierarchyData = async (
req: Request,
res: Response
): Promise<Response> => {
try { try {
const { externalDbConnectionId, hierarchyConfig } = req.body; const { externalDbConnectionId, hierarchyConfig } = req.body;
@ -48,7 +30,9 @@ export const getHierarchyData = async (req: Request, res: Response): Promise<Res
}); });
} }
const connector = await getExternalDbConnector(Number(externalDbConnectionId)); const connector = await getExternalDbConnector(
Number(externalDbConnectionId)
);
const config = JSON.parse(hierarchyConfig); const config = JSON.parse(hierarchyConfig);
const result: any = { const result: any = {
@ -94,7 +78,10 @@ export const getHierarchyData = async (req: Request, res: Response): Promise<Res
logger.info("동적 계층 구조 데이터 조회", { logger.info("동적 계층 구조 데이터 조회", {
externalDbConnectionId, externalDbConnectionId,
warehouseCount: result.warehouse?.length || 0, warehouseCount: result.warehouse?.length || 0,
levelCounts: result.levels.map((l: any) => ({ level: l.level, count: l.data.length })), levelCounts: result.levels.map((l: any) => ({
level: l.level,
count: l.data.length,
})),
}); });
return res.json({ return res.json({
@ -112,22 +99,35 @@ export const getHierarchyData = async (req: Request, res: Response): Promise<Res
}; };
// 특정 레벨의 하위 데이터 조회 // 특정 레벨의 하위 데이터 조회
export const getChildrenData = async (req: Request, res: Response): Promise<Response> => { export const getChildrenData = async (
req: Request,
res: Response
): Promise<Response> => {
try { try {
const { externalDbConnectionId, hierarchyConfig, parentLevel, parentKey } = req.body; const { externalDbConnectionId, hierarchyConfig, parentLevel, parentKey } =
req.body;
if (!externalDbConnectionId || !hierarchyConfig || !parentLevel || !parentKey) { if (
!externalDbConnectionId ||
!hierarchyConfig ||
!parentLevel ||
!parentKey
) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "필수 파라미터가 누락되었습니다.", message: "필수 파라미터가 누락되었습니다.",
}); });
} }
const connector = await getExternalDbConnector(Number(externalDbConnectionId)); const connector = await getExternalDbConnector(
Number(externalDbConnectionId)
);
const config = JSON.parse(hierarchyConfig); const config = JSON.parse(hierarchyConfig);
// 다음 레벨 찾기 // 다음 레벨 찾기
const nextLevel = config.levels?.find((l: any) => l.level === parentLevel + 1); const nextLevel = config.levels?.find(
(l: any) => l.level === parentLevel + 1
);
if (!nextLevel) { if (!nextLevel) {
return res.json({ return res.json({
@ -168,7 +168,10 @@ export const getChildrenData = async (req: Request, res: Response): Promise<Resp
}; };
// 창고 목록 조회 (사용자 지정 테이블) - 레거시, 호환성 유지 // 창고 목록 조회 (사용자 지정 테이블) - 레거시, 호환성 유지
export const getWarehouses = async (req: Request, res: Response): Promise<Response> => { export const getWarehouses = async (
req: Request,
res: Response
): Promise<Response> => {
try { try {
const { externalDbConnectionId, tableName } = req.query; const { externalDbConnectionId, tableName } = req.query;
@ -186,7 +189,9 @@ export const getWarehouses = async (req: Request, res: Response): Promise<Respon
}); });
} }
const connector = await getExternalDbConnector(Number(externalDbConnectionId)); const connector = await getExternalDbConnector(
Number(externalDbConnectionId)
);
// 테이블명을 사용하여 모든 컬럼 조회 // 테이블명을 사용하여 모든 컬럼 조회
const query = `SELECT * FROM ${tableName} LIMIT 100`; const query = `SELECT * FROM ${tableName} LIMIT 100`;
@ -215,7 +220,10 @@ export const getWarehouses = async (req: Request, res: Response): Promise<Respon
}; };
// 구역 목록 조회 (사용자 지정 테이블) - 레거시, 호환성 유지 // 구역 목록 조회 (사용자 지정 테이블) - 레거시, 호환성 유지
export const getAreas = async (req: Request, res: Response): Promise<Response> => { export const getAreas = async (
req: Request,
res: Response
): Promise<Response> => {
try { try {
const { externalDbConnectionId, warehouseKey, tableName } = req.query; const { externalDbConnectionId, warehouseKey, tableName } = req.query;
@ -226,7 +234,9 @@ export const getAreas = async (req: Request, res: Response): Promise<Response> =
}); });
} }
const connector = await getExternalDbConnector(Number(externalDbConnectionId)); const connector = await getExternalDbConnector(
Number(externalDbConnectionId)
);
const query = ` const query = `
SELECT * FROM ${tableName} SELECT * FROM ${tableName}
@ -258,7 +268,10 @@ export const getAreas = async (req: Request, res: Response): Promise<Response> =
}; };
// 위치 목록 조회 (사용자 지정 테이블) - 레거시, 호환성 유지 // 위치 목록 조회 (사용자 지정 테이블) - 레거시, 호환성 유지
export const getLocations = async (req: Request, res: Response): Promise<Response> => { export const getLocations = async (
req: Request,
res: Response
): Promise<Response> => {
try { try {
const { externalDbConnectionId, areaKey, tableName } = req.query; const { externalDbConnectionId, areaKey, tableName } = req.query;
@ -269,7 +282,9 @@ export const getLocations = async (req: Request, res: Response): Promise<Respons
}); });
} }
const connector = await getExternalDbConnector(Number(externalDbConnectionId)); const connector = await getExternalDbConnector(
Number(externalDbConnectionId)
);
const query = ` const query = `
SELECT * FROM ${tableName} SELECT * FROM ${tableName}
@ -301,7 +316,10 @@ export const getLocations = async (req: Request, res: Response): Promise<Respons
}; };
// 자재 목록 조회 (동적 컬럼 매핑 지원) // 자재 목록 조회 (동적 컬럼 매핑 지원)
export const getMaterials = async (req: Request, res: Response): Promise<Response> => { export const getMaterials = async (
req: Request,
res: Response
): Promise<Response> => {
try { try {
const { const {
externalDbConnectionId, externalDbConnectionId,
@ -309,20 +327,27 @@ export const getMaterials = async (req: Request, res: Response): Promise<Respons
tableName, tableName,
keyColumn, keyColumn,
locationKeyColumn, locationKeyColumn,
layerColumn layerColumn,
} = req.query; } = req.query;
if (!externalDbConnectionId || !locaKey || !tableName || !locationKeyColumn) { if (
!externalDbConnectionId ||
!locaKey ||
!tableName ||
!locationKeyColumn
) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "필수 파라미터가 누락되었습니다.", message: "필수 파라미터가 누락되었습니다.",
}); });
} }
const connector = await getExternalDbConnector(Number(externalDbConnectionId)); const connector = await getExternalDbConnector(
Number(externalDbConnectionId)
);
// 동적 쿼리 생성 // 동적 쿼리 생성
const orderByClause = layerColumn ? `ORDER BY ${layerColumn}` : ''; const orderByClause = layerColumn ? `ORDER BY ${layerColumn}` : "";
const query = ` const query = `
SELECT * FROM ${tableName} SELECT * FROM ${tableName}
WHERE ${locationKeyColumn} = '${locaKey}' WHERE ${locationKeyColumn} = '${locaKey}'
@ -356,7 +381,10 @@ export const getMaterials = async (req: Request, res: Response): Promise<Respons
}; };
// 자재 개수 조회 (여러 Location 일괄) - 레거시, 호환성 유지 // 자재 개수 조회 (여러 Location 일괄) - 레거시, 호환성 유지
export const getMaterialCounts = async (req: Request, res: Response): Promise<Response> => { export const getMaterialCounts = async (
req: Request,
res: Response
): Promise<Response> => {
try { try {
const { externalDbConnectionId, locationKeys, tableName } = req.body; const { externalDbConnectionId, locationKeys, tableName } = req.body;
@ -367,7 +395,9 @@ export const getMaterialCounts = async (req: Request, res: Response): Promise<Re
}); });
} }
const connector = await getExternalDbConnector(Number(externalDbConnectionId)); const connector = await getExternalDbConnector(
Number(externalDbConnectionId)
);
const keysString = locationKeys.map((key: string) => `'${key}'`).join(","); const keysString = locationKeys.map((key: string) => `'${key}'`).join(",");

View File

@ -113,6 +113,7 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
lastUsedAt: Date; lastUsedAt: Date;
activeConnections = 0; activeConnections = 0;
maxConnections: number; maxConnections: number;
private isPoolClosed = false;
constructor(config: ExternalDbConnection) { constructor(config: ExternalDbConnection) {
this.connectionId = config.id!; this.connectionId = config.id!;
@ -131,6 +132,9 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
waitForConnections: true, waitForConnections: true,
queueLimit: 0, queueLimit: 0,
connectTimeout: (config.connection_timeout || 30) * 1000, connectTimeout: (config.connection_timeout || 30) * 1000,
// 연결 유지 및 자동 재연결 설정
enableKeepAlive: true,
keepAliveInitialDelay: 10000, // 10초마다 keep-alive 패킷 전송
ssl: ssl:
config.ssl_enabled === "Y" ? { rejectUnauthorized: false } : undefined, config.ssl_enabled === "Y" ? { rejectUnauthorized: false } : undefined,
}); });
@ -153,11 +157,33 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
async query(sql: string, params?: any[]): Promise<any> { async query(sql: string, params?: any[]): Promise<any> {
this.lastUsedAt = new Date(); this.lastUsedAt = new Date();
const [rows] = await this.pool.execute(sql, params);
return rows; // 연결 풀이 닫힌 상태인지 확인
if (this.isPoolClosed) {
throw new Error("연결 풀이 닫힌 상태입니다. 재연결이 필요합니다.");
}
try {
const [rows] = await this.pool.execute(sql, params);
return rows;
} catch (error: any) {
// 연결 닫힘 오류 감지
if (
error.message.includes("closed state") ||
error.code === "PROTOCOL_CONNECTION_LOST" ||
error.code === "ECONNRESET"
) {
this.isPoolClosed = true;
logger.warn(
`[${this.dbType.toUpperCase()}] 연결 끊김 감지 (ID: ${this.connectionId})`
);
}
throw error;
}
} }
async disconnect(): Promise<void> { async disconnect(): Promise<void> {
this.isPoolClosed = true;
await this.pool.end(); await this.pool.end();
logger.info( logger.info(
`[${this.dbType.toUpperCase()}] 연결 풀 종료 (ID: ${this.connectionId})` `[${this.dbType.toUpperCase()}] 연결 풀 종료 (ID: ${this.connectionId})`
@ -165,6 +191,10 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
} }
isHealthy(): boolean { isHealthy(): boolean {
// 연결 풀이 닫혔으면 비정상
if (this.isPoolClosed) {
return false;
}
return this.activeConnections < this.maxConnections; return this.activeConnections < this.maxConnections;
} }
} }
@ -230,9 +260,11 @@ export class ExternalDbConnectionPoolService {
): Promise<ConnectionPoolWrapper> { ): Promise<ConnectionPoolWrapper> {
logger.info(`🔧 새 연결 풀 생성 중 (ID: ${connectionId})...`); logger.info(`🔧 새 연결 풀 생성 중 (ID: ${connectionId})...`);
// DB 연결 정보 조회 // DB 연결 정보 조회 (실제 비밀번호 포함)
const connectionResult = const connectionResult =
await ExternalDbConnectionService.getConnectionById(connectionId); await ExternalDbConnectionService.getConnectionByIdWithPassword(
connectionId
);
if (!connectionResult.success || !connectionResult.data) { if (!connectionResult.success || !connectionResult.data) {
throw new Error(`연결 정보를 찾을 수 없습니다 (ID: ${connectionId})`); throw new Error(`연결 정보를 찾을 수 없습니다 (ID: ${connectionId})`);
@ -296,16 +328,19 @@ export class ExternalDbConnectionPoolService {
} }
/** /**
* ( ) * ( + )
*/ */
async executeQuery( async executeQuery(
connectionId: number, connectionId: number,
sql: string, sql: string,
params?: any[] params?: any[],
retryCount = 0
): Promise<any> { ): Promise<any> {
const pool = await this.getPool(connectionId); const MAX_RETRIES = 2;
try { try {
const pool = await this.getPool(connectionId);
logger.debug( logger.debug(
`📊 쿼리 실행 (ID: ${connectionId}): ${sql.substring(0, 100)}...` `📊 쿼리 실행 (ID: ${connectionId}): ${sql.substring(0, 100)}...`
); );
@ -314,7 +349,29 @@ export class ExternalDbConnectionPoolService {
`✅ 쿼리 완료 (ID: ${connectionId}), 결과: ${result.length}` `✅ 쿼리 완료 (ID: ${connectionId}), 결과: ${result.length}`
); );
return result; return result;
} catch (error) { } catch (error: any) {
// 연결 끊김 오류인 경우 재시도
const isConnectionError =
error.message?.includes("closed state") ||
error.message?.includes("연결 풀이 닫힌 상태") ||
error.code === "PROTOCOL_CONNECTION_LOST" ||
error.code === "ECONNRESET" ||
error.code === "ETIMEDOUT";
if (isConnectionError && retryCount < MAX_RETRIES) {
logger.warn(
`🔄 연결 오류 감지, 재시도 중... (${retryCount + 1}/${MAX_RETRIES}) (ID: ${connectionId})`
);
// 기존 풀 제거 후 새로 생성
await this.removePool(connectionId);
// 잠시 대기 후 재시도
await new Promise((resolve) => setTimeout(resolve, 500));
return this.executeQuery(connectionId, sql, params, retryCount + 1);
}
logger.error(`❌ 쿼리 실행 실패 (ID: ${connectionId}):`, error); logger.error(`❌ 쿼리 실행 실패 (ID: ${connectionId}):`, error);
throw error; throw error;
} }

View File

@ -3596,7 +3596,7 @@ export class NodeFlowExecutionService {
// 계정 ID 우선순위: nodeAccountId > smtpConfigId > 첫 번째 활성 계정 // 계정 ID 우선순위: nodeAccountId > smtpConfigId > 첫 번째 활성 계정
let accountId = nodeAccountId || smtpConfigId; let accountId = nodeAccountId || smtpConfigId;
if (!accountId) { if (!accountId) {
const accounts = await mailAccountFileService.getAccounts(); const accounts = await mailAccountFileService.getAllAccounts();
const activeAccount = accounts.find( const activeAccount = accounts.find(
(acc: any) => acc.status === "active" (acc: any) => acc.status === "active"
); );
@ -4216,7 +4216,7 @@ export class NodeFlowExecutionService {
return this.evaluateFunction(func, sourceRow, targetRow, resultValues); return this.evaluateFunction(func, sourceRow, targetRow, resultValues);
case "condition": case "condition":
return this.evaluateCondition( return this.evaluateCaseCondition(
condition, condition,
sourceRow, sourceRow,
targetRow, targetRow,
@ -4393,7 +4393,7 @@ export class NodeFlowExecutionService {
/** /**
* (CASE WHEN ... THEN ... ELSE) * (CASE WHEN ... THEN ... ELSE)
*/ */
private static evaluateCondition( private static evaluateCaseCondition(
condition: any, condition: any,
sourceRow: any, sourceRow: any,
targetRow: any, targetRow: any,

View File

@ -34,9 +34,9 @@
"@react-three/fiber": "^9.4.0", "@react-three/fiber": "^9.4.0",
"@tanstack/react-query": "^5.86.0", "@tanstack/react-query": "^5.86.0",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@tiptap/core": "^3.13.0", "@tiptap/core": "^2.27.1",
"@tiptap/extension-placeholder": "^2.27.1", "@tiptap/extension-placeholder": "^2.27.1",
"@tiptap/pm": "^2.11.5", "@tiptap/pm": "^2.27.1",
"@tiptap/react": "^2.27.1", "@tiptap/react": "^2.27.1",
"@tiptap/starter-kit": "^2.27.1", "@tiptap/starter-kit": "^2.27.1",
"@turf/buffer": "^7.2.0", "@turf/buffer": "^7.2.0",
@ -3302,16 +3302,16 @@
} }
}, },
"node_modules/@tiptap/core": { "node_modules/@tiptap/core": {
"version": "3.13.0", "version": "2.27.1",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.13.0.tgz", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.1.tgz",
"integrity": "sha512-iUelgiTMgPVMpY5ZqASUpk8mC8HuR9FWKaDzK27w9oWip9tuB54Z8mePTxNcQaSPb6ErzEaC8x8egrRt7OsdGQ==", "integrity": "sha512-nkerkl8syHj44ZzAB7oA2GPmmZINKBKCa79FuNvmGJrJ4qyZwlkDzszud23YteFZEytbc87kVd/fP76ROS6sLg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/pm": "^3.13.0" "@tiptap/pm": "^2.7.0"
} }
}, },
"node_modules/@tiptap/extension-blockquote": { "node_modules/@tiptap/extension-blockquote": {
@ -3700,19 +3700,6 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
} }
}, },
"node_modules/@tiptap/starter-kit/node_modules/@tiptap/core": {
"version": "2.27.1",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.1.tgz",
"integrity": "sha512-nkerkl8syHj44ZzAB7oA2GPmmZINKBKCa79FuNvmGJrJ4qyZwlkDzszud23YteFZEytbc87kVd/fP76ROS6sLg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/pm": "^2.7.0"
}
},
"node_modules/@turf/along": { "node_modules/@turf/along": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/@turf/along/-/along-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@turf/along/-/along-7.2.0.tgz",
@ -6084,7 +6071,7 @@
"version": "20.19.24", "version": "20.19.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz",
"integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
@ -6122,7 +6109,7 @@
"version": "19.2.2", "version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
@ -12538,6 +12525,13 @@
"react-dom": ">=16" "react-dom": ">=16"
} }
}, },
"node_modules/react-is": {
"version": "19.2.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.1.tgz",
"integrity": "sha512-L7BnWgRbMwzMAubQcS7sXdPdNLmKlucPlopgAzx7FtYbksWZgEWiuYM5x9T6UqS2Ne0rsgQTq5kY2SGqpzUkYA==",
"license": "MIT",
"peer": true
},
"node_modules/react-leaflet": { "node_modules/react-leaflet": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
@ -14197,7 +14191,7 @@
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unrs-resolver": { "node_modules/unrs-resolver": {

View File

@ -42,9 +42,9 @@
"@react-three/fiber": "^9.4.0", "@react-three/fiber": "^9.4.0",
"@tanstack/react-query": "^5.86.0", "@tanstack/react-query": "^5.86.0",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@tiptap/core": "^3.13.0", "@tiptap/core": "^2.27.1",
"@tiptap/extension-placeholder": "^2.27.1", "@tiptap/extension-placeholder": "^2.27.1",
"@tiptap/pm": "^2.11.5", "@tiptap/pm": "^2.27.1",
"@tiptap/react": "^2.27.1", "@tiptap/react": "^2.27.1",
"@tiptap/starter-kit": "^2.27.1", "@tiptap/starter-kit": "^2.27.1",
"@turf/buffer": "^7.2.0", "@turf/buffer": "^7.2.0",