Files
Aislo/docs/raw/plans/2026-08-08_plan_project_delete_hard.md
T
eomsangdonandClaude Opus 5 eb30b774f8 chore(docs): docs 폴더 git 추적 전환 · PLAN·OWNERS 최상위 이관
- `docs/` 를 .gitignore 에서 빼 git 추적 대상으로 전환 (위키·완료 이력·검증 기록)
- `docs/raw/PLAN.md` · `docs/raw/OWNERS.md` 를 저장소 최상위로 이동 후 .gitignore 에 등록
  — 창끼리 공유하되 저장소에는 안 올리는 장부
- 살아 있는 경로 참조 7개 파일 정정 (아카이브 107건은 그때 사실이라 그대로 둠)
- graphify 날짜별 산출물(`docs/wiki/graphify-out/20*/`) 제외 — `graphify update` 가 다시 만듦

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 18:19:07 +09:00

8.3 KiB

완료 계획서 — B01 프로젝트 삭제: 개발용 하드 삭제 스위치

  • 일자: 2026-08-08
  • 상태: 완료 (검증 완료)

배경

대시보드 프로젝트 삭제 버튼은 현재 소프트 삭제만 한다 — projects.deleted_at에 시각을 찍고 system_audit_logsPROJECT_DELETE 한 줄을 넣는 것이 전부다(B01_Dashboard_Repository.py:255). 파일시스템과 자식 테이블은 손대지 않는다. 목록 쿼리가 deleted_at IS NULL로 걸러서 화면에서만 사라진다.

배포 환경에서는 이게 맞다 — 사용자가 올린 라이다 원본은 나중에 다른 프로젝트 개발에 재활용할 수 있는 자산이다. 그러나 지금은 개발 단계라 프로젝트를 반복 생성·삭제하는데, 삭제할 때마다 수십 GB 라이다 원본이 디스크에 영구 누적된다. 하드 삭제 잡도 없다.

사용자 지시(2026-08-08): config_system.py에 true/false 스위치를 두고, 배포 시 false(현행 소프트 삭제 유지), 개발 중 true(DB + 영구저장소 프로젝트 폴더 자체 삭제)로 운용한다.

설계 결정

1. 설정 변수config/config_system.py 맨 위 0. 위험 스위치 전용 절에 배치. load_dotenv() 직후, 기존 1절(FastAPI 서버) 앞이다.

PROJECT_DELETE_HARD_ENABLED = os.getenv("PROJECT_DELETE_HARD_ENABLED", "False").lower() == "true"

기본값 False — 환경변수를 안 넣은 배포 환경이 자동으로 안전한 쪽에 선다. 개발 PC는 .envPROJECT_DELETE_HARD_ENABLED=True.

2. 삭제 대상storage/{회사}/{사용자}/{project_id}/ 폴더 자체를 shutil.rmtree로 제거(B03_FileInput 원본 포함). 기존 purge_output_folders()는 B04~B09만 비우고 B03을 남기므로 재사용하지 않는다.

3. DB는 FK CASCADE에 맡긴다db_management/001_create_schema.sql:505~586 확인 결과 projects.id를 참조하는 자식 테이블 전부가 ON DELETE CASCADE다: project_versions, input_files, processed_point_cloud, surface_models, routes, longitudinal_sections, cross_sections, structures, quantity_items, outputs, change_logs, upload_sessions(010_temp_upload.sql:68), project_workflow_stages(006_workflow_state.sql:22). route_points·route_statisticsroutes 경유로 연쇄(001:538,543). 따라서 DELETE FROM projects WHERE id = %s 한 줄이면 충분하다 — 테이블 목록을 코드에 다시 나열하지 않는다(스키마 변경 시 어긋남 방지).

4. 남기는 것 2가지

  • system_audit_logs — 감사 기록은 삭제 후에도 남겨야 한다. resource_id가 현재 NULL로만 들어가고 FK도 없어 고아 참조 문제 없음. actionPROJECT_HARD_DELETE로 구분해 기록.
  • temp_batches.linked_project_id — FK 없는 이력 보존용 컬럼. 설계 의도대로 그대로 둔다.

5. 실행 순서 — DB 먼저, 파일 나중

  1. storage_path 조회 (삭제 전에 읽어 둬야 함)
  2. DELETE FROM projects + 감사 로그 INSERT → 커밋
  3. 커밋 성공 후 rmtree DB가 먼저 지워지면 rmtree 실패 시 고아 폴더(디스크 낭비)만 남고 시스템은 정합하다. 반대 순서면 파일 없는 DB 행이 남아 화면이 깨진다. rmtree 실패는 logger.error로 경로를 남겨 수동 정리 가능하게 한다 — 삭제 API 자체는 성공으로 응답한다.

6. 경로 안전장치 (필수)resolve_stored_project_path()는 내부에서 os.makedirs + ensure_project_storage_layout()을 호출해 폴더를 만든다. 삭제 경로에 그대로 쓰면 지우기 직전에 폴더를 되살린다. 따라서 생성하지 않는 별도 해석 함수를 추가한다.

common_util_storage.py에 신규 함수:

resolve_project_root_for_delete(relative_path: str) -> str

검증 조건 (하나라도 어기면 ValueError, 삭제 중단):

  • 상대 경로이고 .. 미포함, storage/로 시작 (기존 resolve_stored_project_path 앞부분과 동일)
  • 정규화 후 STORAGE_BASE_DIR 하위이고 루트 자신이 아님
  • 세그먼트가 정확히 storage/{회사}/{사용자}/{project_id} 4개 — 상위 폴더 통삭제 방지
  • 마지막 세그먼트가 URL의 project_id와 문자열 일치 — DB storage_path가 오염돼도 남의 폴더를 못 지운다
  • os.makedirs 호출하지 않음

7. 빈 상위 폴더 — 프로젝트 삭제 후 비게 된 {사용자}/, {회사}/ 폴더는 정리하지 않는다. 동시 생성과 경합할 수 있고 실익이 없다.

8. 권한 불변SYSTEM_ADMIN만 삭제 가능한 현행 유지. 주석 처리된 ADMIN 분기도 그대로 둔다.

9. 화면 경고 — 하드 삭제 모드일 때 삭제 확인 모달 문구를 다르게 보여준다. 개발 중 실수로 원본을 날리는 사고를 막는 최소 장치다. GET /api/dashboard/me 응답에 project_delete_hard: bool을 실어 프론트가 판단한다.

대상 파일

파일 변경
config/config_system.py PROJECT_DELETE_HARD_ENABLED 추가 (4절, AUTO_DESIGN_CHAIN_ENABLED 아래)
common_util/common_util_storage.py resolve_project_root_for_delete() 신규
B01_Dashboard/B01_Dashboard_Repository.py hard_delete_project() 신규. 기존 soft_delete_project()는 그대로
B01_Dashboard/B01_Dashboard_Router.py dashboard_delete_project()에서 플래그 분기. /me 응답에 플래그 노출
B01_Dashboard/B01_Dashboard_UI_Modals.ts openDeleteProjectModal() 경고 문구 분기
ui_template/ 다국어 B01_Dashboard_Confirm_DeleteProject_Hard 키 추가

B01_Dashboard_Repository.py는 현재 790줄로 이미 700줄 제한 초과 상태(기존 기술부채). 이번에 함수를 더하면 악화되므로, 삭제 로직은 저장소 파일이 아니라 common_util/common_util_project_delete.py 신규 파일에 둔다. 저장소 파일에는 손대지 않는다.

구현 체크리스트

  • config_system.py 최상단 0. 위험 스위치 절에 PROJECT_DELETE_HARD_ENABLED 추가 (기본 False, 주석에 배포/개발 운용 방침 명시)
  • common_util_storage.py:77resolve_project_root_for_delete() 추가 — 4세그먼트 검증 + project_id 일치 + 폴더 생성 없음
  • common_util/common_util_project_delete.py 신규 — hard_delete_project(project_id, actor_id) : storage_path 조회 → 경로 검증 → DELETE FROM projects + PROJECT_HARD_DELETE 감사 로그 → 커밋 → rmtree(실패 시 error 로그만)
  • B01_Dashboard_Router.py:237 분기 — 플래그 True면 hard_delete_project(), False면 기존 soft_delete_project()
  • /api/dashboard/me 응답 userproject_delete_hard 필드 추가
  • B01_Dashboard_UI_Modals.ts:121 — 하드 삭제 모드일 때 원본까지 지운다는 경고 문구로 교체. openDeleteProjectModal(user, project)로 시그니처 변경, 호출부 B01_Dashboard_UI_Projects.ts:39 수정
  • 다국어 키 B01_Dashboard_Confirm_DeleteProject_Hard 추가 (ui_template_locale_b1.ts:174)
  • ruff format + ruff check + prettier + tsc --noEmit 전부 통과
  • .envPROJECT_DELETE_HARD_ENABLED=True 기재 — .env.example은 이 저장소에 없다

완료 조건

  • 플래그 False: 삭제 동작이 지금과 100% 동일 (deleted_at UPDATE + 감사 로그)
  • 플래그 True: projects 행과 자식 테이블 전부 소멸, storage/.../{project_id}/ 폴더 부재, system_audit_logsPROJECT_HARD_DELETE 잔존
  • 조작된 storage_path(.. 포함, 3세그먼트, project_id 불일치)로는 ValueError 발생해 아무것도 지워지지 않음

검증자 인계 사항

경로 가드 8케이스 수동 확인 완료 (정상 허용 1 / 차단 7: 3세그먼트, 저장소 루트, .. 탈출, ID 불일치, 절대경로, 5세그먼트, 역슬래시 탈출). 실제 프로젝트 생성→삭제 E2E는 미수행.

미결 리스크: 이 저장소는 .env를 git으로 추적한다(.gitignore:12.env 줄이 주석 처리됨). 따라서 PROJECT_DELETE_HARD_ENABLED=True가 커밋되면 다른 PC와 배포 환경까지 하드 삭제가 켜진 채 전파된다. 배포 전에 .env를 추적 해제(git rm --cached .env + .gitignore 주석 해제)하거나, 최소한 배포 브랜치에서 False로 되돌려야 한다.