사번 필드 및 상세 정보 반영
This commit is contained in:
@@ -12,6 +12,7 @@ SCHEMA_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS members (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
employee_id TEXT,
|
||||
company TEXT,
|
||||
rank TEXT,
|
||||
role TEXT,
|
||||
@@ -31,24 +32,131 @@ CREATE TABLE IF NOT EXISTS members (
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS seat_positions (
|
||||
member_id INTEGER PRIMARY KEY REFERENCES members(id) ON DELETE CASCADE,
|
||||
x INTEGER NOT NULL DEFAULT 0,
|
||||
y INTEGER NOT NULL DEFAULT 0,
|
||||
floor_label TEXT,
|
||||
CREATE TABLE IF NOT EXISTS seat_maps (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
image_url TEXT NOT NULL,
|
||||
source_type TEXT NOT NULL DEFAULT 'image',
|
||||
source_url TEXT,
|
||||
preview_svg TEXT,
|
||||
view_box_min_x DOUBLE PRECISION,
|
||||
view_box_min_y DOUBLE PRECISION,
|
||||
view_box_width DOUBLE PRECISION,
|
||||
view_box_height DOUBLE PRECISION,
|
||||
image_width INTEGER,
|
||||
image_height INTEGER,
|
||||
grid_rows INTEGER NOT NULL,
|
||||
grid_cols INTEGER NOT NULL,
|
||||
cell_gap INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS snapshots (
|
||||
CREATE TABLE IF NOT EXISTS seat_positions (
|
||||
member_id INTEGER PRIMARY KEY REFERENCES members(id) ON DELETE CASCADE,
|
||||
seat_map_id INTEGER REFERENCES seat_maps(id) ON DELETE CASCADE,
|
||||
seat_slot_id INTEGER,
|
||||
row_index INTEGER NOT NULL DEFAULT 0,
|
||||
col_index INTEGER NOT NULL DEFAULT 0,
|
||||
seat_label TEXT,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS seat_slots (
|
||||
id SERIAL PRIMARY KEY,
|
||||
snapshot_month TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
seat_map_id INTEGER NOT NULL REFERENCES seat_maps(id) ON DELETE CASCADE,
|
||||
slot_key TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
x DOUBLE PRECISION NOT NULL,
|
||||
y DOUBLE PRECISION NOT NULL,
|
||||
rotation DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
layer_name TEXT NOT NULL DEFAULT 'chair',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (seat_map_id, slot_key)
|
||||
);
|
||||
"""
|
||||
|
||||
MIGRATION_SQL = """
|
||||
ALTER TABLE members ADD COLUMN IF NOT EXISTS employee_id TEXT;
|
||||
ALTER TABLE members ADD COLUMN IF NOT EXISTS sort_order INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE seat_positions ADD COLUMN IF NOT EXISTS seat_map_id INTEGER REFERENCES seat_maps(id) ON DELETE CASCADE;
|
||||
ALTER TABLE seat_positions ADD COLUMN IF NOT EXISTS seat_slot_id INTEGER;
|
||||
ALTER TABLE seat_positions ADD COLUMN IF NOT EXISTS row_index INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE seat_positions ADD COLUMN IF NOT EXISTS col_index INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE seat_positions ADD COLUMN IF NOT EXISTS seat_label TEXT;
|
||||
ALTER TABLE seat_positions ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS source_type TEXT NOT NULL DEFAULT 'image';
|
||||
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS source_url TEXT;
|
||||
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS preview_svg TEXT;
|
||||
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS view_box_min_x DOUBLE PRECISION;
|
||||
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS view_box_min_y DOUBLE PRECISION;
|
||||
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS view_box_width DOUBLE PRECISION;
|
||||
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS view_box_height DOUBLE PRECISION;
|
||||
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS image_width INTEGER;
|
||||
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS image_height INTEGER;
|
||||
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS cell_gap INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE seat_maps ALTER COLUMN image_url DROP NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS seat_slots (
|
||||
id SERIAL PRIMARY KEY,
|
||||
seat_map_id INTEGER NOT NULL REFERENCES seat_maps(id) ON DELETE CASCADE,
|
||||
slot_key TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
x DOUBLE PRECISION NOT NULL,
|
||||
y DOUBLE PRECISION NOT NULL,
|
||||
rotation DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
layer_name TEXT NOT NULL DEFAULT 'chair',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (seat_map_id, slot_key)
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'seat_positions' AND column_name = 'x'
|
||||
) THEN
|
||||
EXECUTE 'UPDATE seat_positions SET row_index = COALESCE(y, row_index, 0), col_index = COALESCE(x, col_index, 0) WHERE seat_map_id IS NULL';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'seat_positions' AND column_name = 'floor_label'
|
||||
) THEN
|
||||
EXECUTE 'UPDATE seat_positions SET seat_label = COALESCE(seat_label, floor_label) WHERE seat_label IS NULL';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS seat_positions_map_cell_idx
|
||||
ON seat_positions (seat_map_id, row_index, col_index)
|
||||
WHERE seat_map_id IS NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS seat_positions_slot_idx
|
||||
ON seat_positions (seat_slot_id)
|
||||
WHERE seat_slot_id IS NOT NULL;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.table_constraints
|
||||
WHERE constraint_name = 'seat_positions_seat_slot_id_fkey'
|
||||
AND table_name = 'seat_positions'
|
||||
) THEN
|
||||
ALTER TABLE seat_positions
|
||||
ADD CONSTRAINT seat_positions_seat_slot_id_fkey
|
||||
FOREIGN KEY (seat_slot_id) REFERENCES seat_slots(id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ LEGACY_STATIC_DIR = LEGACY_DIR / "static"
|
||||
class MemberPayload(BaseModel):
|
||||
id: int | None = None
|
||||
name: str = Field(min_length=1)
|
||||
employee_id: str = ""
|
||||
company: str = ""
|
||||
rank: str = ""
|
||||
role: str = ""
|
||||
@@ -91,6 +92,8 @@ class SeatLayoutPayload(BaseModel):
|
||||
LEGACY_HEADER_MAP = {
|
||||
"이름": "name",
|
||||
"name": "name",
|
||||
"tag": "employee_id",
|
||||
"employee_id": "employee_id",
|
||||
"소속회사": "company",
|
||||
"co": "company",
|
||||
"company": "company",
|
||||
@@ -131,6 +134,7 @@ LEGACY_HEADER_MAP = {
|
||||
def serialize_member_payload(item: MemberPayload, sort_order: int) -> tuple[object, ...]:
|
||||
return (
|
||||
item.name.strip(),
|
||||
item.employee_id.strip(),
|
||||
item.company.strip(),
|
||||
item.rank.strip(),
|
||||
item.role.strip(),
|
||||
@@ -154,7 +158,7 @@ def fetch_members() -> list[dict[str, object]]:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, company, rank, role, department, grp, division, team, cell,
|
||||
SELECT id, name, employee_id, company, rank, role, department, grp, division, team, cell,
|
||||
work_status, work_time, phone, email, seat_label, photo_url,
|
||||
sort_order, created_at, updated_at
|
||||
FROM members
|
||||
@@ -235,6 +239,48 @@ def compute_slot_label(index: int) -> str:
|
||||
return f"CHAIR-{index + 1:03d}"
|
||||
|
||||
|
||||
def is_chair_layer(layer_name: str) -> bool:
|
||||
raw = layer_name.strip().lower()
|
||||
compact = raw.replace("-", "").replace("_", "").replace(" ", "")
|
||||
return raw in {"chair", "_chair", "-chair"} or compact.endswith("chair")
|
||||
|
||||
|
||||
def inspect_dxf_header(file_path: Path) -> tuple[str, str]:
|
||||
with file_path.open("rb") as source:
|
||||
header = source.read(128)
|
||||
header_text = header.decode("latin-1", errors="ignore").replace("\x00", "")
|
||||
preview = header[:32].hex(" ")
|
||||
|
||||
if header_text.startswith("AutoCAD Binary DXF"):
|
||||
return ("binary_dxf", preview)
|
||||
if header_text.startswith("0\nSECTION") or header_text.startswith("0\r\nSECTION"):
|
||||
return ("ascii_dxf", preview)
|
||||
if header.startswith(b"AC10"):
|
||||
return ("dwg_or_dwg_like", preview)
|
||||
return ("unknown", preview)
|
||||
|
||||
|
||||
def iter_render_entities(entity: ezdxf.entities.DXFGraphic, inherited_layer: str | None = None, depth: int = 0) -> list[ezdxf.entities.DXFGraphic]:
|
||||
if depth > 6:
|
||||
return []
|
||||
entity_type = entity.dxftype()
|
||||
current_layer = inherited_layer or entity.dxf.layer
|
||||
if entity_type == "INSERT":
|
||||
expanded: list[ezdxf.entities.DXFGraphic] = []
|
||||
try:
|
||||
for child in entity.virtual_entities():
|
||||
child_layer = child.dxf.layer
|
||||
if child_layer == "0":
|
||||
child.dxf.layer = current_layer
|
||||
expanded.extend(iter_render_entities(child, inherited_layer=current_layer, depth=depth + 1))
|
||||
except Exception:
|
||||
return []
|
||||
return expanded
|
||||
if inherited_layer and entity.dxf.layer == "0":
|
||||
entity.dxf.layer = inherited_layer
|
||||
return [entity]
|
||||
|
||||
|
||||
def get_entity_points(entity: ezdxf.entities.DXFGraphic) -> list[tuple[float, float]]:
|
||||
entity_type = entity.dxftype()
|
||||
if entity_type == "LINE":
|
||||
@@ -263,6 +309,24 @@ def get_entity_points(entity: ezdxf.entities.DXFGraphic) -> list[tuple[float, fl
|
||||
if entity_type == "POINT":
|
||||
location = entity.dxf.location
|
||||
return [(float(location.x), float(location.y))]
|
||||
if entity_type == "SPLINE":
|
||||
try:
|
||||
return [(float(point[0]), float(point[1])) for point in entity.flattening(2)]
|
||||
except Exception:
|
||||
return []
|
||||
if entity_type == "ELLIPSE":
|
||||
try:
|
||||
return [(float(point[0]), float(point[1])) for point in entity.flattening(2)]
|
||||
except Exception:
|
||||
center = entity.dxf.center
|
||||
major_axis = entity.dxf.major_axis
|
||||
ratio = float(entity.dxf.ratio)
|
||||
radius_x = math.hypot(float(major_axis.x), float(major_axis.y))
|
||||
radius_y = radius_x * ratio
|
||||
return [
|
||||
(float(center.x - radius_x), float(center.y - radius_y)),
|
||||
(float(center.x + radius_x), float(center.y + radius_y)),
|
||||
]
|
||||
if entity_type == "INSERT":
|
||||
insert = entity.dxf.insert
|
||||
return [(float(insert.x), float(insert.y))]
|
||||
@@ -280,23 +344,84 @@ def get_entity_center(entity: ezdxf.entities.DXFGraphic) -> tuple[float, float]
|
||||
return ((min_x + max_x) / 2.0, (min_y + max_y) / 2.0)
|
||||
|
||||
|
||||
def line_svg(points: list[tuple[float, float]]) -> str:
|
||||
def get_entity_bounds(entity: ezdxf.entities.DXFGraphic) -> tuple[float, float, float, float] | None:
|
||||
points = get_entity_points(entity)
|
||||
if not points:
|
||||
return None
|
||||
min_x = min(point[0] for point in points)
|
||||
max_x = max(point[0] for point in points)
|
||||
min_y = min(point[1] for point in points)
|
||||
max_y = max(point[1] for point in points)
|
||||
return (min_x, min_y, max_x, max_y)
|
||||
|
||||
|
||||
def compute_bounds_from_points(points: list[tuple[float, float]]) -> tuple[float, float, float, float]:
|
||||
min_x = min(point[0] for point in points)
|
||||
max_x = max(point[0] for point in points)
|
||||
min_y = min(point[1] for point in points)
|
||||
max_y = max(point[1] for point in points)
|
||||
return (min_x, min_y, max(max_x - min_x, 1.0), max(max_y - min_y, 1.0))
|
||||
|
||||
|
||||
def percentile(values: list[float], ratio: float) -> float:
|
||||
if not values:
|
||||
return 0.0
|
||||
ordered = sorted(values)
|
||||
index = max(0, min(len(ordered) - 1, round((len(ordered) - 1) * ratio)))
|
||||
return float(ordered[index])
|
||||
|
||||
|
||||
def compute_focus_bounds(slot_points: list[tuple[float, float]]) -> tuple[float, float, float, float]:
|
||||
x_values = [point[0] for point in slot_points]
|
||||
y_values = [point[1] for point in slot_points]
|
||||
min_x = percentile(x_values, 0.02)
|
||||
max_x = percentile(x_values, 0.98)
|
||||
min_y = percentile(y_values, 0.02)
|
||||
max_y = percentile(y_values, 0.98)
|
||||
width = max(max_x - min_x, 1.0)
|
||||
height = max(max_y - min_y, 1.0)
|
||||
pad_x = max(width * 0.08, 500.0)
|
||||
pad_y = max(height * 0.08, 500.0)
|
||||
return (min_x - pad_x, min_y - pad_y, max_x + pad_x, max_y + pad_y)
|
||||
|
||||
|
||||
def bounds_intersect(bounds: tuple[float, float, float, float], focus_bounds: tuple[float, float, float, float]) -> bool:
|
||||
min_x, min_y, max_x, max_y = bounds
|
||||
focus_min_x, focus_min_y, focus_max_x, focus_max_y = focus_bounds
|
||||
return not (
|
||||
max_x < focus_min_x
|
||||
or min_x > focus_max_x
|
||||
or max_y < focus_min_y
|
||||
or min_y > focus_max_y
|
||||
)
|
||||
|
||||
|
||||
def line_svg(points: list[tuple[float, float]], css_class: str = "seatmap-dxf-entity") -> str:
|
||||
if len(points) < 2:
|
||||
return ""
|
||||
coordinates = " ".join(f"{x:.2f},{-y:.2f}" for x, y in points)
|
||||
return f'<polyline points="{coordinates}" fill="none" stroke="#94a3b8" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round" />'
|
||||
|
||||
|
||||
def circle_svg(center_x: float, center_y: float, radius: float, stroke: str = "#cbd5e1", fill: str = "none") -> str:
|
||||
return (
|
||||
f'<circle cx="{center_x:.2f}" cy="{-center_y:.2f}" r="{radius:.2f}" '
|
||||
f'stroke="{stroke}" stroke-width="1.1" fill="{fill}" />'
|
||||
f'<polyline class="{css_class}" points="{coordinates}" fill="none" '
|
||||
'stroke-linejoin="round" stroke-linecap="round" />'
|
||||
)
|
||||
|
||||
|
||||
def circle_svg(
|
||||
center_x: float,
|
||||
center_y: float,
|
||||
radius: float,
|
||||
stroke: str = "#475569",
|
||||
fill: str = "none",
|
||||
css_class: str = "seatmap-dxf-entity",
|
||||
) -> str:
|
||||
return (
|
||||
f'<circle class="{css_class}" cx="{center_x:.2f}" cy="{-center_y:.2f}" r="{radius:.2f}" '
|
||||
f'stroke="{stroke}" fill="{fill}" />'
|
||||
)
|
||||
|
||||
|
||||
def build_dxf_preview_svg(
|
||||
entities: list[ezdxf.entities.DXFGraphic],
|
||||
chair_slots: list[dict[str, object]],
|
||||
bounds: tuple[float, float, float, float],
|
||||
) -> str:
|
||||
min_x, min_y, width, height = bounds
|
||||
@@ -304,17 +429,25 @@ def build_dxf_preview_svg(
|
||||
svg_parts: list[str] = []
|
||||
|
||||
for entity in entities:
|
||||
layer_name = entity.dxf.layer.lower()
|
||||
if layer_name == "chair":
|
||||
continue
|
||||
layer_name = entity.dxf.layer
|
||||
is_chair = is_chair_layer(layer_name)
|
||||
css_class = "seatmap-dxf-chair-entity" if is_chair else "seatmap-dxf-entity"
|
||||
entity_type = entity.dxftype()
|
||||
if entity_type in {"LINE", "LWPOLYLINE", "POLYLINE"}:
|
||||
svg = line_svg(get_entity_points(entity))
|
||||
if entity_type in {"LINE", "LWPOLYLINE", "POLYLINE", "SPLINE", "ELLIPSE"}:
|
||||
svg = line_svg(get_entity_points(entity), css_class=css_class)
|
||||
if svg:
|
||||
svg_parts.append(svg)
|
||||
elif entity_type == "CIRCLE":
|
||||
center = entity.dxf.center
|
||||
svg_parts.append(circle_svg(float(center.x), float(center.y), float(entity.dxf.radius)))
|
||||
svg_parts.append(
|
||||
circle_svg(
|
||||
float(center.x),
|
||||
float(center.y),
|
||||
float(entity.dxf.radius),
|
||||
fill="none",
|
||||
css_class=css_class,
|
||||
)
|
||||
)
|
||||
elif entity_type == "ARC":
|
||||
center = entity.dxf.center
|
||||
radius = float(entity.dxf.radius)
|
||||
@@ -326,25 +459,14 @@ def build_dxf_preview_svg(
|
||||
end_y = float(center.y) + radius * math.sin(end_angle)
|
||||
large_arc = 1 if abs(float(entity.dxf.end_angle) - float(entity.dxf.start_angle)) > 180 else 0
|
||||
svg_parts.append(
|
||||
f'<path d="M {start_x:.2f} {-start_y:.2f} A {radius:.2f} {radius:.2f} 0 {large_arc} 0 {end_x:.2f} {-end_y:.2f}" '
|
||||
'fill="none" stroke="#94a3b8" stroke-width="1.2" />'
|
||||
f'<path class="{css_class}" d="M {start_x:.2f} {-start_y:.2f} '
|
||||
f'A {radius:.2f} {radius:.2f} 0 {large_arc} 0 {end_x:.2f} {-end_y:.2f}" fill="none" />'
|
||||
)
|
||||
|
||||
for slot in chair_slots:
|
||||
svg_parts.append(
|
||||
circle_svg(
|
||||
float(slot["x"]),
|
||||
float(slot["y"]),
|
||||
10,
|
||||
stroke="#0f766e",
|
||||
fill="rgba(45, 212, 191, 0.22)",
|
||||
)
|
||||
)
|
||||
|
||||
view_box = f"{min_x:.2f} {-max_y:.2f} {max(width, 1.0):.2f} {max(height, 1.0):.2f}"
|
||||
return (
|
||||
f'<svg class="seatmap-preview-svg" viewBox="{view_box}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">'
|
||||
'<rect width="100%" height="100%" fill="#f8fafc" />'
|
||||
'<rect width="100%" height="100%" fill="#ffffff" />'
|
||||
+ "".join(svg_parts)
|
||||
+ "</svg>"
|
||||
)
|
||||
@@ -357,42 +479,44 @@ def parse_dxf_layout(file_path: Path) -> tuple[dict[str, object], list[dict[str,
|
||||
try:
|
||||
document, _ = recover.readfile(file_path)
|
||||
except Exception as exc:
|
||||
with file_path.open("rb") as source:
|
||||
header = source.read(64)
|
||||
header_text = header.decode("latin-1", errors="ignore")
|
||||
if header.startswith(b"AC10") or "AutoCAD Binary DXF" in header_text:
|
||||
kind, preview = inspect_dxf_header(file_path)
|
||||
if kind == "binary_dxf":
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="DXF 파일을 해석하지 못했습니다. binary DXF 또는 손상된 DXF일 수 있습니다. 가능하면 ASCII DXF로 다시 저장해 업로드하세요.",
|
||||
detail=f"Binary DXF로 보이지만 해석에 실패했습니다. 가능하면 ASCII DXF로 다시 저장해 업로드하세요. 헤더={preview}",
|
||||
) from exc
|
||||
if kind == "dwg_or_dwg_like":
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"업로드한 파일은 DWG 계열 헤더(AC10xx)로 보입니다. DWG가 아니라 ASCII DXF로 다시 저장해 업로드하세요. 헤더={preview}",
|
||||
) from exc
|
||||
if kind == "ascii_dxf":
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"ASCII DXF로 보이지만 구조를 해석하지 못했습니다. 도면을 다른 DXF 버전으로 다시 저장해보세요. 헤더={preview}",
|
||||
) from exc
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="업로드한 파일이 DXF 형식으로 읽히지 않습니다. DWG 파일이거나 확장자만 dxf로 바뀐 파일일 수 있습니다.",
|
||||
detail=f"업로드한 파일 형식을 판별하지 못했습니다. 확장자만 dxf인 파일일 수 있습니다. 헤더={preview}",
|
||||
) from exc
|
||||
modelspace = document.modelspace()
|
||||
all_entities = [entity for entity in modelspace if entity.dxftype() in {"LINE", "LWPOLYLINE", "POLYLINE", "CIRCLE", "ARC", "POINT", "INSERT"}]
|
||||
points: list[tuple[float, float]] = []
|
||||
base_entities = [entity for entity in modelspace if entity.dxftype() in {"LINE", "LWPOLYLINE", "POLYLINE", "CIRCLE", "ARC", "INSERT", "SPLINE", "ELLIPSE"}]
|
||||
all_entities: list[ezdxf.entities.DXFGraphic] = []
|
||||
for entity in base_entities:
|
||||
all_entities.extend(iter_render_entities(entity))
|
||||
chair_entities: list[ezdxf.entities.DXFGraphic] = []
|
||||
chair_points: list[tuple[float, float]] = []
|
||||
for entity in all_entities:
|
||||
entity_points = get_entity_points(entity)
|
||||
if entity_points:
|
||||
points.extend(entity_points)
|
||||
if entity.dxf.layer.lower() == "chair":
|
||||
if is_chair_layer(entity.dxf.layer):
|
||||
chair_entities.append(entity)
|
||||
chair_points.extend(get_entity_points(entity))
|
||||
|
||||
if not chair_entities:
|
||||
raise HTTPException(status_code=400, detail="DXF 파일에서 chair 레이어를 찾지 못했습니다.")
|
||||
raise HTTPException(status_code=400, detail="DXF 파일에서 chair 계열 레이어를 찾지 못했습니다.")
|
||||
|
||||
if not points:
|
||||
if not chair_points:
|
||||
raise HTTPException(status_code=400, detail="DXF 좌표를 해석하지 못했습니다.")
|
||||
|
||||
min_x = min(point[0] for point in points)
|
||||
max_x = max(point[0] for point in points)
|
||||
min_y = min(point[1] for point in points)
|
||||
max_y = max(point[1] for point in points)
|
||||
width = max(max_x - min_x, 1.0)
|
||||
height = max(max_y - min_y, 1.0)
|
||||
|
||||
slots: list[dict[str, object]] = []
|
||||
for index, entity in enumerate(sorted(chair_entities, key=lambda item: (-(get_entity_center(item) or (0.0, 0.0))[1], (get_entity_center(item) or (0.0, 0.0))[0]))):
|
||||
center = get_entity_center(entity)
|
||||
@@ -412,7 +536,29 @@ def parse_dxf_layout(file_path: Path) -> tuple[dict[str, object], list[dict[str,
|
||||
if not slots:
|
||||
raise HTTPException(status_code=400, detail="chair 레이어에서 좌석 위치를 추출하지 못했습니다.")
|
||||
|
||||
preview_svg = build_dxf_preview_svg(all_entities, slots, (min_x, min_y, width, height))
|
||||
slot_points = [(float(slot["x"]), float(slot["y"])) for slot in slots]
|
||||
focus_bounds = compute_focus_bounds(slot_points)
|
||||
visible_entities: list[ezdxf.entities.DXFGraphic] = []
|
||||
visible_points: list[tuple[float, float]] = []
|
||||
for entity in all_entities:
|
||||
entity_bounds = get_entity_bounds(entity)
|
||||
if entity_bounds is None:
|
||||
continue
|
||||
if bounds_intersect(entity_bounds, focus_bounds):
|
||||
visible_entities.append(entity)
|
||||
visible_points.extend(get_entity_points(entity))
|
||||
|
||||
if not visible_entities or not visible_points:
|
||||
visible_entities = all_entities
|
||||
visible_points = chair_points
|
||||
|
||||
focus_min_x, focus_min_y, focus_max_x, focus_max_y = focus_bounds
|
||||
min_x = focus_min_x
|
||||
min_y = focus_min_y
|
||||
width = max(focus_max_x - focus_min_x, 1.0)
|
||||
height = max(focus_max_y - focus_min_y, 1.0)
|
||||
|
||||
preview_svg = build_dxf_preview_svg(visible_entities, (min_x, min_y, width, height))
|
||||
metadata = {
|
||||
"source_type": "dxf",
|
||||
"view_box_min_x": round(min_x, 3),
|
||||
@@ -421,7 +567,7 @@ def parse_dxf_layout(file_path: Path) -> tuple[dict[str, object], list[dict[str,
|
||||
"view_box_height": round(height, 3),
|
||||
"preview_svg": preview_svg,
|
||||
"grid_rows": 1,
|
||||
"grid_cols": max(len(slots), 1),
|
||||
"grid_cols": 1,
|
||||
"image_width": None,
|
||||
"image_height": None,
|
||||
"cell_gap": 0,
|
||||
@@ -581,10 +727,10 @@ def replace_members(items: list[MemberPayload]) -> list[dict[str, object]]:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO members (
|
||||
name, company, rank, role, department, grp, division, team, cell,
|
||||
name, employee_id, company, rank, role, department, grp, division, team, cell,
|
||||
work_status, work_time, phone, email, seat_label, photo_url, sort_order
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
serialize_member_payload(item, index),
|
||||
)
|
||||
@@ -702,11 +848,11 @@ def create_member(payload: MemberPayload) -> dict[str, object]:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO members (
|
||||
name, company, rank, role, department, grp, division, team, cell,
|
||||
name, employee_id, company, rank, role, department, grp, division, team, cell,
|
||||
work_status, work_time, phone, email, seat_label, photo_url, sort_order
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id, name, company, rank, role, department, grp, division, team, cell,
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id, name, employee_id, company, rank, role, department, grp, division, team, cell,
|
||||
work_status, work_time, phone, email, seat_label, photo_url,
|
||||
sort_order, created_at, updated_at
|
||||
""",
|
||||
@@ -730,6 +876,7 @@ def update_member(member_id: int, payload: MemberPayload) -> dict[str, object]:
|
||||
"""
|
||||
UPDATE members
|
||||
SET name = %s,
|
||||
employee_id = %s,
|
||||
company = %s,
|
||||
rank = %s,
|
||||
role = %s,
|
||||
@@ -747,7 +894,7 @@ def update_member(member_id: int, payload: MemberPayload) -> dict[str, object]:
|
||||
sort_order = COALESCE(%s, sort_order),
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
RETURNING id, name, company, rank, role, department, grp, division, team, cell,
|
||||
RETURNING id, name, employee_id, company, rank, role, department, grp, division, team, cell,
|
||||
work_status, work_time, phone, email, seat_label, photo_url,
|
||||
sort_order, created_at, updated_at
|
||||
""",
|
||||
@@ -821,8 +968,6 @@ async def create_dxf_seat_map(file: UploadFile = File(...), name: str = Form(...
|
||||
try:
|
||||
metadata, slots = parse_dxf_layout(target)
|
||||
except Exception:
|
||||
if target.exists():
|
||||
target.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
payload = SeatMapPayload(
|
||||
@@ -838,7 +983,7 @@ async def create_dxf_seat_map(file: UploadFile = File(...), name: str = Form(...
|
||||
image_width=None,
|
||||
image_height=None,
|
||||
grid_rows=1,
|
||||
grid_cols=max(len(slots), 1),
|
||||
grid_cols=1,
|
||||
cell_gap=0,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user