- 대시보드 지움 = 늘 휴지통(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>
75 lines
3.1 KiB
Python
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
|