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
@@ -1,26 +1,52 @@
package issueissyu.backend.domain.community.service.command;

import issueissyu.backend.utils.S3.S3Utils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.List;

@Slf4j
@Component
@RequiredArgsConstructor
public class CommunicationPinCleaner {

private final JdbcTemplate jdbcTemplate;
private final S3Utils s3Utils;

@Transactional
public void deleteByPinId(Long pinId) {
jdbcTemplate.update("DELETE FROM pin_emoji WHERE pin_id = ?", pinId);
jdbcTemplate.update("DELETE FROM declaration WHERE pin_id = ?", pinId);
jdbcTemplate.update("DELETE FROM \"comment\" WHERE pin_id = ?", pinId);
jdbcTemplate.update("DELETE FROM pin_like WHERE pin_id = ?", pinId);
jdbcTemplate.update("DELETE FROM community WHERE pin_id = ?", pinId);
jdbcTemplate.update("DELETE FROM pin_location WHERE pin_id = ?", pinId);
// DB 삭제 전에 pin_image S3 key 수집
List<String> pinImageKeys = jdbcTemplate.queryForList(
"SELECT pin_s3_key FROM pin_image WHERE pin_id = ?",
String.class, pinId);

jdbcTemplate.update("DELETE FROM pin_emoji WHERE pin_id = ?", pinId);
jdbcTemplate.update("DELETE FROM declaration WHERE pin_id = ?", pinId);
jdbcTemplate.update("DELETE FROM \"comment\" WHERE pin_id = ?", pinId);
jdbcTemplate.update("DELETE FROM pin_like WHERE pin_id = ?", pinId);
jdbcTemplate.update("DELETE FROM community WHERE pin_id = ?", pinId);
jdbcTemplate.update("DELETE FROM pin_location WHERE pin_id = ?", pinId);
jdbcTemplate.update("DELETE FROM communication_pin WHERE pin_id = ?", pinId);
jdbcTemplate.update("DELETE FROM pin_image WHERE pin_id = ?", pinId);
jdbcTemplate.update("DELETE FROM pin WHERE pin_id = ?", pinId);
jdbcTemplate.update("DELETE FROM pin_image WHERE pin_id = ?", pinId);
jdbcTemplate.update("DELETE FROM pin WHERE pin_id = ?", pinId);

// DB 삭제 완료 후 트랜잭션 커밋이 성공하면 S3 객체 삭제
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
pinImageKeys.forEach(s3Utils::deleteIfNotReserved);
}
});
} else {
pinImageKeys.forEach(s3Utils::deleteIfNotReserved);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package issueissyu.backend.domain.community.service.command;

import issueissyu.backend.domain.community.entity.CardnewsImageS3;
import issueissyu.backend.domain.community.entity.Community;
import issueissyu.backend.domain.community.enums.CommunityType;
import issueissyu.backend.domain.community.exception.CommunityException;
import issueissyu.backend.domain.community.exception.code.CommunityErrorCode;
import issueissyu.backend.domain.community.repository.CardnewsImageS3Repository;
import issueissyu.backend.domain.community.repository.CommunityRepository;
import issueissyu.backend.utils.S3.S3Utils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.List;

@Service
@RequiredArgsConstructor
Expand All @@ -16,6 +23,8 @@ public class CommunityCommandServiceImpl implements CommunityCommandService {

private final CommunityRepository communityRepository;
private final CommunicationPinCleaner communicationPinCleaner;
private final CardnewsImageS3Repository cardnewsImageS3Repository;
private final S3Utils s3Utils;

@Override
public void deleteCommunity(Long communityId, String uid) {
Expand Down Expand Up @@ -45,6 +54,29 @@ public void takedownCommunity(Long communityId, String uid) {
if (!community.getPin().getUser().getUid().equals(uid)) {
throw CommunityException.of(CommunityErrorCode.COMMUNITY_403_3);
}

// cardnews_image_s3는 Community에 cascade가 없으므로 명시적으로 먼저 삭제
// findDetailById에서 left join fetch로 로드된 데이터를 활용
List<String> cardnewsKeys = community.getCardnewsImages().stream()
.map(CardnewsImageS3::getCardnewsImageS3Key)
.toList();
if (!cardnewsKeys.isEmpty()) {
cardnewsImageS3Repository.deleteByCommunity_CommunityId(communityId);
}

communityRepository.delete(community);

// DB 삭제 완료 후 트랜잭션 커밋이 성공하면 S3 객체 삭제
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
cardnewsKeys.forEach(s3Utils::deleteIfNotReserved);
}
});
} else {
cardnewsKeys.forEach(s3Utils::deleteIfNotReserved);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import issueissyu.backend.domain.issue.entity.ProblemSolverImage;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
Expand All @@ -12,6 +13,9 @@ public interface ProblemSolverImageRepository extends JpaRepository<ProblemSolve

Optional<ProblemSolverImage> findByProblemSolver_ProblemSolverId(Long problemSolverId);

@Query("SELECT psi FROM ProblemSolverImage psi WHERE psi.problemSolver.problemSolverId IN :ids")
List<ProblemSolverImage> findAllByProblemSolver_ProblemSolverIdIn(@Param("ids") Collection<Long> ids);

@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query("DELETE FROM ProblemSolverImage psi WHERE psi.problemSolver.problemSolverId IN :ids")
void deleteAllByProblemSolver_ProblemSolverIdIn(@Param("ids") Collection<Long> ids);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
import org.postgresql.geometric.PGpoint;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.ArrayList;
import java.time.LocalDateTime;
Expand Down Expand Up @@ -293,9 +295,27 @@ private boolean syncPinImages(Pin pin, List<PinImageItemReqDTO> items) {
boolean changed = false;
Set<String> keepUrls = items.stream().map(PinImageItemReqDTO::pinImageUrl).collect(Collectors.toSet());

// 제거 대상 key를 removeIf 전에 먼저 수집 (orphanRemoval로 DB 삭제되기 전)
List<String> removedKeys = pin.getPinImages().stream()
.filter(pi -> !keepUrls.contains(pi.getPinS3Url()))
.map(PinImage::getPinS3Key)
.filter(key -> key != null && !key.isBlank())
.toList();

boolean removedAny = pin.getPinImages().removeIf(pi -> !keepUrls.contains(pi.getPinS3Url()));
if (removedAny) {
changed = true;
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
removedKeys.forEach(s3Utils::deleteIfNotReserved);
}
});
} else {
removedKeys.forEach(s3Utils::deleteIfNotReserved);
}
}
Comment thread
taerimiiii marked this conversation as resolved.

for (PinImageItemReqDTO item : items) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package issueissyu.backend.domain.pin.service.command;

import issueissyu.backend.domain.community.entity.CardnewsImageS3;
import issueissyu.backend.domain.community.repository.CardnewsImageS3Repository;
import issueissyu.backend.domain.community.repository.CommunityRepository;
import issueissyu.backend.domain.alarm.service.command.PinAlarmCleaner;
import issueissyu.backend.domain.issue.entity.IssuePin;
import issueissyu.backend.domain.issue.entity.ProblemSolverImage;
import issueissyu.backend.domain.issue.repository.ComplaintPetitionRepository;
import issueissyu.backend.domain.issue.repository.IssuePinRepository;
import issueissyu.backend.domain.issue.repository.IssuePetitionRepository;
import issueissyu.backend.domain.issue.repository.ProblemSolverImageRepository;
import issueissyu.backend.domain.issue.repository.ProblemSolverRepository;
import issueissyu.backend.domain.map.repository.NoticeRepository;
import issueissyu.backend.domain.pin.entity.Pin;
import issueissyu.backend.domain.pin.entity.PinImage;
import issueissyu.backend.domain.pin.enums.PinType;
import issueissyu.backend.domain.pin.exception.PinException;
import issueissyu.backend.domain.pin.exception.code.PinErrorCode;
Expand All @@ -19,18 +22,25 @@
import issueissyu.backend.domain.pin.repository.DeclarationRepository;
import issueissyu.backend.domain.pin.repository.EventPinRepository;
import issueissyu.backend.domain.pin.repository.PinEmojiRepository;
import issueissyu.backend.domain.pin.repository.PinImageRepository;
import issueissyu.backend.domain.pin.repository.PinLikeRepository;
import issueissyu.backend.domain.pin.repository.PinRepository;
import issueissyu.backend.domain.pin.repository.StoreImageRepository;
import issueissyu.backend.domain.location.repository.PinLocationRepository;
import issueissyu.backend.domain.user.enums.UserRole;
import issueissyu.backend.domain.user.repository.UserRepository;
import issueissyu.backend.utils.S3.S3Utils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
Expand All @@ -52,9 +62,11 @@ public class PinDeleteCommandServiceImpl implements PinDeleteCommandService {
private final CommunicationPinRepository communicationPinRepository;
private final EventPinRepository eventPinRepository;
private final StoreImageRepository storeImageRepository;
private final PinImageRepository pinImageRepository;
private final PinLocationRepository pinLocationRepository;
private final UserRepository userRepository;
private final PinAlarmCleaner pinAlarmCleaner;
private final S3Utils s3Utils;

@Override
public void deletePin(String uid, Long pinId) {
Expand Down Expand Up @@ -93,17 +105,25 @@ public void deletePin(String uid, Long pinId) {
Long communityId = communityOpt.map(c -> c.getCommunityId()).orElse(null);
pinAlarmCleaner.deleteByPinId(pinId, communityId);

communityOpt.ifPresent(
c ->
cardnewsImageS3Repository.deleteByCommunity_CommunityId(
c.getCommunityId()));
// S3 정리 대상 key 수집 (DB 삭제 전에 미리 조회)
List<String> s3KeysToDelete = new ArrayList<>();

communityOpt.ifPresent(c -> {
cardnewsImageS3Repository.findAllByCommunityCommunityId(c.getCommunityId())
.stream()
.map(CardnewsImageS3::getCardnewsImageS3Key)
.forEach(s3KeysToDelete::add);
cardnewsImageS3Repository.deleteByCommunity_CommunityId(c.getCommunityId());
});
communityRepository.deleteByPin_PinId(pinId);

issuePinRepository
.findByPin_PinId(pinId)
.ifPresent(this::deleteIssueAssociations);
.ifPresent(ip -> deleteIssueAssociations(ip, s3KeysToDelete));
issuePinRepository.deleteByPin_PinId(pinId);

storeImageRepository.findByEventPin_Pin_PinId(pinId)
.ifPresent(si -> s3KeysToDelete.add(si.getImageS3Key()));
storeImageRepository.deleteByEventPin_Pin_PinId(pinId);
eventPinRepository.deleteByPin_PinId(pinId);

Expand All @@ -114,16 +134,40 @@ public void deletePin(String uid, Long pinId) {
pinEmojiRepository.deleteByPin_PinId(pinId);
communicationPinRepository.deleteByPin_PinId(pinId);
pinLocationRepository.deleteByPin_PinId(pinId);

// pin_image는 pinRepository.delete(pin) cascade로 삭제되므로 key를 먼저 수집
pinImageRepository.findByPin_PinIdOrderByPinImageIdAsc(pinId)
.stream()
.map(PinImage::getPinS3Key)
.forEach(s3KeysToDelete::add);

pinRepository.delete(pin);

// DB 삭제 완료 후 트랜잭션 커밋이 성공하면 S3 객체 일괄 삭제
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
s3KeysToDelete.forEach(s3Utils::deleteIfNotReserved);
}
});
} else {
s3KeysToDelete.forEach(s3Utils::deleteIfNotReserved);
}
}

private void deleteIssueAssociations(IssuePin issuePin) {
private void deleteIssueAssociations(IssuePin issuePin, List<String> s3KeysToDelete) {
Long issuePinId = issuePin.getIssuePinId();
issuePetitionRepository.deleteByIssuePin_IssuePinId(issuePinId);
complaintPetitionRepository.deleteByIssuePin_IssuePinId(issuePinId);

List<Long> solverIds = problemSolverRepository.findAllProblemSolverIdsByIssuePin_IssuePinId(issuePinId);
if (!solverIds.isEmpty()) {
problemSolverImageRepository.findAllByProblemSolver_ProblemSolverIdIn(solverIds)
.stream()
.map(ProblemSolverImage::getProblemSolverImageS3Key)
.forEach(s3KeysToDelete::add);
problemSolverImageRepository.deleteAllByProblemSolver_ProblemSolverIdIn(solverIds);
}
problemSolverRepository.deleteAllByIssuePin_IssuePinId(issuePinId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package issueissyu.backend.domain.user.service;

import issueissyu.backend.domain.alarm.service.command.PinAlarmCleaner;
import issueissyu.backend.utils.S3.S3Utils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@Component
@RequiredArgsConstructor
public class UserSignOutCleaner {
Expand All @@ -23,9 +31,13 @@ public class UserSignOutCleaner {

private final JdbcTemplate jdbcTemplate;
private final PinAlarmCleaner pinAlarmCleaner;
private final S3Utils s3Utils;

@Transactional
public void deleteRowsReferencingUser(String uid) {
// DB 삭제 전에 S3 key 일괄 수집
List<String> s3KeysToDelete = collectS3Keys(uid);

jdbcTemplate.update(
"DELETE FROM problem_solver_image WHERE problem_solver_id IN ("
+ "SELECT problem_solver_id FROM problem_solver WHERE uid = ? "
Expand Down Expand Up @@ -118,5 +130,53 @@ public void deleteRowsReferencingUser(String uid) {

jdbcTemplate.update("DELETE FROM user_custom_collection WHERE uid = ?", uid);
jdbcTemplate.update("DELETE FROM user_emoji WHERE uid = ?", uid);

// DB 삭제 완료 후 트랜잭션 커밋이 성공하면 S3 객체 일괄 삭제
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
s3KeysToDelete.forEach(s3Utils::deleteIfNotReserved);
}
});
} else {
s3KeysToDelete.forEach(s3Utils::deleteIfNotReserved);
}
}

private List<String> collectS3Keys(String uid) {
List<String> keys = new ArrayList<>();

// problem_solver_image: 본인이 참여한 solver + 본인 핀에 달린 solver 이미지
keys.addAll(jdbcTemplate.queryForList(
"SELECT problem_solver_image_s3_key FROM problem_solver_image"
+ " WHERE problem_solver_id IN ("
+ "SELECT problem_solver_id FROM problem_solver WHERE uid = ?"
+ " OR issue_pin_id IN " + ISSUE_PIN_IDS_FOR_OWNED_PINS
+ ")",
String.class, uid, uid));

// cardnews_image_s3: 본인 핀의 커뮤니티 카드뉴스 이미지
keys.addAll(jdbcTemplate.queryForList(
"SELECT cardnews_image_s3_key FROM cardnews_image_s3"
+ " WHERE community_id IN ("
+ "SELECT community_id FROM community WHERE pin_id IN "
+ PIN_IDS_OWNED_BY_USER + ")",
String.class, uid));

// store_image: 본인 핀의 이벤트 핀 스토어 이미지
keys.addAll(jdbcTemplate.queryForList(
"SELECT store_image_s3_key FROM store_image"
+ " WHERE event_pin_id IN " + EVENT_PIN_IDS_FOR_OWNED_PINS,
String.class, uid));

// pin_image: 본인 핀의 일반 이미지
keys.addAll(jdbcTemplate.queryForList(
"SELECT pin_s3_key FROM pin_image"
+ " WHERE pin_id IN " + PIN_IDS_OWNED_BY_USER,
String.class, uid));

return keys;
}
}
Loading
Loading