1090 lines
32 KiB
JavaScript
1090 lines
32 KiB
JavaScript
|
|
/**
|
||
|
|
* 메일 라우터
|
||
|
|
* 메일 CRUD, 읽음/별표 처리, 휴지통 관리, 첨부파일 다운로드 등
|
||
|
|
*/
|
||
|
|
const express = require("express");
|
||
|
|
const router = express.Router();
|
||
|
|
const { Op } = require("sequelize");
|
||
|
|
const sequelize = require("../config/database");
|
||
|
|
const SentLog = require("../models/SentLog");
|
||
|
|
const SystemConfig = require("../models/SystemConfig");
|
||
|
|
const User = require("../models/User");
|
||
|
|
const { getObjectStream } = require("../services/s3Service");
|
||
|
|
const { sendEmail } = require("../services/emailService");
|
||
|
|
const sseService = require("../services/sseService");
|
||
|
|
const { authenticateToken } = require("../middleware/auth");
|
||
|
|
const jwt = require("jsonwebtoken");
|
||
|
|
|
||
|
|
// 공통 헬퍼 함수 import
|
||
|
|
const {
|
||
|
|
getModel,
|
||
|
|
normalizeEmail,
|
||
|
|
calculateStorageSize,
|
||
|
|
findEmail,
|
||
|
|
safeRollback,
|
||
|
|
MAILBOX_MODELS,
|
||
|
|
} = require("../utils/helpers");
|
||
|
|
const { calculateEmailSize } = require("../utils/emailUtils");
|
||
|
|
|
||
|
|
// 자주 사용하는 모델 destructuring
|
||
|
|
const {
|
||
|
|
INBOX: Inbox,
|
||
|
|
SENT: Sent,
|
||
|
|
TRASH: Trash,
|
||
|
|
SPAM: Spam,
|
||
|
|
DRAFTS: Draft,
|
||
|
|
IMPORTANT: Important,
|
||
|
|
} = MAILBOX_MODELS;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* SSE - 실시간 메일 알림 (인증 미들웨어 전에 배치)
|
||
|
|
*/
|
||
|
|
router.get("/events", (req, res) => {
|
||
|
|
// 쿼리 파라미터에서 토큰 확인
|
||
|
|
const token = req.query.token;
|
||
|
|
|
||
|
|
if (!token) {
|
||
|
|
return res.status(401).json({ error: "토큰이 필요합니다" });
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 토큰 검증
|
||
|
|
jwt.verify(token, process.env.JWT_SECRET || "your-secret-key");
|
||
|
|
|
||
|
|
// SSE 헤더 설정
|
||
|
|
res.setHeader("Content-Type", "text/event-stream");
|
||
|
|
res.setHeader("Cache-Control", "no-cache");
|
||
|
|
res.setHeader("Connection", "keep-alive");
|
||
|
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||
|
|
|
||
|
|
// 연결 유지 메시지
|
||
|
|
res.write('data: {"type":"connected"}\n\n');
|
||
|
|
|
||
|
|
// 클라이언트 추가
|
||
|
|
sseService.addClient(res);
|
||
|
|
|
||
|
|
// 연결 종료 시 클라이언트 제거
|
||
|
|
req.on("close", () => {
|
||
|
|
sseService.removeClient(res);
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
return res.status(401).json({ error: "유효하지 않은 토큰입니다" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 모든 라우트에 인증 미들웨어 적용
|
||
|
|
router.use(authenticateToken);
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Gemini 번역 API (로그인 사용자 모두 사용 가능)
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
const geminiService = require("../services/geminiService");
|
||
|
|
const EmailTranslation = require("../models/EmailTranslation");
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gemini 현재 모델명 조회 (일반 사용자용)
|
||
|
|
* GET /api/emails/gemini-model
|
||
|
|
*/
|
||
|
|
router.get("/emails/gemini-model", async (req, res) => {
|
||
|
|
try {
|
||
|
|
const config = await geminiService.getGeminiConfig();
|
||
|
|
const modelInfo = config.models?.find((m) => m.id === config.gemini_model);
|
||
|
|
res.json({
|
||
|
|
modelId: config.gemini_model,
|
||
|
|
modelName: modelInfo?.name || config.gemini_model || "Gemini AI",
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
res.json({ modelId: "", modelName: "Gemini AI" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gemini 번역 (캐시 지원)
|
||
|
|
* POST /api/emails/translate
|
||
|
|
* Body: { emailId, mailbox, text, targetLang }
|
||
|
|
*/
|
||
|
|
router.post("/emails/translate", async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { emailId, mailbox, text, targetLang } = req.body;
|
||
|
|
const lang = targetLang || "ko";
|
||
|
|
|
||
|
|
if (!text) {
|
||
|
|
return res.status(400).json({ error: "번역할 텍스트가 필요합니다" });
|
||
|
|
}
|
||
|
|
|
||
|
|
// emailId와 mailbox가 있으면 캐시 확인
|
||
|
|
if (emailId && mailbox) {
|
||
|
|
const cached = await EmailTranslation.findOne({
|
||
|
|
where: { emailId, mailbox, targetLang: lang },
|
||
|
|
});
|
||
|
|
|
||
|
|
if (cached) {
|
||
|
|
return res.json({
|
||
|
|
translatedText: cached.translatedContent,
|
||
|
|
cached: true,
|
||
|
|
modelName: cached.modelUsed || "Gemini AI",
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 캐시가 없으면 번역 실행
|
||
|
|
const config = await geminiService.getGeminiConfig();
|
||
|
|
if (!config.gemini_api_key) {
|
||
|
|
return res.status(400).json({
|
||
|
|
error: "Gemini API 키가 설정되지 않았습니다. 관리자에게 문의하세요.",
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const translatedText = await geminiService.translateText(
|
||
|
|
text,
|
||
|
|
lang,
|
||
|
|
config.gemini_api_key,
|
||
|
|
config.gemini_model
|
||
|
|
);
|
||
|
|
|
||
|
|
// 번역 결과 캐시 저장 (emailId, mailbox가 있을 때만)
|
||
|
|
if (emailId && mailbox) {
|
||
|
|
await EmailTranslation.upsert({
|
||
|
|
emailId,
|
||
|
|
mailbox,
|
||
|
|
targetLang: lang,
|
||
|
|
translatedContent: translatedText,
|
||
|
|
modelUsed: config.gemini_model,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 모델명 조회
|
||
|
|
const modelInfo = config.models?.find((m) => m.id === config.gemini_model);
|
||
|
|
const modelName = modelInfo?.name || config.gemini_model;
|
||
|
|
|
||
|
|
res.json({ translatedText, cached: false, modelName });
|
||
|
|
} catch (error) {
|
||
|
|
console.error("번역 오류:", error);
|
||
|
|
res.status(500).json({ error: error.message || "번역 실패" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// API 라우트
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 메일함별 개수 및 스토리지 사용량 조회
|
||
|
|
* GET /api/emails/counts
|
||
|
|
*/
|
||
|
|
router.get("/emails/counts", async (req, res) => {
|
||
|
|
try {
|
||
|
|
const userEmail = req.user.email;
|
||
|
|
|
||
|
|
// 각 메일함 개수 병렬 조회
|
||
|
|
const [inboxC, sentC, trashC, spamC, draftsC, importantC] =
|
||
|
|
await Promise.all([
|
||
|
|
Inbox.count({
|
||
|
|
where: { to: { [Op.like]: `%${userEmail}%` }, isDeleted: false },
|
||
|
|
}),
|
||
|
|
Sent.count({ where: { from: userEmail, isDeleted: false } }),
|
||
|
|
Trash.count({
|
||
|
|
where: {
|
||
|
|
[Op.or]: [
|
||
|
|
{ from: userEmail },
|
||
|
|
{ to: { [Op.like]: `%${userEmail}%` } },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
Spam.count({
|
||
|
|
where: { to: { [Op.like]: `%${userEmail}%` }, isDeleted: false },
|
||
|
|
}),
|
||
|
|
Draft.count({ where: { from: userEmail, isDeleted: false } }),
|
||
|
|
Important.count({
|
||
|
|
where: {
|
||
|
|
[Op.or]: [
|
||
|
|
{ from: userEmail },
|
||
|
|
{ to: { [Op.like]: `%${userEmail}%` } },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
]);
|
||
|
|
|
||
|
|
// 스토리지 사용량 계산 (전체 시스템)
|
||
|
|
const [inboxItems, sentItems] = await Promise.all([
|
||
|
|
Inbox.findAll({ attributes: ["attachments", "subject", "text", "html"] }),
|
||
|
|
Sent.findAll({ attributes: ["attachments", "subject", "text", "html"] }),
|
||
|
|
]);
|
||
|
|
|
||
|
|
const totalSize =
|
||
|
|
calculateStorageSize(inboxItems) + calculateStorageSize(sentItems);
|
||
|
|
const storageUsedMB = (totalSize / (1024 * 1024)).toFixed(2);
|
||
|
|
|
||
|
|
// 쿼터 설정 가져오기 (기본값 50GB)
|
||
|
|
const quotaConfig = await SystemConfig.findOne({
|
||
|
|
where: { key: "user_storage_quota" },
|
||
|
|
});
|
||
|
|
const storageLimit = quotaConfig ? parseInt(quotaConfig.value) : 51200;
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
INBOX: inboxC,
|
||
|
|
SENT: sentC,
|
||
|
|
TRASH: trashC,
|
||
|
|
SPAM: spamC,
|
||
|
|
DRAFTS: draftsC,
|
||
|
|
IMPORTANT: importantC,
|
||
|
|
storageUsed: storageUsedMB,
|
||
|
|
storageLimit: storageLimit,
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error("메일함 개수 조회 오류:", error);
|
||
|
|
res.status(500).json({ error: "개수 조회 실패" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 메일 검색 (상세 필터 지원)
|
||
|
|
* GET /api/emails/search
|
||
|
|
* 쿼리 파라미터:
|
||
|
|
* - q: 기본 검색어
|
||
|
|
* - scope: 검색 범위 (ALL|INBOX|SENT|...)
|
||
|
|
* - from: 발신자 필터
|
||
|
|
* - to: 수신자 필터
|
||
|
|
* - subject: 제목 필터
|
||
|
|
* - includes: 포함 키워드
|
||
|
|
* - excludes: 제외 키워드
|
||
|
|
* - minSize: 최소 크기 (bytes)
|
||
|
|
* - maxSize: 최대 크기 (bytes)
|
||
|
|
* - dateAfter: 이후 날짜 (YYYY-MM-DD)
|
||
|
|
* - dateBefore: 이전 날짜 (YYYY-MM-DD)
|
||
|
|
* - hasAttachment: 첨부파일 유무 (true/false)
|
||
|
|
*/
|
||
|
|
router.get("/emails/search", async (req, res) => {
|
||
|
|
try {
|
||
|
|
const {
|
||
|
|
q,
|
||
|
|
scope = "ALL",
|
||
|
|
from: fromFilter,
|
||
|
|
to: toFilter,
|
||
|
|
subject: subjectFilter,
|
||
|
|
includes,
|
||
|
|
excludes,
|
||
|
|
minSize,
|
||
|
|
maxSize,
|
||
|
|
dateAfter,
|
||
|
|
dateBefore,
|
||
|
|
hasAttachment,
|
||
|
|
page = 1,
|
||
|
|
limit = 50,
|
||
|
|
} = req.query;
|
||
|
|
const userEmail = req.user.email;
|
||
|
|
|
||
|
|
// 최소한 하나의 검색 조건이 필요
|
||
|
|
const hasAnyFilter =
|
||
|
|
q ||
|
||
|
|
fromFilter ||
|
||
|
|
toFilter ||
|
||
|
|
subjectFilter ||
|
||
|
|
includes ||
|
||
|
|
minSize ||
|
||
|
|
maxSize ||
|
||
|
|
dateAfter ||
|
||
|
|
dateBefore ||
|
||
|
|
hasAttachment;
|
||
|
|
if (!hasAnyFilter) {
|
||
|
|
return res.json({ emails: [], total: 0, page: 1, totalPages: 0 });
|
||
|
|
}
|
||
|
|
|
||
|
|
const searchQuery = q ? q.trim() : "";
|
||
|
|
const pageNum = parseInt(page) || 1;
|
||
|
|
const limitNum = parseInt(limit) || 50;
|
||
|
|
const offset = (pageNum - 1) * limitNum;
|
||
|
|
|
||
|
|
// 검색할 메일함 결정
|
||
|
|
const mailboxesToSearch =
|
||
|
|
scope === "ALL"
|
||
|
|
? ["INBOX", "SENT", "DRAFTS", "SPAM", "TRASH", "IMPORTANT"]
|
||
|
|
: [scope.toUpperCase()];
|
||
|
|
|
||
|
|
let allResults = [];
|
||
|
|
|
||
|
|
// 각 메일함에서 검색
|
||
|
|
for (const box of mailboxesToSearch) {
|
||
|
|
const Model = getModel(box);
|
||
|
|
if (!Model) continue;
|
||
|
|
|
||
|
|
// 사용자별 기본 필터
|
||
|
|
let userFilter = {};
|
||
|
|
if (box === "INBOX" || box === "SPAM") {
|
||
|
|
userFilter.to = { [Op.like]: `%${userEmail}%` };
|
||
|
|
} else if (box === "SENT" || box === "DRAFTS") {
|
||
|
|
userFilter.from = userEmail;
|
||
|
|
} else {
|
||
|
|
userFilter[Op.or] = [
|
||
|
|
{ from: userEmail },
|
||
|
|
{ to: { [Op.like]: `%${userEmail}%` } },
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
// 검색 조건 배열
|
||
|
|
const andConditions = [];
|
||
|
|
|
||
|
|
// 기본 검색어 (제목, 발신자, 수신자, 본문 검색)
|
||
|
|
if (searchQuery) {
|
||
|
|
andConditions.push({
|
||
|
|
[Op.or]: [
|
||
|
|
{ subject: { [Op.like]: `%${searchQuery}%` } },
|
||
|
|
{ from: { [Op.like]: `%${searchQuery}%` } },
|
||
|
|
{ fromName: { [Op.like]: `%${searchQuery}%` } },
|
||
|
|
{ to: { [Op.like]: `%${searchQuery}%` } },
|
||
|
|
{ text: { [Op.like]: `%${searchQuery}%` } },
|
||
|
|
{ html: { [Op.like]: `%${searchQuery}%` } },
|
||
|
|
],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 발신자 필터
|
||
|
|
if (fromFilter) {
|
||
|
|
andConditions.push({
|
||
|
|
[Op.or]: [
|
||
|
|
{ from: { [Op.like]: `%${fromFilter}%` } },
|
||
|
|
{ fromName: { [Op.like]: `%${fromFilter}%` } },
|
||
|
|
],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 수신자 필터
|
||
|
|
if (toFilter) {
|
||
|
|
andConditions.push({ to: { [Op.like]: `%${toFilter}%` } });
|
||
|
|
}
|
||
|
|
|
||
|
|
// 제목 필터
|
||
|
|
if (subjectFilter) {
|
||
|
|
andConditions.push({ subject: { [Op.like]: `%${subjectFilter}%` } });
|
||
|
|
}
|
||
|
|
|
||
|
|
// 포함 키워드
|
||
|
|
if (includes) {
|
||
|
|
andConditions.push({
|
||
|
|
[Op.or]: [
|
||
|
|
{ text: { [Op.like]: `%${includes}%` } },
|
||
|
|
{ html: { [Op.like]: `%${includes}%` } },
|
||
|
|
{ subject: { [Op.like]: `%${includes}%` } },
|
||
|
|
],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 날짜 필터
|
||
|
|
if (dateAfter) {
|
||
|
|
andConditions.push({ date: { [Op.gte]: new Date(dateAfter) } });
|
||
|
|
}
|
||
|
|
if (dateBefore) {
|
||
|
|
andConditions.push({
|
||
|
|
date: { [Op.lte]: new Date(dateBefore + "T23:59:59") },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const where = {
|
||
|
|
...userFilter,
|
||
|
|
isDeleted: false,
|
||
|
|
};
|
||
|
|
|
||
|
|
if (andConditions.length > 0) {
|
||
|
|
where[Op.and] = andConditions;
|
||
|
|
}
|
||
|
|
|
||
|
|
let results = await Model.findAll({
|
||
|
|
where,
|
||
|
|
order: [["date", "DESC"]],
|
||
|
|
});
|
||
|
|
|
||
|
|
// 제외 키워드 (메모리에서 필터링)
|
||
|
|
if (excludes) {
|
||
|
|
const excludeTerms = excludes.toLowerCase();
|
||
|
|
results = results.filter((email) => {
|
||
|
|
const content = `${email.subject || ""} ${email.text || ""} ${
|
||
|
|
email.html || ""
|
||
|
|
}`.toLowerCase();
|
||
|
|
return !content.includes(excludeTerms);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 크기 필터 (DB size 컬럼 우선 사용, 없는 메일은 메모리에서 계산)
|
||
|
|
if (minSize || maxSize) {
|
||
|
|
const minBytes = minSize ? parseInt(minSize) : 0;
|
||
|
|
const maxBytes = maxSize ? parseInt(maxSize) : Infinity;
|
||
|
|
|
||
|
|
results = results.filter((email) => {
|
||
|
|
// size 컬럼이 있으면 사용
|
||
|
|
if (
|
||
|
|
email.size !== null &&
|
||
|
|
email.size !== undefined &&
|
||
|
|
email.size > 0
|
||
|
|
) {
|
||
|
|
return email.size >= minBytes && email.size <= maxBytes;
|
||
|
|
}
|
||
|
|
|
||
|
|
// size 컬럼이 없는 기존 메일은 메모리에서 계산 (fallback)
|
||
|
|
let size = 0;
|
||
|
|
if (email.subject) size += Buffer.byteLength(email.subject, "utf8");
|
||
|
|
if (email.text) size += Buffer.byteLength(email.text, "utf8");
|
||
|
|
if (email.html) size += Buffer.byteLength(email.html, "utf8");
|
||
|
|
|
||
|
|
try {
|
||
|
|
const atts =
|
||
|
|
typeof email.attachments === "string"
|
||
|
|
? JSON.parse(email.attachments)
|
||
|
|
: email.attachments || [];
|
||
|
|
atts.forEach((att) => {
|
||
|
|
size += att.size || 0;
|
||
|
|
});
|
||
|
|
} catch {}
|
||
|
|
|
||
|
|
return size >= minBytes && size <= maxBytes;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 첨부파일 필터 (메모리에서 필터링)
|
||
|
|
if (hasAttachment === "true") {
|
||
|
|
results = results.filter((email) => {
|
||
|
|
try {
|
||
|
|
const atts =
|
||
|
|
typeof email.attachments === "string"
|
||
|
|
? JSON.parse(email.attachments)
|
||
|
|
: email.attachments || [];
|
||
|
|
return Array.isArray(atts) && atts.length > 0;
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 메일함 정보 추가
|
||
|
|
results.forEach((email) => {
|
||
|
|
const normalized = normalizeEmail(email, box);
|
||
|
|
normalized.mailbox = box;
|
||
|
|
allResults.push(normalized);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 날짜순 정렬
|
||
|
|
allResults.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||
|
|
|
||
|
|
// 페이징
|
||
|
|
const total = allResults.length;
|
||
|
|
const paged = allResults.slice(offset, offset + limitNum);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
emails: paged,
|
||
|
|
total,
|
||
|
|
page: pageNum,
|
||
|
|
totalPages: Math.ceil(total / limitNum),
|
||
|
|
query: searchQuery,
|
||
|
|
scope,
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error("메일 검색 오류:", error);
|
||
|
|
res.status(500).json({ error: "검색 실패" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 메일 목록 조회 (페이징)
|
||
|
|
* GET /api/emails?mailbox=INBOX&page=1&limit=20
|
||
|
|
*/
|
||
|
|
router.get("/emails", async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { mailbox = "INBOX", page = 1, limit = 20 } = req.query;
|
||
|
|
const Model = getModel(mailbox);
|
||
|
|
const userEmail = req.user.email;
|
||
|
|
|
||
|
|
const pageNum = parseInt(page) || 1;
|
||
|
|
const limitNum = parseInt(limit) || 20;
|
||
|
|
const offset = (pageNum - 1) * limitNum;
|
||
|
|
|
||
|
|
// 사용자별 필터링 조건
|
||
|
|
let where = { isDeleted: false };
|
||
|
|
if (mailbox === "INBOX" || mailbox === "SPAM") {
|
||
|
|
where.to = { [Op.like]: `%${userEmail}%` };
|
||
|
|
} else if (mailbox === "SENT" || mailbox === "DRAFTS") {
|
||
|
|
where.from = userEmail;
|
||
|
|
} else {
|
||
|
|
where[Op.or] = [
|
||
|
|
{ from: userEmail },
|
||
|
|
{ to: { [Op.like]: `%${userEmail}%` } },
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
const { count, rows } = await Model.findAndCountAll({
|
||
|
|
where,
|
||
|
|
order: [["date", "DESC"]],
|
||
|
|
limit: limitNum,
|
||
|
|
offset: offset,
|
||
|
|
});
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
emails: rows.map((e) => normalizeEmail(e, mailbox)),
|
||
|
|
total: count,
|
||
|
|
page: pageNum,
|
||
|
|
totalPages: Math.ceil(count / limitNum),
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error("메일 목록 조회 오류:", error);
|
||
|
|
res.status(500).json({ error: "메일 조회 실패" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 메일 상세 조회
|
||
|
|
* GET /api/emails/:id?mailbox=INBOX
|
||
|
|
*/
|
||
|
|
router.get("/emails/:id", async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { mailbox } = req.query;
|
||
|
|
const result = await findEmail(req.params.id, req.user.email, mailbox);
|
||
|
|
if (!result) return res.status(404).json({ error: "메일 찾기 실패" });
|
||
|
|
|
||
|
|
res.json(normalizeEmail(result.email, result.box));
|
||
|
|
} catch (error) {
|
||
|
|
console.error("메일 상세 조회 오류:", error);
|
||
|
|
res.status(500).json({ error: "오류" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 읽음 처리
|
||
|
|
* PATCH /api/emails/:id/read?mailbox=INBOX
|
||
|
|
*/
|
||
|
|
router.patch("/emails/:id/read", async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { mailbox } = req.query;
|
||
|
|
const result = await findEmail(req.params.id, req.user.email, mailbox);
|
||
|
|
if (!result) return res.status(404).json({ error: "메일 찾기 실패" });
|
||
|
|
|
||
|
|
result.email.isRead = true;
|
||
|
|
|
||
|
|
// \\Seen 플래그 추가
|
||
|
|
let flags = result.email.flags || [];
|
||
|
|
if (typeof flags === "string") {
|
||
|
|
try {
|
||
|
|
flags = JSON.parse(flags);
|
||
|
|
} catch {
|
||
|
|
flags = [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!Array.isArray(flags)) flags = [];
|
||
|
|
if (!flags.includes("\\Seen")) {
|
||
|
|
flags.push("\\Seen");
|
||
|
|
}
|
||
|
|
result.email.flags = JSON.stringify(flags);
|
||
|
|
|
||
|
|
await result.email.save();
|
||
|
|
res.json({ success: true });
|
||
|
|
} catch (error) {
|
||
|
|
console.error("읽음 처리 오류:", error);
|
||
|
|
res.status(500).json({ error: "오류" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 안읽음 처리
|
||
|
|
* PATCH /api/emails/:id/unread?mailbox=INBOX
|
||
|
|
*/
|
||
|
|
router.patch("/emails/:id/unread", async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { mailbox } = req.query;
|
||
|
|
const result = await findEmail(req.params.id, req.user.email, mailbox);
|
||
|
|
if (!result) return res.status(404).json({ error: "메일 찾기 실패" });
|
||
|
|
|
||
|
|
result.email.isRead = false;
|
||
|
|
|
||
|
|
// \Seen 플래그 제거
|
||
|
|
let flags = result.email.flags || [];
|
||
|
|
if (typeof flags === "string") {
|
||
|
|
try {
|
||
|
|
flags = JSON.parse(flags);
|
||
|
|
} catch {
|
||
|
|
flags = [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!Array.isArray(flags)) flags = [];
|
||
|
|
result.email.flags = JSON.stringify(flags.filter((f) => f !== "\\Seen"));
|
||
|
|
|
||
|
|
await result.email.save();
|
||
|
|
res.json({ success: true });
|
||
|
|
} catch (error) {
|
||
|
|
console.error("안읽음 처리 오류:", error);
|
||
|
|
res.status(500).json({ error: "오류" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 별표(중요) 토글 - 이동 방식
|
||
|
|
* PATCH /api/emails/:id/star?mailbox=INBOX
|
||
|
|
* - 별표 추가: 원본 메일함 → Important로 이동
|
||
|
|
* - 별표 해제: Important → 원래 메일함으로 이동
|
||
|
|
*/
|
||
|
|
router.patch("/emails/:id/star", async (req, res) => {
|
||
|
|
const t = await sequelize.transaction();
|
||
|
|
try {
|
||
|
|
const { mailbox } = req.query;
|
||
|
|
const result = await findEmail(req.params.id, req.user.email, mailbox);
|
||
|
|
if (!result) {
|
||
|
|
await safeRollback(t);
|
||
|
|
return res.status(404).json({ error: "메일 찾기 실패" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const { email, box } = result;
|
||
|
|
|
||
|
|
// flags 파싱
|
||
|
|
let flags = email.flags || [];
|
||
|
|
if (typeof flags === "string") {
|
||
|
|
try {
|
||
|
|
flags = JSON.parse(flags);
|
||
|
|
} catch {
|
||
|
|
flags = [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!Array.isArray(flags)) flags = [];
|
||
|
|
|
||
|
|
const isFlagged = flags.includes("\\Flagged");
|
||
|
|
|
||
|
|
if (box === "IMPORTANT") {
|
||
|
|
// 중요편지함에서 별표 해제 → 원래 메일함으로 복원
|
||
|
|
flags = flags.filter((f) => f !== "\\Flagged");
|
||
|
|
|
||
|
|
const originalBox = email.originalMailbox || "INBOX";
|
||
|
|
const TargetModel = getModel(originalBox);
|
||
|
|
|
||
|
|
// 원래 메일함으로 복사
|
||
|
|
const emailData = email.toJSON();
|
||
|
|
delete emailData.id;
|
||
|
|
delete emailData.originalMailbox;
|
||
|
|
emailData.flags = flags;
|
||
|
|
const newEmail = await TargetModel.create(emailData, { transaction: t });
|
||
|
|
|
||
|
|
// Important에서 삭제
|
||
|
|
await email.destroy({ transaction: t });
|
||
|
|
|
||
|
|
await t.commit();
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
starred: false,
|
||
|
|
movedTo: originalBox.toLowerCase(),
|
||
|
|
newEmailId: newEmail.id,
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
// 다른 메일함에서 별표 추가 → Important로 이동
|
||
|
|
flags.push("\\Flagged");
|
||
|
|
|
||
|
|
// Important로 복사
|
||
|
|
const emailData = email.toJSON();
|
||
|
|
delete emailData.id;
|
||
|
|
emailData.originalMailbox = box;
|
||
|
|
emailData.flags = flags;
|
||
|
|
const newEmail = await Important.create(emailData, { transaction: t });
|
||
|
|
|
||
|
|
// 원본 삭제
|
||
|
|
await email.destroy({ transaction: t });
|
||
|
|
|
||
|
|
await t.commit();
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
starred: true,
|
||
|
|
movedTo: "important",
|
||
|
|
newEmailId: newEmail.id,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
await safeRollback(t);
|
||
|
|
console.error("별표 처리 오류:", error);
|
||
|
|
res.status(500).json({ error: "오류" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 휴지통으로 이동 (트랜잭션 사용)
|
||
|
|
* PATCH /api/emails/:id/trash?mailbox=INBOX
|
||
|
|
*/
|
||
|
|
router.patch("/emails/:id/trash", async (req, res) => {
|
||
|
|
const t = await sequelize.transaction();
|
||
|
|
try {
|
||
|
|
const { mailbox } = req.query;
|
||
|
|
const result = await findEmail(req.params.id, req.user.email, mailbox);
|
||
|
|
if (!result) {
|
||
|
|
await safeRollback(t);
|
||
|
|
return res.status(404).json({ error: "메일 찾기 실패" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const { email, box } = result;
|
||
|
|
if (box === "TRASH") {
|
||
|
|
await safeRollback(t);
|
||
|
|
return res.json({ success: true });
|
||
|
|
}
|
||
|
|
|
||
|
|
// 휴지통으로 복사
|
||
|
|
const emailData = email.toJSON();
|
||
|
|
delete emailData.id;
|
||
|
|
emailData.originalMailbox = box;
|
||
|
|
if (emailData.isRead === undefined) emailData.isRead = true;
|
||
|
|
|
||
|
|
const trashEmail = await Trash.create(emailData, { transaction: t });
|
||
|
|
await email.destroy({ transaction: t });
|
||
|
|
|
||
|
|
await t.commit();
|
||
|
|
res.json({ success: true, trashId: trashEmail.id });
|
||
|
|
} catch (error) {
|
||
|
|
await safeRollback(t);
|
||
|
|
console.error("휴지통 이동 오류:", error);
|
||
|
|
res.status(500).json({ error: "휴지통 이동 실패" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 휴지통에서 복구 (트랜잭션 사용)
|
||
|
|
* PATCH /api/emails/:id/restore?mailbox=TRASH
|
||
|
|
*/
|
||
|
|
router.patch("/emails/:id/restore", async (req, res) => {
|
||
|
|
const t = await sequelize.transaction();
|
||
|
|
try {
|
||
|
|
const { mailbox } = req.query;
|
||
|
|
const result = await findEmail(
|
||
|
|
req.params.id,
|
||
|
|
req.user.email,
|
||
|
|
mailbox || "TRASH"
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!result || result.box !== "TRASH") {
|
||
|
|
await safeRollback(t);
|
||
|
|
return res.status(404).json({ error: "휴지통에서 찾을 수 없음" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const { email } = result;
|
||
|
|
const targetBox = email.originalMailbox || "INBOX";
|
||
|
|
const TargetModel = getModel(targetBox);
|
||
|
|
|
||
|
|
const emailData = email.toJSON();
|
||
|
|
delete emailData.id;
|
||
|
|
delete emailData.originalMailbox;
|
||
|
|
if (emailData.isRead === undefined) emailData.isRead = true;
|
||
|
|
|
||
|
|
await TargetModel.create(emailData, { transaction: t });
|
||
|
|
await email.destroy({ transaction: t });
|
||
|
|
|
||
|
|
await t.commit();
|
||
|
|
res.json({ success: true });
|
||
|
|
} catch (error) {
|
||
|
|
await safeRollback(t);
|
||
|
|
console.error("복구 오류:", error);
|
||
|
|
res.status(500).json({ error: "복구 실패" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 스팸함으로 이동 (트랜잭션 사용) + rspamd 학습
|
||
|
|
* PATCH /api/emails/:id/spam?mailbox=INBOX
|
||
|
|
*/
|
||
|
|
router.patch("/emails/:id/spam", async (req, res) => {
|
||
|
|
const t = await sequelize.transaction();
|
||
|
|
try {
|
||
|
|
const { mailbox } = req.query;
|
||
|
|
const result = await findEmail(req.params.id, req.user.email, mailbox);
|
||
|
|
if (!result) {
|
||
|
|
await safeRollback(t);
|
||
|
|
return res.status(404).json({ error: "메일 찾기 실패" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const { email, box } = result;
|
||
|
|
if (box === "SPAM") {
|
||
|
|
await safeRollback(t);
|
||
|
|
return res.json({ success: true });
|
||
|
|
}
|
||
|
|
|
||
|
|
// rspamd 스팸 학습 (원본 이메일이 있는 경우)
|
||
|
|
try {
|
||
|
|
const rspamd = require("../services/rspamdService");
|
||
|
|
const rawEmail = email.rawEmail || rspamd.buildRawEmail(email.toJSON());
|
||
|
|
await rspamd.learnSpam(rawEmail);
|
||
|
|
console.log(`[rspamd] 스팸 학습 완료: ${email.subject}`);
|
||
|
|
} catch (rspamdError) {
|
||
|
|
console.warn("[rspamd] 스팸 학습 실패 (무시):", rspamdError.message);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 스팸함으로 복사
|
||
|
|
const emailData = email.toJSON();
|
||
|
|
delete emailData.id;
|
||
|
|
emailData.originalMailbox = box;
|
||
|
|
if (emailData.isRead === undefined) emailData.isRead = true;
|
||
|
|
|
||
|
|
const spamEmail = await Spam.create(emailData, { transaction: t });
|
||
|
|
await email.destroy({ transaction: t });
|
||
|
|
|
||
|
|
await t.commit();
|
||
|
|
res.json({ success: true, spamId: spamEmail.id, learned: true });
|
||
|
|
} catch (error) {
|
||
|
|
await safeRollback(t);
|
||
|
|
console.error("스팸함 이동 오류:", error);
|
||
|
|
res.status(500).json({ error: "스팸함 이동 실패" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 메일 이동 (범용) + rspamd 학습
|
||
|
|
* PATCH /api/emails/:id/move?mailbox=INBOX&target=SPAM
|
||
|
|
*/
|
||
|
|
router.patch("/emails/:id/move", async (req, res) => {
|
||
|
|
const t = await sequelize.transaction();
|
||
|
|
try {
|
||
|
|
const { mailbox, target } = req.query;
|
||
|
|
const result = await findEmail(req.params.id, req.user.email, mailbox);
|
||
|
|
if (!result) {
|
||
|
|
await safeRollback(t);
|
||
|
|
return res.status(404).json({ error: "메일 찾기 실패" });
|
||
|
|
}
|
||
|
|
|
||
|
|
const { email, box } = result;
|
||
|
|
if (box === target) {
|
||
|
|
await safeRollback(t);
|
||
|
|
return res.json({ success: true });
|
||
|
|
}
|
||
|
|
|
||
|
|
// rspamd 학습 (스팸함 관련 이동 시)
|
||
|
|
let learned = false;
|
||
|
|
try {
|
||
|
|
const rspamd = require("../services/rspamdService");
|
||
|
|
const rawEmail = email.rawEmail || rspamd.buildRawEmail(email.toJSON());
|
||
|
|
|
||
|
|
if (target === "SPAM" && box !== "SPAM") {
|
||
|
|
// 스팸함으로 이동 → 스팸으로 학습
|
||
|
|
await rspamd.learnSpam(rawEmail);
|
||
|
|
console.log(`[rspamd] 스팸 학습 완료: ${email.subject}`);
|
||
|
|
learned = true;
|
||
|
|
} else if (box === "SPAM" && target !== "SPAM") {
|
||
|
|
// 스팸함에서 나감 → 정상 메일(ham)로 학습
|
||
|
|
await rspamd.learnHam(rawEmail);
|
||
|
|
console.log(`[rspamd] 햄 학습 완료: ${email.subject}`);
|
||
|
|
learned = true;
|
||
|
|
}
|
||
|
|
} catch (rspamdError) {
|
||
|
|
console.warn("[rspamd] 학습 실패 (무시):", rspamdError.message);
|
||
|
|
}
|
||
|
|
|
||
|
|
const TargetModel = getModel(target);
|
||
|
|
|
||
|
|
// 대상 메일함으로 복사
|
||
|
|
const emailData = email.toJSON();
|
||
|
|
delete emailData.id;
|
||
|
|
emailData.originalMailbox = box;
|
||
|
|
if (emailData.isRead === undefined) emailData.isRead = true;
|
||
|
|
|
||
|
|
const newEmail = await TargetModel.create(emailData, { transaction: t });
|
||
|
|
await email.destroy({ transaction: t });
|
||
|
|
|
||
|
|
await t.commit();
|
||
|
|
res.json({ success: true, newId: newEmail.id, target, learned });
|
||
|
|
} catch (error) {
|
||
|
|
await safeRollback(t);
|
||
|
|
console.error("메일 이동 오류:", error);
|
||
|
|
res.status(500).json({ error: "메일 이동 실패" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 영구 삭제
|
||
|
|
* DELETE /api/emails/:id?mailbox=TRASH
|
||
|
|
*/
|
||
|
|
router.delete("/emails/:id", async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { mailbox } = req.query;
|
||
|
|
const result = await findEmail(req.params.id, req.user.email, mailbox);
|
||
|
|
if (!result) return res.status(404).json({ error: "메일 찾기 실패" });
|
||
|
|
|
||
|
|
await result.email.destroy();
|
||
|
|
res.json({ success: true });
|
||
|
|
} catch (error) {
|
||
|
|
console.error("삭제 오류:", error);
|
||
|
|
res.status(500).json({ error: "삭제 실패" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 첨부파일 다운로드 (스트리밍)
|
||
|
|
* GET /api/emails/:id/attachments/:filename?mailbox=INBOX
|
||
|
|
*/
|
||
|
|
router.get("/emails/:id/attachments/:filename", async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { mailbox } = req.query;
|
||
|
|
const result = await findEmail(req.params.id, req.user.email, mailbox);
|
||
|
|
if (!result) return res.status(404).json({ error: "메일 찾기 실패" });
|
||
|
|
|
||
|
|
// 첨부파일 목록 파싱
|
||
|
|
let attachments = result.email.attachments || [];
|
||
|
|
if (typeof attachments === "string") {
|
||
|
|
try {
|
||
|
|
attachments = JSON.parse(attachments);
|
||
|
|
} catch {
|
||
|
|
attachments = [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 요청된 파일명으로 검색
|
||
|
|
const att = attachments.find((a) => a.filename === req.params.filename);
|
||
|
|
if (!att) return res.status(404).json({ error: "파일을 찾을 수 없음" });
|
||
|
|
|
||
|
|
// S3에서 스트리밍 다운로드
|
||
|
|
const { stream, contentType, contentLength } = await getObjectStream(
|
||
|
|
att.key
|
||
|
|
);
|
||
|
|
res.setHeader("Content-Type", contentType || "application/octet-stream");
|
||
|
|
if (contentLength) res.setHeader("Content-Length", contentLength);
|
||
|
|
|
||
|
|
const encoded = encodeURIComponent(att.filename);
|
||
|
|
res.setHeader(
|
||
|
|
"Content-Disposition",
|
||
|
|
`attachment; filename="${encoded}"; filename*=UTF-8''${encoded}`
|
||
|
|
);
|
||
|
|
stream.pipe(res);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("첨부파일 다운로드 오류:", error);
|
||
|
|
res.status(500).json({ error: "다운로드 오류" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 메일 발송 (SES + 보낸편지함 저장)
|
||
|
|
* POST /api/send
|
||
|
|
*/
|
||
|
|
router.post("/send", async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { to, subject, html, text, attachments = [] } = req.body;
|
||
|
|
const userEmail = req.user.email;
|
||
|
|
|
||
|
|
// 사용자 이름 조회하여 From 헤더 구성
|
||
|
|
const currentUser = await User.findOne({ where: { email: userEmail } });
|
||
|
|
const fromHeader = currentUser?.name
|
||
|
|
? `"${currentUser.name}" <${userEmail}>`
|
||
|
|
: userEmail;
|
||
|
|
|
||
|
|
// 첨부파일을 Garage에 업로드
|
||
|
|
const { uploadAttachment } = require("../services/s3Service");
|
||
|
|
const attachmentsData = [];
|
||
|
|
|
||
|
|
for (const att of attachments) {
|
||
|
|
const buffer = Buffer.from(att.content, "base64");
|
||
|
|
const key = await uploadAttachment(buffer, att.filename, att.contentType);
|
||
|
|
attachmentsData.push({
|
||
|
|
filename: att.filename,
|
||
|
|
contentType: att.contentType,
|
||
|
|
size: buffer.length,
|
||
|
|
key: key,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// SES를 통한 발송 (From 헤더에 이름 포함)
|
||
|
|
await sendEmail({ from: fromHeader, to, subject, html, text, attachments });
|
||
|
|
|
||
|
|
// 보낸편지함에 저장 (첨부파일 정보 포함)
|
||
|
|
const sentEmailData = {
|
||
|
|
from: userEmail,
|
||
|
|
fromName: currentUser?.name || null,
|
||
|
|
to: Array.isArray(to) ? to.join(",") : to,
|
||
|
|
subject,
|
||
|
|
html: html,
|
||
|
|
text: text || (html ? html.replace(/<[^>]*>/g, "") : ""),
|
||
|
|
attachments: attachmentsData,
|
||
|
|
date: new Date(),
|
||
|
|
isRead: true,
|
||
|
|
};
|
||
|
|
sentEmailData.size = calculateEmailSize(sentEmailData);
|
||
|
|
await Sent.create(sentEmailData);
|
||
|
|
|
||
|
|
// 발송 로그 테이블에도 기록 (통계용 - 보낸편지함 삭제와 무관하게 유지)
|
||
|
|
await SentLog.create({
|
||
|
|
from: userEmail,
|
||
|
|
to: Array.isArray(to) ? to.join(",") : to,
|
||
|
|
subject: subject || "(제목 없음)",
|
||
|
|
success: true,
|
||
|
|
sentAt: new Date(),
|
||
|
|
});
|
||
|
|
|
||
|
|
res.json({ success: true });
|
||
|
|
} catch (error) {
|
||
|
|
console.error("메일 발송 오류:", error);
|
||
|
|
res.status(500).json({ error: "발송 실패" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 임시저장 생성
|
||
|
|
* POST /api/drafts
|
||
|
|
*/
|
||
|
|
router.post("/drafts", async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { to, subject, html, text } = req.body;
|
||
|
|
const userEmail = req.user.email;
|
||
|
|
|
||
|
|
const draft = await Draft.create({
|
||
|
|
from: userEmail,
|
||
|
|
to: Array.isArray(to) ? to.join(",") : to || "",
|
||
|
|
subject: subject || "(제목 없음)",
|
||
|
|
html: html || "",
|
||
|
|
text: text || (html ? html.replace(/<[^>]*>/g, "") : ""),
|
||
|
|
date: new Date(),
|
||
|
|
isRead: true,
|
||
|
|
isDeleted: false,
|
||
|
|
});
|
||
|
|
|
||
|
|
res.json({ success: true, id: draft.id });
|
||
|
|
} catch (error) {
|
||
|
|
console.error("임시저장 오류:", error);
|
||
|
|
res.status(500).json({ error: "임시저장 실패" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 임시저장 삭제
|
||
|
|
* DELETE /api/drafts/:id
|
||
|
|
*/
|
||
|
|
router.delete("/drafts/:id", async (req, res) => {
|
||
|
|
try {
|
||
|
|
const draft = await Draft.findOne({
|
||
|
|
where: { id: req.params.id, from: req.user.email },
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!draft) {
|
||
|
|
return res.status(404).json({ error: "임시저장 찾기 실패" });
|
||
|
|
}
|
||
|
|
|
||
|
|
await draft.destroy();
|
||
|
|
res.json({ success: true });
|
||
|
|
} catch (error) {
|
||
|
|
console.error("임시저장 삭제 오류:", error);
|
||
|
|
res.status(500).json({ error: "삭제 실패" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 사용자 이메일-이름 매핑 조회
|
||
|
|
* GET /api/user-names
|
||
|
|
* 로컬 가입 사용자의 이메일과 이름 매핑 반환
|
||
|
|
*/
|
||
|
|
router.get("/user-names", authenticateToken, async (req, res) => {
|
||
|
|
try {
|
||
|
|
const users = await User.findAll({
|
||
|
|
attributes: ["email", "name"],
|
||
|
|
where: { name: { [Op.ne]: null } },
|
||
|
|
});
|
||
|
|
|
||
|
|
// 이메일 -> 이름 매핑 객체로 변환
|
||
|
|
const userMap = {};
|
||
|
|
users.forEach((user) => {
|
||
|
|
if (user.name) {
|
||
|
|
userMap[user.email] = user.name;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
res.json(userMap);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("사용자 이름 조회 오류:", error);
|
||
|
|
res.status(500).json({ error: "조회 실패" });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
module.exports = router;
|