144 lines
4.1 KiB
Markdown
144 lines
4.1 KiB
Markdown
# 소프트 삭제 (Soft Delete) 가이드
|
|
|
|
## 개요
|
|
|
|
소프트 삭제는 데이터를 실제로 삭제하지 않고 `is_deleted` 필드를 `True`로 설정하여 삭제된 것처럼 처리하는 방식입니다.
|
|
이를 통해 데이터 복구가 가능하고, 삭제 이력을 추적할 수 있습니다.
|
|
|
|
## 적용 테이블
|
|
|
|
| 테이블 | is_deleted | 인덱스 | 비고 |
|
|
|--------|------------|--------|------|
|
|
| User | ✅ | idx_user_is_deleted | deleted_at 필드도 포함 |
|
|
| Project | ✅ | idx_project_is_deleted | |
|
|
| Image | ✅ | idx_image_is_deleted | |
|
|
| Lyric | ✅ | idx_lyric_is_deleted | |
|
|
| Song | ✅ | idx_song_is_deleted | |
|
|
| SongTimestamp | ✅ | idx_song_timestamp_is_deleted | |
|
|
| Video | ✅ | idx_video_is_deleted | |
|
|
| SocialAccount | ✅ | idx_social_account_is_deleted | |
|
|
| RefreshToken | ❌ | - | 토큰은 is_revoked로 관리 |
|
|
|
|
## 필드 정의
|
|
|
|
```python
|
|
is_deleted: Mapped[bool] = mapped_column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=False,
|
|
comment="소프트 삭제 여부 (True: 삭제됨)",
|
|
)
|
|
```
|
|
|
|
## API 엔드포인트
|
|
|
|
### 아카이브 삭제 API
|
|
|
|
```
|
|
DELETE /archive/videos/delete/{task_id}
|
|
```
|
|
|
|
task_id에 해당하는 모든 관련 데이터를 소프트 삭제합니다.
|
|
백그라운드에서 비동기로 처리됩니다.
|
|
|
|
## 백그라운드 태스크
|
|
|
|
### soft_delete_by_task_id
|
|
|
|
위치: `app/archive/worker/archive_task.py`
|
|
|
|
```python
|
|
from app.archive.worker.archive_task import soft_delete_by_task_id
|
|
|
|
# 백그라운드 태스크로 실행
|
|
background_tasks.add_task(soft_delete_by_task_id, task_id)
|
|
```
|
|
|
|
삭제 대상 테이블 (순서대로):
|
|
1. Video
|
|
2. SongTimestamp (suno_audio_id 기준)
|
|
3. Song
|
|
4. Lyric
|
|
5. Image
|
|
6. Project
|
|
|
|
## 사용 예시
|
|
|
|
### 소프트 삭제 수행
|
|
|
|
```python
|
|
async def soft_delete_project(session: AsyncSession, project_id: int) -> None:
|
|
"""프로젝트를 소프트 삭제합니다."""
|
|
stmt = (
|
|
update(Project)
|
|
.where(Project.id == project_id)
|
|
.values(is_deleted=True)
|
|
)
|
|
await session.execute(stmt)
|
|
await session.commit()
|
|
```
|
|
|
|
### 삭제되지 않은 데이터만 조회
|
|
|
|
```python
|
|
async def get_active_projects(session: AsyncSession) -> list[Project]:
|
|
"""삭제되지 않은 프로젝트만 조회합니다."""
|
|
stmt = select(Project).where(Project.is_deleted == False)
|
|
result = await session.execute(stmt)
|
|
return result.scalars().all()
|
|
```
|
|
|
|
### 삭제된 데이터 복구
|
|
|
|
```python
|
|
async def restore_project(session: AsyncSession, project_id: int) -> None:
|
|
"""삭제된 프로젝트를 복구합니다."""
|
|
stmt = (
|
|
update(Project)
|
|
.where(Project.id == project_id)
|
|
.values(is_deleted=False)
|
|
)
|
|
await session.execute(stmt)
|
|
await session.commit()
|
|
```
|
|
|
|
## 쿼리 시 주의사항
|
|
|
|
1. **기본 조회 시 is_deleted 필터 추가**
|
|
- 모든 조회 쿼리에서 `is_deleted == False` 조건을 명시적으로 추가해야 합니다.
|
|
|
|
2. **관리자 기능에서만 삭제된 데이터 포함**
|
|
- 일반 사용자 API에서는 삭제된 데이터가 노출되지 않도록 해야 합니다.
|
|
|
|
3. **CASCADE 삭제와의 관계**
|
|
- 부모 테이블 소프트 삭제 시 자식 테이블도 함께 소프트 삭제하는 로직 필요
|
|
- 또는 부모만 소프트 삭제하고 자식은 JOIN 시 필터링
|
|
|
|
## 마이그레이션
|
|
|
|
기존 데이터베이스에 `is_deleted` 필드를 추가하려면:
|
|
|
|
```bash
|
|
# 마이그레이션 SQL 실행
|
|
mysql -u <user> -p <database> < docs/database-schema/migration_add_is_deleted.sql
|
|
```
|
|
|
|
또는 Alembic 마이그레이션 사용:
|
|
|
|
```bash
|
|
alembic revision --autogenerate -m "Add is_deleted field to all tables"
|
|
alembic upgrade head
|
|
```
|
|
|
|
## 인덱스 활용
|
|
|
|
`is_deleted` 필드에 인덱스가 생성되어 있으므로, 다음과 같은 쿼리가 효율적으로 실행됩니다:
|
|
|
|
```sql
|
|
-- 삭제되지 않은 프로젝트 조회 (인덱스 활용)
|
|
SELECT * FROM project WHERE is_deleted = FALSE;
|
|
|
|
-- 복합 조건 (task_id + is_deleted)
|
|
SELECT * FROM project WHERE task_id = 'xxx' AND is_deleted = FALSE;
|
|
```
|