Skip to content

Commit e4f804f

Browse files
authored
fix/#150 -> staging merge commit
fix: 네컷사진 삭제 시 Soft Delete 로 변경
1 parent 2d4064e commit e4f804f

File tree

7 files changed

+38
-66
lines changed

7 files changed

+38
-66
lines changed

src/main/kotlin/com/neki/media/application/port/MediaRepositoryPort.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface MediaRepositoryPort {
1717
fun getMediaForUploadConfirmation(ownerId: Long, ids: List<Long>): List<Media>
1818

1919
fun save(media: Media): Media
20+
fun saveAll(medias: List<Media>): List<Media>
2021

2122
fun delete(id: Long)
2223
fun deleteAll(ids: List<Long>)

src/main/kotlin/com/neki/media/application/usecase/DeleteMediaUseCase.kt

Lines changed: 7 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import com.neki.media.application.command.DeleteMediaCommand
88
import com.neki.media.application.command.DeleteMediasCommand
99
import com.neki.media.application.port.MediaBinaryCachePort
1010
import com.neki.media.application.port.MediaRepositoryPort
11-
import com.neki.media.application.port.MediaStoragePort
1211
import org.slf4j.LoggerFactory
1312

1413
/**
@@ -22,7 +21,6 @@ import org.slf4j.LoggerFactory
2221
@UseCase
2322
class DeleteMediaUseCase(
2423
private val mediaRepository: MediaRepositoryPort,
25-
private val mediaStorage: MediaStoragePort,
2624
private val cache: MediaBinaryCachePort,
2725
private val transactionRunner: TransactionRunner,
2826
) {
@@ -33,83 +31,29 @@ class DeleteMediaUseCase(
3331
* media 단건 삭제 usecase
3432
*/
3533
fun execute(command: DeleteMediaCommand) {
36-
// DB media metadata 삭제
3734
val media = transactionRunner.run {
3835
val foundMedia = mediaRepository.getActiveMedia(command.ownerId, command.mediaId)
3936
?: throw BusinessException(ResultCode.NOT_FOUND)
4037

41-
foundMedia.markAsDeleteRequested()
38+
foundMedia.markAsDeleted()
39+
mediaRepository.save(foundMedia)
4240
foundMedia
4341
}
4442

45-
cache.evict(media.storageKey) // cache 무효화 시도
46-
47-
return runCatching {
48-
// object storage 삭제 요청
49-
mediaStorage.deleteByKey(media.storageKey)
50-
}.fold(
51-
onSuccess = {
52-
transactionRunner.runNew {
53-
// 추후 scheduling 검토
54-
mediaRepository.delete(media.id!!)
55-
}
56-
},
57-
onFailure = { e ->
58-
log.warn(
59-
"Media delete failed. Will retry later. fileId={}, key={}",
60-
media.id,
61-
media.storageKey,
62-
e,
63-
)
64-
},
65-
)
66-
67-
// TODO : object storage 삭제 실패한 media에 대해 삭제 필요
43+
cache.evict(media.storageKey)
6844
}
6945

7046
/**
7147
* media bulk 삭제 usecase
7248
*/
7349
fun execute(command: DeleteMediasCommand) {
74-
// 삭제할 media 조회
7550
val medias = transactionRunner.run {
76-
val foundMedias =
77-
mediaRepository.getActiveMedias(command.ownerId, command.mediaIds)
78-
79-
foundMedias.forEach {
80-
it.markAsDeleteRequested()
81-
cache.evict(it.storageKey) // cache 무효화 시도
82-
}
51+
val foundMedias = mediaRepository.getActiveMedias(command.ownerId, command.mediaIds)
52+
foundMedias.forEach { it.markAsDeleted() }
53+
mediaRepository.saveAll(foundMedias)
8354
foundMedias
8455
}
8556

86-
val deletedMediaIds = mutableListOf<Long>()
87-
val failedMedias = mutableListOf<Long>()
88-
89-
// object storage 삭제
90-
medias.forEach { media ->
91-
runCatching {
92-
mediaStorage.deleteByKey(media.storageKey)
93-
}.onSuccess {
94-
deletedMediaIds.add(media.id!!)
95-
}.onFailure { e ->
96-
failedMedias.add(media.id!!)
97-
log.warn(
98-
"Media delete failed. Will retry later. mediaId={}, key={}",
99-
media.id,
100-
media.storageKey,
101-
e,
102-
)
103-
}
104-
}
105-
106-
// object storage 삭제 성공한 오브젝트에 대해 DB soft delete
107-
if (deletedMediaIds.isNotEmpty()) {
108-
transactionRunner.runNew {
109-
mediaRepository.deleteAll(deletedMediaIds)
110-
}
111-
}
112-
113-
// TODO : object storage 삭제 실패한 media에 대해 삭제 필요
57+
medias.forEach { cache.evict(it.storageKey) }
11458
}
11559
}

src/main/kotlin/com/neki/media/infra/persist/MediaRepositoryAdapter.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class MediaRepositoryAdapter(private val jpaRepository: JpaMediaRepository) : Me
3535

3636
override fun save(media: Media): Media = jpaRepository.save(media)
3737

38+
override fun saveAll(medias: List<Media>): List<Media> = jpaRepository.saveAll(medias)
39+
3840
override fun delete(id: Long) {
3941
jpaRepository.deleteById(id)
4042
}

src/main/kotlin/com/neki/photo/domain/entity/PhotoImage.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import jakarta.persistence.GenerationType
88
import jakarta.persistence.Id
99
import jakarta.persistence.Table
1010
import org.hibernate.annotations.DynamicUpdate
11+
import org.hibernate.annotations.SQLRestriction
12+
import java.time.LocalDateTime
1113

1214
/**
1315
* fileName : PhotoImage
@@ -17,6 +19,7 @@ import org.hibernate.annotations.DynamicUpdate
1719
*/
1820
@Entity
1921
@DynamicUpdate
22+
@SQLRestriction("deleted_at IS NULL")
2023
@Table(name = "TB_PHOTO_IMAGE")
2124
class PhotoImage(
2225
@Id
@@ -34,4 +37,13 @@ class PhotoImage(
3437

3538
@Column(name = "memo", nullable = true)
3639
var memo: String? = null,
37-
) : BaseTimeEntity()
40+
41+
@Column(name = "deleted_at", nullable = true)
42+
var deletedAt: LocalDateTime? = null,
43+
) : BaseTimeEntity() {
44+
45+
fun softDelete() {
46+
folderId = null
47+
deletedAt = LocalDateTime.now()
48+
}
49+
}

src/main/kotlin/com/neki/photo/infra/persist/PhotoImageRepositoryAdapter.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ class PhotoImageRepositoryAdapter(
6161
return emptyList()
6262
}
6363

64-
jpaRepository.deleteAll(photos)
64+
photos.forEach { it.softDelete() }
65+
jpaRepository.saveAll(photos)
6566
jpaRepository.flush()
6667

6768
return photos

src/main/kotlin/com/neki/photo/infra/persist/jpa/PhotoImageQueryRepository.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ class PhotoImageQueryRepository(private val queryFactory: JPAQueryFactory) {
131131
return queryFactory
132132
.update(photoImage)
133133
.setNull(photoImage.folderId)
134-
.where(photoImage.userId.eq(userId), photoImage.folderId.`in`(folderIds))
134+
.where(photoImage.userId.eq(userId), photoImage.folderId.`in`(folderIds), photoImage.deletedAt.isNull)
135135
.execute().toInt()
136136
}
137137

@@ -169,6 +169,7 @@ class PhotoImageQueryRepository(private val queryFactory: JPAQueryFactory) {
169169
photoImage.userId.eq(userId),
170170
photoImage.folderId.eq(folderId),
171171
photoImage.id.`in`(photoIds),
172+
photoImage.deletedAt.isNull,
172173
)
173174
.execute().toInt()
174175
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- deleted_at 컬럼 추가 (soft delete용)
2+
ALTER TABLE TB_PHOTO_IMAGE ADD COLUMN deleted_at TIMESTAMP NULL;
3+
4+
-- 기존 unique constraint 제거
5+
-- soft-deleted 레코드까지 포함하면 삭제 후 같은 미디어 재등록 시 unique violation 발생
6+
ALTER TABLE TB_PHOTO_IMAGE DROP CONSTRAINT uk_photo_image_media_id;
7+
8+
-- Partial Unique Index로 교체 (deleted_at IS NULL인 레코드에만 unique 보장)
9+
CREATE UNIQUE INDEX uk_photo_image_media_id
10+
ON TB_PHOTO_IMAGE (media_id)
11+
WHERE deleted_at IS NULL;

0 commit comments

Comments
 (0)