Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management

This commit is contained in:
kjs
2025-10-16 18:19:21 +09:00
48 changed files with 3805 additions and 905 deletions

View File

@@ -55,6 +55,7 @@ import riskAlertRoutes from "./routes/riskAlertRoutes"; // 리스크/알림 관
import todoRoutes from "./routes/todoRoutes"; // To-Do 관리
import bookingRoutes from "./routes/bookingRoutes"; // 예약 요청 관리
import mapDataRoutes from "./routes/mapDataRoutes"; // 지도 데이터 관리
import warehouseRoutes from "./routes/warehouseRoutes"; // 창고 관리
import { BatchSchedulerService } from "./services/batchSchedulerService";
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석
@@ -204,6 +205,7 @@ app.use("/api/risk-alerts", riskAlertRoutes); // 리스크/알림 관리
app.use("/api/todos", todoRoutes); // To-Do 관리
app.use("/api/bookings", bookingRoutes); // 예약 요청 관리
app.use("/api/map-data", mapDataRoutes); // 지도 데이터 조회
app.use("/api/warehouse", warehouseRoutes); // 창고 관리
// app.use("/api/collections", collectionRoutes); // 임시 주석
// app.use("/api/batch", batchRoutes); // 임시 주석
// app.use('/api/users', userRoutes);
@@ -231,6 +233,14 @@ app.listen(PORT, HOST, async () => {
logger.info(`🔗 Health check: http://${HOST}:${PORT}/health`);
logger.info(`🌐 External access: http://39.117.244.52:${PORT}/health`);
// 대시보드 마이그레이션 실행
try {
const { runDashboardMigration } = await import('./database/runMigration');
await runDashboardMigration();
} catch (error) {
logger.error(`❌ 대시보드 마이그레이션 실패:`, error);
}
// 배치 스케줄러 초기화
try {
await BatchSchedulerService.initialize();
@@ -241,7 +251,9 @@ app.listen(PORT, HOST, async () => {
// 리스크/알림 자동 갱신 시작
try {
const { RiskAlertCacheService } = await import('./services/riskAlertCacheService');
const { RiskAlertCacheService } = await import(
"./services/riskAlertCacheService"
);
const cacheService = RiskAlertCacheService.getInstance();
cacheService.startAutoRefresh();
logger.info(`⏰ 리스크/알림 자동 갱신이 시작되었습니다. (10분 간격)`);

View File

@@ -0,0 +1,97 @@
import { Request, Response } from "express";
import { WarehouseService } from "../services/WarehouseService";
export class WarehouseController {
private warehouseService: WarehouseService;
constructor() {
this.warehouseService = new WarehouseService();
}
// 창고 및 자재 데이터 조회
getWarehouseData = async (req: Request, res: Response) => {
try {
const data = await this.warehouseService.getWarehouseData();
return res.json({
success: true,
warehouses: data.warehouses,
materials: data.materials,
});
} catch (error: any) {
console.error("창고 데이터 조회 오류:", error);
return res.status(500).json({
success: false,
message: "창고 데이터를 불러오는데 실패했습니다.",
error: error.message,
});
}
};
// 특정 창고 정보 조회
getWarehouseById = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const warehouse = await this.warehouseService.getWarehouseById(id);
if (!warehouse) {
return res.status(404).json({
success: false,
message: "창고를 찾을 수 없습니다.",
});
}
return res.json({
success: true,
data: warehouse,
});
} catch (error: any) {
console.error("창고 조회 오류:", error);
return res.status(500).json({
success: false,
message: "창고 정보를 불러오는데 실패했습니다.",
error: error.message,
});
}
};
// 창고별 자재 목록 조회
getMaterialsByWarehouse = async (req: Request, res: Response) => {
try {
const { warehouseId } = req.params;
const materials =
await this.warehouseService.getMaterialsByWarehouse(warehouseId);
return res.json({
success: true,
data: materials,
});
} catch (error: any) {
console.error("자재 목록 조회 오류:", error);
return res.status(500).json({
success: false,
message: "자재 목록을 불러오는데 실패했습니다.",
error: error.message,
});
}
};
// 창고 통계 조회
getWarehouseStats = async (req: Request, res: Response) => {
try {
const stats = await this.warehouseService.getWarehouseStats();
return res.json({
success: true,
data: stats,
});
} catch (error: any) {
console.error("창고 통계 조회 오류:", error);
return res.status(500).json({
success: false,
message: "창고 통계를 불러오는데 실패했습니다.",
error: error.message,
});
}
};
}

View File

@@ -0,0 +1,42 @@
import { PostgreSQLService } from './PostgreSQLService';
/**
* 데이터베이스 마이그레이션 실행
* dashboard_elements 테이블에 custom_title, show_header 컬럼 추가
*/
export async function runDashboardMigration() {
try {
console.log('🔄 대시보드 마이그레이션 시작...');
// custom_title 컬럼 추가
await PostgreSQLService.query(`
ALTER TABLE dashboard_elements
ADD COLUMN IF NOT EXISTS custom_title VARCHAR(255)
`);
console.log('✅ custom_title 컬럼 추가 완료');
// show_header 컬럼 추가
await PostgreSQLService.query(`
ALTER TABLE dashboard_elements
ADD COLUMN IF NOT EXISTS show_header BOOLEAN DEFAULT true
`);
console.log('✅ show_header 컬럼 추가 완료');
// 기존 데이터 업데이트
await PostgreSQLService.query(`
UPDATE dashboard_elements
SET show_header = true
WHERE show_header IS NULL
`);
console.log('✅ 기존 데이터 업데이트 완료');
console.log('✅ 대시보드 마이그레이션 완료!');
} catch (error) {
console.error('❌ 대시보드 마이그레이션 실패:', error);
// 이미 컬럼이 있는 경우는 무시
if (error instanceof Error && error.message.includes('already exists')) {
console.log(' 컬럼이 이미 존재합니다.');
}
}
}

View File

@@ -0,0 +1,22 @@
import { Router } from "express";
import { WarehouseController } from "../controllers/WarehouseController";
const router = Router();
const warehouseController = new WarehouseController();
// 창고 및 자재 데이터 조회
router.get("/data", warehouseController.getWarehouseData);
// 특정 창고 정보 조회
router.get("/:id", warehouseController.getWarehouseById);
// 창고별 자재 목록 조회
router.get(
"/:warehouseId/materials",
warehouseController.getMaterialsByWarehouse
);
// 창고 통계 조회
router.get("/stats/summary", warehouseController.getWarehouseStats);
export default router;

View File

@@ -1,89 +1,100 @@
import { v4 as uuidv4 } from 'uuid';
import { PostgreSQLService } from '../database/PostgreSQLService';
import {
Dashboard,
DashboardElement,
CreateDashboardRequest,
import { v4 as uuidv4 } from "uuid";
import { PostgreSQLService } from "../database/PostgreSQLService";
import {
Dashboard,
DashboardElement,
CreateDashboardRequest,
UpdateDashboardRequest,
DashboardListQuery
} from '../types/dashboard';
DashboardListQuery,
} from "../types/dashboard";
/**
* 대시보드 서비스 - Raw Query 방식
* PostgreSQL 직접 연결을 통한 CRUD 작업
*/
export class DashboardService {
/**
* 대시보드 생성
*/
static async createDashboard(data: CreateDashboardRequest, userId: string): Promise<Dashboard> {
static async createDashboard(
data: CreateDashboardRequest,
userId: string
): Promise<Dashboard> {
const dashboardId = uuidv4();
const now = new Date();
try {
// 트랜잭션으로 대시보드와 요소들을 함께 생성
const result = await PostgreSQLService.transaction(async (client) => {
// 1. 대시보드 메인 정보 저장
await client.query(`
await client.query(
`
INSERT INTO dashboards (
id, title, description, is_public, created_by,
created_at, updated_at, tags, category, view_count
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
`, [
dashboardId,
data.title,
data.description || null,
data.isPublic || false,
userId,
now,
now,
JSON.stringify(data.tags || []),
data.category || null,
0
]);
created_at, updated_at, tags, category, view_count, settings
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`,
[
dashboardId,
data.title,
data.description || null,
data.isPublic || false,
userId,
now,
now,
JSON.stringify(data.tags || []),
data.category || null,
0,
JSON.stringify(data.settings || {}),
]
);
// 2. 대시보드 요소들 저장
if (data.elements && data.elements.length > 0) {
for (let i = 0; i < data.elements.length; i++) {
const element = data.elements[i];
const elementId = uuidv4(); // 항상 새로운 UUID 생성
await client.query(`
await client.query(
`
INSERT INTO dashboard_elements (
id, dashboard_id, element_type, element_subtype,
position_x, position_y, width, height,
title, content, data_source_config, chart_config,
title, custom_title, show_header, content, data_source_config, chart_config,
display_order, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
`, [
elementId,
dashboardId,
element.type,
element.subtype,
element.position.x,
element.position.y,
element.size.width,
element.size.height,
element.title,
element.content || null,
JSON.stringify(element.dataSource || {}),
JSON.stringify(element.chartConfig || {}),
i,
now,
now
]);
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
`,
[
elementId,
dashboardId,
element.type,
element.subtype,
element.position.x,
element.position.y,
element.size.width,
element.size.height,
element.title,
element.customTitle || null,
element.showHeader !== false, // 기본값 true
element.content || null,
JSON.stringify(element.dataSource || {}),
JSON.stringify(element.chartConfig || {}),
i,
now,
now,
]
);
}
}
return dashboardId;
});
// 생성된 대시보드 반환
try {
const dashboard = await this.getDashboardById(dashboardId, userId);
if (!dashboard) {
console.error('대시보드 생성은 성공했으나 조회에 실패:', dashboardId);
console.error("대시보드 생성은 성공했으나 조회에 실패:", dashboardId);
// 생성은 성공했으므로 기본 정보만이라도 반환
return {
id: dashboardId,
@@ -97,13 +108,13 @@ export class DashboardService {
tags: data.tags || [],
category: data.category,
viewCount: 0,
elements: data.elements || []
elements: data.elements || [],
};
}
return dashboard;
} catch (fetchError) {
console.error('생성된 대시보드 조회 중 오류:', fetchError);
console.error("생성된 대시보드 조회 중 오류:", fetchError);
// 생성은 성공했으므로 기본 정보 반환
return {
id: dashboardId,
@@ -117,76 +128,79 @@ export class DashboardService {
tags: data.tags || [],
category: data.category,
viewCount: 0,
elements: data.elements || []
elements: data.elements || [],
};
}
} catch (error) {
console.error('Dashboard creation error:', error);
console.error("Dashboard creation error:", error);
throw error;
}
}
/**
* 대시보드 목록 조회
*/
static async getDashboards(query: DashboardListQuery, userId?: string) {
const {
page = 1,
limit = 20,
search,
category,
isPublic,
createdBy
const {
page = 1,
limit = 20,
search,
category,
isPublic,
createdBy,
} = query;
const offset = (page - 1) * limit;
try {
// 기본 WHERE 조건
let whereConditions = ['d.deleted_at IS NULL'];
let whereConditions = ["d.deleted_at IS NULL"];
let params: any[] = [];
let paramIndex = 1;
// 권한 필터링
if (userId) {
whereConditions.push(`(d.created_by = $${paramIndex} OR d.is_public = true)`);
whereConditions.push(
`(d.created_by = $${paramIndex} OR d.is_public = true)`
);
params.push(userId);
paramIndex++;
} else {
whereConditions.push('d.is_public = true');
whereConditions.push("d.is_public = true");
}
// 검색 조건
if (search) {
whereConditions.push(`(d.title ILIKE $${paramIndex} OR d.description ILIKE $${paramIndex + 1})`);
whereConditions.push(
`(d.title ILIKE $${paramIndex} OR d.description ILIKE $${paramIndex + 1})`
);
params.push(`%${search}%`, `%${search}%`);
paramIndex += 2;
}
// 카테고리 필터
if (category) {
whereConditions.push(`d.category = $${paramIndex}`);
params.push(category);
paramIndex++;
}
// 공개/비공개 필터
if (typeof isPublic === 'boolean') {
if (typeof isPublic === "boolean") {
whereConditions.push(`d.is_public = $${paramIndex}`);
params.push(isPublic);
paramIndex++;
}
// 작성자 필터
if (createdBy) {
whereConditions.push(`d.created_by = $${paramIndex}`);
params.push(createdBy);
paramIndex++;
}
const whereClause = whereConditions.join(' AND ');
const whereClause = whereConditions.join(" AND ");
// 대시보드 목록 조회 (users 테이블 조인 제거)
const dashboardQuery = `
SELECT
@@ -211,22 +225,23 @@ export class DashboardService {
ORDER BY d.updated_at DESC
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`;
const dashboardResult = await PostgreSQLService.query(
dashboardQuery,
[...params, limit, offset]
);
const dashboardResult = await PostgreSQLService.query(dashboardQuery, [
...params,
limit,
offset,
]);
// 전체 개수 조회
const countQuery = `
SELECT COUNT(DISTINCT d.id) as total
FROM dashboards d
WHERE ${whereClause}
`;
const countResult = await PostgreSQLService.query(countQuery, params);
const total = parseInt(countResult.rows[0]?.total || '0');
const total = parseInt(countResult.rows[0]?.total || "0");
return {
dashboards: dashboardResult.rows.map((row: any) => ({
id: row.id,
@@ -237,33 +252,36 @@ export class DashboardService {
createdBy: row.created_by,
createdAt: row.created_at,
updatedAt: row.updated_at,
tags: JSON.parse(row.tags || '[]'),
tags: JSON.parse(row.tags || "[]"),
category: row.category,
viewCount: parseInt(row.view_count || '0'),
elementsCount: parseInt(row.elements_count || '0')
viewCount: parseInt(row.view_count || "0"),
elementsCount: parseInt(row.elements_count || "0"),
})),
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
totalPages: Math.ceil(total / limit),
},
};
} catch (error) {
console.error('Dashboard list error:', error);
console.error("Dashboard list error:", error);
throw error;
}
}
/**
* 대시보드 상세 조회
*/
static async getDashboardById(dashboardId: string, userId?: string): Promise<Dashboard | null> {
static async getDashboardById(
dashboardId: string,
userId?: string
): Promise<Dashboard | null> {
try {
// 1. 대시보드 기본 정보 조회 (권한 체크 포함)
let dashboardQuery: string;
let dashboardParams: any[];
if (userId) {
dashboardQuery = `
SELECT d.*
@@ -281,43 +299,52 @@ export class DashboardService {
`;
dashboardParams = [dashboardId];
}
const dashboardResult = await PostgreSQLService.query(dashboardQuery, dashboardParams);
const dashboardResult = await PostgreSQLService.query(
dashboardQuery,
dashboardParams
);
if (dashboardResult.rows.length === 0) {
return null;
}
const dashboard = dashboardResult.rows[0];
// 2. 대시보드 요소들 조회
const elementsQuery = `
SELECT * FROM dashboard_elements
WHERE dashboard_id = $1
ORDER BY display_order ASC
`;
const elementsResult = await PostgreSQLService.query(elementsQuery, [dashboardId]);
const elementsResult = await PostgreSQLService.query(elementsQuery, [
dashboardId,
]);
// 3. 요소 데이터 변환
const elements: DashboardElement[] = elementsResult.rows.map((row: any) => ({
id: row.id,
type: row.element_type,
subtype: row.element_subtype,
position: {
x: row.position_x,
y: row.position_y
},
size: {
width: row.width,
height: row.height
},
title: row.title,
content: row.content,
dataSource: JSON.parse(row.data_source_config || '{}'),
chartConfig: JSON.parse(row.chart_config || '{}')
}));
const elements: DashboardElement[] = elementsResult.rows.map(
(row: any) => ({
id: row.id,
type: row.element_type,
subtype: row.element_subtype,
position: {
x: row.position_x,
y: row.position_y,
},
size: {
width: row.width,
height: row.height,
},
title: row.title,
customTitle: row.custom_title || undefined,
showHeader: row.show_header !== false, // 기본값 true
content: row.content,
dataSource: JSON.parse(row.data_source_config || "{}"),
chartConfig: JSON.parse(row.chart_config || "{}"),
})
);
return {
id: dashboard.id,
title: dashboard.title,
@@ -327,44 +354,48 @@ export class DashboardService {
createdBy: dashboard.created_by,
createdAt: dashboard.created_at,
updatedAt: dashboard.updated_at,
tags: JSON.parse(dashboard.tags || '[]'),
tags: JSON.parse(dashboard.tags || "[]"),
category: dashboard.category,
viewCount: parseInt(dashboard.view_count || '0'),
elements
viewCount: parseInt(dashboard.view_count || "0"),
settings: dashboard.settings || undefined,
elements,
};
} catch (error) {
console.error('Dashboard get error:', error);
console.error("Dashboard get error:", error);
throw error;
}
}
/**
* 대시보드 업데이트
*/
static async updateDashboard(
dashboardId: string,
data: UpdateDashboardRequest,
dashboardId: string,
data: UpdateDashboardRequest,
userId: string
): Promise<Dashboard | null> {
try {
const result = await PostgreSQLService.transaction(async (client) => {
// 권한 체크
const authCheckResult = await client.query(`
const authCheckResult = await client.query(
`
SELECT id FROM dashboards
WHERE id = $1 AND created_by = $2 AND deleted_at IS NULL
`, [dashboardId, userId]);
`,
[dashboardId, userId]
);
if (authCheckResult.rows.length === 0) {
throw new Error('대시보드를 찾을 수 없거나 수정 권한이 없습니다.');
throw new Error("대시보드를 찾을 수 없거나 수정 권한이 없습니다.");
}
const now = new Date();
// 1. 대시보드 메인 정보 업데이트
const updateFields: string[] = [];
const updateParams: any[] = [];
let paramIndex = 1;
if (data.title !== undefined) {
updateFields.push(`title = $${paramIndex}`);
updateParams.push(data.title);
@@ -390,120 +421,143 @@ export class DashboardService {
updateParams.push(data.category);
paramIndex++;
}
if (data.settings !== undefined) {
updateFields.push(`settings = $${paramIndex}`);
updateParams.push(JSON.stringify(data.settings));
paramIndex++;
}
updateFields.push(`updated_at = $${paramIndex}`);
updateParams.push(now);
paramIndex++;
updateParams.push(dashboardId);
if (updateFields.length > 1) { // updated_at 외에 다른 필드가 있는 경우
if (updateFields.length > 1) {
// updated_at 외에 다른 필드가 있는 경우
const updateQuery = `
UPDATE dashboards
SET ${updateFields.join(', ')}
SET ${updateFields.join(", ")}
WHERE id = $${paramIndex}
`;
await client.query(updateQuery, updateParams);
}
// 2. 요소 업데이트 (있는 경우)
if (data.elements) {
// 기존 요소들 삭제
await client.query(`
await client.query(
`
DELETE FROM dashboard_elements WHERE dashboard_id = $1
`, [dashboardId]);
`,
[dashboardId]
);
// 새 요소들 추가
for (let i = 0; i < data.elements.length; i++) {
const element = data.elements[i];
const elementId = uuidv4();
await client.query(`
await client.query(
`
INSERT INTO dashboard_elements (
id, dashboard_id, element_type, element_subtype,
position_x, position_y, width, height,
title, content, data_source_config, chart_config,
title, custom_title, show_header, content, data_source_config, chart_config,
display_order, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
`, [
elementId,
dashboardId,
element.type,
element.subtype,
element.position.x,
element.position.y,
element.size.width,
element.size.height,
element.title,
element.content || null,
JSON.stringify(element.dataSource || {}),
JSON.stringify(element.chartConfig || {}),
i,
now,
now
]);
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
`,
[
elementId,
dashboardId,
element.type,
element.subtype,
element.position.x,
element.position.y,
element.size.width,
element.size.height,
element.title,
element.customTitle || null,
element.showHeader !== false, // 기본값 true
element.content || null,
JSON.stringify(element.dataSource || {}),
JSON.stringify(element.chartConfig || {}),
i,
now,
now,
]
);
}
}
return dashboardId;
});
// 업데이트된 대시보드 반환
return await this.getDashboardById(dashboardId, userId);
} catch (error) {
console.error('Dashboard update error:', error);
console.error("Dashboard update error:", error);
throw error;
}
}
/**
* 대시보드 삭제 (소프트 삭제)
*/
static async deleteDashboard(dashboardId: string, userId: string): Promise<boolean> {
static async deleteDashboard(
dashboardId: string,
userId: string
): Promise<boolean> {
try {
const now = new Date();
const result = await PostgreSQLService.query(`
const result = await PostgreSQLService.query(
`
UPDATE dashboards
SET deleted_at = $1, updated_at = $2
WHERE id = $3 AND created_by = $4 AND deleted_at IS NULL
`, [now, now, dashboardId, userId]);
`,
[now, now, dashboardId, userId]
);
return (result.rowCount || 0) > 0;
} catch (error) {
console.error('Dashboard delete error:', error);
console.error("Dashboard delete error:", error);
throw error;
}
}
/**
* 조회수 증가
*/
static async incrementViewCount(dashboardId: string): Promise<void> {
try {
await PostgreSQLService.query(`
await PostgreSQLService.query(
`
UPDATE dashboards
SET view_count = view_count + 1
WHERE id = $1 AND deleted_at IS NULL
`, [dashboardId]);
`,
[dashboardId]
);
} catch (error) {
console.error('View count increment error:', error);
console.error("View count increment error:", error);
// 조회수 증가 실패는 치명적이지 않으므로 에러를 던지지 않음
}
}
/**
* 사용자 권한 체크
*/
static async checkUserPermission(
dashboardId: string,
userId: string,
requiredPermission: 'view' | 'edit' | 'admin' = 'view'
dashboardId: string,
userId: string,
requiredPermission: "view" | "edit" | "admin" = "view"
): Promise<boolean> {
try {
const result = await PostgreSQLService.query(`
const result = await PostgreSQLService.query(
`
SELECT
CASE
WHEN d.created_by = $2 THEN 'admin'
@@ -512,23 +566,26 @@ export class DashboardService {
END as permission
FROM dashboards d
WHERE d.id = $1 AND d.deleted_at IS NULL
`, [dashboardId, userId]);
`,
[dashboardId, userId]
);
if (result.rows.length === 0) {
return false;
}
const userPermission = result.rows[0].permission;
// 권한 레벨 체크
const permissionLevels = { 'view': 1, 'edit': 2, 'admin': 3 };
const userLevel = permissionLevels[userPermission as keyof typeof permissionLevels] || 0;
const permissionLevels = { view: 1, edit: 2, admin: 3 };
const userLevel =
permissionLevels[userPermission as keyof typeof permissionLevels] || 0;
const requiredLevel = permissionLevels[requiredPermission];
return userLevel >= requiredLevel;
} catch (error) {
console.error('Permission check error:', error);
console.error("Permission check error:", error);
return false;
}
}
}
}

View File

@@ -0,0 +1,170 @@
import pool from "../database/db";
export class WarehouseService {
// 창고 및 자재 데이터 조회
async getWarehouseData() {
try {
// 창고 목록 조회
const warehousesResult = await pool.query(`
SELECT
id,
name,
position_x,
position_y,
position_z,
size_x,
size_y,
size_z,
color,
capacity,
current_usage,
status,
description,
created_at,
updated_at
FROM warehouse
WHERE status = 'active'
ORDER BY id
`);
// 자재 목록 조회
const materialsResult = await pool.query(`
SELECT
id,
warehouse_id,
name,
material_code,
quantity,
unit,
position_x,
position_y,
position_z,
size_x,
size_y,
size_z,
color,
status,
last_updated,
created_at
FROM warehouse_material
ORDER BY warehouse_id, id
`);
return {
warehouses: warehousesResult,
materials: materialsResult,
};
} catch (error) {
throw error;
}
}
// 특정 창고 정보 조회
async getWarehouseById(id: string) {
try {
const result = await pool.query(
`
SELECT
id,
name,
position_x,
position_y,
position_z,
size_x,
size_y,
size_z,
color,
capacity,
current_usage,
status,
description,
created_at,
updated_at
FROM warehouse
WHERE id = $1
`,
[id]
);
return result[0] || null;
} catch (error) {
throw error;
}
}
// 창고별 자재 목록 조회
async getMaterialsByWarehouse(warehouseId: string) {
try {
const result = await pool.query(
`
SELECT
id,
warehouse_id,
name,
material_code,
quantity,
unit,
position_x,
position_y,
position_z,
size_x,
size_y,
size_z,
color,
status,
last_updated,
created_at
FROM warehouse_material
WHERE warehouse_id = $1
ORDER BY id
`,
[warehouseId]
);
return result;
} catch (error) {
throw error;
}
}
// 창고 통계 조회
async getWarehouseStats() {
try {
const result = await pool.query(`
SELECT
COUNT(DISTINCT w.id) as total_warehouses,
COUNT(m.id) as total_materials,
SUM(w.capacity) as total_capacity,
SUM(w.current_usage) as total_usage,
ROUND(AVG((w.current_usage::numeric / NULLIF(w.capacity, 0)) * 100), 2) as avg_usage_percent
FROM warehouse w
LEFT JOIN warehouse_material m ON w.id = m.warehouse_id
WHERE w.status = 'active'
`);
// 상태별 자재 수
const statusResult = await pool.query(`
SELECT
status,
COUNT(*) as count
FROM warehouse_material
GROUP BY status
`);
const statusCounts = statusResult.reduce(
(acc: Record<string, number>, row: any) => {
acc[row.status] = parseInt(row.count);
return acc;
},
{} as Record<string, number>
);
return {
...result[0],
materialsByStatus: statusCounts,
};
} catch (error) {
throw error;
}
}
}

View File

@@ -53,13 +53,20 @@ export class BookingService {
}
private ensureDataDirectory(): void {
if (!fs.existsSync(BOOKING_DIR)) {
fs.mkdirSync(BOOKING_DIR, { recursive: true });
logger.info(`📁 예약 데이터 디렉토리 생성: ${BOOKING_DIR}`);
}
if (!fs.existsSync(BOOKING_FILE)) {
fs.writeFileSync(BOOKING_FILE, JSON.stringify([], null, 2));
logger.info(`📄 예약 파일 생성: ${BOOKING_FILE}`);
try {
if (!fs.existsSync(BOOKING_DIR)) {
fs.mkdirSync(BOOKING_DIR, { recursive: true, mode: 0o755 });
logger.info(`📁 예약 데이터 디렉토리 생성: ${BOOKING_DIR}`);
}
if (!fs.existsSync(BOOKING_FILE)) {
fs.writeFileSync(BOOKING_FILE, JSON.stringify([], null, 2), {
mode: 0o644,
});
logger.info(`📄 예약 파일 생성: ${BOOKING_FILE}`);
}
} catch (error) {
logger.error(`❌ 예약 디렉토리 생성 실패: ${BOOKING_DIR}`, error);
throw error;
}
}
@@ -111,13 +118,16 @@ export class BookingService {
priority?: string;
}): Promise<{ bookings: BookingRequest[]; newCount: number }> {
try {
const bookings = DATA_SOURCE === "database"
? await this.loadBookingsFromDB(filter)
: this.loadBookingsFromFile(filter);
const bookings =
DATA_SOURCE === "database"
? await this.loadBookingsFromDB(filter)
: this.loadBookingsFromFile(filter);
bookings.sort((a, b) => {
if (a.priority !== b.priority) return a.priority === "urgent" ? -1 : 1;
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
return (
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
});
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
@@ -145,7 +155,10 @@ export class BookingService {
}
}
public async rejectBooking(id: string, reason?: string): Promise<BookingRequest> {
public async rejectBooking(
id: string,
reason?: string
): Promise<BookingRequest> {
try {
if (DATA_SOURCE === "database") {
return await this.rejectBookingDB(id, reason);
@@ -194,9 +207,15 @@ export class BookingService {
scheduledTime: new Date(row.scheduledTime).toISOString(),
createdAt: new Date(row.createdAt).toISOString(),
updatedAt: new Date(row.updatedAt).toISOString(),
acceptedAt: row.acceptedAt ? new Date(row.acceptedAt).toISOString() : undefined,
rejectedAt: row.rejectedAt ? new Date(row.rejectedAt).toISOString() : undefined,
completedAt: row.completedAt ? new Date(row.completedAt).toISOString() : undefined,
acceptedAt: row.acceptedAt
? new Date(row.acceptedAt).toISOString()
: undefined,
rejectedAt: row.rejectedAt
? new Date(row.rejectedAt).toISOString()
: undefined,
completedAt: row.completedAt
? new Date(row.completedAt).toISOString()
: undefined,
}));
}
@@ -230,7 +249,10 @@ export class BookingService {
};
}
private async rejectBookingDB(id: string, reason?: string): Promise<BookingRequest> {
private async rejectBookingDB(
id: string,
reason?: string
): Promise<BookingRequest> {
const rows = await query(
`UPDATE booking_requests
SET status = 'rejected', rejected_at = NOW(), updated_at = NOW(), rejection_reason = $2

View File

@@ -33,11 +33,7 @@ class MailAccountFileService {
try {
await fs.access(this.accountsDir);
} catch {
try {
await fs.mkdir(this.accountsDir, { recursive: true });
} catch (error) {
console.error("메일 계정 디렉토리 생성 실패:", error);
}
await fs.mkdir(this.accountsDir, { recursive: true, mode: 0o755 });
}
}

View File

@@ -59,11 +59,7 @@ export class MailReceiveBasicService {
try {
await fs.access(this.attachmentsDir);
} catch {
try {
await fs.mkdir(this.attachmentsDir, { recursive: true });
} catch (error) {
console.error("메일 첨부파일 디렉토리 생성 실패:", error);
}
await fs.mkdir(this.attachmentsDir, { recursive: true, mode: 0o755 });
}
}

View File

@@ -20,15 +20,13 @@ const SENT_MAIL_DIR =
class MailSentHistoryService {
constructor() {
// 디렉토리 생성 (없으면) - try-catch로 권한 에러 방지
try {
if (!fs.existsSync(SENT_MAIL_DIR)) {
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true });
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true, mode: 0o755 });
}
} catch (error) {
console.error("메일 발송 이력 디렉토리 생성 실패:", error);
// 디렉토리가 이미 존재하거나 권한이 없어도 서비스는 계속 실행
// 실제 파일 쓰기 시점에 에러 처리
throw error;
}
}
@@ -45,13 +43,15 @@ class MailSentHistoryService {
};
try {
// 디렉토리가 없으면 다시 시도
if (!fs.existsSync(SENT_MAIL_DIR)) {
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true });
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true, mode: 0o755 });
}
const filePath = path.join(SENT_MAIL_DIR, `${history.id}.json`);
fs.writeFileSync(filePath, JSON.stringify(history, null, 2), "utf-8");
fs.writeFileSync(filePath, JSON.stringify(history, null, 2), {
encoding: "utf-8",
mode: 0o644,
});
console.log("발송 이력 저장:", history.id);
} catch (error) {

View File

@@ -54,17 +54,13 @@ class MailTemplateFileService {
}
/**
* 템플릿 디렉토리 생성 (없으면) - try-catch로 권한 에러 방지
* 템플릿 디렉토리 생성
*/
private async ensureDirectoryExists() {
try {
await fs.access(this.templatesDir);
} catch {
try {
await fs.mkdir(this.templatesDir, { recursive: true });
} catch (error) {
console.error("메일 템플릿 디렉토리 생성 실패:", error);
}
await fs.mkdir(this.templatesDir, { recursive: true, mode: 0o755 });
}
}

View File

@@ -61,13 +61,20 @@ export class TodoService {
* 데이터 디렉토리 생성 (파일 모드)
*/
private ensureDataDirectory(): void {
if (!fs.existsSync(TODO_DIR)) {
fs.mkdirSync(TODO_DIR, { recursive: true });
logger.info(`📁 To-Do 데이터 디렉토리 생성: ${TODO_DIR}`);
}
if (!fs.existsSync(TODO_FILE)) {
fs.writeFileSync(TODO_FILE, JSON.stringify([], null, 2));
logger.info(`📄 To-Do 파일 생성: ${TODO_FILE}`);
try {
if (!fs.existsSync(TODO_DIR)) {
fs.mkdirSync(TODO_DIR, { recursive: true, mode: 0o755 });
logger.info(`📁 To-Do 데이터 디렉토리 생성: ${TODO_DIR}`);
}
if (!fs.existsSync(TODO_FILE)) {
fs.writeFileSync(TODO_FILE, JSON.stringify([], null, 2), {
mode: 0o644,
});
logger.info(`📄 To-Do 파일 생성: ${TODO_FILE}`);
}
} catch (error) {
logger.error(`❌ To-Do 디렉토리 생성 실패: ${TODO_DIR}`, error);
throw error;
}
}
@@ -80,15 +87,17 @@ export class TodoService {
assignedTo?: string;
}): Promise<TodoListResponse> {
try {
const todos = DATA_SOURCE === "database"
? await this.loadTodosFromDB(filter)
: this.loadTodosFromFile(filter);
const todos =
DATA_SOURCE === "database"
? await this.loadTodosFromDB(filter)
: this.loadTodosFromFile(filter);
// 정렬: 긴급 > 우선순위 > 순서
todos.sort((a, b) => {
if (a.isUrgent !== b.isUrgent) return a.isUrgent ? -1 : 1;
const priorityOrder = { urgent: 0, high: 1, normal: 2, low: 3 };
if (a.priority !== b.priority) return priorityOrder[a.priority] - priorityOrder[b.priority];
if (a.priority !== b.priority)
return priorityOrder[a.priority] - priorityOrder[b.priority];
return a.order - b.order;
});
@@ -124,7 +133,8 @@ export class TodoService {
await this.createTodoDB(newTodo);
} else {
const todos = this.loadTodosFromFile();
newTodo.order = todos.length > 0 ? Math.max(...todos.map((t) => t.order)) + 1 : 0;
newTodo.order =
todos.length > 0 ? Math.max(...todos.map((t) => t.order)) + 1 : 0;
todos.push(newTodo);
this.saveTodosToFile(todos);
}
@@ -140,7 +150,10 @@ export class TodoService {
/**
* To-Do 항목 수정
*/
public async updateTodo(id: string, updates: Partial<TodoItem>): Promise<TodoItem> {
public async updateTodo(
id: string,
updates: Partial<TodoItem>
): Promise<TodoItem> {
try {
if (DATA_SOURCE === "database") {
return await this.updateTodoDB(id, updates);
@@ -231,7 +244,9 @@ export class TodoService {
dueDate: row.dueDate ? new Date(row.dueDate).toISOString() : undefined,
createdAt: new Date(row.createdAt).toISOString(),
updatedAt: new Date(row.updatedAt).toISOString(),
completedAt: row.completedAt ? new Date(row.completedAt).toISOString() : undefined,
completedAt: row.completedAt
? new Date(row.completedAt).toISOString()
: undefined,
}));
}
@@ -263,7 +278,10 @@ export class TodoService {
);
}
private async updateTodoDB(id: string, updates: Partial<TodoItem>): Promise<TodoItem> {
private async updateTodoDB(
id: string,
updates: Partial<TodoItem>
): Promise<TodoItem> {
const setClauses: string[] = ["updated_at = NOW()"];
const params: any[] = [];
let paramIndex = 1;
@@ -327,12 +345,17 @@ export class TodoService {
dueDate: row.dueDate ? new Date(row.dueDate).toISOString() : undefined,
createdAt: new Date(row.createdAt).toISOString(),
updatedAt: new Date(row.updatedAt).toISOString(),
completedAt: row.completedAt ? new Date(row.completedAt).toISOString() : undefined,
completedAt: row.completedAt
? new Date(row.completedAt).toISOString()
: undefined,
};
}
private async deleteTodoDB(id: string): Promise<void> {
const rows = await query("DELETE FROM todo_items WHERE id = $1 RETURNING id", [id]);
const rows = await query(
"DELETE FROM todo_items WHERE id = $1 RETURNING id",
[id]
);
if (rows.length === 0) {
throw new Error(`To-Do 항목을 찾을 수 없습니다: ${id}`);
}
@@ -443,7 +466,10 @@ export class TodoService {
inProgress: todos.filter((t) => t.status === "in_progress").length,
completed: todos.filter((t) => t.status === "completed").length,
urgent: todos.filter((t) => t.isUrgent).length,
overdue: todos.filter((t) => t.dueDate && new Date(t.dueDate) < now && t.status !== "completed").length,
overdue: todos.filter(
(t) =>
t.dueDate && new Date(t.dueDate) < now && t.status !== "completed"
).length,
};
}
}

View File

@@ -4,8 +4,8 @@
export interface DashboardElement {
id: string;
type: 'chart' | 'widget';
subtype: 'bar' | 'pie' | 'line' | 'exchange' | 'weather';
type: "chart" | "widget";
subtype: "bar" | "pie" | "line" | "exchange" | "weather";
position: {
x: number;
y: number;
@@ -15,9 +15,11 @@ export interface DashboardElement {
height: number;
};
title: string;
customTitle?: string; // 사용자 정의 제목 (옵션)
showHeader?: boolean; // 헤더 표시 여부 (기본값: true)
content?: string;
dataSource?: {
type: 'api' | 'database' | 'static';
type: "api" | "database" | "static";
endpoint?: string;
query?: string;
refreshInterval?: number;
@@ -28,7 +30,7 @@ export interface DashboardElement {
xAxis?: string;
yAxis?: string;
groupBy?: string;
aggregation?: 'sum' | 'avg' | 'count' | 'max' | 'min';
aggregation?: "sum" | "avg" | "count" | "max" | "min";
colors?: string[];
title?: string;
showLegend?: boolean;
@@ -48,6 +50,10 @@ export interface Dashboard {
tags?: string[];
category?: string;
viewCount: number;
settings?: {
resolution?: string;
backgroundColor?: string;
};
elements: DashboardElement[];
}
@@ -58,6 +64,10 @@ export interface CreateDashboardRequest {
elements: DashboardElement[];
tags?: string[];
category?: string;
settings?: {
resolution?: string;
backgroundColor?: string;
};
}
export interface UpdateDashboardRequest {
@@ -67,6 +77,10 @@ export interface UpdateDashboardRequest {
elements?: DashboardElement[];
tags?: string[];
category?: string;
settings?: {
resolution?: string;
backgroundColor?: string;
};
}
export interface DashboardListQuery {
@@ -83,7 +97,7 @@ export interface DashboardShare {
dashboardId: string;
sharedWithUser?: string;
sharedWithRole?: string;
permissionLevel: 'view' | 'edit' | 'admin';
permissionLevel: "view" | "edit" | "admin";
createdBy: string;
createdAt: string;
expiresAt?: string;