From 126138dbc7a5895c149b34dd55987922ec3401ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:54:46 +0900 Subject: [PATCH 01/13] =?UTF-8?q?[Feature]=20-=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#421)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MyPageController.java | 4 +-- .../java/kr/touroot/member/domain/Member.java | 4 ++- .../dto/request/ProfileUpdateRequest.java | 5 ++- .../member/dto/response/ProfileResponse.java | 1 + .../touroot/member/service/MemberService.java | 12 ++++++- .../controller/MyPageControllerTest.java | 22 ++++++++++--- .../kr/touroot/member/domain/MemberTest.java | 6 ++-- .../member/service/MemberServiceTest.java | 31 ++++++++++++++++--- .../service/TravelPlanFacadeServiceTest.java | 6 +++- 9 files changed, 73 insertions(+), 18 deletions(-) diff --git a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java index 32ab4550b..d1ff6b989 100644 --- a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java +++ b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java @@ -24,7 +24,7 @@ import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -120,7 +120,7 @@ public ResponseEntity<Page<PlanResponse>> readTravelPlans( content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) ) }) - @PatchMapping("/profile") + @PutMapping("/profile") public ResponseEntity<ProfileResponse> updateProfile( @Valid @RequestBody ProfileUpdateRequest request, @NotNull MemberAuth memberAuth diff --git a/backend/src/main/java/kr/touroot/member/domain/Member.java b/backend/src/main/java/kr/touroot/member/domain/Member.java index 88566c6d3..bef12987c 100644 --- a/backend/src/main/java/kr/touroot/member/domain/Member.java +++ b/backend/src/main/java/kr/touroot/member/domain/Member.java @@ -131,8 +131,10 @@ private void validateProfileImageUrlForm(String profileImageUrl) { } } - public void changeNickname(String nickname) { + public void update(String nickname, String profileImageUrl) { validateNickname(nickname); + validateProfileImageUrl(profileImageUrl); this.nickname = nickname; + this.profileImageUrl = profileImageUrl; } } diff --git a/backend/src/main/java/kr/touroot/member/dto/request/ProfileUpdateRequest.java b/backend/src/main/java/kr/touroot/member/dto/request/ProfileUpdateRequest.java index 45926ed69..fc998bb06 100644 --- a/backend/src/main/java/kr/touroot/member/dto/request/ProfileUpdateRequest.java +++ b/backend/src/main/java/kr/touroot/member/dto/request/ProfileUpdateRequest.java @@ -6,6 +6,9 @@ public record ProfileUpdateRequest( @Schema(description = "사용자 닉네임", example = "아기뚜리") @NotBlank(message = "닉네임은 비어있을 수 없습니다.") - String nickname + String nickname, + @Schema(description = "사용자 프로필 사진 URL", example = "https://dev.touroot.kr/profile-image-ex.png") + @NotBlank(message = "프로필 사진 URL은 비어있을 수 없습니다.") + String profileImageUrl ) { } diff --git a/backend/src/main/java/kr/touroot/member/dto/response/ProfileResponse.java b/backend/src/main/java/kr/touroot/member/dto/response/ProfileResponse.java index c90b4ce2d..878c77e42 100644 --- a/backend/src/main/java/kr/touroot/member/dto/response/ProfileResponse.java +++ b/backend/src/main/java/kr/touroot/member/dto/response/ProfileResponse.java @@ -10,6 +10,7 @@ public static ProfileResponse from(Member member) { return ProfileResponse.builder() .profileImageUrl(member.getProfileImageUrl()) .nickname(member.getNickname()) + .profileImageUrl(member.getProfileImageUrl()) .build(); } } diff --git a/backend/src/main/java/kr/touroot/member/service/MemberService.java b/backend/src/main/java/kr/touroot/member/service/MemberService.java index ae81b01f7..b3414dc20 100644 --- a/backend/src/main/java/kr/touroot/member/service/MemberService.java +++ b/backend/src/main/java/kr/touroot/member/service/MemberService.java @@ -1,8 +1,10 @@ package kr.touroot.member.service; +import java.util.Objects; import kr.touroot.authentication.infrastructure.PasswordEncryptor; import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.BadRequestException; +import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; import kr.touroot.member.dto.request.MemberRequest; import kr.touroot.member.dto.request.ProfileUpdateRequest; @@ -18,6 +20,7 @@ public class MemberService { private final MemberRepository memberRepository; private final PasswordEncryptor passwordEncryptor; + private final AwsS3Provider s3Provider; @Transactional(readOnly = true) public Member getMemberById(Long memberId) { @@ -54,7 +57,14 @@ private void validateNicknameDuplication(String nickname) { @Transactional public ProfileResponse updateProfile(ProfileUpdateRequest request, MemberAuth memberAuth) { Member member = getMemberById(memberAuth.memberId()); - member.changeNickname(request.nickname()); + String requestProfileImageUrl = request.profileImageUrl(); + if (!Objects.equals(request.profileImageUrl(), member.getProfileImageUrl())) { + requestProfileImageUrl = s3Provider.copyImageToPermanentStorage(request.profileImageUrl()); + } + member.update(request.nickname(), requestProfileImageUrl); + +// Member member = getMemberById(memberAuth.memberId()); +// member.changeNickname(request.nickname()); return ProfileResponse.from(member); } diff --git a/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java index 4df72d07e..93051ab46 100644 --- a/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java +++ b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java @@ -4,8 +4,11 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; +import java.util.List; import kr.touroot.authentication.infrastructure.JwtTokenProvider; import kr.touroot.global.AcceptanceTest; +import kr.touroot.image.domain.ImageFile; +import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; import kr.touroot.member.dto.request.ProfileUpdateRequest; import kr.touroot.travelogue.helper.TravelogueTestHelper; @@ -17,6 +20,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; @DisplayName("마이 페이지 컨트롤러") @AcceptanceTest @@ -31,18 +36,21 @@ class MyPageControllerTest { private int port; private String accessToken; private Member member; + private final AwsS3Provider s3Provider; @Autowired public MyPageControllerTest( DatabaseCleaner databaseCleaner, JwtTokenProvider jwtTokenProvider, TravelogueTestHelper travelogueTestHelper, - TravelPlanTestHelper travelPlanTestHelper + TravelPlanTestHelper travelPlanTestHelper, + AwsS3Provider s3Provider ) { this.databaseCleaner = databaseCleaner; this.jwtTokenProvider = jwtTokenProvider; this.travelogueTestHelper = travelogueTestHelper; this.travelPlanTestHelper = travelPlanTestHelper; + this.s3Provider = s3Provider; } @BeforeEach @@ -98,7 +106,12 @@ void readTravelPlans() { void updateProfile() { // given String newNickname = "newNickname"; - ProfileUpdateRequest request = new ProfileUpdateRequest(newNickname); + MultipartFile multipartFile = new MockMultipartFile("file", "image.jpg", "image/jpeg", + "image content".getBytes()); + String newProfileImageUrl = s3Provider.uploadImages(List.of(new ImageFile(multipartFile))) + .get(0) + .replace("temporary", "images"); + ProfileUpdateRequest request = new ProfileUpdateRequest(newNickname, newProfileImageUrl); // when & then RestAssured.given().log().all() @@ -106,9 +119,10 @@ void updateProfile() { .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .body(request) .when().log().all() - .patch("/api/v1/member/me/profile") + .put("/api/v1/member/me/profile") .then().log().all() .statusCode(200) - .body("nickname", is(newNickname)); + .body("nickname", is(newNickname)) + .body("profileImageUrl", is(newProfileImageUrl)); } } diff --git a/backend/src/test/java/kr/touroot/member/domain/MemberTest.java b/backend/src/test/java/kr/touroot/member/domain/MemberTest.java index 26c1ca872..94254320a 100644 --- a/backend/src/test/java/kr/touroot/member/domain/MemberTest.java +++ b/backend/src/test/java/kr/touroot/member/domain/MemberTest.java @@ -94,7 +94,7 @@ void createMemberWithInvalidProfileImageUrl(String invalidProfileImageUrl) { @Test void changeNicknameWithValidData() { Member member = new Member(VALID_SOCIAl_ID, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL, KAKAO); - assertThatCode(() -> member.changeNickname(VALID_NICKNAME + "a")) + assertThatCode(() -> member.update(VALID_NICKNAME + "a", VALID_PROFILE_IMAGE_URL)) .doesNotThrowAnyException(); } @@ -103,7 +103,7 @@ void changeNicknameWithValidData() { @NullAndEmptySource() void changeNicknameWithOrEmpty(String nullOrEmptyNickname) { Member member = new Member(VALID_SOCIAl_ID, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL, KAKAO); - assertThatThrownBy(() -> member.changeNickname(nullOrEmptyNickname)) + assertThatThrownBy(() -> member.update(nullOrEmptyNickname, VALID_PROFILE_IMAGE_URL)) .isInstanceOf(BadRequestException.class) .hasMessage("닉네임은 비어 있을 수 없습니다"); } @@ -113,7 +113,7 @@ void changeNicknameWithOrEmpty(String nullOrEmptyNickname) { @ValueSource(strings = {"21-length-nicknameeee", "22-length-nicknameeeee"}) void changeNicknameWithInvalidLength(String invalidLengthNickname) { Member member = new Member(VALID_SOCIAl_ID, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL, KAKAO); - assertThatThrownBy(() -> member.changeNickname(invalidLengthNickname)) + assertThatThrownBy(() -> member.update(invalidLengthNickname, VALID_PROFILE_IMAGE_URL)) .isInstanceOf(BadRequestException.class) .hasMessage("닉네임은 1자 이상, 20자 이하여야 합니다"); } diff --git a/backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java b/backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java index b1a16b682..6faea26e5 100644 --- a/backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java +++ b/backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java @@ -4,11 +4,16 @@ import static kr.touroot.member.fixture.MemberRequestFixture.VALID_MEMBER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import java.util.List; import kr.touroot.authentication.infrastructure.PasswordEncryptor; import kr.touroot.global.ServiceTest; import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.global.config.EmbeddedS3Config; import kr.touroot.global.exception.BadRequestException; +import kr.touroot.image.domain.ImageFile; +import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; import kr.touroot.member.dto.request.MemberRequest; import kr.touroot.member.dto.request.ProfileUpdateRequest; @@ -19,25 +24,31 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; @DisplayName("사용자 서비스") -@Import(value = {MemberService.class, MemberTestHelper.class, PasswordEncryptor.class}) +@Import(value = {MemberService.class, MemberTestHelper.class, PasswordEncryptor.class, AwsS3Provider.class, + EmbeddedS3Config.class}) @ServiceTest class MemberServiceTest { private final MemberService memberService; private final MemberTestHelper testHelper; private final DatabaseCleaner databaseCleaner; + private final AwsS3Provider s3Provider; @Autowired public MemberServiceTest( MemberService memberService, MemberTestHelper testHelper, - DatabaseCleaner databaseCleaner + DatabaseCleaner databaseCleaner, + AwsS3Provider s3Provider ) { this.memberService = memberService; this.testHelper = testHelper; this.databaseCleaner = databaseCleaner; + this.s3Provider = s3Provider; } @BeforeEach @@ -99,11 +110,21 @@ void createMemberWithDuplicatedNickname() { void updateProfile() { Member member = testHelper.persistMember(); MemberAuth memberAuth = new MemberAuth(member.getId()); - ProfileUpdateRequest request = new ProfileUpdateRequest("newNickname"); + String newNickname = "newNickname"; + MultipartFile multipartFile = new MockMultipartFile("file", "image.jpg", "image/jpeg", + "image content".getBytes()); + String newProfileImageUrl = s3Provider.uploadImages(List.of(new ImageFile(multipartFile))) + .get(0) + .replace("temporary", "images"); + ProfileUpdateRequest request = new ProfileUpdateRequest(newNickname, newProfileImageUrl); memberService.updateProfile(request, memberAuth); - assertThat(memberService.getMemberById(member.getId()).getNickname()) - .isEqualTo("newNickname"); + assertAll( + () -> assertThat(memberService.getMemberById(member.getId()).getNickname()) + .isEqualTo(newNickname), + () -> assertThat(memberService.getMemberById(member.getId()).getProfileImageUrl()) + .isEqualTo(newProfileImageUrl) + ); } } diff --git a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanFacadeServiceTest.java index b0372da0e..655fda9ee 100644 --- a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanFacadeServiceTest.java @@ -9,7 +9,9 @@ import kr.touroot.authentication.infrastructure.PasswordEncryptor; import kr.touroot.global.ServiceTest; import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.global.config.EmbeddedS3Config; import kr.touroot.global.exception.BadRequestException; +import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; import kr.touroot.member.service.MemberService; import kr.touroot.travelplan.domain.TravelPlan; @@ -33,7 +35,9 @@ TravelPlanService.class, MemberService.class, PasswordEncryptor.class, - TravelPlanTestHelper.class + TravelPlanTestHelper.class, + AwsS3Provider.class, + EmbeddedS3Config.class }) @ServiceTest class TravelPlanFacadeServiceTest { From efeb765e7b64496cb679175fde9b0200a1f5531a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:41:55 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20OSIV=20=EC=98=B5=EC=85=98=20?= =?UTF-8?q?=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94=20(#532)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/application.yml | 4 ++++ backend/src/test/resources/application-test.yml | 1 + 2 files changed, 5 insertions(+) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index fb5121d8c..f551c4692 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -32,6 +32,10 @@ management: include: info, health, metrics base-path: ENC(zc5tP1eNIEjv3uN5Kuih7wlo5zILgWxq) enabled-by-default: false + +spring: + jpa: + open-in-view: false --- # local profile spring: diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml index 4436b5d28..bad48d5b4 100644 --- a/backend/src/test/resources/application-test.yml +++ b/backend/src/test/resources/application-test.yml @@ -39,6 +39,7 @@ spring: hibernate: ddl-auto: create defer-datasource-initialization: true + open-in-view: false sql: init: mode: never From 933fc438d20358a9235eb8f12957ee85c07f27ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Sun, 20 Oct 2024 21:04:21 +0900 Subject: [PATCH 03/13] =?UTF-8?q?[Feature]=20-=20=EB=82=98=EB=9D=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#544)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../travelogue/domain/TravelogueCountry.java | 47 ++++ .../travelogue/domain/search/CountryCode.java | 261 ++++++++++++++++++ .../travelogue/domain/search/SearchType.java | 2 +- .../dto/request/TraveloguePlaceRequest.java | 5 +- .../dto/request/TravelogueSearchRequest.java | 2 +- .../TravelogueCountryRepository.java | 15 + .../query/TravelogueQueryRepository.java | 3 + .../query/TravelogueQueryRepositoryImpl.java | 19 +- .../service/TravelogueCountryService.java | 52 ++++ .../service/TravelogueFacadeService.java | 4 + .../travelogue/service/TravelogueService.java | 9 + .../mysql/V6_add_travelogue_country.sql | 13 + .../domain/search/CountryCodeTest.java | 31 +++ .../fixture/TravelogueCountryFixture.java | 18 ++ .../fixture/TravelogueRequestFixture.java | 6 +- .../helper/TravelogueTestHelper.java | 16 +- .../service/TravelogueFacadeServiceTest.java | 14 + 17 files changed, 510 insertions(+), 7 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/travelogue/domain/TravelogueCountry.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/TravelogueCountryRepository.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/service/TravelogueCountryService.java create mode 100644 backend/src/main/resources/db/migration/mysql/V6_add_travelogue_country.sql create mode 100644 backend/src/test/java/kr/touroot/travelogue/domain/search/CountryCodeTest.java create mode 100644 backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueCountryFixture.java diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueCountry.java b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueCountry.java new file mode 100644 index 000000000..01913662c --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueCountry.java @@ -0,0 +1,47 @@ +package kr.touroot.travelogue.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import kr.touroot.travelogue.domain.search.CountryCode; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@EqualsAndHashCode(of = "id", callSuper = false) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Entity +public class TravelogueCountry { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Travelogue travelogue; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private CountryCode countryCode; + + @Column(nullable = false) + private Long count; + + public TravelogueCountry(Travelogue travelogue, CountryCode countryCode, Long count) { + this.travelogue = travelogue; + this.countryCode = countryCode; + this.count = count; + } +} diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java b/backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java new file mode 100644 index 000000000..6ec439a9a --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java @@ -0,0 +1,261 @@ +package kr.touroot.travelogue.domain.search; + +import java.util.Arrays; +import java.util.Set; +import kr.touroot.global.exception.BadRequestException; + +public enum CountryCode { + + AF(Set.of("아프가니스탄")), + AL(Set.of("알바니아")), + DZ(Set.of("알제리")), + AS(Set.of("아메리칸 사모아")), + AD(Set.of("안도라")), + AO(Set.of("앙골라")), + AI(Set.of("앵귈라")), + AQ(Set.of("남극")), + AG(Set.of("앤티가 바부다")), + AR(Set.of("아르헨티나")), + AM(Set.of("아르메니아")), + AW(Set.of("아루바")), + AU(Set.of("호주")), + AT(Set.of("오스트리아")), + AZ(Set.of("아제르바이잔")), + BS(Set.of("바하마")), + BH(Set.of("바레인")), + BD(Set.of("방글라데시")), + BB(Set.of("바베이도스")), + BY(Set.of("벨라루스")), + BE(Set.of("벨기에")), + BZ(Set.of("벨리즈")), + BJ(Set.of("베냉")), + BM(Set.of("버뮤다")), + BT(Set.of("부탄")), + BO(Set.of("볼리비아")), + BA(Set.of("보스니아 헤르체고비나")), + BW(Set.of("보츠와나")), + BV(Set.of("부베섬")), + BR(Set.of("브라질")), + IO(Set.of("영국령 인도양 지역")), + VG(Set.of("영국령 버진 아일랜드")), + BN(Set.of("브루나이")), + BG(Set.of("불가리아")), + BF(Set.of("부르키나파소")), + BI(Set.of("부룬디")), + KH(Set.of("캄보디아")), + CM(Set.of("카메룬")), + CA(Set.of("캐나다")), + CV(Set.of("카보베르데")), + KY(Set.of("케이맨 제도")), + CF(Set.of("중앙아프리카 공화국")), + TD(Set.of("차드")), + CL(Set.of("칠레")), + CN(Set.of("중국")), + CX(Set.of("크리스마스 섬")), + CC(Set.of("코코스 제도")), + CO(Set.of("콜롬비아")), + KM(Set.of("코모로")), + CD(Set.of("콩고 민주공화국")), + CG(Set.of("콩고 공화국")), + CK(Set.of("쿡 제도")), + CR(Set.of("코스타리카")), + CI(Set.of("코트디부아르")), + CU(Set.of("쿠바")), + CY(Set.of("키프로스")), + CZ(Set.of("체코")), + DK(Set.of("덴마크")), + DJ(Set.of("지부티")), + DM(Set.of("도미니카")), + DO(Set.of("도미니카 공화국")), + EC(Set.of("에콰도르")), + EG(Set.of("이집트")), + SV(Set.of("엘살바도르")), + GQ(Set.of("적도기니")), + ER(Set.of("에리트레아")), + EE(Set.of("에스토니아")), + ET(Set.of("에티오피아")), + FO(Set.of("페로 제도")), + FK(Set.of("포클랜드 제도")), + FJ(Set.of("피지")), + FI(Set.of("핀란드")), + FR(Set.of("프랑스")), + GF(Set.of("프랑스령 기아나")), + PF(Set.of("프랑스령 폴리네시아")), + TF(Set.of("프랑스령 남부 지역")), + GA(Set.of("가봉")), + GM(Set.of("감비아")), + GE(Set.of("조지아")), + DE(Set.of("독일")), + GH(Set.of("가나")), + GI(Set.of("지브롤터")), + GR(Set.of("그리스")), + GL(Set.of("그린란드")), + GD(Set.of("그레나다")), + GP(Set.of("과들루프")), + GU(Set.of("괌")), + GT(Set.of("과테말라")), + GN(Set.of("기니")), + GW(Set.of("기니비사우")), + GY(Set.of("가이아나")), + HT(Set.of("아이티")), + HM(Set.of("허드 맥도널드 제도")), + VA(Set.of("바티칸")), + HN(Set.of("온두라스")), + HK(Set.of("홍콩")), + HR(Set.of("크로아티아")), + HU(Set.of("헝가리")), + IS(Set.of("아이슬란드")), + IN(Set.of("인도")), + ID(Set.of("인도네시아")), + IR(Set.of("이란")), + IQ(Set.of("이라크")), + IE(Set.of("아일랜드")), + IL(Set.of("이스라엘")), + IT(Set.of("이탈리아")), + JM(Set.of("자메이카")), + JP(Set.of("일본")), + JO(Set.of("요르단")), + KZ(Set.of("카자흐스탄")), + KE(Set.of("케냐")), + KI(Set.of("키리바시")), + KP(Set.of("북한", "조선민주주의인민공화국")), + KR(Set.of("대한민국", "한국")), + KW(Set.of("쿠웨이트")), + KG(Set.of("키르기스스탄")), + LA(Set.of("라오스")), + LV(Set.of("라트비아")), + LB(Set.of("레바논")), + LS(Set.of("레소토")), + LR(Set.of("라이베리아")), + LY(Set.of("리비아")), + LI(Set.of("리히텐슈타인")), + LT(Set.of("리투아니아")), + LU(Set.of("룩셈부르크")), + MO(Set.of("마카오")), + MK(Set.of("북마케도니아")), + MG(Set.of("마다가스카르")), + MW(Set.of("말라위")), + MY(Set.of("말레이시아")), + MV(Set.of("몰디브")), + ML(Set.of("말리")), + MT(Set.of("몰타")), + MH(Set.of("마셜제도")), + MQ(Set.of("마르티니크")), + MR(Set.of("모리타니")), + MU(Set.of("모리셔스")), + YT(Set.of("마요트")), + MX(Set.of("멕시코")), + FM(Set.of("미크로네시아")), + MD(Set.of("몰도바")), + MC(Set.of("모나코")), + MN(Set.of("몽골")), + MS(Set.of("몬트세랫")), + MA(Set.of("모로코")), + MZ(Set.of("모잠비크")), + MM(Set.of("미얀마")), + NA(Set.of("나미비아")), + NR(Set.of("나우루")), + NP(Set.of("네팔")), + AN(Set.of("네덜란드 안틸레스")), + NL(Set.of("네덜란드")), + NC(Set.of("뉴칼레도니아")), + NZ(Set.of("뉴질랜드")), + NI(Set.of("니카라과")), + NE(Set.of("니제르")), + NG(Set.of("나이지리아")), + NU(Set.of("니우에")), + NF(Set.of("노퍽 섬")), + MP(Set.of("북마리아나 제도")), + NO(Set.of("노르웨이")), + OM(Set.of("오만")), + PK(Set.of("파키스탄")), + PW(Set.of("팔라우")), + PS(Set.of("팔레스타인")), + PA(Set.of("파나마")), + PG(Set.of("파푸아 뉴기니")), + PY(Set.of("파라과이")), + PE(Set.of("페루")), + PH(Set.of("필리핀")), + PN(Set.of("핏케언 제도")), + PL(Set.of("폴란드")), + PT(Set.of("포르투갈")), + PR(Set.of("푸에르토리코")), + QA(Set.of("카타르")), + RE(Set.of("레위니옹")), + RO(Set.of("루마니아")), + RU(Set.of("러시아")), + RW(Set.of("르완다")), + SH(Set.of("세인트헬레나")), + KN(Set.of("세인트키츠 네비스")), + LC(Set.of("세인트루시아")), + PM(Set.of("세인트피에르 미클롱")), + VC(Set.of("세인트빈센트 그레나딘")), + WS(Set.of("사모아")), + SM(Set.of("산마리노")), + ST(Set.of("상투메 프린시페")), + SA(Set.of("사우디아라비아")), + SN(Set.of("세네갈")), + CS(Set.of("세르비아 몬테네그로")), + SC(Set.of("세이셸")), + SL(Set.of("시에라리온")), + SG(Set.of("싱가포르")), + SK(Set.of("슬로바키아")), + SI(Set.of("슬로베니아")), + SB(Set.of("솔로몬 제도")), + SO(Set.of("소말리아")), + ZA(Set.of("남아프리카 공화국", "남아공")), + GS(Set.of("사우스조지아 사우스샌드위치 제도")), + ES(Set.of("스페인", "에스파냐")), + LK(Set.of("스리랑카")), + SD(Set.of("수단")), + SR(Set.of("수리남")), + SJ(Set.of("스발바르 얀마웬")), + SZ(Set.of("스와질란드", "에스와티니")), + SE(Set.of("스웨덴")), + CH(Set.of("스위스")), + SY(Set.of("시리아")), + TW(Set.of("대만", "타이완")), + TJ(Set.of("타지키스탄")), + TZ(Set.of("탄자니아")), + TH(Set.of("태국")), + TL(Set.of("동티모르")), + TG(Set.of("토고")), + TK(Set.of("토켈라우")), + TO(Set.of("통가")), + TT(Set.of("트리니다드 토바고")), + TN(Set.of("튀니지")), + TR(Set.of("터키", "튀르키예")), + TM(Set.of("투르크메니스탄")), + TC(Set.of("터크스 케이커스 제도")), + TV(Set.of("투발루")), + VI(Set.of("미국령 버진 아일랜드")), + UG(Set.of("우간다")), + UA(Set.of("우크라이나")), + AE(Set.of("아랍에미리트")), + GB(Set.of("영국")), + UM(Set.of("미국령 외곽 소섬")), + US(Set.of("미국")), + UY(Set.of("우루과이")), + UZ(Set.of("우즈베키스탄")), + VU(Set.of("바누아투")), + VE(Set.of("베네수엘라")), + VN(Set.of("베트남")), + WF(Set.of("왈리스 푸투나")), + EH(Set.of("서사하라")), + YE(Set.of("예멘")), + ZM(Set.of("잠비아")), + ZW(Set.of("짐바브웨")); + + private final Set<String> names; + + CountryCode(Set<String> names) { + this.names = names; + } + + public static CountryCode findByName(String name) { + return Arrays.stream(values()) + .filter(code -> code.names.contains(name)) + .findFirst() + .orElseThrow(() -> new BadRequestException("국가 이름을 찾을 수 없습니다.")); + } +} diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchType.java b/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchType.java index 299cbd76f..804bc6321 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchType.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchType.java @@ -3,7 +3,7 @@ import java.util.Arrays; public enum SearchType { - TITLE, AUTHOR; + TITLE, AUTHOR, COUNTRY; public static SearchType from(String searchType) { return Arrays.stream(SearchType.values()) diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java index ad48e8fea..8bd4c8988 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java @@ -26,7 +26,10 @@ public record TraveloguePlaceRequest( @NotNull(message = "여행기 장소 사진은 null일 수 없습니다.") @Size(message = "여행기 장소 사진은 최대 10개입니다.", max = 10) @Valid - List<TraveloguePhotoRequest> photoUrls + List<TraveloguePhotoRequest> photoUrls, + @Schema(description = "여행기 장소 국가 코드") + @NotBlank(message = "여행기 장소 국가 코드는 비어있을 수 없습니다.") + String countryCode ) { public TraveloguePlace toTraveloguePlace(int order, TravelogueDay travelogueDay) { diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueSearchRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueSearchRequest.java index 9017c06cc..c03c6003e 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueSearchRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueSearchRequest.java @@ -9,7 +9,7 @@ public record TravelogueSearchRequest( @NotBlank(message = "검색어는 2글자 이상이어야 합니다.") @Size(min = 2, message = "검색어는 2글자 이상이어야 합니다.") String keyword, - @Schema(description = "검색 키워드 종류 (TITLE, AUTHOR)", example = "TITLE") + @Schema(description = "검색 키워드 종류 (TITLE, AUTHOR, COUNTRY)", example = "TITLE") @NotBlank(message = "검색 키워드 종류는 필수입니다.") String searchType ) { diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueCountryRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueCountryRepository.java new file mode 100644 index 000000000..4a420ecbd --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueCountryRepository.java @@ -0,0 +1,15 @@ +package kr.touroot.travelogue.repository; + +import java.util.List; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueCountry; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TravelogueCountryRepository extends JpaRepository<TravelogueCountry, Long> { + + List<TravelogueCountry> findAllByTravelogue(Travelogue travelogue); + + void deleteAllByTravelogue(Travelogue travelogue); +} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepository.java index 7d4eae28d..853c9ca63 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepository.java @@ -2,6 +2,7 @@ import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueFilterCondition; +import kr.touroot.travelogue.domain.search.CountryCode; import kr.touroot.travelogue.domain.search.SearchCondition; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -10,5 +11,7 @@ public interface TravelogueQueryRepository { Page<Travelogue> findByKeywordAndSearchType(SearchCondition condition, Pageable pageable); + Page<Travelogue> findByKeywordAndCountryCode(CountryCode countryCode, Pageable pageable); + Page<Travelogue> findAllByFilter(TravelogueFilterCondition filter, Pageable pageable); } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java index 377fd2e5d..d47be6d55 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java @@ -1,17 +1,19 @@ package kr.touroot.travelogue.repository.query; import static kr.touroot.travelogue.domain.QTravelogue.travelogue; +import static kr.touroot.travelogue.domain.QTravelogueCountry.travelogueCountry; import static kr.touroot.travelogue.domain.QTravelogueTag.travelogueTag; import com.querydsl.core.types.Order; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.StringPath; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueFilterCondition; -import com.querydsl.core.types.dsl.StringPath; +import kr.touroot.travelogue.domain.search.CountryCode; import kr.touroot.travelogue.domain.search.SearchCondition; import kr.touroot.travelogue.domain.search.SearchType; import lombok.RequiredArgsConstructor; @@ -45,6 +47,21 @@ public Page<Travelogue> findByKeywordAndSearchType(SearchCondition condition, Pa return new PageImpl<>(results, pageable, results.size()); } + @Override + public Page<Travelogue> findByKeywordAndCountryCode(CountryCode countryCode, Pageable pageable) { + List<Travelogue> results = jpaQueryFactory.select(travelogue) + .from(travelogue) + .join(travelogueCountry) + .on(travelogue.id.eq(travelogueCountry.travelogue.id)) + .where(travelogueCountry.countryCode.eq(countryCode)) + .orderBy(travelogueCountry.count.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + return new PageImpl<>(results, pageable, results.size()); + } + private StringPath getTargetField(SearchType searchType) { if (SearchType.AUTHOR.equals(searchType)) { return travelogue.author.nickname; diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueCountryService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueCountryService.java new file mode 100644 index 000000000..e4fa860ed --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueCountryService.java @@ -0,0 +1,52 @@ +package kr.touroot.travelogue.service; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueCountry; +import kr.touroot.travelogue.domain.search.CountryCode; +import kr.touroot.travelogue.dto.request.TravelogueRequest; +import kr.touroot.travelogue.repository.TravelogueCountryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class TravelogueCountryService { + + private final TravelogueCountryRepository travelogueCountryRepository; + + @Transactional(readOnly = true) + public List<TravelogueCountry> readCountryByTravelogue(Travelogue travelogue) { + return travelogueCountryRepository.findAllByTravelogue(travelogue); + } + + @Transactional + public void createTravelogueCountries(Travelogue travelogue, TravelogueRequest request) { + Map<CountryCode, Long> countryCounts = countCountries(request); + + countryCounts.forEach((countryCode, count) -> travelogueCountryRepository.save( + new TravelogueCountry(travelogue, countryCode, count))); + } + + private Map<CountryCode, Long> countCountries(TravelogueRequest request) { + return request.days().stream() + .flatMap(day -> day.places().stream()) + .map(place -> CountryCode.valueOf(place.countryCode())) + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); + } + + @Transactional + public void updateTravelogueCountries(Travelogue travelogue, TravelogueRequest request) { + deleteAllByTravelogue(travelogue); + createTravelogueCountries(travelogue, request); + } + + @Transactional + public void deleteAllByTravelogue(Travelogue travelogue) { + travelogueCountryRepository.deleteAllByTravelogue(travelogue); + } +} diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java index 8c919b598..47ab457c5 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java @@ -28,6 +28,7 @@ public class TravelogueFacadeService { private final TravelogueImagePerpetuationService travelogueImagePerpetuationService; private final TravelogueTagService travelogueTagService; private final TravelogueLikeService travelogueLikeService; + private final TravelogueCountryService travelogueCountryService; private final MemberService memberService; @Transactional @@ -36,6 +37,7 @@ public TravelogueCreateResponse createTravelogue(MemberAuth member, TravelogueRe Travelogue travelogue = travelogueService.save(request.toTravelogue(author)); travelogueImagePerpetuationService.copyTravelogueImagesToPermanentStorage(travelogue); travelogueTagService.createTravelogueTags(travelogue, request.tags()); + travelogueCountryService.createTravelogueCountries(travelogue, request); return TravelogueCreateResponse.from(travelogue); } @@ -89,6 +91,7 @@ public TravelogueResponse updateTravelogue(Long id, MemberAuth member, Travelogu Travelogue updated = travelogueService.update(id, author, updateRequest); travelogueImagePerpetuationService.copyTravelogueImagesToPermanentStorage(updated); List<TravelogueTag> travelogueTags = travelogueTagService.updateTravelogueTag(updated, updateRequest.tags()); + travelogueCountryService.updateTravelogueCountries(updated, updateRequest); boolean isLikedFromAccessor = travelogueLikeService.existByTravelogueAndMember(updated, author); return TravelogueResponse.of(updated, travelogueTags, isLikedFromAccessor); @@ -101,6 +104,7 @@ public void deleteTravelogueById(Long id, MemberAuth member) { travelogueTagService.deleteAllByTravelogue(travelogue); travelogueLikeService.deleteAllByTravelogue(travelogue); + travelogueCountryService.deleteAllByTravelogue(travelogue); travelogueService.delete(travelogue, author); } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java index 4a42a7ef8..eb67a40be 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java @@ -5,6 +5,7 @@ import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueFilterCondition; +import kr.touroot.travelogue.domain.search.CountryCode; import kr.touroot.travelogue.domain.search.SearchCondition; import kr.touroot.travelogue.domain.search.SearchType; import kr.touroot.travelogue.dto.request.TravelogueRequest; @@ -43,11 +44,19 @@ public Page<Travelogue> findAllByMember(Member member, Pageable pageable) { @Transactional(readOnly = true) public Page<Travelogue> findByKeyword(TravelogueSearchRequest request, Pageable pageable) { SearchType searchType = SearchType.from(request.searchType()); + if (searchType == SearchType.COUNTRY) { + return findByKeywordAndCountryCode(request.keyword(), pageable); + } SearchCondition searchCondition = new SearchCondition(request.keyword(), searchType); return travelogueQueryRepository.findByKeywordAndSearchType(searchCondition, pageable); } + private Page<Travelogue> findByKeywordAndCountryCode(String keyword, Pageable pageable) { + CountryCode countryCode = CountryCode.findByName(keyword); + return travelogueQueryRepository.findByKeywordAndCountryCode(countryCode, pageable); + } + @Transactional(readOnly = true) public Page<Travelogue> findAllByFilter(TravelogueFilterCondition filter, Pageable pageable) { if (filter.isEmptyCondition()) { diff --git a/backend/src/main/resources/db/migration/mysql/V6_add_travelogue_country.sql b/backend/src/main/resources/db/migration/mysql/V6_add_travelogue_country.sql new file mode 100644 index 000000000..ea7607840 --- /dev/null +++ b/backend/src/main/resources/db/migration/mysql/V6_add_travelogue_country.sql @@ -0,0 +1,13 @@ +CREATE TABLE travelogue_country +( + id BIGINT NOT NULL AUTO_INCREMENT, + travelogue_id BIGINT NOT NULL, + country_code VARCHAR(50) NOT NULL, + count INT NOT NULL, + PRIMARY KEY (id) +); + +ALTER TABLE travelogue_country + ADD CONSTRAINT fk_travelogue_country_travelogue_id FOREIGN KEY (travelogue_id) REFERENCES travelogue (id); + +CREATE INDEX idx_country_code_count ON travelogue_country (country_code, count); diff --git a/backend/src/test/java/kr/touroot/travelogue/domain/search/CountryCodeTest.java b/backend/src/test/java/kr/touroot/travelogue/domain/search/CountryCodeTest.java new file mode 100644 index 000000000..6a268d0a3 --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelogue/domain/search/CountryCodeTest.java @@ -0,0 +1,31 @@ +package kr.touroot.travelogue.domain.search; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.touroot.global.exception.BadRequestException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class CountryCodeTest { + + @DisplayName("국가 이름으로 찾으면 국가 코드를 반환한다.") + @ValueSource(strings = {"한국", "대한민국"}) + @ParameterizedTest + void findByName(String name) { + CountryCode code = CountryCode.findByName(name); + + assertThat(code) + .isEqualTo(CountryCode.KR); + } + + @DisplayName("없는 나라 이름으로 찾으면 예외로 처리한다.") + @Test + void findByNonCountryName() { + assertThatThrownBy(() -> CountryCode.findByName("미역국")) + .isInstanceOf(BadRequestException.class) + .hasMessage("국가 이름을 찾을 수 없습니다."); + } +} diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueCountryFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueCountryFixture.java new file mode 100644 index 000000000..767b8fa56 --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueCountryFixture.java @@ -0,0 +1,18 @@ +package kr.touroot.travelogue.fixture; + +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueCountry; +import kr.touroot.travelogue.domain.search.CountryCode; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum TravelogueCountryFixture { + TRAVELOGUE_COUNTRY(CountryCode.KR, 1L); + + private final CountryCode countryCode; + private final Long count; + + public TravelogueCountry create(Travelogue travelogue) { + return new TravelogueCountry(travelogue, countryCode, count); + } +} diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java index f0b8f8871..d38b3bc86 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java @@ -54,7 +54,8 @@ public static List<TraveloguePlaceRequest> getTraveloguePlaceRequests(List<Trave "함덕 해수욕장", getTraveloguePositionRequest(), "에메랄드 빛 해변", - photos + photos, + "KR" )); } @@ -63,7 +64,8 @@ public static List<TraveloguePlaceRequest> getUpdateTraveloguePlaceRequests(List "함덕 해수욕장", getTraveloguePositionRequest(), "에메랄드 빛 해변은 해외 휴양지와 견줘도 밀리지 않습니다.", - photos + photos, + "KR" )); } diff --git a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index ccfd0411a..60a157a9d 100644 --- a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -1,5 +1,6 @@ package kr.touroot.travelogue.helper; +import static kr.touroot.travelogue.fixture.TravelogueCountryFixture.TRAVELOGUE_COUNTRY; import static kr.touroot.travelogue.fixture.TravelogueDayFixture.TRAVELOGUE_DAY; import static kr.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; import static kr.touroot.travelogue.fixture.TraveloguePhotoFixture.TRAVELOGUE_PHOTO; @@ -14,11 +15,13 @@ import kr.touroot.tag.fixture.TagFixture; import kr.touroot.tag.repository.TagRepository; import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueCountry; import kr.touroot.travelogue.domain.TravelogueDay; import kr.touroot.travelogue.domain.TravelogueLike; import kr.touroot.travelogue.domain.TraveloguePhoto; import kr.touroot.travelogue.domain.TraveloguePlace; import kr.touroot.travelogue.domain.TravelogueTag; +import kr.touroot.travelogue.repository.TravelogueCountryRepository; import kr.touroot.travelogue.repository.TravelogueDayRepository; import kr.touroot.travelogue.repository.TravelogueLikeRepository; import kr.touroot.travelogue.repository.TraveloguePhotoRepository; @@ -39,6 +42,7 @@ public class TravelogueTestHelper { private final TagRepository tagRepository; private final TravelogueTagRepository travelogueTagRepository; private final TravelogueLikeRepository travelogueLikeRepository; + private final TravelogueCountryRepository travelogueCountryRepository; @Autowired public TravelogueTestHelper( @@ -49,7 +53,8 @@ public TravelogueTestHelper( MemberRepository memberRepository, TagRepository tagRepository, TravelogueTagRepository travelogueTagRepository, - TravelogueLikeRepository travelogueLikeRepository + TravelogueLikeRepository travelogueLikeRepository, + TravelogueCountryRepository travelogueCountryRepository ) { this.travelogueRepository = travelogueRepository; this.travelogueDayRepository = travelogueDayRepository; @@ -59,6 +64,7 @@ public TravelogueTestHelper( this.tagRepository = tagRepository; this.travelogueTagRepository = travelogueTagRepository; this.travelogueLikeRepository = travelogueLikeRepository; + this.travelogueCountryRepository = travelogueCountryRepository; } public void initAllTravelogueTestData() { @@ -82,6 +88,7 @@ public Travelogue initTravelogueTestData(Member author) { Travelogue travelogue = persistTravelogue(author); TravelogueDay day = persistTravelogueDay(travelogue); TraveloguePlace place = persistTraveloguePlace(day); + persistTravelogueCountry(travelogue); persistTraveloguePhoto(place); return travelogue; @@ -103,6 +110,7 @@ public Travelogue initTravelogueTestDataWithTag(Member author) { Travelogue travelogue = persistTravelogue(author); TravelogueDay day = persistTravelogueDay(travelogue); TraveloguePlace place = persistTraveloguePlace(day); + persistTravelogueCountry(travelogue); persistTraveloguePhoto(place); persisTravelogueTag(travelogue, TagFixture.TAG_1.get()); @@ -156,6 +164,12 @@ public TraveloguePlace persistTraveloguePlace(TravelogueDay day) { return traveloguePlaceRepository.save(place); } + public TravelogueCountry persistTravelogueCountry(Travelogue travelogue) { + TravelogueCountry travelogueCountry = TRAVELOGUE_COUNTRY.create(travelogue); + + return travelogueCountryRepository.save(travelogueCountry); + } + public TraveloguePhoto persistTraveloguePhoto(TraveloguePlace place) { TraveloguePhoto photo = TRAVELOGUE_PHOTO.create(place); diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index ba0c8820c..99ae5bb92 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -48,6 +48,7 @@ TravelogueImagePerpetuationService.class, TravelogueTagService.class, TravelogueLikeService.class, + TravelogueCountryService.class, MemberService.class, TravelogueTestHelper.class, PasswordEncryptor.class, @@ -197,6 +198,19 @@ void findTraveloguesByAuthorNicknameKeyword() { assertThat(searchResults).containsAll(responses); } + @DisplayName("국가 코드를 기반으로 여행기 목록을 조회한다.") + @Test + void findTraveloguesByCountryCodeKeyword() { + testHelper.initAllTravelogueTestData(); + Page<TravelogueSimpleResponse> responses = TravelogueResponseFixture.getTravelogueSimpleResponses(); + + TravelogueSearchRequest searchRequest = new TravelogueSearchRequest("한국", "country"); + PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("id")); + Page<TravelogueSimpleResponse> searchResults = service.findSimpleTravelogues(searchRequest, pageRequest); + + assertThat(searchResults).containsAll(responses); + } + @DisplayName("여행기를 수정할 수 있다.") @Test void updateTravelogue() { From 73258c670a9f1680ecdd8a8c17061a83d4b99874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:04:33 +0900 Subject: [PATCH 04/13] =?UTF-8?q?[Fix]=20-=20flyway=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EA=B7=9C=EC=B9=99=EC=9D=B4=20=EB=A7=9E=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EC=95=84=20=EC=8B=A4=ED=96=89=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ..._add_travelogue_country.sql => V6__add_travelogue_country.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/src/main/resources/db/migration/mysql/{V6_add_travelogue_country.sql => V6__add_travelogue_country.sql} (100%) diff --git a/backend/src/main/resources/db/migration/mysql/V6_add_travelogue_country.sql b/backend/src/main/resources/db/migration/mysql/V6__add_travelogue_country.sql similarity index 100% rename from backend/src/main/resources/db/migration/mysql/V6_add_travelogue_country.sql rename to backend/src/main/resources/db/migration/mysql/V6__add_travelogue_country.sql From c0d4c0beb3e16b64b7d6c82794f16d31d0256b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:26:27 +0900 Subject: [PATCH 05/13] =?UTF-8?q?[Fix]=20-=20=EC=8A=A4=ED=82=A4=EB=A7=88?= =?UTF-8?q?=EC=99=80=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EC=9D=98=20=EC=BB=AC=EB=9F=BC=20=ED=83=80=EC=9E=85=20=EB=B6=88?= =?UTF-8?q?=EC=9D=BC=EC=B9=98=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=ED=95=B4=EA=B2=B0=20(#549)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/touroot/travelogue/domain/TravelogueCountry.java | 4 ++-- .../touroot/travelogue/service/TravelogueCountryService.java | 2 +- .../touroot/travelogue/fixture/TravelogueCountryFixture.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueCountry.java b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueCountry.java index 01913662c..503e0b60c 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueCountry.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueCountry.java @@ -37,9 +37,9 @@ public class TravelogueCountry { private CountryCode countryCode; @Column(nullable = false) - private Long count; + private Integer count; - public TravelogueCountry(Travelogue travelogue, CountryCode countryCode, Long count) { + public TravelogueCountry(Travelogue travelogue, CountryCode countryCode, Integer count) { this.travelogue = travelogue; this.countryCode = countryCode; this.count = count; diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueCountryService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueCountryService.java index e4fa860ed..62b6de825 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueCountryService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueCountryService.java @@ -29,7 +29,7 @@ public void createTravelogueCountries(Travelogue travelogue, TravelogueRequest r Map<CountryCode, Long> countryCounts = countCountries(request); countryCounts.forEach((countryCode, count) -> travelogueCountryRepository.save( - new TravelogueCountry(travelogue, countryCode, count))); + new TravelogueCountry(travelogue, countryCode, count.intValue()))); } private Map<CountryCode, Long> countCountries(TravelogueRequest request) { diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueCountryFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueCountryFixture.java index 767b8fa56..adc7b8d30 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueCountryFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueCountryFixture.java @@ -13,6 +13,6 @@ public enum TravelogueCountryFixture { private final Long count; public TravelogueCountry create(Travelogue travelogue) { - return new TravelogueCountry(travelogue, countryCode, count); + return new TravelogueCountry(travelogue, countryCode, count.intValue()); } } From ff555a3a6789dee40151ef338e03e1ed3cf76e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:31:23 +0900 Subject: [PATCH 06/13] =?UTF-8?q?[Feature]=20-=20=EC=9E=90=EC=8B=A0?= =?UTF-8?q?=EC=9D=B4=20=EC=A2=8B=EC=95=84=EC=9A=94=EB=A5=BC=20=EB=88=84?= =?UTF-8?q?=EB=A5=B8=20=EA=B2=8C=EC=8B=9C=EA=B8=80=EC=9D=84=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#540)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 내가 좋아요 한 여행기 조회 기능 구현 * test: 내가 좋아요 한 여행기 조회 테스트 코드 추가 * refactor: 다른 dto 와 형식 통일 --- .../member/controller/MyPageController.java | 25 +++++++++++++++ .../response/MyLikeTravelogueResponse.java | 31 +++++++++++++++++++ .../member/service/MyPageFacadeService.java | 13 ++++++++ .../repository/TravelogueLikeRepository.java | 4 +++ .../service/TravelogueLikeService.java | 7 +++++ .../controller/MyPageControllerTest.java | 19 ++++++++++++ .../service/TravelogueLikeServiceTest.java | 17 ++++++++++ 7 files changed, 116 insertions(+) create mode 100644 backend/src/main/java/kr/touroot/member/dto/response/MyLikeTravelogueResponse.java diff --git a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java index d1ff6b989..6ea08a839 100644 --- a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java +++ b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java @@ -12,6 +12,7 @@ import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.dto.ExceptionResponse; import kr.touroot.member.dto.request.ProfileUpdateRequest; +import kr.touroot.member.dto.response.MyLikeTravelogueResponse; import kr.touroot.member.dto.response.MyTravelogueResponse; import kr.touroot.member.dto.response.ProfileResponse; import kr.touroot.member.service.MyPageFacadeService; @@ -103,6 +104,30 @@ public ResponseEntity<Page<PlanResponse>> readTravelPlans( return ResponseEntity.ok(data); } + @Operation(summary = "내가 좋아요 한 여행기 조회") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "내가 좋아요 한 여행기 조회에 성공했을 때" + ), + @ApiResponse( + responseCode = "401", + description = "로그인하지 않은 사용자가 조회를 시도할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + @PageableAsQueryParam + @GetMapping("/likes") + public ResponseEntity<Page<MyLikeTravelogueResponse>> readLikes( + @NotNull MemberAuth memberAuth, + @Parameter(hidden = true) + @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC) + Pageable pageable + ) { + Page<MyLikeTravelogueResponse> data = myPageFacadeService.readLikes(memberAuth, pageable); + return ResponseEntity.ok(data); + } + @Operation(summary = "나의 프로필 정보 수정") @ApiResponses(value = { @ApiResponse( diff --git a/backend/src/main/java/kr/touroot/member/dto/response/MyLikeTravelogueResponse.java b/backend/src/main/java/kr/touroot/member/dto/response/MyLikeTravelogueResponse.java new file mode 100644 index 000000000..a3840e0e9 --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/dto/response/MyLikeTravelogueResponse.java @@ -0,0 +1,31 @@ +package kr.touroot.member.dto.response; + +import java.time.format.DateTimeFormatter; +import kr.touroot.travelogue.domain.Travelogue; +import lombok.Builder; + +@Builder +public record MyLikeTravelogueResponse( + long id, + String title, + String thumbnailUrl, + String createdAt, + String authorName, + String authorProfileImageUrl +) { + + public static MyLikeTravelogueResponse from(Travelogue travelogue) { + String createdAt = travelogue.getCreatedAt() + .toLocalDate() + .format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); + + return MyLikeTravelogueResponse.builder() + .id(travelogue.getId()) + .title(travelogue.getTitle()) + .thumbnailUrl(travelogue.getThumbnail()) + .createdAt(createdAt) + .authorName(travelogue.getAuthorNickname()) + .authorProfileImageUrl(travelogue.getAuthorProfileImageUrl()) + .build(); + } +} diff --git a/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java b/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java index f495395f3..5644377ca 100644 --- a/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java +++ b/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java @@ -3,9 +3,12 @@ import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.member.domain.Member; import kr.touroot.member.dto.request.ProfileUpdateRequest; +import kr.touroot.member.dto.response.MyLikeTravelogueResponse; import kr.touroot.member.dto.response.MyTravelogueResponse; import kr.touroot.member.dto.response.ProfileResponse; import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueLike; +import kr.touroot.travelogue.service.TravelogueLikeService; import kr.touroot.travelogue.service.TravelogueService; import kr.touroot.travelplan.domain.TravelPlan; import kr.touroot.travelplan.dto.response.PlanResponse; @@ -23,6 +26,7 @@ public class MyPageFacadeService { private final MemberService memberService; private final TravelogueService travelogueService; private final TravelPlanService travelPlanService; + private final TravelogueLikeService travelogueLikeService; @Transactional(readOnly = true) public ProfileResponse readProfile(MemberAuth memberAuth) { @@ -46,6 +50,15 @@ public Page<PlanResponse> readTravelPlans(MemberAuth memberAuth, Pageable pageab return travelPlans.map(PlanResponse::from); } + @Transactional(readOnly = true) + public Page<MyLikeTravelogueResponse> readLikes(MemberAuth memberAuth, Pageable pageable) { + Member member = memberService.getMemberById(memberAuth.memberId()); + + return travelogueLikeService.findByLiker(member, pageable) + .map(TravelogueLike::getTravelogue) + .map(MyLikeTravelogueResponse::from); + } + @Transactional public ProfileResponse updateProfile(ProfileUpdateRequest request, MemberAuth memberAuth) { return memberService.updateProfile(request, memberAuth); diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueLikeRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueLikeRepository.java index 52df6652c..31aa2f1aa 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueLikeRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueLikeRepository.java @@ -3,10 +3,14 @@ import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueLike; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface TravelogueLikeRepository extends JpaRepository<TravelogueLike, Long> { + Page<TravelogueLike> findAllByLiker(Member liker, Pageable pageable); + boolean existsByTravelogueAndLiker(Travelogue travelogue, Member liker); void deleteAllByTravelogue(Travelogue travelogue); diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueLikeService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueLikeService.java index a5f6eea85..82b193c8b 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueLikeService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueLikeService.java @@ -5,6 +5,8 @@ import kr.touroot.travelogue.domain.TravelogueLike; import kr.touroot.travelogue.repository.TravelogueLikeRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,6 +16,11 @@ public class TravelogueLikeService { private final TravelogueLikeRepository travelogueLikeRepository; + @Transactional(readOnly = true) + public Page<TravelogueLike> findByLiker(Member liker, Pageable pageable) { + return travelogueLikeRepository.findAllByLiker(liker, pageable); + } + @Transactional(readOnly = true) public boolean existByTravelogueAndMember(Travelogue travelogue, Member liker) { return travelogueLikeRepository.existsByTravelogueAndLiker(travelogue, liker); diff --git a/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java index 93051ab46..3676ead71 100644 --- a/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java +++ b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java @@ -101,6 +101,25 @@ void readTravelPlans() { .body("content.size()", is(2)); } + @DisplayName("마이 페이지 컨트롤러는 내가 좋아요 한 여행기 조회 시 요청이 들어오면 로그인한 사용자의 여행 계획을 조회한다.") + @Test + void readLikeTravelogues() { + // given + travelogueTestHelper.initTravelogueTestDataWithLike(member); + travelogueTestHelper.initTravelogueTestDataWithLike(member); + travelogueTestHelper.initTravelogueTestData(); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().log().all() + .get("/api/v1/member/me/likes") + .then().log().all() + .statusCode(200) + .body("content.size()", is(2)); + } + @DisplayName("마이 페이지 컨트롤러는 내 프로필 수정 요청이 들어오면 로그인한 사용자의 프로필을 수정한다.") @Test void updateProfile() { diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueLikeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueLikeServiceTest.java index f1dd162b0..509082643 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueLikeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueLikeServiceTest.java @@ -12,12 +12,15 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Pageable; @DisplayName("여행기 좋아요 서비스") @Import(value = {TravelogueLikeService.class, TravelogueTestHelper.class}) @ServiceTest class TravelogueLikeServiceTest { + public static final int BASIC_PAGE_SIZE = 5; + private final TravelogueLikeService travelogueLikeService; private final DatabaseCleaner databaseCleaner; private final TravelogueTestHelper testHelper; @@ -38,6 +41,20 @@ void setUp() { databaseCleaner.executeTruncate(); } + @DisplayName("특정 멤버가 좋아요 한 여행기를 조회할 수 있다.") + @Test + void findByLiker() { + // given + Member liker = testHelper.initKakaoMemberTestData(); + testHelper.initTravelogueTestDataWithLike(liker); + testHelper.initTravelogueTestDataWithLike(liker); + testHelper.initTravelogueTestData(); + + // when & then + assertThat(travelogueLikeService.findByLiker(liker, Pageable.ofSize(BASIC_PAGE_SIZE))) + .hasSize(2); + } + @DisplayName("특정 여행기에 특정 멤버가 좋아요 했는지 알 수 있다") @Test void existByTravelogueAndMember() { From 755c5c80b009ee405bd85ba4fa366fcd5b94ad34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:16:12 +0900 Subject: [PATCH 07/13] =?UTF-8?q?[Fix]=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20=EB=B9=88=20=EA=B0=92=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=82=AD=EC=A0=9C=20(#542)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/kr/touroot/member/domain/Member.java | 8 +++++++- .../member/dto/request/ProfileUpdateRequest.java | 3 ++- .../java/kr/touroot/member/service/MemberService.java | 10 ++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/kr/touroot/member/domain/Member.java b/backend/src/main/java/kr/touroot/member/domain/Member.java index bef12987c..7de4c5de8 100644 --- a/backend/src/main/java/kr/touroot/member/domain/Member.java +++ b/backend/src/main/java/kr/touroot/member/domain/Member.java @@ -131,9 +131,15 @@ private void validateProfileImageUrlForm(String profileImageUrl) { } } + private void validateProfileImageUrlNotNull(String profileImageUrl) { + if (profileImageUrl == null) { + throw new BadRequestException("프로필 이미지는 비어 있을 수 없습니다"); + } + } + public void update(String nickname, String profileImageUrl) { validateNickname(nickname); - validateProfileImageUrl(profileImageUrl); + validateProfileImageUrlNotNull(profileImageUrl); this.nickname = nickname; this.profileImageUrl = profileImageUrl; } diff --git a/backend/src/main/java/kr/touroot/member/dto/request/ProfileUpdateRequest.java b/backend/src/main/java/kr/touroot/member/dto/request/ProfileUpdateRequest.java index fc998bb06..280bd4d8b 100644 --- a/backend/src/main/java/kr/touroot/member/dto/request/ProfileUpdateRequest.java +++ b/backend/src/main/java/kr/touroot/member/dto/request/ProfileUpdateRequest.java @@ -2,13 +2,14 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; public record ProfileUpdateRequest( @Schema(description = "사용자 닉네임", example = "아기뚜리") @NotBlank(message = "닉네임은 비어있을 수 없습니다.") String nickname, @Schema(description = "사용자 프로필 사진 URL", example = "https://dev.touroot.kr/profile-image-ex.png") - @NotBlank(message = "프로필 사진 URL은 비어있을 수 없습니다.") + @NotNull(message = "프로필 사진 URL은 null 값일 수 없습니다.") String profileImageUrl ) { } diff --git a/backend/src/main/java/kr/touroot/member/service/MemberService.java b/backend/src/main/java/kr/touroot/member/service/MemberService.java index b3414dc20..b2e455308 100644 --- a/backend/src/main/java/kr/touroot/member/service/MemberService.java +++ b/backend/src/main/java/kr/touroot/member/service/MemberService.java @@ -58,14 +58,16 @@ private void validateNicknameDuplication(String nickname) { public ProfileResponse updateProfile(ProfileUpdateRequest request, MemberAuth memberAuth) { Member member = getMemberById(memberAuth.memberId()); String requestProfileImageUrl = request.profileImageUrl(); - if (!Objects.equals(request.profileImageUrl(), member.getProfileImageUrl())) { + if (isUpdatable(requestProfileImageUrl, member)) { requestProfileImageUrl = s3Provider.copyImageToPermanentStorage(request.profileImageUrl()); } member.update(request.nickname(), requestProfileImageUrl); -// Member member = getMemberById(memberAuth.memberId()); -// member.changeNickname(request.nickname()); - return ProfileResponse.from(member); } + + private boolean isUpdatable(String requestProfileImageUrl, Member member) { + return !requestProfileImageUrl.isEmpty() && !Objects.equals(requestProfileImageUrl, + member.getProfileImageUrl()); + } } From bccfe51166a5bdacac005288b17f483d3bd6c1d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Mon, 21 Oct 2024 20:01:12 +0900 Subject: [PATCH 08/13] =?UTF-8?q?[Feature]=20-=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EB=B6=84=EA=B8=B0=20=EA=B3=84=EC=B8=B5=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../travelogue/domain/search/CountryCode.java | 4 +-- .../query/TravelogueQueryRepository.java | 3 -- .../query/TravelogueQueryRepositoryImpl.java | 32 ++++++++++--------- .../travelogue/service/TravelogueService.java | 9 ------ .../domain/search/CountryCodeTest.java | 11 +++---- 5 files changed, 24 insertions(+), 35 deletions(-) diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java b/backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java index 6ec439a9a..a20793d71 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java @@ -2,10 +2,10 @@ import java.util.Arrays; import java.util.Set; -import kr.touroot.global.exception.BadRequestException; public enum CountryCode { + NONE(Set.of()), AF(Set.of("아프가니스탄")), AL(Set.of("알바니아")), DZ(Set.of("알제리")), @@ -256,6 +256,6 @@ public static CountryCode findByName(String name) { return Arrays.stream(values()) .filter(code -> code.names.contains(name)) .findFirst() - .orElseThrow(() -> new BadRequestException("국가 이름을 찾을 수 없습니다.")); + .orElse(NONE); } } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepository.java index 853c9ca63..7d4eae28d 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepository.java @@ -2,7 +2,6 @@ import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueFilterCondition; -import kr.touroot.travelogue.domain.search.CountryCode; import kr.touroot.travelogue.domain.search.SearchCondition; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -11,7 +10,5 @@ public interface TravelogueQueryRepository { Page<Travelogue> findByKeywordAndSearchType(SearchCondition condition, Pageable pageable); - Page<Travelogue> findByKeywordAndCountryCode(CountryCode countryCode, Pageable pageable); - Page<Travelogue> findAllByFilter(TravelogueFilterCondition filter, Pageable pageable); } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java index d47be6d55..2b22ab225 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java @@ -36,30 +36,32 @@ public class TravelogueQueryRepositoryImpl implements TravelogueQueryRepository @Override public Page<Travelogue> findByKeywordAndSearchType(SearchCondition condition, Pageable pageable) { String keyword = condition.getKeyword(); - List<Travelogue> results = jpaQueryFactory.selectFrom(travelogue) - .where(Expressions.stringTemplate(TEMPLATE, getTargetField(condition.getSearchType())) - .containsIgnoreCase(keyword.replace(BLANK, EMPTY))) - .orderBy(travelogue.id.desc()) - .offset(pageable.getOffset()) + JPAQuery<Travelogue> query = jpaQueryFactory.selectFrom(travelogue); + if (condition.getSearchType() == SearchType.COUNTRY) { + CountryCode countryCode = CountryCode.findByName(keyword); + findByCountryCode(query, countryCode); + } + if (condition.getSearchType() == SearchType.AUTHOR || condition.getSearchType() == SearchType.TITLE) { + findByTitleOrAuthor(condition, query, keyword); + } + List<Travelogue> results = query.offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); return new PageImpl<>(results, pageable, results.size()); } - @Override - public Page<Travelogue> findByKeywordAndCountryCode(CountryCode countryCode, Pageable pageable) { - List<Travelogue> results = jpaQueryFactory.select(travelogue) - .from(travelogue) - .join(travelogueCountry) + private void findByCountryCode(JPAQuery<Travelogue> query, CountryCode countryCode) { + query.join(travelogueCountry) .on(travelogue.id.eq(travelogueCountry.travelogue.id)) .where(travelogueCountry.countryCode.eq(countryCode)) - .orderBy(travelogueCountry.count.desc()) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); + .orderBy(travelogueCountry.count.desc()); + } - return new PageImpl<>(results, pageable, results.size()); + private void findByTitleOrAuthor(SearchCondition condition, JPAQuery<Travelogue> query, String keyword) { + query.where(Expressions.stringTemplate(TEMPLATE, getTargetField(condition.getSearchType())) + .containsIgnoreCase(keyword.replace(BLANK, EMPTY))) + .orderBy(travelogue.id.desc()); } private StringPath getTargetField(SearchType searchType) { diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java index eb67a40be..4a42a7ef8 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java @@ -5,7 +5,6 @@ import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueFilterCondition; -import kr.touroot.travelogue.domain.search.CountryCode; import kr.touroot.travelogue.domain.search.SearchCondition; import kr.touroot.travelogue.domain.search.SearchType; import kr.touroot.travelogue.dto.request.TravelogueRequest; @@ -44,19 +43,11 @@ public Page<Travelogue> findAllByMember(Member member, Pageable pageable) { @Transactional(readOnly = true) public Page<Travelogue> findByKeyword(TravelogueSearchRequest request, Pageable pageable) { SearchType searchType = SearchType.from(request.searchType()); - if (searchType == SearchType.COUNTRY) { - return findByKeywordAndCountryCode(request.keyword(), pageable); - } SearchCondition searchCondition = new SearchCondition(request.keyword(), searchType); return travelogueQueryRepository.findByKeywordAndSearchType(searchCondition, pageable); } - private Page<Travelogue> findByKeywordAndCountryCode(String keyword, Pageable pageable) { - CountryCode countryCode = CountryCode.findByName(keyword); - return travelogueQueryRepository.findByKeywordAndCountryCode(countryCode, pageable); - } - @Transactional(readOnly = true) public Page<Travelogue> findAllByFilter(TravelogueFilterCondition filter, Pageable pageable) { if (filter.isEmptyCondition()) { diff --git a/backend/src/test/java/kr/touroot/travelogue/domain/search/CountryCodeTest.java b/backend/src/test/java/kr/touroot/travelogue/domain/search/CountryCodeTest.java index 6a268d0a3..3f4825b4e 100644 --- a/backend/src/test/java/kr/touroot/travelogue/domain/search/CountryCodeTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/domain/search/CountryCodeTest.java @@ -1,9 +1,7 @@ package kr.touroot.travelogue.domain.search; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import kr.touroot.global.exception.BadRequestException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -21,11 +19,12 @@ void findByName(String name) { .isEqualTo(CountryCode.KR); } - @DisplayName("없는 나라 이름으로 찾으면 예외로 처리한다.") + @DisplayName("없는 나라 이름으로 찾으면 NONE을 반환한다.") @Test void findByNonCountryName() { - assertThatThrownBy(() -> CountryCode.findByName("미역국")) - .isInstanceOf(BadRequestException.class) - .hasMessage("국가 이름을 찾을 수 없습니다."); + CountryCode code = CountryCode.findByName("미역국"); + + assertThat(code) + .isEqualTo(CountryCode.NONE); } } From cee8fd5a2a3a7e4771f7fd880cbc48583446625c Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Tue, 22 Oct 2024 12:55:15 +0900 Subject: [PATCH 09/13] =?UTF-8?q?[Feature]=20-=20=EA=B2=80=EC=83=89=20+=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=EB=A7=81=20API=20=ED=86=B5=ED=95=A9=20(#557)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 검색 및 필터링 API 통합 * refactor: 더 이상 사용하지 않는 filtering 관련 메서드 제거 * refactor: TravelogueQueryRepositoryImpl 메서드 순서 정리 및 TODO 추가 * refactor: TravelogueQueryRepositoryImpl 메서드 순서 정리 및 검색 API 삭제 TODO 추가 * test: 검색 테스트 필터와 검색 통합된 메서드로 변경 --- .../controller/TravelogueController.java | 13 +++- .../domain/search/SearchCondition.java | 4 ++ .../dto/request/TravelogueSearchRequest.java | 17 ++++- .../query/TravelogueQueryRepository.java | 4 +- .../query/TravelogueQueryRepositoryImpl.java | 63 +++++++++++++------ .../service/TravelogueFacadeService.java | 6 +- .../travelogue/service/TravelogueService.java | 13 ++-- .../controller/TravelogueControllerTest.java | 8 ++- .../service/TravelogueFacadeServiceTest.java | 39 ++++++++++-- .../service/TravelogueServiceTest.java | 34 +++++++--- 10 files changed, 155 insertions(+), 46 deletions(-) diff --git a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java index f9ed4e6d6..9a43c0cc2 100644 --- a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java +++ b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java @@ -141,11 +141,20 @@ public ResponseEntity<Page<TravelogueSimpleResponse>> findMainPageTravelogues( @Parameter(hidden = true) @PageableDefault(size = 5, sort = "id", direction = Direction.DESC) Pageable pageable, - TravelogueFilterRequest filter + TravelogueFilterRequest filterRequest, + @Valid + TravelogueSearchRequest searchRequest + ) { - return ResponseEntity.ok(travelogueFacadeService.findSimpleTravelogues(filter, pageable)); + Page<TravelogueSimpleResponse> data = travelogueFacadeService.findSimpleTravelogues( + filterRequest, + searchRequest, + pageable + ); + return ResponseEntity.ok(data); } + // TODO: 프론트엔드 엔드포인트 이전 작업 완료 후 제거 @Operation(summary = "여행기 검색") @ApiResponses(value = { @ApiResponse( diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchCondition.java b/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchCondition.java index e8fe8fd4e..af30d3794 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchCondition.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchCondition.java @@ -12,4 +12,8 @@ public SearchCondition(String keyword, SearchType searchType) { this.keyword = keyword; this.searchType = searchType; } + + public boolean isEmptyCondition() { + return keyword == null && searchType == null; + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueSearchRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueSearchRequest.java index c03c6003e..f851258e5 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueSearchRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueSearchRequest.java @@ -1,16 +1,27 @@ package kr.touroot.travelogue.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +import kr.touroot.travelogue.domain.search.SearchCondition; +import kr.touroot.travelogue.domain.search.SearchType; public record TravelogueSearchRequest( @Schema(description = "검색어 (제목, 작성자 닉네임 모두 가능)", example = "서울") - @NotBlank(message = "검색어는 2글자 이상이어야 합니다.") @Size(min = 2, message = "검색어는 2글자 이상이어야 합니다.") String keyword, @Schema(description = "검색 키워드 종류 (TITLE, AUTHOR, COUNTRY)", example = "TITLE") - @NotBlank(message = "검색 키워드 종류는 필수입니다.") String searchType ) { + + public SearchCondition toSearchCondition() { + return new SearchCondition(keyword, getSearchType()); + } + + private SearchType getSearchType() { + if (this.searchType == null) { + return null; + } + + return SearchType.from(this.searchType); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepository.java index 7d4eae28d..91f6cb164 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepository.java @@ -8,7 +8,7 @@ public interface TravelogueQueryRepository { - Page<Travelogue> findByKeywordAndSearchType(SearchCondition condition, Pageable pageable); + Page<Travelogue> findAllBySearchCondition(SearchCondition condition, Pageable pageable); - Page<Travelogue> findAllByFilter(TravelogueFilterCondition filter, Pageable pageable); + Page<Travelogue> findAllByCondition(SearchCondition searchCondition, TravelogueFilterCondition filterCondition, Pageable pageable); } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java index 2b22ab225..2094b66fb 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java @@ -34,21 +34,36 @@ public class TravelogueQueryRepositoryImpl implements TravelogueQueryRepository private final JPAQueryFactory jpaQueryFactory; @Override - public Page<Travelogue> findByKeywordAndSearchType(SearchCondition condition, Pageable pageable) { + public Page<Travelogue> findAllByCondition( + SearchCondition searchCondition, + TravelogueFilterCondition filterCondition, + Pageable pageable + ) { + JPAQuery<Travelogue> baseQuery = jpaQueryFactory.selectFrom(travelogue); + + addSearchCondition(baseQuery, searchCondition); + addFilterCondition(baseQuery, filterCondition); + + List<Travelogue> results = baseQuery.orderBy(findSortCondition(pageable.getSort())) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + return new PageImpl<>(results, pageable, results.size()); + } + + private void addSearchCondition(JPAQuery<Travelogue> query, SearchCondition condition) { String keyword = condition.getKeyword(); - JPAQuery<Travelogue> query = jpaQueryFactory.selectFrom(travelogue); + if (condition.getSearchType() == SearchType.COUNTRY) { CountryCode countryCode = CountryCode.findByName(keyword); findByCountryCode(query, countryCode); + return; } + if (condition.getSearchType() == SearchType.AUTHOR || condition.getSearchType() == SearchType.TITLE) { findByTitleOrAuthor(condition, query, keyword); } - List<Travelogue> results = query.offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - return new PageImpl<>(results, pageable, results.size()); } private void findByCountryCode(JPAQuery<Travelogue> query, CountryCode countryCode) { @@ -71,19 +86,9 @@ private StringPath getTargetField(SearchType searchType) { return travelogue.title; } - @Override - public Page<Travelogue> findAllByFilter(TravelogueFilterCondition filter, Pageable pageable) { - JPAQuery<Travelogue> query = jpaQueryFactory.selectFrom(travelogue); - + private void addFilterCondition(JPAQuery<Travelogue> query, TravelogueFilterCondition filter) { addTagFilter(query, filter); addPeriodFilter(query, filter); - - List<Travelogue> results = query.orderBy(findSortCondition(pageable.getSort())) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - return new PageImpl<>(results, pageable, results.size()); } public void addTagFilter(JPAQuery<Travelogue> query, TravelogueFilterCondition filter) { @@ -132,4 +137,26 @@ private Order getDirection(Sort.Order order) { return Order.DESC; } + + // TODO: 프론트엔드 엔드포인트 이전 작업 완료 후 제거 + @Override + public Page<Travelogue> findAllBySearchCondition(SearchCondition condition, Pageable pageable) { + String keyword = condition.getKeyword(); + JPAQuery<Travelogue> query = jpaQueryFactory.selectFrom(travelogue); + + if (condition.getSearchType() == SearchType.COUNTRY) { + CountryCode countryCode = CountryCode.findByName(keyword); + findByCountryCode(query, countryCode); + } + + if (condition.getSearchType() == SearchType.AUTHOR || condition.getSearchType() == SearchType.TITLE) { + findByTitleOrAuthor(condition, query, keyword); + } + + List<Travelogue> results = query.offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + return new PageImpl<>(results, pageable, results.size()); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java index 47ab457c5..c87274790 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java @@ -7,6 +7,7 @@ import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueFilterCondition; import kr.touroot.travelogue.domain.TravelogueTag; +import kr.touroot.travelogue.domain.search.SearchCondition; import kr.touroot.travelogue.dto.request.TravelogueFilterRequest; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.dto.request.TravelogueSearchRequest; @@ -63,14 +64,17 @@ public TravelogueResponse findTravelogueByIdForAuthenticated(Long id, MemberAuth @Transactional(readOnly = true) public Page<TravelogueSimpleResponse> findSimpleTravelogues( TravelogueFilterRequest filterRequest, + TravelogueSearchRequest searchRequest, Pageable pageable ) { TravelogueFilterCondition filter = filterRequest.toFilterCondition(); - Page<Travelogue> travelogues = travelogueService.findAllByFilter(filter, pageable); + SearchCondition searchCondition = searchRequest.toSearchCondition(); + Page<Travelogue> travelogues = travelogueService.findAll(searchCondition, filter, pageable); return travelogues.map(this::getTravelogueSimpleResponse); } + // TODO: 프론트엔드 엔드포인트 이전 작업 완료 후 제거 @Transactional(readOnly = true) public Page<TravelogueSimpleResponse> findSimpleTravelogues(TravelogueSearchRequest request, Pageable pageable) { Page<Travelogue> travelogues = travelogueService.findByKeyword(request, pageable); diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java index 4a42a7ef8..48cf73f31 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java @@ -40,21 +40,26 @@ public Page<Travelogue> findAllByMember(Member member, Pageable pageable) { return travelogueRepository.findAllByAuthor(member, pageable); } + // TODO: 프론트엔드 엔드포인트 이전 작업 완료 후 제거 @Transactional(readOnly = true) public Page<Travelogue> findByKeyword(TravelogueSearchRequest request, Pageable pageable) { SearchType searchType = SearchType.from(request.searchType()); SearchCondition searchCondition = new SearchCondition(request.keyword(), searchType); - return travelogueQueryRepository.findByKeywordAndSearchType(searchCondition, pageable); + return travelogueQueryRepository.findAllBySearchCondition(searchCondition, pageable); } @Transactional(readOnly = true) - public Page<Travelogue> findAllByFilter(TravelogueFilterCondition filter, Pageable pageable) { - if (filter.isEmptyCondition()) { + public Page<Travelogue> findAll( + SearchCondition searchCondition, + TravelogueFilterCondition filter, + Pageable pageable + ) { + if (filter.isEmptyCondition() && searchCondition.isEmptyCondition()) { return travelogueRepository.findAll(pageable); } - return travelogueQueryRepository.findAllByFilter(filter, pageable); + return travelogueQueryRepository.findAllByCondition(searchCondition, filter, pageable); } @Transactional diff --git a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index d2ef43315..d2b649109 100644 --- a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -28,6 +28,7 @@ import kr.touroot.travelogue.helper.TravelogueTestHelper; import kr.touroot.utils.DatabaseCleaner; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -389,7 +390,7 @@ void findTraveloguesByTitleKeyword() throws JsonProcessingException { .param("searchType", "TITLE") .log().all() .accept(ContentType.JSON) - .when().get("/api/v1/travelogues/search") + .when().get("/api/v1/travelogues") .then().log().all() .statusCode(200).assertThat() .body(is(objectMapper.writeValueAsString(responses))); @@ -407,7 +408,7 @@ void findTraveloguesKeywordNotBlank(String keyword) { .param("searchType", "TITLE") .log().all() .accept(ContentType.JSON) - .when().get("/api/v1/travelogues/search") + .when().get("/api/v1/travelogues") .then().log().all() .statusCode(400).assertThat() .body("message", is("검색어는 2글자 이상이어야 합니다.")); @@ -425,12 +426,13 @@ void findTraveloguesKeywordWithMiddleBlank(String keyword) throws JsonProcessing .param("searchType", "TITLE") .log().all() .accept(ContentType.JSON) - .when().get("/api/v1/travelogues/search") + .when().get("/api/v1/travelogues") .then().log().all() .statusCode(200).assertThat() .body(is(objectMapper.writeValueAsString(responses))); } + @Disabled // 검색과 필터링 API 통합으로 검색 키워드 빈 값 가능 @DisplayName("검색 키워드의 종류를 명시해야 한다.") @Test void findTraveloguesByKeywordWithoutSearchType() { diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index 99ae5bb92..ffb2c195d 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -147,12 +147,17 @@ void findTravelogueByIdAndLiker() { @DisplayName("메인 페이지에 표시할 여행기 목록을 조회한다.") @Test void findTravelogues() { + TravelogueSearchRequest searchRequest = new TravelogueSearchRequest(null, null); TravelogueFilterRequest filterRequest = new TravelogueFilterRequest(null, null); testHelper.initAllTravelogueTestData(); Page<TravelogueSimpleResponse> expect = TravelogueResponseFixture.getTravelogueSimpleResponses(); PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("id")); - Page<TravelogueSimpleResponse> result = service.findSimpleTravelogues(filterRequest, pageRequest); + Page<TravelogueSimpleResponse> result = service.findSimpleTravelogues( + filterRequest, + searchRequest, + pageRequest + ); assertThat(result).containsAll(expect); } @@ -164,9 +169,10 @@ void filterTravelogues() { testHelper.initAllTravelogueTestData(); PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("id")); TravelogueFilterRequest filter = new TravelogueFilterRequest(List.of(1L), null); + TravelogueSearchRequest searchRequest = new TravelogueSearchRequest(null, null); // when - Page<TravelogueSimpleResponse> result = service.findSimpleTravelogues(filter, pageRequest); + Page<TravelogueSimpleResponse> result = service.findSimpleTravelogues(filter, searchRequest, pageRequest); // then assertThat(result.getContent()).hasSize(1); @@ -175,39 +181,62 @@ void filterTravelogues() { @DisplayName("제목 키워드를 기반으로 여행기 목록을 조회한다.") @Test void findTraveloguesByTitleKeyword() { + // given testHelper.initAllTravelogueTestData(); Page<TravelogueSimpleResponse> responses = TravelogueResponseFixture.getTravelogueSimpleResponses(); TravelogueSearchRequest searchRequest = new TravelogueSearchRequest("제주", "title"); + TravelogueFilterRequest filterRequest = new TravelogueFilterRequest(null, null); PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("id")); - Page<TravelogueSimpleResponse> searchResults = service.findSimpleTravelogues(searchRequest, pageRequest); + // when + Page<TravelogueSimpleResponse> searchResults = service.findSimpleTravelogues( + filterRequest, + searchRequest, + pageRequest + ); + + // then assertThat(searchResults).containsAll(responses); } @DisplayName("사용자 닉네임을 기반으로 여행기 목록을 조회한다.") @Test void findTraveloguesByAuthorNicknameKeyword() { + // given testHelper.initAllTravelogueTestData(); Page<TravelogueSimpleResponse> responses = TravelogueResponseFixture.getTravelogueSimpleResponses(); TravelogueSearchRequest searchRequest = new TravelogueSearchRequest("리비", "author"); + TravelogueFilterRequest filterRequest = new TravelogueFilterRequest(null, null); PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("id")); - Page<TravelogueSimpleResponse> searchResults = service.findSimpleTravelogues(searchRequest, pageRequest); + // when + Page<TravelogueSimpleResponse> searchResults = service.findSimpleTravelogues( + filterRequest, + searchRequest, + pageRequest + ); + + // then assertThat(searchResults).containsAll(responses); } @DisplayName("국가 코드를 기반으로 여행기 목록을 조회한다.") @Test void findTraveloguesByCountryCodeKeyword() { + // given testHelper.initAllTravelogueTestData(); Page<TravelogueSimpleResponse> responses = TravelogueResponseFixture.getTravelogueSimpleResponses(); TravelogueSearchRequest searchRequest = new TravelogueSearchRequest("한국", "country"); + TravelogueFilterRequest filterRequest = new TravelogueFilterRequest(null, null); PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("id")); - Page<TravelogueSimpleResponse> searchResults = service.findSimpleTravelogues(searchRequest, pageRequest); + // when + Page<TravelogueSimpleResponse> searchResults = service.findSimpleTravelogues(filterRequest, searchRequest, pageRequest); + + // then assertThat(searchResults).containsAll(responses); } diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java index 8d0b91b0c..20409b85c 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java @@ -12,11 +12,13 @@ import kr.touroot.global.exception.ForbiddenException; import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueFilterCondition; +import kr.touroot.travelogue.domain.search.SearchCondition; +import kr.touroot.travelogue.domain.search.SearchType; import kr.touroot.travelogue.dto.request.TravelogueDayRequest; import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.dto.request.TravelogueRequest; -import kr.touroot.travelogue.dto.request.TravelogueSearchRequest; import kr.touroot.travelogue.fixture.TravelogueFixture; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; import kr.touroot.travelogue.helper.TravelogueTestHelper; @@ -26,7 +28,9 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; -import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; @DisplayName("여행기 서비스") @Import(value = {TravelogueService.class, TravelogueTestHelper.class, TestQueryDslConfig.class}) @@ -90,21 +94,35 @@ void getTravelogueByNotExistsIdThrowException() { @DisplayName("여행기를 검색할 수 있다.") @Test void findByKeyword() { + // given testHelper.initTravelogueTestData(); - TravelogueSearchRequest request = new TravelogueSearchRequest("제주", "title"); - assertThat(travelogueService.findByKeyword(request, Pageable.ofSize(BASIC_PAGE_SIZE))) - .hasSize(1); + SearchCondition searchCondition = new SearchCondition("제주", SearchType.TITLE); + TravelogueFilterCondition filter = new TravelogueFilterCondition(null, null); + PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("createdAt")); + + // when + Page<Travelogue> actual = travelogueService.findAll(searchCondition, filter, pageRequest); + + // then + assertThat(actual).hasSize(1); } @DisplayName("존재하지 않는 키워드로 여행기를 조회하면 빈 페이지가 반환된다.") @Test void findByKeywordWithNotExistRequest() { + // given testHelper.initTravelogueTestData(); - TravelogueSearchRequest request = new TravelogueSearchRequest("서울", "title"); - assertThat(travelogueService.findByKeyword(request, Pageable.ofSize(BASIC_PAGE_SIZE))) - .isEmpty(); + SearchCondition searchCondition = new SearchCondition("서울", SearchType.TITLE); + TravelogueFilterCondition filter = new TravelogueFilterCondition(null, null); + PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("createdAt")); + + // when + Page<Travelogue> actual = travelogueService.findAll(searchCondition, filter, pageRequest); + + // then + assertThat(actual).isEmpty(); } @DisplayName("여행기를 수정할 수 있다.") From f736f2fbd26b761603da8167dff8308047868481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Wed, 23 Oct 2024 10:34:33 +0900 Subject: [PATCH 10/13] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0,=20=EC=97=AC=ED=96=89=20=EA=B3=84=ED=9A=8D=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=EC=97=90=20=EA=B5=AD=EA=B0=80=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=BB=AC=EB=9F=BC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#568)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../travelogue/domain/TraveloguePlace.java | 39 ++++++++++--- .../travelogue/domain/search/CountryCode.java | 4 ++ .../dto/request/TraveloguePlaceRequest.java | 5 +- .../dto/response/TraveloguePlaceResponse.java | 5 +- .../travelplan/domain/TravelPlanPlace.java | 38 ++++++++++--- .../dto/request/PlanPlaceRequest.java | 8 ++- .../dto/response/PlanPlaceResponse.java | 4 +- ...y.sql => V6.0__add_travelogue_country.sql} | 0 .../mysql/V6.1__add_country_code_column.sql | 5 ++ .../mysql/V6.2__update_country_code_none.sql | 5 ++ ...add_not_null_contraint_to_country_code.sql | 5 ++ .../domain/TraveloguePlaceTest.java | 56 +++++++++++++++---- .../fixture/TraveloguePlaceFixture.java | 7 ++- .../fixture/TravelogueResponseFixture.java | 1 + .../controller/TravelPlanControllerTest.java | 5 ++ .../domain/TravelPlanPlaceTest.java | 46 +++++++++++---- .../fixture/TravelPlanPlaceFixture.java | 9 ++- .../helper/TravelPlanTestHelper.java | 9 +-- .../service/TravelPlanFacadeServiceTest.java | 2 + .../service/TravelPlanServiceTest.java | 1 + 20 files changed, 198 insertions(+), 56 deletions(-) rename backend/src/main/resources/db/migration/mysql/{V6__add_travelogue_country.sql => V6.0__add_travelogue_country.sql} (100%) create mode 100644 backend/src/main/resources/db/migration/mysql/V6.1__add_country_code_column.sql create mode 100644 backend/src/main/resources/db/migration/mysql/V6.2__update_country_code_none.sql create mode 100644 backend/src/main/resources/db/migration/mysql/V6.3__add_not_null_contraint_to_country_code.sql diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java index 99e29c526..680a87913 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java @@ -4,6 +4,8 @@ import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -17,6 +19,7 @@ import kr.touroot.global.exception.BadRequestException; import kr.touroot.place.domain.Place; import kr.touroot.position.domain.Position; +import kr.touroot.travelogue.domain.search.CountryCode; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -61,21 +64,27 @@ public class TraveloguePlace extends BaseEntity { @OneToMany(mappedBy = "traveloguePlace", cascade = CascadeType.ALL, orphanRemoval = true) private List<TraveloguePhoto> traveloguePhotos = new ArrayList<>(); + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private CountryCode countryCode; + public TraveloguePlace( Long id, Integer order, String description, String name, Position position, - TravelogueDay travelogueDay + TravelogueDay travelogueDay, + String countryCode ) { - validate(order, description, name, position, travelogueDay); + validate(order, description, name, position, travelogueDay, countryCode); this.id = id; this.order = order; this.description = description; this.name = name; this.position = position; this.travelogueDay = travelogueDay; + this.countryCode = CountryCode.valueOfIgnoreCase(countryCode); } public TraveloguePlace( @@ -84,9 +93,10 @@ public TraveloguePlace( String name, String latitude, String longitude, - TravelogueDay travelogueDay + TravelogueDay travelogueDay, + String countryCode ) { - this(null, order, description, name, new Position(latitude, longitude), travelogueDay); + this(null, order, description, name, new Position(latitude, longitude), travelogueDay, countryCode); } private void validate( @@ -94,18 +104,21 @@ private void validate( String description, String name, Position coordinate, - TravelogueDay travelogueDay + TravelogueDay travelogueDay, + String countryCode ) { - validateNotNull(order, name, coordinate, travelogueDay); + validateNotNull(order, name, coordinate, travelogueDay, countryCode); validateNotBlank(name); validateOrderRange(order); validateDescriptionLength(description); validatePlaceNameLength(name); + validateCountryCode(countryCode); } - private void validateNotNull(Integer order, String name, Position coordinate, TravelogueDay day) { - if (order == null || name == null || coordinate == null || day == null) { - throw new BadRequestException("여행기 장소에서 순서와 장소 위치, 그리고 방문 날짜는 비어 있을 수 없습니다"); + private void validateNotNull(Integer order, String name, Position coordinate, TravelogueDay day, + String countryCode) { + if (order == null || name == null || coordinate == null || day == null || countryCode == null) { + throw new BadRequestException("여행기 장소에서 순서와 장소 위치, 방문 날짜, 그리고 국가 코드는 비어 있을 수 없습니다"); } } @@ -133,6 +146,14 @@ private void validatePlaceNameLength(String placeName) { } } + private void validateCountryCode(String countryCode) { + try { + CountryCode.valueOfIgnoreCase(countryCode); + } catch (IllegalArgumentException e) { + throw new BadRequestException("존재하지 않는 국가 코드입니다"); + } + } + public void addPhoto(TraveloguePhoto photo) { traveloguePhotos.add(photo); photo.updateTraveloguePlace(this); diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java b/backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java index a20793d71..eaf797e9f 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java @@ -258,4 +258,8 @@ public static CountryCode findByName(String name) { .findFirst() .orElse(NONE); } + + public static CountryCode valueOfIgnoreCase(String name) { + return CountryCode.valueOf(name.toUpperCase()); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java index 8bd4c8988..3fa113a63 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java @@ -27,7 +27,7 @@ public record TraveloguePlaceRequest( @Size(message = "여행기 장소 사진은 최대 10개입니다.", max = 10) @Valid List<TraveloguePhotoRequest> photoUrls, - @Schema(description = "여행기 장소 국가 코드") + @Schema(description = "여행기 장소 국가 코드", example = "KR") @NotBlank(message = "여행기 장소 국가 코드는 비어있을 수 없습니다.") String countryCode ) { @@ -39,7 +39,8 @@ public TraveloguePlace toTraveloguePlace(int order, TravelogueDay travelogueDay) placeName, position().lat(), position().lng(), - travelogueDay + travelogueDay, + countryCode ); addTraveloguePhotos(traveloguePlace); return traveloguePlace; diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/response/TraveloguePlaceResponse.java b/backend/src/main/java/kr/touroot/travelogue/dto/response/TraveloguePlaceResponse.java index c0e73ab34..55593dc95 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/response/TraveloguePlaceResponse.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/response/TraveloguePlaceResponse.java @@ -15,7 +15,9 @@ public record TraveloguePlaceResponse( @Schema(description = "여행기 장소 설명", example = "성담 빌딩에 위치한 선릉 캠퍼스입니다.") String description, TraveloguePositionResponse position, - List<String> photoUrls + List<String> photoUrls, + @Schema(description = "여행기 장소 국가 코드", example = "KR") + String countryCode ) { public static TraveloguePlaceResponse from(TraveloguePlace traveloguePlace) { @@ -25,6 +27,7 @@ public static TraveloguePlaceResponse from(TraveloguePlace traveloguePlace) { .description(traveloguePlace.getDescription()) .position(TraveloguePositionResponse.from(traveloguePlace.getPosition())) .photoUrls(getTraveloguePhotosResponse(traveloguePlace)) + .countryCode(traveloguePlace.getCountryCode().name()) .build(); } diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java index 256019a5a..11af2c9fe 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java @@ -4,6 +4,8 @@ import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -17,6 +19,7 @@ import kr.touroot.global.exception.BadRequestException; import kr.touroot.place.domain.Place; import kr.touroot.position.domain.Position; +import kr.touroot.travelogue.domain.search.CountryCode; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -56,30 +59,39 @@ public class TravelPlanPlace extends BaseEntity { @OneToMany(mappedBy = "travelPlanPlace", cascade = CascadeType.ALL, orphanRemoval = true) private List<TravelPlaceTodo> travelPlaceTodos = new ArrayList<>(); - public TravelPlanPlace(Long id, Integer order, TravelPlanDay day, String name, Position position) { - validate(order, day, name, position); + @Column + @Enumerated(EnumType.STRING) + private CountryCode countryCode; + + public TravelPlanPlace(Long id, Integer order, TravelPlanDay day, String name, Position position, + String countryCode) { + validate(order, day, name, position, countryCode); this.id = id; this.order = order; this.day = day; this.name = name; this.position = position; + this.countryCode = CountryCode.valueOfIgnoreCase(countryCode); } - public TravelPlanPlace(Integer order, TravelPlanDay day, String name, String latitude, String longitude) { - this(null, order, day, name, new Position(latitude, longitude)); + public TravelPlanPlace(Integer order, TravelPlanDay day, String name, String latitude, String longitude, + String countryCode) { + this(null, order, day, name, new Position(latitude, longitude), countryCode); } - private void validate(Integer order, TravelPlanDay day, String name, Position coordinate) { - validateNotNull(order, day, name, coordinate); + private void validate(Integer order, TravelPlanDay day, String name, Position coordinate, String countryCode) { + validateNotNull(order, day, name, coordinate, countryCode); validateNotBlank(name); validateOrderRange(order); validatePlaceNameLength(name); + validateCountryCode(countryCode); } - private void validateNotNull(Integer order, TravelPlanDay day, String name, Position coordinate) { - if (order == null || day == null || name == null || coordinate == null) { - throw new BadRequestException("여행 계획 장소에서 순서와 날짜, 그리고 장소 위치는 비어 있을 수 없습니다"); + private void validateNotNull(Integer order, TravelPlanDay day, String name, Position coordinate, + String countryCode) { + if (order == null || day == null || name == null || coordinate == null || countryCode == null) { + throw new BadRequestException("여행 계획 장소에서 순서와 날짜, 장소 위치, 그리고 국가 코드는 비어 있을 수 없습니다"); } } @@ -101,6 +113,14 @@ private void validatePlaceNameLength(String placeName) { } } + private void validateCountryCode(String countryCode) { + try { + CountryCode.valueOfIgnoreCase(countryCode); + } catch (IllegalArgumentException e) { + throw new BadRequestException("존재하지 않는 국가 코드입니다"); + } + } + public void addTodo(TravelPlaceTodo todo) { travelPlaceTodos.add(todo); todo.updateTravelPlanPlace(this); diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceRequest.java index 97f5645da..3217885f5 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceRequest.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceRequest.java @@ -19,7 +19,10 @@ public record PlanPlaceRequest( PlanPositionRequest position, @Valid @NotNull(message = "TODO 리스트는 필수 입니다.") - List<PlanPlaceTodoRequest> todos + List<PlanPlaceTodoRequest> todos, + @Schema(description = "여행기 장소 국가 코드", example = "KR") + @NotBlank(message = "여행기 장소 국가 코드는 비어있을 수 없습니다.") + String countryCode ) { public TravelPlanPlace toPlanPlace(int order, TravelPlanDay day) { @@ -28,7 +31,8 @@ public TravelPlanPlace toPlanPlace(int order, TravelPlanDay day) { day, placeName, position().lat(), - position().lng() + position().lng(), + countryCode ); addTodos(travelPlanPlace); return travelPlanPlace; diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanPlaceResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanPlaceResponse.java index 235a9e0cc..1d8ea2bc6 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanPlaceResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanPlaceResponse.java @@ -10,7 +10,8 @@ public record PlanPlaceResponse( @Schema(description = "여행 장소 Id", example = "1") Long id, @Schema(description = "여행 장소 이름", example = "잠실한강공원") String placeName, @Schema(description = "여행 장소 위치") PlanPositionResponse position, - @Schema(description = "여행 장소 TODO") List<PlanPlaceTodoResponse> todos + @Schema(description = "여행 장소 TODO") List<PlanPlaceTodoResponse> todos, + @Schema(description = "국가 코드") String countryCode ) { public static PlanPlaceResponse from(TravelPlanPlace planPlace) { @@ -19,6 +20,7 @@ public static PlanPlaceResponse from(TravelPlanPlace planPlace) { .placeName(planPlace.getName()) .position(PlanPositionResponse.from(planPlace.getPosition())) .todos(getTodoResponse(planPlace)) + .countryCode(planPlace.getCountryCode().name()) .build(); } diff --git a/backend/src/main/resources/db/migration/mysql/V6__add_travelogue_country.sql b/backend/src/main/resources/db/migration/mysql/V6.0__add_travelogue_country.sql similarity index 100% rename from backend/src/main/resources/db/migration/mysql/V6__add_travelogue_country.sql rename to backend/src/main/resources/db/migration/mysql/V6.0__add_travelogue_country.sql diff --git a/backend/src/main/resources/db/migration/mysql/V6.1__add_country_code_column.sql b/backend/src/main/resources/db/migration/mysql/V6.1__add_country_code_column.sql new file mode 100644 index 000000000..bdd635386 --- /dev/null +++ b/backend/src/main/resources/db/migration/mysql/V6.1__add_country_code_column.sql @@ -0,0 +1,5 @@ +ALTER TABLE travelogue_place + ADD COLUMN country_code VARCHAR(10); + +ALTER TABLE travel_plan_place + ADD COLUMN country_code VARCHAR(10); diff --git a/backend/src/main/resources/db/migration/mysql/V6.2__update_country_code_none.sql b/backend/src/main/resources/db/migration/mysql/V6.2__update_country_code_none.sql new file mode 100644 index 000000000..a22889c5d --- /dev/null +++ b/backend/src/main/resources/db/migration/mysql/V6.2__update_country_code_none.sql @@ -0,0 +1,5 @@ +UPDATE travelogue_place +SET country_code = 'NONE'; + +UPDATE travel_plan_place +SET country_code = 'NONE'; diff --git a/backend/src/main/resources/db/migration/mysql/V6.3__add_not_null_contraint_to_country_code.sql b/backend/src/main/resources/db/migration/mysql/V6.3__add_not_null_contraint_to_country_code.sql new file mode 100644 index 000000000..6568f56a2 --- /dev/null +++ b/backend/src/main/resources/db/migration/mysql/V6.3__add_not_null_contraint_to_country_code.sql @@ -0,0 +1,5 @@ +ALTER TABLE travelogue_place + MODIFY country_code VARCHAR(10) NOT NULL; + +ALTER TABLE travel_plan_place + MODIFY country_code VARCHAR(10) NOT NULL; diff --git a/backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePlaceTest.java b/backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePlaceTest.java index 8959594e6..395d92ab9 100644 --- a/backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePlaceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePlaceTest.java @@ -22,49 +22,67 @@ class TraveloguePlaceTest { private static final String VALID_LNG = "126.6728"; private static final TravelogueDay VALID_DAY = TravelogueDayFixture.TRAVELOGUE_DAY.get(); private static final String VALID_DESC = "장소에 대한 설명"; + private static final String VALID_COUNTRY_CODE = "KR"; @DisplayName("올바른 여행기 장소 생성 시 예외가 발생하지 않는다") @Test void createTraveloguePlaceWithValidData() { - assertThatCode(() -> new TraveloguePlace(VALID_ORDER, VALID_DESC, VALID_NAME, VALID_LAT, VALID_LNG, VALID_DAY)) + assertThatCode(() -> new TraveloguePlace(VALID_ORDER, VALID_DESC, VALID_NAME, VALID_LAT, VALID_LNG, VALID_DAY, + VALID_COUNTRY_CODE)) .doesNotThrowAnyException(); } @DisplayName("여행기 장소 생성 시 순서가 비어 있다면 예외가 발생한다") @Test void createTraveloguePlaceWithNullOrder() { - assertThatThrownBy(() -> new TraveloguePlace(null, VALID_DESC, VALID_NAME, VALID_LAT, VALID_LNG, VALID_DAY)) - .isInstanceOf(BadRequestException.class).hasMessage("여행기 장소에서 순서와 장소 위치, 그리고 방문 날짜는 비어 있을 수 없습니다"); + assertThatThrownBy(() -> new TraveloguePlace(null, VALID_DESC, VALID_NAME, VALID_LAT, VALID_LNG, VALID_DAY, + VALID_COUNTRY_CODE)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행기 장소에서 순서와 장소 위치, 방문 날짜, 그리고 국가 코드는 비어 있을 수 없습니다"); } @DisplayName("여행기 장소 생성 시 이름 정보가 비어 있다면 예외가 발생한다") @Test void createTraveloguePlaceWithNullPlaceName() { - assertThatThrownBy(() -> new TraveloguePlace(VALID_ORDER, VALID_DESC, null, VALID_LAT, VALID_LNG, VALID_DAY)) + assertThatThrownBy(() -> new TraveloguePlace(VALID_ORDER, VALID_DESC, null, VALID_LAT, VALID_LNG, VALID_DAY, + VALID_COUNTRY_CODE)) .isInstanceOf(BadRequestException.class) - .hasMessage("여행기 장소에서 순서와 장소 위치, 그리고 방문 날짜는 비어 있을 수 없습니다"); + .hasMessage("여행기 장소에서 순서와 장소 위치, 방문 날짜, 그리고 국가 코드는 비어 있을 수 없습니다"); } @DisplayName("여행기 장소 생성 시 장소가 속한 날짜가 비어 있다면 예외가 발생한다") @Test void createTraveloguePlaceWithNullDay() { - assertThatThrownBy(() -> new TraveloguePlace(VALID_ORDER, VALID_DESC, VALID_NAME, VALID_LAT, VALID_LNG, null)) + assertThatThrownBy(() -> new TraveloguePlace(VALID_ORDER, VALID_DESC, VALID_NAME, VALID_LAT, VALID_LNG, null, + VALID_COUNTRY_CODE)) .isInstanceOf(BadRequestException.class) - .hasMessage("여행기 장소에서 순서와 장소 위치, 그리고 방문 날짜는 비어 있을 수 없습니다"); + .hasMessage("여행기 장소에서 순서와 장소 위치, 방문 날짜, 그리고 국가 코드는 비어 있을 수 없습니다"); } @DisplayName("여행기 장소 생성 시 장소 설명이 비어 있더라도 여행기를 생성할 수 있다") @Test void createTraveloguePlaceWithNullDescription() { - assertThatCode(() -> new TraveloguePlace(VALID_ORDER, null, VALID_NAME, VALID_LAT, VALID_LNG, VALID_DAY)) + assertThatCode(() -> new TraveloguePlace(VALID_ORDER, null, VALID_NAME, VALID_LAT, VALID_LNG, VALID_DAY, + VALID_COUNTRY_CODE)) .doesNotThrowAnyException(); } + @DisplayName("여행기 장소 생성 시 국가 코드가 비어 있다면 예외가 발생한다") + @Test + void createTraveloguePlaceWithNullCountryCode() { + assertThatThrownBy( + () -> new TraveloguePlace(VALID_ORDER, VALID_DESC, VALID_NAME, VALID_LAT, VALID_LNG, VALID_DAY, + null)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행기 장소에서 순서와 장소 위치, 방문 날짜, 그리고 국가 코드는 비어 있을 수 없습니다"); + } + @DisplayName("여행 장소 이름이 공백문자로만 이루어져 있는 경우 예외가 발생한다") @ParameterizedTest @ValueSource(strings = {"", " ", " "}) void createTraveloguePlaceWithBlankName(String blank) { - assertThatThrownBy(() -> new TraveloguePlace(VALID_ORDER, VALID_DESC, blank, VALID_LAT, VALID_LNG, VALID_DAY)) + assertThatThrownBy(() -> new TraveloguePlace(VALID_ORDER, VALID_DESC, blank, VALID_LAT, VALID_LNG, VALID_DAY, + VALID_COUNTRY_CODE)) .isInstanceOf(BadRequestException.class) .hasMessage("장소 이름은 비어 있을 수 없습니다"); } @@ -73,7 +91,8 @@ void createTraveloguePlaceWithBlankName(String blank) { @ParameterizedTest @ValueSource(ints = {-1, -2, -3, -4, -5}) void createTraveloguePlaceWithNegativeOrder(int negative) { - assertThatThrownBy(() -> new TraveloguePlace(negative, VALID_DESC, VALID_NAME, VALID_LAT, VALID_LNG, VALID_DAY)) + assertThatThrownBy(() -> new TraveloguePlace(negative, VALID_DESC, VALID_NAME, VALID_LAT, VALID_LNG, VALID_DAY, + VALID_COUNTRY_CODE)) .isInstanceOf(BadRequestException.class) .hasMessage("여행 장소의 순서는 음수일 수 없습니다"); } @@ -84,7 +103,8 @@ void createTraveloguePlaceWithInvalidLengthDescription() { String invalid = "서울의 명동은 현대와 전통이 조화롭게 어우러진 매력적인 지역입니다. 이곳의 거리에는 최신 패션 아이템을 갖춘 상점들이 즐비하며, 각종 뷰티 제품을 직접 체험할 수 있는 기회가 많습니다. 다양한 길거리 음식과 맛집이 가득해 미식가들의 입맛을 사로잡습니다. 서울타워와 N서울타워 전망대에서는 서울 전경을 한눈에 감상할 수 있으며, 남산 공원에서는 도심 속의 자연을 즐길 수 있습니다. 전통 시장인 남대문 시장과 청계천은 서울의 풍부한 역사와 문화를 체험할 수 있는 명소입니다. 명동의 활기 넘치는 분위기 속에서 쇼핑과 먹거리를 동시에 즐겨요!"; System.out.println("length301Description = " + invalid.length()); - assertThatThrownBy(() -> new TraveloguePlace(VALID_ORDER, invalid, VALID_NAME, VALID_LAT, VALID_LNG, VALID_DAY)) + assertThatThrownBy(() -> new TraveloguePlace(VALID_ORDER, invalid, VALID_NAME, VALID_LAT, VALID_LNG, VALID_DAY, + VALID_COUNTRY_CODE)) .isInstanceOf(BadRequestException.class) .hasMessage("여행 장소에 대한 설명은 300자를 넘길 수 없습니다"); } @@ -95,12 +115,24 @@ void createPlaceWithInvalidLengthPlaceName() { String length86 = "Under the warm summer sun, feeling the cool breeze by the sea is absolute pure joy!!!!"; assertThatThrownBy( - () -> new TraveloguePlace(VALID_ORDER, VALID_DESC, length86, VALID_LAT, VALID_LNG, VALID_DAY) + () -> new TraveloguePlace(VALID_ORDER, VALID_DESC, length86, VALID_LAT, VALID_LNG, VALID_DAY, + VALID_COUNTRY_CODE) ) .isInstanceOf(BadRequestException.class) .hasMessage("장소 이름은 85자 이하여야 합니다"); } + @DisplayName("국가 코드가 존재하지 않는 경우 장소 생성 시 예외가 발생한다") + @Test + void createPlaceWithInvalidCountryCode() { + assertThatThrownBy( + () -> new TraveloguePlace(VALID_ORDER, VALID_DESC, VALID_NAME, VALID_LAT, VALID_LNG, VALID_DAY, + "SAM-572") + ) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 국가 코드입니다"); + } + @DisplayName("장소 사진을 추가할 수 있다") @Test void addPhotoInTraveloguePlace() { diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePlaceFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePlaceFixture.java index 7835417a6..dc1c359e6 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePlaceFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePlaceFixture.java @@ -9,7 +9,7 @@ @AllArgsConstructor public enum TraveloguePlaceFixture { - TRAVELOGUE_PLACE(1, "에메랄드 빛 해변", "함덕 해수욕장", "34.54343", "126.66977", TRAVELOGUE_DAY.get()), + TRAVELOGUE_PLACE(1, "에메랄드 빛 해변", "함덕 해수욕장", "34.54343", "126.66977", TRAVELOGUE_DAY.get(), "KR"), ; private final int order; @@ -18,12 +18,13 @@ public enum TraveloguePlaceFixture { private final String latitude; private final String longitude; private final TravelogueDay day; + private final String countryCode; public TraveloguePlace get() { - return new TraveloguePlace(order, description, name, latitude, longitude, day); + return new TraveloguePlace(order, description, name, latitude, longitude, day, countryCode); } public TraveloguePlace create(TravelogueDay day) { - return new TraveloguePlace(order, description, name, latitude, longitude, day); + return new TraveloguePlace(order, description, name, latitude, longitude, day, countryCode); } } diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java index 6ab5f01f7..42784a1b0 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java @@ -162,6 +162,7 @@ public static List<TraveloguePlaceResponse> getTraveloguePlaceResponses() { .description("에메랄드 빛 해변") .position(getTraveloguePositionResponse()) .photoUrls(getTraveloguePhotoUrls()) + .countryCode("KR") .build() ); } diff --git a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java index a6657172b..d15d6d406 100644 --- a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -70,6 +70,7 @@ void createTravelPlan() { .placeName("잠실한강공원") .todos(Collections.EMPTY_LIST) .position(locationRequest) + .countryCode("KR") .build(); PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); @@ -100,6 +101,7 @@ void createTravelPlanWithInvalidStartDate() { .placeName("잠실한강공원") .todos(Collections.EMPTY_LIST) .position(locationRequest) + .countryCode("KR") .build(); PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); PlanRequest request = PlanRequest.builder() @@ -255,6 +257,7 @@ void updateTravelPlan() { PlanPlaceRequest planPlaceRequest = PlanPlaceRequest.builder() .placeName("잠실한강공원") .todos(Collections.EMPTY_LIST) + .countryCode("KR") .position(locationRequest) .build(); @@ -284,6 +287,7 @@ void updateTravelPlanWithNonExist() { .placeName("잠실한강공원") .todos(Collections.EMPTY_LIST) .position(locationRequest) + .countryCode("KR") .build(); PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); @@ -316,6 +320,7 @@ void updateTravelPlanWithNotAuthor() { .placeName("잠실한강공원") .todos(Collections.EMPTY_LIST) .position(locationRequest) + .countryCode("KR") .build(); PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); diff --git a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java index b29f429ef..7c7e1cc79 100644 --- a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java @@ -20,43 +20,58 @@ class TravelPlanPlaceTest { private static final String VALID_NAME = "함덕 해수욕장"; private static final String VALID_LAT = "33.5431"; private static final String VALID_LNG = "126.6728"; + private static final String VALID_COUNTRY_CODE = "KR"; @DisplayName("올바른 여행 계획 장소 생성 시 예외가 발생하지 않는다") @Test void createTravelPlanPlaceWithValidData() { - assertThatCode(() -> new TravelPlanPlace(VALID_ORDER, VALID_DAY, VALID_NAME, VALID_LAT, VALID_LNG)) + assertThatCode( + () -> new TravelPlanPlace(VALID_ORDER, VALID_DAY, VALID_NAME, VALID_LAT, VALID_LNG, VALID_COUNTRY_CODE)) .doesNotThrowAnyException(); } @DisplayName("방문 순서가 비어 있을 경우 여행 계획 장소 생성 시 예외가 발생한다") @Test void createTravelPlanPlaceWithNullOrder() { - assertThatThrownBy(() -> new TravelPlanPlace(null, VALID_DAY, VALID_NAME, VALID_LAT, VALID_LNG)) + assertThatThrownBy( + () -> new TravelPlanPlace(null, VALID_DAY, VALID_NAME, VALID_LAT, VALID_LNG, VALID_COUNTRY_CODE)) .isInstanceOf(BadRequestException.class) - .hasMessage("여행 계획 장소에서 순서와 날짜, 그리고 장소 위치는 비어 있을 수 없습니다"); + .hasMessage("여행 계획 장소에서 순서와 날짜, 장소 위치, 그리고 국가 코드는 비어 있을 수 없습니다"); } @DisplayName("장소의 방문 날짜가 비어 있을 경우 여행 계획 장소 생성 시 예외가 발생한다") @Test void createTravelPlanPlaceWithNullDay() { - assertThatThrownBy(() -> new TravelPlanPlace(VALID_ORDER, null, VALID_NAME, VALID_LAT, VALID_LNG)) + assertThatThrownBy( + () -> new TravelPlanPlace(VALID_ORDER, null, VALID_NAME, VALID_LAT, VALID_LNG, VALID_COUNTRY_CODE)) .isInstanceOf(BadRequestException.class) - .hasMessage("여행 계획 장소에서 순서와 날짜, 그리고 장소 위치는 비어 있을 수 없습니다"); + .hasMessage("여행 계획 장소에서 순서와 날짜, 장소 위치, 그리고 국가 코드는 비어 있을 수 없습니다"); } @DisplayName("장소 이름이 비어 있을 경우 여행 계획 장소 생성 시 예외가 발생한다") @Test void createTravelPlanPlaceWithPlaceNullName() { - assertThatThrownBy(() -> new TravelPlanPlace(VALID_ORDER, VALID_DAY, null, VALID_LAT, VALID_LNG)) + assertThatThrownBy( + () -> new TravelPlanPlace(VALID_ORDER, VALID_DAY, null, VALID_LAT, VALID_LNG, VALID_COUNTRY_CODE)) .isInstanceOf(BadRequestException.class) - .hasMessage("여행 계획 장소에서 순서와 날짜, 그리고 장소 위치는 비어 있을 수 없습니다"); + .hasMessage("여행 계획 장소에서 순서와 날짜, 장소 위치, 그리고 국가 코드는 비어 있을 수 없습니다"); + } + + @DisplayName("국가 코드가 비어 있을 경우 여행 계획 장소 생성 시 예외가 발생한다") + @Test + void createTravelPlanPlaceWithPlaceNullCountryCode() { + assertThatThrownBy( + () -> new TravelPlanPlace(VALID_ORDER, VALID_DAY, VALID_NAME, VALID_LAT, VALID_LNG, null)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 계획 장소에서 순서와 날짜, 장소 위치, 그리고 국가 코드는 비어 있을 수 없습니다"); } @DisplayName("여행 장소 이름이 공백문자로만 이루어져 있는 경우 예외가 발생한다") @ParameterizedTest @ValueSource(strings = {"", " ", " "}) void createTravelPlanPlaceWithBlankName(String blank) { - assertThatThrownBy(() -> new TravelPlanPlace(VALID_ORDER, VALID_DAY, blank, VALID_LAT, VALID_LNG)) + assertThatThrownBy( + () -> new TravelPlanPlace(VALID_ORDER, VALID_DAY, blank, VALID_LAT, VALID_LNG, VALID_COUNTRY_CODE)) .isInstanceOf(BadRequestException.class) .hasMessage("장소 이름은 공백문자로만 이루어질 수 없습니다"); } @@ -65,7 +80,8 @@ void createTravelPlanPlaceWithBlankName(String blank) { @ParameterizedTest @ValueSource(ints = {-1, -2, -3, -4, -5}) void createTravelPlanPlaceWithNegativeOrder(int negative) { - assertThatThrownBy(() -> new TravelPlanPlace(negative, VALID_DAY, VALID_NAME, VALID_LAT, VALID_LNG)) + assertThatThrownBy( + () -> new TravelPlanPlace(negative, VALID_DAY, VALID_NAME, VALID_LAT, VALID_LNG, VALID_COUNTRY_CODE)) .isInstanceOf(BadRequestException.class) .hasMessage("장소의 방문 순서는 음수일 수 없습니다"); } @@ -74,11 +90,21 @@ void createTravelPlanPlaceWithNegativeOrder(int negative) { @Test void createPlaceWithInvalidLengthPlaceName() { String length61 = "Under the summer sun, feeling the cool breeze by the sea is pure joy!!"; - assertThatThrownBy(() -> new TravelPlanPlace(VALID_ORDER, VALID_DAY, length61, VALID_LAT, VALID_LNG)) + assertThatThrownBy( + () -> new TravelPlanPlace(VALID_ORDER, VALID_DAY, length61, VALID_LAT, VALID_LNG, VALID_COUNTRY_CODE)) .isInstanceOf(BadRequestException.class) .hasMessage("장소 이름은 60자 이하여야 합니다"); } + @DisplayName("존재하지 않는 국가 코드인 경우 장소 생성 시 예외가 발생한다") + @Test + void createPlaceWithInvalidCountryCode() { + assertThatThrownBy( + () -> new TravelPlanPlace(VALID_ORDER, VALID_DAY, VALID_NAME, VALID_LAT, VALID_LNG, "CONCODE")) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 국가 코드입니다"); + } + @DisplayName("Todo를 추가할 수 있다") @Test void addTodoInPlace() { diff --git a/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanPlaceFixture.java b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanPlaceFixture.java index 24a3d4a54..360b73bfb 100644 --- a/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanPlaceFixture.java +++ b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanPlaceFixture.java @@ -5,23 +5,26 @@ public enum TravelPlanPlaceFixture { - TRAVEL_PLAN_PLACE(0, TravelPlanDayFixture.TRAVEL_PLAN_DAY.get(), "함덕 해수욕장", "34.54343", "126.66977"); + TRAVEL_PLAN_PLACE(0, TravelPlanDayFixture.TRAVEL_PLAN_DAY.get(), "함덕 해수욕장", "34.54343", "126.66977", "KR"); private final Integer order; private final TravelPlanDay travelPlanDay; private final String name; private final String latitude; private final String longitude; + private final String countryCode; - TravelPlanPlaceFixture(Integer order, TravelPlanDay day, String name, String latitude, String longitude) { + TravelPlanPlaceFixture(Integer order, TravelPlanDay day, String name, String latitude, String longitude, + String countryCode) { this.order = order; this.travelPlanDay = day; this.name = name; this.latitude = latitude; this.longitude = longitude; + this.countryCode = countryCode; } public TravelPlanPlace get() { - return new TravelPlanPlace(order, travelPlanDay, name, latitude, longitude); + return new TravelPlanPlace(order, travelPlanDay, name, latitude, longitude, countryCode); } } diff --git a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java index f5ee92007..17597399a 100644 --- a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java @@ -57,9 +57,10 @@ public static TravelPlanPlace getTravelPlanPlace( String name, String latitude, String longitude, - TravelPlanDay day + TravelPlanDay day, + String countryCode ) { - return new TravelPlanPlace(order, day, name, latitude, longitude); + return new TravelPlanPlace(order, day, name, latitude, longitude, countryCode); } public static TravelPlaceTodo getTravelPlaceTodo(TravelPlanPlace travelPlanPlace, String content, Integer order, @@ -72,7 +73,7 @@ public TravelPlan initTravelPlanTestData() { Member author = initMemberTestData(); TravelPlan travelPlan = getTravelPlan("여행계획", LocalDate.MAX, author); TravelPlanDay travelPlanDay = getTravelPlanDay(0, travelPlan); - TravelPlanPlace travelPlanPlace = getTravelPlanPlace(0, "장소", "37.5175896", "127.0867236", travelPlanDay); + TravelPlanPlace travelPlanPlace = getTravelPlanPlace(0, "장소", "37.5175896", "127.0867236", travelPlanDay, "KR"); TravelPlaceTodo travelPlaceTodo = getTravelPlaceTodo(travelPlanPlace, "테스트짜기", 0, false); travelPlanRepository.save(travelPlan); @@ -86,7 +87,7 @@ public TravelPlan initTravelPlanTestData() { public TravelPlan initTravelPlanTestData(Member author) { TravelPlan travelPlan = getTravelPlan("여행계획", LocalDate.MAX, author); TravelPlanDay travelPlanDay = getTravelPlanDay(0, travelPlan); - TravelPlanPlace travelPlanPlace = getTravelPlanPlace(0, "장소", "37.5175896", "127.0867236", travelPlanDay); + TravelPlanPlace travelPlanPlace = getTravelPlanPlace(0, "장소", "37.5175896", "127.0867236", travelPlanDay, "KR"); TravelPlaceTodo travelPlaceTodo = getTravelPlaceTodo(travelPlanPlace, "테스트짜기", 0, false); travelPlanRepository.save(travelPlan); diff --git a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanFacadeServiceTest.java index 655fda9ee..483922491 100644 --- a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanFacadeServiceTest.java @@ -77,6 +77,7 @@ void createTravelPlan() { .placeName("잠실한강공원") .todos(Collections.EMPTY_LIST) .position(locationRequest) + .countryCode("KR") .build(); PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); PlanRequest request = PlanRequest.builder() @@ -128,6 +129,7 @@ void updateTravelPlan() { .placeName("잠실한강공원") .todos(Collections.EMPTY_LIST) .position(locationRequest) + .countryCode("KR") .build(); PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); PlanRequest request = PlanRequest.builder() diff --git a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java index 598532dab..f2a75e1aa 100644 --- a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java @@ -147,6 +147,7 @@ void updateTravelPlan() { .placeName("잠실한강공원") .todos(Collections.EMPTY_LIST) .position(locationRequest) + .countryCode("KR") .build(); PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); PlanRequest request = PlanRequest.builder() From 41af9023b9a60383503a73361d2a384c6fe74be8 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:56:52 +0900 Subject: [PATCH 11/13] =?UTF-8?q?[Refactor]=20-=20=ED=86=B0=EC=BA=A3=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EA=B0=92=20=EC=A1=B0=EC=A0=95=EC=9D=84=20?= =?UTF-8?q?=ED=86=B5=ED=95=9C=20=EC=95=A0=ED=94=8C=EB=A6=AC=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=84=9C=EB=B2=84=20=EC=84=B1=EB=8A=A5=20?= =?UTF-8?q?=ED=8A=9C=EB=8B=9D=20(#571)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/application.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index f551c4692..4c7dc1837 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -36,6 +36,13 @@ management: spring: jpa: open-in-view: false + +server: + tomcat: + threads: + max: 20 + max-connections: 100 + accept-count: 50 --- # local profile spring: From d5747952ca988de5301215a67ece21e3797e4879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:55:32 +0900 Subject: [PATCH 12/13] =?UTF-8?q?feat:=20travelogueCountry.count=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EC=BF=BC=EB=A6=AC=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?(#575)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/query/TravelogueQueryRepositoryImpl.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java index 2094b66fb..1f8ef4dc8 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java @@ -69,8 +69,7 @@ private void addSearchCondition(JPAQuery<Travelogue> query, SearchCondition cond private void findByCountryCode(JPAQuery<Travelogue> query, CountryCode countryCode) { query.join(travelogueCountry) .on(travelogue.id.eq(travelogueCountry.travelogue.id)) - .where(travelogueCountry.countryCode.eq(countryCode)) - .orderBy(travelogueCountry.count.desc()); + .where(travelogueCountry.countryCode.eq(countryCode)); } private void findByTitleOrAuthor(SearchCondition condition, JPAQuery<Travelogue> query, String keyword) { From 425166a05b2f5571dcc0be5f947f5e49a5f42ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:00:02 +0900 Subject: [PATCH 13/13] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0=20=EB=82=98=EB=9D=BC=20=EA=B4=80=EB=A0=A8=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20(#586)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: CountryCode 내부로 검증 로직 이동 * refactor: 통일성을 위해 SearchType 의 from 메소드 수정 * feat: TravelogueCountry 검증 로직 추가 * style: 통일성을 위해 "나라"를 "국가" 로 변경 * feat: 여행기 장소 생성시 국가 코드가 NONE 인지 검증하는 로직 추가 * feat: 여행기 생성 시 국가 코드가 NONE 인 여행기 장소가 생성되지 않는 기능 구현 * feat: 태그 생성 기능 삭제 * sytle: 통일성을 위해 괄호 수정 Co-authored-by: eunjungL <62099953+eunjungL@users.noreply.github.com> * feat: TravelogueCountry 에 NONE 국가 코드 허용 * feat: 존재하지 않는 국가로 검색 시 빈 여행기 목록을 반환하는 기능 구현 * refactor: 비즈니스 로직을 facade 가 아닌 서비스에서 수행하도록 변경 * test: 존재하지 않는 국가 검색 테스트 코드 추가 --------- Co-authored-by: eunjungL <62099953+eunjungL@users.noreply.github.com> --- .../touroot/tag/controller/TagController.java | 27 --- .../kr/touroot/tag/dto/TagCreateRequest.java | 15 -- .../touroot/tag/repository/TagRepository.java | 2 - .../kr/touroot/tag/service/TagService.java | 17 -- .../travelogue/domain/TravelogueCountry.java | 22 +++ .../travelogue/domain/TraveloguePlace.java | 8 +- .../travelogue/domain/search/CountryCode.java | 9 +- .../domain/search/SearchCondition.java | 4 + .../travelogue/domain/search/SearchType.java | 11 +- .../service/TravelogueCountryService.java | 24 +-- .../service/TravelogueFacadeService.java | 1 + .../travelogue/service/TravelogueService.java | 7 + .../travelplan/domain/TravelPlanPlace.java | 4 +- .../kr/touroot/tag/TagControllerTest.java | 56 ------- .../kr/touroot/tag/fixture/TagFixture.java | 5 - .../kr/touroot/tag/helper/TagTestHelper.java | 27 --- .../touroot/tag/service/TagServiceTest.java | 53 ------ .../domain/TravelogueCountryTest.java | 55 +++++++ .../domain/TraveloguePlaceTest.java | 2 +- .../domain/search/CountryCodeTest.java | 22 ++- .../fixture/TraveloguePlaceFixture.java | 2 +- .../fixture/TravelogueRequestFixture.java | 12 ++ .../helper/TravelogueTestHelper.java | 26 +++ .../service/TravelogueCountryServiceTest.java | 154 ++++++++++++++++++ .../service/TravelogueFacadeServiceTest.java | 22 ++- .../service/TravelogueServiceTest.java | 17 ++ .../domain/TravelPlanPlaceTest.java | 2 +- 27 files changed, 374 insertions(+), 232 deletions(-) delete mode 100644 backend/src/main/java/kr/touroot/tag/dto/TagCreateRequest.java delete mode 100644 backend/src/test/java/kr/touroot/tag/TagControllerTest.java delete mode 100644 backend/src/test/java/kr/touroot/tag/helper/TagTestHelper.java delete mode 100644 backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java create mode 100644 backend/src/test/java/kr/touroot/travelogue/domain/TravelogueCountryTest.java create mode 100644 backend/src/test/java/kr/touroot/travelogue/service/TravelogueCountryServiceTest.java diff --git a/backend/src/main/java/kr/touroot/tag/controller/TagController.java b/backend/src/main/java/kr/touroot/tag/controller/TagController.java index 3dffd2e68..d1ed5e507 100644 --- a/backend/src/main/java/kr/touroot/tag/controller/TagController.java +++ b/backend/src/main/java/kr/touroot/tag/controller/TagController.java @@ -1,23 +1,15 @@ package kr.touroot.tag.controller; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import java.net.URI; import java.util.List; -import kr.touroot.global.exception.dto.ExceptionResponse; -import kr.touroot.tag.dto.TagCreateRequest; import kr.touroot.tag.dto.TagResponse; import kr.touroot.tag.service.TagService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -29,25 +21,6 @@ public class TagController { private final TagService tagService; - @Operation(summary = "태그 생성") - @ApiResponses(value = { - @ApiResponse( - responseCode = "201", - description = "태그가 생성이 정상적으로 성공했을 때" - ), - @ApiResponse( - responseCode = "400", - description = "Body에 유효하지 않은 값이 존재하거나 중복된 태그가 존재할 때", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - }) - @PostMapping - public ResponseEntity<TagResponse> createTag(@Valid @RequestBody TagCreateRequest request) { - TagResponse data = tagService.createTag(request); - return ResponseEntity.created(URI.create("/api/v1/tags/" + data.id())) - .body(data); - } - @Operation(summary = "모든 태그 조회") @ApiResponses(value = { @ApiResponse( diff --git a/backend/src/main/java/kr/touroot/tag/dto/TagCreateRequest.java b/backend/src/main/java/kr/touroot/tag/dto/TagCreateRequest.java deleted file mode 100644 index 5c97f88ff..000000000 --- a/backend/src/main/java/kr/touroot/tag/dto/TagCreateRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package kr.touroot.tag.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; -import kr.touroot.tag.domain.Tag; - -public record TagCreateRequest( - @Schema(description = "태그 이름", example = "강아지와 함께") - @NotEmpty(message = "태그는 비어있을 수 없습니다.") String tag -) { - - public Tag toTag() { - return new Tag(tag); - } -} diff --git a/backend/src/main/java/kr/touroot/tag/repository/TagRepository.java b/backend/src/main/java/kr/touroot/tag/repository/TagRepository.java index 23430a275..c5bfd6b2f 100644 --- a/backend/src/main/java/kr/touroot/tag/repository/TagRepository.java +++ b/backend/src/main/java/kr/touroot/tag/repository/TagRepository.java @@ -4,6 +4,4 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface TagRepository extends JpaRepository<Tag, Long> { - - boolean existsByTag(String tag); } diff --git a/backend/src/main/java/kr/touroot/tag/service/TagService.java b/backend/src/main/java/kr/touroot/tag/service/TagService.java index 645dd53c4..4a95ff97a 100644 --- a/backend/src/main/java/kr/touroot/tag/service/TagService.java +++ b/backend/src/main/java/kr/touroot/tag/service/TagService.java @@ -1,9 +1,6 @@ package kr.touroot.tag.service; import java.util.List; -import kr.touroot.global.exception.BadRequestException; -import kr.touroot.tag.domain.Tag; -import kr.touroot.tag.dto.TagCreateRequest; import kr.touroot.tag.dto.TagResponse; import kr.touroot.tag.repository.TagRepository; import lombok.RequiredArgsConstructor; @@ -19,20 +16,6 @@ public class TagService { private final TagRepository tagRepository; - @Transactional - public TagResponse createTag(TagCreateRequest tagCreateRequest) { - validateDuplicated(tagCreateRequest); - Tag savedTag = tagRepository.save(tagCreateRequest.toTag()); - - return TagResponse.from(savedTag); - } - - private void validateDuplicated(TagCreateRequest tagCreateRequest) { - if (tagRepository.existsByTag(tagCreateRequest.tag())) { - throw new BadRequestException("이미 존재하는 태그입니다."); - } - } - @Cacheable(cacheNames = "tag") @Transactional(readOnly = true) public List<TagResponse> readTags() { diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueCountry.java b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueCountry.java index 503e0b60c..a6a641d05 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueCountry.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueCountry.java @@ -10,6 +10,7 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import kr.touroot.global.exception.BadRequestException; import kr.touroot.travelogue.domain.search.CountryCode; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -24,6 +25,8 @@ @Entity public class TravelogueCountry { + private static final int MIN_COUNT = 1; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -40,8 +43,27 @@ public class TravelogueCountry { private Integer count; public TravelogueCountry(Travelogue travelogue, CountryCode countryCode, Integer count) { + validate(travelogue, countryCode, count); this.travelogue = travelogue; this.countryCode = countryCode; this.count = count; } + + + private void validate(Travelogue travelogue, CountryCode countryCode, Integer count) { + validateNotNull(travelogue, countryCode, count); + validateCount(count); + } + + private void validateNotNull(Travelogue travelogue, CountryCode countryCode, Integer count) { + if (travelogue == null || countryCode == null || count == null) { + throw new BadRequestException("여행기와 국가 코드, 국가 코드의 count 는 null 일 수 없습니다."); + } + } + + private void validateCount(Integer count) { + if (count < MIN_COUNT) { + throw new BadRequestException(String.format("국가 코드의 개수는 %d 보다 커야합니다.", MIN_COUNT)); + } + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java index 680a87913..bec2e4acc 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java @@ -84,7 +84,7 @@ public TraveloguePlace( this.name = name; this.position = position; this.travelogueDay = travelogueDay; - this.countryCode = CountryCode.valueOfIgnoreCase(countryCode); + this.countryCode = CountryCode.from(countryCode); } public TraveloguePlace( @@ -147,11 +147,7 @@ private void validatePlaceNameLength(String placeName) { } private void validateCountryCode(String countryCode) { - try { - CountryCode.valueOfIgnoreCase(countryCode); - } catch (IllegalArgumentException e) { - throw new BadRequestException("존재하지 않는 국가 코드입니다"); - } + CountryCode.from(countryCode); } public void addPhoto(TraveloguePhoto photo) { diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java b/backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java index eaf797e9f..d5ef161dd 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/search/CountryCode.java @@ -2,6 +2,7 @@ import java.util.Arrays; import java.util.Set; +import kr.touroot.global.exception.BadRequestException; public enum CountryCode { @@ -259,7 +260,11 @@ public static CountryCode findByName(String name) { .orElse(NONE); } - public static CountryCode valueOfIgnoreCase(String name) { - return CountryCode.valueOf(name.toUpperCase()); + public static CountryCode from(String code) { + try { + return CountryCode.valueOf(code.toUpperCase()); + } catch (IllegalArgumentException exception) { + throw new BadRequestException("존재하지 않는 국가 코드입니다."); + } } } diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchCondition.java b/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchCondition.java index af30d3794..dc466a49f 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchCondition.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchCondition.java @@ -16,4 +16,8 @@ public SearchCondition(String keyword, SearchType searchType) { public boolean isEmptyCondition() { return keyword == null && searchType == null; } + + public boolean isNoneCountry() { + return searchType == SearchType.COUNTRY && CountryCode.findByName(keyword) == CountryCode.NONE; + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchType.java b/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchType.java index 804bc6321..bfd84557f 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchType.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/search/SearchType.java @@ -1,14 +1,15 @@ package kr.touroot.travelogue.domain.search; -import java.util.Arrays; +import kr.touroot.global.exception.BadRequestException; public enum SearchType { TITLE, AUTHOR, COUNTRY; public static SearchType from(String searchType) { - return Arrays.stream(SearchType.values()) - .filter(type -> searchType.equalsIgnoreCase(type.name())) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 검색 키워드 종류입니다.")); + try { + return SearchType.valueOf(searchType.toUpperCase()); + } catch (IllegalArgumentException exception) { + throw new BadRequestException("존재하지 않는 검색 키워드 종류입니다."); + } } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueCountryService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueCountryService.java index 62b6de825..b28f4212c 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueCountryService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueCountryService.java @@ -19,30 +19,34 @@ public class TravelogueCountryService { private final TravelogueCountryRepository travelogueCountryRepository; - @Transactional(readOnly = true) - public List<TravelogueCountry> readCountryByTravelogue(Travelogue travelogue) { - return travelogueCountryRepository.findAllByTravelogue(travelogue); - } - @Transactional - public void createTravelogueCountries(Travelogue travelogue, TravelogueRequest request) { + public List<TravelogueCountry> createTravelogueCountries(Travelogue travelogue, TravelogueRequest request) { Map<CountryCode, Long> countryCounts = countCountries(request); - countryCounts.forEach((countryCode, count) -> travelogueCountryRepository.save( - new TravelogueCountry(travelogue, countryCode, count.intValue()))); + return countryCounts.entrySet().stream() + .map(entry -> travelogueCountryRepository.save( + new TravelogueCountry(travelogue, entry.getKey(), entry.getValue().intValue())) + ) + .toList(); } private Map<CountryCode, Long> countCountries(TravelogueRequest request) { return request.days().stream() .flatMap(day -> day.places().stream()) .map(place -> CountryCode.valueOf(place.countryCode())) + .filter(countryCode -> countryCode != CountryCode.NONE) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); } + @Transactional(readOnly = true) + public List<TravelogueCountry> getTravelogueCountryByTravelogue(Travelogue travelogue) { + return travelogueCountryRepository.findAllByTravelogue(travelogue); + } + @Transactional - public void updateTravelogueCountries(Travelogue travelogue, TravelogueRequest request) { + public List<TravelogueCountry> updateTravelogueCountries(Travelogue travelogue, TravelogueRequest request) { deleteAllByTravelogue(travelogue); - createTravelogueCountries(travelogue, request); + return createTravelogueCountries(travelogue, request); } @Transactional diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java index c87274790..cae32a9cb 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java @@ -69,6 +69,7 @@ public Page<TravelogueSimpleResponse> findSimpleTravelogues( ) { TravelogueFilterCondition filter = filterRequest.toFilterCondition(); SearchCondition searchCondition = searchRequest.toSearchCondition(); + Page<Travelogue> travelogues = travelogueService.findAll(searchCondition, filter, pageable); return travelogues.map(this::getTravelogueSimpleResponse); diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java index 48cf73f31..df8ce340d 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java @@ -45,6 +45,9 @@ public Page<Travelogue> findAllByMember(Member member, Pageable pageable) { public Page<Travelogue> findByKeyword(TravelogueSearchRequest request, Pageable pageable) { SearchType searchType = SearchType.from(request.searchType()); SearchCondition searchCondition = new SearchCondition(request.keyword(), searchType); + if (searchCondition.isNoneCountry()) { + return Page.empty(); + } return travelogueQueryRepository.findAllBySearchCondition(searchCondition, pageable); } @@ -55,6 +58,10 @@ public Page<Travelogue> findAll( TravelogueFilterCondition filter, Pageable pageable ) { + if (searchCondition.isNoneCountry()) { + return Page.empty(); + } + if (filter.isEmptyCondition() && searchCondition.isEmptyCondition()) { return travelogueRepository.findAll(pageable); } diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java index 11af2c9fe..161068400 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java @@ -71,7 +71,7 @@ public TravelPlanPlace(Long id, Integer order, TravelPlanDay day, String name, P this.day = day; this.name = name; this.position = position; - this.countryCode = CountryCode.valueOfIgnoreCase(countryCode); + this.countryCode = CountryCode.from(countryCode); } public TravelPlanPlace(Integer order, TravelPlanDay day, String name, String latitude, String longitude, @@ -115,7 +115,7 @@ private void validatePlaceNameLength(String placeName) { private void validateCountryCode(String countryCode) { try { - CountryCode.valueOfIgnoreCase(countryCode); + CountryCode.from(countryCode); } catch (IllegalArgumentException e) { throw new BadRequestException("존재하지 않는 국가 코드입니다"); } diff --git a/backend/src/test/java/kr/touroot/tag/TagControllerTest.java b/backend/src/test/java/kr/touroot/tag/TagControllerTest.java deleted file mode 100644 index 53bfbfc07..000000000 --- a/backend/src/test/java/kr/touroot/tag/TagControllerTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package kr.touroot.tag; - -import static org.hamcrest.Matchers.is; - -import io.restassured.RestAssured; -import io.restassured.http.ContentType; -import kr.touroot.global.AcceptanceTest; -import kr.touroot.tag.dto.TagCreateRequest; -import kr.touroot.tag.fixture.TagFixture; -import kr.touroot.tag.helper.TagTestHelper; -import kr.touroot.utils.DatabaseCleaner; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.web.server.LocalServerPort; - -@DisplayName("태그 컨트롤러") -@AcceptanceTest -class TagControllerTest { - - private final DatabaseCleaner databaseCleaner; - private final TagTestHelper testHelper; - - @LocalServerPort - private int port; - - @Autowired - public TagControllerTest(DatabaseCleaner databaseCleaner, TagTestHelper testHelper) { - this.databaseCleaner = databaseCleaner; - this.testHelper = testHelper; - } - - @BeforeEach - void setUp() { - RestAssured.port = port; - databaseCleaner.executeTruncate(); - } - - @DisplayName("태그 컨트롤러는 태그 생성 요청 시 201을 응답한다.") - @Test - void createTag() { - // given - TagCreateRequest request = TagFixture.TAG_1.getCreateRequest(); - - // when & then - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(request) - .when().log().all() - .post("/api/v1/tags") - .then().log().all() - .statusCode(201) - .header("Location", is("/api/v1/tags/1")); - } -} diff --git a/backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java b/backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java index dcd3e47de..b506c705d 100644 --- a/backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java +++ b/backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java @@ -1,7 +1,6 @@ package kr.touroot.tag.fixture; import kr.touroot.tag.domain.Tag; -import kr.touroot.tag.dto.TagCreateRequest; import kr.touroot.tag.dto.TagResponse; public enum TagFixture { @@ -21,10 +20,6 @@ public Tag get() { return new Tag(tag); } - public TagCreateRequest getCreateRequest() { - return new TagCreateRequest(tag); - } - public TagResponse getResponse(Long id) { return new TagResponse(id, tag); } diff --git a/backend/src/test/java/kr/touroot/tag/helper/TagTestHelper.java b/backend/src/test/java/kr/touroot/tag/helper/TagTestHelper.java deleted file mode 100644 index 169ae524a..000000000 --- a/backend/src/test/java/kr/touroot/tag/helper/TagTestHelper.java +++ /dev/null @@ -1,27 +0,0 @@ -package kr.touroot.tag.helper; - -import kr.touroot.tag.domain.Tag; -import kr.touroot.tag.fixture.TagFixture; -import kr.touroot.tag.repository.TagRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class TagTestHelper { - - private final TagRepository tagRepository; - - @Autowired - public TagTestHelper(TagRepository tagRepository) { - this.tagRepository = tagRepository; - } - - public Tag initTagData() { - Tag tag = TagFixture.TAG_1.get();; - return tagRepository.save(tag); - } - - public Tag initTagData(Tag tag) { - return tagRepository.save(tag); - } -} diff --git a/backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java b/backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java deleted file mode 100644 index 5e30d5d06..000000000 --- a/backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package kr.touroot.tag.service; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import kr.touroot.global.ServiceTest; -import kr.touroot.global.config.TestQueryDslConfig; -import kr.touroot.global.exception.BadRequestException; -import kr.touroot.tag.domain.Tag; -import kr.touroot.tag.dto.TagCreateRequest; -import kr.touroot.tag.fixture.TagFixture; -import kr.touroot.tag.helper.TagTestHelper; -import kr.touroot.utils.DatabaseCleaner; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Import; - -@DisplayName("태그 서비스") -@Import(value = {TagService.class, TagTestHelper.class, TestQueryDslConfig.class}) -@ServiceTest -class TagServiceTest { - - private final DatabaseCleaner databaseCleaner; - private final TagTestHelper testHelper; - private final TagService tagService; - - @Autowired - public TagServiceTest(DatabaseCleaner databaseCleaner, TagTestHelper testHelper, TagService tagService) { - this.databaseCleaner = databaseCleaner; - this.testHelper = testHelper; - this.tagService = tagService; - } - - @BeforeEach - void setUp() { - databaseCleaner.executeTruncate(); - } - - @DisplayName("태그 서비스는 중복된 태그 생성 요청시 예외가 발생한다.") - @Test - void validateDuplicated() { - // given - Tag tag = TagFixture.TAG_1.get(); - testHelper.initTagData(tag); - TagCreateRequest request = new TagCreateRequest(tag.getTag()); - - // when & then - assertThatThrownBy(() -> tagService.createTag(request)) - .isInstanceOf(BadRequestException.class) - .hasMessage("이미 존재하는 태그입니다."); - } -} diff --git a/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueCountryTest.java b/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueCountryTest.java new file mode 100644 index 000000000..118aa893e --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueCountryTest.java @@ -0,0 +1,55 @@ +package kr.touroot.travelogue.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.travelogue.domain.search.CountryCode; +import kr.touroot.travelogue.fixture.TravelogueFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class TravelogueCountryTest { + + @DisplayName("검증 규칙에 어긋나지 않는 여행기 생성 시 예외가 발생하지 않는다") + @Test + void createTravelogueCountryWithValidData() { + assertThatCode(() -> new TravelogueCountry(TravelogueFixture.TRAVELOGUE.get(), CountryCode.KR, 1)) + .doesNotThrowAnyException(); + } + + @DisplayName("여행기가 null인 경우 여행기 국가 생성 시 예외가 발생한다") + @Test + void createTravelogueCountryWithNullTravelogue() { + assertThatThrownBy(() -> new TravelogueCountry(null, CountryCode.KR, 1)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행기와 국가 코드, 국가 코드의 count 는 null 일 수 없습니다."); + } + + @DisplayName("국가 코드가 null인 경우 여행기 국가 생성 시 예외가 발생한다") + @Test + void createTravelogueCountryWithNullCountryCode() { + assertThatThrownBy(() -> new TravelogueCountry(TravelogueFixture.TRAVELOGUE.get(), null, 1)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행기와 국가 코드, 국가 코드의 count 는 null 일 수 없습니다."); + } + + @DisplayName("count가 null인 경우 여행기 국가 생성 시 예외가 발생한다") + @Test + void createTravelogueCountryWithNullCount() { + assertThatThrownBy(() -> new TravelogueCountry(TravelogueFixture.TRAVELOGUE.get(), CountryCode.KR, null)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행기와 국가 코드, 국가 코드의 count 는 null 일 수 없습니다."); + } + + @DisplayName("count가 1보다 작은 경우 여행기 국가 생성 시 예외가 발생한다") + @ValueSource(ints = {0, -1}) + @ParameterizedTest + void createTravelogueCountryWithLessThanMinCount(int count) { + assertThatThrownBy(() -> new TravelogueCountry(TravelogueFixture.TRAVELOGUE.get(), CountryCode.KR, count)) + .isInstanceOf(BadRequestException.class) + .hasMessage("국가 코드의 개수는 1 보다 커야합니다."); + } +} diff --git a/backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePlaceTest.java b/backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePlaceTest.java index 395d92ab9..19ff4b643 100644 --- a/backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePlaceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePlaceTest.java @@ -130,7 +130,7 @@ void createPlaceWithInvalidCountryCode() { "SAM-572") ) .isInstanceOf(BadRequestException.class) - .hasMessage("존재하지 않는 국가 코드입니다"); + .hasMessage("존재하지 않는 국가 코드입니다."); } @DisplayName("장소 사진을 추가할 수 있다") diff --git a/backend/src/test/java/kr/touroot/travelogue/domain/search/CountryCodeTest.java b/backend/src/test/java/kr/touroot/travelogue/domain/search/CountryCodeTest.java index 3f4825b4e..429110daa 100644 --- a/backend/src/test/java/kr/touroot/travelogue/domain/search/CountryCodeTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/domain/search/CountryCodeTest.java @@ -1,7 +1,9 @@ package kr.touroot.travelogue.domain.search; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import kr.touroot.global.exception.BadRequestException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -19,7 +21,7 @@ void findByName(String name) { .isEqualTo(CountryCode.KR); } - @DisplayName("없는 나라 이름으로 찾으면 NONE을 반환한다.") + @DisplayName("없는 국가 이름으로 찾으면 NONE을 반환한다.") @Test void findByNonCountryName() { CountryCode code = CountryCode.findByName("미역국"); @@ -27,4 +29,22 @@ void findByNonCountryName() { assertThat(code) .isEqualTo(CountryCode.NONE); } + + @DisplayName("대소문자와 관계없이 국가코드로 찾을 수 있다.") + @ValueSource(strings = {"KR", "kr", "Kr"}) + @ParameterizedTest + void from(String code) { + CountryCode countryCode = CountryCode.from(code); + + assertThat(countryCode) + .isEqualTo(CountryCode.KR); + } + + @DisplayName("존재하지 않는 국가 코드로 찾으면 예외가 발생한다.") + @Test + void fromNotExists() { + assertThatThrownBy(() -> CountryCode.from("WOO")) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 국가 코드입니다."); + } } diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePlaceFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePlaceFixture.java index dc1c359e6..d7fa730c2 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePlaceFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePlaceFixture.java @@ -10,7 +10,7 @@ public enum TraveloguePlaceFixture { TRAVELOGUE_PLACE(1, "에메랄드 빛 해변", "함덕 해수욕장", "34.54343", "126.66977", TRAVELOGUE_DAY.get(), "KR"), - ; + TRAVELOGUE_PLACE_WITH_NONE_COUNTRY_CODE(1, "해변", "함덕", "34.54343", "126.66977", TRAVELOGUE_DAY.get(), "NONE"); private final int order; private final String description; diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java index d38b3bc86..43ed8d60f 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java @@ -59,6 +59,18 @@ public static List<TraveloguePlaceRequest> getTraveloguePlaceRequests(List<Trave )); } + public static List<TraveloguePlaceRequest> getTraveloguePlaceRequestsWithNoneCountryCode( + List<TraveloguePhotoRequest> photos) { + return List.of(new TraveloguePlaceRequest( + "함덕 해수욕장", + getTraveloguePositionRequest(), + "에메랄드 빛 해변", + photos, + "NONE" + )); + } + + public static List<TraveloguePlaceRequest> getUpdateTraveloguePlaceRequests(List<TraveloguePhotoRequest> photos) { return List.of(new TraveloguePlaceRequest( "함덕 해수욕장", diff --git a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index 60a157a9d..23a6b08c3 100644 --- a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -5,6 +5,7 @@ import static kr.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; import static kr.touroot.travelogue.fixture.TraveloguePhotoFixture.TRAVELOGUE_PHOTO; import static kr.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE; +import static kr.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE_WITH_NONE_COUNTRY_CODE; import java.util.List; import kr.touroot.member.domain.LoginType; @@ -79,6 +80,15 @@ public Travelogue initTravelogueTestData() { return initTravelogueTestData(author); } + public Travelogue initTravelogueTestDataWithoutCountryCode() { + Member author = persistMember(); + Travelogue travelogue = persistTravelogue(author); + TravelogueDay day = persistTravelogueDay(travelogue); + TraveloguePlace place = persistTraveloguePlace(day); + persistTraveloguePhoto(place); + return travelogue; + } + public Travelogue initTravelogueTestDataWithSeveralDays() { Member author = persistMember(); return initTravelogueTestDataWithSeveralDays(author); @@ -135,6 +145,16 @@ public Travelogue initTravelogueTestDataWithLike(Member liker) { return travelogue; } + public Travelogue initTravelogueTestDataWithNoneCountryCode() { + Member author = persistMember(); + Travelogue travelogue = persistTravelogue(author); + TravelogueDay day = persistTravelogueDay(travelogue); + TraveloguePlace place = persistTraveloguePlaceWithNoneCountryCode(day); + persistTraveloguePhoto(place); + + return travelogue; + } + private void persisTravelogueTag(Travelogue travelogue, Tag tag) { Tag savedTag = initTagTestData(tag); travelogueTagRepository.save(new TravelogueTag(travelogue, savedTag)); @@ -164,6 +184,12 @@ public TraveloguePlace persistTraveloguePlace(TravelogueDay day) { return traveloguePlaceRepository.save(place); } + public TraveloguePlace persistTraveloguePlaceWithNoneCountryCode(TravelogueDay day) { + TraveloguePlace place = TRAVELOGUE_PLACE_WITH_NONE_COUNTRY_CODE.create(day); + + return traveloguePlaceRepository.save(place); + } + public TravelogueCountry persistTravelogueCountry(Travelogue travelogue) { TravelogueCountry travelogueCountry = TRAVELOGUE_COUNTRY.create(travelogue); diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueCountryServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueCountryServiceTest.java new file mode 100644 index 000000000..9107a627b --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueCountryServiceTest.java @@ -0,0 +1,154 @@ +package kr.touroot.travelogue.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; +import kr.touroot.global.ServiceTest; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueCountry; +import kr.touroot.travelogue.domain.search.CountryCode; +import kr.touroot.travelogue.dto.request.TravelogueDayRequest; +import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; +import kr.touroot.travelogue.dto.request.TravelogueRequest; +import kr.touroot.travelogue.fixture.TravelogueRequestFixture; +import kr.touroot.travelogue.helper.TravelogueTestHelper; +import kr.touroot.utils.DatabaseCleaner; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; + +@DisplayName("여행기 국가 서비스") +@Import(value = {TravelogueCountryService.class, TravelogueTestHelper.class}) +@ServiceTest +class TravelogueCountryServiceTest { + + public static final int BASIC_PAGE_SIZE = 5; + + private final TravelogueCountryService travelogueCountryService; + private final DatabaseCleaner databaseCleaner; + private final TravelogueTestHelper testHelper; + + @Autowired + public TravelogueCountryServiceTest( + TravelogueCountryService travelogueCountryService, + DatabaseCleaner databaseCleaner, + TravelogueTestHelper testHelper + ) { + this.travelogueCountryService = travelogueCountryService; + this.databaseCleaner = databaseCleaner; + this.testHelper = testHelper; + } + + @BeforeEach + void setUp() { + databaseCleaner.executeTruncate(); + } + + @DisplayName("여행기 국가들을 생성할 수 있다.") + @Test + void createTravelogueCountries() { + // given + Travelogue travelogue = testHelper.initTravelogueTestDataWithoutCountryCode(); + TravelogueRequest travelogueRequest = getTravelogueRequest(); + + // when + List<TravelogueCountry> travelogueCountries = + travelogueCountryService.createTravelogueCountries(travelogue, travelogueRequest); + + // then + assertAll( + () -> assertThat(travelogueCountries).hasSize(1), + () -> assertThat(travelogueCountries.get(0).getTravelogue().getId()).isEqualTo(1L), + () -> assertThat(travelogueCountries.get(0).getCount()).isEqualTo(1), + () -> assertThat(travelogueCountries.get(0).getCountryCode()).isEqualTo(CountryCode.KR) + ); + } + + @DisplayName("여행기 장소의 국가 코드가 None 이면 여행기 국가가 생성되지 않는다.") + @Test + void createTravelogueCountriesWithNoneCountryCode() { + // given + Travelogue travelogue = testHelper.initTravelogueTestDataWithNoneCountryCode(); + TravelogueRequest travelogueRequest = getTravelogueRequestWithNoneCountryCode(); + + // when + List<TravelogueCountry> travelogueCountries = + travelogueCountryService.createTravelogueCountries(travelogue, travelogueRequest); + + // then + assertThat(travelogueCountries).isEmpty(); + } + + @DisplayName("여행기의 여행기 국가를 조회할 수 있다.") + @Test + void getTravelogueCountryByTravelogue() { + // given + Travelogue travelogue = testHelper.initTravelogueTestDataWithoutCountryCode(); + TravelogueRequest travelogueRequest = getTravelogueRequest(); + travelogueCountryService.createTravelogueCountries(travelogue, travelogueRequest); + + // when + List<TravelogueCountry> travelogueCountries = travelogueCountryService.getTravelogueCountryByTravelogue( + travelogue); + + // then + assertAll( + () -> assertThat(travelogueCountries).hasSize(1), + () -> assertThat(travelogueCountries.get(0).getTravelogue().getId()).isEqualTo(1L), + () -> assertThat(travelogueCountries.get(0).getCount()).isEqualTo(1), + () -> assertThat(travelogueCountries.get(0).getCountryCode()).isEqualTo(CountryCode.KR) + ); + } + + @DisplayName("여행기 국가를 업데이트 할 수 있다.") + @Test + void updateTravelogueCountries() { + // given + Travelogue travelogue = testHelper.initTravelogueTestDataWithoutCountryCode(); + TravelogueRequest travelogueRequest = getTravelogueRequest(); + Travelogue newTravelogue = testHelper.initTravelogueTestDataWithNoneCountryCode(); + TravelogueRequest newTravelogueRequest = getTravelogueRequestWithNoneCountryCode(); + travelogueCountryService.createTravelogueCountries(travelogue, travelogueRequest); + + // when + List<TravelogueCountry> travelogueCountries = + travelogueCountryService.updateTravelogueCountries(newTravelogue, newTravelogueRequest); + + // then + assertThat(travelogueCountries).isEmpty(); + } + + @DisplayName("여행기 국가를 업데이트 할 수 있다.") + @Test + void deleteAllByTravelogue() { + // given + Travelogue travelogue = testHelper.initTravelogueTestDataWithoutCountryCode(); + TravelogueRequest travelogueRequest = getTravelogueRequest(); + travelogueCountryService.createTravelogueCountries(travelogue, travelogueRequest); + + // when + travelogueCountryService.deleteAllByTravelogue(travelogue); + + // then + assertThat(travelogueCountryService.getTravelogueCountryByTravelogue(travelogue)).isEmpty(); + } + + private TravelogueRequest getTravelogueRequest() { + List<TraveloguePhotoRequest> photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); + List<TraveloguePlaceRequest> places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); + List<TravelogueDayRequest> days = TravelogueRequestFixture.getTravelogueDayRequests(places); + return TravelogueRequestFixture.getTravelogueRequest(days); + } + + private TravelogueRequest getTravelogueRequestWithNoneCountryCode() { + List<TraveloguePhotoRequest> photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); + List<TraveloguePlaceRequest> places = + TravelogueRequestFixture.getTraveloguePlaceRequestsWithNoneCountryCode(photos); + List<TravelogueDayRequest> days = TravelogueRequestFixture.getTravelogueDayRequests(places); + return TravelogueRequestFixture.getTravelogueRequest(days); + } +} diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index ffb2c195d..65becd7e5 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -234,12 +234,32 @@ void findTraveloguesByCountryCodeKeyword() { PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("id")); // when - Page<TravelogueSimpleResponse> searchResults = service.findSimpleTravelogues(filterRequest, searchRequest, pageRequest); + Page<TravelogueSimpleResponse> searchResults = service.findSimpleTravelogues(filterRequest, searchRequest, + pageRequest); // then assertThat(searchResults).containsAll(responses); } + @DisplayName("존재하지 않는 국가를 기반으로 여행기 목록을 조회하면 빈 결과를 반환한다.") + @Test + void findTraveloguesByNoneCountryCodeKeyword() { + // given + testHelper.initAllTravelogueTestData(); + Page<TravelogueSimpleResponse> responses = TravelogueResponseFixture.getTravelogueSimpleResponses(); + + TravelogueSearchRequest searchRequest = new TravelogueSearchRequest("미역국", "country"); + TravelogueFilterRequest filterRequest = new TravelogueFilterRequest(null, null); + PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("id")); + + // when + Page<TravelogueSimpleResponse> searchResults = service.findSimpleTravelogues(filterRequest, searchRequest, + pageRequest); + + // then + assertThat(searchResults).isEmpty(); + } + @DisplayName("여행기를 수정할 수 있다.") @Test void updateTravelogue() { diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java index 20409b85c..5099d030f 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java @@ -125,6 +125,23 @@ void findByKeywordWithNotExistRequest() { assertThat(actual).isEmpty(); } + @DisplayName("존재하지 않는 국가로 여행기를 조회하면 빈 페이지가 반환된다.") + @Test + void findByKeywordWithNotExistCountryRequest() { + // given + testHelper.initTravelogueTestData(); + + SearchCondition searchCondition = new SearchCondition("미역국", SearchType.TITLE); + TravelogueFilterCondition filter = new TravelogueFilterCondition(null, null); + PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("createdAt")); + + // when + Page<Travelogue> actual = travelogueService.findAll(searchCondition, filter, pageRequest); + + // then + assertThat(actual).isEmpty(); + } + @DisplayName("여행기를 수정할 수 있다.") @Test void updateTravelogue() { diff --git a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java index 7c7e1cc79..2aa2b8ba9 100644 --- a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java @@ -102,7 +102,7 @@ void createPlaceWithInvalidCountryCode() { assertThatThrownBy( () -> new TravelPlanPlace(VALID_ORDER, VALID_DAY, VALID_NAME, VALID_LAT, VALID_LNG, "CONCODE")) .isInstanceOf(BadRequestException.class) - .hasMessage("존재하지 않는 국가 코드입니다"); + .hasMessage("존재하지 않는 국가 코드입니다."); } @DisplayName("Todo를 추가할 수 있다")