| 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); |
|
|
| |
| |
| 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; |
|
|
| 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 })); |
|
|
| |
| |
| |
|
|
| |
| |
| |
| 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); |
| } |
|
|
| |
| const { token, ...userInfo } = data; |
|
|
| if (!token) { |
| return res.status(500).json({ detail: "ν ν°μ΄ μλ΅μ μμ΅λλ€" }); |
| } |
|
|
| res.cookie(COOKIE_NAME, token, { |
| httpOnly: true, |
| sameSite: "lax", |
| secure: false, |
| maxAge: COOKIE_MAX_AGE, |
| path: "/", |
| }); |
|
|
| |
| return res.json(userInfo); |
| } catch (err) { |
| console.error("[auth/login] μΈμ¦ μλ² νΈμΆ μ€ν¨:", err); |
| return res.status(502).json({ detail: "μΈμ¦ μλ² μ°κ²° μ€ν¨" }); |
| } |
| }); |
|
|
| |
| |
| 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: "λ‘κ·Έμμ λμμ΅λλ€" }); |
| }); |
|
|
| |
| |
| |
| 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" }); |
| } |
| }); |
|
|
| |
| 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" }); |
| } |
| }); |
|
|
| |
| |
| |
| |
| 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" }); |
| } |
| }); |
|
|
| |
| |
| |
| |
| 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 { |
| |
| 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; |
| |
| 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" }); |
| } |
| }); |
|
|
| |
| |
| |
| app.use( |
| "/snap_data", |
| express.static(SNAP_DATA_DIR, { |
| fallthrough: true, |
| maxAge: "1h", |
| }), |
| ); |
| console.log(`[snap_data] serving from ${SNAP_DATA_DIR}`); |
|
|
| |
| |
| |
| 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(); |