| 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 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); |
| 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; |
|
|
| 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, 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" }); |
| } |
| }); |
|
|
| |
| 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.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 }); |
| }); |
|
|
| |
| 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 }); |
| }); |
|
|
| |
| |
| |
| |
| 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}`); |
| console.log(`[logs] Activity logs: ${LOG_DIR}`); |
| }); |
| } |
|
|
| startServer(); |