Compare commits

..

No commits in common. "c0f2fbbd885d0f2f78020827ccf301f8744eccea" and "561d9cb855a580adacb4b11a0e5a840d985c07e6" have entirely different histories.

6 changed files with 54 additions and 195 deletions

View File

@ -461,13 +461,12 @@ export class CommonCodeController {
} }
/** /**
* () *
* GET /api/common-codes/categories/check-duplicate?field=categoryCode&value=USER_STATUS&excludeCode=OLD_CODE * GET /api/common-codes/categories/check-duplicate?field=categoryCode&value=USER_STATUS&excludeCode=OLD_CODE
*/ */
async checkCategoryDuplicate(req: AuthenticatedRequest, res: Response) { async checkCategoryDuplicate(req: AuthenticatedRequest, res: Response) {
try { try {
const { field, value, excludeCode } = req.query; const { field, value, excludeCode } = req.query;
const userCompanyCode = req.user?.companyCode;
// 입력값 검증 // 입력값 검증
if (!field || !value) { if (!field || !value) {
@ -489,8 +488,7 @@ export class CommonCodeController {
const result = await this.commonCodeService.checkCategoryDuplicate( const result = await this.commonCodeService.checkCategoryDuplicate(
field as "categoryCode" | "categoryName" | "categoryNameEng", field as "categoryCode" | "categoryName" | "categoryNameEng",
value as string, value as string,
excludeCode as string, excludeCode as string
userCompanyCode
); );
return res.json({ return res.json({
@ -513,14 +511,13 @@ export class CommonCodeController {
} }
/** /**
* () *
* GET /api/common-codes/categories/:categoryCode/codes/check-duplicate?field=codeValue&value=ACTIVE&excludeCode=OLD_CODE * GET /api/common-codes/categories/:categoryCode/codes/check-duplicate?field=codeValue&value=ACTIVE&excludeCode=OLD_CODE
*/ */
async checkCodeDuplicate(req: AuthenticatedRequest, res: Response) { async checkCodeDuplicate(req: AuthenticatedRequest, res: Response) {
try { try {
const { categoryCode } = req.params; const { categoryCode } = req.params;
const { field, value, excludeCode } = req.query; const { field, value, excludeCode } = req.query;
const userCompanyCode = req.user?.companyCode;
// 입력값 검증 // 입력값 검증
if (!field || !value) { if (!field || !value) {
@ -543,8 +540,7 @@ export class CommonCodeController {
categoryCode, categoryCode,
field as "codeValue" | "codeName" | "codeNameEng", field as "codeValue" | "codeName" | "codeNameEng",
value as string, value as string,
excludeCode as string, excludeCode as string
userCompanyCode
); );
return res.json({ return res.json({

View File

@ -87,10 +87,7 @@ router.get(
filter, filter,
}); });
const result = await ExternalDbConnectionService.getConnections( const result = await ExternalDbConnectionService.getConnections(filter);
filter,
userCompanyCode
);
if (result.success) { if (result.success) {
return res.status(200).json(result); return res.status(200).json(result);
@ -322,12 +319,7 @@ router.delete(
}); });
} }
const userCompanyCode = req.user?.companyCode; const result = await ExternalDbConnectionService.deleteConnection(id);
const result = await ExternalDbConnectionService.deleteConnection(
id,
userCompanyCode
);
if (result.success) { if (result.success) {
return res.status(200).json(result); return res.status(200).json(result);
@ -525,10 +517,7 @@ router.get(
}); });
const externalConnections = const externalConnections =
await ExternalDbConnectionService.getConnections( await ExternalDbConnectionService.getConnections(filter);
filter,
userCompanyCode
);
if (!externalConnections.success) { if (!externalConnections.success) {
return res.status(400).json(externalConnections); return res.status(400).json(externalConnections);

View File

@ -29,12 +29,8 @@ router.get(
company_code: req.query.company_code as string, company_code: req.query.company_code as string,
}; };
const userCompanyCode = req.user?.companyCode; const result =
await ExternalRestApiConnectionService.getConnections(filter);
const result = await ExternalRestApiConnectionService.getConnections(
filter,
userCompanyCode
);
return res.status(result.success ? 200 : 400).json(result); return res.status(result.success ? 200 : 400).json(result);
} catch (error) { } catch (error) {
@ -66,12 +62,8 @@ router.get(
}); });
} }
const userCompanyCode = req.user?.companyCode; const result =
await ExternalRestApiConnectionService.getConnectionById(id);
const result = await ExternalRestApiConnectionService.getConnectionById(
id,
userCompanyCode
);
return res.status(result.success ? 200 : 404).json(result); return res.status(result.success ? 200 : 404).json(result);
} catch (error) { } catch (error) {
@ -137,12 +129,9 @@ router.put(
updated_by: req.user?.userId || "system", updated_by: req.user?.userId || "system",
}; };
const userCompanyCode = req.user?.companyCode;
const result = await ExternalRestApiConnectionService.updateConnection( const result = await ExternalRestApiConnectionService.updateConnection(
id, id,
data, data
userCompanyCode
); );
return res.status(result.success ? 200 : 400).json(result); return res.status(result.success ? 200 : 400).json(result);
@ -175,12 +164,8 @@ router.delete(
}); });
} }
const userCompanyCode = req.user?.companyCode; const result =
await ExternalRestApiConnectionService.deleteConnection(id);
const result = await ExternalRestApiConnectionService.deleteConnection(
id,
userCompanyCode
);
return res.status(result.success ? 200 : 404).json(result); return res.status(result.success ? 200 : 404).json(result);
} catch (error) { } catch (error) {

View File

@ -604,13 +604,12 @@ export class CommonCodeService {
} }
/** /**
* () *
*/ */
async checkCategoryDuplicate( async checkCategoryDuplicate(
field: "categoryCode" | "categoryName" | "categoryNameEng", field: "categoryCode" | "categoryName" | "categoryNameEng",
value: string, value: string,
excludeCategoryCode?: string, excludeCategoryCode?: string
userCompanyCode?: string
): Promise<{ isDuplicate: boolean; message: string }> { ): Promise<{ isDuplicate: boolean; message: string }> {
try { try {
if (!value || !value.trim()) { if (!value || !value.trim()) {
@ -656,12 +655,6 @@ export class CommonCodeService {
break; break;
} }
// 회사별 필터링 (최고 관리자가 아닌 경우)
if (userCompanyCode && userCompanyCode !== "*") {
sql += ` AND company_code = $${paramIndex++}`;
values.push(userCompanyCode);
}
// 수정 시 자기 자신 제외 // 수정 시 자기 자신 제외
if (excludeCategoryCode) { if (excludeCategoryCode) {
sql += ` AND category_code != $${paramIndex++}`; sql += ` AND category_code != $${paramIndex++}`;
@ -682,10 +675,6 @@ export class CommonCodeService {
categoryNameEng: "카테고리 영문명", categoryNameEng: "카테고리 영문명",
}; };
logger.info(
`카테고리 중복 검사: ${field}=${value}, 회사=${userCompanyCode}, 중복=${isDuplicate}`
);
return { return {
isDuplicate, isDuplicate,
message: isDuplicate message: isDuplicate
@ -699,14 +688,13 @@ export class CommonCodeService {
} }
/** /**
* () *
*/ */
async checkCodeDuplicate( async checkCodeDuplicate(
categoryCode: string, categoryCode: string,
field: "codeValue" | "codeName" | "codeNameEng", field: "codeValue" | "codeName" | "codeNameEng",
value: string, value: string,
excludeCodeValue?: string, excludeCodeValue?: string
userCompanyCode?: string
): Promise<{ isDuplicate: boolean; message: string }> { ): Promise<{ isDuplicate: boolean; message: string }> {
try { try {
if (!value || !value.trim()) { if (!value || !value.trim()) {
@ -755,12 +743,6 @@ export class CommonCodeService {
break; break;
} }
// 회사별 필터링 (최고 관리자가 아닌 경우)
if (userCompanyCode && userCompanyCode !== "*") {
sql += ` AND company_code = $${paramIndex++}`;
values.push(userCompanyCode);
}
// 수정 시 자기 자신 제외 // 수정 시 자기 자신 제외
if (excludeCodeValue) { if (excludeCodeValue) {
sql += ` AND code_value != $${paramIndex++}`; sql += ` AND code_value != $${paramIndex++}`;
@ -778,10 +760,6 @@ export class CommonCodeService {
codeNameEng: "코드 영문명", codeNameEng: "코드 영문명",
}; };
logger.info(
`코드 중복 검사: ${categoryCode}.${field}=${value}, 회사=${userCompanyCode}, 중복=${isDuplicate}`
);
return { return {
isDuplicate, isDuplicate,
message: isDuplicate message: isDuplicate

View File

@ -17,8 +17,7 @@ export class ExternalDbConnectionService {
* DB * DB
*/ */
static async getConnections( static async getConnections(
filter: ExternalDbConnectionFilter, filter: ExternalDbConnectionFilter
userCompanyCode?: string
): Promise<ApiResponse<ExternalDbConnection[]>> { ): Promise<ApiResponse<ExternalDbConnection[]>> {
try { try {
// WHERE 조건 동적 생성 // WHERE 조건 동적 생성
@ -26,26 +25,6 @@ export class ExternalDbConnectionService {
const params: any[] = []; const params: any[] = [];
let paramIndex = 1; let paramIndex = 1;
// 회사별 필터링 (최고 관리자가 아닌 경우 필수)
if (userCompanyCode && userCompanyCode !== "*") {
whereConditions.push(`company_code = $${paramIndex++}`);
params.push(userCompanyCode);
logger.info(`회사별 외부 DB 연결 필터링: ${userCompanyCode}`);
} else if (userCompanyCode === "*") {
logger.info(`최고 관리자: 모든 외부 DB 연결 조회`);
// 필터가 있으면 적용
if (filter.company_code) {
whereConditions.push(`company_code = $${paramIndex++}`);
params.push(filter.company_code);
}
} else {
// userCompanyCode가 없는 경우 (하위 호환성)
if (filter.company_code) {
whereConditions.push(`company_code = $${paramIndex++}`);
params.push(filter.company_code);
}
}
// 필터 조건 적용 // 필터 조건 적용
if (filter.db_type) { if (filter.db_type) {
whereConditions.push(`db_type = $${paramIndex++}`); whereConditions.push(`db_type = $${paramIndex++}`);
@ -57,6 +36,11 @@ export class ExternalDbConnectionService {
params.push(filter.is_active); params.push(filter.is_active);
} }
if (filter.company_code) {
whereConditions.push(`company_code = $${paramIndex++}`);
params.push(filter.company_code);
}
// 검색 조건 적용 (연결명 또는 설명에서 검색) // 검색 조건 적용 (연결명 또는 설명에서 검색)
if (filter.search && filter.search.trim()) { if (filter.search && filter.search.trim()) {
whereConditions.push( whereConditions.push(
@ -512,36 +496,23 @@ export class ExternalDbConnectionService {
/** /**
* DB ( ) * DB ( )
*/ */
static async deleteConnection( static async deleteConnection(id: number): Promise<ApiResponse<void>> {
id: number,
userCompanyCode?: string
): Promise<ApiResponse<void>> {
try { try {
let selectQuery = `SELECT id FROM external_db_connections WHERE id = $1`; const existingConnection = await queryOne(
const selectParams: any[] = [id]; `SELECT id FROM external_db_connections WHERE id = $1`,
[id]
// 회사별 필터링 (최고 관리자가 아닌 경우) );
if (userCompanyCode && userCompanyCode !== "*") {
selectQuery += ` AND company_code = $2`;
selectParams.push(userCompanyCode);
}
const existingConnection = await queryOne(selectQuery, selectParams);
if (!existingConnection) { if (!existingConnection) {
return { return {
success: false, success: false,
message: "해당 연결 설정을 찾을 수 없거나 권한이 없습니다.", message: "해당 연결 설정을 찾을 수 없습니다.",
}; };
} }
// 물리 삭제 (실제 데이터 삭제) // 물리 삭제 (실제 데이터 삭제)
await query(`DELETE FROM external_db_connections WHERE id = $1`, [id]); await query(`DELETE FROM external_db_connections WHERE id = $1`, [id]);
logger.info(
`외부 DB 연결 삭제: ID ${id} (회사: ${userCompanyCode || "전체"})`
);
return { return {
success: true, success: true,
message: "연결 설정이 삭제되었습니다.", message: "연결 설정이 삭제되었습니다.",
@ -776,11 +747,8 @@ export class ExternalDbConnectionService {
try { try {
// 보안 검증: SELECT 쿼리만 허용 // 보안 검증: SELECT 쿼리만 허용
const trimmedQuery = query.trim().toUpperCase(); const trimmedQuery = query.trim().toUpperCase();
if (!trimmedQuery.startsWith("SELECT")) { if (!trimmedQuery.startsWith('SELECT')) {
console.log("보안 오류: SELECT가 아닌 쿼리 시도:", { console.log("보안 오류: SELECT가 아닌 쿼리 시도:", { id, query: query.substring(0, 100) });
id,
query: query.substring(0, 100),
});
return { return {
success: false, success: false,
message: "외부 데이터베이스에서는 SELECT 쿼리만 실행할 수 있습니다.", message: "외부 데이터베이스에서는 SELECT 쿼리만 실행할 수 있습니다.",
@ -788,32 +756,16 @@ export class ExternalDbConnectionService {
} }
// 위험한 키워드 검사 // 위험한 키워드 검사
const dangerousKeywords = [ const dangerousKeywords = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER', 'TRUNCATE', 'EXEC', 'EXECUTE', 'CALL', 'MERGE'];
"INSERT", const hasDangerousKeyword = dangerousKeywords.some(keyword =>
"UPDATE",
"DELETE",
"DROP",
"CREATE",
"ALTER",
"TRUNCATE",
"EXEC",
"EXECUTE",
"CALL",
"MERGE",
];
const hasDangerousKeyword = dangerousKeywords.some((keyword) =>
trimmedQuery.includes(keyword) trimmedQuery.includes(keyword)
); );
if (hasDangerousKeyword) { if (hasDangerousKeyword) {
console.log("보안 오류: 위험한 키워드 포함 쿼리 시도:", { console.log("보안 오류: 위험한 키워드 포함 쿼리 시도:", { id, query: query.substring(0, 100) });
id,
query: query.substring(0, 100),
});
return { return {
success: false, success: false,
message: message: "데이터를 변경하거나 삭제하는 쿼리는 허용되지 않습니다. SELECT 쿼리만 사용해주세요.",
"데이터를 변경하거나 삭제하는 쿼리는 허용되지 않습니다. SELECT 쿼리만 사용해주세요.",
}; };
} }

View File

@ -23,8 +23,7 @@ export class ExternalRestApiConnectionService {
* REST API * REST API
*/ */
static async getConnections( static async getConnections(
filter: ExternalRestApiConnectionFilter = {}, filter: ExternalRestApiConnectionFilter = {}
userCompanyCode?: string
): Promise<ApiResponse<ExternalRestApiConnection[]>> { ): Promise<ApiResponse<ExternalRestApiConnection[]>> {
try { try {
let query = ` let query = `
@ -40,27 +39,11 @@ export class ExternalRestApiConnectionService {
const params: any[] = []; const params: any[] = [];
let paramIndex = 1; let paramIndex = 1;
// 회사별 필터링 (최고 관리자가 아닌 경우 필수) // 회사 코드 필터
if (userCompanyCode && userCompanyCode !== "*") { if (filter.company_code) {
query += ` AND company_code = $${paramIndex}`; query += ` AND company_code = $${paramIndex}`;
params.push(userCompanyCode); params.push(filter.company_code);
paramIndex++; paramIndex++;
logger.info(`회사별 REST API 연결 필터링: ${userCompanyCode}`);
} else if (userCompanyCode === "*") {
logger.info(`최고 관리자: 모든 REST API 연결 조회`);
// 필터가 있으면 적용
if (filter.company_code) {
query += ` AND company_code = $${paramIndex}`;
params.push(filter.company_code);
paramIndex++;
}
} else {
// userCompanyCode가 없는 경우 (하위 호환성)
if (filter.company_code) {
query += ` AND company_code = $${paramIndex}`;
params.push(filter.company_code);
paramIndex++;
}
} }
// 활성 상태 필터 // 활성 상태 필터
@ -122,11 +105,10 @@ export class ExternalRestApiConnectionService {
* REST API * REST API
*/ */
static async getConnectionById( static async getConnectionById(
id: number, id: number
userCompanyCode?: string
): Promise<ApiResponse<ExternalRestApiConnection>> { ): Promise<ApiResponse<ExternalRestApiConnection>> {
try { try {
let query = ` const query = `
SELECT SELECT
id, connection_name, description, base_url, default_headers, id, connection_name, description, base_url, default_headers,
auth_type, auth_config, timeout, retry_count, retry_delay, auth_type, auth_config, timeout, retry_count, retry_delay,
@ -136,20 +118,12 @@ export class ExternalRestApiConnectionService {
WHERE id = $1 WHERE id = $1
`; `;
const params: any[] = [id]; const result: QueryResult<any> = await pool.query(query, [id]);
// 회사별 필터링 (최고 관리자가 아닌 경우)
if (userCompanyCode && userCompanyCode !== "*") {
query += ` AND company_code = $2`;
params.push(userCompanyCode);
}
const result: QueryResult<any> = await pool.query(query, params);
if (result.rows.length === 0) { if (result.rows.length === 0) {
return { return {
success: false, success: false,
message: "연결을 찾을 수 없거나 권한이 없습니다.", message: "연결을 찾을 수 없습니다.",
}; };
} }
@ -251,12 +225,11 @@ export class ExternalRestApiConnectionService {
*/ */
static async updateConnection( static async updateConnection(
id: number, id: number,
data: Partial<ExternalRestApiConnection>, data: Partial<ExternalRestApiConnection>
userCompanyCode?: string
): Promise<ApiResponse<ExternalRestApiConnection>> { ): Promise<ApiResponse<ExternalRestApiConnection>> {
try { try {
// 기존 연결 확인 (회사 코드로 권한 체크) // 기존 연결 확인
const existing = await this.getConnectionById(id, userCompanyCode); const existing = await this.getConnectionById(id);
if (!existing.success) { if (!existing.success) {
return existing; return existing;
} }
@ -380,38 +353,24 @@ export class ExternalRestApiConnectionService {
/** /**
* REST API * REST API
*/ */
static async deleteConnection( static async deleteConnection(id: number): Promise<ApiResponse<void>> {
id: number,
userCompanyCode?: string
): Promise<ApiResponse<void>> {
try { try {
let query = ` const query = `
DELETE FROM external_rest_api_connections DELETE FROM external_rest_api_connections
WHERE id = $1 WHERE id = $1
RETURNING connection_name
`; `;
const params: any[] = [id]; const result: QueryResult<any> = await pool.query(query, [id]);
// 회사별 필터링 (최고 관리자가 아닌 경우)
if (userCompanyCode && userCompanyCode !== "*") {
query += ` AND company_code = $2`;
params.push(userCompanyCode);
}
query += ` RETURNING connection_name`;
const result: QueryResult<any> = await pool.query(query, params);
if (result.rows.length === 0) { if (result.rows.length === 0) {
return { return {
success: false, success: false,
message: "연결을 찾을 수 없거나 권한이 없습니다.", message: "연결을 찾을 수 없습니다.",
}; };
} }
logger.info( logger.info(`REST API 연결 삭제 성공: ${result.rows[0].connection_name}`);
`REST API 연결 삭제 성공: ${result.rows[0].connection_name} (회사: ${userCompanyCode || "전체"})`
);
return { return {
success: true, success: true,