Files
Aislo/B01_Dashboard/B01_Dashboard_Repository_Trash.py
T
eomsangdonandClaude Opus 5.5 ac481a0764 feat(B01): 프로젝트 휴지통 — 지움 = 휴지통 · 7 일 뒤 영구 삭제 · 되살리기 (PLAN 592-1 · 요구 454 · 455)
- 대시보드 지움 = 늘 휴지통(deleted_at) · PROJECT_DELETE_HARD_ENABLED 스위치 걷음 → TRASH_RETENTION_DAYS(7)
- 영구 삭제(hard_delete_project) = 휴지통 것만 · FOR UPDATE · 이미 지운 것 False 로 건너뜀 · 감사 기록 동작 이름 받음
- 정리 루프에 purge_expired_trash — GET_LOCK 이름 잠금(두 PC 서버 중 하나만) · 기한 지난 것 PROJECT_PURGE
- API /api/dashboard/trash/projects — 보기 · 되살리기 = 시스템 관리자(전부) · 회사 관리자(자기 회사) · 영구 삭제 = 시스템 관리자만
- 화면 휴지통 — 시스템 관리 › 휴지통 · 회사 관리 › 휴지통(회사 관리자 · 되살리기만) · 지운 사람 = 감사 기록
- 시험 test_592_1_trash 새로 · test_546 메뉴 기대값에 휴지통 · test_573 복제본 지움을 휴지통 거쳐 영구 삭제로(영구 삭제 = 휴지통 것만)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 08:31:11 +09:00

75 lines
3.1 KiB
Python

"""B01_Dashboard 프로젝트 휴지통(592) — 목록 · 되살리기 · 영구 삭제는 common_util_project_delete."""
from typing import Any
import aiomysql
from common_util.common_util_audit import record_audit
from common_util.common_util_project_delete import DELETED_BY_SQL
from config.config_db import get_db_pool
from config.config_system import TRASH_RETENTION_DAYS
def days_left(seconds_left: int | None) -> int:
"""영구 삭제까지 남은 날 — 하루 덜 찼어도 하루로(올림) · 지났으면 0."""
if seconds_left is None or seconds_left <= 0:
return 0
return -(-int(seconds_left) // 86400)
async def list_trash_projects(company_id: int | None = None) -> list[dict[str, Any]]:
"""휴지통 — 최근 지운 것부터 · company_id 면 그 회사 것만."""
where = " AND p.company_id = %s" if company_id is not None else ""
pool = get_db_pool()
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(
f"""SELECT t.*, du.name AS deleted_by_name, du.email AS deleted_by_email
FROM (SELECT p.id, p.name, p.project_kind, p.company_id, c.name AS company_name,
p.deleted_at, {DELETED_BY_SQL} AS deleted_by,
TIMESTAMPDIFF(SECOND, CURRENT_TIMESTAMP,
p.deleted_at + INTERVAL %s DAY) AS seconds_left
FROM projects p LEFT JOIN companies c ON c.id = p.company_id
WHERE p.deleted_at IS NOT NULL{where}) t
LEFT JOIN users du ON du.id = t.deleted_by
ORDER BY t.deleted_at DESC""",
(TRASH_RETENTION_DAYS, company_id)
if company_id is not None
else (TRASH_RETENTION_DAYS,),
)
rows = list(await cursor.fetchall())
for row in rows:
row["days_left"] = days_left(row.pop("seconds_left"))
return rows
async def get_trash_project(project_id: str) -> dict[str, Any] | None:
pool = get_db_pool()
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(
"SELECT id, company_id, name FROM projects WHERE id = %s AND deleted_at IS NOT NULL",
(project_id,),
)
return await cursor.fetchone()
async def restore_project(project_id: str, actor_id: int, request: Any | None = None) -> bool:
pool = get_db_pool()
async with pool.acquire() as connection, connection.cursor() as cursor:
await connection.begin()
await cursor.execute(
"UPDATE projects SET deleted_at = NULL WHERE id = %s AND deleted_at IS NOT NULL",
(project_id,),
)
changed = cursor.rowcount > 0
if changed:
await record_audit(
cursor,
actor_id=actor_id,
action="PROJECT_RESTORE",
resource_type="project",
resource_ref=project_id,
request=request,
)
await connection.commit()
return changed