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
15 changes: 14 additions & 1 deletion src/main/java/eatda/controller/cheer/CheerRegisterRequest.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
package eatda.controller.cheer;

import eatda.domain.cheer.CheerTagName;
import java.util.Collections;
import java.util.List;

public record CheerRegisterRequest(
String storeKakaoId,
String storeName,
String description
String description,
List<CheerTagName> tags
) {

@Override
public List<CheerTagName> tags() { // TODO : 클라이언트 태그 구현 완료 시 삭제
if (tags == null) {
return Collections.emptyList();
}
return tags;
Copy link
Member

Choose a reason for hiding this comment

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

[선택]
접근자 오버라이드도 좋지만 컴펙트 생성자를 만들어서 처리하는건 어떨까요??

public CheerRegisterRequest {
        if (tags == null) {
            tags = Collections.emptyList();
        }
    }

Copy link
Member Author

Choose a reason for hiding this comment

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

ObjectMapper를 이용해서 Request Body JSON을 CheerRegisterRequest로 파싱하는 것으로 알고 있습니다.
ObjectMapper의 구현 방식에 따라서 필도로 주입하기도 하고, 생성자를 이용해 주입하기도 해서 아마 내부 구현이 어떤지에 따라 의도한대로 작동할 수도 안할수도 있다고 알고 있습니다.

그래서 Record의 getter를 이용해서 오버라이딩하여 일시적으로 처리했습니다.

}
}
17 changes: 14 additions & 3 deletions src/main/java/eatda/controller/cheer/CheerResponse.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
package eatda.controller.cheer;

import eatda.domain.cheer.Cheer;
import eatda.domain.cheer.CheerTag;
import eatda.domain.cheer.CheerTagName;
import eatda.domain.store.Store;
import java.util.List;

public record CheerResponse(
long storeId,
long cheerId,
String imageUrl,
String cheerDescription
String cheerDescription,
List<CheerTagName> tags
) {

public CheerResponse(Cheer cheer, String imageUrl, Store store) {
public CheerResponse(Cheer cheer, List<CheerTag> cheerTags, Store store, String imageUrl) {
this(
store.getId(),
cheer.getId(),
imageUrl,
cheer.getDescription()
cheer.getDescription(),
toTagNames(cheerTags)
);
}

private static List<CheerTagName> toTagNames(List<CheerTag> cheerTags) {
return cheerTags.stream()
.map(CheerTag::getName)
.toList();
}
}
8 changes: 8 additions & 0 deletions src/main/java/eatda/repository/cheer/CheerTagRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package eatda.repository.cheer;

import eatda.domain.cheer.CheerTag;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CheerTagRepository extends JpaRepository<CheerTag, Long> {

}
8 changes: 7 additions & 1 deletion src/main/java/eatda/service/cheer/CheerService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
import eatda.controller.cheer.CheersResponse;
import eatda.domain.ImageKey;
import eatda.domain.cheer.Cheer;
import eatda.domain.cheer.CheerTag;
import eatda.domain.cheer.CheerTagNames;
import eatda.domain.member.Member;
import eatda.domain.store.Store;
import eatda.domain.store.StoreSearchResult;
import eatda.exception.BusinessErrorCode;
import eatda.exception.BusinessException;
import eatda.repository.cheer.CheerRepository;
import eatda.repository.cheer.CheerTagRepository;
import eatda.repository.member.MemberRepository;
import eatda.repository.store.StoreRepository;
import eatda.storage.image.ImageStorage;
Expand All @@ -32,20 +35,23 @@ public class CheerService {
private final MemberRepository memberRepository;
private final StoreRepository storeRepository;
private final CheerRepository cheerRepository;
private final CheerTagRepository cheerTagRepository;
private final ImageStorage imageStorage;

@Transactional
public CheerResponse registerCheer(CheerRegisterRequest request,
StoreSearchResult result,
ImageKey imageKey,
long memberId) {
CheerTagNames cheerTagNames = new CheerTagNames(request.tags());
Member member = memberRepository.getById(memberId);
validateRegisterCheer(member, request.storeKakaoId());

Store store = storeRepository.findByKakaoId(result.kakaoId())
.orElseGet(() -> storeRepository.save(result.toStore())); // TODO 상점 조회/저장 동시성 이슈 해결
Cheer cheer = cheerRepository.save(new Cheer(member, store, request.description(), imageKey));
return new CheerResponse(cheer, imageStorage.getPreSignedUrl(imageKey), store);
List<CheerTag> cheerTags = cheerTagRepository.saveAll(cheerTagNames.toCheerTags(cheer));
Copy link
Member

Choose a reason for hiding this comment

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

[선택 및 질문]
지금 방식도 충분히 좋지만 영속성 전이를 이용하는것도 좋을것 같은데 어떻게 생각하시는지 의견이 궁금해요 😄

## Cheer 엔티티
@OneToMany(mappedBy = "cheer", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CheerTag> cheerTags = new ArrayList<>();

public void addTags(CheerTagNames cheerTagNames) {
    List<CheerTag> newTags = cheerTagNames.toCheerTags(this);
    this.cheerTags.addAll(newTags);
}
사용
Cheer cheer = new Cheer(member, store, request.description(), imageKey);
cheer.addTags(cheerTagNames);
cheerRepository.save(cheer);

Copy link
Member Author

Choose a reason for hiding this comment

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

  1. 일단은 조회 시에도 N+1 문제가 발생할 것 같아요. (물론 최적화가 안된 부분도 많아서 크게 상관 없을 수 있지만요)
  2. 영속성 전이를 코드를 간결하게 사용할 수 있다는 면에서 좋다고 생각해요! 하지만 특정 부분을 사용할 때마다 SQL이 어떻게 나가는지가 예측하기 어렵다는 단점이 있다고 생각합니다. 그래서 일단은 익숙한 방식으로 빠르게 구현했습니다.

저도 코드를 간결하게 작성하는 것을 좋아하는 입장에서 선호합니다! 하지만 이것을 사용하려면 테스트를 진행하면서 SQL이 최적화된 상태로 나가는지 체크해야 되는 복잡함이 있다고 생각합니다.
그래서 일단은 기존의 OneToMany를 사용해서 빠르게 구현하고, 추후 빠르게 개선하도록 하겠습니다!

return new CheerResponse(cheer, cheerTags, store, imageStorage.getPreSignedUrl(imageKey));
}

private void validateRegisterCheer(Member member, String storeKakaoId) {
Expand Down
4 changes: 4 additions & 0 deletions src/test/java/eatda/controller/BaseControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import eatda.fixture.StoreGenerator;
import eatda.fixture.StoryGenerator;
import eatda.repository.cheer.CheerRepository;
import eatda.repository.cheer.CheerTagRepository;
import eatda.repository.member.MemberRepository;
import eatda.repository.store.StoreRepository;
import eatda.repository.story.StoryRepository;
Expand Down Expand Up @@ -71,6 +72,9 @@ public class BaseControllerTest {
@Autowired
protected CheerRepository cheerRepository;

@Autowired
protected CheerTagRepository cheerTagRepository;

@Autowired
protected StoryRepository storyRepository;

Expand Down
22 changes: 17 additions & 5 deletions src/test/java/eatda/controller/cheer/CheerControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@

import eatda.controller.BaseControllerTest;
import eatda.domain.cheer.Cheer;
import eatda.domain.cheer.CheerTagName;
import eatda.domain.member.Member;
import eatda.domain.store.District;
import eatda.domain.store.Store;
import eatda.util.ImageUtils;
import eatda.util.MappingUtils;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
Expand All @@ -22,8 +24,9 @@ class RegisterCheer {

@Test
void 응원을_등록한다() {
Store store = storeGenerator.generate("123", "서울시 노원구 월계3동 123-45", District.NOWON);
CheerRegisterRequest request = new CheerRegisterRequest(store.getKakaoId(), store.getName(), "맛있어요!");
Store store = storeGenerator.generate("123", "서울시 노원구 월계3동 123-45");
CheerRegisterRequest request = new CheerRegisterRequest(store.getKakaoId(), store.getName(), "맛있어요!",
List.of(CheerTagName.INSTAGRAMMABLE, CheerTagName.CLEAN_RESTROOM));

CheerResponse response = given()
.header(HttpHeaders.AUTHORIZATION, accessToken())
Expand All @@ -36,13 +39,18 @@ class RegisterCheer {
.statusCode(201)
.extract().as(CheerResponse.class);

assertThat(response.storeId()).isEqualTo(store.getId());
assertAll(
() -> assertThat(response.storeId()).isEqualTo(store.getId()),
() -> assertThat(response.cheerDescription()).isEqualTo(request.description()),
() -> assertThat(response.tags()).containsExactlyInAnyOrderElementsOf(request.tags())
);
}

@Test
void 이미지가_비어있을_경우에도_응원을_등록한다() {
Store store = storeGenerator.generate("123", "서울시 노원구 월계3동 123-45", District.NOWON);
CheerRegisterRequest request = new CheerRegisterRequest(store.getKakaoId(), store.getName(), "맛있어요!");
CheerRegisterRequest request = new CheerRegisterRequest(store.getKakaoId(), store.getName(), "맛있어요!",
List.of(CheerTagName.INSTAGRAMMABLE, CheerTagName.CLEAN_RESTROOM));

CheerResponse response = given()
.header(HttpHeaders.AUTHORIZATION, accessToken())
Expand All @@ -54,7 +62,11 @@ class RegisterCheer {
.statusCode(201)
.extract().as(CheerResponse.class);

assertThat(response.storeId()).isEqualTo(store.getId());
assertAll(
() -> assertThat(response.storeId()).isEqualTo(store.getId()),
() -> assertThat(response.cheerDescription()).isEqualTo(request.description()),
() -> assertThat(response.tags()).containsExactlyInAnyOrderElementsOf(request.tags())
);
}
}

Expand Down
16 changes: 11 additions & 5 deletions src/test/java/eatda/document/store/CheerDocumentTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import eatda.document.RestDocsRequest;
import eatda.document.RestDocsResponse;
import eatda.document.Tag;
import eatda.domain.cheer.CheerTagName;
import eatda.exception.BusinessErrorCode;
import eatda.exception.BusinessException;
import eatda.util.ImageUtils;
Expand Down Expand Up @@ -67,21 +68,25 @@ class RegisterCheer {
).requestBodyField("request",
fieldWithPath("storeKakaoId").type(STRING).description("가게 카카오 ID"),
fieldWithPath("storeName").type(STRING).description("가게 이름"),
fieldWithPath("description").type(STRING).description("응원 내용")
fieldWithPath("description").type(STRING).description("응원 내용"),
fieldWithPath("tags").type(ARRAY).description("응원 태그 목록")
);

RestDocsResponse responseDocument = response()
.responseBodyField(
fieldWithPath("storeId").type(NUMBER).description("가게 ID"),
fieldWithPath("cheerId").type(NUMBER).description("응원 ID"),
fieldWithPath("imageUrl").type(STRING).description("이미지 URL").optional(),
fieldWithPath("cheerDescription").type(STRING).description("응원 내용")
fieldWithPath("cheerDescription").type(STRING).description("응원 내용"),
fieldWithPath("tags").type(ARRAY).description("응원 태그 목록")
);

@Test
void 응원_등록_성공() {
CheerRegisterRequest request = new CheerRegisterRequest("123", "농민백암순대 본점", "너무 맛있어요!");
CheerResponse response = new CheerResponse(1L, 1L, "https://example.img", "너무 맛있어요!");
CheerRegisterRequest request = new CheerRegisterRequest("123", "농민백암순대 본점", "너무 맛있어요!",
List.of(CheerTagName.GOOD_FOR_DATING, CheerTagName.CLEAN_RESTROOM));
CheerResponse response = new CheerResponse(1L, 1L, "https://example.img", "너무 맛있어요!",
List.of(CheerTagName.GOOD_FOR_DATING, CheerTagName.CLEAN_RESTROOM));
doReturn(response).when(cheerService).registerCheer(eq(request), any(), any(), anyLong());

var document = document("cheer/register", 201)
Expand Down Expand Up @@ -110,7 +115,8 @@ class RegisterCheer {
"INVALID_CHEER_DESCRIPTION"})
@ParameterizedTest
void 응원_등록_실패(BusinessErrorCode errorCode) {
CheerRegisterRequest request = new CheerRegisterRequest("123", "농민백암순대 본점", "너무 맛있어요!");
CheerRegisterRequest request = new CheerRegisterRequest("123", "농민백암순대 본점", "너무 맛있어요!",
List.of(CheerTagName.GOOD_FOR_DATING, CheerTagName.CLEAN_RESTROOM));
doThrow(new BusinessException(errorCode))
.when(cheerService).registerCheer(eq(request), any(), any(), anyLong());

Expand Down
4 changes: 4 additions & 0 deletions src/test/java/eatda/service/BaseServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import eatda.fixture.MemberGenerator;
import eatda.fixture.StoreGenerator;
import eatda.repository.cheer.CheerRepository;
import eatda.repository.cheer.CheerTagRepository;
import eatda.repository.member.MemberRepository;
import eatda.repository.store.StoreRepository;
import eatda.repository.story.StoryRepository;
Expand Down Expand Up @@ -67,6 +68,9 @@ public abstract class BaseServiceTest {
@Autowired
protected CheerRepository cheerRepository;

@Autowired
protected CheerTagRepository cheerTagRepository;

@Autowired
protected StoryRepository storyRepository;

Expand Down
29 changes: 21 additions & 8 deletions src/test/java/eatda/service/cheer/CheerServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import eatda.controller.cheer.CheersResponse;
import eatda.domain.ImageKey;
import eatda.domain.cheer.Cheer;
import eatda.domain.cheer.CheerTagName;
import eatda.domain.member.Member;
import eatda.domain.store.District;
import eatda.domain.store.Store;
Expand All @@ -19,6 +20,7 @@
import eatda.exception.BusinessException;
import eatda.service.BaseServiceTest;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -41,7 +43,8 @@ class RegisterCheer {
cheerGenerator.generateCommon(member, store2);
cheerGenerator.generateCommon(member, store3);

CheerRegisterRequest request = new CheerRegisterRequest("123", "농민백암순대 본점", "추가 응원");
CheerRegisterRequest request = new CheerRegisterRequest("123", "농민백암순대 본점", "추가 응원",
List.of(CheerTagName.GOOD_FOR_DATING, CheerTagName.CLEAN_RESTROOM));
StoreSearchResult result = new StoreSearchResult(
"123", StoreCategory.KOREAN, "02-755-5232", "농민백암순대 본점", "http://place.map.kakao.com/123",
"서울시 강남구 역삼동 123-45", "서울시 강남구 역삼동 123-45", District.GANGNAM, 37.5665, 126.9780);
Expand All @@ -59,7 +62,8 @@ class RegisterCheer {
Store store = storeGenerator.generate("123", "서울시 강남구 역삼동 123-45");
cheerGenerator.generateCommon(member, store);

CheerRegisterRequest request = new CheerRegisterRequest("123", "농민백암순대 본점", "추가 응원");
CheerRegisterRequest request = new CheerRegisterRequest("123", "농민백암순대 본점", "추가 응원",
List.of(CheerTagName.GOOD_FOR_DATING, CheerTagName.CLEAN_RESTROOM));
StoreSearchResult result = new StoreSearchResult(
"123", StoreCategory.KOREAN, "02-755-5232", "농민백암순대 본점", "http://place.map.kakao.com/123",
"서울시 강남구 역삼동 123-45", "서울시 강남구 역삼동 123-45", District.GANGNAM, 37.5665, 126.9780);
Expand All @@ -75,7 +79,8 @@ class RegisterCheer {
void 해당_응원의_가게가_저장되어_있지_않다면_가게와_응원을_저장한다() {
Member member = memberGenerator.generate("123");

CheerRegisterRequest request = new CheerRegisterRequest("123", "농민백암순대 본점", "맛있어요!");
CheerRegisterRequest request = new CheerRegisterRequest("123", "농민백암순대 본점", "맛있어요!",
List.of(CheerTagName.GOOD_FOR_DATING, CheerTagName.CLEAN_RESTROOM));
StoreSearchResult result = new StoreSearchResult(
"123", StoreCategory.KOREAN, "02-755-5232", "농민백암순대 본점", "http://place.map.kakao.com/123",
"서울시 강남구 역삼동 123-45", "서울시 강남구 역삼동 123-45", District.GANGNAM, 37.5665, 126.9780);
Expand All @@ -87,7 +92,9 @@ class RegisterCheer {
assertAll(
() -> assertThat(response.storeId()).isEqualTo(foundStore.getId()),
() -> assertThat(response.cheerDescription()).isEqualTo("맛있어요!"),
() -> assertThat(response.imageUrl()).isNotNull()
() -> assertThat(response.imageUrl()).isNotNull(),
() -> assertThat(response.tags()).containsExactlyInAnyOrder(
CheerTagName.GOOD_FOR_DATING, CheerTagName.CLEAN_RESTROOM)
);
}

Expand All @@ -96,7 +103,8 @@ class RegisterCheer {
Member member = memberGenerator.generate("123");
Store store = storeGenerator.generate("123", "서울시 강남구 역삼동 123-45");

CheerRegisterRequest request = new CheerRegisterRequest("123", "농민백암순대 본점", "맛있어요!");
CheerRegisterRequest request = new CheerRegisterRequest("123", "농민백암순대 본점", "맛있어요!",
List.of(CheerTagName.GOOD_FOR_DATING, CheerTagName.CLEAN_RESTROOM));
StoreSearchResult result = new StoreSearchResult(
"123", StoreCategory.KOREAN, "02-755-5232", "농민백암순대 본점", "http://place.map.kakao.com/123",
"서울시 강남구 역삼동 123-45", "서울시 강남구 역삼동 123-45", District.GANGNAM, 37.5665, 126.9780);
Expand All @@ -109,15 +117,18 @@ class RegisterCheer {
() -> assertThat(foundStore.getId()).isEqualTo(store.getId()),
() -> assertThat(response.storeId()).isEqualTo(foundStore.getId()),
() -> assertThat(response.cheerDescription()).isEqualTo("맛있어요!"),
() -> assertThat(response.imageUrl()).isNotNull()
() -> assertThat(response.imageUrl()).isNotNull(),
() -> assertThat(response.tags()).containsExactlyInAnyOrder(
CheerTagName.GOOD_FOR_DATING, CheerTagName.CLEAN_RESTROOM)
);
}

@Test
void 해당_응원의_이미지가_비어있어도_응원을_저장할_수_있다() {
Member member = memberGenerator.generate("123");

CheerRegisterRequest request = new CheerRegisterRequest("123", "농민백암순대 본점", "맛있어요!");
CheerRegisterRequest request = new CheerRegisterRequest("123", "농민백암순대 본점", "맛있어요!",
List.of(CheerTagName.GOOD_FOR_DATING, CheerTagName.CLEAN_RESTROOM));
StoreSearchResult result = new StoreSearchResult(
"123", StoreCategory.KOREAN, "02-755-5232", "농민백암순대 본점", "http://place.map.kakao.com/123",
"서울시 강남구 역삼동 123-45", "서울시 강남구 역삼동 123-45", District.GANGNAM, 37.5665, 126.9780);
Expand All @@ -129,7 +140,9 @@ class RegisterCheer {
assertAll(
() -> assertThat(response.storeId()).isEqualTo(foundStore.getId()),
() -> assertThat(response.cheerDescription()).isEqualTo("맛있어요!"),
() -> assertThat(response.imageUrl()).isNull()
() -> assertThat(response.imageUrl()).isNull(),
() -> assertThat(response.tags()).containsExactlyInAnyOrder(
CheerTagName.GOOD_FOR_DATING, CheerTagName.CLEAN_RESTROOM)
);
}
}
Expand Down