Compare commits

...

6 Commits

11 changed files with 929 additions and 397 deletions

View File

@ -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(
batchName, Number(id),
description, {
cronSchedule, batchName,
mappings, description,
isActive cronSchedule,
} as UpdateBatchConfigRequest); mappings,
isActive,
} 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: "배치 설정 삭제에 실패했습니다.",
}); });
} }
} }

View File

@ -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 : "알 수 없는 오류",
}); });
} }
} }
} }

View File

@ -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: "배치 저장 중 오류가 발생했습니다.",
}); });
} }
} }

View File

@ -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",

View File

@ -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);

View File

@ -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,

View File

@ -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]

View File

@ -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(
const newConfig = { ...currentConfig, ...updates }; (updates: Partial<ChartConfig>) => {
setCurrentConfig(newConfig); const newConfig = { ...currentConfig, ...updates };
onConfigChange(newConfig); setCurrentConfig(newConfig);
}, [currentConfig, onConfigChange]); onConfigChange(newConfig);
},
[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-1.5"> <div className="space-y-2 border-t pt-3">
<label className="block text-xs font-medium text-gray-700"> <h5 className="text-xs font-semibold text-gray-800">🎨 </h5>
( )
</label> {/* 색상 모드 선택 */}
<select <div className="space-y-1.5">
value={currentConfig.statusColumn || ''} <label className="block text-xs font-medium text-gray-700"> </label>
onChange={(e) => updateConfig({ statusColumn: e.target.value })} <div className="flex gap-2">
className="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs" <button
> type="button"
<option value=""> ()</option> onClick={() => handleMarkerColorModeChange("single")}
{availableColumns.map((col) => ( className={`flex-1 rounded-lg border px-3 py-2 text-xs transition-colors ${
<option key={col} value={col}> (currentConfig.markerColorMode || "single") === "single"
{col} ? "border-blue-300 bg-blue-50 font-medium text-blue-700"
</option> : "border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
))} }`}
</select> >
</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">
<label className="block text-xs font-medium text-gray-700">
<span className="ml-1 text-red-500">*</span>
</label>
<select
value={currentConfig.markerColorColumn || ""}
onChange={(e) => updateConfig({ markerColorColumn: e.target.value })}
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
>
<option value=""></option>
{availableColumns.map((col) => (
<option key={col} value={col}>
{col}
</option>
))}
</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>
</div>
</p>
</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>
); );
} }

View File

@ -205,6 +205,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 {

View File

@ -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,54 +722,81 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
})} })}
{/* 마커 표시 */} {/* 마커 표시 */}
{markers.map((marker, idx) => ( {markers.map((marker, idx) => {
<Marker key={idx} position={[marker.lat, marker.lng]}> // Leaflet 커스텀 아이콘 생성 (클라이언트 사이드에서만)
<Popup> let customIcon;
<div className="min-w-[200px] text-xs"> if (typeof window !== "undefined") {
{/* 마커 정보 */} const L = require("leaflet");
<div className="mb-2 border-b pb-2"> customIcon = L.divIcon({
<div className="mb-1 text-sm font-bold">{marker.name}</div> className: "custom-marker",
{Object.entries(marker.info) html: `
.filter(([key]) => !["latitude", "longitude", "lat", "lng"].includes(key.toLowerCase())) <div style="
.map(([key, value]) => ( width: 30px;
<div key={key} className="text-xs"> height: 30px;
<strong>{key}:</strong> {String(value)} background-color: ${marker.markerColor || "#3b82f6"};
</div> border: 3px solid white;
))} border-radius: 50%;
</div> 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.weather && ( <Marker key={idx} position={[marker.lat, marker.lng]} icon={customIcon}>
<div className="space-y-1"> <Popup>
<div className="mb-1 flex items-center gap-2"> <div className="min-w-[200px] text-xs">
{getWeatherIcon(marker.weather.weatherMain)} {/* 마커 정보 */}
<span className="text-xs font-semibold"> </span> <div className="mb-2 border-b pb-2">
</div> <div className="mb-1 text-sm font-bold">{marker.name}</div>
<div className="text-xs text-gray-600">{marker.weather.weatherDescription}</div> {Object.entries(marker.info)
<div className="mt-2 space-y-1 text-xs"> .filter(([key]) => !["latitude", "longitude", "lat", "lng"].includes(key.toLowerCase()))
<div className="flex justify-between"> .map(([key, value]) => (
<span className="text-gray-500"></span> <div key={key} className="text-xs">
<span className="font-medium">{marker.weather.temperature}°C</span> <strong>{key}:</strong> {String(value)}
</div> </div>
<div className="flex justify-between"> ))}
<span className="text-gray-500"></span>
<span className="font-medium">{marker.weather.feelsLike}°C</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="font-medium">{marker.weather.humidity}%</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="font-medium">{marker.weather.windSpeed} m/s</span>
</div>
</div>
</div> </div>
)}
</div> {/* 날씨 정보 */}
</Popup> {marker.weather && (
</Marker> <div className="space-y-1">
))} <div className="mb-1 flex items-center gap-2">
{getWeatherIcon(marker.weather.weatherMain)}
<span className="text-xs font-semibold"> </span>
</div>
<div className="text-xs text-gray-600">{marker.weather.weatherDescription}</div>
<div className="mt-2 space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="font-medium">{marker.weather.temperature}°C</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="font-medium">{marker.weather.feelsLike}°C</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="font-medium">{marker.weather.humidity}%</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500"></span>
<span className="font-medium">{marker.weather.windSpeed} m/s</span>
</div>
</div>
</div>
)}
</div>
</Popup>
</Marker>
);
})}
</MapContainer> </MapContainer>
{/* 범례 (특보가 있을 때만 표시) */} {/* 범례 (특보가 있을 때만 표시) */}

View File

@ -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({});
} }