Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,7 @@ class ManageAccountTransactionUseCase(
}

private fun deleteTransactionReceipts(transactionId: Long) {
val receipts = findTransactionReceipts(transactionId)
if (receipts.isNotEmpty()) {
fileRepository.deleteAll(receipts)
fileRepository.flush()
}
fileRepository.hardDeleteActiveByOwnerTypeAndOwnerId(FileOwnerType.ACCOUNT_TRANSACTION, transactionId)
}

private fun findTransactionReceipts(transactionId: Long): List<File> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import com.weeth.domain.club.domain.service.ClubMemberPolicy
import com.weeth.domain.file.application.dto.request.FileSaveRequest
import com.weeth.domain.file.application.mapper.FileMapper
import com.weeth.domain.file.domain.enums.FileOwnerType
import com.weeth.domain.file.domain.repository.FileReader
import com.weeth.domain.file.domain.repository.FileRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -30,7 +29,6 @@ class ManagePostUseCase(
private val clubMemberPolicy: ClubMemberPolicy,
private val clubMemberCardinalReader: ClubMemberCardinalReader,
private val fileRepository: FileRepository,
private val fileReader: FileReader,
private val fileMapper: FileMapper,
private val postMapper: PostMapper,
) {
Expand Down Expand Up @@ -111,8 +109,7 @@ class ManagePostUseCase(
clubId: Long,
): Board = boardRepository.findByIdAndClubIdAndIsDeletedFalse(boardId, clubId) ?: throw BoardNotFoundException()

private fun findPost(postId: Long): Post =
postRepository.findActivePostById(postId) ?: throw PostNotFoundException()
private fun findPost(postId: Long): Post = postRepository.findByIdWithLock(postId) ?: throw PostNotFoundException()

private fun validateOwner(
post: Post,
Expand Down Expand Up @@ -154,11 +151,6 @@ class ManagePostUseCase(
}

private fun deletePostFiles(postId: Long) {
val files = fileReader.findAll(FileOwnerType.POST, postId)

if (files.isNotEmpty()) {
fileRepository.deleteAll(files)
fileRepository.flush()
}
fileRepository.hardDeleteActiveByOwnerTypeAndOwnerId(FileOwnerType.POST, postId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ interface PostLikeRepository :
@Param("clubId") clubId: Long,
): List<Long>

@Query(
"""
SELECT p.id
FROM PostLike pl
JOIN pl.post p
JOIN p.board b
WHERE pl.userId = :userId
AND b.club.id IN :clubIds
AND pl.isActive = true
AND pl.deletedAt IS NULL
ORDER BY p.id ASC
""",
)
fun findActivePostIdsByUserIdAndClubIdIn(
@Param("userId") userId: Long,
@Param("clubIds") clubIds: List<Long>,
): List<Long>

@Query(
"""
SELECT pl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,18 @@ interface PostRepository :
fun countActivePostsByBoardIds(
@Param("boardIds") boardIds: List<Long>,
): List<BoardPostCount>

@Query(
"""
SELECT p.id
FROM Post p
WHERE p.clubMember.id IN :clubMemberIds
AND p.isDeleted = false
AND p.board.isDeleted = false
ORDER BY p.id ASC
""",
)
fun findActiveIdsByClubMemberIdIn(
@Param("clubMemberIds") clubMemberIds: List<Long>,
): List<Long>
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import com.weeth.domain.club.domain.service.ClubJoinPolicy
import com.weeth.domain.club.domain.service.ClubMemberPolicy
import com.weeth.domain.file.domain.entity.File
import com.weeth.domain.file.domain.enums.FileOwnerType
import com.weeth.domain.file.domain.enums.FileStatus
import com.weeth.domain.file.domain.port.FileAccessUrlPort
import com.weeth.domain.file.domain.repository.FileRepository
import com.weeth.domain.user.application.exception.UserInActiveException
import com.weeth.domain.user.domain.repository.UserReader
Expand All @@ -47,7 +45,6 @@ class ManageClubMemberUsecase(
private val clubJoinPolicy: ClubJoinPolicy,
private val clubActivityDeletionPolicy: ClubActivityDeletionPolicy,
private val fileRepository: FileRepository,
private val fileAccessUrlPort: FileAccessUrlPort,
private val clock: Clock,
) {
/**
Expand Down Expand Up @@ -92,19 +89,11 @@ class ManageClubMemberUsecase(
userId: Long,
request: UpdateMemberProfileRequest,
) {
val members = clubMemberRepository.findActiveByUserId(userId)
val members = clubMemberRepository.findAllActiveByUserIdWithLock(userId)
if (members.isEmpty()) throw ClubMemberNotFoundException()

request.profileImage?.let { profileImage ->
val existingFiles =
fileRepository.findAllByOwnerTypeAndOwnerIdAndStatus(
FileOwnerType.CLUB_MEMBER_PROFILE,
userId,
FileStatus.UPLOADED,
)
if (existingFiles.isNotEmpty()) {
fileRepository.deleteAll(existingFiles)
}
markFilesDeleted(userId)

val file =
File.createUploaded(
Expand All @@ -125,18 +114,10 @@ class ManageClubMemberUsecase(

@Transactional
fun deleteProfileImage(userId: Long) {
val members = clubMemberRepository.findActiveByUserId(userId)
val members = clubMemberRepository.findAllActiveByUserIdWithLock(userId)
if (members.isEmpty()) throw ClubMemberNotFoundException()

val existingFiles =
fileRepository.findAllByOwnerTypeAndOwnerIdAndStatus(
FileOwnerType.CLUB_MEMBER_PROFILE,
userId,
FileStatus.UPLOADED,
)
if (existingFiles.isNotEmpty()) {
fileRepository.deleteAll(existingFiles)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

근데 기존에 하드 딜리트를 하던 파트를 소프트 딜리트 + 즉시 정리 대상으로 표시하는 방향으로 수정된 이유가 있을까요??
어차피 즉시 정리 대상으로 처리할 것이라면 하드 딜리트를 해도 문제가 없지 않나 싶어서요!

@soo0711 soo0711 Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 DB와 S3 삭제 시점이 분리되어 있어서 소프트 딜리트 + 즉시 정리 대상으로 바꿨습니다!

하드 딜리트를 바로 해버리면 DB row는 삭제됐는데 S3 삭제가 실패하거나 반대로 S3를 먼저 삭제했는데 이후 트랜잭션이 롤백되면서 DB에는 파일 정보가 남아 있는데 S3는 없는 상태가 생길 수 있다고 생각했습니닷

그래서 API 호출 시점에는 hardDeleteAfter = now로 즉시 정리 대상임을 표시한 뒤 클린업 과정에서 S3 삭제 성공 후 하드 딜리트를 처리하는 것이 데이터 정합성 측면에서 더 적절하다고 생각해서 해당 방식으로 수정했습니다!!

이 방향에 대해서는 어떻게 생각하시나용??

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 S3 삭제를 진행하고 있진 않긴 해서... 요금이 크게 나오고 있지도 않고.. 음 고민이네요

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

트래픽이 늘어난다면 S3 비용도 무시 못할 정도가 되긴 할 것 같은데 지금 함께 고려할 단계인지를 고민해봐야할 것 같아요!

}
markFilesDeleted(userId)

members.forEach { it.removeProfileImage() }
}
Expand Down Expand Up @@ -186,4 +167,8 @@ class ManageClubMemberUsecase(
clubActivityDeletionPolicy.markMemberActivitiesDeleted(member, now)
member.leave(now)
}

private fun markFilesDeleted(userId: Long) {
fileRepository.hardDeleteActiveByOwnerTypeAndOwnerId(FileOwnerType.CLUB_MEMBER_PROFILE, userId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import com.weeth.domain.club.domain.vo.ClubContact
import com.weeth.domain.file.application.dto.request.FileSaveRequest
import com.weeth.domain.file.domain.entity.File
import com.weeth.domain.file.domain.enums.FileOwnerType
import com.weeth.domain.file.domain.enums.FileStatus
import com.weeth.domain.file.domain.repository.FileRepository
import com.weeth.domain.user.application.exception.UserInActiveException
import com.weeth.domain.user.domain.repository.UserReader
Expand Down Expand Up @@ -143,7 +142,7 @@ class ManageClubUseCase(
) {
clubPermissionPolicy.requireAdmin(clubId, userId)

val club = clubRepository.getClubById(clubId)
val club = clubRepository.getClubByIdForUpdate(clubId)

if (request.primaryContact == PrimaryContact.EMAIL) {
val resolvedEmail = request.contactEmail ?: club.clubContact.email
Expand Down Expand Up @@ -181,7 +180,7 @@ class ManageClubUseCase(
) {
clubPermissionPolicy.requireAdmin(clubId, userId)

val club = clubRepository.getClubById(clubId)
val club = clubRepository.getClubByIdForUpdate(clubId)
val newCode = ClubCodePolicy.generateCode()
club.regenerateCode(newCode)
}
Expand All @@ -193,7 +192,7 @@ class ManageClubUseCase(
) {
clubPermissionPolicy.requireAdmin(clubId, userId)

val club = clubRepository.getClubById(clubId)
val club = clubRepository.getClubByIdForUpdate(clubId)
deleteExistingFiles(FileOwnerType.CLUB_PROFILE, clubId)
club.removeProfileImage()
}
Expand All @@ -205,7 +204,7 @@ class ManageClubUseCase(
) {
clubPermissionPolicy.requireAdmin(clubId, userId)

val club = clubRepository.getClubById(clubId)
val club = clubRepository.getClubByIdForUpdate(clubId)
deleteExistingFiles(FileOwnerType.CLUB_BACKGROUND, clubId)
club.removeBackgroundImage()
}
Expand Down Expand Up @@ -239,11 +238,7 @@ class ManageClubUseCase(
ownerType: FileOwnerType,
ownerId: Long,
) {
val files = fileRepository.findAllByOwnerTypeAndOwnerIdAndStatus(ownerType, ownerId, FileStatus.UPLOADED)

if (files.isNotEmpty()) {
fileRepository.deleteAll(files)
}
fileRepository.hardDeleteActiveByOwnerTypeAndOwnerId(ownerType, ownerId)
}

private fun validatePrimaryContactEmail(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package com.weeth.domain.club.domain.service
import com.weeth.domain.board.domain.repository.PostLikeRepository
import com.weeth.domain.board.domain.repository.PostRepository
import com.weeth.domain.club.domain.entity.ClubMember
import com.weeth.domain.comment.domain.repository.CommentRepository
import com.weeth.domain.file.domain.enums.FileOwnerType
import com.weeth.domain.file.domain.repository.FileRepository
import org.springframework.stereotype.Service
import java.time.LocalDateTime

Expand All @@ -19,31 +22,74 @@ import java.time.LocalDateTime
class ClubActivityDeletionPolicy(
private val postLikeRepository: PostLikeRepository,
private val postRepository: PostRepository,
private val commentRepository: CommentRepository,
private val fileRepository: FileRepository,
) {
fun markMemberActivitiesDeleted(
member: ClubMember,
now: LocalDateTime,
) {
val postIds =
postLikeRepository
.findActivePostIdsByUserIdAndClubId(
userId = member.user.id,
clubId = member.club.id,
).distinct()
.sorted()

if (postIds.isEmpty()) return

val postsById = postRepository.findAllByIdsWithLock(postIds).associateBy { it.id }
if (postsById.isEmpty()) return

postLikeRepository
.findAllActiveByUserIdAndPostIds(
userId = member.user.id,
postIds = postsById.keys.toList(),
).forEach { like ->
if (!like.markDeleted(now)) return@forEach
postsById.getValue(like.post.id).decreaseLikeCount()
markMembersActivitiesDeleted(listOf(member), now)
}

fun markMembersActivitiesDeleted(
members: List<ClubMember>,
now: LocalDateTime,
) {
if (members.isEmpty()) return

deleteMembersFiles(members)
markMembersPostLikesDeleted(members, now)
}

private fun deleteMembersFiles(members: List<ClubMember>) {
val memberIds = members.map { it.id }.distinct().sorted()
val postIds = postRepository.findActiveIdsByClubMemberIdIn(memberIds)
val commentIds = commentRepository.findActiveIdsByClubMemberIdIn(memberIds)

deleteFiles(FileOwnerType.POST, postIds)
deleteFiles(FileOwnerType.COMMENT, commentIds)
}

private fun deleteFiles(
ownerType: FileOwnerType,
ownerIds: List<Long>,
) {
if (ownerIds.isEmpty()) return

fileRepository.hardDeleteActiveByOwnerTypeAndOwnerIdIn(ownerType, ownerIds)
}

private fun markMembersPostLikesDeleted(
members: List<ClubMember>,
now: LocalDateTime,
) {
members
.groupBy { it.user.id }
.forEach { (userId, userMembers) ->
val clubIds = userMembers.map { it.club.id }.distinct().sorted()
val postIds =
postLikeRepository
.findActivePostIdsByUserIdAndClubIdIn(
userId = userId,
clubIds = clubIds,
).distinct()
.sorted()

if (postIds.isEmpty()) return@forEach

val postsById = postRepository.findAllByIdsWithLock(postIds).associateBy { it.id }

val likes =
postLikeRepository.findAllActiveByUserIdAndPostIds(
userId = userId,
postIds = postIds,
)
Comment thread
soo0711 marked this conversation as resolved.

for (like in likes) {
if (!like.markDeleted(now)) continue
postsById[like.post.id]?.decreaseLikeCount()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import com.weeth.domain.comment.domain.repository.CommentRepository
import com.weeth.domain.file.application.dto.request.FileSaveRequest
import com.weeth.domain.file.application.mapper.FileMapper
import com.weeth.domain.file.domain.enums.FileOwnerType
import com.weeth.domain.file.domain.repository.FileReader
import com.weeth.domain.file.domain.repository.FileRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -25,7 +24,6 @@ class ManageCommentUseCase(
private val commentRepository: CommentRepository,
private val postRepository: PostRepository, // 타 도메인 이므로 Reader 사용 검토
private val clubMemberPolicy: ClubMemberPolicy,
private val fileReader: FileReader,
private val fileRepository: FileRepository,
private val fileMapper: FileMapper,
) : PostCommentUsecase {
Expand Down Expand Up @@ -64,7 +62,7 @@ class ManageCommentUseCase(
commentId: Long,
userId: Long,
) {
val comment = commentRepository.findByIdAndPostId(commentId, postId) ?: throw CommentNotFoundException()
val comment = findCommentWithLock(commentId, postId)
ensureOwner(comment, userId)
ensureNotDeleted(comment)

Expand All @@ -79,7 +77,7 @@ class ManageCommentUseCase(
userId: Long,
) {
val post = findPostWithLock(postId)
val comment = commentRepository.findByIdAndPostId(commentId, postId) ?: throw CommentNotFoundException()
val comment = findCommentWithLock(commentId, postId)
ensureOwner(comment, userId)

deleteComment(comment)
Expand Down Expand Up @@ -137,11 +135,7 @@ class ManageCommentUseCase(
}

private fun deleteCommentFiles(commentId: Long) {
val files = fileReader.findAll(FileOwnerType.COMMENT, commentId)

if (files.isNotEmpty()) {
fileRepository.deleteAll(files)
}
fileRepository.hardDeleteActiveByOwnerTypeAndOwnerId(FileOwnerType.COMMENT, commentId)
}

private fun ensureOwner(
Expand All @@ -167,4 +161,9 @@ class ManageCommentUseCase(

private fun findPostWithLock(postId: Long): Post =
postRepository.findByIdWithLock(postId) ?: throw PostNotFoundException()

private fun findCommentWithLock(
commentId: Long,
postId: Long,
): Comment = commentRepository.findByIdAndPostIdWithLock(commentId, postId) ?: throw CommentNotFoundException()
}
Loading