QPIDS / server.ts
Hyungseoky's picture
Rename server+.ts to server.ts
41fc7a2 verified
Raw
History Blame Contribute Delete
9.28 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");
// ─────────────────────────────────────────────────────────────
// 인증 μ„œλ²„ μ„€μ •
// ─────────────────────────────────────────────────────────────
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 } = req.body || {};
try {
const result = await searchPipeline.execute({
query,
region,
product,
topK,
});
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" });
}
});
// ─────────────────────────────────────
// λ¬Έμ„œμ˜ 전체 νŽ˜μ΄μ§€ 수 쑰회 β€” 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}`);
});
}
startServer();