Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary, ; especially if it merges an updated upstream into a topic branch. ; ; Lines starting with ';' will be ignored, and an empty message aborts ; the commit.
This commit is contained in:
commit
fb73ee2878
|
|
@ -4,7 +4,11 @@
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { BatchService } from "../services/batchService";
|
import { BatchService } from "../services/batchService";
|
||||||
import { BatchSchedulerService } from "../services/batchSchedulerService";
|
import { BatchSchedulerService } from "../services/batchSchedulerService";
|
||||||
import { BatchConfigFilter, CreateBatchConfigRequest, UpdateBatchConfigRequest } from "../types/batchTypes";
|
import {
|
||||||
|
BatchConfigFilter,
|
||||||
|
CreateBatchConfigRequest,
|
||||||
|
UpdateBatchConfigRequest,
|
||||||
|
} from "../types/batchTypes";
|
||||||
|
|
||||||
export interface AuthenticatedRequest extends Request {
|
export interface AuthenticatedRequest extends Request {
|
||||||
user?: {
|
user?: {
|
||||||
|
|
@ -16,32 +20,36 @@ export interface AuthenticatedRequest extends Request {
|
||||||
|
|
||||||
export class BatchController {
|
export class BatchController {
|
||||||
/**
|
/**
|
||||||
* 배치 설정 목록 조회
|
* 배치 설정 목록 조회 (회사별)
|
||||||
* GET /api/batch-configs
|
* GET /api/batch-configs
|
||||||
*/
|
*/
|
||||||
static async getBatchConfigs(req: AuthenticatedRequest, res: Response) {
|
static async getBatchConfigs(req: AuthenticatedRequest, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { page = 1, limit = 10, search, isActive } = req.query;
|
const { page = 1, limit = 10, search, isActive } = req.query;
|
||||||
|
const userCompanyCode = req.user?.companyCode;
|
||||||
|
|
||||||
const filter: BatchConfigFilter = {
|
const filter: BatchConfigFilter = {
|
||||||
page: Number(page),
|
page: Number(page),
|
||||||
limit: Number(limit),
|
limit: Number(limit),
|
||||||
search: search as string,
|
search: search as string,
|
||||||
is_active: isActive as string
|
is_active: isActive as string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await BatchService.getBatchConfigs(filter);
|
const result = await BatchService.getBatchConfigs(
|
||||||
|
filter,
|
||||||
|
userCompanyCode
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: result.data,
|
data: result.data,
|
||||||
pagination: result.pagination
|
pagination: result.pagination,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("배치 설정 목록 조회 오류:", error);
|
console.error("배치 설정 목록 조회 오류:", error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 설정 목록 조회에 실패했습니다."
|
message: "배치 설정 목록 조회에 실패했습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +58,10 @@ export class BatchController {
|
||||||
* 사용 가능한 커넥션 목록 조회
|
* 사용 가능한 커넥션 목록 조회
|
||||||
* GET /api/batch-configs/connections
|
* GET /api/batch-configs/connections
|
||||||
*/
|
*/
|
||||||
static async getAvailableConnections(req: AuthenticatedRequest, res: Response) {
|
static async getAvailableConnections(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const result = await BatchService.getAvailableConnections();
|
const result = await BatchService.getAvailableConnections();
|
||||||
|
|
||||||
|
|
@ -63,7 +74,7 @@ export class BatchController {
|
||||||
console.error("커넥션 목록 조회 오류:", error);
|
console.error("커넥션 목록 조회 오류:", error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "커넥션 목록 조회에 실패했습니다."
|
message: "커넥션 목록 조회에 실패했습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,19 +84,25 @@ export class BatchController {
|
||||||
* GET /api/batch-configs/connections/:type/tables
|
* GET /api/batch-configs/connections/:type/tables
|
||||||
* GET /api/batch-configs/connections/:type/:id/tables
|
* GET /api/batch-configs/connections/:type/:id/tables
|
||||||
*/
|
*/
|
||||||
static async getTablesFromConnection(req: AuthenticatedRequest, res: Response) {
|
static async getTablesFromConnection(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const { type, id } = req.params;
|
const { type, id } = req.params;
|
||||||
|
|
||||||
if (!type || (type !== 'internal' && type !== 'external')) {
|
if (!type || (type !== "internal" && type !== "external")) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)"
|
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectionId = type === 'external' ? Number(id) : undefined;
|
const connectionId = type === "external" ? Number(id) : undefined;
|
||||||
const result = await BatchService.getTablesFromConnection(type, connectionId);
|
const result = await BatchService.getTablesFromConnection(
|
||||||
|
type,
|
||||||
|
connectionId
|
||||||
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return res.json(result);
|
return res.json(result);
|
||||||
|
|
@ -96,7 +113,7 @@ export class BatchController {
|
||||||
console.error("테이블 목록 조회 오류:", error);
|
console.error("테이블 목록 조회 오류:", error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "테이블 목록 조회에 실패했습니다."
|
message: "테이블 목록 조회에 실패했습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -113,19 +130,23 @@ export class BatchController {
|
||||||
if (!type || !tableName) {
|
if (!type || !tableName) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "연결 타입과 테이블명을 모두 지정해주세요."
|
message: "연결 타입과 테이블명을 모두 지정해주세요.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type !== 'internal' && type !== 'external') {
|
if (type !== "internal" && type !== "external") {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)"
|
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectionId = type === 'external' ? Number(id) : undefined;
|
const connectionId = type === "external" ? Number(id) : undefined;
|
||||||
const result = await BatchService.getTableColumns(type, connectionId, tableName);
|
const result = await BatchService.getTableColumns(
|
||||||
|
type,
|
||||||
|
connectionId,
|
||||||
|
tableName
|
||||||
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return res.json(result);
|
return res.json(result);
|
||||||
|
|
@ -136,36 +157,40 @@ export class BatchController {
|
||||||
console.error("컬럼 정보 조회 오류:", error);
|
console.error("컬럼 정보 조회 오류:", error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "컬럼 정보 조회에 실패했습니다."
|
message: "컬럼 정보 조회에 실패했습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 배치 설정 조회
|
* 특정 배치 설정 조회 (회사별)
|
||||||
* GET /api/batch-configs/:id
|
* GET /api/batch-configs/:id
|
||||||
*/
|
*/
|
||||||
static async getBatchConfigById(req: AuthenticatedRequest, res: Response) {
|
static async getBatchConfigById(req: AuthenticatedRequest, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const batchConfig = await BatchService.getBatchConfigById(Number(id));
|
const userCompanyCode = req.user?.companyCode;
|
||||||
|
const batchConfig = await BatchService.getBatchConfigById(
|
||||||
|
Number(id),
|
||||||
|
userCompanyCode
|
||||||
|
);
|
||||||
|
|
||||||
if (!batchConfig) {
|
if (!batchConfig) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 설정을 찾을 수 없습니다."
|
message: "배치 설정을 찾을 수 없습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: batchConfig
|
data: batchConfig,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("배치 설정 조회 오류:", error);
|
console.error("배치 설정 조회 오류:", error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 설정 조회에 실패했습니다."
|
message: "배치 설정 조회에 실패했습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -178,10 +203,16 @@ export class BatchController {
|
||||||
try {
|
try {
|
||||||
const { batchName, description, cronSchedule, mappings } = req.body;
|
const { batchName, description, cronSchedule, mappings } = req.body;
|
||||||
|
|
||||||
if (!batchName || !cronSchedule || !mappings || !Array.isArray(mappings)) {
|
if (
|
||||||
|
!batchName ||
|
||||||
|
!cronSchedule ||
|
||||||
|
!mappings ||
|
||||||
|
!Array.isArray(mappings)
|
||||||
|
) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "필수 필드가 누락되었습니다. (batchName, cronSchedule, mappings)"
|
message:
|
||||||
|
"필수 필드가 누락되었습니다. (batchName, cronSchedule, mappings)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,56 +220,71 @@ export class BatchController {
|
||||||
batchName,
|
batchName,
|
||||||
description,
|
description,
|
||||||
cronSchedule,
|
cronSchedule,
|
||||||
mappings
|
mappings,
|
||||||
} as CreateBatchConfigRequest);
|
} as CreateBatchConfigRequest);
|
||||||
|
|
||||||
// 생성된 배치가 활성화 상태라면 스케줄러에 등록 (즉시 실행 비활성화)
|
// 생성된 배치가 활성화 상태라면 스케줄러에 등록 (즉시 실행 비활성화)
|
||||||
if (batchConfig.data && batchConfig.data.is_active === 'Y' && batchConfig.data.id) {
|
if (
|
||||||
await BatchSchedulerService.updateBatchSchedule(batchConfig.data.id, false);
|
batchConfig.data &&
|
||||||
|
batchConfig.data.is_active === "Y" &&
|
||||||
|
batchConfig.data.id
|
||||||
|
) {
|
||||||
|
await BatchSchedulerService.updateBatchSchedule(
|
||||||
|
batchConfig.data.id,
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(201).json({
|
return res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: batchConfig,
|
data: batchConfig,
|
||||||
message: "배치 설정이 성공적으로 생성되었습니다."
|
message: "배치 설정이 성공적으로 생성되었습니다.",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("배치 설정 생성 오류:", error);
|
console.error("배치 설정 생성 오류:", error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 설정 생성에 실패했습니다."
|
message: "배치 설정 생성에 실패했습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 배치 설정 수정
|
* 배치 설정 수정 (회사별)
|
||||||
* PUT /api/batch-configs/:id
|
* PUT /api/batch-configs/:id
|
||||||
*/
|
*/
|
||||||
static async updateBatchConfig(req: AuthenticatedRequest, res: Response) {
|
static async updateBatchConfig(req: AuthenticatedRequest, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { batchName, description, cronSchedule, mappings, isActive } = req.body;
|
const { batchName, description, cronSchedule, mappings, isActive } =
|
||||||
|
req.body;
|
||||||
|
const userId = req.user?.userId;
|
||||||
|
const userCompanyCode = req.user?.companyCode;
|
||||||
|
|
||||||
if (!batchName || !cronSchedule) {
|
if (!batchName || !cronSchedule) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "필수 필드가 누락되었습니다. (batchName, cronSchedule)"
|
message: "필수 필드가 누락되었습니다. (batchName, cronSchedule)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const batchConfig = await BatchService.updateBatchConfig(Number(id), {
|
const batchConfig = await BatchService.updateBatchConfig(
|
||||||
|
Number(id),
|
||||||
|
{
|
||||||
batchName,
|
batchName,
|
||||||
description,
|
description,
|
||||||
cronSchedule,
|
cronSchedule,
|
||||||
mappings,
|
mappings,
|
||||||
isActive
|
isActive,
|
||||||
} as UpdateBatchConfigRequest);
|
} as UpdateBatchConfigRequest,
|
||||||
|
userId,
|
||||||
|
userCompanyCode
|
||||||
|
);
|
||||||
|
|
||||||
if (!batchConfig) {
|
if (!batchConfig) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 설정을 찾을 수 없습니다."
|
message: "배치 설정을 찾을 수 없습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,42 +294,48 @@ export class BatchController {
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: batchConfig,
|
data: batchConfig,
|
||||||
message: "배치 설정이 성공적으로 수정되었습니다."
|
message: "배치 설정이 성공적으로 수정되었습니다.",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("배치 설정 수정 오류:", error);
|
console.error("배치 설정 수정 오류:", error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 설정 수정에 실패했습니다."
|
message: "배치 설정 수정에 실패했습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 배치 설정 삭제 (논리 삭제)
|
* 배치 설정 삭제 (논리 삭제, 회사별)
|
||||||
* DELETE /api/batch-configs/:id
|
* DELETE /api/batch-configs/:id
|
||||||
*/
|
*/
|
||||||
static async deleteBatchConfig(req: AuthenticatedRequest, res: Response) {
|
static async deleteBatchConfig(req: AuthenticatedRequest, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const result = await BatchService.deleteBatchConfig(Number(id));
|
const userId = req.user?.userId;
|
||||||
|
const userCompanyCode = req.user?.companyCode;
|
||||||
|
const result = await BatchService.deleteBatchConfig(
|
||||||
|
Number(id),
|
||||||
|
userId,
|
||||||
|
userCompanyCode
|
||||||
|
);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 설정을 찾을 수 없습니다."
|
message: "배치 설정을 찾을 수 없습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "배치 설정이 성공적으로 삭제되었습니다."
|
message: "배치 설정이 성공적으로 삭제되었습니다.",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("배치 설정 삭제 오류:", error);
|
console.error("배치 설정 삭제 오류:", error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 설정 삭제에 실패했습니다."
|
message: "배치 설정 삭제에 실패했습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { AuthenticatedRequest } from "../types/auth";
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
import { BatchExecutionLogService } from "../services/batchExecutionLogService";
|
import { BatchExecutionLogService } from "../services/batchExecutionLogService";
|
||||||
import { BatchExecutionLogFilter, CreateBatchExecutionLogRequest, UpdateBatchExecutionLogRequest } from "../types/batchExecutionLogTypes";
|
import {
|
||||||
|
BatchExecutionLogFilter,
|
||||||
|
CreateBatchExecutionLogRequest,
|
||||||
|
UpdateBatchExecutionLogRequest,
|
||||||
|
} from "../types/batchExecutionLogTypes";
|
||||||
|
|
||||||
export class BatchExecutionLogController {
|
export class BatchExecutionLogController {
|
||||||
/**
|
/**
|
||||||
|
|
@ -18,7 +22,7 @@ export class BatchExecutionLogController {
|
||||||
start_date,
|
start_date,
|
||||||
end_date,
|
end_date,
|
||||||
page,
|
page,
|
||||||
limit
|
limit,
|
||||||
} = req.query;
|
} = req.query;
|
||||||
|
|
||||||
const filter: BatchExecutionLogFilter = {
|
const filter: BatchExecutionLogFilter = {
|
||||||
|
|
@ -27,10 +31,14 @@ export class BatchExecutionLogController {
|
||||||
start_date: start_date ? new Date(start_date as string) : undefined,
|
start_date: start_date ? new Date(start_date as string) : undefined,
|
||||||
end_date: end_date ? new Date(end_date as string) : undefined,
|
end_date: end_date ? new Date(end_date as string) : undefined,
|
||||||
page: page ? Number(page) : undefined,
|
page: page ? Number(page) : undefined,
|
||||||
limit: limit ? Number(limit) : undefined
|
limit: limit ? Number(limit) : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await BatchExecutionLogService.getExecutionLogs(filter);
|
const userCompanyCode = req.user?.companyCode;
|
||||||
|
const result = await BatchExecutionLogService.getExecutionLogs(
|
||||||
|
filter,
|
||||||
|
userCompanyCode
|
||||||
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.json(result);
|
res.json(result);
|
||||||
|
|
@ -42,7 +50,7 @@ export class BatchExecutionLogController {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 실행 로그 조회 중 오류가 발생했습니다.",
|
message: "배치 실행 로그 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +74,7 @@ export class BatchExecutionLogController {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 실행 로그 생성 중 오류가 발생했습니다.",
|
message: "배치 실행 로그 생성 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +87,10 @@ export class BatchExecutionLogController {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const data: UpdateBatchExecutionLogRequest = req.body;
|
const data: UpdateBatchExecutionLogRequest = req.body;
|
||||||
|
|
||||||
const result = await BatchExecutionLogService.updateExecutionLog(Number(id), data);
|
const result = await BatchExecutionLogService.updateExecutionLog(
|
||||||
|
Number(id),
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.json(result);
|
res.json(result);
|
||||||
|
|
@ -91,7 +102,7 @@ export class BatchExecutionLogController {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 실행 로그 업데이트 중 오류가 발생했습니다.",
|
message: "배치 실행 로그 업데이트 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +114,9 @@ export class BatchExecutionLogController {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
const result = await BatchExecutionLogService.deleteExecutionLog(Number(id));
|
const result = await BatchExecutionLogService.deleteExecutionLog(
|
||||||
|
Number(id)
|
||||||
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.json(result);
|
res.json(result);
|
||||||
|
|
@ -115,7 +128,7 @@ export class BatchExecutionLogController {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 실행 로그 삭제 중 오류가 발생했습니다.",
|
message: "배치 실행 로그 삭제 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +140,9 @@ export class BatchExecutionLogController {
|
||||||
try {
|
try {
|
||||||
const { batchConfigId } = req.params;
|
const { batchConfigId } = req.params;
|
||||||
|
|
||||||
const result = await BatchExecutionLogService.getLatestExecutionLog(Number(batchConfigId));
|
const result = await BatchExecutionLogService.getLatestExecutionLog(
|
||||||
|
Number(batchConfigId)
|
||||||
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.json(result);
|
res.json(result);
|
||||||
|
|
@ -139,7 +154,7 @@ export class BatchExecutionLogController {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "최신 배치 실행 로그 조회 중 오류가 발생했습니다.",
|
message: "최신 배치 실행 로그 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -149,11 +164,7 @@ export class BatchExecutionLogController {
|
||||||
*/
|
*/
|
||||||
static async getExecutionStats(req: AuthenticatedRequest, res: Response) {
|
static async getExecutionStats(req: AuthenticatedRequest, res: Response) {
|
||||||
try {
|
try {
|
||||||
const {
|
const { batch_config_id, start_date, end_date } = req.query;
|
||||||
batch_config_id,
|
|
||||||
start_date,
|
|
||||||
end_date
|
|
||||||
} = req.query;
|
|
||||||
|
|
||||||
const result = await BatchExecutionLogService.getExecutionStats(
|
const result = await BatchExecutionLogService.getExecutionStats(
|
||||||
batch_config_id ? Number(batch_config_id) : undefined,
|
batch_config_id ? Number(batch_config_id) : undefined,
|
||||||
|
|
@ -171,9 +182,8 @@ export class BatchExecutionLogController {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 실행 통계 조회 중 오류가 발생했습니다.",
|
message: "배치 실행 통계 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,12 @@
|
||||||
|
|
||||||
import { Response } from "express";
|
import { Response } from "express";
|
||||||
import { AuthenticatedRequest } from "../types/auth";
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
import { BatchManagementService, BatchConnectionInfo, BatchTableInfo, BatchColumnInfo } from "../services/batchManagementService";
|
import {
|
||||||
|
BatchManagementService,
|
||||||
|
BatchConnectionInfo,
|
||||||
|
BatchTableInfo,
|
||||||
|
BatchColumnInfo,
|
||||||
|
} from "../services/batchManagementService";
|
||||||
import { BatchService } from "../services/batchService";
|
import { BatchService } from "../services/batchService";
|
||||||
import { BatchSchedulerService } from "../services/batchSchedulerService";
|
import { BatchSchedulerService } from "../services/batchSchedulerService";
|
||||||
import { BatchExternalDbService } from "../services/batchExternalDbService";
|
import { BatchExternalDbService } from "../services/batchExternalDbService";
|
||||||
|
|
@ -11,11 +16,16 @@ import { CreateBatchConfigRequest, BatchConfig } from "../types/batchTypes";
|
||||||
|
|
||||||
export class BatchManagementController {
|
export class BatchManagementController {
|
||||||
/**
|
/**
|
||||||
* 사용 가능한 커넥션 목록 조회
|
* 사용 가능한 커넥션 목록 조회 (회사별)
|
||||||
*/
|
*/
|
||||||
static async getAvailableConnections(req: AuthenticatedRequest, res: Response) {
|
static async getAvailableConnections(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const result = await BatchManagementService.getAvailableConnections();
|
const userCompanyCode = req.user?.companyCode;
|
||||||
|
const result =
|
||||||
|
await BatchManagementService.getAvailableConnections(userCompanyCode);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -26,27 +36,35 @@ export class BatchManagementController {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "커넥션 목록 조회 중 오류가 발생했습니다.",
|
message: "커넥션 목록 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 커넥션의 테이블 목록 조회
|
* 특정 커넥션의 테이블 목록 조회 (회사별)
|
||||||
*/
|
*/
|
||||||
static async getTablesFromConnection(req: AuthenticatedRequest, res: Response) {
|
static async getTablesFromConnection(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const { type, id } = req.params;
|
const { type, id } = req.params;
|
||||||
|
const userCompanyCode = req.user?.companyCode;
|
||||||
|
|
||||||
if (type !== 'internal' && type !== 'external') {
|
if (type !== "internal" && type !== "external") {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)"
|
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectionId = type === 'external' ? Number(id) : undefined;
|
const connectionId = type === "external" ? Number(id) : undefined;
|
||||||
const result = await BatchManagementService.getTablesFromConnection(type, connectionId);
|
const result = await BatchManagementService.getTablesFromConnection(
|
||||||
|
type,
|
||||||
|
connectionId,
|
||||||
|
userCompanyCode
|
||||||
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return res.json(result);
|
return res.json(result);
|
||||||
|
|
@ -58,27 +76,33 @@ export class BatchManagementController {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "테이블 목록 조회 중 오류가 발생했습니다.",
|
message: "테이블 목록 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 테이블의 컬럼 정보 조회
|
* 특정 테이블의 컬럼 정보 조회 (회사별)
|
||||||
*/
|
*/
|
||||||
static async getTableColumns(req: AuthenticatedRequest, res: Response) {
|
static async getTableColumns(req: AuthenticatedRequest, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { type, id, tableName } = req.params;
|
const { type, id, tableName } = req.params;
|
||||||
|
const userCompanyCode = req.user?.companyCode;
|
||||||
|
|
||||||
if (type !== 'internal' && type !== 'external') {
|
if (type !== "internal" && type !== "external") {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)"
|
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectionId = type === 'external' ? Number(id) : undefined;
|
const connectionId = type === "external" ? Number(id) : undefined;
|
||||||
const result = await BatchManagementService.getTableColumns(type, connectionId, tableName);
|
const result = await BatchManagementService.getTableColumns(
|
||||||
|
type,
|
||||||
|
connectionId,
|
||||||
|
tableName,
|
||||||
|
userCompanyCode
|
||||||
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return res.json(result);
|
return res.json(result);
|
||||||
|
|
@ -90,7 +114,7 @@ export class BatchManagementController {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
|
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -101,12 +125,19 @@ export class BatchManagementController {
|
||||||
*/
|
*/
|
||||||
static async createBatchConfig(req: AuthenticatedRequest, res: Response) {
|
static async createBatchConfig(req: AuthenticatedRequest, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { batchName, description, cronSchedule, mappings, isActive } = req.body;
|
const { batchName, description, cronSchedule, mappings, isActive } =
|
||||||
|
req.body;
|
||||||
|
|
||||||
if (!batchName || !cronSchedule || !mappings || !Array.isArray(mappings)) {
|
if (
|
||||||
|
!batchName ||
|
||||||
|
!cronSchedule ||
|
||||||
|
!mappings ||
|
||||||
|
!Array.isArray(mappings)
|
||||||
|
) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "필수 필드가 누락되었습니다. (batchName, cronSchedule, mappings)"
|
message:
|
||||||
|
"필수 필드가 누락되었습니다. (batchName, cronSchedule, mappings)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,20 +146,20 @@ export class BatchManagementController {
|
||||||
description,
|
description,
|
||||||
cronSchedule,
|
cronSchedule,
|
||||||
mappings,
|
mappings,
|
||||||
isActive: isActive !== undefined ? isActive : true
|
isActive: isActive !== undefined ? isActive : true,
|
||||||
} as CreateBatchConfigRequest);
|
} as CreateBatchConfigRequest);
|
||||||
|
|
||||||
return res.status(201).json({
|
return res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: batchConfig,
|
data: batchConfig,
|
||||||
message: "배치 설정이 성공적으로 생성되었습니다."
|
message: "배치 설정이 성공적으로 생성되었습니다.",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("배치 설정 생성 오류:", error);
|
console.error("배치 설정 생성 오류:", error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 설정 생성에 실패했습니다.",
|
message: "배치 설정 생성에 실패했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -147,7 +178,7 @@ export class BatchManagementController {
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: result.message || "배치 설정을 찾을 수 없습니다."
|
message: result.message || "배치 설정을 찾을 수 없습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,14 +186,14 @@ export class BatchManagementController {
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: result.data
|
data: result.data,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ 배치 설정 조회 오류:", error);
|
console.error("❌ 배치 설정 조회 오류:", error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 설정 조회에 실패했습니다.",
|
message: "배치 설정 조회에 실패했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -179,7 +210,7 @@ export class BatchManagementController {
|
||||||
page: Number(page),
|
page: Number(page),
|
||||||
limit: Number(limit),
|
limit: Number(limit),
|
||||||
search: search as string,
|
search: search as string,
|
||||||
is_active: isActive as string
|
is_active: isActive as string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await BatchService.getBatchConfigs(filter);
|
const result = await BatchService.getBatchConfigs(filter);
|
||||||
|
|
@ -187,14 +218,14 @@ export class BatchManagementController {
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: result.data,
|
data: result.data,
|
||||||
pagination: result.pagination
|
pagination: result.pagination,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("배치 설정 목록 조회 오류:", error);
|
console.error("배치 설정 목록 조회 오류:", error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 설정 목록 조회에 실패했습니다.",
|
message: "배치 설정 목록 조회에 실패했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -210,16 +241,18 @@ export class BatchManagementController {
|
||||||
if (!id || isNaN(Number(id))) {
|
if (!id || isNaN(Number(id))) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "올바른 배치 설정 ID를 제공해주세요."
|
message: "올바른 배치 설정 ID를 제공해주세요.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 배치 설정 조회
|
// 배치 설정 조회
|
||||||
const batchConfigResult = await BatchService.getBatchConfigById(Number(id));
|
const batchConfigResult = await BatchService.getBatchConfigById(
|
||||||
|
Number(id)
|
||||||
|
);
|
||||||
if (!batchConfigResult.success || !batchConfigResult.data) {
|
if (!batchConfigResult.success || !batchConfigResult.data) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 설정을 찾을 수 없습니다."
|
message: "배치 설정을 찾을 수 없습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -234,20 +267,23 @@ export class BatchManagementController {
|
||||||
// 실행 로그 생성
|
// 실행 로그 생성
|
||||||
executionLog = await BatchService.createExecutionLog({
|
executionLog = await BatchService.createExecutionLog({
|
||||||
batch_config_id: Number(id),
|
batch_config_id: Number(id),
|
||||||
execution_status: 'RUNNING',
|
execution_status: "RUNNING",
|
||||||
start_time: startTime,
|
start_time: startTime,
|
||||||
total_records: 0,
|
total_records: 0,
|
||||||
success_records: 0,
|
success_records: 0,
|
||||||
failed_records: 0
|
failed_records: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// BatchSchedulerService의 executeBatchConfig 메서드 사용 (중복 로직 제거)
|
// BatchSchedulerService의 executeBatchConfig 메서드 사용 (중복 로직 제거)
|
||||||
const { BatchSchedulerService } = await import('../services/batchSchedulerService');
|
const { BatchSchedulerService } = await import(
|
||||||
const result = await BatchSchedulerService.executeBatchConfig(batchConfig);
|
"../services/batchSchedulerService"
|
||||||
|
);
|
||||||
|
const result =
|
||||||
|
await BatchSchedulerService.executeBatchConfig(batchConfig);
|
||||||
|
|
||||||
// result가 undefined인 경우 처리
|
// result가 undefined인 경우 처리
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error('배치 실행 결과를 받을 수 없습니다.');
|
throw new Error("배치 실행 결과를 받을 수 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const endTime = new Date();
|
const endTime = new Date();
|
||||||
|
|
@ -255,12 +291,12 @@ export class BatchManagementController {
|
||||||
|
|
||||||
// 실행 로그 업데이트 (성공)
|
// 실행 로그 업데이트 (성공)
|
||||||
await BatchService.updateExecutionLog(executionLog.id, {
|
await BatchService.updateExecutionLog(executionLog.id, {
|
||||||
execution_status: 'SUCCESS',
|
execution_status: "SUCCESS",
|
||||||
end_time: endTime,
|
end_time: endTime,
|
||||||
duration_ms: duration,
|
duration_ms: duration,
|
||||||
total_records: result.totalRecords,
|
total_records: result.totalRecords,
|
||||||
success_records: result.successRecords,
|
success_records: result.successRecords,
|
||||||
failed_records: result.failedRecords
|
failed_records: result.failedRecords,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
@ -270,11 +306,10 @@ export class BatchManagementController {
|
||||||
totalRecords: result.totalRecords,
|
totalRecords: result.totalRecords,
|
||||||
successRecords: result.successRecords,
|
successRecords: result.successRecords,
|
||||||
failedRecords: result.failedRecords,
|
failedRecords: result.failedRecords,
|
||||||
executionTime: duration
|
executionTime: duration,
|
||||||
},
|
},
|
||||||
message: "배치가 성공적으로 실행되었습니다."
|
message: "배치가 성공적으로 실행되었습니다.",
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (batchError) {
|
} catch (batchError) {
|
||||||
console.error(`배치 실행 실패: ${batchConfig.batch_name}`, batchError);
|
console.error(`배치 실행 실패: ${batchConfig.batch_name}`, batchError);
|
||||||
|
|
||||||
|
|
@ -284,31 +319,36 @@ export class BatchManagementController {
|
||||||
const duration = endTime.getTime() - startTime.getTime();
|
const duration = endTime.getTime() - startTime.getTime();
|
||||||
|
|
||||||
// executionLog가 정의되어 있는지 확인
|
// executionLog가 정의되어 있는지 확인
|
||||||
if (typeof executionLog !== 'undefined') {
|
if (typeof executionLog !== "undefined") {
|
||||||
await BatchService.updateExecutionLog(executionLog.id, {
|
await BatchService.updateExecutionLog(executionLog.id, {
|
||||||
execution_status: 'FAILED',
|
execution_status: "FAILED",
|
||||||
end_time: endTime,
|
end_time: endTime,
|
||||||
duration_ms: duration,
|
duration_ms: duration,
|
||||||
error_message: batchError instanceof Error ? batchError.message : "알 수 없는 오류"
|
error_message:
|
||||||
|
batchError instanceof Error
|
||||||
|
? batchError.message
|
||||||
|
: "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (logError) {
|
} catch (logError) {
|
||||||
console.error('실행 로그 업데이트 실패:', logError);
|
console.error("실행 로그 업데이트 실패:", logError);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 실행에 실패했습니다.",
|
message: "배치 실행에 실패했습니다.",
|
||||||
error: batchError instanceof Error ? batchError.message : "알 수 없는 오류"
|
error:
|
||||||
|
batchError instanceof Error
|
||||||
|
? batchError.message
|
||||||
|
: "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`배치 실행 오류 (ID: ${req.params.id}):`, error);
|
console.error(`배치 실행 오류 (ID: ${req.params.id}):`, error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 실행 중 오류가 발생했습니다.",
|
message: "배치 실행 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "Unknown error"
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -325,11 +365,14 @@ export class BatchManagementController {
|
||||||
if (!id || isNaN(Number(id))) {
|
if (!id || isNaN(Number(id))) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "올바른 배치 설정 ID를 제공해주세요."
|
message: "올바른 배치 설정 ID를 제공해주세요.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const batchConfig = await BatchService.updateBatchConfig(Number(id), updateData);
|
const batchConfig = await BatchService.updateBatchConfig(
|
||||||
|
Number(id),
|
||||||
|
updateData
|
||||||
|
);
|
||||||
|
|
||||||
// 스케줄러에서 배치 스케줄 업데이트 (즉시 실행 비활성화)
|
// 스케줄러에서 배치 스케줄 업데이트 (즉시 실행 비활성화)
|
||||||
await BatchSchedulerService.updateBatchSchedule(Number(id), false);
|
await BatchSchedulerService.updateBatchSchedule(Number(id), false);
|
||||||
|
|
@ -337,14 +380,14 @@ export class BatchManagementController {
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: batchConfig,
|
data: batchConfig,
|
||||||
message: "배치 설정이 성공적으로 업데이트되었습니다."
|
message: "배치 설정이 성공적으로 업데이트되었습니다.",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("배치 설정 업데이트 오류:", error);
|
console.error("배치 설정 업데이트 오류:", error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 설정 업데이트에 실패했습니다.",
|
message: "배치 설정 업데이트에 실패했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -358,17 +401,17 @@ export class BatchManagementController {
|
||||||
apiUrl,
|
apiUrl,
|
||||||
apiKey,
|
apiKey,
|
||||||
endpoint,
|
endpoint,
|
||||||
method = 'GET',
|
method = "GET",
|
||||||
paramType,
|
paramType,
|
||||||
paramName,
|
paramName,
|
||||||
paramValue,
|
paramValue,
|
||||||
paramSource
|
paramSource,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!apiUrl || !apiKey || !endpoint) {
|
if (!apiUrl || !apiKey || !endpoint) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "API URL, API Key, 엔드포인트는 필수입니다."
|
message: "API URL, API Key, 엔드포인트는 필수입니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -378,16 +421,16 @@ export class BatchManagementController {
|
||||||
paramType,
|
paramType,
|
||||||
paramName,
|
paramName,
|
||||||
paramValue,
|
paramValue,
|
||||||
paramSource
|
paramSource,
|
||||||
});
|
});
|
||||||
|
|
||||||
// RestApiConnector 사용하여 데이터 조회
|
// RestApiConnector 사용하여 데이터 조회
|
||||||
const { RestApiConnector } = await import('../database/RestApiConnector');
|
const { RestApiConnector } = await import("../database/RestApiConnector");
|
||||||
|
|
||||||
const connector = new RestApiConnector({
|
const connector = new RestApiConnector({
|
||||||
baseUrl: apiUrl,
|
baseUrl: apiUrl,
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
timeout: 30000
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 연결 테스트
|
// 연결 테스트
|
||||||
|
|
@ -396,7 +439,7 @@ export class BatchManagementController {
|
||||||
// 파라미터가 있는 경우 엔드포인트 수정
|
// 파라미터가 있는 경우 엔드포인트 수정
|
||||||
let finalEndpoint = endpoint;
|
let finalEndpoint = endpoint;
|
||||||
if (paramType && paramName && paramValue) {
|
if (paramType && paramName && paramValue) {
|
||||||
if (paramType === 'url') {
|
if (paramType === "url") {
|
||||||
// URL 파라미터: /api/users/{userId} → /api/users/123
|
// URL 파라미터: /api/users/{userId} → /api/users/123
|
||||||
if (endpoint.includes(`{${paramName}}`)) {
|
if (endpoint.includes(`{${paramName}}`)) {
|
||||||
finalEndpoint = endpoint.replace(`{${paramName}}`, paramValue);
|
finalEndpoint = endpoint.replace(`{${paramName}}`, paramValue);
|
||||||
|
|
@ -404,9 +447,9 @@ export class BatchManagementController {
|
||||||
// 엔드포인트에 {paramName}이 없으면 뒤에 추가
|
// 엔드포인트에 {paramName}이 없으면 뒤에 추가
|
||||||
finalEndpoint = `${endpoint}/${paramValue}`;
|
finalEndpoint = `${endpoint}/${paramValue}`;
|
||||||
}
|
}
|
||||||
} else if (paramType === 'query') {
|
} else if (paramType === "query") {
|
||||||
// 쿼리 파라미터: /api/users?userId=123
|
// 쿼리 파라미터: /api/users?userId=123
|
||||||
const separator = endpoint.includes('?') ? '&' : '?';
|
const separator = endpoint.includes("?") ? "&" : "?";
|
||||||
finalEndpoint = `${endpoint}${separator}${paramName}=${paramValue}`;
|
finalEndpoint = `${endpoint}${separator}${paramName}=${paramValue}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -417,8 +460,9 @@ export class BatchManagementController {
|
||||||
const result = await connector.executeQuery(finalEndpoint, method);
|
const result = await connector.executeQuery(finalEndpoint, method);
|
||||||
console.log(`[previewRestApiData] executeQuery 결과:`, {
|
console.log(`[previewRestApiData] executeQuery 결과:`, {
|
||||||
rowCount: result.rowCount,
|
rowCount: result.rowCount,
|
||||||
rowsLength: result.rows ? result.rows.length : 'undefined',
|
rowsLength: result.rows ? result.rows.length : "undefined",
|
||||||
firstRow: result.rows && result.rows.length > 0 ? result.rows[0] : 'no data'
|
firstRow:
|
||||||
|
result.rows && result.rows.length > 0 ? result.rows[0] : "no data",
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = result.rows.slice(0, 5); // 최대 5개 샘플만
|
const data = result.rows.slice(0, 5); // 최대 5개 샘플만
|
||||||
|
|
@ -434,9 +478,9 @@ export class BatchManagementController {
|
||||||
data: {
|
data: {
|
||||||
fields: fields,
|
fields: fields,
|
||||||
samples: data,
|
samples: data,
|
||||||
totalCount: result.rowCount || data.length
|
totalCount: result.rowCount || data.length,
|
||||||
},
|
},
|
||||||
message: `${fields.length}개 필드, ${result.rowCount || data.length}개 레코드를 조회했습니다.`
|
message: `${fields.length}개 필드, ${result.rowCount || data.length}개 레코드를 조회했습니다.`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
@ -444,9 +488,9 @@ export class BatchManagementController {
|
||||||
data: {
|
data: {
|
||||||
fields: [],
|
fields: [],
|
||||||
samples: [],
|
samples: [],
|
||||||
totalCount: 0
|
totalCount: 0,
|
||||||
},
|
},
|
||||||
message: "API에서 데이터를 가져올 수 없습니다."
|
message: "API에서 데이터를 가져올 수 없습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -454,7 +498,7 @@ export class BatchManagementController {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "REST API 데이터 미리보기 중 오류가 발생했습니다.",
|
message: "REST API 데이터 미리보기 중 오류가 발생했습니다.",
|
||||||
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -464,18 +508,19 @@ export class BatchManagementController {
|
||||||
*/
|
*/
|
||||||
static async saveRestApiBatch(req: AuthenticatedRequest, res: Response) {
|
static async saveRestApiBatch(req: AuthenticatedRequest, res: Response) {
|
||||||
try {
|
try {
|
||||||
const {
|
const { batchName, batchType, cronSchedule, description, apiMappings } =
|
||||||
batchName,
|
req.body;
|
||||||
batchType,
|
|
||||||
cronSchedule,
|
|
||||||
description,
|
|
||||||
apiMappings
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
if (!batchName || !batchType || !cronSchedule || !apiMappings || apiMappings.length === 0) {
|
if (
|
||||||
|
!batchName ||
|
||||||
|
!batchType ||
|
||||||
|
!cronSchedule ||
|
||||||
|
!apiMappings ||
|
||||||
|
apiMappings.length === 0
|
||||||
|
) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "필수 필드가 누락되었습니다."
|
message: "필수 필드가 누락되었습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -484,15 +529,15 @@ export class BatchManagementController {
|
||||||
batchType,
|
batchType,
|
||||||
cronSchedule,
|
cronSchedule,
|
||||||
description,
|
description,
|
||||||
apiMappings
|
apiMappings,
|
||||||
});
|
});
|
||||||
|
|
||||||
// BatchService를 사용하여 배치 설정 저장
|
// BatchService를 사용하여 배치 설정 저장
|
||||||
const batchConfig: CreateBatchConfigRequest = {
|
const batchConfig: CreateBatchConfigRequest = {
|
||||||
batchName: batchName,
|
batchName: batchName,
|
||||||
description: description || '',
|
description: description || "",
|
||||||
cronSchedule: cronSchedule,
|
cronSchedule: cronSchedule,
|
||||||
mappings: apiMappings
|
mappings: apiMappings,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await BatchService.createBatchConfig(batchConfig);
|
const result = await BatchService.createBatchConfig(batchConfig);
|
||||||
|
|
@ -501,7 +546,9 @@ export class BatchManagementController {
|
||||||
// 스케줄러에 자동 등록 ✅
|
// 스케줄러에 자동 등록 ✅
|
||||||
try {
|
try {
|
||||||
await BatchSchedulerService.scheduleBatchConfig(result.data);
|
await BatchSchedulerService.scheduleBatchConfig(result.data);
|
||||||
console.log(`✅ 새로운 배치가 스케줄러에 등록되었습니다: ${batchName} (ID: ${result.data.id})`);
|
console.log(
|
||||||
|
`✅ 새로운 배치가 스케줄러에 등록되었습니다: ${batchName} (ID: ${result.data.id})`
|
||||||
|
);
|
||||||
} catch (schedulerError) {
|
} catch (schedulerError) {
|
||||||
console.error(`❌ 스케줄러 등록 실패: ${batchName}`, schedulerError);
|
console.error(`❌ 스케줄러 등록 실패: ${batchName}`, schedulerError);
|
||||||
// 스케줄러 등록 실패해도 배치 저장은 성공으로 처리
|
// 스케줄러 등록 실패해도 배치 저장은 성공으로 처리
|
||||||
|
|
@ -510,19 +557,19 @@ export class BatchManagementController {
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "REST API 배치가 성공적으로 저장되었습니다.",
|
message: "REST API 배치가 성공적으로 저장되었습니다.",
|
||||||
data: result.data
|
data: result.data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: result.message || "배치 저장에 실패했습니다."
|
message: result.message || "배치 저장에 실패했습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("REST API 배치 저장 오류:", error);
|
console.error("REST API 배치 저장 오류:", error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 저장 중 오류가 발생했습니다."
|
message: "배치 저장 중 오류가 발생했습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -461,12 +461,13 @@ 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) {
|
||||||
|
|
@ -488,7 +489,8 @@ 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({
|
||||||
|
|
@ -511,13 +513,14 @@ 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) {
|
||||||
|
|
@ -540,7 +543,8 @@ 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({
|
||||||
|
|
|
||||||
|
|
@ -557,9 +557,14 @@ export class FlowController {
|
||||||
getStepColumnLabels = async (req: Request, res: Response): Promise<void> => {
|
getStepColumnLabels = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { flowId, stepId } = req.params;
|
const { flowId, stepId } = req.params;
|
||||||
|
console.log("🏷️ [FlowController] 컬럼 라벨 조회 요청:", {
|
||||||
|
flowId,
|
||||||
|
stepId,
|
||||||
|
});
|
||||||
|
|
||||||
const step = await this.flowStepService.getById(parseInt(stepId));
|
const step = await this.flowStepService.findById(parseInt(stepId));
|
||||||
if (!step) {
|
if (!step) {
|
||||||
|
console.warn("⚠️ [FlowController] 스텝을 찾을 수 없음:", stepId);
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Step not found",
|
message: "Step not found",
|
||||||
|
|
@ -567,10 +572,11 @@ export class FlowController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const flowDef = await this.flowDefinitionService.getById(
|
const flowDef = await this.flowDefinitionService.findById(
|
||||||
parseInt(flowId)
|
parseInt(flowId)
|
||||||
);
|
);
|
||||||
if (!flowDef) {
|
if (!flowDef) {
|
||||||
|
console.warn("⚠️ [FlowController] 플로우를 찾을 수 없음:", flowId);
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Flow definition not found",
|
message: "Flow definition not found",
|
||||||
|
|
@ -580,7 +586,14 @@ export class FlowController {
|
||||||
|
|
||||||
// 테이블명 결정 (스텝 테이블 우선, 없으면 플로우 테이블)
|
// 테이블명 결정 (스텝 테이블 우선, 없으면 플로우 테이블)
|
||||||
const tableName = step.tableName || flowDef.tableName;
|
const tableName = step.tableName || flowDef.tableName;
|
||||||
|
console.log("📋 [FlowController] 테이블명 결정:", {
|
||||||
|
stepTableName: step.tableName,
|
||||||
|
flowTableName: flowDef.tableName,
|
||||||
|
selectedTableName: tableName,
|
||||||
|
});
|
||||||
|
|
||||||
if (!tableName) {
|
if (!tableName) {
|
||||||
|
console.warn("⚠️ [FlowController] 테이블명이 지정되지 않음");
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {},
|
data: {},
|
||||||
|
|
@ -589,7 +602,7 @@ export class FlowController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// column_labels 테이블에서 라벨 정보 조회
|
// column_labels 테이블에서 라벨 정보 조회
|
||||||
const { query } = await import("../config/database");
|
const { query } = await import("../database/db");
|
||||||
const labelRows = await query<{
|
const labelRows = await query<{
|
||||||
column_name: string;
|
column_name: string;
|
||||||
column_label: string | null;
|
column_label: string | null;
|
||||||
|
|
@ -600,6 +613,15 @@ export class FlowController {
|
||||||
[tableName]
|
[tableName]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(`✅ [FlowController] column_labels 조회 완료:`, {
|
||||||
|
tableName,
|
||||||
|
rowCount: labelRows.length,
|
||||||
|
labels: labelRows.map((r) => ({
|
||||||
|
col: r.column_name,
|
||||||
|
label: r.column_label,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
// { columnName: label } 형태의 객체로 변환
|
// { columnName: label } 형태의 객체로 변환
|
||||||
const labels: Record<string, string> = {};
|
const labels: Record<string, string> = {};
|
||||||
labelRows.forEach((row) => {
|
labelRows.forEach((row) => {
|
||||||
|
|
@ -608,12 +630,14 @@ export class FlowController {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("📦 [FlowController] 반환할 라벨 객체:", labels);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: labels,
|
data: labels,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error getting step column labels:", error);
|
console.error("❌ [FlowController] 컬럼 라벨 조회 오류:", error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: error.message || "Failed to get step column labels",
|
message: error.message || "Failed to get step column labels",
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,10 @@ router.get(
|
||||||
filter,
|
filter,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await ExternalDbConnectionService.getConnections(filter);
|
const result = await ExternalDbConnectionService.getConnections(
|
||||||
|
filter,
|
||||||
|
userCompanyCode
|
||||||
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return res.status(200).json(result);
|
return res.status(200).json(result);
|
||||||
|
|
@ -319,7 +322,12 @@ router.delete(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await ExternalDbConnectionService.deleteConnection(id);
|
const userCompanyCode = req.user?.companyCode;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
@ -517,7 +525,10 @@ router.get(
|
||||||
});
|
});
|
||||||
|
|
||||||
const externalConnections =
|
const externalConnections =
|
||||||
await ExternalDbConnectionService.getConnections(filter);
|
await ExternalDbConnectionService.getConnections(
|
||||||
|
filter,
|
||||||
|
userCompanyCode
|
||||||
|
);
|
||||||
|
|
||||||
if (!externalConnections.success) {
|
if (!externalConnections.success) {
|
||||||
return res.status(400).json(externalConnections);
|
return res.status(400).json(externalConnections);
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,12 @@ router.get(
|
||||||
company_code: req.query.company_code as string,
|
company_code: req.query.company_code as string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result =
|
const userCompanyCode = req.user?.companyCode;
|
||||||
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) {
|
||||||
|
|
@ -62,8 +66,12 @@ router.get(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result =
|
const userCompanyCode = req.user?.companyCode;
|
||||||
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) {
|
||||||
|
|
@ -129,9 +137,12 @@ 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);
|
||||||
|
|
@ -164,8 +175,12 @@ router.delete(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result =
|
const userCompanyCode = req.user?.companyCode;
|
||||||
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) {
|
||||||
|
|
|
||||||
|
|
@ -1,554 +0,0 @@
|
||||||
import { query } from "../database/db";
|
|
||||||
import { logger } from "../utils/logger";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 권한 그룹 인터페이스
|
|
||||||
*/
|
|
||||||
export interface RoleGroup {
|
|
||||||
objid: number;
|
|
||||||
authName: string;
|
|
||||||
authCode: string;
|
|
||||||
companyCode: string;
|
|
||||||
status: string;
|
|
||||||
writer: string;
|
|
||||||
regdate: Date;
|
|
||||||
memberCount?: number;
|
|
||||||
menuCount?: number;
|
|
||||||
memberNames?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 권한 그룹 멤버 인터페이스
|
|
||||||
*/
|
|
||||||
export interface RoleMember {
|
|
||||||
objid: number;
|
|
||||||
masterObjid: number;
|
|
||||||
userId: string;
|
|
||||||
userName?: string;
|
|
||||||
deptName?: string;
|
|
||||||
positionName?: string;
|
|
||||||
writer: string;
|
|
||||||
regdate: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 메뉴 권한 인터페이스
|
|
||||||
*/
|
|
||||||
export interface MenuPermission {
|
|
||||||
objid: number;
|
|
||||||
menuObjid: number;
|
|
||||||
authObjid: number;
|
|
||||||
menuName?: string;
|
|
||||||
createYn: string;
|
|
||||||
readYn: string;
|
|
||||||
updateYn: string;
|
|
||||||
deleteYn: string;
|
|
||||||
writer: string;
|
|
||||||
regdate: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 권한 그룹 서비스
|
|
||||||
*/
|
|
||||||
export class RoleService {
|
|
||||||
/**
|
|
||||||
* 회사별 권한 그룹 목록 조회
|
|
||||||
* @param companyCode - 회사 코드 (undefined 시 전체 조회)
|
|
||||||
* @param search - 검색어
|
|
||||||
*/
|
|
||||||
static async getRoleGroups(
|
|
||||||
companyCode?: string,
|
|
||||||
search?: string
|
|
||||||
): Promise<RoleGroup[]> {
|
|
||||||
try {
|
|
||||||
let sql = `
|
|
||||||
SELECT
|
|
||||||
objid,
|
|
||||||
auth_name AS "authName",
|
|
||||||
auth_code AS "authCode",
|
|
||||||
company_code AS "companyCode",
|
|
||||||
status,
|
|
||||||
writer,
|
|
||||||
regdate,
|
|
||||||
(SELECT COUNT(*) FROM authority_sub_user asu WHERE asu.master_objid = am.objid) AS "memberCount",
|
|
||||||
(SELECT COUNT(*) FROM rel_menu_auth rma WHERE rma.auth_objid = am.objid) AS "menuCount",
|
|
||||||
(SELECT STRING_AGG(ui.user_name, ', ' ORDER BY ui.user_name)
|
|
||||||
FROM authority_sub_user asu
|
|
||||||
JOIN user_info ui ON asu.user_id = ui.user_id
|
|
||||||
WHERE asu.master_objid = am.objid) AS "memberNames"
|
|
||||||
FROM authority_master am
|
|
||||||
WHERE 1=1
|
|
||||||
`;
|
|
||||||
|
|
||||||
const params: any[] = [];
|
|
||||||
let paramIndex = 1;
|
|
||||||
|
|
||||||
// 회사 코드 필터 (companyCode가 undefined면 전체 조회)
|
|
||||||
if (companyCode) {
|
|
||||||
sql += ` AND company_code = $${paramIndex}`;
|
|
||||||
params.push(companyCode);
|
|
||||||
paramIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 검색어 필터
|
|
||||||
if (search && search.trim()) {
|
|
||||||
sql += ` AND (auth_name ILIKE $${paramIndex} OR auth_code ILIKE $${paramIndex})`;
|
|
||||||
params.push(`%${search.trim()}%`);
|
|
||||||
paramIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
sql += ` ORDER BY regdate DESC`;
|
|
||||||
|
|
||||||
logger.info("권한 그룹 조회 SQL", { sql, params });
|
|
||||||
const result = await query<RoleGroup>(sql, params);
|
|
||||||
logger.info("권한 그룹 조회 결과", { count: result.length });
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("권한 그룹 목록 조회 실패", { error, companyCode, search });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 권한 그룹 상세 조회
|
|
||||||
*/
|
|
||||||
static async getRoleGroupById(objid: number): Promise<RoleGroup | null> {
|
|
||||||
try {
|
|
||||||
const sql = `
|
|
||||||
SELECT
|
|
||||||
objid,
|
|
||||||
auth_name AS "authName",
|
|
||||||
auth_code AS "authCode",
|
|
||||||
company_code AS "companyCode",
|
|
||||||
status,
|
|
||||||
writer,
|
|
||||||
regdate
|
|
||||||
FROM authority_master
|
|
||||||
WHERE objid = $1
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = await query<RoleGroup>(sql, [objid]);
|
|
||||||
return result.length > 0 ? result[0] : null;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("권한 그룹 상세 조회 실패", { error, objid });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 권한 그룹 생성
|
|
||||||
*/
|
|
||||||
static async createRoleGroup(data: {
|
|
||||||
authName: string;
|
|
||||||
authCode: string;
|
|
||||||
companyCode: string;
|
|
||||||
writer: string;
|
|
||||||
}): Promise<RoleGroup> {
|
|
||||||
try {
|
|
||||||
const sql = `
|
|
||||||
INSERT INTO authority_master (objid, auth_name, auth_code, company_code, status, writer, regdate)
|
|
||||||
VALUES (nextval('seq_authority_master'), $1, $2, $3, 'active', $4, NOW())
|
|
||||||
RETURNING objid, auth_name AS "authName", auth_code AS "authCode",
|
|
||||||
company_code AS "companyCode", status, writer, regdate
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = await query<RoleGroup>(sql, [
|
|
||||||
data.authName,
|
|
||||||
data.authCode,
|
|
||||||
data.companyCode,
|
|
||||||
data.writer,
|
|
||||||
]);
|
|
||||||
|
|
||||||
logger.info("권한 그룹 생성 성공", {
|
|
||||||
objid: result[0].objid,
|
|
||||||
authName: data.authName,
|
|
||||||
});
|
|
||||||
return result[0];
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("권한 그룹 생성 실패", { error, data });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 권한 그룹 수정
|
|
||||||
*/
|
|
||||||
static async updateRoleGroup(
|
|
||||||
objid: number,
|
|
||||||
data: {
|
|
||||||
authName?: string;
|
|
||||||
authCode?: string;
|
|
||||||
status?: string;
|
|
||||||
}
|
|
||||||
): Promise<RoleGroup> {
|
|
||||||
try {
|
|
||||||
const updates: string[] = [];
|
|
||||||
const params: any[] = [];
|
|
||||||
let paramIndex = 1;
|
|
||||||
|
|
||||||
if (data.authName !== undefined) {
|
|
||||||
updates.push(`auth_name = $${paramIndex}`);
|
|
||||||
params.push(data.authName);
|
|
||||||
paramIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.authCode !== undefined) {
|
|
||||||
updates.push(`auth_code = $${paramIndex}`);
|
|
||||||
params.push(data.authCode);
|
|
||||||
paramIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.status !== undefined) {
|
|
||||||
updates.push(`status = $${paramIndex}`);
|
|
||||||
params.push(data.status);
|
|
||||||
paramIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updates.length === 0) {
|
|
||||||
throw new Error("수정할 데이터가 없습니다");
|
|
||||||
}
|
|
||||||
|
|
||||||
params.push(objid);
|
|
||||||
|
|
||||||
const sql = `
|
|
||||||
UPDATE authority_master
|
|
||||||
SET ${updates.join(", ")}
|
|
||||||
WHERE objid = $${paramIndex}
|
|
||||||
RETURNING objid, auth_name AS "authName", auth_code AS "authCode",
|
|
||||||
company_code AS "companyCode", status, writer, regdate
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = await query<RoleGroup>(sql, params);
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
|
||||||
throw new Error("권한 그룹을 찾을 수 없습니다");
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("권한 그룹 수정 성공", { objid, updates });
|
|
||||||
return result[0];
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("권한 그룹 수정 실패", { error, objid, data });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 권한 그룹 삭제
|
|
||||||
*/
|
|
||||||
static async deleteRoleGroup(objid: number): Promise<void> {
|
|
||||||
try {
|
|
||||||
// CASCADE로 연결된 데이터도 함께 삭제됨 (authority_sub_user, rel_menu_auth)
|
|
||||||
await query("DELETE FROM authority_master WHERE objid = $1", [objid]);
|
|
||||||
logger.info("권한 그룹 삭제 성공", { objid });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("권한 그룹 삭제 실패", { error, objid });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 권한 그룹 멤버 목록 조회
|
|
||||||
*/
|
|
||||||
static async getRoleMembers(masterObjid: number): Promise<RoleMember[]> {
|
|
||||||
try {
|
|
||||||
const sql = `
|
|
||||||
SELECT
|
|
||||||
asu.objid,
|
|
||||||
asu.master_objid AS "masterObjid",
|
|
||||||
asu.user_id AS "userId",
|
|
||||||
ui.user_name AS "userName",
|
|
||||||
ui.dept_name AS "deptName",
|
|
||||||
ui.position_name AS "positionName",
|
|
||||||
asu.writer,
|
|
||||||
asu.regdate
|
|
||||||
FROM authority_sub_user asu
|
|
||||||
JOIN user_info ui ON asu.user_id = ui.user_id
|
|
||||||
WHERE asu.master_objid = $1
|
|
||||||
ORDER BY ui.user_name
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = await query<RoleMember>(sql, [masterObjid]);
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("권한 그룹 멤버 조회 실패", { error, masterObjid });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 권한 그룹 멤버 추가 (여러 명)
|
|
||||||
*/
|
|
||||||
static async addRoleMembers(
|
|
||||||
masterObjid: number,
|
|
||||||
userIds: string[],
|
|
||||||
writer: string
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
// 이미 존재하는 멤버 제외
|
|
||||||
const existingSql = `
|
|
||||||
SELECT user_id
|
|
||||||
FROM authority_sub_user
|
|
||||||
WHERE master_objid = $1 AND user_id = ANY($2)
|
|
||||||
`;
|
|
||||||
const existing = await query<{ user_id: string }>(existingSql, [
|
|
||||||
masterObjid,
|
|
||||||
userIds,
|
|
||||||
]);
|
|
||||||
const existingIds = new Set(
|
|
||||||
existing.map((row: { user_id: string }) => row.user_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
const newUserIds = userIds.filter((userId) => !existingIds.has(userId));
|
|
||||||
|
|
||||||
if (newUserIds.length === 0) {
|
|
||||||
logger.info("추가할 신규 멤버가 없습니다", { masterObjid, userIds });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 배치 삽입
|
|
||||||
const values = newUserIds
|
|
||||||
.map(
|
|
||||||
(_, index) =>
|
|
||||||
`(nextval('seq_authority_sub_user'), $1, $${index + 2}, $${newUserIds.length + 2}, NOW())`
|
|
||||||
)
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
const sql = `
|
|
||||||
INSERT INTO authority_sub_user (objid, master_objid, user_id, writer, regdate)
|
|
||||||
VALUES ${values}
|
|
||||||
`;
|
|
||||||
|
|
||||||
await query(sql, [masterObjid, ...newUserIds, writer]);
|
|
||||||
|
|
||||||
// 히스토리 기록
|
|
||||||
for (const userId of newUserIds) {
|
|
||||||
await this.insertAuthorityHistory(masterObjid, userId, "ADD", writer);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("권한 그룹 멤버 추가 성공", {
|
|
||||||
masterObjid,
|
|
||||||
count: newUserIds.length,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("권한 그룹 멤버 추가 실패", { error, masterObjid, userIds });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 권한 그룹 멤버 제거 (여러 명)
|
|
||||||
*/
|
|
||||||
static async removeRoleMembers(
|
|
||||||
masterObjid: number,
|
|
||||||
userIds: string[],
|
|
||||||
writer: string
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
await query(
|
|
||||||
"DELETE FROM authority_sub_user WHERE master_objid = $1 AND user_id = ANY($2)",
|
|
||||||
[masterObjid, userIds]
|
|
||||||
);
|
|
||||||
|
|
||||||
// 히스토리 기록
|
|
||||||
for (const userId of userIds) {
|
|
||||||
await this.insertAuthorityHistory(
|
|
||||||
masterObjid,
|
|
||||||
userId,
|
|
||||||
"REMOVE",
|
|
||||||
writer
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("권한 그룹 멤버 제거 성공", {
|
|
||||||
masterObjid,
|
|
||||||
count: userIds.length,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("권한 그룹 멤버 제거 실패", { error, masterObjid, userIds });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 권한 히스토리 기록
|
|
||||||
*/
|
|
||||||
private static async insertAuthorityHistory(
|
|
||||||
masterObjid: number,
|
|
||||||
userId: string,
|
|
||||||
historyType: "ADD" | "REMOVE",
|
|
||||||
writer: string
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
const sql = `
|
|
||||||
INSERT INTO authority_master_history
|
|
||||||
(objid, parent_objid, parent_name, parent_code, user_id, active, history_type, writer, reg_date)
|
|
||||||
SELECT
|
|
||||||
nextval('seq_authority_master'),
|
|
||||||
$1,
|
|
||||||
am.auth_name,
|
|
||||||
am.auth_code,
|
|
||||||
$2,
|
|
||||||
am.status,
|
|
||||||
$3,
|
|
||||||
$4,
|
|
||||||
NOW()
|
|
||||||
FROM authority_master am
|
|
||||||
WHERE am.objid = $1
|
|
||||||
`;
|
|
||||||
|
|
||||||
await query(sql, [masterObjid, userId, historyType, writer]);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("권한 히스토리 기록 실패", {
|
|
||||||
error,
|
|
||||||
masterObjid,
|
|
||||||
userId,
|
|
||||||
historyType,
|
|
||||||
});
|
|
||||||
// 히스토리 기록 실패는 메인 작업을 중단하지 않음
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 메뉴 권한 목록 조회
|
|
||||||
*/
|
|
||||||
static async getMenuPermissions(
|
|
||||||
authObjid: number
|
|
||||||
): Promise<MenuPermission[]> {
|
|
||||||
try {
|
|
||||||
const sql = `
|
|
||||||
SELECT
|
|
||||||
rma.objid,
|
|
||||||
rma.menu_objid AS "menuObjid",
|
|
||||||
rma.auth_objid AS "authObjid",
|
|
||||||
mi.menu_name_kor AS "menuName",
|
|
||||||
mi.menu_code AS "menuCode",
|
|
||||||
mi.menu_url AS "menuUrl",
|
|
||||||
rma.create_yn AS "createYn",
|
|
||||||
rma.read_yn AS "readYn",
|
|
||||||
rma.update_yn AS "updateYn",
|
|
||||||
rma.delete_yn AS "deleteYn",
|
|
||||||
rma.execute_yn AS "executeYn",
|
|
||||||
rma.export_yn AS "exportYn",
|
|
||||||
rma.writer,
|
|
||||||
rma.regdate
|
|
||||||
FROM rel_menu_auth rma
|
|
||||||
LEFT JOIN menu_info mi ON rma.menu_objid = mi.objid
|
|
||||||
WHERE rma.auth_objid = $1
|
|
||||||
ORDER BY mi.menu_name_kor
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = await query<MenuPermission>(sql, [authObjid]);
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("메뉴 권한 조회 실패", { error, authObjid });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 메뉴 권한 설정 (여러 메뉴)
|
|
||||||
*/
|
|
||||||
static async setMenuPermissions(
|
|
||||||
authObjid: number,
|
|
||||||
permissions: Array<{
|
|
||||||
menuObjid: number;
|
|
||||||
createYn: string;
|
|
||||||
readYn: string;
|
|
||||||
updateYn: string;
|
|
||||||
deleteYn: string;
|
|
||||||
}>,
|
|
||||||
writer: string
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
// 기존 권한 삭제
|
|
||||||
await query("DELETE FROM rel_menu_auth WHERE auth_objid = $1", [
|
|
||||||
authObjid,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 새로운 권한 삽입
|
|
||||||
if (permissions.length > 0) {
|
|
||||||
const values = permissions
|
|
||||||
.map(
|
|
||||||
(_, index) =>
|
|
||||||
`(nextval('seq_rel_menu_auth'), $${index * 5 + 2}, $1, $${index * 5 + 3}, $${index * 5 + 4}, $${index * 5 + 5}, $${index * 5 + 6}, $${permissions.length * 5 + 2}, NOW())`
|
|
||||||
)
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
const params = permissions.flatMap((p) => [
|
|
||||||
p.menuObjid,
|
|
||||||
p.createYn,
|
|
||||||
p.readYn,
|
|
||||||
p.updateYn,
|
|
||||||
p.deleteYn,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const sql = `
|
|
||||||
INSERT INTO rel_menu_auth (objid, menu_objid, auth_objid, create_yn, read_yn, update_yn, delete_yn, writer, regdate)
|
|
||||||
VALUES ${values}
|
|
||||||
`;
|
|
||||||
|
|
||||||
await query(sql, [authObjid, ...params, writer]);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("메뉴 권한 설정 성공", {
|
|
||||||
authObjid,
|
|
||||||
count: permissions.length,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("메뉴 권한 설정 실패", { error, authObjid, permissions });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 사용자가 속한 권한 그룹 목록 조회
|
|
||||||
*/
|
|
||||||
static async getUserRoleGroups(
|
|
||||||
userId: string,
|
|
||||||
companyCode: string
|
|
||||||
): Promise<RoleGroup[]> {
|
|
||||||
try {
|
|
||||||
const sql = `
|
|
||||||
SELECT
|
|
||||||
am.objid,
|
|
||||||
am.auth_name AS "authName",
|
|
||||||
am.auth_code AS "authCode",
|
|
||||||
am.company_code AS "companyCode",
|
|
||||||
am.status,
|
|
||||||
am.writer,
|
|
||||||
am.regdate
|
|
||||||
FROM authority_master am
|
|
||||||
JOIN authority_sub_user asu ON am.objid = asu.master_objid
|
|
||||||
WHERE asu.user_id = $1
|
|
||||||
AND am.company_code = $2
|
|
||||||
AND am.status = 'active'
|
|
||||||
ORDER BY am.auth_name
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = await query<RoleGroup>(sql, [userId, companyCode]);
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("사용자 권한 그룹 조회 실패", {
|
|
||||||
error,
|
|
||||||
userId,
|
|
||||||
companyCode,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 전체 메뉴 목록 조회 (권한 설정용)
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* 전체 메뉴 목록 조회 (권한 설정용)
|
|
||||||
*/
|
|
||||||
static async getAllMenus(companyCode?: string): Promise<any[]> {
|
|
||||||
try {
|
|
||||||
logger.info("🔍 전체 메뉴 목록 조회 시작", { companyCode });
|
|
||||||
|
|
||||||
let whereConditions: string[] = ["status = 'active'"];
|
|
||||||
const params: any[] = [];
|
|
||||||
let paramIndex = 1;
|
|
||||||
|
|
||||||
// 회사 코드 필터 (선택적)
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
/**
|
|
||||||
* 전체 메뉴 목록 조회 (권한 설정용)
|
|
||||||
*/
|
|
||||||
static async getAllMenus(companyCode?: string): Promise<any[]> {
|
|
||||||
try {
|
|
||||||
logger.info("🔍 전체 메뉴 목록 조회 시작", { companyCode });
|
|
||||||
|
|
||||||
let whereConditions: string[] = ["status = 'active'"];
|
|
||||||
const params: any[] = [];
|
|
||||||
let paramIndex = 1;
|
|
||||||
|
|
||||||
// 회사 코드 필터 (선택적)
|
|
||||||
// 공통 메뉴(*)와 특정 회사 메뉴를 모두 조회
|
|
||||||
if (companyCode) {
|
|
||||||
whereConditions.push(`(company_code = \$${paramIndex} OR company_code = '*')`);
|
|
||||||
params.push(companyCode);
|
|
||||||
paramIndex++;
|
|
||||||
logger.info("📋 회사 코드 필터 적용", { companyCode });
|
|
||||||
} else {
|
|
||||||
logger.info("📋 회사 코드 필터 없음 (전체 조회)");
|
|
||||||
}
|
|
||||||
|
|
||||||
const whereClause = whereConditions.join(" AND ");
|
|
||||||
|
|
||||||
const sql = `
|
|
||||||
SELECT
|
|
||||||
objid,
|
|
||||||
menu_name_kor AS "menuName",
|
|
||||||
menu_name_eng AS "menuNameEng",
|
|
||||||
menu_code AS "menuCode",
|
|
||||||
menu_url AS "menuUrl",
|
|
||||||
menu_type AS "menuType",
|
|
||||||
parent_obj_id AS "parentObjid",
|
|
||||||
seq AS "sortOrder",
|
|
||||||
company_code AS "companyCode"
|
|
||||||
FROM menu_info
|
|
||||||
WHERE ${whereClause}
|
|
||||||
ORDER BY seq, menu_name_kor
|
|
||||||
`;
|
|
||||||
|
|
||||||
logger.info("🔍 SQL 쿼리 실행", {
|
|
||||||
whereClause,
|
|
||||||
params,
|
|
||||||
sql: sql.substring(0, 200) + "...",
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await query<any>(sql, params);
|
|
||||||
|
|
||||||
logger.info("✅ 메뉴 목록 조회 성공", {
|
|
||||||
count: result.length,
|
|
||||||
companyCode,
|
|
||||||
menus: result.map((m) => ({
|
|
||||||
objid: m.objid,
|
|
||||||
name: m.menuName,
|
|
||||||
code: m.menuCode,
|
|
||||||
companyCode: m.companyCode,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("❌ 메뉴 목록 조회 실패", { error, companyCode });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -13,10 +13,11 @@ import { ApiResponse } from "../types/batchTypes";
|
||||||
|
|
||||||
export class BatchExecutionLogService {
|
export class BatchExecutionLogService {
|
||||||
/**
|
/**
|
||||||
* 배치 실행 로그 목록 조회
|
* 배치 실행 로그 목록 조회 (회사별)
|
||||||
*/
|
*/
|
||||||
static async getExecutionLogs(
|
static async getExecutionLogs(
|
||||||
filter: BatchExecutionLogFilter = {}
|
filter: BatchExecutionLogFilter = {},
|
||||||
|
userCompanyCode?: string
|
||||||
): Promise<ApiResponse<BatchExecutionLogWithConfig[]>> {
|
): Promise<ApiResponse<BatchExecutionLogWithConfig[]>> {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
|
|
@ -36,6 +37,12 @@ export class BatchExecutionLogService {
|
||||||
const params: any[] = [];
|
const params: any[] = [];
|
||||||
let paramIndex = 1;
|
let paramIndex = 1;
|
||||||
|
|
||||||
|
// 회사별 필터링 (최고 관리자가 아닌 경우)
|
||||||
|
if (userCompanyCode && userCompanyCode !== "*") {
|
||||||
|
whereConditions.push(`bc.company_code = $${paramIndex++}`);
|
||||||
|
params.push(userCompanyCode);
|
||||||
|
}
|
||||||
|
|
||||||
if (batch_config_id) {
|
if (batch_config_id) {
|
||||||
whereConditions.push(`bel.batch_config_id = $${paramIndex++}`);
|
whereConditions.push(`bel.batch_config_id = $${paramIndex++}`);
|
||||||
params.push(batch_config_id);
|
params.push(batch_config_id);
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,11 @@ export interface BatchApiResponse<T = unknown> {
|
||||||
|
|
||||||
export class BatchManagementService {
|
export class BatchManagementService {
|
||||||
/**
|
/**
|
||||||
* 배치관리용 연결 목록 조회
|
* 배치관리용 연결 목록 조회 (회사별)
|
||||||
*/
|
*/
|
||||||
static async getAvailableConnections(): Promise<
|
static async getAvailableConnections(
|
||||||
BatchApiResponse<BatchConnectionInfo[]>
|
userCompanyCode?: string
|
||||||
> {
|
): Promise<BatchApiResponse<BatchConnectionInfo[]>> {
|
||||||
try {
|
try {
|
||||||
const connections: BatchConnectionInfo[] = [];
|
const connections: BatchConnectionInfo[] = [];
|
||||||
|
|
||||||
|
|
@ -50,19 +50,27 @@ export class BatchManagementService {
|
||||||
db_type: "postgresql",
|
db_type: "postgresql",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 활성화된 외부 DB 연결 조회
|
// 활성화된 외부 DB 연결 조회 (회사별 필터링)
|
||||||
|
let query_sql = `SELECT id, connection_name, db_type, description
|
||||||
|
FROM external_db_connections
|
||||||
|
WHERE is_active = 'Y'`;
|
||||||
|
|
||||||
|
const params: any[] = [];
|
||||||
|
|
||||||
|
// 회사별 필터링 (최고 관리자가 아닌 경우)
|
||||||
|
if (userCompanyCode && userCompanyCode !== "*") {
|
||||||
|
query_sql += ` AND company_code = $1`;
|
||||||
|
params.push(userCompanyCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
query_sql += ` ORDER BY connection_name ASC`;
|
||||||
|
|
||||||
const externalConnections = await query<{
|
const externalConnections = await query<{
|
||||||
id: number;
|
id: number;
|
||||||
connection_name: string;
|
connection_name: string;
|
||||||
db_type: string;
|
db_type: string;
|
||||||
description: string;
|
description: string;
|
||||||
}>(
|
}>(query_sql, params);
|
||||||
`SELECT id, connection_name, db_type, description
|
|
||||||
FROM external_db_connections
|
|
||||||
WHERE is_active = 'Y'
|
|
||||||
ORDER BY connection_name ASC`,
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
// 외부 DB 연결 추가
|
// 외부 DB 연결 추가
|
||||||
externalConnections.forEach((conn) => {
|
externalConnections.forEach((conn) => {
|
||||||
|
|
@ -90,11 +98,12 @@ export class BatchManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 배치관리용 테이블 목록 조회
|
* 배치관리용 테이블 목록 조회 (회사별)
|
||||||
*/
|
*/
|
||||||
static async getTablesFromConnection(
|
static async getTablesFromConnection(
|
||||||
connectionType: "internal" | "external",
|
connectionType: "internal" | "external",
|
||||||
connectionId?: number
|
connectionId?: number,
|
||||||
|
userCompanyCode?: string
|
||||||
): Promise<BatchApiResponse<BatchTableInfo[]>> {
|
): Promise<BatchApiResponse<BatchTableInfo[]>> {
|
||||||
try {
|
try {
|
||||||
let tables: BatchTableInfo[] = [];
|
let tables: BatchTableInfo[] = [];
|
||||||
|
|
@ -115,8 +124,11 @@ export class BatchManagementService {
|
||||||
columns: [],
|
columns: [],
|
||||||
}));
|
}));
|
||||||
} else if (connectionType === "external" && connectionId) {
|
} else if (connectionType === "external" && connectionId) {
|
||||||
// 외부 DB 테이블 조회
|
// 외부 DB 테이블 조회 (회사별 필터링)
|
||||||
const tablesResult = await this.getExternalTables(connectionId);
|
const tablesResult = await this.getExternalTables(
|
||||||
|
connectionId,
|
||||||
|
userCompanyCode
|
||||||
|
);
|
||||||
if (tablesResult.success && tablesResult.data) {
|
if (tablesResult.success && tablesResult.data) {
|
||||||
tables = tablesResult.data;
|
tables = tablesResult.data;
|
||||||
}
|
}
|
||||||
|
|
@ -138,12 +150,13 @@ export class BatchManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 배치관리용 테이블 컬럼 정보 조회
|
* 배치관리용 테이블 컬럼 정보 조회 (회사별)
|
||||||
*/
|
*/
|
||||||
static async getTableColumns(
|
static async getTableColumns(
|
||||||
connectionType: "internal" | "external",
|
connectionType: "internal" | "external",
|
||||||
connectionId: number | undefined,
|
connectionId: number | undefined,
|
||||||
tableName: string
|
tableName: string,
|
||||||
|
userCompanyCode?: string
|
||||||
): Promise<BatchApiResponse<BatchColumnInfo[]>> {
|
): Promise<BatchApiResponse<BatchColumnInfo[]>> {
|
||||||
try {
|
try {
|
||||||
console.log(`[BatchManagementService] getTableColumns 호출:`, {
|
console.log(`[BatchManagementService] getTableColumns 호출:`, {
|
||||||
|
|
@ -189,14 +202,15 @@ export class BatchManagementService {
|
||||||
column_default: row.column_default,
|
column_default: row.column_default,
|
||||||
}));
|
}));
|
||||||
} else if (connectionType === "external" && connectionId) {
|
} else if (connectionType === "external" && connectionId) {
|
||||||
// 외부 DB 컬럼 조회
|
// 외부 DB 컬럼 조회 (회사별 필터링)
|
||||||
console.log(
|
console.log(
|
||||||
`[BatchManagementService] 외부 DB 컬럼 조회 시작: connectionId=${connectionId}, tableName=${tableName}`
|
`[BatchManagementService] 외부 DB 컬럼 조회 시작: connectionId=${connectionId}, tableName=${tableName}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const columnsResult = await this.getExternalTableColumns(
|
const columnsResult = await this.getExternalTableColumns(
|
||||||
connectionId,
|
connectionId,
|
||||||
tableName
|
tableName,
|
||||||
|
userCompanyCode
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
|
|
@ -226,22 +240,29 @@ export class BatchManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 외부 DB 테이블 목록 조회 (내부 구현)
|
* 외부 DB 테이블 목록 조회 (내부 구현, 회사별)
|
||||||
*/
|
*/
|
||||||
private static async getExternalTables(
|
private static async getExternalTables(
|
||||||
connectionId: number
|
connectionId: number,
|
||||||
|
userCompanyCode?: string
|
||||||
): Promise<BatchApiResponse<BatchTableInfo[]>> {
|
): Promise<BatchApiResponse<BatchTableInfo[]>> {
|
||||||
try {
|
try {
|
||||||
// 연결 정보 조회
|
// 연결 정보 조회 (회사별 필터링)
|
||||||
const connection = await queryOne<any>(
|
let query_sql = `SELECT * FROM external_db_connections WHERE id = $1`;
|
||||||
`SELECT * FROM external_db_connections WHERE id = $1`,
|
const params: any[] = [connectionId];
|
||||||
[connectionId]
|
|
||||||
);
|
// 회사별 필터링 (최고 관리자가 아닌 경우)
|
||||||
|
if (userCompanyCode && userCompanyCode !== "*") {
|
||||||
|
query_sql += ` AND company_code = $2`;
|
||||||
|
params.push(userCompanyCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = await queryOne<any>(query_sql, params);
|
||||||
|
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "연결 정보를 찾을 수 없습니다.",
|
message: "연결 정보를 찾을 수 없거나 권한이 없습니다.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -299,26 +320,33 @@ export class BatchManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 외부 DB 테이블 컬럼 정보 조회 (내부 구현)
|
* 외부 DB 테이블 컬럼 정보 조회 (내부 구현, 회사별)
|
||||||
*/
|
*/
|
||||||
private static async getExternalTableColumns(
|
private static async getExternalTableColumns(
|
||||||
connectionId: number,
|
connectionId: number,
|
||||||
tableName: string
|
tableName: string,
|
||||||
|
userCompanyCode?: string
|
||||||
): Promise<BatchApiResponse<BatchColumnInfo[]>> {
|
): Promise<BatchApiResponse<BatchColumnInfo[]>> {
|
||||||
try {
|
try {
|
||||||
console.log(
|
console.log(
|
||||||
`[BatchManagementService] getExternalTableColumns 호출: connectionId=${connectionId}, tableName=${tableName}`
|
`[BatchManagementService] getExternalTableColumns 호출: connectionId=${connectionId}, tableName=${tableName}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// 연결 정보 조회
|
// 연결 정보 조회 (회사별 필터링)
|
||||||
const connection = await queryOne<any>(
|
let query_sql = `SELECT * FROM external_db_connections WHERE id = $1`;
|
||||||
`SELECT * FROM external_db_connections WHERE id = $1`,
|
const params: any[] = [connectionId];
|
||||||
[connectionId]
|
|
||||||
);
|
// 회사별 필터링 (최고 관리자가 아닌 경우)
|
||||||
|
if (userCompanyCode && userCompanyCode !== "*") {
|
||||||
|
query_sql += ` AND company_code = $2`;
|
||||||
|
params.push(userCompanyCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = await queryOne<any>(query_sql, params);
|
||||||
|
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
console.log(
|
console.log(
|
||||||
`[BatchManagementService] 연결 정보를 찾을 수 없음: connectionId=${connectionId}`
|
`[BatchManagementService] 연결 정보를 찾을 수 없거나 권한이 없음: connectionId=${connectionId}`
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|
|
||||||
|
|
@ -20,27 +20,33 @@ import { DbConnectionManager } from "./dbConnectionManager";
|
||||||
|
|
||||||
export class BatchService {
|
export class BatchService {
|
||||||
/**
|
/**
|
||||||
* 배치 설정 목록 조회
|
* 배치 설정 목록 조회 (회사별)
|
||||||
*/
|
*/
|
||||||
static async getBatchConfigs(
|
static async getBatchConfigs(
|
||||||
filter: BatchConfigFilter
|
filter: BatchConfigFilter,
|
||||||
|
userCompanyCode?: string
|
||||||
): Promise<ApiResponse<BatchConfig[]>> {
|
): Promise<ApiResponse<BatchConfig[]>> {
|
||||||
try {
|
try {
|
||||||
const whereConditions: string[] = [];
|
const whereConditions: string[] = [];
|
||||||
const values: any[] = [];
|
const values: any[] = [];
|
||||||
let paramIndex = 1;
|
let paramIndex = 1;
|
||||||
|
|
||||||
|
// 회사별 필터링 (최고 관리자가 아닌 경우 필수)
|
||||||
|
if (userCompanyCode && userCompanyCode !== "*") {
|
||||||
|
whereConditions.push(`bc.company_code = $${paramIndex++}`);
|
||||||
|
values.push(userCompanyCode);
|
||||||
|
} else if (userCompanyCode === "*" && filter.company_code) {
|
||||||
|
// 최고 관리자: 필터가 있으면 적용
|
||||||
|
whereConditions.push(`bc.company_code = $${paramIndex++}`);
|
||||||
|
values.push(filter.company_code);
|
||||||
|
}
|
||||||
|
|
||||||
// 필터 조건 적용
|
// 필터 조건 적용
|
||||||
if (filter.is_active) {
|
if (filter.is_active) {
|
||||||
whereConditions.push(`bc.is_active = $${paramIndex++}`);
|
whereConditions.push(`bc.is_active = $${paramIndex++}`);
|
||||||
values.push(filter.is_active);
|
values.push(filter.is_active);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.company_code) {
|
|
||||||
whereConditions.push(`bc.company_code = $${paramIndex++}`);
|
|
||||||
values.push(filter.company_code);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 검색 조건 적용 (OR)
|
// 검색 조건 적용 (OR)
|
||||||
if (filter.search && filter.search.trim()) {
|
if (filter.search && filter.search.trim()) {
|
||||||
whereConditions.push(
|
whereConditions.push(
|
||||||
|
|
@ -122,14 +128,14 @@ export class BatchService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 배치 설정 조회
|
* 특정 배치 설정 조회 (회사별)
|
||||||
*/
|
*/
|
||||||
static async getBatchConfigById(
|
static async getBatchConfigById(
|
||||||
id: number
|
id: number,
|
||||||
|
userCompanyCode?: string
|
||||||
): Promise<ApiResponse<BatchConfig>> {
|
): Promise<ApiResponse<BatchConfig>> {
|
||||||
try {
|
try {
|
||||||
const batchConfig = await queryOne<any>(
|
let query = `SELECT bc.id, bc.batch_name, bc.description, bc.cron_schedule,
|
||||||
`SELECT bc.id, bc.batch_name, bc.description, bc.cron_schedule,
|
|
||||||
bc.is_active, bc.company_code, bc.created_date, bc.created_by,
|
bc.is_active, bc.company_code, bc.created_date, bc.created_by,
|
||||||
bc.updated_date, bc.updated_by,
|
bc.updated_date, bc.updated_by,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
|
|
@ -155,15 +161,25 @@ export class BatchService {
|
||||||
) as batch_mappings
|
) as batch_mappings
|
||||||
FROM batch_configs bc
|
FROM batch_configs bc
|
||||||
LEFT JOIN batch_mappings bm ON bc.id = bm.batch_config_id
|
LEFT JOIN batch_mappings bm ON bc.id = bm.batch_config_id
|
||||||
WHERE bc.id = $1
|
WHERE bc.id = $1`;
|
||||||
GROUP BY bc.id`,
|
|
||||||
[id]
|
const params: any[] = [id];
|
||||||
);
|
let paramIndex = 2;
|
||||||
|
|
||||||
|
// 회사별 필터링 (최고 관리자가 아닌 경우)
|
||||||
|
if (userCompanyCode && userCompanyCode !== "*") {
|
||||||
|
query += ` AND bc.company_code = $${paramIndex}`;
|
||||||
|
params.push(userCompanyCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
query += ` GROUP BY bc.id`;
|
||||||
|
|
||||||
|
const batchConfig = await queryOne<any>(query, params);
|
||||||
|
|
||||||
if (!batchConfig) {
|
if (!batchConfig) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "배치 설정을 찾을 수 없습니다.",
|
message: "배치 설정을 찾을 수 없거나 권한이 없습니다.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -267,15 +283,21 @@ export class BatchService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 배치 설정 수정
|
* 배치 설정 수정 (회사별)
|
||||||
*/
|
*/
|
||||||
static async updateBatchConfig(
|
static async updateBatchConfig(
|
||||||
id: number,
|
id: number,
|
||||||
data: UpdateBatchConfigRequest,
|
data: UpdateBatchConfigRequest,
|
||||||
userId?: string
|
userId?: string,
|
||||||
|
userCompanyCode?: string
|
||||||
): Promise<ApiResponse<BatchConfig>> {
|
): Promise<ApiResponse<BatchConfig>> {
|
||||||
try {
|
try {
|
||||||
// 기존 배치 설정 확인
|
// 기존 배치 설정 확인 (회사 권한 체크 포함)
|
||||||
|
const existing = await this.getBatchConfigById(id, userCompanyCode);
|
||||||
|
if (!existing.success) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
const existingConfig = await queryOne<any>(
|
const existingConfig = await queryOne<any>(
|
||||||
`SELECT bc.*,
|
`SELECT bc.*,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
|
|
@ -416,13 +438,20 @@ export class BatchService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 배치 설정 삭제 (논리 삭제)
|
* 배치 설정 삭제 (논리 삭제, 회사별)
|
||||||
*/
|
*/
|
||||||
static async deleteBatchConfig(
|
static async deleteBatchConfig(
|
||||||
id: number,
|
id: number,
|
||||||
userId?: string
|
userId?: string,
|
||||||
|
userCompanyCode?: string
|
||||||
): Promise<ApiResponse<void>> {
|
): Promise<ApiResponse<void>> {
|
||||||
try {
|
try {
|
||||||
|
// 기존 배치 설정 확인 (회사 권한 체크 포함)
|
||||||
|
const existing = await this.getBatchConfigById(id, userCompanyCode);
|
||||||
|
if (!existing.success) {
|
||||||
|
return existing as ApiResponse<void>;
|
||||||
|
}
|
||||||
|
|
||||||
const existingConfig = await queryOne<any>(
|
const existingConfig = await queryOne<any>(
|
||||||
`SELECT * FROM batch_configs WHERE id = $1`,
|
`SELECT * FROM batch_configs WHERE id = $1`,
|
||||||
[id]
|
[id]
|
||||||
|
|
|
||||||
|
|
@ -604,12 +604,13 @@ 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()) {
|
||||||
|
|
@ -655,6 +656,12 @@ 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++}`;
|
||||||
|
|
@ -675,6 +682,10 @@ export class CommonCodeService {
|
||||||
categoryNameEng: "카테고리 영문명",
|
categoryNameEng: "카테고리 영문명",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`카테고리 중복 검사: ${field}=${value}, 회사=${userCompanyCode}, 중복=${isDuplicate}`
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDuplicate,
|
isDuplicate,
|
||||||
message: isDuplicate
|
message: isDuplicate
|
||||||
|
|
@ -688,13 +699,14 @@ 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()) {
|
||||||
|
|
@ -743,6 +755,12 @@ 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++}`;
|
||||||
|
|
@ -760,6 +778,10 @@ export class CommonCodeService {
|
||||||
codeNameEng: "코드 영문명",
|
codeNameEng: "코드 영문명",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`코드 중복 검사: ${categoryCode}.${field}=${value}, 회사=${userCompanyCode}, 중복=${isDuplicate}`
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDuplicate,
|
isDuplicate,
|
||||||
message: isDuplicate
|
message: isDuplicate
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ 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 조건 동적 생성
|
||||||
|
|
@ -25,6 +26,26 @@ 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++}`);
|
||||||
|
|
@ -36,11 +57,6 @@ 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(
|
||||||
|
|
@ -496,23 +512,36 @@ export class ExternalDbConnectionService {
|
||||||
/**
|
/**
|
||||||
* 외부 DB 연결 삭제 (물리 삭제)
|
* 외부 DB 연결 삭제 (물리 삭제)
|
||||||
*/
|
*/
|
||||||
static async deleteConnection(id: number): Promise<ApiResponse<void>> {
|
static async deleteConnection(
|
||||||
|
id: number,
|
||||||
|
userCompanyCode?: string
|
||||||
|
): Promise<ApiResponse<void>> {
|
||||||
try {
|
try {
|
||||||
const existingConnection = await queryOne(
|
let selectQuery = `SELECT id FROM external_db_connections WHERE id = $1`;
|
||||||
`SELECT id FROM external_db_connections WHERE id = $1`,
|
const selectParams: any[] = [id];
|
||||||
[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: "연결 설정이 삭제되었습니다.",
|
||||||
|
|
@ -747,8 +776,11 @@ 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가 아닌 쿼리 시도:", { id, query: query.substring(0, 100) });
|
console.log("보안 오류: SELECT가 아닌 쿼리 시도:", {
|
||||||
|
id,
|
||||||
|
query: query.substring(0, 100),
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "외부 데이터베이스에서는 SELECT 쿼리만 실행할 수 있습니다.",
|
message: "외부 데이터베이스에서는 SELECT 쿼리만 실행할 수 있습니다.",
|
||||||
|
|
@ -756,16 +788,32 @@ export class ExternalDbConnectionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 위험한 키워드 검사
|
// 위험한 키워드 검사
|
||||||
const dangerousKeywords = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER', 'TRUNCATE', 'EXEC', 'EXECUTE', 'CALL', 'MERGE'];
|
const dangerousKeywords = [
|
||||||
const hasDangerousKeyword = dangerousKeywords.some(keyword =>
|
"INSERT",
|
||||||
|
"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("보안 오류: 위험한 키워드 포함 쿼리 시도:", { id, query: query.substring(0, 100) });
|
console.log("보안 오류: 위험한 키워드 포함 쿼리 시도:", {
|
||||||
|
id,
|
||||||
|
query: query.substring(0, 100),
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "데이터를 변경하거나 삭제하는 쿼리는 허용되지 않습니다. SELECT 쿼리만 사용해주세요.",
|
message:
|
||||||
|
"데이터를 변경하거나 삭제하는 쿼리는 허용되지 않습니다. SELECT 쿼리만 사용해주세요.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ 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 = `
|
||||||
|
|
@ -39,12 +40,28 @@ export class ExternalRestApiConnectionService {
|
||||||
const params: any[] = [];
|
const params: any[] = [];
|
||||||
let paramIndex = 1;
|
let paramIndex = 1;
|
||||||
|
|
||||||
// 회사 코드 필터
|
// 회사별 필터링 (최고 관리자가 아닌 경우 필수)
|
||||||
|
if (userCompanyCode && userCompanyCode !== "*") {
|
||||||
|
query += ` AND company_code = $${paramIndex}`;
|
||||||
|
params.push(userCompanyCode);
|
||||||
|
paramIndex++;
|
||||||
|
logger.info(`회사별 REST API 연결 필터링: ${userCompanyCode}`);
|
||||||
|
} else if (userCompanyCode === "*") {
|
||||||
|
logger.info(`최고 관리자: 모든 REST API 연결 조회`);
|
||||||
|
// 필터가 있으면 적용
|
||||||
if (filter.company_code) {
|
if (filter.company_code) {
|
||||||
query += ` AND company_code = $${paramIndex}`;
|
query += ` AND company_code = $${paramIndex}`;
|
||||||
params.push(filter.company_code);
|
params.push(filter.company_code);
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// userCompanyCode가 없는 경우 (하위 호환성)
|
||||||
|
if (filter.company_code) {
|
||||||
|
query += ` AND company_code = $${paramIndex}`;
|
||||||
|
params.push(filter.company_code);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 활성 상태 필터
|
// 활성 상태 필터
|
||||||
if (filter.is_active) {
|
if (filter.is_active) {
|
||||||
|
|
@ -105,10 +122,11 @@ 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 {
|
||||||
const query = `
|
let query = `
|
||||||
SELECT
|
SELECT
|
||||||
id, connection_name, description, base_url, endpoint_path, default_headers,
|
id, connection_name, description, base_url, endpoint_path, default_headers,
|
||||||
auth_type, auth_config, timeout, retry_count, retry_delay,
|
auth_type, auth_config, timeout, retry_count, retry_delay,
|
||||||
|
|
@ -118,12 +136,20 @@ export class ExternalRestApiConnectionService {
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result: QueryResult<any> = await pool.query(query, [id]);
|
const params: any[] = [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: "연결을 찾을 수 없거나 권한이 없습니다.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,11 +252,12 @@ 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);
|
const existing = await this.getConnectionById(id, userCompanyCode);
|
||||||
if (!existing.success) {
|
if (!existing.success) {
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
@ -360,24 +387,38 @@ export class ExternalRestApiConnectionService {
|
||||||
/**
|
/**
|
||||||
* REST API 연결 삭제
|
* REST API 연결 삭제
|
||||||
*/
|
*/
|
||||||
static async deleteConnection(id: number): Promise<ApiResponse<void>> {
|
static async deleteConnection(
|
||||||
|
id: number,
|
||||||
|
userCompanyCode?: string
|
||||||
|
): Promise<ApiResponse<void>> {
|
||||||
try {
|
try {
|
||||||
const query = `
|
let query = `
|
||||||
DELETE FROM external_rest_api_connections
|
DELETE FROM external_rest_api_connections
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING connection_name
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result: QueryResult<any> = await pool.query(query, [id]);
|
const params: any[] = [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(`REST API 연결 삭제 성공: ${result.rows[0].connection_name}`);
|
logger.info(
|
||||||
|
`REST API 연결 삭제 성공: ${result.rows[0].connection_name} (회사: ${userCompanyCode || "전체"})`
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from "react";
|
||||||
import { ChartConfig, QueryResult } from './types';
|
import { ChartConfig, QueryResult, MarkerColorRule } from "./types";
|
||||||
|
import { Plus, Trash2 } from "lucide-react";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
interface VehicleMapConfigPanelProps {
|
interface VehicleMapConfigPanelProps {
|
||||||
config?: ChartConfig;
|
config?: ChartConfig;
|
||||||
|
|
@ -18,24 +20,80 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
||||||
const [currentConfig, setCurrentConfig] = useState<ChartConfig>(config || {});
|
const [currentConfig, setCurrentConfig] = useState<ChartConfig>(config || {});
|
||||||
|
|
||||||
// 설정 업데이트
|
// 설정 업데이트
|
||||||
const updateConfig = useCallback((updates: Partial<ChartConfig>) => {
|
const updateConfig = useCallback(
|
||||||
|
(updates: Partial<ChartConfig>) => {
|
||||||
const newConfig = { ...currentConfig, ...updates };
|
const newConfig = { ...currentConfig, ...updates };
|
||||||
setCurrentConfig(newConfig);
|
setCurrentConfig(newConfig);
|
||||||
onConfigChange(newConfig);
|
onConfigChange(newConfig);
|
||||||
}, [currentConfig, onConfigChange]);
|
},
|
||||||
|
[currentConfig, onConfigChange],
|
||||||
|
);
|
||||||
|
|
||||||
// 사용 가능한 컬럼 목록
|
// 사용 가능한 컬럼 목록
|
||||||
const availableColumns = queryResult?.columns || [];
|
const availableColumns = queryResult?.columns || [];
|
||||||
const sampleData = queryResult?.rows?.[0] || {};
|
const sampleData = queryResult?.rows?.[0] || {};
|
||||||
|
|
||||||
|
// 마커 색상 모드 변경
|
||||||
|
const handleMarkerColorModeChange = useCallback(
|
||||||
|
(mode: "single" | "conditional") => {
|
||||||
|
if (mode === "single") {
|
||||||
|
updateConfig({
|
||||||
|
markerColorMode: "single",
|
||||||
|
markerColorColumn: undefined,
|
||||||
|
markerColorRules: undefined,
|
||||||
|
markerDefaultColor: "#3b82f6", // 파란색
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateConfig({
|
||||||
|
markerColorMode: "conditional",
|
||||||
|
markerColorRules: [],
|
||||||
|
markerDefaultColor: "#6b7280", // 회색
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[updateConfig],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 마커 색상 규칙 추가
|
||||||
|
const addColorRule = useCallback(() => {
|
||||||
|
const newRule: MarkerColorRule = {
|
||||||
|
id: uuidv4(),
|
||||||
|
value: "",
|
||||||
|
color: "#3b82f6",
|
||||||
|
label: "",
|
||||||
|
};
|
||||||
|
const currentRules = currentConfig.markerColorRules || [];
|
||||||
|
updateConfig({ markerColorRules: [...currentRules, newRule] });
|
||||||
|
}, [currentConfig.markerColorRules, updateConfig]);
|
||||||
|
|
||||||
|
// 마커 색상 규칙 삭제
|
||||||
|
const deleteColorRule = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
const currentRules = currentConfig.markerColorRules || [];
|
||||||
|
updateConfig({ markerColorRules: currentRules.filter((rule) => rule.id !== id) });
|
||||||
|
},
|
||||||
|
[currentConfig.markerColorRules, updateConfig],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 마커 색상 규칙 업데이트
|
||||||
|
const updateColorRule = useCallback(
|
||||||
|
(id: string, updates: Partial<MarkerColorRule>) => {
|
||||||
|
const currentRules = currentConfig.markerColorRules || [];
|
||||||
|
updateConfig({
|
||||||
|
markerColorRules: currentRules.map((rule) => (rule.id === id ? { ...rule, ...updates } : rule)),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[currentConfig.markerColorRules, updateConfig],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-xs font-semibold text-gray-800">🗺️ 지도 설정</h4>
|
<h4 className="text-xs font-semibold text-gray-800">🗺️ 지도 설정</h4>
|
||||||
|
|
||||||
{/* 쿼리 결과가 없을 때 */}
|
{/* 쿼리 결과가 없을 때 */}
|
||||||
{!queryResult && (
|
{!queryResult && (
|
||||||
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-3">
|
||||||
<div className="text-yellow-800 text-xs">
|
<div className="text-xs text-yellow-800">
|
||||||
💡 먼저 SQL 쿼리를 실행하여 데이터를 가져온 후 지도를 설정할 수 있습니다.
|
💡 먼저 SQL 쿼리를 실행하여 데이터를 가져온 후 지도를 설정할 수 있습니다.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -49,10 +107,10 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
||||||
<label className="block text-xs font-medium text-gray-700">지도 제목</label>
|
<label className="block text-xs font-medium text-gray-700">지도 제목</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={currentConfig.title || ''}
|
value={currentConfig.title || ""}
|
||||||
onChange={(e) => updateConfig({ title: e.target.value })}
|
onChange={(e) => updateConfig({ title: e.target.value })}
|
||||||
placeholder="차량 위치 지도"
|
placeholder="차량 위치 지도"
|
||||||
className="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs"
|
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -60,12 +118,12 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="block text-xs font-medium text-gray-700">
|
<label className="block text-xs font-medium text-gray-700">
|
||||||
위도 컬럼 (Latitude)
|
위도 컬럼 (Latitude)
|
||||||
<span className="text-red-500 ml-1">*</span>
|
<span className="ml-1 text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={currentConfig.latitudeColumn || ''}
|
value={currentConfig.latitudeColumn || ""}
|
||||||
onChange={(e) => updateConfig({ latitudeColumn: e.target.value })}
|
onChange={(e) => updateConfig({ latitudeColumn: e.target.value })}
|
||||||
className="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs"
|
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
|
||||||
>
|
>
|
||||||
<option value="">선택하세요</option>
|
<option value="">선택하세요</option>
|
||||||
{availableColumns.map((col) => (
|
{availableColumns.map((col) => (
|
||||||
|
|
@ -80,12 +138,12 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="block text-xs font-medium text-gray-700">
|
<label className="block text-xs font-medium text-gray-700">
|
||||||
경도 컬럼 (Longitude)
|
경도 컬럼 (Longitude)
|
||||||
<span className="text-red-500 ml-1">*</span>
|
<span className="ml-1 text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={currentConfig.longitudeColumn || ''}
|
value={currentConfig.longitudeColumn || ""}
|
||||||
onChange={(e) => updateConfig({ longitudeColumn: e.target.value })}
|
onChange={(e) => updateConfig({ longitudeColumn: e.target.value })}
|
||||||
className="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs"
|
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
|
||||||
>
|
>
|
||||||
<option value="">선택하세요</option>
|
<option value="">선택하세요</option>
|
||||||
{availableColumns.map((col) => (
|
{availableColumns.map((col) => (
|
||||||
|
|
@ -98,13 +156,11 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
||||||
|
|
||||||
{/* 라벨 컬럼 (선택사항) */}
|
{/* 라벨 컬럼 (선택사항) */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="block text-xs font-medium text-gray-700">
|
<label className="block text-xs font-medium text-gray-700">라벨 컬럼 (마커 표시명)</label>
|
||||||
라벨 컬럼 (마커 표시명)
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
value={currentConfig.labelColumn || ''}
|
value={currentConfig.labelColumn || ""}
|
||||||
onChange={(e) => updateConfig({ labelColumn: e.target.value })}
|
onChange={(e) => updateConfig({ labelColumn: e.target.value })}
|
||||||
className="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs"
|
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
|
||||||
>
|
>
|
||||||
<option value="">선택하세요 (선택사항)</option>
|
<option value="">선택하세요 (선택사항)</option>
|
||||||
{availableColumns.map((col) => (
|
{availableColumns.map((col) => (
|
||||||
|
|
@ -115,74 +171,265 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 상태 컬럼 (선택사항) */}
|
{/* 마커 색상 설정 */}
|
||||||
|
<div className="space-y-2 border-t pt-3">
|
||||||
|
<h5 className="text-xs font-semibold text-gray-800">🎨 마커 색상 설정</h5>
|
||||||
|
|
||||||
|
{/* 색상 모드 선택 */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="block text-xs font-medium text-gray-700">색상 모드</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleMarkerColorModeChange("single")}
|
||||||
|
className={`flex-1 rounded-lg border px-3 py-2 text-xs transition-colors ${
|
||||||
|
(currentConfig.markerColorMode || "single") === "single"
|
||||||
|
? "border-blue-300 bg-blue-50 font-medium text-blue-700"
|
||||||
|
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
단일 색상
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleMarkerColorModeChange("conditional")}
|
||||||
|
className={`flex-1 rounded-lg border px-3 py-2 text-xs transition-colors ${
|
||||||
|
currentConfig.markerColorMode === "conditional"
|
||||||
|
? "border-blue-300 bg-blue-50 font-medium text-blue-700"
|
||||||
|
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
조건부 색상
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 단일 색상 모드 */}
|
||||||
|
{(currentConfig.markerColorMode || "single") === "single" && (
|
||||||
|
<div className="space-y-1.5 rounded-lg bg-gray-50 p-3">
|
||||||
|
<label className="block text-xs font-medium text-gray-700">마커 색상</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={currentConfig.markerDefaultColor || "#3b82f6"}
|
||||||
|
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
|
||||||
|
className="h-8 w-12 cursor-pointer rounded border border-gray-300"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={currentConfig.markerDefaultColor || "#3b82f6"}
|
||||||
|
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
|
||||||
|
placeholder="#3b82f6"
|
||||||
|
className="flex-1 rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500">모든 마커가 동일한 색상으로 표시됩니다</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 조건부 색상 모드 */}
|
||||||
|
{currentConfig.markerColorMode === "conditional" && (
|
||||||
|
<div className="space-y-2 rounded-lg bg-gray-50 p-3">
|
||||||
|
{/* 색상 조건 컬럼 선택 */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="block text-xs font-medium text-gray-700">
|
<label className="block text-xs font-medium text-gray-700">
|
||||||
상태 컬럼 (마커 색상)
|
색상 조건 컬럼
|
||||||
|
<span className="ml-1 text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={currentConfig.statusColumn || ''}
|
value={currentConfig.markerColorColumn || ""}
|
||||||
onChange={(e) => updateConfig({ statusColumn: e.target.value })}
|
onChange={(e) => updateConfig({ markerColorColumn: e.target.value })}
|
||||||
className="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs"
|
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
|
||||||
>
|
>
|
||||||
<option value="">선택하세요 (선택사항)</option>
|
<option value="">선택하세요</option>
|
||||||
{availableColumns.map((col) => (
|
{availableColumns.map((col) => (
|
||||||
<option key={col} value={col}>
|
<option key={col} value={col}>
|
||||||
{col}
|
{col}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
<p className="text-xs text-gray-500">이 컬럼의 값에 따라 마커 색상이 결정됩니다</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 기본 색상 */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="block text-xs font-medium text-gray-700">기본 색상</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={currentConfig.markerDefaultColor || "#6b7280"}
|
||||||
|
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
|
||||||
|
className="h-8 w-12 cursor-pointer rounded border border-gray-300"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={currentConfig.markerDefaultColor || "#6b7280"}
|
||||||
|
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
|
||||||
|
placeholder="#6b7280"
|
||||||
|
className="flex-1 rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500">규칙에 매칭되지 않는 경우 사용할 색상</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 색상 규칙 목록 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="block text-xs font-medium text-gray-700">색상 규칙</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={addColorRule}
|
||||||
|
className="flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1 text-xs text-white transition-colors hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />
|
||||||
|
추가
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 규칙 리스트 */}
|
||||||
|
{(currentConfig.markerColorRules || []).length === 0 ? (
|
||||||
|
<div className="rounded-lg border border-gray-200 bg-white p-3 text-center">
|
||||||
|
<p className="text-xs text-gray-500">추가 버튼을 눌러 색상 규칙을 만드세요</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(currentConfig.markerColorRules || []).map((rule) => (
|
||||||
|
<div key={rule.id} className="space-y-2 rounded-lg border border-gray-200 bg-white p-2">
|
||||||
|
{/* 규칙 헤더 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs font-medium text-gray-700">규칙</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => deleteColorRule(rule.id)}
|
||||||
|
className="text-red-500 transition-colors hover:text-red-700"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 조건 값 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="block text-xs font-medium text-gray-600">값 (조건)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={rule.value}
|
||||||
|
onChange={(e) => updateColorRule(rule.id, { value: e.target.value })}
|
||||||
|
placeholder="예: active, inactive"
|
||||||
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 색상 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="block text-xs font-medium text-gray-600">색상</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={rule.color}
|
||||||
|
onChange={(e) => updateColorRule(rule.id, { color: e.target.value })}
|
||||||
|
className="h-8 w-12 cursor-pointer rounded border border-gray-300"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={rule.color}
|
||||||
|
onChange={(e) => updateColorRule(rule.id, { color: e.target.value })}
|
||||||
|
placeholder="#3b82f6"
|
||||||
|
className="flex-1 rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 라벨 (선택사항) */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="block text-xs font-medium text-gray-600">라벨 (선택)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={rule.label || ""}
|
||||||
|
onChange={(e) => updateColorRule(rule.id, { label: e.target.value })}
|
||||||
|
placeholder="예: 활성, 비활성"
|
||||||
|
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 날씨 정보 표시 옵션 */}
|
{/* 날씨 정보 표시 옵션 */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="flex items-center gap-2 text-xs font-medium text-gray-700 cursor-pointer">
|
<label className="flex cursor-pointer items-center gap-2 text-xs font-medium text-gray-700">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={currentConfig.showWeather || false}
|
checked={currentConfig.showWeather || false}
|
||||||
onChange={(e) => updateConfig({ showWeather: e.target.checked })}
|
onChange={(e) => updateConfig({ showWeather: e.target.checked })}
|
||||||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-2 focus:ring-primary"
|
className="text-primary focus:ring-primary h-4 w-4 rounded border-gray-300 focus:ring-2"
|
||||||
/>
|
/>
|
||||||
<span>날씨 정보 표시</span>
|
<span>날씨 정보 표시</span>
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-gray-500 ml-6">
|
<p className="ml-6 text-xs text-gray-500">마커 팝업에 해당 위치의 날씨 정보를 함께 표시합니다</p>
|
||||||
마커 팝업에 해당 위치의 날씨 정보를 함께 표시합니다
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="flex items-center gap-2 text-xs font-medium text-gray-700 cursor-pointer">
|
<label className="flex cursor-pointer items-center gap-2 text-xs font-medium text-gray-700">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={currentConfig.showWeatherAlerts || false}
|
checked={currentConfig.showWeatherAlerts || false}
|
||||||
onChange={(e) => updateConfig({ showWeatherAlerts: e.target.checked })}
|
onChange={(e) => updateConfig({ showWeatherAlerts: e.target.checked })}
|
||||||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-2 focus:ring-primary"
|
className="text-primary focus:ring-primary h-4 w-4 rounded border-gray-300 focus:ring-2"
|
||||||
/>
|
/>
|
||||||
<span>기상특보 영역 표시</span>
|
<span>기상특보 영역 표시</span>
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-gray-500 ml-6">
|
<p className="ml-6 text-xs text-gray-500">
|
||||||
현재 발효 중인 기상특보(주의보/경보)를 지도에 색상 영역으로 표시합니다
|
현재 발효 중인 기상특보(주의보/경보)를 지도에 색상 영역으로 표시합니다
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 설정 미리보기 */}
|
{/* 설정 미리보기 */}
|
||||||
<div className="p-3 bg-gray-50 rounded-lg">
|
<div className="rounded-lg bg-gray-50 p-3">
|
||||||
<div className="text-xs font-medium text-gray-700 mb-2">📋 설정 미리보기</div>
|
<div className="mb-2 text-xs font-medium text-gray-700">📋 설정 미리보기</div>
|
||||||
<div className="text-xs text-muted-foreground space-y-1">
|
<div className="text-muted-foreground space-y-1 text-xs">
|
||||||
<div><strong>위도:</strong> {currentConfig.latitudeColumn || '미설정'}</div>
|
<div>
|
||||||
<div><strong>경도:</strong> {currentConfig.longitudeColumn || '미설정'}</div>
|
<strong>위도:</strong> {currentConfig.latitudeColumn || "미설정"}
|
||||||
<div><strong>라벨:</strong> {currentConfig.labelColumn || '없음'}</div>
|
</div>
|
||||||
<div><strong>상태:</strong> {currentConfig.statusColumn || '없음'}</div>
|
<div>
|
||||||
<div><strong>날씨 표시:</strong> {currentConfig.showWeather ? '활성화' : '비활성화'}</div>
|
<strong>경도:</strong> {currentConfig.longitudeColumn || "미설정"}
|
||||||
<div><strong>기상특보 표시:</strong> {currentConfig.showWeatherAlerts ? '활성화' : '비활성화'}</div>
|
</div>
|
||||||
<div><strong>데이터 개수:</strong> {queryResult.rows.length}개</div>
|
<div>
|
||||||
|
<strong>라벨:</strong> {currentConfig.labelColumn || "없음"}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>색상 모드:</strong> {currentConfig.markerColorMode === "conditional" ? "조건부" : "단일"}
|
||||||
|
</div>
|
||||||
|
{currentConfig.markerColorMode === "conditional" && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<strong>색상 조건 컬럼:</strong> {currentConfig.markerColorColumn || "미설정"}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>색상 규칙 개수:</strong> {(currentConfig.markerColorRules || []).length}개
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<strong>날씨 표시:</strong> {currentConfig.showWeather ? "활성화" : "비활성화"}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>기상특보 표시:</strong> {currentConfig.showWeatherAlerts ? "활성화" : "비활성화"}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>데이터 개수:</strong> {queryResult.rows.length}개
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 필수 필드 확인 */}
|
{/* 필수 필드 확인 */}
|
||||||
{(!currentConfig.latitudeColumn || !currentConfig.longitudeColumn) && (
|
{(!currentConfig.latitudeColumn || !currentConfig.longitudeColumn) && (
|
||||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
|
||||||
<div className="text-red-800 text-xs">
|
<div className="text-xs text-red-800">
|
||||||
⚠️ 위도와 경도 컬럼을 반드시 선택해야 지도에 표시할 수 있습니다.
|
⚠️ 위도와 경도 컬럼을 반드시 선택해야 지도에 표시할 수 있습니다.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -192,4 +439,3 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,20 @@ export interface ChartConfig {
|
||||||
statusColumn?: string; // 상태 컬럼
|
statusColumn?: string; // 상태 컬럼
|
||||||
showWeather?: boolean; // 날씨 정보 표시 여부
|
showWeather?: boolean; // 날씨 정보 표시 여부
|
||||||
showWeatherAlerts?: boolean; // 기상특보 영역 표시 여부
|
showWeatherAlerts?: boolean; // 기상특보 영역 표시 여부
|
||||||
|
|
||||||
|
// 마커 색상 설정
|
||||||
|
markerColorMode?: "single" | "conditional"; // 마커 색상 모드 (단일/조건부)
|
||||||
|
markerColorColumn?: string; // 색상 조건 컬럼
|
||||||
|
markerColorRules?: MarkerColorRule[]; // 색상 규칙 배열
|
||||||
|
markerDefaultColor?: string; // 기본 마커 색상
|
||||||
|
}
|
||||||
|
|
||||||
|
// 마커 색상 규칙
|
||||||
|
export interface MarkerColorRule {
|
||||||
|
id: string; // 고유 ID
|
||||||
|
value: string; // 컬럼 값 (예: "active", "inactive")
|
||||||
|
color: string; // 마커 색상 (hex)
|
||||||
|
label?: string; // 라벨 (선택사항)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryResult {
|
export interface QueryResult {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ interface MarkerData {
|
||||||
name: string;
|
name: string;
|
||||||
info: any;
|
info: any;
|
||||||
weather?: WeatherData | null;
|
weather?: WeatherData | null;
|
||||||
|
markerColor?: string; // 마커 색상
|
||||||
}
|
}
|
||||||
|
|
||||||
// 테이블명 한글 번역
|
// 테이블명 한글 번역
|
||||||
|
|
@ -472,6 +473,33 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
|
||||||
const latCol = element.chartConfig?.latitudeColumn || "latitude";
|
const latCol = element.chartConfig?.latitudeColumn || "latitude";
|
||||||
const lngCol = element.chartConfig?.longitudeColumn || "longitude";
|
const lngCol = element.chartConfig?.longitudeColumn || "longitude";
|
||||||
|
|
||||||
|
// 마커 색상 결정 함수
|
||||||
|
const getMarkerColor = (row: any): string => {
|
||||||
|
const colorMode = element.chartConfig?.markerColorMode || "single";
|
||||||
|
|
||||||
|
if (colorMode === "single") {
|
||||||
|
// 단일 색상 모드
|
||||||
|
return element.chartConfig?.markerDefaultColor || "#3b82f6";
|
||||||
|
} else {
|
||||||
|
// 조건부 색상 모드
|
||||||
|
const colorColumn = element.chartConfig?.markerColorColumn;
|
||||||
|
const colorRules = element.chartConfig?.markerColorRules || [];
|
||||||
|
const defaultColor = element.chartConfig?.markerDefaultColor || "#6b7280";
|
||||||
|
|
||||||
|
if (!colorColumn || colorRules.length === 0) {
|
||||||
|
return defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 컬럼 값 가져오기
|
||||||
|
const columnValue = String(row[colorColumn] || "");
|
||||||
|
|
||||||
|
// 색상 규칙 매칭
|
||||||
|
const matchedRule = colorRules.find((rule) => String(rule.value) === columnValue);
|
||||||
|
|
||||||
|
return matchedRule ? matchedRule.color : defaultColor;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 유효한 좌표 필터링 및 마커 데이터 생성
|
// 유효한 좌표 필터링 및 마커 데이터 생성
|
||||||
const markerData = rows
|
const markerData = rows
|
||||||
.filter((row: any) => row[latCol] && row[lngCol])
|
.filter((row: any) => row[latCol] && row[lngCol])
|
||||||
|
|
@ -481,6 +509,7 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
|
||||||
name: row.name || row.vehicle_number || row.warehouse_name || row.customer_name || "알 수 없음",
|
name: row.name || row.vehicle_number || row.warehouse_name || row.customer_name || "알 수 없음",
|
||||||
info: row,
|
info: row,
|
||||||
weather: null,
|
weather: null,
|
||||||
|
markerColor: getMarkerColor(row), // 마커 색상 추가
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setMarkers(markerData);
|
setMarkers(markerData);
|
||||||
|
|
@ -693,8 +722,34 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* 마커 표시 */}
|
{/* 마커 표시 */}
|
||||||
{markers.map((marker, idx) => (
|
{markers.map((marker, idx) => {
|
||||||
<Marker key={idx} position={[marker.lat, marker.lng]}>
|
// Leaflet 커스텀 아이콘 생성 (클라이언트 사이드에서만)
|
||||||
|
let customIcon;
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const L = require("leaflet");
|
||||||
|
customIcon = L.divIcon({
|
||||||
|
className: "custom-marker",
|
||||||
|
html: `
|
||||||
|
<div style="
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background-color: ${marker.markerColor || "#3b82f6"};
|
||||||
|
border: 3px solid white;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
"></div>
|
||||||
|
`,
|
||||||
|
iconSize: [30, 30],
|
||||||
|
iconAnchor: [15, 15],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Marker key={idx} position={[marker.lat, marker.lng]} icon={customIcon}>
|
||||||
<Popup>
|
<Popup>
|
||||||
<div className="min-w-[200px] text-xs">
|
<div className="min-w-[200px] text-xs">
|
||||||
{/* 마커 정보 */}
|
{/* 마커 정보 */}
|
||||||
|
|
@ -740,7 +795,8 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
</Marker>
|
</Marker>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
|
|
||||||
{/* 범례 (특보가 있을 때만 표시) */}
|
{/* 범례 (특보가 있을 때만 표시) */}
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,12 @@ export function FlowWidget({
|
||||||
|
|
||||||
// 컬럼 라벨 조회
|
// 컬럼 라벨 조회
|
||||||
const labelsResponse = await getStepColumnLabels(flowId, selectedStepId);
|
const labelsResponse = await getStepColumnLabels(flowId, selectedStepId);
|
||||||
|
console.log("🔄 새로고침 시 컬럼 라벨 조회:", {
|
||||||
|
stepId: selectedStepId,
|
||||||
|
success: labelsResponse.success,
|
||||||
|
labelsCount: labelsResponse.data ? Object.keys(labelsResponse.data).length : 0,
|
||||||
|
labels: labelsResponse.data,
|
||||||
|
});
|
||||||
if (labelsResponse.success && labelsResponse.data) {
|
if (labelsResponse.success && labelsResponse.data) {
|
||||||
setColumnLabels(labelsResponse.data);
|
setColumnLabels(labelsResponse.data);
|
||||||
}
|
}
|
||||||
|
|
@ -220,6 +226,12 @@ export function FlowWidget({
|
||||||
try {
|
try {
|
||||||
// 컬럼 라벨 조회
|
// 컬럼 라벨 조회
|
||||||
const labelsResponse = await getStepColumnLabels(flowId!, firstStep.id);
|
const labelsResponse = await getStepColumnLabels(flowId!, firstStep.id);
|
||||||
|
console.log("🏷️ 첫 번째 스텝 컬럼 라벨 조회:", {
|
||||||
|
stepId: firstStep.id,
|
||||||
|
success: labelsResponse.success,
|
||||||
|
labelsCount: labelsResponse.data ? Object.keys(labelsResponse.data).length : 0,
|
||||||
|
labels: labelsResponse.data,
|
||||||
|
});
|
||||||
if (labelsResponse.success && labelsResponse.data) {
|
if (labelsResponse.success && labelsResponse.data) {
|
||||||
setColumnLabels(labelsResponse.data);
|
setColumnLabels(labelsResponse.data);
|
||||||
}
|
}
|
||||||
|
|
@ -297,9 +309,16 @@ export function FlowWidget({
|
||||||
try {
|
try {
|
||||||
// 컬럼 라벨 조회
|
// 컬럼 라벨 조회
|
||||||
const labelsResponse = await getStepColumnLabels(flowId!, stepId);
|
const labelsResponse = await getStepColumnLabels(flowId!, stepId);
|
||||||
|
console.log("🏷️ 컬럼 라벨 조회 결과:", {
|
||||||
|
stepId,
|
||||||
|
success: labelsResponse.success,
|
||||||
|
labelsCount: labelsResponse.data ? Object.keys(labelsResponse.data).length : 0,
|
||||||
|
labels: labelsResponse.data,
|
||||||
|
});
|
||||||
if (labelsResponse.success && labelsResponse.data) {
|
if (labelsResponse.success && labelsResponse.data) {
|
||||||
setColumnLabels(labelsResponse.data);
|
setColumnLabels(labelsResponse.data);
|
||||||
} else {
|
} else {
|
||||||
|
console.warn("⚠️ 컬럼 라벨 조회 실패 또는 데이터 없음:", labelsResponse);
|
||||||
setColumnLabels({});
|
setColumnLabels({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue