- `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>
8.3 KiB
완료 계획서 — B01 프로젝트 삭제: 개발용 하드 삭제 스위치
- 일자: 2026-08-08
- 상태: 완료 (검증 완료)
배경
대시보드 프로젝트 삭제 버튼은 현재 소프트 삭제만 한다 — projects.deleted_at에 시각을 찍고 system_audit_logs에 PROJECT_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는 .env에 PROJECT_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_statistics는 routes 경유로 연쇄(001:538,543). 따라서 DELETE FROM projects WHERE id = %s 한 줄이면 충분하다 — 테이블 목록을 코드에 다시 나열하지 않는다(스키마 변경 시 어긋남 방지).
4. 남기는 것 2가지
system_audit_logs— 감사 기록은 삭제 후에도 남겨야 한다.resource_id가 현재NULL로만 들어가고 FK도 없어 고아 참조 문제 없음.action은PROJECT_HARD_DELETE로 구분해 기록.temp_batches.linked_project_id— FK 없는 이력 보존용 컬럼. 설계 의도대로 그대로 둔다.
5. 실행 순서 — DB 먼저, 파일 나중
storage_path조회 (삭제 전에 읽어 둬야 함)DELETE FROM projects+ 감사 로그 INSERT → 커밋- 커밋 성공 후
rmtreeDB가 먼저 지워지면 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와 문자열 일치 — DBstorage_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:77에resolve_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응답user에project_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전부 통과.env에PROJECT_DELETE_HARD_ENABLED=True기재 —.env.example은 이 저장소에 없다
완료 조건
- 플래그
False: 삭제 동작이 지금과 100% 동일 (deleted_at UPDATE + 감사 로그) - 플래그
True:projects행과 자식 테이블 전부 소멸,storage/.../{project_id}/폴더 부재,system_audit_logs에PROJECT_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로 되돌려야 한다.