Files
sam-docs/dev/guides/archive-restore-feature-analysis.md
권혁성 db63fcff85 refactor: [docs] 팀별 폴더 구조 재편 (공유/개발/프론트/기획)
- 개발팀 전용 폴더 dev/ 생성 (standards, guides, quickstart, changes, deploys, data, history, dev_plans 이동)
- 프론트엔드 전용 폴더 frontend/ 생성 (api/ → frontend/api-specs/)
- 기획팀 폴더 requests/ 생성
- plans/ → dev/dev_plans/ 이름 변경
- README.md 신규 (사람용 안내), INDEX.md 재작성 (Claude Code용)
- resources.md 신규 (노션 링크용, assets/brochure 이관 예정)
- CURRENT_WORKS.md 삭제, TODO.md → dev/ 이동
- 전체 참조 경로 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 16:46:03 +09:00

8.6 KiB

Archive & Restore Feature Analysis

날짜: 2025-11-30 작업자: Claude Code 요청: 영구 삭제 데이터 복원 기능 구현


1. 요청 내용

  • https://mng.sam.kr/archived-records 에서 영구 삭제된 데이터를 복원할 수 있는 기능
  • 삭제/복구 프로세스 정립:
    • 일반 관리자/테넌트: Soft Delete
    • 슈퍼관리자: 영구 삭제 가능 → archived_records에 저장
    • 영구 삭제 데이터: 복원 가능해야 함
  • UI 개선: 작업 설명 컬럼 개선

2. 현재 상태 분석

2.1 forceDelete 사용 서비스 (8개)

모든 서비스가 아카이브 없이 바로 영구 삭제:

서비스 메서드 삭제 대상 파일 위치
TenantService forceDeleteTenant() 테넌트 + 부서/메뉴/역할 app/Services/TenantService.php:115
UserService forceDeleteUser() 사용자 app/Services/UserService.php:232
DepartmentService forceDeleteDepartment() 부서 app/Services/DepartmentService.php:171
MenuService forceDeleteMenu() 메뉴 app/Services/MenuService.php:281
BoardService forceDeleteBoard() 게시판 app/Services/BoardService.php:141
ProjectService forceDeleteProject() 프로젝트 app/Services/ProjectManagement/ProjectService.php:134
IssueService forceDeleteIssue() 이슈 app/Services/ProjectManagement/IssueService.php:160
TaskService forceDeleteTask() 작업 app/Services/ProjectManagement/TaskService.php:168

2.2 현재 DB 스키마

archived_records 테이블:

id                bigint PK
batch_id          char(36)         -- UUID, 그룹핑용
batch_description varchar(255)     -- 배치 설명
record_type       varchar(50)      -- ✅ varchar로 변경됨 (기존 enum)
original_id       bigint           -- 원본 레코드 ID
main_data         json             -- 원본 데이터 (JSON)
schema_version    varchar(50)      -- 스키마 버전
deleted_by        bigint FK        -- 삭제자
deleted_at        timestamp        -- 삭제 시간
notes             text             -- 메모
created_at, updated_at, created_by, updated_by

archived_record_relations 테이블:

id                  bigint PK
archived_record_id  bigint FK      -- archived_records.id
table_name          varchar(100)   -- 관련 테이블명
data                json           -- 관련 데이터 (JSON)
record_count        int            -- 레코드 수
created_at, updated_at, created_by, updated_by

2.3 문제점

  1. 아카이브 생성 코드 없음: ArchivedRecord::create() 호출하는 곳이 없음
  2. record_type enum 제한: varchar로 변경 완료
  3. 복원 기능 없음: RestoreService 미존재
  4. 데이터 유실: forceDelete 시 데이터가 완전히 삭제됨

3. 구현 계획

Phase 1: 인프라 구축 (이번 작업)

3.1 마이그레이션 완료

  • record_type enum → varchar(50) 변경 완료

3.2 ArchiveService 생성

class ArchiveService {
    // 단일 모델 아카이브
    public function archiveModel(Model $model, array $relations = [], ?string $batchId = null): ArchivedRecord

    // 배치 아카이브 (여러 모델)
    public function archiveBatch(Collection $models, string $description, array $relations = []): string

    // 모델별 record_type 매핑
    private function getRecordType(Model $model): string
}

3.3 RestoreService 생성

class RestoreService {
    // 단일 레코드 복원
    public function restoreRecord(ArchivedRecord $record): Model

    // 배치 전체 복원
    public function restoreBatch(string $batchId): Collection

    // 관계 데이터 복원
    private function restoreRelations(ArchivedRecord $record): void
}

3.4 기존 서비스 수정 (TenantService, UserService 먼저)

3.5 UI 개선

  • 복원 버튼 추가
  • 라우트 추가
  • 컨트롤러 메서드 추가

4. 수정 대상 파일

# 파일 작업 상태
1 database/migrations/2025_11_30_*_modify_archived_records_record_type_to_varchar.php 신규 완료
2 app/Services/ArchiveService.php 신규 🔄 진행 중
3 app/Services/RestoreService.php 신규 대기
4 app/Services/TenantService.php 수정 대기
5 app/Services/UserService.php 수정 대기
6 app/Http/Controllers/ArchivedRecordController.php 수정 대기
7 routes/web.php 수정 대기
8 resources/views/archived-records/show.blade.php 수정 대기

5. record_type 매핑

$recordTypeMap = [
    Tenant::class       => 'tenant',
    User::class         => 'user',
    Department::class   => 'department',
    Menu::class         => 'menu',
    Role::class         => 'role',
    Board::class        => 'board',
    Project::class      => 'project',
    Issue::class        => 'issue',
    Task::class         => 'task',
];

6. 복원 로직 흐름

1. ArchivedRecord 조회 (batch_id 또는 id)
2. main_data에서 원본 데이터 추출
3. 원본 테이블에 INSERT (새 ID 할당)
4. relations 복원 (ArchivedRecordRelation)
5. ArchivedRecord 삭제
6. 트랜잭션 커밋

7. 주의 사항

  • FK 제약: 복원 시 관계 테이블 순서 중요 (부모 먼저)
  • ID 할당: 복원 시 새 ID 할당 (original_id는 참조용)
  • tenant_id 무결성: Multi-tenant 데이터 복원 시 tenant_id 검증
  • 트랜잭션: 복원 실패 시 롤백 필수

8. Phase 2: 테넌트 필터링 기능 추가 (신규 요청)

8.1 요청 내용

  1. 대상 테넌트 필드 추가:
    • 테넌트 삭제 시: 어떤 테넌트인지 표시
    • 사용자 삭제 시: 어떤 테넌트 소속인지 표시
  2. 상단 테넌트 선택 필터링: 현재 선택된 테넌트의 아카이브만 표시

8.2 현재 문제점

  • archived_records 테이블에 tenant_id 컬럼 없음
  • 사용자 삭제 시 소속 테넌트 정보 저장 안 됨
  • 테넌트 선택 필터링 불가

8.3 해결 방안

방안 A: tenant_id 컬럼 추가 (권장)

장점:
- 직접 필터링 가능 (성능 좋음)
- 명확한 테넌트 소속 관계
- 인덱스 활용 가능

단점:
- 마이그레이션 필요
- 기존 데이터 처리 필요 (main_data에서 추출)

방안 B: main_data에서 JSON 추출 (현재 방식)

장점:
- DB 스키마 변경 없음

단점:
- JSON 추출 쿼리 복잡
- 성능 저하 (인덱스 불가)
- 사용자의 경우 tenant_id가 main_data에 없을 수 있음

8.4 권장 방안: A (tenant_id 컬럼 추가)

수정 대상 파일

# 저장소 파일 작업
1 api/ database/migrations/2025_12_01_*_add_tenant_id_to_archived_records.php 신규 - DB 마이그레이션
2 mng/ app/Services/ArchiveService.php 수정 - tenant_id 저장 로직
3 mng/ app/Services/ArchivedRecordService.php 수정 - 테넌트 필터링
4 mng/ app/Models/Archives/ArchivedRecord.php 수정 - fillable, 관계 추가
5 mng/ resources/views/archived-records/partials/table.blade.php 수정 - 대상 테넌트 표시

Note

: DB 마이그레이션은 api/ 저장소에서 관리됨. mng/에서는 모델과 서비스만 수정.

마이그레이션 내용 (api/)

// api/database/migrations/2025_12_01_*_add_tenant_id_to_archived_records.php
Schema::table('archived_records', function (Blueprint $table) {
    $table->unsignedBigInteger('tenant_id')->nullable()->after('record_type');
    $table->foreign('tenant_id')->references('id')->on('tenants')->nullOnDelete();
    $table->index('tenant_id');
});

tenant_id 결정 로직

- 테넌트 삭제: tenant_id = 삭제되는 테넌트의 ID (자기 자신)
- 사용자 삭제: tenant_id = session('selected_tenant_id') (현재 선택된 테넌트)
- 부서/메뉴/역할 삭제: tenant_id = 해당 레코드의 tenant_id

기존 데이터 처리

-- 테넌트 타입: main_data에서 id 추출
UPDATE archived_records
SET tenant_id = JSON_UNQUOTE(JSON_EXTRACT(main_data, '$.id'))
WHERE record_type = 'tenant' AND tenant_id IS NULL;

-- 사용자 타입: main_data에 tenant_id가 없으므로 NULL 유지
-- (또는 user_tenants 관계에서 추출 - 복잡)

8.5 UI 변경

목록 테이블 컬럼

| 작업 설명 | 대상 테넌트 | 대상 정보 | 레코드 타입 | ... |

필터링

  • 상단 테넌트 선택 시 session('selected_tenant_id') 기준 필터링
  • 슈퍼관리자: 전체 보기 가능
  • 일반 관리자: 소속 테넌트만 보기