Merge branch 'main' of https://g.wace.me/jskim/vexplor_dev
Some checks failed
Build and Push Images / build-and-push (push) Failing after 1m29s
Some checks failed
Build and Push Images / build-and-push (push) Failing after 1m29s
This commit is contained in:
@@ -237,7 +237,7 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
||||
|
||||
// 회사 코드 필터 (권한 그룹 멤버 관리 시 사용)
|
||||
if (companyCode && typeof companyCode === "string" && companyCode.trim()) {
|
||||
whereConditions.push(`company_code = $${paramIndex}`);
|
||||
whereConditions.push(`u.company_code = $${paramIndex}`);
|
||||
queryParams.push(companyCode.trim());
|
||||
paramIndex++;
|
||||
logger.info("회사 코드 필터 적용", { companyCode });
|
||||
@@ -246,7 +246,7 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
||||
// 최고 관리자 필터링 (회사 관리자와 일반 사용자는 최고 관리자를 볼 수 없음)
|
||||
if (req.user && req.user.companyCode !== "*") {
|
||||
// 최고 관리자가 아닌 경우, company_code가 "*"인 사용자는 제외
|
||||
whereConditions.push(`company_code != '*'`);
|
||||
whereConditions.push(`u.company_code != '*'`);
|
||||
logger.info("최고 관리자 필터링 적용", {
|
||||
userCompanyCode: req.user.companyCode,
|
||||
});
|
||||
@@ -259,15 +259,15 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
||||
const searchTerm = search.trim();
|
||||
|
||||
whereConditions.push(`(
|
||||
sabun ILIKE $${paramIndex} OR
|
||||
user_type_name ILIKE $${paramIndex} OR
|
||||
dept_name ILIKE $${paramIndex} OR
|
||||
position_name ILIKE $${paramIndex} OR
|
||||
user_id ILIKE $${paramIndex} OR
|
||||
user_name ILIKE $${paramIndex} OR
|
||||
tel ILIKE $${paramIndex} OR
|
||||
cell_phone ILIKE $${paramIndex} OR
|
||||
email ILIKE $${paramIndex}
|
||||
u.sabun ILIKE $${paramIndex} OR
|
||||
u.user_type_name ILIKE $${paramIndex} OR
|
||||
u.dept_name ILIKE $${paramIndex} OR
|
||||
u.position_name ILIKE $${paramIndex} OR
|
||||
u.user_id ILIKE $${paramIndex} OR
|
||||
u.user_name ILIKE $${paramIndex} OR
|
||||
u.tel ILIKE $${paramIndex} OR
|
||||
u.cell_phone ILIKE $${paramIndex} OR
|
||||
u.email ILIKE $${paramIndex}
|
||||
)`);
|
||||
queryParams.push(`%${searchTerm}%`);
|
||||
paramIndex++;
|
||||
@@ -277,21 +277,21 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
||||
// 단일 필드 검색
|
||||
searchType = "single";
|
||||
const fieldMap: { [key: string]: string } = {
|
||||
sabun: "sabun",
|
||||
companyName: "user_type_name",
|
||||
deptName: "dept_name",
|
||||
positionName: "position_name",
|
||||
userId: "user_id",
|
||||
userName: "user_name",
|
||||
tel: "tel",
|
||||
cellPhone: "cell_phone",
|
||||
email: "email",
|
||||
sabun: "u.sabun",
|
||||
companyName: "u.user_type_name",
|
||||
deptName: "u.dept_name",
|
||||
positionName: "u.position_name",
|
||||
userId: "u.user_id",
|
||||
userName: "u.user_name",
|
||||
tel: "u.tel",
|
||||
cellPhone: "u.cell_phone",
|
||||
email: "u.email",
|
||||
};
|
||||
|
||||
if (fieldMap[searchField as string]) {
|
||||
if (searchField === "tel") {
|
||||
whereConditions.push(
|
||||
`(tel ILIKE $${paramIndex} OR cell_phone ILIKE $${paramIndex})`
|
||||
`(u.tel ILIKE $${paramIndex} OR u.cell_phone ILIKE $${paramIndex})`
|
||||
);
|
||||
queryParams.push(`%${searchValue}%`);
|
||||
paramIndex++;
|
||||
@@ -307,13 +307,13 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
||||
} else {
|
||||
// 고급 검색 (개별 필드별 AND 조건)
|
||||
const advancedSearchFields = [
|
||||
{ param: search_sabun, field: "sabun" },
|
||||
{ param: search_companyName, field: "user_type_name" },
|
||||
{ param: search_deptName, field: "dept_name" },
|
||||
{ param: search_positionName, field: "position_name" },
|
||||
{ param: search_userId, field: "user_id" },
|
||||
{ param: search_userName, field: "user_name" },
|
||||
{ param: search_email, field: "email" },
|
||||
{ param: search_sabun, field: "u.sabun" },
|
||||
{ param: search_companyName, field: "u.user_type_name" },
|
||||
{ param: search_deptName, field: "u.dept_name" },
|
||||
{ param: search_positionName, field: "u.position_name" },
|
||||
{ param: search_userId, field: "u.user_id" },
|
||||
{ param: search_userName, field: "u.user_name" },
|
||||
{ param: search_email, field: "u.email" },
|
||||
];
|
||||
|
||||
let hasAdvancedSearch = false;
|
||||
@@ -330,7 +330,7 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
||||
// 전화번호 검색
|
||||
if (search_tel && typeof search_tel === "string" && search_tel.trim()) {
|
||||
whereConditions.push(
|
||||
`(tel ILIKE $${paramIndex} OR cell_phone ILIKE $${paramIndex})`
|
||||
`(u.tel ILIKE $${paramIndex} OR u.cell_phone ILIKE $${paramIndex})`
|
||||
);
|
||||
queryParams.push(`%${search_tel.trim()}%`);
|
||||
paramIndex++;
|
||||
@@ -354,7 +354,7 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
||||
|
||||
// 현재 로그인한 사용자의 회사 코드 필터 (슈퍼관리자가 아닌 경우)
|
||||
if (req.user && req.user.companyCode !== "*" && !companyCode) {
|
||||
whereConditions.push(`company_code = $${paramIndex}`);
|
||||
whereConditions.push(`u.company_code = $${paramIndex}`);
|
||||
queryParams.push(req.user.companyCode);
|
||||
paramIndex++;
|
||||
logger.info("사용자 회사 코드 필터 적용", {
|
||||
@@ -364,13 +364,13 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
||||
|
||||
// 기존 필터들
|
||||
if (deptCode) {
|
||||
whereConditions.push(`dept_code = $${paramIndex}`);
|
||||
whereConditions.push(`u.dept_code = $${paramIndex}`);
|
||||
queryParams.push(deptCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereConditions.push(`status = $${paramIndex}`);
|
||||
whereConditions.push(`u.status = $${paramIndex}`);
|
||||
queryParams.push(status);
|
||||
paramIndex++;
|
||||
}
|
||||
@@ -383,7 +383,7 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
||||
// 총 개수 조회
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM user_info
|
||||
FROM user_info u
|
||||
${whereClause}
|
||||
`;
|
||||
const countResult = await query<{ total: string }>(countQuery, queryParams);
|
||||
@@ -394,26 +394,28 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
||||
const offset = (Number(page) - 1) * limit;
|
||||
const usersQuery = `
|
||||
SELECT
|
||||
sabun,
|
||||
user_id,
|
||||
user_name,
|
||||
user_name_eng,
|
||||
dept_code,
|
||||
dept_name,
|
||||
position_code,
|
||||
position_name,
|
||||
email,
|
||||
tel,
|
||||
cell_phone,
|
||||
user_type,
|
||||
user_type_name,
|
||||
regdate,
|
||||
status,
|
||||
company_code,
|
||||
locale
|
||||
FROM user_info
|
||||
u.sabun,
|
||||
u.user_id,
|
||||
u.user_name,
|
||||
u.user_name_eng,
|
||||
u.dept_code,
|
||||
u.dept_name,
|
||||
u.position_code,
|
||||
u.position_name,
|
||||
u.email,
|
||||
u.tel,
|
||||
u.cell_phone,
|
||||
u.user_type,
|
||||
u.user_type_name,
|
||||
u.regdate,
|
||||
u.status,
|
||||
u.company_code,
|
||||
u.locale,
|
||||
c.company_name
|
||||
FROM user_info u
|
||||
LEFT JOIN company_mng c ON u.company_code = c.company_code
|
||||
${whereClause}
|
||||
ORDER BY regdate DESC, user_name ASC
|
||||
ORDER BY u.regdate DESC, u.user_name ASC
|
||||
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||
`;
|
||||
|
||||
@@ -436,6 +438,7 @@ export const getUserList = async (req: AuthenticatedRequest, res: Response) => {
|
||||
userTypeName: user.user_type_name || null,
|
||||
status: user.status || "active",
|
||||
companyCode: user.company_code || null,
|
||||
companyName: user.company_name || null,
|
||||
locale: user.locale || null,
|
||||
regDate: user.regdate
|
||||
? new Date(user.regdate).toISOString().split("T")[0]
|
||||
@@ -1402,10 +1405,11 @@ export async function updateMenu(
|
||||
]
|
||||
);
|
||||
|
||||
// menu_url이 비어있으면 화면 할당도 해제 (screen_menu_assignments의 is_active를 'N'으로)
|
||||
if (!menuUrl) {
|
||||
// menu_url이 비어있거나 화면관리 URL이 아니면 화면 할당 해제
|
||||
const isScreenUrl = menuUrl && (menuUrl.startsWith("/screens/") || menuUrl.startsWith("/screen/"));
|
||||
if (!menuUrl || !isScreenUrl) {
|
||||
await query(
|
||||
`UPDATE screen_menu_assignments
|
||||
`UPDATE screen_menu_assignments
|
||||
SET is_active = 'N'
|
||||
WHERE menu_objid = $1 AND company_code = $2`,
|
||||
[Number(menuId), companyCode]
|
||||
@@ -2696,6 +2700,35 @@ export const saveUser = async (req: AuthenticatedRequest, res: Response) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// SUPER_ADMIN 권한 부여는 최고관리자만 가능
|
||||
const requestUser = req.user;
|
||||
const isRequesterSuperAdmin = requestUser?.companyCode === "*" && requestUser?.userType === "SUPER_ADMIN";
|
||||
|
||||
if (userData.userType.trim() === "SUPER_ADMIN" && !isRequesterSuperAdmin) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
message: "최고 관리자 권한은 최고 관리자만 부여할 수 있습니다.",
|
||||
error: { code: "FORBIDDEN_SUPER_ADMIN_GRANT" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 기존 SUPER_ADMIN 사용자의 권한은 최고관리자만 변경 가능
|
||||
if (isUpdate && !isRequesterSuperAdmin) {
|
||||
const targetUser = await queryOne<{ user_type: string }>(
|
||||
`SELECT user_type FROM user_info WHERE user_id = $1`,
|
||||
[userData.userId?.trim()]
|
||||
);
|
||||
if (targetUser?.user_type === "SUPER_ADMIN") {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
message: "최고 관리자의 권한은 다른 최고 관리자만 변경할 수 있습니다.",
|
||||
error: { code: "FORBIDDEN_SUPER_ADMIN_MODIFY" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 비밀번호 최소 길이 검증 (신규 등록 시)
|
||||
@@ -3696,17 +3729,6 @@ export const resetUserPassword = async (
|
||||
return;
|
||||
}
|
||||
|
||||
// 비밀번호 길이 검증 (최소 4자)
|
||||
if (newPassword.length < 4) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
result: false,
|
||||
message: "비밀번호는 최소 4자 이상이어야 합니다.",
|
||||
msg: "비밀번호는 최소 4자 이상이어야 합니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Raw Query로 사용자 존재 여부 확인
|
||||
const currentUser = await queryOne<any>(
|
||||
@@ -3724,19 +3746,10 @@ export const resetUserPassword = async (
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 비밀번호 암호화 (기존 Java 로직과 동일)
|
||||
// 2. 비밀번호 암호화 (EncryptUtil 사용)
|
||||
let encryptedPassword: string;
|
||||
try {
|
||||
// EncryptUtil과 동일한 암호화 사용
|
||||
const crypto = require("crypto");
|
||||
const keyName = "ILJIAESSECRETKEY";
|
||||
const algorithm = "aes-128-ecb";
|
||||
|
||||
// AES-128-ECB 암호화
|
||||
const cipher = crypto.createCipher(algorithm, keyName);
|
||||
let encrypted = cipher.update(newPassword, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
encryptedPassword = encrypted.toUpperCase();
|
||||
encryptedPassword = EncryptUtil.encrypt(newPassword);
|
||||
} catch (encryptError) {
|
||||
logger.error("비밀번호 암호화 중 오류 발생", {
|
||||
error: encryptError,
|
||||
|
||||
@@ -89,6 +89,7 @@ export class AuthController {
|
||||
userId: userInfo.userId,
|
||||
remoteAddr,
|
||||
useType: "접속",
|
||||
companyCode: userInfo.companyCode,
|
||||
}).catch(() => {});
|
||||
|
||||
// POP 랜딩 경로 조회
|
||||
|
||||
220
backend-node/src/controllers/messengerController.ts
Normal file
220
backend-node/src/controllers/messengerController.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { messengerService } from '../services/messengerService';
|
||||
import { AuthenticatedRequest } from '../types/auth';
|
||||
import { getIo } from '../socket/socketManager';
|
||||
import path from 'path';
|
||||
|
||||
class MessengerController {
|
||||
async getRooms(req: Request, res: Response) {
|
||||
try {
|
||||
const user = (req as AuthenticatedRequest).user!;
|
||||
const rooms = await messengerService.getRooms(user.userId, user.companyCode!);
|
||||
res.json({ success: true, data: rooms });
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('getRooms error:', err.message);
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async createRoom(req: Request, res: Response) {
|
||||
try {
|
||||
const user = (req as AuthenticatedRequest).user!;
|
||||
const room_type = req.body.room_type ?? req.body.type;
|
||||
const room_name = req.body.room_name ?? req.body.name;
|
||||
const participant_ids = req.body.participant_ids ?? req.body.participantIds;
|
||||
|
||||
if (!room_type || !participant_ids || !Array.isArray(participant_ids)) {
|
||||
return res.status(400).json({ success: false, message: 'room_type and participant_ids are required.' });
|
||||
}
|
||||
|
||||
const room = await messengerService.createRoom(user.userId, user.companyCode!, {
|
||||
room_type,
|
||||
room_name,
|
||||
participant_ids,
|
||||
});
|
||||
res.json({ success: true, data: room });
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('createRoom error:', err.message);
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getMessages(req: Request, res: Response) {
|
||||
try {
|
||||
const user = (req as AuthenticatedRequest).user!;
|
||||
const roomId = parseInt(req.params.roomId, 10);
|
||||
const limit = parseInt(req.query.limit as string, 10) || 50;
|
||||
const before = req.query.before ? parseInt(req.query.before as string, 10) : undefined;
|
||||
|
||||
const messages = await messengerService.getMessages(roomId, user.userId, user.companyCode!, limit, before);
|
||||
res.json({ success: true, data: messages });
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('getMessages error:', err.message);
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(req: Request, res: Response) {
|
||||
try {
|
||||
const user = (req as AuthenticatedRequest).user!;
|
||||
const roomId = parseInt(req.params.roomId, 10);
|
||||
const content = req.body.content;
|
||||
const messageType = req.body.type ?? req.body.message_type ?? 'text';
|
||||
const parentId = req.body.parentId ?? req.body.parent_message_id ?? null;
|
||||
|
||||
if (!content) {
|
||||
return res.status(400).json({ success: false, message: 'content is required.' });
|
||||
}
|
||||
|
||||
const message = await messengerService.sendMessage(roomId, user.userId, user.companyCode!, content, messageType, parentId);
|
||||
|
||||
// Broadcast to all room participants via Socket.IO
|
||||
const io = getIo();
|
||||
if (io) {
|
||||
io.to(`${user.companyCode}:${roomId}`).emit('new_message', message);
|
||||
}
|
||||
|
||||
res.json({ success: true, data: message });
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('sendMessage error:', err.message);
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async markAsRead(req: Request, res: Response) {
|
||||
try {
|
||||
const user = (req as AuthenticatedRequest).user!;
|
||||
const roomId = parseInt(req.params.roomId, 10);
|
||||
await messengerService.markAsRead(roomId, user.userId);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('markAsRead error:', err.message);
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFile(req: Request, res: Response) {
|
||||
try {
|
||||
const user = (req as AuthenticatedRequest).user!;
|
||||
const files = req.files as Express.Multer.File[];
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
return res.status(400).json({ success: false, message: 'No files uploaded.' });
|
||||
}
|
||||
|
||||
const roomId = parseInt(req.body.room_id, 10);
|
||||
if (!roomId) {
|
||||
return res.status(400).json({ success: false, message: 'room_id is required.' });
|
||||
}
|
||||
|
||||
const io = getIo();
|
||||
const savedFiles = [];
|
||||
for (const file of files) {
|
||||
// Use a readable placeholder as content to avoid filename encoding issues
|
||||
const isImage = file.mimetype.startsWith('image/');
|
||||
const content = isImage ? '[이미지]' : '[파일]';
|
||||
|
||||
// Create a file message
|
||||
const message = await messengerService.sendMessage(
|
||||
roomId,
|
||||
user.userId,
|
||||
user.companyCode!,
|
||||
content,
|
||||
'file'
|
||||
);
|
||||
|
||||
const savedFile = await messengerService.saveFile(message.id, {
|
||||
originalName: file.originalname,
|
||||
storedName: file.filename,
|
||||
filePath: file.path,
|
||||
fileSize: file.size,
|
||||
mimeType: file.mimetype,
|
||||
});
|
||||
|
||||
message.files = [savedFile];
|
||||
|
||||
// Broadcast to room so recipients receive it in real-time
|
||||
io.to(`${user.companyCode}:${roomId}`).emit('new_message', message);
|
||||
|
||||
savedFiles.push({ message, file: savedFile });
|
||||
}
|
||||
|
||||
res.json({ success: true, data: savedFiles });
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('uploadFile error:', err.message);
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async downloadFile(req: Request, res: Response) {
|
||||
try {
|
||||
const fileId = parseInt(req.params.fileId, 10);
|
||||
const file = await messengerService.getFileById(fileId);
|
||||
|
||||
if (!file) {
|
||||
return res.status(404).json({ success: false, message: 'File not found.' });
|
||||
}
|
||||
|
||||
res.download(file.file_path, file.original_name);
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('downloadFile error:', err.message);
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getCompanyUsers(req: Request, res: Response) {
|
||||
try {
|
||||
const user = (req as AuthenticatedRequest).user!;
|
||||
const users = await messengerService.getCompanyUsers(user.companyCode!, user.userId);
|
||||
res.json({ success: true, data: users });
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('getCompanyUsers error:', err.message);
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async updateRoom(req: Request, res: Response) {
|
||||
try {
|
||||
const user = (req as AuthenticatedRequest).user!;
|
||||
const roomId = parseInt(req.params.roomId, 10);
|
||||
const { room_name } = req.body;
|
||||
|
||||
if (!room_name) {
|
||||
return res.status(400).json({ success: false, message: 'room_name is required.' });
|
||||
}
|
||||
|
||||
const room = await messengerService.updateRoom(roomId, user.companyCode!, room_name);
|
||||
if (!room) {
|
||||
return res.status(404).json({ success: false, message: 'Room not found.' });
|
||||
}
|
||||
|
||||
res.json({ success: true, data: room });
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('updateRoom error:', err.message);
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getUnreadCount(req: Request, res: Response) {
|
||||
try {
|
||||
const user = (req as AuthenticatedRequest).user!;
|
||||
const count = await messengerService.getUnreadCount(user.userId, user.companyCode!);
|
||||
res.json({ success: true, data: { unread_count: count } });
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('getUnreadCount error:', err.message);
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const messengerController = new MessengerController();
|
||||
84
backend-node/src/controllers/quoteController.ts
Normal file
84
backend-node/src/controllers/quoteController.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import * as quoteService from "../services/quoteService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { search, status, startDate, endDate } = req.query as Record<string, string>;
|
||||
|
||||
const data = await quoteService.getList(companyCode, { search, status, startDate, endDate });
|
||||
return res.json({ success: true, data });
|
||||
} catch (error: any) {
|
||||
logger.error("견적 목록 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getById(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { id } = req.params;
|
||||
|
||||
const data = await quoteService.getById(companyCode, parseInt(id));
|
||||
if (!data) {
|
||||
return res.status(404).json({ success: false, message: "견적을 찾을 수 없습니다." });
|
||||
}
|
||||
return res.json({ success: true, data });
|
||||
} catch (error: any) {
|
||||
logger.error("견적 상세 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateNumber(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const quoteNo = await quoteService.generateNumber(companyCode);
|
||||
return res.json({ success: true, data: { quoteNo } });
|
||||
} catch (error: any) {
|
||||
logger.error("견적번호 생성 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const data = await quoteService.create(companyCode, userId, req.body);
|
||||
return res.status(201).json({ success: true, data, message: "견적이 등록되었습니다." });
|
||||
} catch (error: any) {
|
||||
logger.error("견적 등록 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { id } = req.params;
|
||||
|
||||
await quoteService.update(companyCode, userId, parseInt(id), req.body);
|
||||
return res.json({ success: true, message: "견적이 수정되었습니다." });
|
||||
} catch (error: any) {
|
||||
logger.error("견적 수정 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { id } = req.params;
|
||||
|
||||
await quoteService.remove(companyCode, parseInt(id));
|
||||
return res.json({ success: true, message: "견적이 삭제되었습니다." });
|
||||
} catch (error: any) {
|
||||
logger.error("견적 삭제 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
56
backend-node/src/controllers/sampleController.ts
Normal file
56
backend-node/src/controllers/sampleController.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Request, Response } from "express";
|
||||
import { SampleService } from "../services/sampleService";
|
||||
|
||||
const sampleService = SampleService.getInstance();
|
||||
|
||||
/**
|
||||
* 목록 조회
|
||||
* GET /api/sample/list?page=1&limit=20
|
||||
*/
|
||||
export const getList = async (req: Request, res: Response): Promise<void> => {
|
||||
const page = Math.max(1, parseInt(req.query.page as string) || 1);
|
||||
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit as string) || 20));
|
||||
const result = await sampleService.getList(page, limit);
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.items,
|
||||
total: result.total,
|
||||
page,
|
||||
limit,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 등록
|
||||
* POST /api/sample
|
||||
*/
|
||||
export const create = async (req: Request, res: Response): Promise<void> => {
|
||||
const { name, description } = req.body;
|
||||
if (!name || !description) {
|
||||
res.status(400).json({ success: false, message: "name, description은 필수입니다." });
|
||||
return;
|
||||
}
|
||||
const item = await sampleService.create(name, description);
|
||||
res.status(201).json({ success: true, data: item });
|
||||
};
|
||||
|
||||
/**
|
||||
* 수정
|
||||
* PUT /api/sample/:id
|
||||
*/
|
||||
export const update = async (req: Request, res: Response): Promise<void> => {
|
||||
const { id } = req.params;
|
||||
const { name, description } = req.body;
|
||||
const item = await sampleService.update(id, name, description);
|
||||
res.status(200).json({ success: true, data: item });
|
||||
};
|
||||
|
||||
/**
|
||||
* 삭제 (soft delete)
|
||||
* DELETE /api/sample/:id
|
||||
*/
|
||||
export const remove = async (req: Request, res: Response): Promise<void> => {
|
||||
const { id } = req.params;
|
||||
await sampleService.softDelete(id);
|
||||
res.status(200).json({ success: true, message: "삭제되었습니다." });
|
||||
};
|
||||
358
backend-node/src/controllers/userMailController.ts
Normal file
358
backend-node/src/controllers/userMailController.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
import { Response } from 'express';
|
||||
import { AuthenticatedRequest } from '../types/auth';
|
||||
import { userMailAccountService } from '../services/userMailAccountService';
|
||||
import { userMailImapService } from '../services/userMailImapService';
|
||||
import { userMailSmtpService } from '../services/userMailSmtpService';
|
||||
import { encryptionService } from '../services/encryptionService';
|
||||
import { imapConnectionPool } from '../services/imapConnectionPool';
|
||||
import { mailCache } from '../services/mailCache';
|
||||
|
||||
class UserMailController {
|
||||
async listAccounts(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const userId = (req as any).user?.userId;
|
||||
if (!userId) return res.status(401).json({ success: false, message: '인증 필요' });
|
||||
|
||||
const protocol = req.query.protocol as string | undefined;
|
||||
const accounts = await userMailAccountService.getAccountsByUserId(userId, protocol);
|
||||
// 비밀번호 제거 후 반환
|
||||
const safe = accounts.map(({ password, ...rest }) => rest);
|
||||
res.json({ success: true, data: safe });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error instanceof Error ? error.message : '서버 오류' });
|
||||
}
|
||||
}
|
||||
|
||||
async createAccount(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const userId = (req as any).user?.userId;
|
||||
if (!userId) return res.status(401).json({ success: false, message: '인증 필요' });
|
||||
|
||||
// 저장 전 연결 테스트 (임시 account 객체 사용, 평문 비밀번호를 암호화해서 전달)
|
||||
const tempAccount = { ...req.body, id: 0, userId, status: 'active', password: encryptionService.encrypt(req.body.password) };
|
||||
const service = userMailImapService;
|
||||
const testResult = await service.testConnection(tempAccount);
|
||||
if (!testResult.success) {
|
||||
return res.status(400).json({ success: false, message: `연결 테스트 실패: ${testResult.message}` });
|
||||
}
|
||||
|
||||
const account = await userMailAccountService.createAccount(userId, req.body);
|
||||
const { password, ...safe } = account;
|
||||
res.status(201).json({ success: true, data: safe });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error instanceof Error ? error.message : '서버 오류' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateAccount(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const userId = (req as any).user?.userId;
|
||||
if (!userId) return res.status(401).json({ success: false, message: '인증 필요' });
|
||||
|
||||
const accountId = parseInt(req.params.accountId);
|
||||
|
||||
// 비밀번호 변경이 포함된 경우 연결 테스트
|
||||
if (req.body.password) {
|
||||
const existing = await userMailAccountService.getAccountById(accountId, userId);
|
||||
if (!existing) return res.status(404).json({ success: false, message: '계정을 찾을 수 없습니다.' });
|
||||
|
||||
const tempAccount = { ...existing, ...req.body, password: encryptionService.encrypt(req.body.password) };
|
||||
const service = userMailImapService;
|
||||
const testResult = await service.testConnection(tempAccount);
|
||||
if (!testResult.success) {
|
||||
return res.status(400).json({ success: false, message: `연결 테스트 실패: ${testResult.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
const account = await userMailAccountService.updateAccount(accountId, userId, req.body);
|
||||
if (!account) return res.status(404).json({ success: false, message: '계정을 찾을 수 없습니다.' });
|
||||
|
||||
imapConnectionPool.destroyByAccount(accountId);
|
||||
mailCache.invalidateByPrefix(`mailList:${accountId}:`);
|
||||
mailCache.invalidateByPrefix(`mailDetail:${accountId}:`);
|
||||
|
||||
const { password, ...safe } = account;
|
||||
res.json({ success: true, data: safe });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error instanceof Error ? error.message : '서버 오류' });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAccount(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const userId = (req as any).user?.userId;
|
||||
if (!userId) return res.status(401).json({ success: false, message: '인증 필요' });
|
||||
|
||||
const accountId = parseInt(req.params.accountId);
|
||||
const deleted = await userMailAccountService.deleteAccount(accountId, userId);
|
||||
if (!deleted) return res.status(404).json({ success: false, message: '계정을 찾을 수 없습니다.' });
|
||||
|
||||
imapConnectionPool.destroyByAccount(accountId);
|
||||
mailCache.invalidateByPrefix(`mailList:${accountId}:`);
|
||||
mailCache.invalidateByPrefix(`mailDetail:${accountId}:`);
|
||||
|
||||
res.json({ success: true, message: '계정이 삭제되었습니다.' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error instanceof Error ? error.message : '서버 오류' });
|
||||
}
|
||||
}
|
||||
|
||||
async testConnectionDirect(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const userId = (req as any).user?.userId;
|
||||
if (!userId) return res.status(401).json({ success: false, message: '인증 필요' });
|
||||
|
||||
const { protocol, host, port, useTls, username, password } = req.body;
|
||||
if (!protocol || !host || !port || !username || !password) {
|
||||
return res.status(400).json({ success: false, message: '필수 항목 누락' });
|
||||
}
|
||||
|
||||
const tempAccount = {
|
||||
id: 0, userId, displayName: '', email: '', protocol, host, port,
|
||||
useTls: useTls ?? true, username, status: 'active',
|
||||
password: encryptionService.encrypt(password),
|
||||
createdAt: new Date(), updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const service = userMailImapService;
|
||||
const result = await service.testConnection(tempAccount);
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error instanceof Error ? error.message : '서버 오류' });
|
||||
}
|
||||
}
|
||||
|
||||
async testConnection(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const userId = (req as any).user?.userId;
|
||||
if (!userId) return res.status(401).json({ success: false, message: '인증 필요' });
|
||||
|
||||
const accountId = parseInt(req.params.accountId);
|
||||
const account = await userMailAccountService.getAccountById(accountId, userId);
|
||||
if (!account) return res.status(404).json({ success: false, message: '계정을 찾을 수 없습니다.' });
|
||||
|
||||
const service = userMailImapService;
|
||||
const result = await service.testConnection(account);
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error instanceof Error ? error.message : '서버 오류' });
|
||||
}
|
||||
}
|
||||
|
||||
async listMails(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const userId = (req as any).user?.userId;
|
||||
if (!userId) return res.status(401).json({ success: false, message: '인증 필요' });
|
||||
|
||||
const accountId = parseInt(req.params.accountId);
|
||||
const account = await userMailAccountService.getAccountById(accountId, userId);
|
||||
if (!account) return res.status(404).json({ success: false, message: '계정을 찾을 수 없습니다.' });
|
||||
|
||||
const limit = parseInt(req.query.limit as string) || 50;
|
||||
const service = userMailImapService;
|
||||
const mails = await service.fetchMailList(account, limit);
|
||||
res.json({ success: true, data: mails });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error instanceof Error ? error.message : '서버 오류' });
|
||||
}
|
||||
}
|
||||
|
||||
async streamMails(req: AuthenticatedRequest, res: Response) {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.flushHeaders();
|
||||
|
||||
const userId = (req as any).user?.userId;
|
||||
const accountId = parseInt(req.params.accountId);
|
||||
const limit = parseInt(req.query.limit as string) || 20;
|
||||
const before = req.query.before ? parseInt(req.query.before as string) : null;
|
||||
|
||||
const account = await userMailAccountService.getAccountById(accountId, userId);
|
||||
if (!account) {
|
||||
res.write(`event: error\ndata: ${JSON.stringify({ message: '계정을 찾을 수 없습니다.' })}\n\n`);
|
||||
return res.end();
|
||||
}
|
||||
|
||||
let ended = false;
|
||||
req.on('close', () => { ended = true; });
|
||||
|
||||
await userMailImapService.fetchMailListStream(
|
||||
account, limit, before,
|
||||
(mail) => {
|
||||
if (!ended) res.write(`data: ${JSON.stringify(mail)}\n\n`);
|
||||
},
|
||||
() => {
|
||||
if (!ended) {
|
||||
res.write(`event: done\ndata: {}\n\n`);
|
||||
res.end();
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
if (!ended) {
|
||||
res.write(`event: error\ndata: ${JSON.stringify({ message: err.message })}\n\n`);
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async getMailDetail(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const userId = (req as any).user?.userId;
|
||||
if (!userId) return res.status(401).json({ success: false, message: '인증 필요' });
|
||||
|
||||
const accountId = parseInt(req.params.accountId);
|
||||
const account = await userMailAccountService.getAccountById(accountId, userId);
|
||||
if (!account) return res.status(404).json({ success: false, message: '계정을 찾을 수 없습니다.' });
|
||||
|
||||
const seqno = parseInt(req.params.seqno);
|
||||
const service = userMailImapService;
|
||||
const detail = await service.getMailDetail(account, seqno);
|
||||
if (!detail) return res.status(404).json({ success: false, message: '메일을 찾을 수 없습니다.' });
|
||||
|
||||
res.json({ success: true, data: detail });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error instanceof Error ? error.message : '서버 오류' });
|
||||
}
|
||||
}
|
||||
|
||||
async markAsRead(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const userId = (req as any).user?.userId;
|
||||
if (!userId) return res.status(401).json({ success: false, message: '인증 필요' });
|
||||
|
||||
const accountId = parseInt(req.params.accountId);
|
||||
const account = await userMailAccountService.getAccountById(accountId, userId);
|
||||
if (!account) return res.status(404).json({ success: false, message: '계정을 찾을 수 없습니다.' });
|
||||
|
||||
const seqno = parseInt(req.params.seqno);
|
||||
const service = userMailImapService;
|
||||
const result = await service.markAsRead(account, seqno);
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error instanceof Error ? error.message : '서버 오류' });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteMail(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const userId = (req as any).user?.userId;
|
||||
if (!userId) return res.status(401).json({ success: false, message: '인증 필요' });
|
||||
|
||||
const accountId = parseInt(req.params.accountId);
|
||||
const account = await userMailAccountService.getAccountById(accountId, userId);
|
||||
if (!account) return res.status(404).json({ success: false, message: '계정을 찾을 수 없습니다.' });
|
||||
|
||||
const seqno = parseInt(req.params.seqno);
|
||||
const service = userMailImapService;
|
||||
const result = await service.deleteMail(account, seqno);
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error instanceof Error ? error.message : '서버 오류' });
|
||||
}
|
||||
}
|
||||
|
||||
async listFolders(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const accountId = parseInt(req.params.accountId);
|
||||
const account = await userMailAccountService.getAccountById(accountId, (req as any).user!.userId);
|
||||
if (!account) { res.status(404).json({ success: false, message: '계정 없음' }); return; }
|
||||
const folders = await userMailImapService.listFolders(account);
|
||||
res.json({ success: true, data: folders });
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, message: err instanceof Error ? err.message : '오류' });
|
||||
}
|
||||
}
|
||||
|
||||
async streamFolderMails(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
const accountId = parseInt(req.params.accountId);
|
||||
const folder = decodeURIComponent(req.params.folder);
|
||||
const limit = parseInt(req.query.limit as string) || 20;
|
||||
const before = req.query.before ? parseInt(req.query.before as string) : null;
|
||||
|
||||
const account = await userMailAccountService.getAccountById(accountId, (req as any).user!.userId);
|
||||
if (!account) { res.status(404).json({ success: false, message: '계정 없음' }); return; }
|
||||
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.flushHeaders();
|
||||
|
||||
await userMailImapService.streamMailsByFolder(
|
||||
account, folder, limit, before,
|
||||
(mail) => {
|
||||
res.write(`event: message\ndata: ${JSON.stringify(mail)}\n\n`);
|
||||
},
|
||||
() => {
|
||||
res.write(`event: done\ndata: {}\n\n`);
|
||||
res.end();
|
||||
},
|
||||
(err) => {
|
||||
res.write(`event: error\ndata: ${JSON.stringify({ message: err.message })}\n\n`);
|
||||
res.end();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async moveMail(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const accountId = parseInt(req.params.accountId);
|
||||
const seqno = parseInt(req.params.seqno);
|
||||
const { targetFolder } = req.body;
|
||||
if (!targetFolder) { res.status(400).json({ success: false, message: 'targetFolder 필요' }); return; }
|
||||
const account = await userMailAccountService.getAccountById(accountId, (req as any).user!.userId);
|
||||
if (!account) { res.status(404).json({ success: false, message: '계정 없음' }); return; }
|
||||
const result = await userMailImapService.moveMail(account, seqno, targetFolder);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, message: err instanceof Error ? err.message : '오류' });
|
||||
}
|
||||
}
|
||||
|
||||
async getAttachments(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const accountId = parseInt(req.params.accountId);
|
||||
const seqno = parseInt(req.params.seqno);
|
||||
const account = await userMailAccountService.getAccountById(accountId, (req as any).user!.userId);
|
||||
if (!account) { res.status(404).json({ success: false, message: '계정 없음' }); return; }
|
||||
const folder = (req.query.folder as string) || 'INBOX';
|
||||
const attachments = await userMailImapService.getAttachmentList(account, seqno, folder);
|
||||
res.json({ success: true, data: attachments });
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, message: err instanceof Error ? err.message : '오류' });
|
||||
}
|
||||
}
|
||||
|
||||
async downloadAttachment(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const accountId = parseInt(req.params.accountId);
|
||||
const seqno = parseInt(req.params.seqno);
|
||||
const partId = decodeURIComponent(req.params.partId);
|
||||
const account = await userMailAccountService.getAccountById(accountId, (req as any).user!.userId);
|
||||
if (!account) { res.status(404).json({ success: false, message: '계정 없음' }); return; }
|
||||
const folder = (req.query.folder as string) || 'INBOX';
|
||||
const filenameHint = (req.params.filename as string | undefined) || (req.query.filename as string | undefined);
|
||||
await userMailImapService.downloadAttachment(account, seqno, partId, res, folder, filenameHint);
|
||||
} catch (err) {
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ success: false, message: err instanceof Error ? err.message : '오류' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async sendMail(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const accountId = parseInt(req.params.accountId);
|
||||
const account = await userMailAccountService.getAccountById(accountId, (req as any).user!.userId);
|
||||
if (!account) { res.status(404).json({ success: false, message: '계정 없음' }); return; }
|
||||
const result = await userMailSmtpService.sendMail(account, req.body);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, message: err instanceof Error ? err.message : '오류' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const userMailController = new UserMailController();
|
||||
Reference in New Issue
Block a user