대시보드 삭제 버튼은 지금까지 projects.deleted_at만 찍는 소프트 삭제였다. 배포에서는
그게 맞다 — 사용자가 올린 라이다 원본은 다른 프로젝트에 재활용할 자산이다. 그러나 개발
중에는 프로젝트를 반복 생성·삭제하는데 정리 잡이 없어 수십 GB 원본이 계속 쌓인다.
config_system.py 맨 위에 PROJECT_DELETE_HARD_ENABLED를 두고 갈랐다. 기본값 False라
환경변수를 빠뜨린 배포 환경은 자동으로 안전한 쪽에 선다. True면 projects 행을 실제로
DELETE 하고(자식 테이블은 FK CASCADE로 함께 사라진다) storage/{회사}/{사용자}/{프로젝트ID}/
폴더를 통째로 지운다.
자식 테이블 목록은 코드에 나열하지 않았다. projects.id 참조가 전부 ON DELETE CASCADE라
행 하나면 충분하고, 목록을 복사해 두면 스키마가 바뀔 때 조용히 어긋난다.
순서는 DB 먼저 커밋, rmtree 나중이다. 파일을 먼저 지우면 DB 실패 시 실체 없는 프로젝트가
목록에 남아 화면이 깨진다. 반대면 rmtree가 실패해도 고아 폴더만 남고 정합성은 유지된다.
resolve_stored_project_path()는 끝에서 makedirs를 하므로 삭제에 쓸 수 없다 — 지우기 직전에
폴더를 되살린다. 검증만 하는 resolve_project_root_for_delete()를 따로 뒀고, 저장소 루트 안 ·
세그먼트 정확히 4개 · 마지막 세그먼트가 요청 project_id와 일치를 모두 요구한다. DB의
storage_path가 오염돼도 상위 폴더나 남의 폴더를 지우지 못한다.
하드 삭제 모드에서는 확인 모달 문구를 바꿔 원본까지 사라진다고 알린다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
69 lines
2.3 KiB
TypeScript
69 lines
2.3 KiB
TypeScript
import { createButton } from "@ui/ui_template_elements";
|
|
import { table, text } from "@ui/ui_template_general_blocks";
|
|
import { createStepBar } from "@ui/ui_template_workflow_layout";
|
|
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
|
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav";
|
|
import type { DashboardUser, ProjectItem } from "./B01_Dashboard_Api_Fetch";
|
|
import { canDeleteProject, canEditProject } from "./B01_Dashboard_UI_Helper";
|
|
import { openDeleteProjectModal, openEditProjectModal } from "./B01_Dashboard_UI_Modals";
|
|
import { formatDate, L } from "./B01_Dashboard_UI_Common";
|
|
|
|
export function projectTable(projects: ProjectItem[], currentUser: DashboardUser): HTMLElement {
|
|
return table(
|
|
[
|
|
L("B01_Dashboard_Table_Project"),
|
|
L("B01_Dashboard_Table_Region"),
|
|
L("B01_Dashboard_Table_Progress"),
|
|
L("B01_Dashboard_Table_Workflow"),
|
|
L("B01_Dashboard_Table_Updated"),
|
|
L("B01_Dashboard_Table_Action"),
|
|
],
|
|
projects.map((project) => {
|
|
const actCell = document.createElement("div");
|
|
actCell.className = "b01-dashboard__actions";
|
|
|
|
if (canEditProject(currentUser, project)) {
|
|
actCell.append(
|
|
createButton({
|
|
label: L("Common_Btn_Edit"),
|
|
variant: "ghost",
|
|
onClick: () => openEditProjectModal(currentUser, project),
|
|
}),
|
|
);
|
|
}
|
|
if (canDeleteProject(currentUser)) {
|
|
actCell.append(
|
|
createButton({
|
|
label: L("Common_Btn_Delete"),
|
|
variant: "danger",
|
|
onClick: () => openDeleteProjectModal(currentUser, project),
|
|
}),
|
|
);
|
|
}
|
|
|
|
return [
|
|
text(project.name),
|
|
text(project.region),
|
|
text(`${project.progress_percent}%`),
|
|
workflow(project),
|
|
text(formatDate(project.updated_at)),
|
|
actCell,
|
|
];
|
|
}),
|
|
);
|
|
}
|
|
|
|
export function workflow(project: ProjectItem): HTMLElement {
|
|
return createStepBar(
|
|
workflowSteps(),
|
|
project.workflow_state?.current_stage ?? project.workflow_stage,
|
|
{
|
|
stages: project.workflow_state?.stages,
|
|
currentStage: project.workflow_state?.current_stage,
|
|
routes: WORKFLOW_STEP_ROUTES,
|
|
compact: true,
|
|
onStepClick: (stepIndex) => goToWorkflowStage(project.id, WORKFLOW_STEP_ROUTES[stepIndex]),
|
|
},
|
|
);
|
|
}
|