Skip to content

Commit

Permalink
[BE] ✨ feat : 장바구니 목록 취소 기능 구현 merge
Browse files Browse the repository at this point in the history
[BE] ✨ feat : 장바구니 목록 취소 기능 구현 #42
  • Loading branch information
hobeen-kim authored Sep 5, 2023
2 parents fad2cbd + 395e387 commit cb10802
Show file tree
Hide file tree
Showing 11 changed files with 328 additions and 0 deletions.
11 changes: 11 additions & 0 deletions Server/src/docs/asciidoc/snippets/video/cartvideo.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ include::{snippets}/video/changecartcancel/http-response.adoc[]
==== Response Fields
include::{snippets}/video/changecartcancel/response-fields.adoc[]

== 비디오 장바구니 목록 취소
=== HTTP Request
include::{snippets}/video/deletecarts/http-request.adoc[]
==== Request Headers
include::{snippets}/video/deletecarts/request-headers.adoc[]
==== Request Body
include::{snippets}/video/deletecarts/request-body.adoc[]
==== Request Fields
include::{snippets}/video/deletecarts/request-fields.adoc[]
=== HTTP Response
include::{snippets}/video/deletecarts/http-response.adoc[]



Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@
import com.server.domain.member.entity.Member;
import com.server.domain.video.entity.Video;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

import java.util.List;
import java.util.Optional;

public interface CartRepository extends JpaRepository<Cart, Long> {

Optional<Cart> findByMemberAndVideo(Member member, Video video);

@Query("delete Cart c " +
"where c.video.videoId in :videoIds " +
"and c.member = :member")
@Modifying
int deleteByMemberAndVideoIds(Member member, List<Long> videoIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,16 @@ public ResponseEntity<ApiSingleResponse<Boolean>> changeCart(
return ResponseEntity.ok(ApiSingleResponse.ok(isInCart, message));
}

@DeleteMapping("/carts")
public ResponseEntity<Void> deleteCarts(
@RequestBody @Valid VideoCartDeleteApiRequest request,
@LoginId Long loginMemberId) {

videoService.deleteCarts(loginMemberId, request.getVideoIds());

return ResponseEntity.noContent().build();
}

@DeleteMapping("/{video-id}")
public ResponseEntity<Void> deleteVideo(@PathVariable("video-id") @Positive(message = "{validation.positive}") Long videoId,
@LoginId Long loginMemberId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.server.domain.video.controller.dto.request;

import com.server.global.validation.EachNotBlank;
import com.server.global.validation.EachPositive;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Builder
public class VideoCartDeleteApiRequest {

@NotNull(message = "{validation.video.cart.videoIds}")
@EachPositive(message = "{validation.positive}")
@Size(min = 1, message = "{validation.video.cart.videoIds}")
public List<Long> videoIds;
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,14 @@ public Boolean changeCart(Long loginMemberId, Long videoId) {
return createOrDeleteCart(member, video);
}

@Transactional
public void deleteCarts(Long loginMemberId, List<Long> videoIds) {

Member member = verifiedMember(loginMemberId);

cartRepository.deleteByMemberAndVideoIds(member, videoIds);
}

@Transactional
public void deleteVideo(Long loginMemberId, Long videoId) {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.server.global.validation;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EachPositiveValidator.class)
public @interface EachPositive {

String message() default "List contains not positive value";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.server.global.validation;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.List;

public class EachPositiveValidator implements ConstraintValidator<EachPositive, List<? extends Number>> {

@Override
public void initialize(EachPositive constraintAnnotation) {
}

@Override
public boolean isValid(List<? extends Number> list, ConstraintValidatorContext context) {
if (list == null) {
return true;
}

for (Number item : list) {
if (item == null || item.longValue() <= 0) {
return false;
}
}
return true;
}
}

1 change: 1 addition & 0 deletions Server/src/main/resources/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ validation.video.price=가격은 필수입니다.
validation.video.price.min=가격은 0원 이상이어야 합니다.
validation.video.categories=카테고리는 필수입니다.
validation.video.categories.size=카테고리는 {min}개 이상이어야 합니다.
validation.video.cart.videoIds=비디오 id 값은 필수입니다.

validation.announcement.content=공지사항 내용은 필수입니다.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

import static org.assertj.core.api.Assertions.*;


Expand All @@ -26,6 +28,7 @@ void findByMemberAndVideo() {
Video video = createAndSaveVideo(channel);

Member loginMember = createAndSaveMember();
Channel loginMemberChannel = createAndSaveChannel(loginMember);

Cart cart = Cart.createCart(loginMember, video, video.getPrice());
em.persist(cart);
Expand All @@ -40,4 +43,39 @@ void findByMemberAndVideo() {
assertThat(findCart.getMember().getMemberId()).isEqualTo(loginMember.getMemberId());
assertThat(findCart.getVideo().getVideoId()).isEqualTo(video.getVideoId());
}

@Test
@DisplayName("member 와 videoId 리스트로 모든 cart 를 삭제한다.")
void deleteByMemberAndVideoIds() {
//given
Member owner = createAndSaveMember();
Channel channel = createAndSaveChannel(owner);
Video video1 = createAndSaveVideo(channel);
Video video2 = createAndSaveVideo(channel);
Video video3 = createAndSaveVideo(channel);

Member loginMember = createAndSaveMember();
Channel loginMemberChannel = createAndSaveChannel(loginMember);

Cart cart1 = Cart.createCart(loginMember, video1, video1.getPrice());
Cart cart2 = Cart.createCart(loginMember, video2, video2.getPrice());
Cart cart3 = Cart.createCart(loginMember, video3, video3.getPrice());
em.persist(cart1);
em.persist(cart2);
em.persist(cart3);

em.flush();
em.clear();

//when (cart 에 담긴 3개 중 2개만 삭제)
cartRepository.deleteByMemberAndVideoIds(
loginMember,
List.of(video1.getVideoId(), video2.getVideoId())
);

//then
assertThat(cartRepository.findAll()).hasSize(1)
.extracting("cartId")
.containsExactlyInAnyOrder(cart3.getCartId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,43 @@ void changeCartCancel() throws Exception {
);
}

@Test
@DisplayName("videoId 를 통한 장바구니 전체 취소 API")
void deleteCarts() throws Exception {
//given
VideoCartDeleteApiRequest request = VideoCartDeleteApiRequest.builder()
.videoIds(List.of(1L, 2L, 3L))
.build();

//when
ResultActions actions = mockMvc.perform(
delete(BASE_URL + "/carts")
.contentType(APPLICATION_JSON)
.header(AUTHORIZATION, TOKEN)
.content(objectMapper.writeValueAsString(request))
);

//then
actions.andDo(print())
.andExpect(status().isNoContent());

//restDocs
setConstraintClass(VideoCartDeleteApiRequest.class);

actions
.andDo(
documentHandler.document(
requestHeaders(
headerWithName(AUTHORIZATION).description("Access Token")
),
requestFields(
fieldWithPath("videoIds").description("장바구니에서 삭제할 비디오 ID 목록")
.attributes(getConstraint("videoIds"))
)
)
);
}

@Test
@DisplayName("비디오 삭제 API")
void deleteVideo() throws Exception {
Expand Down Expand Up @@ -1478,6 +1515,83 @@ void changeCartValidation() throws Exception {
.andExpect(jsonPath("$.data[0].reason").value("해당 값은 양수만 가능합니다."));
}

@TestFactory
@DisplayName("장바구니 삭제 시 validation 테스트")
Collection<DynamicTest> deleteCartsValidation() {
//given


return List.of(
dynamicTest("videoIds 가 Null 이면 검증에 실패한다.", ()-> {
//given
VideoCartDeleteApiRequest request = VideoCartDeleteApiRequest.builder()
.videoIds(null)
.build();

//when
ResultActions actions = mockMvc.perform(
delete(BASE_URL + "/carts")
.contentType(APPLICATION_JSON)
.header(AUTHORIZATION, TOKEN)
.content(objectMapper.writeValueAsString(request))
);

//then
actions.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.data[0].field").value("videoIds"))
.andExpect(jsonPath("$.data[0].value").value("null"))
.andExpect(jsonPath("$.data[0].reason").value("비디오 id 값은 필수입니다."));
}),
dynamicTest("videoIds 가 있지만 빈 배열이면 검증에 실패한다.", ()-> {
//given
VideoCartDeleteApiRequest request = VideoCartDeleteApiRequest.builder()
.videoIds(new ArrayList<>())
.build();

//when
ResultActions actions = mockMvc.perform(
delete(BASE_URL + "/carts")
.contentType(APPLICATION_JSON)
.header(AUTHORIZATION, TOKEN)
.content(objectMapper.writeValueAsString(request))
);

//then
actions.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.data[0].field").value("videoIds"))
.andExpect(jsonPath("$.data[0].value").value("[]"))
.andExpect(jsonPath("$.data[0].reason").value("비디오 id 값은 필수입니다."));
}),
dynamicTest("videoIds 가 양수가 아니면 검증에 실패한다.", ()-> {
//given
VideoCartDeleteApiRequest request = VideoCartDeleteApiRequest.builder()
.videoIds(List.of(0L, 1L))
.build();

//when
ResultActions actions = mockMvc.perform(
delete(BASE_URL + "/carts")
.contentType(APPLICATION_JSON)
.header(AUTHORIZATION, TOKEN)
.content(objectMapper.writeValueAsString(request))
);

//then
actions.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.data[0].field").value("videoIds"))
.andExpect(jsonPath("$.data[0].value").value("[0, 1]"))
.andExpect(jsonPath("$.data[0].reason").value("해당 값은 양수만 가능합니다."));
})




);
}

@Test
@DisplayName("비디오 삭제 시 validation 테스트 - videoId 가 양수가 아니면 검증에 실패한다.")
void deleteVideoValidation() throws Exception {
Expand Down
Loading

0 comments on commit cb10802

Please sign in to comment.