225 lines
11 KiB
Python
225 lines
11 KiB
Python
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)}
|