From 8ab522379004b1ec285e5523a2cd989adbe75fad Mon Sep 17 00:00:00 2001 From: king Date: Sat, 30 May 2026 08:50:02 +0900 Subject: [PATCH] =?UTF-8?q?feat(vacation):=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=E2=80=94=20=EC=8A=B9=EC=9D=B8=EC=9D=80=20=EC=B7=A8=EC=86=8C?= =?UTF-8?q?=EC=B2=98=EB=A6=AC,=20=EB=AF=B8=EC=8A=B9=EC=9D=B8(=EC=9E=91?= =?UTF-8?q?=EC=84=B1=EC=A4=91/=EC=A0=9C=EC=B6=9C/=EB=B0=98=EB=A0=A4)?= =?UTF-8?q?=EC=9D=80=20=EC=82=AD=EC=A0=9C(hard)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- app/modules/vacation/db.py | 17 +++++++++++++ app/modules/vacation/router.py | 25 +++++++++++++++++++ .../vacation/templates/vacation/detail.html | 19 +++++++++----- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/app/modules/vacation/db.py b/app/modules/vacation/db.py index b2c92b8..cdd7eca 100644 --- a/app/modules/vacation/db.py +++ b/app/modules/vacation/db.py @@ -351,6 +351,23 @@ class VacationDBStore: ).fetchone() return self._req_serialize(row) + def hard_delete(self, *, request_id: str, user_email: str, is_admin: bool) -> None: + """완전 삭제. 승인 전(작성중/제출/반려)만 허용. owner 또는 admin.""" + user_email = user_email.lower().strip() + current = self.get_request(request_id=request_id) + if not current: + raise KeyError(request_id) + if not is_admin and current["owner"] != user_email: + raise PermissionError("본인 신청만 삭제할 수 있습니다.") + if current["status"] == "승인": + raise ValueError("승인된 휴가는 삭제할 수 없습니다. 취소 처리하세요.") + with self._pool.connection() as conn: + cur = conn.execute( + "DELETE FROM vacation_requests WHERE id = %s", (request_id,) + ) + if cur.rowcount == 0: + raise KeyError(request_id) + def list_pending(self) -> list[dict[str, Any]]: with self._pool.connection() as conn: rows = conn.execute( diff --git a/app/modules/vacation/router.py b/app/modules/vacation/router.py index a4601a3..c20fda1 100644 --- a/app/modules/vacation/router.py +++ b/app/modules/vacation/router.py @@ -800,3 +800,28 @@ async def cancel( except PermissionError as exc: raise HTTPException(status_code=403, detail=str(exc)) return RedirectResponse(url=f"/vacation/{request_id}", status_code=303) + + +@router.post("/{request_id}/delete") +async def delete( + request: Request, + request_id: str, + user: dict[str, Any] = Depends(_require_user), +) -> RedirectResponse: + """완전 삭제 — 승인 전(작성중/제출/반려)만. 달력으로 복귀.""" + from app.store import is_admin # noqa: WPS433 + + store = _store(request) + if store is None: + raise HTTPException(status_code=503, detail="vacation_db 미설정") + try: + store.hard_delete( + request_id=request_id, user_email=user["email"], is_admin=is_admin(user) + ) + except KeyError: + raise HTTPException(status_code=404, detail="휴가 신청을 찾을 수 없습니다.") + except PermissionError as exc: + raise HTTPException(status_code=403, detail=str(exc)) + except ValueError as exc: + raise HTTPException(status_code=409, detail=str(exc)) + return RedirectResponse(url="/vacation/", status_code=303) diff --git a/app/modules/vacation/templates/vacation/detail.html b/app/modules/vacation/templates/vacation/detail.html index 0c826c6..89c0b40 100644 --- a/app/modules/vacation/templates/vacation/detail.html +++ b/app/modules/vacation/templates/vacation/detail.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
@@ -65,11 +65,18 @@ {% endif %} - {% if req.status != '취소' and (is_owner or is_admin) %} -
- -
+ {% if (is_owner or is_admin) %} + {% if req.status == '승인' %} +
+ +
+ {% elif req.status in ('작성중', '제출', '반려') %} +
+ +
+ {% endif %} {% endif %} {% if can_edit %}