diff --git a/src/main/java/io/oduck/api/domain/attractionPoint/controller/AttractionPointController.java b/src/main/java/io/oduck/api/domain/attractionPoint/controller/AttractionPointController.java index 39ef04a9..b33463b9 100644 --- a/src/main/java/io/oduck/api/domain/attractionPoint/controller/AttractionPointController.java +++ b/src/main/java/io/oduck/api/domain/attractionPoint/controller/AttractionPointController.java @@ -39,12 +39,11 @@ public ResponseEntity getAttractionPoint( return ResponseEntity.ok(attractionPointService.checkAttractionPoint(user.getId(), animeId)); } - @PatchMapping("/{attractionPointId}") + @PatchMapping public ResponseEntity patchAttractionPoint( @LoginUser AuthUser user, - @PathVariable("attractionPointId") Long attractionPointId, - @RequestBody @Valid UpdateAttractionPoint req){ - boolean update = attractionPointService.update(user.getId(), attractionPointId, req); + @RequestBody @Valid AttractionPointReq req){ + boolean update = attractionPointService.update(user.getId(), req); return ResponseEntity.status(update? HttpStatus.NO_CONTENT : HttpStatus.CONFLICT).build(); } diff --git a/src/main/java/io/oduck/api/domain/attractionPoint/dto/AttractionPointReqDto.java b/src/main/java/io/oduck/api/domain/attractionPoint/dto/AttractionPointReqDto.java index f021fa92..f19fdcba 100644 --- a/src/main/java/io/oduck/api/domain/attractionPoint/dto/AttractionPointReqDto.java +++ b/src/main/java/io/oduck/api/domain/attractionPoint/dto/AttractionPointReqDto.java @@ -22,14 +22,4 @@ public static class AttractionPointReq{ @NotNull(message = "입덕포인트를 선택하세요.") List attractionElements; } - - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class UpdateAttractionPoint{ - @NotNull(message = "입덕포인트를 선택하세요.") - AttractionElement attractionElement; - } - } diff --git a/src/main/java/io/oduck/api/domain/attractionPoint/dto/AttractionPointResDto.java b/src/main/java/io/oduck/api/domain/attractionPoint/dto/AttractionPointResDto.java index f934d9dd..425f9d5b 100644 --- a/src/main/java/io/oduck/api/domain/attractionPoint/dto/AttractionPointResDto.java +++ b/src/main/java/io/oduck/api/domain/attractionPoint/dto/AttractionPointResDto.java @@ -1,15 +1,10 @@ package io.oduck.api.domain.attractionPoint.dto; -import io.oduck.api.domain.attractionPoint.entity.AttractionElement; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.Arrays; -import java.util.Map; -import java.util.stream.Collectors; - @Getter @Builder public class AttractionPointResDto { diff --git a/src/main/java/io/oduck/api/domain/attractionPoint/repository/AttractionPointRepositoryCustom.java b/src/main/java/io/oduck/api/domain/attractionPoint/repository/AttractionPointRepositoryCustom.java index 9c6dda26..563e1e46 100644 --- a/src/main/java/io/oduck/api/domain/attractionPoint/repository/AttractionPointRepositoryCustom.java +++ b/src/main/java/io/oduck/api/domain/attractionPoint/repository/AttractionPointRepositoryCustom.java @@ -7,4 +7,5 @@ public interface AttractionPointRepositoryCustom { Long countElementByAnimeId(AttractionElement attractionElement, Long animeId); + Long countByAnimeId(Long animeId); } diff --git a/src/main/java/io/oduck/api/domain/attractionPoint/repository/AttractionPointRepositoryImpl.java b/src/main/java/io/oduck/api/domain/attractionPoint/repository/AttractionPointRepositoryImpl.java index ce2ad533..aa9c8638 100644 --- a/src/main/java/io/oduck/api/domain/attractionPoint/repository/AttractionPointRepositoryImpl.java +++ b/src/main/java/io/oduck/api/domain/attractionPoint/repository/AttractionPointRepositoryImpl.java @@ -17,13 +17,21 @@ class AttractionPointRepositoryImpl implements AttractionPointRepositoryCustom { @Override public Long countElementByAnimeId(AttractionElement attractionElement, Long animeId) { - Long elementCount = query - .select(attractionPoint.attractionElement.count()) + return query + .select(attractionPoint.count()) .from(attractionPoint) .where(attractionPoint.attractionElement.eq(attractionElement) .and(attractionPoint.anime.id.eq(animeId))) .fetchOne(); - return elementCount; + } + + @Override + public Long countByAnimeId(Long animeId) { + return query + .select(attractionPoint.count()) + .from(attractionPoint) + .where(attractionPoint.anime.id.eq(animeId)) + .fetchOne(); } } diff --git a/src/main/java/io/oduck/api/domain/attractionPoint/service/AttractionPointService.java b/src/main/java/io/oduck/api/domain/attractionPoint/service/AttractionPointService.java index e119cd2f..fbd7e46b 100644 --- a/src/main/java/io/oduck/api/domain/attractionPoint/service/AttractionPointService.java +++ b/src/main/java/io/oduck/api/domain/attractionPoint/service/AttractionPointService.java @@ -17,7 +17,7 @@ public interface AttractionPointService { //입덕포인트 조회(true/false) IsAttractionPoint isAttractionPoint(Long memberId, Long animeId); - boolean update(Long memberId, Long attractionPointId, UpdateAttractionPoint req); + boolean update(Long memberId, AttractionPointReq req); AttractionPointStats getAttractionPointStats(Long anime); } diff --git a/src/main/java/io/oduck/api/domain/attractionPoint/service/AttractionPointServiceImpl.java b/src/main/java/io/oduck/api/domain/attractionPoint/service/AttractionPointServiceImpl.java index f90f719d..e21bfcfc 100644 --- a/src/main/java/io/oduck/api/domain/attractionPoint/service/AttractionPointServiceImpl.java +++ b/src/main/java/io/oduck/api/domain/attractionPoint/service/AttractionPointServiceImpl.java @@ -7,14 +7,8 @@ import io.oduck.api.domain.attractionPoint.entity.AttractionElement; import io.oduck.api.domain.attractionPoint.entity.AttractionPoint; import io.oduck.api.domain.attractionPoint.repository.AttractionPointRepository; - -import java.util.*; -import java.util.function.Predicate; -import java.util.stream.Collectors; - import io.oduck.api.domain.member.entity.Member; import io.oduck.api.domain.member.repository.MemberRepository; -import io.oduck.api.domain.review.entity.ShortReview; import io.oduck.api.global.exception.BadRequestException; import io.oduck.api.global.exception.ConflictException; import io.oduck.api.global.exception.NotFoundException; @@ -23,6 +17,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.stream.Collectors; + @Slf4j @Service @RequiredArgsConstructor @@ -34,13 +31,10 @@ public class AttractionPointServiceImpl implements AttractionPointService { @Override public IsAttractionPoint isAttractionPoint(Long memberId, Long animeId) { - boolean drawing = false; - boolean story = false; - boolean music = false; - boolean character = false; - boolean voiceActor = false; - List points = attractionPointRepository.findAllByAnimeIdAndMemberId(memberId, animeId); + + boolean drawing = false, story = false, music = false, character = false, voiceActor = false; + for (AttractionPoint point : points) { switch (point.getAttractionElement()) { case DRAWING -> drawing = true; @@ -50,8 +44,8 @@ public IsAttractionPoint isAttractionPoint(Long memberId, Long animeId) { default -> voiceActor = true; } } - return IsAttractionPoint - .builder() + + return IsAttractionPoint.builder() .drawing(drawing) .story(story) .music(music) @@ -65,75 +59,59 @@ public IsAttractionPoint isAttractionPoint(Long memberId, Long animeId) { public void save(Long memberId, AttractionPointReq req) { List findPoint = attractionPointRepository.findAllByAnimeIdAndMemberId(memberId, req.getAnimeId()); - List elementList = findPoint.stream() - .map(AttractionPoint::getAttractionElement) - .toList(); - - //중복되지 않은 포인트 - List matchPoint = req.getAttractionElements().stream() - .filter(attractionElements -> - elementList.stream() - .noneMatch(Predicate.isEqual(attractionElements))) - .toList(); - + List matchPoint = findNonOverlappingValues(findPoint, req); if (matchPoint.isEmpty()) { throw new ConflictException("AttractionPoint"); } - Member member = memberRepository.findById(memberId) .orElseThrow(() -> new NotFoundException("Member")); Anime anime = animeRepository.findById(req.getAnimeId()) .orElseThrow(() -> new NotFoundException("Anime")); - List points = matchPoint - .stream() - .map(attractionElement -> AttractionPoint - .builder() - .member(member) - .anime(anime) - .attractionElement(attractionElement) - .build()) - .toList(); - attractionPointRepository.saveAll(points); + attractionPointRepository.saveAll(getAttractionPoint(matchPoint, member, anime)); } @Override public CheckAttractionPoint checkAttractionPoint(Long memberId, Long animeId) { List findPoint = attractionPointRepository.findAllByAnimeIdAndMemberId(memberId, animeId); - return CheckAttractionPoint - .builder() + return CheckAttractionPoint.builder() .isAttractionPoint(!findPoint.isEmpty()) .build(); } @Override - public boolean update(Long memberId, Long attractionPointId, UpdateAttractionPoint req) { - AttractionPoint findAttractionPoint = getAttractionPoint(attractionPointId); + public boolean update(Long memberId, AttractionPointReq req) { + List findPoint = attractionPointRepository.findAllByAnimeIdAndMemberId(memberId, req.getAnimeId()); - if (findAttractionPoint.getAttractionElement().equals(req.getAttractionElement())) { + if (findPoint.isEmpty()) { return false; } - Long findMemberId = findAttractionPoint.getMember().getId(); - //입덕 포인트 작성자 인지 확인 - Optional - .ofNullable(findMemberId) - .ifPresent( - id -> { - if (!findMemberId.equals(memberId)) { - throw new BadRequestException("Not the author of the attractionPoint."); - } - findAttractionPoint.updateElement(req.getAttractionElement()); - } - ); - attractionPointRepository.save(findAttractionPoint); + + Long findMemberId = findPoint.get(0).getMember().getId(); + + if (!findMemberId.equals(memberId)) { + throw new BadRequestException("Not the author of the attractionPoint."); + } + + List findAttractionElement = findNonOverlappingValues(findPoint, req); + + if (findAttractionElement.isEmpty()) { + throw new ConflictException("AttractionPoint"); + } else { + attractionPointRepository.deleteAllInBatch(findPoint); + + Member member = findPoint.get(0).getMember(); + Anime anime = findPoint.get(0).getAnime(); + + attractionPointRepository.saveAll(getAttractionPoint(findAttractionElement, member, anime)); + } return true; } @Override public AttractionPointStats getAttractionPointStats(Long animeId) { - //입덕포인트 / 전체 입덕포인트 개수 - Long totalCount = attractionPointRepository.count(); + Long totalCount = attractionPointRepository.countByAnimeId(animeId); double drawing = calculateElementRatio(AttractionElement.DRAWING, animeId, totalCount); double story = calculateElementRatio(AttractionElement.STORY, animeId, totalCount); double voiceActor = calculateElementRatio(AttractionElement.VOICE_ACTOR, animeId, totalCount); @@ -149,19 +127,27 @@ public AttractionPointStats getAttractionPointStats(Long animeId) { .build(); } - private AttractionPoint getAttractionPoint(Long attractionPointId) { - return attractionPointRepository.findById(attractionPointId) - .orElseThrow( - () -> new NotFoundException("AttractionPoint") - ); + private List getAttractionPoint(List elements, Member member, Anime anime){ + return elements.stream() + .map(attractionElement -> AttractionPoint.builder() + .member(member) + .anime(anime) + .attractionElement(attractionElement) + .build()) + .collect(Collectors.toList()); } - private double calculateElementRatio(AttractionElement element, Long animeId, Long totalCount) { Long countElementByAnimeId = attractionPointRepository.countElementByAnimeId(element, animeId); - if (countElementByAnimeId <= 0) { - return 0; - } - return (double) countElementByAnimeId / totalCount; + return countElementByAnimeId > 0 ? (double) countElementByAnimeId / totalCount * 100 : 0; } -} + private List findNonOverlappingValues(List findPoint, AttractionPointReq req) { + List elementList = findPoint.stream() + .map(AttractionPoint::getAttractionElement) + .toList(); + //중복되지 않은 값 + return req.getAttractionElements().stream() + .filter(attractionElement -> !elementList.contains(attractionElement)) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/test/java/io/oduck/api/e2e/attractionPoint/AttractionPointControllerTest.java b/src/test/java/io/oduck/api/e2e/attractionPoint/AttractionPointControllerTest.java index a790a8df..0d09ed85 100644 --- a/src/test/java/io/oduck/api/e2e/attractionPoint/AttractionPointControllerTest.java +++ b/src/test/java/io/oduck/api/e2e/attractionPoint/AttractionPointControllerTest.java @@ -11,6 +11,7 @@ import io.oduck.api.global.utils.ShortReviewTestUtils; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -164,16 +165,21 @@ class PatchAttractionPoint { @WithCustomMockMember(id = 2L, email = "john", password = "Qwer!234", role = Role.MEMBER) void patchAttractionPointSuccess() throws Exception { //given - Long attractionPointId = 1L; - UpdateAttractionPoint req = UpdateAttractionPoint + List elementList = new ArrayList<>(); + elementList.add(AttractionElement.CHARACTER); + elementList.add(AttractionElement.DRAWING); + + AttractionPointReq req = AttractionPointReq .builder() - .attractionElement(AttractionElement.VOICE_ACTOR) + .animeId(1L) + .attractionElements(elementList) .build(); + String content = gson.toJson(req); //when ResultActions actions = mockMvc.perform( - patch(BASE_URL + "/{attractionPointId}", attractionPointId) + patch(BASE_URL ) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .header(HttpHeaders.COOKIE, "oDuckio.sid={SESSION_VALUE}") @@ -191,31 +197,36 @@ void patchAttractionPointSuccess() throws Exception { .attributes(field("constraints", "oDuckio.sid={SESSION_VALUE}")) .description("Header Cookie, 세션 쿠키") ), - pathParameters( - parameterWithName("attractionPointId") - .description("입덕포인트 식별자")), requestFields(attributes(key("title").value("Fields for AttractionPoint creation")), - fieldWithPath("attractionElement") - .type(JsonFieldType.STRING) - .attributes(field("constraints", "DRAWING, STORY, MUSIC, CHARACTER, VOICE_ACTOR만 입력 가능합니다.")) - .description("입덕 포인트")) + fieldWithPath("animeId") + .type(JsonFieldType.NUMBER) + .attributes(field("constraints", "애니 아이디, NotNull, Min(1)")) + .description("애니 고유 식별 번호"), + fieldWithPath("attractionElements") + .type(JsonFieldType.ARRAY) + .attributes(field("constraints", "DRAWING, STORY, MUSIC, CHARACTER, VOICE_ACTOR 리스트만 허용합니다. ")) + .description("입덕포인트 리스트") + ) )); } - @DisplayName("입덕포인트 수정 성공시 Http Status 409 반환") + @DisplayName("입덕포인트 수정 실패시 Http Status 409 반환") @Test @WithCustomMockMember(id = 2L, email = "john", password = "Qwer!234", role = Role.MEMBER) void patchAttractionPointFalse() throws Exception { //given - Long attractionPointId = 1L; - UpdateAttractionPoint req = UpdateAttractionPoint + List elementList = new ArrayList<>(); + elementList.add(AttractionElement.STORY); + + AttractionPointReq req = AttractionPointReq .builder() - .attractionElement(AttractionElement.DRAWING) + .animeId(1L) + .attractionElements(elementList) .build(); String content = gson.toJson(req); //when ResultActions actions = mockMvc.perform( - patch(BASE_URL + "/{attractionPointId}", attractionPointId) + patch(BASE_URL) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .header(HttpHeaders.COOKIE, "oDuckio.sid={SESSION_VALUE}") @@ -233,14 +244,16 @@ void patchAttractionPointFalse() throws Exception { .attributes(field("constraints", "oDuckio.sid={SESSION_VALUE}")) .description("Header Cookie, 세션 쿠키") ), - pathParameters( - parameterWithName("attractionPointId") - .description("입덕포인트 식별자")), requestFields(attributes(key("title").value("Fields for AttractionPoint creation")), - fieldWithPath("attractionElement") - .type(JsonFieldType.STRING) - .attributes(field("constraints", "DRAWING, STORY, MUSIC, CHARACTER, VOICE_ACTOR만 입력 가능합니다.")) - .description("입덕 포인트")) + fieldWithPath("animeId") + .type(JsonFieldType.NUMBER) + .attributes(field("constraints", "애니 아이디, NotNull, Min(1)")) + .description("애니 고유 식별 번호"), + fieldWithPath("attractionElements") + .type(JsonFieldType.ARRAY) + .attributes(field("constraints", "DRAWING, STORY, MUSIC, CHARACTER, VOICE_ACTOR 리스트만 허용합니다. ")) + .description("입덕포인트 리스트") + ) )); } } diff --git a/src/test/java/io/oduck/api/unit/attractionPoint/service/AttractionPointServiceTest.java b/src/test/java/io/oduck/api/unit/attractionPoint/service/AttractionPointServiceTest.java index dd12a0ef..ec015a7a 100644 --- a/src/test/java/io/oduck/api/unit/attractionPoint/service/AttractionPointServiceTest.java +++ b/src/test/java/io/oduck/api/unit/attractionPoint/service/AttractionPointServiceTest.java @@ -4,7 +4,6 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doReturn; import io.oduck.api.domain.anime.entity.Anime; import io.oduck.api.domain.anime.repository.AnimeRepository; @@ -16,7 +15,6 @@ import io.oduck.api.domain.attractionPoint.service.AttractionPointServiceImpl; import io.oduck.api.domain.member.entity.Member; import io.oduck.api.domain.member.repository.MemberRepository; -import io.oduck.api.global.stub.MemberStub; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -105,25 +103,28 @@ class PatchAttractionPoint{ @DisplayName("입덕 포인트 수정 성공") void patchAttractionPointSuccess(){ //given - Long memberId = 1L; - Long attractionPointId = 1L; - AttractionPoint attractionPoint = AttractionPoint + List list = new ArrayList<>(); + AttractionPoint point = AttractionPoint .builder() .member(member) .anime(anime) - .attractionElement(AttractionElement.CHARACTER) + .attractionElement(AttractionElement.DRAWING) .build(); + list.add(point); + List elementList = new ArrayList<>(); + elementList.add(AttractionElement.VOICE_ACTOR); + elementList.add(AttractionElement.CHARACTER); - UpdateAttractionPoint update = UpdateAttractionPoint + given(attractionPointRepository.findAllByAnimeIdAndMemberId(anyLong(), anyLong())) + .willReturn(list); + AttractionPointReq update = AttractionPointReq .builder() - .attractionElement(AttractionElement.STORY) + .animeId(1L) + .attractionElements(elementList) .build(); - given(attractionPointRepository.findById(attractionPointId)).willReturn(Optional.ofNullable(attractionPoint)); - - //when - boolean updateAttractionPoint = attractionPointService.update(member.getId(), attractionPointId,update); + boolean updateAttractionPoint = attractionPointService.update(member.getId(), update); //then assertTrue(updateAttractionPoint); @@ -133,25 +134,22 @@ void patchAttractionPointSuccess(){ @DisplayName("입덕 포인트 수정 실패") void patchAttractionPointFalse(){ //given - Long memberId = 1L; - Long attractionPointId = 1L; - AttractionPoint attractionPoint = AttractionPoint - .builder() - .member(member) - .anime(anime) - .attractionElement(AttractionElement.CHARACTER) - .build(); + List list = new ArrayList<>(); + List elementList = new ArrayList<>(); + elementList.add(AttractionElement.DRAWING); + elementList.add(AttractionElement.STORY); + + given(attractionPointRepository.findAllByAnimeIdAndMemberId(anyLong(), anyLong())) + .willReturn(list); - UpdateAttractionPoint update = UpdateAttractionPoint + AttractionPointReq update = AttractionPointReq .builder() - .attractionElement(AttractionElement.CHARACTER) + .animeId(1L) + .attractionElements(elementList) .build(); - given(attractionPointRepository.findById(attractionPointId)).willReturn(Optional.ofNullable(attractionPoint)); - - //when - boolean updateAttractionPoint = attractionPointService.update(member.getId(), attractionPointId,update); + boolean updateAttractionPoint = attractionPointService.update(member.getId(),update); //then assertFalse(updateAttractionPoint);