refactor: split backend route domains
This commit is contained in:
224
backend/app/seatmap_routes.py
Normal file
224
backend/app/seatmap_routes.py
Normal file
@@ -0,0 +1,224 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import uuid
|
||||
from typing import Callable
|
||||
|
||||
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
|
||||
def register_seatmap_routes(
|
||||
app: FastAPI,
|
||||
*,
|
||||
upload_dir: Path,
|
||||
get_conn,
|
||||
seat_map_payload_cls,
|
||||
seat_layout_payload_cls,
|
||||
fixed_office_source_key: str,
|
||||
parse_dxf_layout: Callable[[Path], tuple[dict[str, object], list[dict[str, object]]]],
|
||||
serialize_seat_map_payload: Callable[[object], tuple[object, ...]],
|
||||
fetch_seat_layout: Callable[[int, object], dict[str, object]],
|
||||
parse_as_of: Callable[[str | None], object],
|
||||
ensure_fixed_office_seat_map: Callable[[str, bool], dict[str, object] | None],
|
||||
build_center_chair_viewer_html: Callable[[dict[str, object]], str],
|
||||
save_seat_layout: Callable[[int, object], list[dict[str, object]]],
|
||||
) -> None:
|
||||
@app.post("/api/uploads/profile-photo")
|
||||
def upload_profile_photo(file: UploadFile = File(...), member_name: str = Form("")) -> dict[str, str]:
|
||||
suffix = Path(file.filename or "").suffix.lower()
|
||||
if suffix not in {".png", ".jpg", ".jpeg", ".webp", ".gif"}:
|
||||
raise HTTPException(status_code=400, detail="Only image files are allowed.")
|
||||
stem = member_name.strip().replace(" ", "-") or "member"
|
||||
filename = f"{datetime.utcnow().strftime('%Y%m%d%H%M%S')}-{stem}-{uuid.uuid4().hex[:8]}{suffix}"
|
||||
target = upload_dir / filename
|
||||
with target.open("wb") as out_file:
|
||||
shutil.copyfileobj(file.file, out_file)
|
||||
return {"url": f"/uploads/{filename}"}
|
||||
|
||||
@app.post("/api/uploads/seat-map-image")
|
||||
def upload_seat_map_image(file: UploadFile = File(...), seat_map_name: str = Form("")) -> dict[str, str]:
|
||||
suffix = Path(file.filename or "").suffix.lower()
|
||||
if suffix not in {".png", ".jpg", ".jpeg", ".webp", ".gif"}:
|
||||
raise HTTPException(status_code=400, detail="Only image files are allowed.")
|
||||
stem = seat_map_name.strip().replace(" ", "-") or "seat-map"
|
||||
filename = f"seat-map-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}-{stem}-{uuid.uuid4().hex[:8]}{suffix}"
|
||||
target = upload_dir / filename
|
||||
with target.open("wb") as out_file:
|
||||
shutil.copyfileobj(file.file, out_file)
|
||||
return {"url": f"/uploads/{filename}"}
|
||||
|
||||
@app.post("/api/seat-maps/dxf")
|
||||
async def create_dxf_seat_map(file: UploadFile = File(...), name: str = Form(...)) -> dict[str, object]:
|
||||
suffix = Path(file.filename or "").suffix.lower()
|
||||
if suffix != ".dxf":
|
||||
raise HTTPException(status_code=400, detail="DXF 파일만 업로드할 수 있습니다.")
|
||||
|
||||
stem = name.strip().replace(" ", "-") or "seat-map"
|
||||
filename = f"seat-map-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}-{stem}-{uuid.uuid4().hex[:8]}{suffix}"
|
||||
target = upload_dir / filename
|
||||
content = await file.read()
|
||||
with target.open("wb") as out_file:
|
||||
out_file.write(content)
|
||||
|
||||
metadata, slots = parse_dxf_layout(target)
|
||||
|
||||
payload = seat_map_payload_cls(
|
||||
name=name.strip(),
|
||||
source_type="dxf",
|
||||
source_url=f"/uploads/{filename}",
|
||||
image_url="",
|
||||
preview_svg=metadata["preview_svg"],
|
||||
view_box_min_x=metadata["view_box_min_x"],
|
||||
view_box_min_y=metadata["view_box_min_y"],
|
||||
view_box_width=metadata["view_box_width"],
|
||||
view_box_height=metadata["view_box_height"],
|
||||
image_width=None,
|
||||
image_height=None,
|
||||
grid_rows=1,
|
||||
grid_cols=1,
|
||||
cell_gap=0,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
with get_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("UPDATE seat_maps SET is_active = FALSE, updated_at = NOW() WHERE is_active = TRUE")
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO seat_maps (
|
||||
name, source_type, source_url, preview_svg,
|
||||
view_box_min_x, view_box_min_y, view_box_width, view_box_height,
|
||||
image_url, image_width, image_height, grid_rows, grid_cols, cell_gap, is_active
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id, name, source_type, source_url, preview_svg,
|
||||
view_box_min_x, view_box_min_y, view_box_width, view_box_height,
|
||||
image_url, image_width, image_height, grid_rows, grid_cols,
|
||||
cell_gap, is_active, created_at, updated_at
|
||||
""",
|
||||
serialize_seat_map_payload(payload),
|
||||
)
|
||||
seat_map = cur.fetchone()
|
||||
for slot in slots:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO seat_slots (seat_map_id, slot_key, label, x, y, rotation, layer_name)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
seat_map["id"],
|
||||
slot["slot_key"],
|
||||
slot["label"],
|
||||
slot["x"],
|
||||
slot["y"],
|
||||
slot["rotation"],
|
||||
slot["layer_name"],
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
return fetch_seat_layout(int(seat_map["id"]))
|
||||
|
||||
@app.get("/api/seat-maps/active")
|
||||
def get_active_seat_map(office_key: str | None = None) -> dict[str, dict[str, object]]:
|
||||
requested_key = (office_key or "").strip() or fixed_office_source_key
|
||||
seat_map = ensure_fixed_office_seat_map(requested_key, activate=requested_key == fixed_office_source_key)
|
||||
if seat_map is None:
|
||||
raise HTTPException(status_code=404, detail="Active seat map not found.")
|
||||
return {"item": seat_map}
|
||||
|
||||
@app.post("/api/seat-maps")
|
||||
def create_seat_map(payload: seat_map_payload_cls) -> dict[str, dict[str, object]]:
|
||||
with get_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
if payload.is_active:
|
||||
cur.execute("UPDATE seat_maps SET is_active = FALSE, updated_at = NOW() WHERE is_active = TRUE")
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO seat_maps (
|
||||
name, source_type, source_url, preview_svg,
|
||||
view_box_min_x, view_box_min_y, view_box_width, view_box_height,
|
||||
image_url, image_width, image_height, grid_rows, grid_cols, cell_gap, is_active
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id, name, source_type, source_url, preview_svg,
|
||||
view_box_min_x, view_box_min_y, view_box_width, view_box_height,
|
||||
image_url, image_width, image_height, grid_rows, grid_cols,
|
||||
cell_gap, is_active, created_at, updated_at
|
||||
""",
|
||||
serialize_seat_map_payload(payload),
|
||||
)
|
||||
seat_map = cur.fetchone()
|
||||
conn.commit()
|
||||
return {"item": seat_map}
|
||||
|
||||
@app.put("/api/seat-maps/{seat_map_id}")
|
||||
def update_seat_map(seat_map_id: int, payload: seat_map_payload_cls) -> dict[str, dict[str, object]]:
|
||||
with get_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
if payload.source_type != "dxf":
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT COUNT(*) AS count
|
||||
FROM seat_positions
|
||||
WHERE seat_map_id = %s
|
||||
AND (row_index >= %s OR col_index >= %s)
|
||||
""",
|
||||
(seat_map_id, payload.grid_rows, payload.grid_cols),
|
||||
)
|
||||
out_of_bounds_count = int(cur.fetchone()["count"])
|
||||
if out_of_bounds_count > 0:
|
||||
raise HTTPException(status_code=400, detail="현재 배치된 좌석이 새 그리드 범위를 벗어납니다. 먼저 좌석 배치를 정리하세요.")
|
||||
if payload.is_active:
|
||||
cur.execute("UPDATE seat_maps SET is_active = FALSE, updated_at = NOW() WHERE is_active = TRUE AND id <> %s", (seat_map_id,))
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE seat_maps
|
||||
SET name = %s,
|
||||
source_type = %s,
|
||||
source_url = %s,
|
||||
preview_svg = %s,
|
||||
view_box_min_x = %s,
|
||||
view_box_min_y = %s,
|
||||
view_box_width = %s,
|
||||
view_box_height = %s,
|
||||
image_url = %s,
|
||||
image_width = %s,
|
||||
image_height = %s,
|
||||
grid_rows = %s,
|
||||
grid_cols = %s,
|
||||
cell_gap = %s,
|
||||
is_active = %s,
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
RETURNING id, name, source_type, source_url, preview_svg,
|
||||
view_box_min_x, view_box_min_y, view_box_width, view_box_height,
|
||||
image_url, image_width, image_height, grid_rows, grid_cols,
|
||||
cell_gap, is_active, created_at, updated_at
|
||||
""",
|
||||
(*serialize_seat_map_payload(payload), seat_map_id),
|
||||
)
|
||||
seat_map = cur.fetchone()
|
||||
if seat_map is None:
|
||||
raise HTTPException(status_code=404, detail="Seat map not found.")
|
||||
conn.commit()
|
||||
return {"item": seat_map}
|
||||
|
||||
@app.get("/api/seat-maps/{seat_map_id}/layout")
|
||||
def get_seat_layout(seat_map_id: int, as_of: str | None = None) -> dict[str, object]:
|
||||
return fetch_seat_layout(seat_map_id, parse_as_of(as_of))
|
||||
|
||||
@app.get("/api/seat-maps/{seat_map_id}/viewer")
|
||||
def get_seat_map_viewer(seat_map_id: int, as_of: str | None = None) -> HTMLResponse:
|
||||
layout = fetch_seat_layout(seat_map_id, parse_as_of(as_of))
|
||||
seat_map = layout.get("seat_map") or {}
|
||||
if seat_map.get("source_type") not in {"dxf", "fixed_html"}:
|
||||
raise HTTPException(status_code=400, detail="Viewer is only available for supported seat maps.")
|
||||
return HTMLResponse(build_center_chair_viewer_html(layout))
|
||||
|
||||
@app.put("/api/seat-maps/{seat_map_id}/layout")
|
||||
def update_seat_layout(seat_map_id: int, payload: seat_layout_payload_cls) -> dict[str, list[dict[str, object]]]:
|
||||
return {"items": save_seat_layout(seat_map_id, payload)}
|
||||
Reference in New Issue
Block a user