feat: add member history list snapshot and compare views
This commit is contained in:
@@ -620,9 +620,9 @@ def sync_seat_assignment_versions(cur, member_ids: list[int], change_reason: str
|
||||
def fetch_members_as_of(cur, as_of: datetime) -> list[dict[str, object]]:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.id,
|
||||
SELECT mv.member_id AS id,
|
||||
mv.name,
|
||||
m.employee_id,
|
||||
COALESCE(m.employee_id, '') AS employee_id,
|
||||
mv.company,
|
||||
mv.rank,
|
||||
mv.role,
|
||||
@@ -637,25 +637,137 @@ def fetch_members_as_of(cur, as_of: datetime) -> list[dict[str, object]]:
|
||||
mv.email,
|
||||
COALESCE(sav.seat_label, '') AS seat_label,
|
||||
mv.photo_url,
|
||||
m.sort_order,
|
||||
COALESCE(m.sort_order, 2147483647) AS sort_order,
|
||||
mv.created_at,
|
||||
mv.valid_from AS updated_at
|
||||
FROM members m
|
||||
JOIN member_versions mv
|
||||
ON mv.member_id = m.id
|
||||
AND mv.valid_from <= %s
|
||||
AND (mv.valid_to IS NULL OR mv.valid_to > %s)
|
||||
FROM member_versions mv
|
||||
LEFT JOIN members m
|
||||
ON m.id = mv.member_id
|
||||
LEFT JOIN seat_assignment_versions sav
|
||||
ON sav.member_id = m.id
|
||||
ON sav.member_id = mv.member_id
|
||||
AND sav.valid_from <= %s
|
||||
AND (sav.valid_to IS NULL OR sav.valid_to > %s)
|
||||
ORDER BY m.sort_order ASC, m.id ASC
|
||||
WHERE mv.valid_from <= %s
|
||||
AND (mv.valid_to IS NULL OR mv.valid_to > %s)
|
||||
ORDER BY COALESCE(m.sort_order, 2147483647) ASC, mv.member_id ASC
|
||||
""",
|
||||
(as_of, as_of, as_of, as_of),
|
||||
)
|
||||
return cur.fetchall()
|
||||
|
||||
|
||||
def build_member_compare_items(from_items: list[dict[str, object]], to_items: list[dict[str, object]]) -> list[dict[str, object]]:
|
||||
tracked_fields = (
|
||||
("company", "소속회사", "기본"),
|
||||
("rank", "직급", "기본"),
|
||||
("role", "직책", "기본"),
|
||||
("department", "부서", "조직"),
|
||||
("grp", "그룹", "조직"),
|
||||
("division", "디비전", "조직"),
|
||||
("team", "팀", "조직"),
|
||||
("cell", "셀", "조직"),
|
||||
("work_status", "근무상태", "기본"),
|
||||
("work_time", "근무시간", "기본"),
|
||||
("phone", "전화번호", "기본"),
|
||||
("email", "이메일", "기본"),
|
||||
)
|
||||
|
||||
def build_summary(item: dict[str, object] | None) -> list[str]:
|
||||
if not item:
|
||||
return []
|
||||
summary_fields = (
|
||||
("rank", "직급"),
|
||||
("role", "직책"),
|
||||
("department", "부서"),
|
||||
("grp", "그룹"),
|
||||
("division", "디비전"),
|
||||
("team", "팀"),
|
||||
("cell", "셀"),
|
||||
)
|
||||
lines: list[str] = []
|
||||
for field, label in summary_fields:
|
||||
value = str(item.get(field) or "").strip()
|
||||
if value:
|
||||
lines.append(f"{label}: {value}")
|
||||
return lines
|
||||
|
||||
from_map = {int(item["id"]): item for item in from_items}
|
||||
to_map = {int(item["id"]): item for item in to_items}
|
||||
all_ids = sorted(set(from_map) | set(to_map))
|
||||
items: list[dict[str, object]] = []
|
||||
|
||||
for member_id in all_ids:
|
||||
before = from_map.get(member_id)
|
||||
after = to_map.get(member_id)
|
||||
if before is None and after is not None:
|
||||
items.append(
|
||||
{
|
||||
"member_id": member_id,
|
||||
"name": str(after.get("name") or "-"),
|
||||
"status": "added",
|
||||
"status_label": "신규",
|
||||
"categories": ["신규"],
|
||||
"changes": [],
|
||||
"before_lines": [],
|
||||
"after_lines": build_summary(after),
|
||||
}
|
||||
)
|
||||
continue
|
||||
if before is not None and after is None:
|
||||
items.append(
|
||||
{
|
||||
"member_id": member_id,
|
||||
"name": str(before.get("name") or "-"),
|
||||
"status": "removed",
|
||||
"status_label": "삭제",
|
||||
"categories": ["삭제"],
|
||||
"changes": [],
|
||||
"before_lines": build_summary(before),
|
||||
"after_lines": [],
|
||||
}
|
||||
)
|
||||
continue
|
||||
if before is None or after is None:
|
||||
continue
|
||||
|
||||
changes: list[dict[str, str]] = []
|
||||
categories: set[str] = set()
|
||||
for field, label, category in tracked_fields:
|
||||
before_value = str(before.get(field) or "").strip()
|
||||
after_value = str(after.get(field) or "").strip()
|
||||
if before_value == after_value:
|
||||
continue
|
||||
changes.append(
|
||||
{
|
||||
"field": field,
|
||||
"label": label,
|
||||
"before": before_value,
|
||||
"after": after_value,
|
||||
}
|
||||
)
|
||||
categories.add(category)
|
||||
|
||||
if not changes:
|
||||
continue
|
||||
|
||||
items.append(
|
||||
{
|
||||
"member_id": member_id,
|
||||
"name": str(after.get("name") or before.get("name") or "-"),
|
||||
"status": "updated",
|
||||
"status_label": "변경",
|
||||
"categories": sorted(categories),
|
||||
"changes": changes,
|
||||
"before_lines": [f"{change['label']}: {change['before'] or '-'}" for change in changes],
|
||||
"after_lines": [f"{change['label']}: {change['after'] or '-'}" for change in changes],
|
||||
}
|
||||
)
|
||||
|
||||
order_map = {"added": 0, "updated": 1, "removed": 2}
|
||||
items.sort(key=lambda item: (order_map.get(str(item.get("status") or ""), 9), str(item.get("name") or ""), int(item.get("member_id") or 0)))
|
||||
return items
|
||||
|
||||
|
||||
def serialize_seat_map_payload(payload: SeatMapPayload) -> tuple[object, ...]:
|
||||
return (
|
||||
payload.name.strip(),
|
||||
@@ -3959,6 +4071,19 @@ def list_members(as_of: str | None = None) -> dict[str, list[dict[str, object]]]
|
||||
return {"items": fetch_members_as_of(cur, parsed_as_of)}
|
||||
|
||||
|
||||
@app.get("/api/history/members/compare")
|
||||
def compare_members_history(from_date: str, to_date: str) -> dict[str, list[dict[str, object]]]:
|
||||
parsed_from = parse_as_of(from_date)
|
||||
parsed_to = parse_as_of(to_date)
|
||||
if parsed_from is None or parsed_to is None:
|
||||
raise HTTPException(status_code=400, detail="from_date and to_date are required.")
|
||||
with get_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
from_items = fetch_members_as_of(cur, parsed_from)
|
||||
to_items = fetch_members_as_of(cur, parsed_to)
|
||||
return {"items": build_member_compare_items(from_items, to_items)}
|
||||
|
||||
|
||||
@app.post("/api/members")
|
||||
def create_member(payload: MemberPayload) -> dict[str, object]:
|
||||
with get_conn() as conn:
|
||||
|
||||
Reference in New Issue
Block a user