QPIDS / server_.ts
Hyungseoky's picture
Create server_.ts
2b63290 verified
Raw
History Blame Contribute Delete
12 kB
import express from "express";
import cookieParser from "cookie-parser";
import { createServer as createViteServer } from "vite";
import path from "path";
import fs from "fs/promises";
import { fileURLToPath } from "url";
import { searchPipeline } from "./src/services/searchPipeline.ts";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// snap_data 폴더 μ ˆλŒ€ 경둜 β€” q-pids μƒμœ„ 폴더 κΈ°μ€€
// ν™˜κ²½λ³€μˆ˜ SNAP_DATA_DIR둜 μ˜€λ²„λΌμ΄λ“œ κ°€λŠ₯
const SNAP_DATA_DIR =
process.env.SNAP_DATA_DIR || path.resolve(__dirname, "..", "snap_data");
// ─────────────────────────────────────────────────────────────
// ν™œλ™ 둜그 μ‹œμŠ€ν…œ (JSON Lines 파일)
// ─────────────────────────────────────────────────────────────
const LOG_DIR = process.env.SNAP_LOG_DIR || path.resolve(__dirname, "logs");
const LOG_TYPES = ["search", "detail", "download"] as const;
type LogType = (typeof LOG_TYPES)[number];
async function ensureLogDir() {
try {
await fs.mkdir(LOG_DIR, { recursive: true });
} catch {}
}
function getLogFilePath(type: LogType): string {
const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
return path.join(LOG_DIR, `snap-${type}-${date}.log`);
}
async function writeActivityLog(type: LogType, data: Record<string, any>) {
try {
await ensureLogDir();
const entry = {
timestamp: new Date().toISOString(),
type,
...data,
};
const line = JSON.stringify(entry) + "\n";
await fs.appendFile(getLogFilePath(type), line, "utf-8");
} catch (err) {
console.warn(`[activity-log] 둜그 기둝 μ‹€νŒ¨ (${type}):`, err);
}
}
// ─────────────────────────────────────────────────────────────
// 인증 μ„œλ²„ μ„€μ •
// ─────────────────────────────────────────────────────────────
const AUTH_API_BASE = process.env.AUTH_API_BASE || "http://10.150.6.47:8090";
const COOKIE_NAME = "snap_auth";
const COOKIE_MAX_AGE = 12 * 60 * 60 * 1000; // 12μ‹œκ°„ (ms)
async function startServer() {
const app = express();
const PORT = Number(process.env.PORT || 8888);
// ─── 미듀웨어 ───
app.use(cookieParser());
app.use(express.json({ limit: "2mb" }));
app.use(express.urlencoded({ extended: true }));
// ─────────────────────────────────────────────────────────────
// 인증 라우트
// ─────────────────────────────────────────────────────────────
// POST /api/auth/login
// Reactκ°€ { username, password } JSON을 보내면
// 인증 μ„œλ²„μ— form-urlencoded둜 λ³€ν™˜ν•˜μ—¬ 전달
app.post("/api/auth/login", async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ detail: "μ‚¬λ²ˆκ³Ό λΉ„λ°€λ²ˆν˜Έλ₯Ό μž…λ ₯ν•˜μ„Έμš”" });
}
try {
const params = new URLSearchParams({ username, password });
const response = await fetch(`${AUTH_API_BASE}/login`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params.toString(),
});
const data = await response.json();
if (!response.ok) {
return res.status(response.status).json(data);
}
// 토큰 μΆ”μΆœ β†’ HttpOnly μΏ ν‚€ μ„€μ •
const { token, ...userInfo } = data;
if (!token) {
return res.status(500).json({ detail: "토큰이 응닡에 μ—†μŠ΅λ‹ˆλ‹€" });
}
res.cookie(COOKIE_NAME, token, {
httpOnly: true,
sameSite: "lax",
secure: false, // 사내망 HTTPλ©΄ false, HTTPSλ©΄ true
maxAge: COOKIE_MAX_AGE,
path: "/",
});
// 토큰 μ œμ™Έν•œ μ‚¬μš©μž μ •λ³΄λ§Œ React에 λ°˜ν™˜
return res.json(userInfo);
} catch (err) {
console.error("[auth/login] 인증 μ„œλ²„ 호좜 μ‹€νŒ¨:", err);
return res.status(502).json({ detail: "인증 μ„œλ²„ μ—°κ²° μ‹€νŒ¨" });
}
});
// POST /api/auth/logout
// μΏ ν‚€ μ‚­μ œ + 인증 μ„œλ²„μ— λΈ”λž™λ¦¬μŠ€νŠΈ 등둝 μš”μ²­
app.post("/api/auth/logout", async (req, res) => {
const token = req.cookies?.[COOKIE_NAME];
// μΏ ν‚€ 무쑰건 μ‚­μ œ (μ„œλ²„ 호좜 성곡 μ—¬λΆ€ 무관)
res.clearCookie(COOKIE_NAME, { path: "/" });
if (token) {
try {
await fetch(`${AUTH_API_BASE}/logout`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
} catch (err) {
// 인증 μ„œλ²„ 호좜 μ‹€νŒ¨ν•΄λ„ μΏ ν‚€λŠ” 이미 μ‚­μ œλ¨ β€” λ¬΄μ‹œ
console.warn("[auth/logout] 인증 μ„œλ²„ λΈ”λž™λ¦¬μŠ€νŠΈ 등둝 μ‹€νŒ¨:", err);
}
}
return res.json({ message: "λ‘œκ·Έμ•„μ›ƒ λ˜μ—ˆμŠ΅λ‹ˆλ‹€" });
});
// ─────────────────────────────────────
// QP Backend Pipeline Endpoint β€” POST 기반 (filters μ „λ‹¬μš©)
// ─────────────────────────────────────
app.post("/api/search", async (req, res) => {
const { query, region, product, topK, username } = req.body || {};
try {
const result = await searchPipeline.execute({
query,
region,
product,
topK,
});
// 검색 둜그
writeActivityLog("search", {
username: username || "anonymous",
query: query || "",
filters: {
region: region || "전체",
product: product || "전체",
},
resultCount: result?.results?.length ?? 0,
});
res.json(result);
} catch (error: any) {
console.error("Pipeline Error:", error);
res
.status(500)
.json({ error: error?.message || "Internal Pipeline Error" });
}
});
// ν•˜μœ„ ν˜Έν™˜: κΈ°μ‘΄ GET /api/search?q=... 도 μœ μ§€
app.get("/api/search", async (req, res) => {
const { q, region, product } = req.query;
try {
const result = await searchPipeline.execute({
query: q as string,
region: region as string,
product: product as string,
});
res.json(result);
} catch (error: any) {
console.error("Pipeline Error:", error);
res
.status(500)
.json({ error: error?.message || "Internal Pipeline Error" });
}
});
// ─────────────────────────────────────
// λ¬Έμ„œ 상세 쑰회 β€” FastAPI /search/doc-detail ν”„λ‘μ‹œ
// μΉ΄λ“œ 클릭 μ‹œ λͺ¨λ‹¬μ—μ„œ LLM μ’…ν•© λ‹΅λ³€(vlm_answer)을 λ°›μ•„ ν‘œμ‹œ
// ─────────────────────────────────────
app.post("/api/doc-detail", async (req, res) => {
const { query, docId, pages } = req.body || {};
if (!query || !docId || !Array.isArray(pages)) {
res.status(400).json({
error: "query, docId, pages(array) are required",
});
return;
}
try {
const result = await searchPipeline.fetchDocDetail({
query,
docId,
pages,
});
res.json(result);
} catch (error: any) {
console.error("DocDetail Error:", error);
res
.status(500)
.json({ error: error?.message || "Internal DocDetail Error" });
}
});
// ─────────────────────────────────────
// ν™œλ™ 둜그 기둝 API β€” Reactμ—μ„œ 호좜
// ─────────────────────────────────────
// POST /api/log/detail β€” μΉ΄λ“œ ν΄λ¦­ν•˜μ—¬ 상세 λͺ¨λ‹¬ μ—΄μ—ˆμ„ λ•Œ
app.post("/api/log/detail", async (req, res) => {
const { username, docId, query } = req.body || {};
writeActivityLog("detail", {
username: username || "anonymous",
docId: docId || "",
query: query || "",
});
res.json({ ok: true });
});
// POST /api/log/download β€” λ‹€μš΄λ‘œλ“œ λ²„νŠΌ 클릭 μ‹œ
app.post("/api/log/download", async (req, res) => {
const { username, docId, query, downloadUrl } = req.body || {};
writeActivityLog("download", {
username: username || "anonymous",
docId: docId || "",
query: query || "",
downloadUrl: downloadUrl || "",
});
res.json({ ok: true });
});
// ─────────────────────────────────────
// λ¬Έμ„œμ˜ 전체 νŽ˜μ΄μ§€ 수 쑰회 β€” snap_data/<docId>_*/ ν΄λ”μ˜ *.jpg νŒŒμΌλ“€μ„ 카운트
// 파일λͺ… νŒ¨ν„΄: <docId>_<λ¬Έμ„œλͺ…>_<page>.jpg
// ─────────────────────────────────────
app.get("/api/doc-pages", async (req, res) => {
const docId = String(req.query.docId || "").trim();
if (!docId) {
res.status(400).json({ error: "docId required" });
return;
}
try {
// docIdκ°€ 폴더λͺ… prefix와 동일 (README κΈ°μ€€). 폴더 직접 쑰회.
const folderPath = path.join(SNAP_DATA_DIR, docId);
const files = await fs.readdir(folderPath);
const pageNums: number[] = [];
for (const f of files) {
if (!f.toLowerCase().endsWith(".jpg")) continue;
// 파일 끝의 _<page>.jpg μΆ”μΆœ
const m = f.match(/_(\d+)\.jpg$/i);
if (m) pageNums.push(Number(m[1]));
}
pageNums.sort((a, b) => a - b);
const totalPages =
pageNums.length > 0 ? pageNums[pageNums.length - 1] : 0;
res.json({ totalPages, availablePages: pageNums });
} catch (error: any) {
if (error?.code === "ENOENT") {
res.json({ totalPages: 0, availablePages: [] });
return;
}
console.error("doc-pages error:", error);
res.status(500).json({ error: error?.message || "Internal Error" });
}
});
// ─────────────────────────────────────
// νŽ˜μ΄μ§€ μŠ€ν¬λ¦°μƒ· 정적 μ„œλΉ™ β€” `/snap_data/<doc_id>/<image_name>`
// ─────────────────────────────────────
app.use(
"/snap_data",
express.static(SNAP_DATA_DIR, {
fallthrough: true,
maxAge: "1h",
}),
);
console.log(`[snap_data] serving from ${SNAP_DATA_DIR}`);
// ─────────────────────────────────────
// Vite middleware (dev) / static (prod)
// ─────────────────────────────────────
if (process.env.NODE_ENV !== "production") {
const vite = await createViteServer({
server: { middlewareMode: true },
appType: "spa",
});
app.use(vite.middlewares);
} else {
const distPath = path.join(process.cwd(), "dist");
app.use(express.static(distPath));
app.get("*", (req, res) => {
res.sendFile(path.join(distPath, "index.html"));
});
}
app.listen(PORT, "0.0.0.0", () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log(`[auth] Auth server: ${AUTH_API_BASE}`);
console.log(`[logs] Activity logs: ${LOG_DIR}`);
});
}
startServer();