Merge branch 'ycshin-node' of https://g.wace.me/jskim/vexplor_dev into jskim-node

This commit is contained in:
kjs
2026-04-01 16:38:36 +09:00
98 changed files with 8015 additions and 1100 deletions

View 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();

View 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();