746 lines
21 KiB
JavaScript
746 lines
21 KiB
JavaScript
/**
|
|
* 관리자 라우터
|
|
* 통계 대시보드, 사용자 관리, SES 설정 등
|
|
*/
|
|
const express = require("express");
|
|
const router = express.Router();
|
|
const jwt = require("jsonwebtoken");
|
|
const bcrypt = require("bcryptjs");
|
|
const { Op, fn, col } = require("sequelize");
|
|
const User = require("../models/User");
|
|
const Inbox = require("../models/Inbox");
|
|
const Sent = require("../models/Sent");
|
|
const SystemConfig = require("../models/SystemConfig");
|
|
const SmtpLog = require("../models/SmtpLog");
|
|
const SentLog = require("../models/SentLog");
|
|
const Spam = require("../models/Spam");
|
|
const { sendEmail } = require("../services/emailService");
|
|
const rspamd = require("../services/rspamdService");
|
|
|
|
// 공통 헬퍼 함수 import
|
|
const {
|
|
calculateStorageSize,
|
|
getPeriodStartDate,
|
|
} = require("../utils/helpers");
|
|
|
|
const JWT_SECRET =
|
|
process.env.JWT_SECRET || "super_secret_jwt_key_changed_in_production";
|
|
|
|
// ============================================================================
|
|
// 미들웨어
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 관리자 권한 검증 미들웨어
|
|
* JWT 토큰 검증 후 DB에서 실시간 권한 확인
|
|
* (권한이 박탈된 경우 즉시 차단)
|
|
*/
|
|
const verifyAdmin = async (req, res, next) => {
|
|
const token = req.headers.authorization?.split(" ")[1];
|
|
if (!token) return res.status(401).json({ error: "토큰이 없습니다" });
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_SECRET);
|
|
|
|
// DB에서 최신 사용자 정보 조회
|
|
const user = await User.findByPk(decoded.id);
|
|
if (!user) {
|
|
return res.status(403).json({ error: "사용자를 찾을 수 없습니다" });
|
|
}
|
|
|
|
// 실시간 관리자 권한 확인
|
|
if (!user.isAdmin) {
|
|
return res.status(403).json({ error: "관리자 권한이 필요합니다" });
|
|
}
|
|
|
|
req.user = {
|
|
id: user.id,
|
|
email: user.email,
|
|
isAdmin: user.isAdmin,
|
|
};
|
|
next();
|
|
} catch (err) {
|
|
return res.status(403).json({ error: "유효하지 않은 토큰" });
|
|
}
|
|
};
|
|
|
|
// 최고 관리자 계정 (삭제 불가, 비밀번호만 수정 가능)
|
|
const SUPER_ADMIN_EMAIL = "admin@caadiq.co.kr";
|
|
|
|
// 모든 라우트에 관리자 검증 적용
|
|
router.use(verifyAdmin);
|
|
|
|
// ============================================================================
|
|
// API 라우트 - 통계 대시보드
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 대시보드 통계 조회
|
|
* GET /api/admin/stats
|
|
*/
|
|
router.get("/stats", async (req, res) => {
|
|
try {
|
|
const userCount = await User.count();
|
|
|
|
const startOfDay = new Date();
|
|
startOfDay.setHours(0, 0, 0, 0);
|
|
|
|
// 오늘 및 총 발송/수신 메일 수
|
|
const [
|
|
sentToday,
|
|
receivedToday,
|
|
spamToday,
|
|
totalSpam,
|
|
totalSent,
|
|
totalReceived,
|
|
] = await Promise.all([
|
|
SentLog.count({
|
|
where: { sentAt: { [Op.gte]: startOfDay }, success: true },
|
|
}),
|
|
SmtpLog.count({
|
|
where: { connectedAt: { [Op.gte]: startOfDay }, success: true },
|
|
}),
|
|
SmtpLog.count({
|
|
where: { connectedAt: { [Op.gte]: startOfDay }, isSpam: true },
|
|
}),
|
|
Spam.count(),
|
|
Sent.count(), // 보낸편지함 실제 메일 수
|
|
Inbox.count(), // 받은편지함 실제 메일 수
|
|
]);
|
|
|
|
// 스토리지 사용량 계산
|
|
const [inboxItems, sentItems] = await Promise.all([
|
|
Inbox.findAll({
|
|
attributes: ["attachments", "subject", "date", "text", "html"],
|
|
}),
|
|
Sent.findAll({
|
|
attributes: ["attachments", "subject", "date", "text", "html"],
|
|
}),
|
|
]);
|
|
|
|
const totalSize =
|
|
calculateStorageSize(inboxItems) + calculateStorageSize(sentItems);
|
|
const storageUsed = (totalSize / (1024 * 1024)).toFixed(2);
|
|
|
|
// 스토리지 쿼터 설정 (기본값 50GB)
|
|
const quotaConfig = await SystemConfig.findOne({
|
|
where: { key: "user_storage_quota" },
|
|
});
|
|
const storageLimit = quotaConfig ? parseInt(quotaConfig.value) : 51200;
|
|
|
|
// 최근 활동 로그 생성
|
|
const logs = [];
|
|
|
|
// 최근 수신 메일 (5개)
|
|
const recentInbox = [...inboxItems]
|
|
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
|
.slice(0, 5);
|
|
recentInbox.forEach((item) => {
|
|
logs.push({
|
|
type: "INBOX",
|
|
date: new Date(item.date),
|
|
message: `메일 수신: ${item.subject}`,
|
|
detail: item.subject,
|
|
});
|
|
});
|
|
|
|
// 최근 발송 메일 (5개)
|
|
const recentSent = [...sentItems]
|
|
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
|
.slice(0, 5);
|
|
recentSent.forEach((item) => {
|
|
logs.push({
|
|
type: "SENT",
|
|
date: new Date(item.date),
|
|
message: `메일 발송: ${item.subject}`,
|
|
detail: item.subject,
|
|
});
|
|
});
|
|
|
|
// 최근 가입 사용자 (5개)
|
|
const recentUsers = await User.findAll({
|
|
limit: 5,
|
|
order: [["createdAt", "DESC"]],
|
|
attributes: ["email", "createdAt"],
|
|
});
|
|
recentUsers.forEach((user) => {
|
|
if (user.createdAt) {
|
|
logs.push({
|
|
type: "USER",
|
|
date: new Date(user.createdAt),
|
|
message: `사용자 가입: ${user.email}`,
|
|
detail: user.email,
|
|
});
|
|
}
|
|
});
|
|
|
|
// 로그 병합 후 최신순 정렬
|
|
logs.sort((a, b) => b.date - a.date);
|
|
const recentLogs = logs.slice(0, 5);
|
|
|
|
// 차트 데이터 (최근 7일) - sent_logs/smtp_logs 기반
|
|
const chartData = [];
|
|
for (let i = 6; i >= 0; i--) {
|
|
const d = new Date();
|
|
d.setDate(d.getDate() - i);
|
|
const dateStr = d.toISOString().split("T")[0];
|
|
const dayStart = new Date(d.setHours(0, 0, 0, 0));
|
|
const dayEnd = new Date(d.setHours(23, 59, 59, 999));
|
|
|
|
const [sent, received] = await Promise.all([
|
|
SentLog.count({
|
|
where: {
|
|
sentAt: { [Op.between]: [dayStart, dayEnd] },
|
|
success: true,
|
|
},
|
|
}),
|
|
SmtpLog.count({
|
|
where: {
|
|
connectedAt: { [Op.between]: [dayStart, dayEnd] },
|
|
success: true,
|
|
},
|
|
}),
|
|
]);
|
|
|
|
chartData.push({ name: dateStr.slice(5), sent, received });
|
|
}
|
|
|
|
// 시스템 상태 체크
|
|
let dbStatus = "Connected";
|
|
try {
|
|
await User.findOne();
|
|
} catch {
|
|
dbStatus = "Disconnected";
|
|
}
|
|
|
|
// rspamd 상태 체크
|
|
const rspamdStatus = (await rspamd.checkHealth()) ? "Running" : "Stopped";
|
|
|
|
// 기간 파라미터 처리 (1d, 7d, 30d, all)
|
|
const period = req.query.period || "all";
|
|
const periodStart = getPeriodStartDate(period);
|
|
|
|
// 유저별 메일 통계 (Top Users) - sent_logs/smtp_logs 기반
|
|
const allUsers = await User.findAll({ attributes: ["email"] });
|
|
const topUsersData = await Promise.all(
|
|
allUsers.map(async (user) => {
|
|
const sentWhere = { from: user.email, success: true };
|
|
const receivedWhere = {
|
|
rcptTo: { [Op.like]: `%${user.email}%` },
|
|
success: true,
|
|
};
|
|
|
|
if (periodStart) {
|
|
sentWhere.sentAt = { [Op.gte]: periodStart };
|
|
receivedWhere.connectedAt = { [Op.gte]: periodStart };
|
|
}
|
|
|
|
const [sentCount, receivedCount] = await Promise.all([
|
|
SentLog.count({ where: sentWhere }),
|
|
SmtpLog.count({ where: receivedWhere }),
|
|
]);
|
|
return {
|
|
email: user.email,
|
|
sent: sentCount,
|
|
received: receivedCount,
|
|
total: sentCount + receivedCount,
|
|
};
|
|
})
|
|
);
|
|
const topUsers = topUsersData.sort((a, b) => b.total - a.total).slice(0, 5);
|
|
|
|
// SMTP 접속 IP 통계 (Remote IPs) - 국가 정보 포함
|
|
const smtpLogWhere = periodStart
|
|
? { connectedAt: { [Op.gte]: periodStart } }
|
|
: {};
|
|
const remoteIpsData = await SmtpLog.findAll({
|
|
attributes: [
|
|
"remoteAddress",
|
|
"country",
|
|
"countryName",
|
|
"hostname",
|
|
[fn("COUNT", col("id")), "count"],
|
|
],
|
|
where: smtpLogWhere,
|
|
group: ["remoteAddress", "country", "countryName", "hostname"],
|
|
order: [[fn("COUNT", col("id")), "DESC"]],
|
|
limit: 10,
|
|
raw: true,
|
|
});
|
|
const remoteIps = remoteIpsData.map((item) => ({
|
|
ip: item.remoteAddress,
|
|
country: item.country,
|
|
countryName: item.countryName,
|
|
hostname: item.hostname,
|
|
count: parseInt(item.count),
|
|
}));
|
|
|
|
res.json({
|
|
userCount,
|
|
sentToday,
|
|
receivedToday,
|
|
spamToday,
|
|
totalSpam,
|
|
totalSent,
|
|
totalReceived,
|
|
storageUsed: storageUsed + " MB",
|
|
storageLimit,
|
|
chartData,
|
|
recentLogs,
|
|
topUsers,
|
|
remoteIps,
|
|
rspamdStatus,
|
|
dbStatus,
|
|
});
|
|
} catch (error) {
|
|
console.error("통계 조회 오류:", error);
|
|
res.status(500).json({ error: "통계 조회 실패" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 접속 IP 목록 페이징 조회 (개별 로그)
|
|
* GET /api/admin/remote-ips?page=1&limit=20&period=all
|
|
*/
|
|
router.get("/remote-ips", async (req, res) => {
|
|
try {
|
|
const page = parseInt(req.query.page) || 1;
|
|
const limit = Math.min(parseInt(req.query.limit) || 10, 10); // 기본 10개, 최대 10개
|
|
const period = req.query.period || "all";
|
|
const maxItems = 50; // 최대 50개까지만 표시
|
|
const offset = (page - 1) * limit;
|
|
|
|
// 기간 필터
|
|
const periodStart = getPeriodStartDate(period);
|
|
|
|
const smtpLogWhere = periodStart
|
|
? { connectedAt: { [Op.gte]: periodStart } }
|
|
: {};
|
|
|
|
// 전체 개수 조회
|
|
const totalCount = await SmtpLog.count({ where: smtpLogWhere });
|
|
|
|
// 페이징된 개별 로그 조회
|
|
const remoteIpsData = await SmtpLog.findAll({
|
|
attributes: [
|
|
"remoteAddress",
|
|
"country",
|
|
"countryName",
|
|
"hostname",
|
|
"mailFrom",
|
|
"rcptTo",
|
|
"connectedAt",
|
|
],
|
|
where: smtpLogWhere,
|
|
order: [["connectedAt", "DESC"]],
|
|
limit,
|
|
offset,
|
|
raw: true,
|
|
});
|
|
|
|
const remoteIps = remoteIpsData.map((item) => ({
|
|
ip: item.remoteAddress,
|
|
country: item.country,
|
|
countryName: item.countryName,
|
|
hostname: item.hostname,
|
|
mailFrom: item.mailFrom,
|
|
rcptTo: item.rcptTo,
|
|
connectedAt: item.connectedAt,
|
|
}));
|
|
|
|
// 최대 50개로 제한된 전체 개수
|
|
const effectiveTotal = Math.min(totalCount, maxItems);
|
|
|
|
res.json({
|
|
data: remoteIps,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total: effectiveTotal,
|
|
hasMore: offset + remoteIps.length < effectiveTotal,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("접속 IP 조회 오류:", error);
|
|
res.status(500).json({ error: "접속 IP 조회 실패" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 최근 활동 로그 페이징 조회
|
|
* GET /api/admin/recent-logs?page=1&limit=20
|
|
*/
|
|
router.get("/recent-logs", async (req, res) => {
|
|
try {
|
|
const page = parseInt(req.query.page) || 1;
|
|
const limit = Math.min(parseInt(req.query.limit) || 10, 10); // 기본 10개, 최대 10개
|
|
const maxItems = 50; // 최대 50개까지만 표시
|
|
const offset = (page - 1) * limit;
|
|
|
|
const logs = [];
|
|
|
|
// 최근 수신 메일
|
|
const recentInbox = await Inbox.findAll({
|
|
order: [["date", "DESC"]],
|
|
attributes: ["from", "subject", "date"],
|
|
});
|
|
recentInbox.forEach((item) => {
|
|
logs.push({
|
|
type: "INBOX",
|
|
date: new Date(item.date),
|
|
message: `메일 수신: ${item.subject}`,
|
|
detail: item.from,
|
|
});
|
|
});
|
|
|
|
// 최근 발송 메일
|
|
const recentSent = await Sent.findAll({
|
|
order: [["date", "DESC"]],
|
|
attributes: ["to", "subject", "date"],
|
|
});
|
|
recentSent.forEach((item) => {
|
|
logs.push({
|
|
type: "SENT",
|
|
date: new Date(item.date),
|
|
message: `메일 발송: ${item.subject}`,
|
|
detail: item.to,
|
|
});
|
|
});
|
|
|
|
// 최근 가입 사용자
|
|
const recentUsers = await User.findAll({
|
|
order: [["createdAt", "DESC"]],
|
|
attributes: ["email", "createdAt"],
|
|
});
|
|
recentUsers.forEach((user) => {
|
|
if (user.createdAt) {
|
|
logs.push({
|
|
type: "USER",
|
|
date: new Date(user.createdAt),
|
|
message: `사용자 가입: ${user.email}`,
|
|
detail: user.email,
|
|
});
|
|
}
|
|
});
|
|
|
|
// 날짜순 정렬 후 페이징 (최대 50개)
|
|
logs.sort((a, b) => b.date - a.date);
|
|
const effectiveTotal = Math.min(logs.length, maxItems);
|
|
const paginatedLogs = logs.slice(
|
|
offset,
|
|
Math.min(offset + limit, effectiveTotal)
|
|
);
|
|
|
|
res.json({
|
|
data: paginatedLogs,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total: effectiveTotal,
|
|
hasMore: offset + paginatedLogs.length < effectiveTotal,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("최근 활동 조회 오류:", error);
|
|
res.status(500).json({ error: "최근 활동 조회 실패" });
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// API 라우트 - 사용자 관리
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 사용자 목록 조회
|
|
* GET /api/admin/users
|
|
*/
|
|
router.get("/users", async (req, res) => {
|
|
try {
|
|
const users = await User.findAll({ attributes: { exclude: ["password"] } });
|
|
res.json(users);
|
|
} catch (error) {
|
|
console.error("사용자 목록 조회 오류:", error);
|
|
res.status(500).json({ error: "사용자 목록 조회 실패" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 사용자 생성
|
|
* POST /api/admin/users
|
|
*/
|
|
router.post("/users", async (req, res) => {
|
|
try {
|
|
const { email, password, name, isAdmin } = req.body;
|
|
|
|
// 중복 이메일 체크
|
|
const existing = await User.findOne({ where: { email } });
|
|
if (existing)
|
|
return res.status(400).json({ error: "이미 사용 중인 이메일입니다." });
|
|
|
|
const hashedPassword = await bcrypt.hash(password, 10);
|
|
const user = await User.create({
|
|
email,
|
|
password: hashedPassword,
|
|
name,
|
|
isAdmin,
|
|
});
|
|
|
|
res.json({ success: true, user: { id: user.id, email: user.email } });
|
|
} catch (error) {
|
|
console.error("사용자 생성 오류:", error);
|
|
res.status(500).json({ error: "사용자 생성 실패" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 사용자 수정
|
|
* PUT /api/admin/users/:id
|
|
*/
|
|
router.put("/users/:id", async (req, res) => {
|
|
try {
|
|
const { password, name, isAdmin } = req.body;
|
|
const user = await User.findByPk(req.params.id);
|
|
|
|
if (!user) {
|
|
return res.status(404).json({ error: "사용자를 찾을 수 없습니다" });
|
|
}
|
|
|
|
// 최고 관리자는 비밀번호만 수정 가능
|
|
if (user.email === SUPER_ADMIN_EMAIL) {
|
|
if (password) {
|
|
user.password = await bcrypt.hash(password, 10);
|
|
await user.save();
|
|
}
|
|
return res.json({
|
|
success: true,
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
isAdmin: user.isAdmin,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (name !== undefined) user.name = name;
|
|
if (isAdmin !== undefined) user.isAdmin = isAdmin;
|
|
if (password) {
|
|
user.password = await bcrypt.hash(password, 10);
|
|
}
|
|
|
|
await user.save();
|
|
|
|
res.json({
|
|
success: true,
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
isAdmin: user.isAdmin,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("사용자 수정 오류:", error);
|
|
res.status(500).json({ error: "사용자 수정 실패" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 사용자 삭제
|
|
* DELETE /api/admin/users/:id
|
|
*/
|
|
router.delete("/users/:id", async (req, res) => {
|
|
try {
|
|
const user = await User.findByPk(req.params.id);
|
|
|
|
// 최고 관리자 삭제 금지
|
|
if (user && user.email === SUPER_ADMIN_EMAIL) {
|
|
return res
|
|
.status(403)
|
|
.json({ error: "최고 관리자 계정은 삭제할 수 없습니다" });
|
|
}
|
|
|
|
await User.destroy({ where: { id: req.params.id } });
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error("사용자 삭제 오류:", error);
|
|
res.status(500).json({ error: "사용자 삭제 실패" });
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// API 라우트 - Resend 설정
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Resend 설정 조회
|
|
* GET /api/admin/config/email
|
|
*/
|
|
router.get("/config/email", async (req, res) => {
|
|
try {
|
|
const configs = await SystemConfig.findAll({
|
|
where: {
|
|
key: {
|
|
[Op.in]: [
|
|
"resend_api_key",
|
|
"mail_from",
|
|
"user_storage_quota",
|
|
"session_expire_hours",
|
|
],
|
|
},
|
|
},
|
|
});
|
|
|
|
const configMap = {};
|
|
configs.forEach((c) => (configMap[c.key] = c.value));
|
|
|
|
res.json(configMap);
|
|
} catch (error) {
|
|
console.error("이메일 설정 조회 오류:", error);
|
|
res.status(500).json({ error: "설정 조회 실패" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Resend 설정 저장
|
|
* POST /api/admin/config/email
|
|
*/
|
|
router.post("/config/email", async (req, res) => {
|
|
try {
|
|
const {
|
|
resend_api_key,
|
|
mail_from,
|
|
user_storage_quota,
|
|
session_expire_hours,
|
|
} = req.body;
|
|
|
|
// 각 설정값 upsert (있으면 업데이트, 없으면 생성)
|
|
if (resend_api_key && !resend_api_key.includes("*"))
|
|
await SystemConfig.upsert({
|
|
key: "resend_api_key",
|
|
value: resend_api_key,
|
|
});
|
|
if (mail_from)
|
|
await SystemConfig.upsert({ key: "mail_from", value: mail_from });
|
|
if (user_storage_quota)
|
|
await SystemConfig.upsert({
|
|
key: "user_storage_quota",
|
|
value: user_storage_quota,
|
|
});
|
|
if (session_expire_hours)
|
|
await SystemConfig.upsert({
|
|
key: "session_expire_hours",
|
|
value: session_expire_hours,
|
|
});
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error("이메일 설정 저장 오류:", error);
|
|
res.status(500).json({ error: "설정 저장 실패" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Resend 연결 테스트 (테스트 이메일 발송)
|
|
* POST /api/admin/config/email/test
|
|
*/
|
|
router.post("/config/email/test", async (req, res) => {
|
|
try {
|
|
const { to, resend_api_key, mail_from } = req.body;
|
|
|
|
if (!resend_api_key) {
|
|
return res.status(400).json({ error: "Resend API 키가 필요합니다" });
|
|
}
|
|
|
|
// 입력된 값으로 테스트하기 위해 직접 Resend 클라이언트 생성
|
|
const { Resend } = require("resend");
|
|
const resend = new Resend(resend_api_key);
|
|
|
|
const result = await resend.emails.send({
|
|
from: mail_from || to,
|
|
to: [to],
|
|
subject: "Resend 연결 테스트",
|
|
html: "<h1>연결 성공!</h1><p>Resend 이메일 설정이 정상적으로 작동합니다.</p>",
|
|
text: "연결 성공! Resend 이메일 설정이 정상적으로 작동합니다.",
|
|
});
|
|
|
|
if (result.error) {
|
|
throw new Error(result.error.message);
|
|
}
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error("Resend 테스트 오류:", error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// API 라우트 - Gemini 설정
|
|
// ============================================================================
|
|
|
|
const geminiService = require("../services/geminiService");
|
|
|
|
/**
|
|
* Gemini 설정 조회
|
|
* GET /api/admin/config/gemini
|
|
*/
|
|
router.get("/config/gemini", async (req, res) => {
|
|
try {
|
|
const config = await geminiService.getGeminiConfig();
|
|
res.json(config);
|
|
} catch (error) {
|
|
console.error("Gemini 설정 조회 오류:", error);
|
|
res.status(500).json({ error: "Gemini 설정 조회 실패" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Gemini 설정 저장
|
|
* POST /api/admin/config/gemini
|
|
*/
|
|
router.post("/config/gemini", async (req, res) => {
|
|
try {
|
|
const { gemini_api_key, gemini_model } = req.body;
|
|
await geminiService.saveGeminiConfig({ gemini_api_key, gemini_model });
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error("Gemini 설정 저장 오류:", error);
|
|
res.status(500).json({ error: "Gemini 설정 저장 실패" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Gemini 번역 API (일반 사용자도 사용 가능)
|
|
* POST /api/admin/translate
|
|
*/
|
|
router.post("/translate", async (req, res) => {
|
|
try {
|
|
const { text, targetLang } = req.body;
|
|
|
|
if (!text) {
|
|
return res.status(400).json({ error: "번역할 텍스트가 필요합니다" });
|
|
}
|
|
|
|
const config = await geminiService.getGeminiConfig();
|
|
if (!config.gemini_api_key) {
|
|
return res
|
|
.status(400)
|
|
.json({ error: "Gemini API 키가 설정되지 않았습니다" });
|
|
}
|
|
|
|
const translatedText = await geminiService.translateText(
|
|
text,
|
|
targetLang || "ko",
|
|
config.gemini_api_key,
|
|
config.gemini_model
|
|
);
|
|
|
|
res.json({ translatedText });
|
|
} catch (error) {
|
|
console.error("번역 오류:", error);
|
|
res.status(500).json({ error: error.message || "번역 실패" });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|