Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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 @@ -49,4 +49,11 @@ public ResponseEntity<Void> tempAlbumUpdate(
tempAlbumService.updateTempAlbum(tempAlbumId, request);
return ResponseEntity.noContent().build();
}

@DeleteMapping("/{tempAlbumId}")
@Operation(summary = "임시 앨범 삭제", description = "임시 앨범을 삭제합니다.")
public ResponseEntity<Void> tempAlbumDelete(@PathVariable Long tempAlbumId) {
tempAlbumService.deleteTempAlbum(tempAlbumId);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.cherrypic.domain.tempalbum.event;

import org.cherrypic.tempalbum.entity.TempAlbum;

public record TempAlbumDeleteEvent(Long tempAlbumId) {
public static TempAlbumDeleteEvent of(TempAlbum tempAlbum) {
return new TempAlbumDeleteEvent(tempAlbum.getId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import lombok.RequiredArgsConstructor;
import org.cherrypic.s3.S3Util;
import org.cherrypic.s3.enums.ImageType;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
Expand All @@ -18,4 +19,10 @@ public class TempAlbumEventListener {
public void handleTempAlbumImagesDeleteEvent(TempAlbumImagesDeleteEvent event) {
s3Util.deleteAllByUrls(event.tempImageUrls());
}

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleTempAlbumDeleteEvent(TempAlbumDeleteEvent event) {
s3Util.deleteAllByImageTypeAndTargetId(ImageType.TEMP_ALBUM_IMAGE, event.tempAlbumId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,12 @@

import org.cherrypic.tempalbum.entity.TempAlbumImage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

public interface TempAlbumImageRepository extends JpaRepository<TempAlbumImage, Long> {}
public interface TempAlbumImageRepository extends JpaRepository<TempAlbumImage, Long> {

@Modifying(clearAutomatically = true)
@Query("DELETE FROM TempAlbumImage i WHERE i.tempAlbum.id = :tempAlbumId")
void deleteAllByTempAlbumId(Long tempAlbumId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ public interface TempAlbumService {
void updateTempAlbum(Long tempAlbumId, TempAlbumUpdateRequest request);

TempAlbumInfoResponse getTempAlbum(Long tempAlbumId);

void deleteTempAlbum(Long tempAlbumId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
import org.cherrypic.domain.tempalbum.dto.response.TempAlbumCreateResponse;
import org.cherrypic.domain.tempalbum.dto.response.TempAlbumInfoResponse;
import org.cherrypic.domain.tempalbum.dto.response.TempAlbumListResponse;
import org.cherrypic.domain.tempalbum.event.TempAlbumDeleteEvent;
import org.cherrypic.domain.tempalbum.exception.TempAlbumErrorCode;
import org.cherrypic.domain.tempalbum.repository.TempAlbumImageRepository;
import org.cherrypic.domain.tempalbum.repository.TempAlbumRepository;
import org.cherrypic.exception.CustomException;
import org.cherrypic.global.util.MemberUtil;
import org.cherrypic.member.entity.Member;
import org.cherrypic.s3.S3Util;
import org.cherrypic.tempalbum.entity.TempAlbum;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -23,8 +27,12 @@
public class TempAlbumServiceImpl implements TempAlbumService {

private final MemberUtil memberUtil;
private final S3Util s3Util;

private final TempAlbumRepository tempAlbumRepository;
private final TempAlbumImageRepository tempAlbumImageRepository;

private final ApplicationEventPublisher eventPublisher;

@Override
@Transactional
Expand Down Expand Up @@ -74,6 +82,19 @@ public TempAlbumInfoResponse getTempAlbum(Long tempAlbumId) {
tempAlbum.getWebUrl());
}

@Override
@Transactional
public void deleteTempAlbum(Long tempAlbumId) {
final Member currentMember = memberUtil.getCurrentMember();
final TempAlbum tempAlbum = getTempAlbumById(tempAlbumId);

validateTempAlbumOwner(tempAlbum, currentMember.getId());

eventPublisher.publishEvent(TempAlbumDeleteEvent.of(tempAlbum));
tempAlbumImageRepository.deleteAllByTempAlbumId(tempAlbum.getId());
tempAlbumRepository.delete(tempAlbum);
}

private void validateTempAlbumCreateLimit(Member member) {
long count = tempAlbumRepository.countByMemberId(member.getId());
if (count >= 5) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,51 @@ class 임시_앨범_개별_조회_요청_시 {
.andExpect(jsonPath("$.data.message").value("임시 앨범 소유자가 아닌 경우 권한이 없습니다."));
}
}

@Nested
class 임시_앨범_삭제_요청_시 {

@Test
void 유효한_요청이면_임시_앨범을_삭제한다() throws Exception {
// given
willDoNothing().given(tempAlbumService).deleteTempAlbum(1L);

// when & then
ResultActions perform = mockMvc.perform(delete("/temp-albums/1"));

perform.andExpect(status().isNoContent())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.status").value(HttpStatus.NO_CONTENT.value()));
}

@Test
void 임시_앨범이_존재하지_않는_경우_예외가_발생한다() throws Exception {
// given
willThrow(new CustomException(TempAlbumErrorCode.TEMP_ALBUM_NOT_FOUND))
.given(tempAlbumService)
.deleteTempAlbum(1L);

// when & then
ResultActions perform = mockMvc.perform(delete("/temp-albums/1"));

perform.andExpect(status().isNotFound())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.status").value(HttpStatus.NOT_FOUND.value()));
}

@Test
void 임시_앨범_소유자가_아닌_경우_예외가_발생한다() throws Exception {
// given
willThrow(new CustomException(TempAlbumErrorCode.NOT_TEMP_ALBUM_OWNER))
.given(tempAlbumService)
.deleteTempAlbum(1L);

// when & then
ResultActions perform = mockMvc.perform(delete("/temp-albums/1"));

perform.andExpect(status().isForbidden())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.status").value(HttpStatus.FORBIDDEN.value()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,38 @@
import org.cherrypic.domain.tempalbum.dto.request.TempAlbumUpdateRequest;
import org.cherrypic.domain.tempalbum.dto.response.TempAlbumInfoResponse;
import org.cherrypic.domain.tempalbum.dto.response.TempAlbumListResponse;
import org.cherrypic.domain.tempalbum.event.TempAlbumDeleteEvent;
import org.cherrypic.domain.tempalbum.exception.TempAlbumErrorCode;
import org.cherrypic.domain.tempalbum.repository.TempAlbumImageRepository;
import org.cherrypic.domain.tempalbum.repository.TempAlbumRepository;
import org.cherrypic.domain.tempalbum.service.TempAlbumService;
import org.cherrypic.exception.CustomException;
import org.cherrypic.global.util.MemberUtil;
import org.cherrypic.member.entity.Member;
import org.cherrypic.member.entity.OauthInfo;
import org.cherrypic.tempalbum.entity.TempAlbum;
import org.cherrypic.tempalbum.entity.TempAlbumImage;
import org.cherrypic.tempalbum.enums.TempAlbumType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.event.ApplicationEvents;
import org.springframework.test.context.event.RecordApplicationEvents;

@RecordApplicationEvents
public class TempAlbumServiceTest extends IntegrationTest {

@Autowired private TempAlbumService tempAlbumService;

@Autowired TempAlbumRepository tempAlbumRepository;
@Autowired MemberRepository memberRepository;
@Autowired TempAlbumImageRepository tempAlbumImageRepository;

@MockitoBean private MemberUtil memberUtil;
@Autowired private ApplicationEvents applicationEvents;

@Nested
class 임시_앨범을_성성할_때 {
Expand Down Expand Up @@ -249,4 +257,68 @@ void setUp() {
.hasMessage(TempAlbumErrorCode.NOT_TEMP_ALBUM_OWNER.getMessage());
}
}

@Nested
class 임시_앨범을_삭제할_때 {

@BeforeEach
void setUp() {
Member member1 =
Member.createMember(
OauthInfo.createOauthInfo("testOauthId", "testOauthProvider"),
"testNickname1",
"testProfileImageUrl1");
Member member2 =
Member.createMember(
OauthInfo.createOauthInfo("testOauthId", "testOauthProvider"),
"testNickname2",
"testProfileImageUrl2");
memberRepository.saveAll(List.of(member1, member2));
given(memberUtil.getCurrentMember()).willReturn(member1);

TempAlbum tempAlbum1 = TempAlbum.createTempAlbum(member1, "testTitle1");
TempAlbum tempAlbum2 = TempAlbum.createTempAlbum(member2, "testTitle2");
tempAlbumRepository.saveAll(List.of(tempAlbum1, tempAlbum2));

TempAlbumImage image1 =
TempAlbumImage.createTempAlbumImage(tempAlbum1, "testUrl1", BigDecimal.ONE);
TempAlbumImage image2 =
TempAlbumImage.createTempAlbumImage(tempAlbum1, "testUrl2", BigDecimal.ONE);
tempAlbumImageRepository.saveAll(List.of(image1, image2));
}

@Test
void 유효한_요청이면_임시_앨범과_유관_정보를_모두_삭제한다() {
// when
tempAlbumService.deleteTempAlbum(1L);

// then
var events = applicationEvents.stream(TempAlbumDeleteEvent.class).toList();
Assertions.assertAll(
() -> assertThat(events.getFirst().tempAlbumId()).isEqualTo(1L),
() -> assertThat(tempAlbumRepository.findById(1L)).isNotPresent(),
() ->
assertThat(
tempAlbumImageRepository
.findAllById(List.of(1L, 2L))
.isEmpty())
.isTrue());
}

@Test
void 임시_앨범이_존재하지_않는_경우_예외가_발생한다() {
// when & then
assertThatThrownBy(() -> tempAlbumService.deleteTempAlbum(999L))
.isInstanceOf(CustomException.class)
.hasMessage(TempAlbumErrorCode.TEMP_ALBUM_NOT_FOUND.getMessage());
}

@Test
void 임시_앨범_소유자가_아닌_경우_예외가_발생한다() {
// when & then
assertThatThrownBy(() -> tempAlbumService.deleteTempAlbum(2L))
.isInstanceOf(CustomException.class)
.hasMessage(TempAlbumErrorCode.NOT_TEMP_ALBUM_OWNER.getMessage());
}
}
}
7 changes: 7 additions & 0 deletions cherrypic-batch/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@ jar {
enabled = false
}

repositories {
maven {
url = 'https://jitpack.io'
}
}

Comment thread
ht3064 marked this conversation as resolved.
dependencies {
implementation project(':cherrypic-domain')
implementation project(':cherrypic-common')
implementation project(':cherrypic-infrastructure')
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.cherrypic.domain.tempalbum.job;

import lombok.RequiredArgsConstructor;
import org.cherrypic.domain.tempalbum.service.TempAlbumService;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class TempAlbumExpireJob {

private final TempAlbumService tempAlbumService;

public void run() {
tempAlbumService.expireOverdueTempAlbum();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.cherrypic.domain.tempalbum.repository;

import java.util.List;
import org.cherrypic.tempalbum.entity.TempAlbumImage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

public interface TempAlbumImageRepository extends JpaRepository<TempAlbumImage, Long> {

@Modifying(clearAutomatically = true)
@Query("DELETE FROM TempAlbumImage i WHERE i.tempAlbum.id IN :albumIds")
void deleteAllByTempAlbumIds(List<Long> albumIds);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.cherrypic.domain.tempalbum.repository;

import java.time.LocalDate;
import java.util.List;
import org.cherrypic.tempalbum.entity.TempAlbum;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface TempAlbumRepository extends JpaRepository<TempAlbum, Long> {

@Query("SELECT t FROM TempAlbum t WHERE t.expiredAt = :now")
List<TempAlbum> findAllExpiredToday(@Param("now") LocalDate now);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@Param은 제거해도 될 것 같습니다!

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.

Prepared Statement 캐싱을 위해서는 파라미터 바인딩이 필요하지 않나요?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

파라미터 이름이 동일해서 어노테이션 제거해도 될 것 같다고 말씀드린겁니다!

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.cherrypic.domain.tempalbum.schedular;

import lombok.RequiredArgsConstructor;
import org.cherrypic.domain.tempalbum.job.TempAlbumExpireJob;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class TempAlbumExpireScheduler {

private final TempAlbumExpireJob tempAlbumExpireJob;

@Scheduled(cron = "0 0 0 * * *")
public void runTempAlbumExpirejob() {
tempAlbumExpireJob.run();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.cherrypic.domain.tempalbum.service;

import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.cherrypic.domain.tempalbum.repository.TempAlbumImageRepository;
import org.cherrypic.domain.tempalbum.repository.TempAlbumRepository;
import org.cherrypic.s3.S3Util;
import org.cherrypic.tempalbum.entity.TempAlbum;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class TempAlbumService {

private final TempAlbumRepository tempAlbumRepository;
private final TempAlbumImageRepository tempAlbumImageRepository;

private final S3Util s3Util;

public void expireOverdueTempAlbum() {
List<TempAlbum> tempAlbums = tempAlbumRepository.findAllExpiredToday(LocalDate.now());
List<Long> tempAlbumIds = tempAlbums.stream().map(TempAlbum::getId).toList();

s3Util.deleteAllTempAlbumImagesInBatch(tempAlbumIds);
tempAlbumImageRepository.deleteAllByTempAlbumIds(tempAlbumIds);
tempAlbumRepository.deleteAllInBatch(tempAlbums);
}
Comment thread
ht3064 marked this conversation as resolved.
}
Loading