사번 필드 및 상세 정보 반영
This commit is contained in:
@@ -12,6 +12,7 @@ SCHEMA_SQL = """
|
|||||||
CREATE TABLE IF NOT EXISTS members (
|
CREATE TABLE IF NOT EXISTS members (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
|
employee_id TEXT,
|
||||||
company TEXT,
|
company TEXT,
|
||||||
rank TEXT,
|
rank TEXT,
|
||||||
role TEXT,
|
role TEXT,
|
||||||
@@ -31,24 +32,131 @@ CREATE TABLE IF NOT EXISTS members (
|
|||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS seat_positions (
|
CREATE TABLE IF NOT EXISTS seat_maps (
|
||||||
member_id INTEGER PRIMARY KEY REFERENCES members(id) ON DELETE CASCADE,
|
id SERIAL PRIMARY KEY,
|
||||||
x INTEGER NOT NULL DEFAULT 0,
|
name TEXT NOT NULL,
|
||||||
y INTEGER NOT NULL DEFAULT 0,
|
image_url TEXT NOT NULL,
|
||||||
floor_label TEXT,
|
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()
|
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,
|
id SERIAL PRIMARY KEY,
|
||||||
snapshot_month TEXT NOT NULL,
|
seat_map_id INTEGER NOT NULL REFERENCES seat_maps(id) ON DELETE CASCADE,
|
||||||
file_path TEXT NOT NULL,
|
slot_key TEXT NOT NULL,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
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 = """
|
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 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):
|
class MemberPayload(BaseModel):
|
||||||
id: int | None = None
|
id: int | None = None
|
||||||
name: str = Field(min_length=1)
|
name: str = Field(min_length=1)
|
||||||
|
employee_id: str = ""
|
||||||
company: str = ""
|
company: str = ""
|
||||||
rank: str = ""
|
rank: str = ""
|
||||||
role: str = ""
|
role: str = ""
|
||||||
@@ -91,6 +92,8 @@ class SeatLayoutPayload(BaseModel):
|
|||||||
LEGACY_HEADER_MAP = {
|
LEGACY_HEADER_MAP = {
|
||||||
"이름": "name",
|
"이름": "name",
|
||||||
"name": "name",
|
"name": "name",
|
||||||
|
"tag": "employee_id",
|
||||||
|
"employee_id": "employee_id",
|
||||||
"소속회사": "company",
|
"소속회사": "company",
|
||||||
"co": "company",
|
"co": "company",
|
||||||
"company": "company",
|
"company": "company",
|
||||||
@@ -131,6 +134,7 @@ LEGACY_HEADER_MAP = {
|
|||||||
def serialize_member_payload(item: MemberPayload, sort_order: int) -> tuple[object, ...]:
|
def serialize_member_payload(item: MemberPayload, sort_order: int) -> tuple[object, ...]:
|
||||||
return (
|
return (
|
||||||
item.name.strip(),
|
item.name.strip(),
|
||||||
|
item.employee_id.strip(),
|
||||||
item.company.strip(),
|
item.company.strip(),
|
||||||
item.rank.strip(),
|
item.rank.strip(),
|
||||||
item.role.strip(),
|
item.role.strip(),
|
||||||
@@ -154,7 +158,7 @@ def fetch_members() -> list[dict[str, object]]:
|
|||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
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,
|
work_status, work_time, phone, email, seat_label, photo_url,
|
||||||
sort_order, created_at, updated_at
|
sort_order, created_at, updated_at
|
||||||
FROM members
|
FROM members
|
||||||
@@ -235,6 +239,48 @@ def compute_slot_label(index: int) -> str:
|
|||||||
return f"CHAIR-{index + 1:03d}"
|
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]]:
|
def get_entity_points(entity: ezdxf.entities.DXFGraphic) -> list[tuple[float, float]]:
|
||||||
entity_type = entity.dxftype()
|
entity_type = entity.dxftype()
|
||||||
if entity_type == "LINE":
|
if entity_type == "LINE":
|
||||||
@@ -263,6 +309,24 @@ def get_entity_points(entity: ezdxf.entities.DXFGraphic) -> list[tuple[float, fl
|
|||||||
if entity_type == "POINT":
|
if entity_type == "POINT":
|
||||||
location = entity.dxf.location
|
location = entity.dxf.location
|
||||||
return [(float(location.x), float(location.y))]
|
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":
|
if entity_type == "INSERT":
|
||||||
insert = entity.dxf.insert
|
insert = entity.dxf.insert
|
||||||
return [(float(insert.x), float(insert.y))]
|
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)
|
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:
|
if len(points) < 2:
|
||||||
return ""
|
return ""
|
||||||
coordinates = " ".join(f"{x:.2f},{-y:.2f}" for x, y in points)
|
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 (
|
return (
|
||||||
f'<circle cx="{center_x:.2f}" cy="{-center_y:.2f}" r="{radius:.2f}" '
|
f'<polyline class="{css_class}" points="{coordinates}" fill="none" '
|
||||||
f'stroke="{stroke}" stroke-width="1.1" fill="{fill}" />'
|
'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(
|
def build_dxf_preview_svg(
|
||||||
entities: list[ezdxf.entities.DXFGraphic],
|
entities: list[ezdxf.entities.DXFGraphic],
|
||||||
chair_slots: list[dict[str, object]],
|
|
||||||
bounds: tuple[float, float, float, float],
|
bounds: tuple[float, float, float, float],
|
||||||
) -> str:
|
) -> str:
|
||||||
min_x, min_y, width, height = bounds
|
min_x, min_y, width, height = bounds
|
||||||
@@ -304,17 +429,25 @@ def build_dxf_preview_svg(
|
|||||||
svg_parts: list[str] = []
|
svg_parts: list[str] = []
|
||||||
|
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
layer_name = entity.dxf.layer.lower()
|
layer_name = entity.dxf.layer
|
||||||
if layer_name == "chair":
|
is_chair = is_chair_layer(layer_name)
|
||||||
continue
|
css_class = "seatmap-dxf-chair-entity" if is_chair else "seatmap-dxf-entity"
|
||||||
entity_type = entity.dxftype()
|
entity_type = entity.dxftype()
|
||||||
if entity_type in {"LINE", "LWPOLYLINE", "POLYLINE"}:
|
if entity_type in {"LINE", "LWPOLYLINE", "POLYLINE", "SPLINE", "ELLIPSE"}:
|
||||||
svg = line_svg(get_entity_points(entity))
|
svg = line_svg(get_entity_points(entity), css_class=css_class)
|
||||||
if svg:
|
if svg:
|
||||||
svg_parts.append(svg)
|
svg_parts.append(svg)
|
||||||
elif entity_type == "CIRCLE":
|
elif entity_type == "CIRCLE":
|
||||||
center = entity.dxf.center
|
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":
|
elif entity_type == "ARC":
|
||||||
center = entity.dxf.center
|
center = entity.dxf.center
|
||||||
radius = float(entity.dxf.radius)
|
radius = float(entity.dxf.radius)
|
||||||
@@ -326,25 +459,14 @@ def build_dxf_preview_svg(
|
|||||||
end_y = float(center.y) + radius * math.sin(end_angle)
|
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
|
large_arc = 1 if abs(float(entity.dxf.end_angle) - float(entity.dxf.start_angle)) > 180 else 0
|
||||||
svg_parts.append(
|
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}" '
|
f'<path class="{css_class}" d="M {start_x:.2f} {-start_y:.2f} '
|
||||||
'fill="none" stroke="#94a3b8" stroke-width="1.2" />'
|
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}"
|
view_box = f"{min_x:.2f} {-max_y:.2f} {max(width, 1.0):.2f} {max(height, 1.0):.2f}"
|
||||||
return (
|
return (
|
||||||
f'<svg class="seatmap-preview-svg" viewBox="{view_box}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">'
|
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)
|
+ "".join(svg_parts)
|
||||||
+ "</svg>"
|
+ "</svg>"
|
||||||
)
|
)
|
||||||
@@ -357,42 +479,44 @@ def parse_dxf_layout(file_path: Path) -> tuple[dict[str, object], list[dict[str,
|
|||||||
try:
|
try:
|
||||||
document, _ = recover.readfile(file_path)
|
document, _ = recover.readfile(file_path)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
with file_path.open("rb") as source:
|
kind, preview = inspect_dxf_header(file_path)
|
||||||
header = source.read(64)
|
if kind == "binary_dxf":
|
||||||
header_text = header.decode("latin-1", errors="ignore")
|
|
||||||
if header.startswith(b"AC10") or "AutoCAD Binary DXF" in header_text:
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
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
|
) from exc
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="업로드한 파일이 DXF 형식으로 읽히지 않습니다. DWG 파일이거나 확장자만 dxf로 바뀐 파일일 수 있습니다.",
|
detail=f"업로드한 파일 형식을 판별하지 못했습니다. 확장자만 dxf인 파일일 수 있습니다. 헤더={preview}",
|
||||||
) from exc
|
) from exc
|
||||||
modelspace = document.modelspace()
|
modelspace = document.modelspace()
|
||||||
all_entities = [entity for entity in modelspace if entity.dxftype() in {"LINE", "LWPOLYLINE", "POLYLINE", "CIRCLE", "ARC", "POINT", "INSERT"}]
|
base_entities = [entity for entity in modelspace if entity.dxftype() in {"LINE", "LWPOLYLINE", "POLYLINE", "CIRCLE", "ARC", "INSERT", "SPLINE", "ELLIPSE"}]
|
||||||
points: list[tuple[float, float]] = []
|
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_entities: list[ezdxf.entities.DXFGraphic] = []
|
||||||
|
chair_points: list[tuple[float, float]] = []
|
||||||
for entity in all_entities:
|
for entity in all_entities:
|
||||||
entity_points = get_entity_points(entity)
|
if is_chair_layer(entity.dxf.layer):
|
||||||
if entity_points:
|
|
||||||
points.extend(entity_points)
|
|
||||||
if entity.dxf.layer.lower() == "chair":
|
|
||||||
chair_entities.append(entity)
|
chair_entities.append(entity)
|
||||||
|
chair_points.extend(get_entity_points(entity))
|
||||||
|
|
||||||
if not chair_entities:
|
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 좌표를 해석하지 못했습니다.")
|
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]] = []
|
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]))):
|
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)
|
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:
|
if not slots:
|
||||||
raise HTTPException(status_code=400, detail="chair 레이어에서 좌석 위치를 추출하지 못했습니다.")
|
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 = {
|
metadata = {
|
||||||
"source_type": "dxf",
|
"source_type": "dxf",
|
||||||
"view_box_min_x": round(min_x, 3),
|
"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),
|
"view_box_height": round(height, 3),
|
||||||
"preview_svg": preview_svg,
|
"preview_svg": preview_svg,
|
||||||
"grid_rows": 1,
|
"grid_rows": 1,
|
||||||
"grid_cols": max(len(slots), 1),
|
"grid_cols": 1,
|
||||||
"image_width": None,
|
"image_width": None,
|
||||||
"image_height": None,
|
"image_height": None,
|
||||||
"cell_gap": 0,
|
"cell_gap": 0,
|
||||||
@@ -581,10 +727,10 @@ def replace_members(items: list[MemberPayload]) -> list[dict[str, object]]:
|
|||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO members (
|
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
|
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),
|
serialize_member_payload(item, index),
|
||||||
)
|
)
|
||||||
@@ -702,11 +848,11 @@ def create_member(payload: MemberPayload) -> dict[str, object]:
|
|||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO members (
|
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
|
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)
|
||||||
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,
|
work_status, work_time, phone, email, seat_label, photo_url,
|
||||||
sort_order, created_at, updated_at
|
sort_order, created_at, updated_at
|
||||||
""",
|
""",
|
||||||
@@ -730,6 +876,7 @@ def update_member(member_id: int, payload: MemberPayload) -> dict[str, object]:
|
|||||||
"""
|
"""
|
||||||
UPDATE members
|
UPDATE members
|
||||||
SET name = %s,
|
SET name = %s,
|
||||||
|
employee_id = %s,
|
||||||
company = %s,
|
company = %s,
|
||||||
rank = %s,
|
rank = %s,
|
||||||
role = %s,
|
role = %s,
|
||||||
@@ -747,7 +894,7 @@ def update_member(member_id: int, payload: MemberPayload) -> dict[str, object]:
|
|||||||
sort_order = COALESCE(%s, sort_order),
|
sort_order = COALESCE(%s, sort_order),
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = %s
|
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,
|
work_status, work_time, phone, email, seat_label, photo_url,
|
||||||
sort_order, created_at, updated_at
|
sort_order, created_at, updated_at
|
||||||
""",
|
""",
|
||||||
@@ -821,8 +968,6 @@ async def create_dxf_seat_map(file: UploadFile = File(...), name: str = Form(...
|
|||||||
try:
|
try:
|
||||||
metadata, slots = parse_dxf_layout(target)
|
metadata, slots = parse_dxf_layout(target)
|
||||||
except Exception:
|
except Exception:
|
||||||
if target.exists():
|
|
||||||
target.unlink(missing_ok=True)
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
payload = SeatMapPayload(
|
payload = SeatMapPayload(
|
||||||
@@ -838,7 +983,7 @@ async def create_dxf_seat_map(file: UploadFile = File(...), name: str = Form(...
|
|||||||
image_width=None,
|
image_width=None,
|
||||||
image_height=None,
|
image_height=None,
|
||||||
grid_rows=1,
|
grid_rows=1,
|
||||||
grid_cols=max(len(slots), 1),
|
grid_cols=1,
|
||||||
cell_gap=0,
|
cell_gap=0,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ function toLegacyMember(item) {
|
|||||||
_id: String(item.id),
|
_id: String(item.id),
|
||||||
id: item.id,
|
id: item.id,
|
||||||
이름: item.name || '',
|
이름: item.name || '',
|
||||||
|
사번: item.employee_id || '',
|
||||||
소속회사: item.company || '',
|
소속회사: item.company || '',
|
||||||
직급: item.rank || '',
|
직급: item.rank || '',
|
||||||
직책: item.role || '',
|
직책: item.role || '',
|
||||||
@@ -57,6 +58,7 @@ function toLegacyMember(item) {
|
|||||||
function toApiMember(member, sortOrder) {
|
function toApiMember(member, sortOrder) {
|
||||||
return {
|
return {
|
||||||
name: member['이름'] || '',
|
name: member['이름'] || '',
|
||||||
|
employee_id: member['사번'] || '',
|
||||||
company: member['소속회사'] || '',
|
company: member['소속회사'] || '',
|
||||||
rank: member['직급'] || '',
|
rank: member['직급'] || '',
|
||||||
role: member['직책'] || '',
|
role: member['직책'] || '',
|
||||||
@@ -381,6 +383,12 @@ function updateStatsTable() {
|
|||||||
|
|
||||||
const columns = Object.keys(rankGroups);
|
const columns = Object.keys(rankGroups);
|
||||||
const stats = {};
|
const stats = {};
|
||||||
|
const companyLabelHtml = (company) => `
|
||||||
|
<span class="stats-company-label">
|
||||||
|
<span class="stats-company-dot co-${company}"></span>
|
||||||
|
<span>${company}</span>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
companies.forEach((company) => {
|
companies.forEach((company) => {
|
||||||
stats[company] = {};
|
stats[company] = {};
|
||||||
@@ -418,7 +426,7 @@ function updateStatsTable() {
|
|||||||
let grandTotal = 0;
|
let grandTotal = 0;
|
||||||
|
|
||||||
companies.forEach((company) => {
|
companies.forEach((company) => {
|
||||||
html += `<tr><td class="row-label">${company}</td>${columns.map((column) => {
|
html += `<tr><td class="row-label">${companyLabelHtml(company)}</td>${columns.map((column) => {
|
||||||
colSums[column] += stats[company][column];
|
colSums[column] += stats[company][column];
|
||||||
return `<td>${stats[company][column] || '-'}</td>`;
|
return `<td>${stats[company][column] || '-'}</td>`;
|
||||||
}).join('')}<td class="total-cell">${stats[company]._total}</td></tr>`;
|
}).join('')}<td class="total-cell">${stats[company]._total}</td></tr>`;
|
||||||
@@ -515,6 +523,7 @@ function updateFabMenu() {
|
|||||||
let html = '<button class="fab-sub shadow-xl" data-label="리스트" onclick="openListViewModal(event)">📋</button>';
|
let html = '<button class="fab-sub shadow-xl" data-label="리스트" onclick="openListViewModal(event)">📋</button>';
|
||||||
html += '<button class="fab-sub shadow-xl" data-label="조직도 인쇄(A3)" onclick="printA3()">🖨️</button>';
|
html += '<button class="fab-sub shadow-xl" data-label="조직도 인쇄(A3)" onclick="printA3()">🖨️</button>';
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
|
html += '<button class="fab-sub shadow-xl" data-label="자리배치도" onclick="openSeatMapView(event)">🪑</button>';
|
||||||
html += '<button class="fab-sub shadow-xl" data-label="조직현황 업로드" onclick="triggerUpload(event)">⬆️</button>';
|
html += '<button class="fab-sub shadow-xl" data-label="조직현황 업로드" onclick="triggerUpload(event)">⬆️</button>';
|
||||||
html += '<button class="fab-sub shadow-xl" data-label="신규 구성원" onclick="openAddModal(event)">👤</button>';
|
html += '<button class="fab-sub shadow-xl" data-label="신규 구성원" onclick="openAddModal(event)">👤</button>';
|
||||||
html += '<button class="fab-sub shadow-xl" data-label="신규 팀/그룹/셀" onclick="openUnitAddModal(event)">🏢</button>';
|
html += '<button class="fab-sub shadow-xl" data-label="신규 팀/그룹/셀" onclick="openUnitAddModal(event)">🏢</button>';
|
||||||
@@ -522,6 +531,14 @@ function updateFabMenu() {
|
|||||||
menu.innerHTML = html;
|
menu.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openSeatMapView(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
document.getElementById('fab-container').classList.remove('active');
|
||||||
|
if (window.parent && window.parent !== window) {
|
||||||
|
window.parent.postMessage({ type: 'open-seatmap' }, '*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function triggerUpload(event) {
|
function triggerUpload(event) {
|
||||||
if (event) {
|
if (event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -796,6 +813,10 @@ function openModal(id) {
|
|||||||
<p class="text-slate-400 text-xs mt-1 font-medium">${(member._path || []).map((path) => path.name).join(' > ')}</p>
|
<p class="text-slate-400 text-xs mt-1 font-medium">${(member._path || []).map((path) => path.name).join(' > ')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full grid grid-cols-2 gap-3 mt-4">
|
<div class="w-full grid grid-cols-2 gap-3 mt-4">
|
||||||
|
<div class="bg-slate-50 p-4 rounded-2xl border border-slate-100 col-span-2">
|
||||||
|
<label class="text-[10px] text-slate-400 font-bold block mb-1">사번</label>
|
||||||
|
<span class="text-sm font-black text-slate-700">${member['사번'] || '정보 없음'}</span>
|
||||||
|
</div>
|
||||||
<div class="bg-indigo-50 p-4 rounded-2xl border border-indigo-100 col-span-2 flex items-center gap-4">
|
<div class="bg-indigo-50 p-4 rounded-2xl border border-indigo-100 col-span-2 flex items-center gap-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label class="text-[10px] text-indigo-400 font-bold block mb-1">연락처</label>
|
<label class="text-[10px] text-indigo-400 font-bold block mb-1">연락처</label>
|
||||||
@@ -872,6 +893,7 @@ function openModal(id) {
|
|||||||
<div id="modal-sec-basic" class="grid grid-cols-2 gap-4">
|
<div id="modal-sec-basic" class="grid grid-cols-2 gap-4">
|
||||||
<input type="hidden" id="m-id" value="${id || ''}">
|
<input type="hidden" id="m-id" value="${id || ''}">
|
||||||
<div class="col-span-2"><label class="text-[11px] font-black text-slate-600 block">이름 (필수)</label><input id="m-name" value="${member['이름'] || ''}" class="w-full bg-slate-50 p-3 rounded-xl border font-bold text-sm outline-none"></div>
|
<div class="col-span-2"><label class="text-[11px] font-black text-slate-600 block">이름 (필수)</label><input id="m-name" value="${member['이름'] || ''}" class="w-full bg-slate-50 p-3 rounded-xl border font-bold text-sm outline-none"></div>
|
||||||
|
<div class="col-span-2"><label class="text-[11px] font-black text-slate-600 block">사번</label><input id="m-employee-id" value="${member['사번'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none"></div>
|
||||||
<div class="col-span-1"><label class="text-[11px] font-black text-slate-600 block">전화번호</label><input id="m-phone" value="${member['전화번호'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none"></div>
|
<div class="col-span-1"><label class="text-[11px] font-black text-slate-600 block">전화번호</label><input id="m-phone" value="${member['전화번호'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none"></div>
|
||||||
<div class="col-span-1"><label class="text-[11px] font-black text-slate-600 block">이메일</label><input id="m-email" value="${member['이메일'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none"></div>
|
<div class="col-span-1"><label class="text-[11px] font-black text-slate-600 block">이메일</label><input id="m-email" value="${member['이메일'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none"></div>
|
||||||
<div class="col-span-2"><label class="text-[11px] font-black text-slate-600 block">자리 위치</label><input id="m-seat" value="${member['자리위치'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none"></div>
|
<div class="col-span-2"><label class="text-[11px] font-black text-slate-600 block">자리 위치</label><input id="m-seat" value="${member['자리위치'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none"></div>
|
||||||
@@ -912,6 +934,7 @@ async function saveMember() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
member['이름'] = name;
|
member['이름'] = name;
|
||||||
|
member['사번'] = document.getElementById('m-employee-id').value.trim();
|
||||||
dropdownFields.forEach((field) => {
|
dropdownFields.forEach((field) => {
|
||||||
const selectValue = document.getElementById(`sel-${field}`).value;
|
const selectValue = document.getElementById(`sel-${field}`).value;
|
||||||
if (selectValue === '__NEW__') {
|
if (selectValue === '__NEW__') {
|
||||||
|
|||||||
Reference in New Issue
Block a user