diff --git a/README.md b/README.md index 913012b3a..6316eb203 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,92 @@ -# 2023-hang-log +
- -![Group 2695 (1)](https://github.com/woowacourse-teams/2023-hang-log/assets/77482065/cfddda8a-e416-4f03-bc53-edd6eb0c4595) +## [행록 바로가기](https://hanglog.com) + -## 멤버 +
+
+
+
+ + + +
+ +![image2](https://github.com/woowacourse-teams/2023-hang-log/assets/49433615/2981c210-cda3-4644-887f-45ee14268767) + +![image](https://github.com/woowacourse-teams/2023-hang-log/assets/49433615/c2fc4007-d9ba-4f6b-9f64-04c29fc46078) + +![image4](https://github.com/woowacourse-teams/2023-hang-log/assets/49433615/29c24162-8702-4b47-87f6-0b703b4060fb) + +![image5](https://github.com/woowacourse-teams/2023-hang-log/assets/49433615/1205a54a-58c1-4b63-8062-c89274658f9d) + +![image6](https://github.com/woowacourse-teams/2023-hang-log/assets/49433615/6c4ab178-b9ca-4141-a904-f493a68ca082) + +![image7](https://github.com/woowacourse-teams/2023-hang-log/assets/49433615/b3829bde-7870-4836-adab-3d1add7b42a1) + +![image8](https://github.com/woowacourse-teams/2023-hang-log/assets/102305630/bc5d2fc3-1cbc-4cf6-9602-c63872034f6c) + +### [행록 바로가기 ](https://hanglog.com) + +
+ +
+ +
+ +## 기술 스택 ### 프론트엔드 -| | | | -|:-----------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------:| -| [슬링키](https://github.com/dladncks1217) | [애슐리](https://github.com/ashleysyheo) | [헤다](https://github.com/Dahyeeee) | +스크린샷 2023-10-18 오후 5 32 31 ### 백엔드 -| | | | | | -|:-----------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------:| -| [이오](https://github.com/LJW25) | [디노](https://github.com/jjongwa) | [라온](https://github.com/mcodnjs) | [홍고](https://github.com/hgo641) | [달리](https://github.com/waterricecake) | +스크린샷 2023-10-18 오후 5 32 13 + +### 인프라 + +스크린샷 2023-10-18 오후 5 32 49 + +
+## 서비스 요청 흐름도 -![‎README 인프라 구조 ‎008](https://github.com/woowacourse-teams/2023-hang-log/assets/77482065/eb0d792c-9699-4010-95b4-a29e5ab76687) +![서비스요청흐름도](https://github.com/woowacourse-teams/2023-hang-log/assets/102305630/dc9c1562-068d-4c73-84ef-d93e9051b679) -![‎README 인프라 구조 ‎007](https://github.com/woowacourse-teams/2023-hang-log/assets/77482065/e9e38e63-ad72-45cc-80f8-883ae68aa29e) +## 인프라 구조도 -![‎README 인프라 구조 ‎001](https://github.com/woowacourse-teams/2023-hang-log/assets/77482065/1e73f647-7774-450e-863c-7dbc906f5e4d) +![인프라 구조도](https://github.com/woowacourse-teams/2023-hang-log/assets/102305630/656caaa3-125d-48ed-b996-e2858be4d36c) + +## CI/CD + +![CICD](https://github.com/woowacourse-teams/2023-hang-log/assets/64852591/a55b3a1c-ce12-49d2-b4da-5b394c4de6c1) + +## 모니터링 구조도 + +![모니터링 구조도](https://github.com/woowacourse-teams/2023-hang-log/assets/64852591/26da0064-7caf-42a7-b341-4e1f9db99865) + +## 이미지 요청 흐름도 + +![이미지 요청 흐름도](https://github.com/woowacourse-teams/2023-hang-log/assets/64852591/65cdfaea-e546-43ab-80b3-2c57c9336544) + +## [행록 디자인 시스템](https://github.com/hang-log-design-system/design-system) + +![행록디자인시스템](https://github.com/woowacourse-teams/2023-hang-log/assets/49433615/23457a14-fb21-498c-9b65-a6c92826a0c3) + +
+ +## 멤버 + +### 프론트엔드 + +| | | | +| :---------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------: | +| [슬링키](https://github.com/dladncks1217) | [애슐리](https://github.com/ashleysyheo) | [헤다](https://github.com/Dahyeeee) | + +### 백엔드 + +| | | | | | +| :---------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------: | +| [이오](https://github.com/LJW25) | [디노](https://github.com/jjongwa) | [라온](https://github.com/mcodnjs) | [홍고](https://github.com/hgo641) | [달리](https://github.com/waterricecake) | diff --git a/backend/backend-submodule b/backend/backend-submodule index 251536b9c..c8ac913af 160000 --- a/backend/backend-submodule +++ b/backend/backend-submodule @@ -1 +1 @@ -Subproject commit 251536b9ca71fca672f25d5685a3526634739c22 +Subproject commit c8ac913af608cbaffc233280c25923ff129bf317 diff --git a/backend/build.gradle b/backend/build.gradle index 056e6bebe..0d6c59415 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -28,7 +28,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'com.amazonaws:aws-java-sdk-s3:1.12.528' @@ -90,7 +89,7 @@ processResources.dependsOn('copySecret') tasks.register('copySecret', Copy) { from './backend-submodule' - include 'application.yml' + include 'application*.yml' into './src/main/resources' } diff --git a/backend/src/docs/asciidoc/docs.adoc b/backend/src/docs/asciidoc/docs.adoc index 12f62301c..b0942cb60 100644 --- a/backend/src/docs/asciidoc/docs.adoc +++ b/backend/src/docs/asciidoc/docs.adoc @@ -88,6 +88,27 @@ include::{snippets}/trip-controller-test/delete-trip/path-parameters.adoc[] ==== 응답 include::{snippets}/trip-controller-test/delete-trip/http-response.adoc[] +=== 가계부 조회 (GET /trips/:tripId/expense) + +==== 요청 +include::{snippets}/trip-controller-test/get-ledger/http-request.adoc[] +include::{snippets}/trip-controller-test/get-ledger/path-parameters.adoc[] + +==== 응답 +include::{snippets}/trip-controller-test/get-ledger/http-response.adoc[] +include::{snippets}/trip-controller-test/get-ledger/response-fields.adoc[] + +=== 공유 상태 수정 (PATCH /trips/:tripId/share) + +==== 요청 +include::{snippets}/trip-controller-test/update-shared-status/http-request.adoc[] +include::{snippets}/trip-controller-test/update-shared-status/path-parameters.adoc[] +include::{snippets}/trip-controller-test/update-shared-status/request-fields.adoc[] + +==== 응답 +include::{snippets}/trip-controller-test/update-shared-status/http-response.adoc[] +include::{snippets}/trip-controller-test/update-shared-status/path-parameters.adoc[] + === 공개 상태 수정 (PATCH /trips/:tripId/publish) @@ -210,56 +231,57 @@ include::{snippets}/category-controller-test/get-expense-categories/http-request include::{snippets}/category-controller-test/get-expense-categories/http-response.adoc[] include::{snippets}/category-controller-test/get-expense-categories/response-fields.adoc[] -== 경비 API - -=== 경비 조회 (GET /trips/:tripId/expense) - -==== 요청 -include::{snippets}/expense-controller-test/get-expenses/http-request.adoc[] -include::{snippets}/expense-controller-test/get-expenses/path-parameters.adoc[] - -==== 응답 -include::{snippets}/expense-controller-test/get-expenses/http-response.adoc[] -include::{snippets}/expense-controller-test/get-expenses/response-fields.adoc[] - == 멤버 API === 로그인 (POST /login/:provider) ==== 요청 -include::{snippets}/auth-controller-test/login/http-request.adoc[] -include::{snippets}/auth-controller-test/login/path-parameters.adoc[] -include::{snippets}/auth-controller-test/login/request-fields.adoc[] +include::{snippets}/login-controller-test/login/http-request.adoc[] +include::{snippets}/login-controller-test/login/path-parameters.adoc[] +include::{snippets}/login-controller-test/login/request-fields.adoc[] ==== 응답 -include::{snippets}/auth-controller-test/login/http-response.adoc[] +include::{snippets}/login-controller-test/login/http-response.adoc[] 응답 쿠키 -include::{snippets}/auth-controller-test/login/response-cookies.adoc[] -include::{snippets}/auth-controller-test/login/response-fields.adoc[] +include::{snippets}/login-controller-test/login/response-cookies.adoc[] +include::{snippets}/login-controller-test/login/response-fields.adoc[] === 토큰 재발급 (POST /token) ==== 요청 -include::{snippets}/auth-controller-test/extend-login/http-request.adoc[] +include::{snippets}/login-controller-test/extend-login/http-request.adoc[] 요청 쿠키 -include::{snippets}/auth-controller-test/extend-login/request-cookies.adoc[] -include::{snippets}/auth-controller-test/extend-login/request-fields.adoc[] +include::{snippets}/login-controller-test/extend-login/request-cookies.adoc[] +include::{snippets}/login-controller-test/extend-login/request-fields.adoc[] ==== 응답 -include::{snippets}/auth-controller-test/extend-login/http-response.adoc[] -include::{snippets}/auth-controller-test/extend-login/response-fields.adoc[] +include::{snippets}/login-controller-test/extend-login/http-response.adoc[] +include::{snippets}/login-controller-test/extend-login/response-fields.adoc[] === 로그아웃 (POST /logout) ==== 요청 -include::{snippets}/auth-controller-test/logout/http-request.adoc[] +include::{snippets}/login-controller-test/logout/http-request.adoc[] +요청 헤더 +include::{snippets}/login-controller-test/logout/request-headers.adoc[] +요청 쿠키 +include::{snippets}/login-controller-test/logout/request-cookies.adoc[] + +==== 응답 +include::{snippets}/login-controller-test/logout/http-response.adoc[] + +=== 회원 탈퇴 (DELETE /account) + +==== 요청 +include::{snippets}/login-controller-test/delete-account/http-request.adoc[] 요청 헤더 -include::{snippets}/auth-controller-test/logout/request-headers.adoc[] +include::{snippets}/login-controller-test/delete-account/request-headers.adoc[] 요청 쿠키 -include::{snippets}/auth-controller-test/logout/request-cookies.adoc[] +include::{snippets}/login-controller-test/delete-account/request-cookies.adoc[] ==== 응답 -include::{snippets}/auth-controller-test/logout/http-response.adoc[] +include::{snippets}/login-controller-test/delete-account/http-response.adoc[] + === 마이 페이지 조회 (GET /mypage) @@ -298,17 +320,6 @@ include::{snippets}/shared-trip-controller-test/get-shared-trip/path-parameters. include::{snippets}/shared-trip-controller-test/get-shared-trip/http-response.adoc[] include::{snippets}/shared-trip-controller-test/get-shared-trip/response-fields.adoc[] -=== 공유 상태 수정 (PATCH /trips/:tripId/share) - -==== 요청 -include::{snippets}/shared-trip-controller-test/update-shared-status/http-request.adoc[] -include::{snippets}/shared-trip-controller-test/update-shared-status/path-parameters.adoc[] -include::{snippets}/shared-trip-controller-test/update-shared-status/request-fields.adoc[] - -==== 응답 -include::{snippets}/shared-trip-controller-test/update-shared-status/http-response.adoc[] -include::{snippets}/shared-trip-controller-test/update-shared-status/path-parameters.adoc[] - === 공유된 여행 경비 조회 (GET /shared-trips/:shareCode/expense) ==== 요청 diff --git a/backend/src/main/java/hanglog/HangLogApplication.java b/backend/src/main/java/hanglog/HangLogApplication.java index 4fca7ba07..e0095b058 100644 --- a/backend/src/main/java/hanglog/HangLogApplication.java +++ b/backend/src/main/java/hanglog/HangLogApplication.java @@ -3,8 +3,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; +@EnableAsync @EnableJpaAuditing @EnableScheduling @SpringBootApplication diff --git a/backend/src/main/java/hanglog/auth/domain/repository/RefreshTokenRepository.java b/backend/src/main/java/hanglog/auth/domain/repository/RefreshTokenRepository.java deleted file mode 100644 index 8915084dc..000000000 --- a/backend/src/main/java/hanglog/auth/domain/repository/RefreshTokenRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package hanglog.auth.domain.repository; - -import hanglog.auth.domain.RefreshToken; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface RefreshTokenRepository extends JpaRepository { - - Optional findByToken(final String token); - - boolean existsByToken(final String token); - - void deleteByMemberId(final Long memberId); -} diff --git a/backend/src/main/java/hanglog/category/domain/Category.java b/backend/src/main/java/hanglog/category/domain/Category.java index 411f4faee..438e162ab 100644 --- a/backend/src/main/java/hanglog/category/domain/Category.java +++ b/backend/src/main/java/hanglog/category/domain/Category.java @@ -6,6 +6,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; +import java.util.Objects; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -28,4 +29,21 @@ public class Category extends BaseEntity { @Column(nullable = false, length = 50) private String korName; + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Category category)) { + return false; + } + return Objects.equals(id, category.id) && Objects.equals(engName, category.engName) + && Objects.equals(korName, category.korName); + } + + @Override + public int hashCode() { + return Objects.hash(id, engName, korName); + } } diff --git a/backend/src/main/java/hanglog/category/domain/ExpenseCategories.java b/backend/src/main/java/hanglog/category/domain/ExpenseCategories.java new file mode 100644 index 000000000..ce6a8b883 --- /dev/null +++ b/backend/src/main/java/hanglog/category/domain/ExpenseCategories.java @@ -0,0 +1,20 @@ +package hanglog.category.domain; + +import java.util.List; +import java.util.Optional; + +public class ExpenseCategories { + + private static List expenseCategories; + + private ExpenseCategories() { + } + + public static Optional> get() { + return Optional.ofNullable(expenseCategories); + } + + public static void init(final List expenseCategories) { + ExpenseCategories.expenseCategories = expenseCategories; + } +} diff --git a/backend/src/main/java/hanglog/category/service/CategoryService.java b/backend/src/main/java/hanglog/category/service/CategoryService.java index e7bdc5568..bb2fecba3 100644 --- a/backend/src/main/java/hanglog/category/service/CategoryService.java +++ b/backend/src/main/java/hanglog/category/service/CategoryService.java @@ -10,11 +10,12 @@ @Service @RequiredArgsConstructor -@Transactional(readOnly = true) +@Transactional public class CategoryService { private final CategoryRepository categoryRepository; + @Transactional(readOnly = true) public List getExpenseCategories() { final List expenseCategories = categoryRepository.findExpenseCategory(); return expenseCategories.stream() diff --git a/backend/src/main/java/hanglog/city/domain/repository/CityRepository.java b/backend/src/main/java/hanglog/city/domain/repository/CityRepository.java index 04b45d7ff..1f4340b6e 100644 --- a/backend/src/main/java/hanglog/city/domain/repository/CityRepository.java +++ b/backend/src/main/java/hanglog/city/domain/repository/CityRepository.java @@ -1,7 +1,25 @@ package hanglog.city.domain.repository; import hanglog.city.domain.City; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface CityRepository extends JpaRepository { + + @Query(""" + SELECT c + FROM City c + WHERE c.id in :ids + """) + List findCitiesByIds(@Param("ids") final List ids); + + @Query(""" + SELECT c + FROM City c, TripCity tc + WHERE c.id = tc.city.id + AND tc.trip.id = :tripId + """) + List findCitiesByTripId(@Param("tripId") final Long tripId); } diff --git a/backend/src/main/java/hanglog/city/service/CityService.java b/backend/src/main/java/hanglog/city/service/CityService.java index a0f864707..5649c40b8 100644 --- a/backend/src/main/java/hanglog/city/service/CityService.java +++ b/backend/src/main/java/hanglog/city/service/CityService.java @@ -1,8 +1,8 @@ package hanglog.city.service; -import hanglog.city.dto.response.CityResponse; import hanglog.city.domain.City; import hanglog.city.domain.repository.CityRepository; +import hanglog.city.dto.response.CityResponse; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -15,6 +15,7 @@ public class CityService { private final CityRepository cityRepository; + @Transactional(readOnly = true) public List getAllCities() { final List cities = cityRepository.findAll(); return cities.stream() diff --git a/backend/src/main/java/hanglog/community/domain/BaseTripInfo.java b/backend/src/main/java/hanglog/community/domain/BaseTripInfo.java deleted file mode 100644 index 85eee6974..000000000 --- a/backend/src/main/java/hanglog/community/domain/BaseTripInfo.java +++ /dev/null @@ -1,14 +0,0 @@ -package hanglog.community.domain; - -public class BaseTripInfo implements TripInfo { - - @Override - public Long getLikeCount() { - return 0L; - } - - @Override - public Boolean getIsLike() { - return false; - } -} diff --git a/backend/src/main/java/hanglog/community/domain/PublishedTrip.java b/backend/src/main/java/hanglog/community/domain/PublishedTrip.java index 9ea6183b4..c4ed30601 100644 --- a/backend/src/main/java/hanglog/community/domain/PublishedTrip.java +++ b/backend/src/main/java/hanglog/community/domain/PublishedTrip.java @@ -1,16 +1,13 @@ package hanglog.community.domain; -import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; import hanglog.global.BaseEntity; -import hanglog.trip.domain.Trip; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToOne; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -29,11 +26,10 @@ public class PublishedTrip extends BaseEntity { @GeneratedValue(strategy = IDENTITY) private Long id; - @OneToOne(fetch = LAZY) - @JoinColumn(name = "trip_id", nullable = false) - private Trip trip; + @Column(nullable = false) + private Long tripId; - public PublishedTrip(final Trip trip) { - this(null, trip); + public PublishedTrip(final Long tripId) { + this(null, tripId); } } diff --git a/backend/src/main/java/hanglog/community/domain/TripInfo.java b/backend/src/main/java/hanglog/community/domain/TripInfo.java deleted file mode 100644 index c6dd2cb99..000000000 --- a/backend/src/main/java/hanglog/community/domain/TripInfo.java +++ /dev/null @@ -1,8 +0,0 @@ -package hanglog.community.domain; - -public interface TripInfo { - - Long getLikeCount(); - - Boolean getIsLike(); -} diff --git a/backend/src/main/java/hanglog/community/domain/repository/LikeRepository.java b/backend/src/main/java/hanglog/community/domain/repository/LikeRepository.java deleted file mode 100644 index 4239fa0dc..000000000 --- a/backend/src/main/java/hanglog/community/domain/repository/LikeRepository.java +++ /dev/null @@ -1,37 +0,0 @@ -package hanglog.community.domain.repository; - -import hanglog.community.domain.Likes; -import hanglog.community.domain.TripInfo; -import java.util.List; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -public interface LikeRepository extends JpaRepository { - - boolean existsByMemberIdAndTripId(final Long memberId, final Long tripId); - - void deleteByMemberIdAndTripId(final Long memberId, final Long tripId); - - Long countLikesByTripId(final Long tripId); - - @Query(""" - SELECT l.tripId, - COUNT(l.memberId) AS like_count, - EXISTS(SELECT 1 FROM Likes l_1 WHERE l_1.memberId = :memberId AND l_1.tripId = l.tripId) AS is_like - FROM Likes l - WHERE l.tripId in :tripIds - GROUP BY l.tripId - """) - List countByMemberIdAndTripId(@Param("memberId") Long memberId, @Param("tripIds") List tripIds); - - @Query(""" - SELECT COUNT(l.memberId) AS like_count, - EXISTS(SELECT 1 FROM Likes l_1 WHERE l_1.memberId = :memberId AND l_1.tripId = l.tripId) AS is_like - FROM Likes l - WHERE l.tripId = :tripId - GROUP BY l.tripId - """) - Optional countByMemberIdAndTripId(@Param("memberId") Long memberId, @Param("tripId") Long tripId); -} diff --git a/backend/src/main/java/hanglog/community/domain/repository/PublishedTripRepository.java b/backend/src/main/java/hanglog/community/domain/repository/PublishedTripRepository.java new file mode 100644 index 000000000..b90482012 --- /dev/null +++ b/backend/src/main/java/hanglog/community/domain/repository/PublishedTripRepository.java @@ -0,0 +1,32 @@ +package hanglog.community.domain.repository; + +import hanglog.community.domain.PublishedTrip; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface PublishedTripRepository extends JpaRepository { + + boolean existsByTripId(final Long tripId); + + Optional findByTripId(final Long tripId); + + @Modifying + @Query(""" + UPDATE PublishedTrip publishedTrip + SET publishedTrip.status = 'DELETED' + WHERE publishedTrip.tripId = :tripId + """) + void deleteByTripId(@Param("tripId") final Long tripId); + + @Modifying + @Query(""" + UPDATE PublishedTrip publishedTrip + SET publishedTrip.status = 'DELETED' + WHERE publishedTrip.tripId IN :tripIds + """) + void deleteByTripIds(@Param("tripIds") final List tripIds); +} diff --git a/backend/src/main/java/hanglog/community/dto/response/CommunityTripDetailResponse.java b/backend/src/main/java/hanglog/community/dto/response/CommunityTripDetailResponse.java new file mode 100644 index 000000000..c901a5125 --- /dev/null +++ b/backend/src/main/java/hanglog/community/dto/response/CommunityTripDetailResponse.java @@ -0,0 +1,66 @@ +package hanglog.community.dto.response; + +import static lombok.AccessLevel.PRIVATE; + +import hanglog.city.domain.City; +import hanglog.city.dto.response.CityWithPositionResponse; +import hanglog.member.dto.response.WriterResponse; +import hanglog.trip.domain.Trip; +import hanglog.trip.dto.response.DayLogResponse; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor(access = PRIVATE) +public class CommunityTripDetailResponse { + + private final Long id; + private final WriterResponse writer; + private final Boolean isWriter; + private final String title; + private final LocalDate startDate; + private final LocalDate endDate; + private final String description; + private final String imageName; + private final Boolean isLike; + private final Long likeCount; + private final LocalDateTime publishedDate; + private final List cities; + private final List dayLogs; + + public static CommunityTripDetailResponse of( + final Trip trip, + final List cities, + final Boolean isWriter, + final Boolean isLike, + final Long likeCount, + final LocalDateTime publishedDate + ) { + final List dayLogResponses = trip.getDayLogs().stream() + .map(DayLogResponse::of) + .toList(); + + final List cityWithPositionResponses = cities.stream() + .map(CityWithPositionResponse::of) + .toList(); + + return new CommunityTripDetailResponse( + trip.getId(), + WriterResponse.of(trip.getMember()), + isWriter, + trip.getTitle(), + trip.getStartDate(), + trip.getEndDate(), + trip.getDescription(), + trip.getImageName(), + isLike, + likeCount, + publishedDate, + cityWithPositionResponses, + dayLogResponses + ); + } +} diff --git a/backend/src/main/java/hanglog/community/dto/response/CommunityTripResponse.java b/backend/src/main/java/hanglog/community/dto/response/CommunityTripResponse.java index 23810706d..7387504da 100644 --- a/backend/src/main/java/hanglog/community/dto/response/CommunityTripResponse.java +++ b/backend/src/main/java/hanglog/community/dto/response/CommunityTripResponse.java @@ -1,11 +1,10 @@ package hanglog.community.dto.response; -import static hanglog.image.util.ImageUrlConverter.convertNameToUrl; import static lombok.AccessLevel.PRIVATE; import hanglog.city.domain.City; import hanglog.city.dto.response.CityResponse; -import hanglog.share.dto.response.WriterResponse; +import hanglog.member.dto.response.WriterResponse; import hanglog.trip.domain.Trip; import java.time.LocalDate; import java.util.List; @@ -23,7 +22,7 @@ public class CommunityTripResponse { private final LocalDate startDate; private final LocalDate endDate; private final String description; - private final String imageUrl; + private final String imageName; private final Boolean isLike; private final Long likeCount; @@ -45,7 +44,7 @@ public static CommunityTripResponse of( trip.getStartDate(), trip.getEndDate(), trip.getDescription(), - convertNameToUrl(trip.getImageName()), + trip.getImageName(), isLike, likeCount ); diff --git a/backend/src/main/java/hanglog/community/presentation/CommunityController.java b/backend/src/main/java/hanglog/community/presentation/CommunityController.java index 436fa00b8..2e53fa4db 100644 --- a/backend/src/main/java/hanglog/community/presentation/CommunityController.java +++ b/backend/src/main/java/hanglog/community/presentation/CommunityController.java @@ -7,9 +7,9 @@ import hanglog.community.dto.response.CommunityTripListResponse; import hanglog.community.dto.response.RecommendTripListResponse; import hanglog.community.service.CommunityService; -import hanglog.expense.dto.response.TripExpenseResponse; -import hanglog.expense.service.ExpenseService; +import hanglog.trip.dto.response.LedgerResponse; import hanglog.trip.dto.response.TripDetailResponse; +import hanglog.trip.service.LedgerService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; @@ -25,14 +25,14 @@ public class CommunityController { private final CommunityService communityService; - private final ExpenseService expenseService; + private final LedgerService ledgerService; @GetMapping("/trips") public ResponseEntity getTrips( @Auth final Accessor accessor, @PageableDefault(sort = "publishedTrip.id", direction = DESC) final Pageable pageable ) { - final CommunityTripListResponse communityTripListResponse = communityService.getTripsByPage(accessor, pageable); + final CommunityTripListResponse communityTripListResponse = communityService.getCommunityTripsByPage(accessor, pageable); return ResponseEntity.ok().body(communityTripListResponse); } @@ -55,8 +55,8 @@ public ResponseEntity getTrip( } @GetMapping("/trips/{tripId}/expense") - public ResponseEntity getExpenses(@PathVariable final Long tripId) { - final TripExpenseResponse tripExpenseResponse = expenseService.getAllExpenses(tripId); - return ResponseEntity.ok().body(tripExpenseResponse); + public ResponseEntity getExpenses(@PathVariable final Long tripId) { + final LedgerResponse ledgerResponse = ledgerService.getAllExpenses(tripId); + return ResponseEntity.ok().body(ledgerResponse); } } diff --git a/backend/src/main/java/hanglog/community/service/CommunityService.java b/backend/src/main/java/hanglog/community/service/CommunityService.java index d697baabd..cd7454a97 100644 --- a/backend/src/main/java/hanglog/community/service/CommunityService.java +++ b/backend/src/main/java/hanglog/community/service/CommunityService.java @@ -1,34 +1,40 @@ package hanglog.community.service; import static hanglog.community.domain.recommendstrategy.RecommendType.LIKE; -import static hanglog.community.domain.type.PublishedStatusType.PUBLISHED; import static hanglog.global.exception.ExceptionCode.NOT_FOUND_TRIP_ID; +import static hanglog.trip.domain.type.PublishedStatusType.PUBLISHED; import hanglog.auth.domain.Accessor; import hanglog.city.domain.City; +import hanglog.city.domain.repository.CityRepository; import hanglog.community.domain.recommendstrategy.RecommendStrategies; import hanglog.community.domain.recommendstrategy.RecommendStrategy; -import hanglog.community.domain.repository.LikeRepository; +import hanglog.community.domain.repository.PublishedTripRepository; import hanglog.community.dto.response.CommunityTripListResponse; import hanglog.community.dto.response.CommunityTripResponse; import hanglog.community.dto.response.RecommendTripListResponse; import hanglog.global.exception.BadRequestException; +import hanglog.like.domain.LikeInfo; +import hanglog.like.dto.LikeElements; +import hanglog.like.repository.LikeRepository; import hanglog.trip.domain.Trip; -import hanglog.trip.domain.TripCity; -import hanglog.trip.domain.repository.PublishedTripRepository; import hanglog.trip.domain.repository.TripCityRepository; import hanglog.trip.domain.repository.TripRepository; +import hanglog.trip.dto.TripCityElements; import hanglog.trip.dto.response.TripDetailResponse; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor -@Transactional(readOnly = true) +@Transactional +@Slf4j public class CommunityService { private static final int RECOMMEND_AMOUNT = 5; @@ -36,82 +42,96 @@ public class CommunityService { private final LikeRepository likeRepository; private final TripRepository tripRepository; private final TripCityRepository tripCityRepository; + private final CityRepository cityRepository; private final RecommendStrategies recommendStrategies; private final PublishedTripRepository publishedTripRepository; - public CommunityTripListResponse getTripsByPage(final Accessor accessor, final Pageable pageable) { + @Transactional(readOnly = true) + public CommunityTripListResponse getCommunityTripsByPage(final Accessor accessor, final Pageable pageable) { final List trips = tripRepository.findPublishedTripByPageable(pageable.previousOrFirst()); - final List communityTripResponse = trips.stream() - .map(trip -> getTripResponse(accessor, trip)) - .toList(); + final List communityTripResponses = getCommunityTripResponses(accessor, trips); final Long lastPageIndex = getLastPageIndex(pageable.getPageSize()); + return new CommunityTripListResponse(communityTripResponses, lastPageIndex); + } + + @Transactional(readOnly = true) + public RecommendTripListResponse getRecommendTrips(final Accessor accessor) { + final RecommendStrategy recommendStrategy = recommendStrategies.mapByRecommendType(LIKE); + final Pageable pageable = Pageable.ofSize(RECOMMEND_AMOUNT); + final List trips = recommendStrategy.recommend(pageable); + final List communityTripResponses = getCommunityTripResponses(accessor, trips); + return new RecommendTripListResponse(recommendStrategy.getTitle(), communityTripResponses); + } + + private List getCommunityTripResponses(final Accessor accessor, final List trips) { + final List tripIds = trips.stream().map(Trip::getId).toList(); + + final TripCityElements tripCityElements = new TripCityElements( + tripCityRepository.findTripIdAndCitiesByTripIds(tripIds) + ); + final Map> citiesByTrip = tripCityElements.toCityMap(); + + final LikeElements likeElements = new LikeElements(likeRepository.findLikeCountAndIsLikeByTripIds( + accessor.getMemberId(), + tripIds + )); + final Map likeInfoByTrip = likeElements.toLikeMap(); - return new CommunityTripListResponse(communityTripResponse, lastPageIndex); + return trips.stream() + .map(trip -> CommunityTripResponse.of( + trip, + citiesByTrip.get(trip.getId()), + isLike(likeInfoByTrip, trip.getId()), + getLikeCount(likeInfoByTrip, trip.getId()) + )).toList(); } - private CommunityTripResponse getTripResponse(final Accessor accessor, final Trip trip) { - final List cities = getCitiesByTripId(trip.getId()); - final Long likeCount = likeRepository.countLikesByTripId(trip.getId()); - if (accessor.isMember()) { - final boolean isLike = likeRepository.existsByMemberIdAndTripId(accessor.getMemberId(), trip.getId()); - return CommunityTripResponse.of(trip, cities, isLike, likeCount); + private boolean isLike(final Map likeInfoByTrip, final Long tripId) { + final LikeInfo likeInfo = likeInfoByTrip.get(tripId); + if (likeInfo == null) { + return false; } - return CommunityTripResponse.of(trip, cities, false, likeCount); + return likeInfo.isLike(); } - private List getCitiesByTripId(final Long tripId) { - return tripCityRepository.findByTripId(tripId).stream() - .map(TripCity::getCity) - .toList(); + private Long getLikeCount(final Map likeInfoByTrip, final Long tripId) { + final LikeInfo likeInfo = likeInfoByTrip.get(tripId); + if (likeInfo == null) { + return 0L; + } + return likeInfo.getLikeCount(); } private Long getLastPageIndex(final int pageSize) { final Long totalTripCount = tripRepository.countTripByPublishedStatus(PUBLISHED); - final Long lastPageIndex = totalTripCount / pageSize; + final long lastPageIndex = totalTripCount / pageSize; if (totalTripCount % pageSize == 0) { return lastPageIndex; } return lastPageIndex + 1; } - public RecommendTripListResponse getRecommendTrips(final Accessor accessor) { - final RecommendStrategy recommendStrategy = recommendStrategies.mapByRecommendType(LIKE); - final Pageable pageable = Pageable.ofSize(RECOMMEND_AMOUNT); - final List trips = recommendStrategy.recommend(pageable); - - final List communityTripResponses = trips.stream() - .map(trip -> getTripResponse(accessor, trip)) - .toList(); - - return new RecommendTripListResponse(recommendStrategy.getTitle(), communityTripResponses); - } - + @Transactional(readOnly = true) public TripDetailResponse getTripDetail(final Accessor accessor, final Long tripId) { final Trip trip = tripRepository.findById(tripId) .orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ID)); - final List cities = getCitiesByTripId(tripId); + final List cities = cityRepository.findCitiesByTripId(tripId); final LocalDateTime publishedDate = publishedTripRepository.findByTripId(tripId) .orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ID)) .getCreatedAt(); - final Long likeCount = likeRepository.countLikesByTripId(tripId); - if (!accessor.isMember()) { - return TripDetailResponse.publishedTrip( - trip, - cities, - false, - false, - likeCount, - publishedDate - ); - } + final LikeElements likeElements = new LikeElements(likeRepository.findLikeCountAndIsLikeByTripIds( + accessor.getMemberId(), + List.of(tripId) + )); + final Map likeInfoByTrip = likeElements.toLikeMap(); final Boolean isWriter = trip.isWriter(accessor.getMemberId()); - final Boolean isLike = likeRepository.existsByMemberIdAndTripId(accessor.getMemberId(), tripId); + return TripDetailResponse.publishedTrip( trip, cities, isWriter, - isLike, - likeCount, + isLike(likeInfoByTrip, tripId), + getLikeCount(likeInfoByTrip, tripId), publishedDate ); } diff --git a/backend/src/main/java/hanglog/currency/domain/OldestCurrency.java b/backend/src/main/java/hanglog/currency/domain/OldestCurrency.java new file mode 100644 index 000000000..2d6143919 --- /dev/null +++ b/backend/src/main/java/hanglog/currency/domain/OldestCurrency.java @@ -0,0 +1,19 @@ +package hanglog.currency.domain; + +import java.util.Optional; + +public class OldestCurrency { + + private static Currency oldestCurrency; + + private OldestCurrency() { + } + + public static Optional get() { + return Optional.ofNullable(oldestCurrency); + } + + public static void init(final Currency oldestCurrency) { + OldestCurrency.oldestCurrency = oldestCurrency; + } +} diff --git a/backend/src/main/java/hanglog/expense/domain/Expense.java b/backend/src/main/java/hanglog/expense/domain/Expense.java index a0edc3028..0841796f1 100644 --- a/backend/src/main/java/hanglog/expense/domain/Expense.java +++ b/backend/src/main/java/hanglog/expense/domain/Expense.java @@ -14,6 +14,7 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import java.util.Objects; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.SQLDelete; @@ -59,4 +60,21 @@ public Expense( ) { this(null, currency, amount, category); } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Expense expense)) { + return false; + } + return Objects.equals(currency, expense.currency) && Objects.equals(amount, expense.amount) + && Objects.equals(category, expense.category); + } + + @Override + public int hashCode() { + return Objects.hash(currency, amount, category); + } } diff --git a/backend/src/main/java/hanglog/expense/domain/repository/ExpenseRepository.java b/backend/src/main/java/hanglog/expense/domain/repository/ExpenseRepository.java index 9940cc4b2..15bdc6cb6 100644 --- a/backend/src/main/java/hanglog/expense/domain/repository/ExpenseRepository.java +++ b/backend/src/main/java/hanglog/expense/domain/repository/ExpenseRepository.java @@ -1,7 +1,27 @@ package hanglog.expense.domain.repository; import hanglog.expense.domain.Expense; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface ExpenseRepository extends JpaRepository { + + @Modifying + @Query(""" + UPDATE Expense expense + SET expense.status = 'DELETED' + WHERE expense.id = :id + """) + void deleteById(@Param("id") final Long id); + + @Modifying + @Query(""" + UPDATE Expense expense + SET expense.status = 'DELETED' + WHERE expense.id IN :expenseIds + """) + void deleteByIds(@Param("expenseIds") final List expenseIds); } diff --git a/backend/src/main/java/hanglog/expense/presentation/ExpenseController.java b/backend/src/main/java/hanglog/expense/presentation/ExpenseController.java deleted file mode 100644 index fa68e1d5f..000000000 --- a/backend/src/main/java/hanglog/expense/presentation/ExpenseController.java +++ /dev/null @@ -1,34 +0,0 @@ -package hanglog.expense.presentation; - -import hanglog.auth.Auth; -import hanglog.auth.MemberOnly; -import hanglog.auth.domain.Accessor; -import hanglog.expense.dto.response.TripExpenseResponse; -import hanglog.expense.service.ExpenseService; -import hanglog.trip.service.TripService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/trips/{tripId}/expense") -public class ExpenseController { - - private final ExpenseService expenseService; - private final TripService tripService; - - @GetMapping - @MemberOnly - public ResponseEntity getExpenses( - @Auth final Accessor accessor, - @PathVariable final Long tripId - ) { - tripService.validateTripByMember(accessor.getMemberId(), tripId); - final TripExpenseResponse tripExpenseResponse = expenseService.getAllExpenses(tripId); - return ResponseEntity.ok().body(tripExpenseResponse); - } -} diff --git a/backend/src/main/java/hanglog/global/config/DataSourceConfig.java b/backend/src/main/java/hanglog/global/config/DataSourceConfig.java new file mode 100644 index 000000000..05405651e --- /dev/null +++ b/backend/src/main/java/hanglog/global/config/DataSourceConfig.java @@ -0,0 +1,62 @@ +package hanglog.global.config; + +import static hanglog.global.config.datasource.DataSourceType.REPLICA; +import static hanglog.global.config.datasource.DataSourceType.SOURCE; + +import hanglog.global.config.datasource.RoutingDataSource; +import java.util.HashMap; +import javax.sql.DataSource; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; + +@Profile({"dev", "prod"}) +@Configuration +public class DataSourceConfig { + + private static final String SOURCE_SERVER = "SOURCE"; + private static final String REPLICA_SERVER = "REPLICA"; + + @Bean + @Qualifier(SOURCE_SERVER) + @ConfigurationProperties(prefix = "spring.datasource.source") + public DataSource sourceDataSource() { + return DataSourceBuilder.create().build(); + } + + @Bean + @Qualifier(REPLICA_SERVER) + @ConfigurationProperties(prefix = "spring.datasource.replica") + public DataSource replicaDataSource() { + return DataSourceBuilder.create().build(); + } + + @Bean + public DataSource routingDataSource( + @Qualifier(SOURCE_SERVER) final DataSource sourceDataSource, + @Qualifier(REPLICA_SERVER) final DataSource replicaDataSource + ) { + final RoutingDataSource routingDataSource = new RoutingDataSource(); + + final HashMap dataSourceMap = new HashMap<>(); + dataSourceMap.put(SOURCE, sourceDataSource); + dataSourceMap.put(REPLICA, replicaDataSource); + + routingDataSource.setTargetDataSources(dataSourceMap); + routingDataSource.setDefaultTargetDataSource(sourceDataSource); + + return routingDataSource; + } + + @Bean + @Primary + public DataSource dataSource() { + final DataSource determinedDataSource = routingDataSource(sourceDataSource(), replicaDataSource()); + return new LazyConnectionDataSourceProxy(determinedDataSource); + } +} diff --git a/backend/src/main/java/hanglog/global/config/datasource/DataSourceType.java b/backend/src/main/java/hanglog/global/config/datasource/DataSourceType.java new file mode 100644 index 000000000..6db2aaffc --- /dev/null +++ b/backend/src/main/java/hanglog/global/config/datasource/DataSourceType.java @@ -0,0 +1,6 @@ +package hanglog.global.config.datasource; + +public enum DataSourceType { + + SOURCE, REPLICA +} diff --git a/backend/src/main/java/hanglog/global/config/datasource/RoutingDataSource.java b/backend/src/main/java/hanglog/global/config/datasource/RoutingDataSource.java new file mode 100644 index 000000000..f31f5f645 --- /dev/null +++ b/backend/src/main/java/hanglog/global/config/datasource/RoutingDataSource.java @@ -0,0 +1,25 @@ +package hanglog.global.config.datasource; + +import static hanglog.global.config.datasource.DataSourceType.REPLICA; +import static hanglog.global.config.datasource.DataSourceType.SOURCE; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +@Slf4j +public class RoutingDataSource extends AbstractRoutingDataSource { + + @Override + protected Object determineCurrentLookupKey() { + final String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); + final boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); + if (isReadOnly) { + log.info(currentTransactionName + " Transaction:" + "Replica 서버로 요청합니다."); + return REPLICA; + } + + log.info(currentTransactionName + " Transaction:" + "Source 서버로 요청합니다."); + return SOURCE; + } +} diff --git a/backend/src/main/java/hanglog/global/detector/ConnectionProxyHandler.java b/backend/src/main/java/hanglog/global/detector/ConnectionProxyHandler.java new file mode 100644 index 000000000..3c2d50009 --- /dev/null +++ b/backend/src/main/java/hanglog/global/detector/ConnectionProxyHandler.java @@ -0,0 +1,45 @@ +package hanglog.global.detector; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; + +@RequiredArgsConstructor +public class ConnectionProxyHandler implements MethodInterceptor { + + private static final String JDBC_PREPARE_STATEMENT_METHOD_NAME = "prepareStatement"; + + private final Object connection; + private final LoggingForm loggingForm; + + @Nullable + @Override + public Object invoke(@Nonnull final MethodInvocation invocation) throws Throwable { + final Object result = invocation.proceed(); + + if (hasConnection(result) && hasPreparedStatementInvoked(invocation)) { + final ProxyFactory proxyFactory = new ProxyFactory(result); + proxyFactory.addAdvice(new PreparedStatementProxyHandler(loggingForm)); + return proxyFactory.getProxy(); + } + + return result; + } + + private boolean hasPreparedStatementInvoked(final MethodInvocation invocation) { + return invocation.getMethod().getName().equals(JDBC_PREPARE_STATEMENT_METHOD_NAME); + } + + private boolean hasConnection(final Object result) { + return result != null; + } + + public Object getProxy() { + final ProxyFactory proxyFactory = new ProxyFactory(connection); + proxyFactory.addAdvice(this); + return proxyFactory.getProxy(); + } +} diff --git a/backend/src/main/java/hanglog/global/detector/LoggingForm.java b/backend/src/main/java/hanglog/global/detector/LoggingForm.java new file mode 100644 index 000000000..ac1c14c7a --- /dev/null +++ b/backend/src/main/java/hanglog/global/detector/LoggingForm.java @@ -0,0 +1,30 @@ +package hanglog.global.detector; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class LoggingForm { + + private String apiUrl; + private String apiMethod; + private Long queryCounts = 0L; + private Long queryTime = 0L; + + public void queryCountUp() { + queryCounts++; + } + + public void addQueryTime(final Long queryTime) { + this.queryTime += queryTime; + } + + public void setApiUrl(final String apiUrl) { + this.apiUrl = apiUrl; + } + + public void setApiMethod(final String apiMethod) { + this.apiMethod = apiMethod; + } +} diff --git a/backend/src/main/java/hanglog/global/detector/PreparedStatementProxyHandler.java b/backend/src/main/java/hanglog/global/detector/PreparedStatementProxyHandler.java new file mode 100644 index 000000000..10ea4a0f9 --- /dev/null +++ b/backend/src/main/java/hanglog/global/detector/PreparedStatementProxyHandler.java @@ -0,0 +1,37 @@ +package hanglog.global.detector; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.lang.reflect.Method; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +@RequiredArgsConstructor +public class PreparedStatementProxyHandler implements MethodInterceptor { + + private static final List JDBC_QUERY_METHOD = List.of("executeQuery", "execute", "executeUpdate"); + + private final LoggingForm loggingForm; + + @Nullable + @Override + public Object invoke(@Nonnull final MethodInvocation invocation) throws Throwable { + + final Method method = invocation.getMethod(); + + if (JDBC_QUERY_METHOD.contains(method.getName())) { + final long startTime = System.currentTimeMillis(); + final Object result = invocation.proceed(); + final long endTime = System.currentTimeMillis(); + + loggingForm.addQueryTime(endTime - startTime); + loggingForm.queryCountUp(); + + return result; + } + + return invocation.proceed(); + } +} diff --git a/backend/src/main/java/hanglog/global/detector/QueryCounterAop.java b/backend/src/main/java/hanglog/global/detector/QueryCounterAop.java new file mode 100644 index 000000000..39f594d89 --- /dev/null +++ b/backend/src/main/java/hanglog/global/detector/QueryCounterAop.java @@ -0,0 +1,60 @@ +package hanglog.global.detector; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Aspect +@Slf4j +@Component +public class QueryCounterAop { + + private final ThreadLocal currentLoggingForm; + + public QueryCounterAop() { + this.currentLoggingForm = new ThreadLocal<>(); + } + + @Around("execution( * javax.sql.DataSource.getConnection())") + public Object captureConnection(final ProceedingJoinPoint joinPoint) throws Throwable { + final Object connection = joinPoint.proceed(); + + return new ConnectionProxyHandler(connection, getCurrentLoggingForm()).getProxy(); + } + + private LoggingForm getCurrentLoggingForm() { + if (currentLoggingForm.get() == null) { + currentLoggingForm.set(new LoggingForm()); + } + + return currentLoggingForm.get(); + } + + @After("within(@org.springframework.web.bind.annotation.RestController *)") + public void loggingAfterApiFinish() { + final LoggingForm loggingForm = getCurrentLoggingForm(); + + final ServletRequestAttributes attributes = + (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + + if (isInRequestScope(attributes)) { + final HttpServletRequest request = attributes.getRequest(); + + loggingForm.setApiMethod(request.getMethod()); + loggingForm.setApiUrl(request.getRequestURI()); + } + + log.info("{}", getCurrentLoggingForm()); + currentLoggingForm.remove(); + } + + private boolean isInRequestScope(final ServletRequestAttributes attributes) { + return attributes != null; + } +} diff --git a/backend/src/main/java/hanglog/global/filter/RequestLoggingFilter.java b/backend/src/main/java/hanglog/global/filter/RequestLoggingFilter.java index fa3141544..7dad8361c 100644 --- a/backend/src/main/java/hanglog/global/filter/RequestLoggingFilter.java +++ b/backend/src/main/java/hanglog/global/filter/RequestLoggingFilter.java @@ -10,11 +10,12 @@ public class RequestLoggingFilter extends AbstractRequestLoggingFilter { @Override protected void beforeRequest(@NonNull final HttpServletRequest request, @NonNull final String message) { - logger.info(message); + if (!message.contains("prometheus")) { + logger.info(message); + } } @Override protected void afterRequest(@NonNull final HttpServletRequest request, @NonNull final String message) { - logger.info(message); } } diff --git a/backend/src/main/java/hanglog/image/domain/ImageFile.java b/backend/src/main/java/hanglog/image/domain/ImageFile.java index 57d627b41..7b847a7a5 100644 --- a/backend/src/main/java/hanglog/image/domain/ImageFile.java +++ b/backend/src/main/java/hanglog/image/domain/ImageFile.java @@ -1,7 +1,6 @@ package hanglog.image.domain; import static hanglog.global.exception.ExceptionCode.FAIL_IMAGE_NAME_HASH; -import static hanglog.global.exception.ExceptionCode.INVALID_IMAGE_PATH; import static hanglog.global.exception.ExceptionCode.NULL_IMAGE; import static org.springframework.util.StringUtils.getFilenameExtension; @@ -9,7 +8,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.LocalDateTime; diff --git a/backend/src/main/java/hanglog/image/domain/S3ImageEvent.java b/backend/src/main/java/hanglog/image/domain/S3ImageEvent.java new file mode 100644 index 000000000..cacbf410d --- /dev/null +++ b/backend/src/main/java/hanglog/image/domain/S3ImageEvent.java @@ -0,0 +1,11 @@ +package hanglog.image.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class S3ImageEvent { + + private final String imageName; +} diff --git a/backend/src/main/java/hanglog/image/domain/repository/ImageRepository.java b/backend/src/main/java/hanglog/image/domain/repository/ImageRepository.java deleted file mode 100644 index cd2391863..000000000 --- a/backend/src/main/java/hanglog/image/domain/repository/ImageRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package hanglog.image.domain.repository; - -import hanglog.image.domain.Image; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ImageRepository extends JpaRepository { -} diff --git a/backend/src/main/java/hanglog/image/service/ImageService.java b/backend/src/main/java/hanglog/image/infrastructure/ImageUploader.java similarity index 55% rename from backend/src/main/java/hanglog/image/service/ImageService.java rename to backend/src/main/java/hanglog/image/infrastructure/ImageUploader.java index ed0aed663..739e5c8db 100644 --- a/backend/src/main/java/hanglog/image/service/ImageService.java +++ b/backend/src/main/java/hanglog/image/infrastructure/ImageUploader.java @@ -1,30 +1,24 @@ -package hanglog.image.service; +package hanglog.image.infrastructure; -import static hanglog.global.exception.ExceptionCode.EMPTY_IMAGE_LIST; -import static hanglog.global.exception.ExceptionCode.EXCEED_IMAGE_LIST_SIZE; import static hanglog.global.exception.ExceptionCode.INVALID_IMAGE; import static hanglog.global.exception.ExceptionCode.INVALID_IMAGE_PATH; -import static hanglog.image.util.ImageUrlConverter.convertNameToUrl; import com.amazonaws.AmazonServiceException; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.ObjectMetadata; import hanglog.global.exception.ImageException; import hanglog.image.domain.ImageFile; -import hanglog.image.dto.ImagesResponse; import java.io.IOException; import java.io.InputStream; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; +import org.springframework.stereotype.Component; -@Service +@Component @RequiredArgsConstructor -public class ImageService { - private static final int MAX_IMAGE_LIST_SIZE = 5; - private static final int EMPTY_LIST_SIZE = 0; +public class ImageUploader { + private static final String CACHE_CONTROL_VALUE = "max-age=3153600"; private final AmazonS3 s3Client; @@ -35,25 +29,7 @@ public class ImageService { @Value("${cloud.aws.s3.folder}") private String folder; - public ImagesResponse save(final List images) { - validateSizeOfImages(images); - final List imageFiles = images.stream() - .map(ImageFile::new) - .toList(); - final List imageUrls = uploadImages(imageFiles); - return new ImagesResponse(imageUrls); - } - - private void validateSizeOfImages(final List images) { - if (images.size() > MAX_IMAGE_LIST_SIZE) { - throw new ImageException(EXCEED_IMAGE_LIST_SIZE); - } - if (images.size() == EMPTY_LIST_SIZE) { - throw new ImageException(EMPTY_IMAGE_LIST); - } - } - - private List uploadImages(final List imageFiles) { + public List uploadImages(final List imageFiles) { return imageFiles.stream() .map(this::uploadImage) .toList(); @@ -73,6 +49,6 @@ private String uploadImage(final ImageFile imageFile) { } catch (final IOException e) { throw new ImageException(INVALID_IMAGE); } - return convertNameToUrl(imageFile.getHashedName()); + return imageFile.getHashedName(); } } diff --git a/backend/src/main/java/hanglog/image/util/ImageUrlConverter.java b/backend/src/main/java/hanglog/image/util/ImageUrlConverter.java deleted file mode 100644 index 8f3ed312d..000000000 --- a/backend/src/main/java/hanglog/image/util/ImageUrlConverter.java +++ /dev/null @@ -1,43 +0,0 @@ -package hanglog.image.util; - -import static hanglog.global.exception.ExceptionCode.INVALID_IMAGE_URL; - -import hanglog.global.exception.BadRequestException; -import java.util.List; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -@Component -public class ImageUrlConverter { - - private static final String EMPTY_STRING = ""; - private static final int URL_INDEX = 1; - private static final int VALID_PARSED_URL_SIZE = 2; - private static final int EMPTY_STRING_INDEX = 0; - - private static String imageBaseUrl = "https://hanglog.com/img/"; - - @Value("${image.base-url}") - public void setImageBaseUrl(final String value) { - imageBaseUrl = value; - } - - public static String convertNameToUrl(final String name) { - return imageBaseUrl + name; - } - - public static String convertUrlToName(final String url) { - final List parsedUrl = List.of(url.split(imageBaseUrl)); - validateImageUrlFormat(parsedUrl); - return parsedUrl.get(URL_INDEX); - } - - private static void validateImageUrlFormat(final List parsedUrl) { - if (parsedUrl.size() != VALID_PARSED_URL_SIZE) { - throw new BadRequestException(INVALID_IMAGE_URL); - } - if (!parsedUrl.get(EMPTY_STRING_INDEX).equals(EMPTY_STRING)) { - throw new BadRequestException(INVALID_IMAGE_URL); - } - } -} diff --git a/backend/src/main/java/hanglog/like/domain/LikeInfo.java b/backend/src/main/java/hanglog/like/domain/LikeInfo.java new file mode 100644 index 000000000..c968fffa8 --- /dev/null +++ b/backend/src/main/java/hanglog/like/domain/LikeInfo.java @@ -0,0 +1,12 @@ +package hanglog.like.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class LikeInfo { + + private final long likeCount; + private final boolean isLike; +} diff --git a/backend/src/main/java/hanglog/community/domain/Likes.java b/backend/src/main/java/hanglog/like/domain/Likes.java similarity index 95% rename from backend/src/main/java/hanglog/community/domain/Likes.java rename to backend/src/main/java/hanglog/like/domain/Likes.java index b609bd456..3794ebc48 100644 --- a/backend/src/main/java/hanglog/community/domain/Likes.java +++ b/backend/src/main/java/hanglog/like/domain/Likes.java @@ -1,4 +1,4 @@ -package hanglog.community.domain; +package hanglog.like.domain; import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; diff --git a/backend/src/main/java/hanglog/like/dto/LikeElement.java b/backend/src/main/java/hanglog/like/dto/LikeElement.java new file mode 100644 index 000000000..d94d9674f --- /dev/null +++ b/backend/src/main/java/hanglog/like/dto/LikeElement.java @@ -0,0 +1,13 @@ +package hanglog.like.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class LikeElement { + + private final Long tripId; + private final long likeCount; + private final boolean isLike; +} diff --git a/backend/src/main/java/hanglog/like/dto/LikeElements.java b/backend/src/main/java/hanglog/like/dto/LikeElements.java new file mode 100644 index 000000000..2552e8a91 --- /dev/null +++ b/backend/src/main/java/hanglog/like/dto/LikeElements.java @@ -0,0 +1,22 @@ +package hanglog.like.dto; + +import hanglog.like.domain.LikeInfo; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class LikeElements { + + private final List elements; + + public Map toLikeMap() { + final Map map = new HashMap<>(); + for (final LikeElement likeElement : elements) { + final LikeInfo likeInfo = new LikeInfo(likeElement.getLikeCount(), likeElement.isLike()); + map.put(likeElement.getTripId(), likeInfo); + } + return map; + } +} diff --git a/backend/src/main/java/hanglog/community/dto/request/LikeRequest.java b/backend/src/main/java/hanglog/like/dto/request/LikeRequest.java similarity index 89% rename from backend/src/main/java/hanglog/community/dto/request/LikeRequest.java rename to backend/src/main/java/hanglog/like/dto/request/LikeRequest.java index 030965cf4..b66986107 100644 --- a/backend/src/main/java/hanglog/community/dto/request/LikeRequest.java +++ b/backend/src/main/java/hanglog/like/dto/request/LikeRequest.java @@ -1,4 +1,4 @@ -package hanglog.community.dto.request; +package hanglog.like.dto.request; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; diff --git a/backend/src/main/java/hanglog/community/presentation/LikeController.java b/backend/src/main/java/hanglog/like/presentation/LikeController.java similarity index 88% rename from backend/src/main/java/hanglog/community/presentation/LikeController.java rename to backend/src/main/java/hanglog/like/presentation/LikeController.java index 284539857..4fcec00c9 100644 --- a/backend/src/main/java/hanglog/community/presentation/LikeController.java +++ b/backend/src/main/java/hanglog/like/presentation/LikeController.java @@ -1,10 +1,10 @@ -package hanglog.community.presentation; +package hanglog.like.presentation; import hanglog.auth.Auth; import hanglog.auth.MemberOnly; import hanglog.auth.domain.Accessor; -import hanglog.community.dto.request.LikeRequest; -import hanglog.community.service.LikeService; +import hanglog.like.dto.request.LikeRequest; +import hanglog.like.service.LikeService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; diff --git a/backend/src/main/java/hanglog/like/repository/LikeRepository.java b/backend/src/main/java/hanglog/like/repository/LikeRepository.java new file mode 100644 index 000000000..f2762a0e7 --- /dev/null +++ b/backend/src/main/java/hanglog/like/repository/LikeRepository.java @@ -0,0 +1,25 @@ +package hanglog.like.repository; + +import hanglog.like.domain.Likes; +import hanglog.like.dto.LikeElement; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface LikeRepository extends JpaRepository { + + boolean existsByMemberIdAndTripId(final Long memberId, final Long tripId); + + void deleteByMemberIdAndTripId(final Long memberId, final Long tripId); + + @Query(""" + SELECT new hanglog.like.dto.LikeElement + (l.tripId, COUNT(l.memberId), EXISTS(SELECT 1 FROM Likes l_1 WHERE l_1.memberId = :memberId AND l_1.tripId = l.tripId)) + FROM Likes l + WHERE l.tripId in :tripIds + GROUP BY l.tripId + """) + List findLikeCountAndIsLikeByTripIds(@Param("memberId") final Long memberId, + @Param("tripIds") final List tripIds); +} diff --git a/backend/src/main/java/hanglog/community/service/LikeService.java b/backend/src/main/java/hanglog/like/service/LikeService.java similarity index 79% rename from backend/src/main/java/hanglog/community/service/LikeService.java rename to backend/src/main/java/hanglog/like/service/LikeService.java index 695eaeee3..bfd76c50a 100644 --- a/backend/src/main/java/hanglog/community/service/LikeService.java +++ b/backend/src/main/java/hanglog/like/service/LikeService.java @@ -1,8 +1,8 @@ -package hanglog.community.service; +package hanglog.like.service; -import hanglog.community.domain.Likes; -import hanglog.community.domain.repository.LikeRepository; -import hanglog.community.dto.request.LikeRequest; +import hanglog.like.domain.Likes; +import hanglog.like.dto.request.LikeRequest; +import hanglog.like.repository.LikeRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/backend/src/main/java/hanglog/listener/DeleteEventListener.java b/backend/src/main/java/hanglog/listener/DeleteEventListener.java new file mode 100644 index 000000000..2566e8755 --- /dev/null +++ b/backend/src/main/java/hanglog/listener/DeleteEventListener.java @@ -0,0 +1,95 @@ +package hanglog.listener; + +import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; + +import hanglog.expense.domain.repository.ExpenseRepository; +import hanglog.login.domain.repository.RefreshTokenRepository; +import hanglog.member.domain.MemberDeleteEvent; +import hanglog.trip.domain.TripDeleteEvent; +import hanglog.trip.domain.repository.CustomDayLogRepository; +import hanglog.trip.domain.repository.CustomItemRepository; +import hanglog.trip.domain.repository.DayLogRepository; +import hanglog.trip.domain.repository.ImageRepository; +import hanglog.trip.domain.repository.ItemRepository; +import hanglog.trip.domain.repository.PlaceRepository; +import hanglog.trip.domain.repository.TripCityRepository; +import hanglog.trip.domain.repository.TripRepository; +import hanglog.trip.dto.ItemElement; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +public class DeleteEventListener { + + private final CustomDayLogRepository customDayLogRepository; + private final CustomItemRepository customItemRepository; + private final PlaceRepository placeRepository; + private final ExpenseRepository expenseRepository; + private final ImageRepository imageRepository; + private final ItemRepository itemRepository; + private final DayLogRepository dayLogRepository; + private final TripCityRepository tripCityRepository; + private final TripRepository tripRepository; + private final RefreshTokenRepository refreshTokenRepository; + + @Async + @Transactional(propagation = REQUIRES_NEW) + @TransactionalEventListener(fallbackExecution = true) + public void deleteMember(final MemberDeleteEvent event) { + final List dayLogIds = customDayLogRepository.findDayLogIdsByTripIds(event.getTripIds()); + final List itemElements = customItemRepository.findItemIdsByDayLogIds(dayLogIds); + + deletePlaces(itemElements); + deleteExpenses(itemElements); + deleteImageAndItems(itemElements); + + dayLogRepository.deleteByIds(dayLogIds); + tripRepository.deleteByMemberId(event.getMemberId()); + refreshTokenRepository.deleteByMemberId(event.getMemberId()); + } + + @Async + @Transactional(propagation = REQUIRES_NEW) + @TransactionalEventListener(fallbackExecution = true) + public void deleteTrip(final TripDeleteEvent event) { + final List dayLogIds = customDayLogRepository.findDayLogIdsByTripId(event.getTripId()); + final List itemElements = customItemRepository.findItemIdsByDayLogIds(dayLogIds); + + deletePlaces(itemElements); + deleteExpenses(itemElements); + deleteImageAndItems(itemElements); + + dayLogRepository.deleteByIds(dayLogIds); + tripCityRepository.deleteAllByTripId(event.getTripId()); + } + + private void deletePlaces(final List itemElements) { + final List placeIds = itemElements.stream() + .map(ItemElement::getPlaceId) + .toList(); + + placeRepository.deleteByIds(placeIds); + } + + private void deleteExpenses(final List itemElements) { + final List expenseIds = itemElements.stream() + .map(ItemElement::getExpenseId) + .toList(); + + expenseRepository.deleteByIds(expenseIds); + } + + private void deleteImageAndItems(final List itemElements) { + final List itemIds = itemElements.stream() + .map(ItemElement::getItemId) + .toList(); + + imageRepository.deleteByItemIds(itemIds); + itemRepository.deleteByIds(itemIds); + } +} diff --git a/backend/src/main/java/hanglog/listener/PublishEventListener.java b/backend/src/main/java/hanglog/listener/PublishEventListener.java new file mode 100644 index 000000000..8e1a3828a --- /dev/null +++ b/backend/src/main/java/hanglog/listener/PublishEventListener.java @@ -0,0 +1,31 @@ +package hanglog.listener; + +import hanglog.community.domain.PublishedTrip; +import hanglog.community.domain.repository.PublishedTripRepository; +import hanglog.trip.domain.PublishDeleteEvent; +import hanglog.trip.domain.PublishEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PublishEventListener { + + private final PublishedTripRepository publishedTripRepository; + + @EventListener + public void publishTrip(final PublishEvent publishEvent) { + final Long tripId = publishEvent.getTripId(); + + if (!publishedTripRepository.existsByTripId(tripId)) { + final PublishedTrip publishedTrip = new PublishedTrip(tripId); + publishedTripRepository.save(publishedTrip); + } + } + + @EventListener + public void deletePublishedTrip(final PublishDeleteEvent publishDeleteEvent) { + publishedTripRepository.deleteByTripId(publishDeleteEvent.getTripId()); + } +} diff --git a/backend/src/main/java/hanglog/listener/S3ImageEventListener.java b/backend/src/main/java/hanglog/listener/S3ImageEventListener.java new file mode 100644 index 000000000..97911de70 --- /dev/null +++ b/backend/src/main/java/hanglog/listener/S3ImageEventListener.java @@ -0,0 +1,38 @@ +package hanglog.listener; + +import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; + +import com.amazonaws.services.s3.AmazonS3; +import hanglog.image.domain.S3ImageEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +public class S3ImageEventListener { + + private static final String DEFAULT_IMAGE_NAME = "default-image.png"; + + private final AmazonS3 s3Client; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Value("${cloud.aws.s3.folder}") + private String folder; + + @Async + @Transactional(propagation = REQUIRES_NEW) + @TransactionalEventListener(fallbackExecution = true) + public void deleteImageFileInS3(final S3ImageEvent event) { + final String imageName = event.getImageName(); + if (imageName.equals(DEFAULT_IMAGE_NAME)) { + return; + } + s3Client.deleteObject(bucket, folder + imageName); + } +} diff --git a/backend/src/main/java/hanglog/auth/AuthArgumentResolver.java b/backend/src/main/java/hanglog/login/LoginArgumentResolver.java similarity index 89% rename from backend/src/main/java/hanglog/auth/AuthArgumentResolver.java rename to backend/src/main/java/hanglog/login/LoginArgumentResolver.java index 5bbf8f9d7..f9cd84c1a 100644 --- a/backend/src/main/java/hanglog/auth/AuthArgumentResolver.java +++ b/backend/src/main/java/hanglog/login/LoginArgumentResolver.java @@ -1,16 +1,17 @@ -package hanglog.auth; +package hanglog.login; import static hanglog.global.exception.ExceptionCode.INVALID_REQUEST; import static hanglog.global.exception.ExceptionCode.NOT_FOUND_REFRESH_TOKEN; import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import hanglog.auth.Auth; import hanglog.auth.domain.Accessor; -import hanglog.auth.domain.BearerAuthorizationExtractor; -import hanglog.auth.domain.JwtProvider; -import hanglog.auth.domain.MemberTokens; -import hanglog.auth.domain.repository.RefreshTokenRepository; import hanglog.global.exception.BadRequestException; import hanglog.global.exception.RefreshTokenException; +import hanglog.login.domain.MemberTokens; +import hanglog.login.domain.repository.RefreshTokenRepository; +import hanglog.login.infrastructure.BearerAuthorizationExtractor; +import hanglog.login.infrastructure.JwtProvider; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; @@ -24,7 +25,7 @@ @RequiredArgsConstructor @Component -public class AuthArgumentResolver implements HandlerMethodArgumentResolver { +public class LoginArgumentResolver implements HandlerMethodArgumentResolver { private static final String REFRESH_TOKEN = "refresh-token"; diff --git a/backend/src/main/java/hanglog/global/config/ResolverConfig.java b/backend/src/main/java/hanglog/login/LoginResolverConfig.java similarity index 64% rename from backend/src/main/java/hanglog/global/config/ResolverConfig.java rename to backend/src/main/java/hanglog/login/LoginResolverConfig.java index 0721ff37b..16146de11 100644 --- a/backend/src/main/java/hanglog/global/config/ResolverConfig.java +++ b/backend/src/main/java/hanglog/login/LoginResolverConfig.java @@ -1,6 +1,5 @@ -package hanglog.global.config; +package hanglog.login; -import hanglog.auth.AuthArgumentResolver; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; @@ -9,12 +8,12 @@ @Configuration @RequiredArgsConstructor -public class ResolverConfig implements WebMvcConfigurer { +public class LoginResolverConfig implements WebMvcConfigurer { - private final AuthArgumentResolver authArgumentResolver; + private final LoginArgumentResolver loginArgumentResolver; @Override public void addArgumentResolvers(final List resolvers) { - resolvers.add(authArgumentResolver); + resolvers.add(loginArgumentResolver); } } diff --git a/backend/src/main/java/hanglog/auth/domain/MemberTokens.java b/backend/src/main/java/hanglog/login/domain/MemberTokens.java similarity index 87% rename from backend/src/main/java/hanglog/auth/domain/MemberTokens.java rename to backend/src/main/java/hanglog/login/domain/MemberTokens.java index edc1d3ee4..e471bc341 100644 --- a/backend/src/main/java/hanglog/auth/domain/MemberTokens.java +++ b/backend/src/main/java/hanglog/login/domain/MemberTokens.java @@ -1,4 +1,4 @@ -package hanglog.auth.domain; +package hanglog.login.domain; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/backend/src/main/java/hanglog/auth/domain/OauthAccessToken.java b/backend/src/main/java/hanglog/login/domain/OauthAccessToken.java similarity index 94% rename from backend/src/main/java/hanglog/auth/domain/OauthAccessToken.java rename to backend/src/main/java/hanglog/login/domain/OauthAccessToken.java index b4171ab2c..874e8fb02 100644 --- a/backend/src/main/java/hanglog/auth/domain/OauthAccessToken.java +++ b/backend/src/main/java/hanglog/login/domain/OauthAccessToken.java @@ -1,4 +1,4 @@ -package hanglog.auth.domain; +package hanglog.login.domain; import static lombok.AccessLevel.PRIVATE; diff --git a/backend/src/main/java/hanglog/auth/domain/oauthprovider/OauthProvider.java b/backend/src/main/java/hanglog/login/domain/OauthProvider.java similarity index 68% rename from backend/src/main/java/hanglog/auth/domain/oauthprovider/OauthProvider.java rename to backend/src/main/java/hanglog/login/domain/OauthProvider.java index c2f2ac5e4..935b52c21 100644 --- a/backend/src/main/java/hanglog/auth/domain/oauthprovider/OauthProvider.java +++ b/backend/src/main/java/hanglog/login/domain/OauthProvider.java @@ -1,6 +1,5 @@ -package hanglog.auth.domain.oauthprovider; +package hanglog.login.domain; -import hanglog.auth.domain.oauthuserinfo.OauthUserInfo; import org.springframework.web.client.RestTemplate; public interface OauthProvider { diff --git a/backend/src/main/java/hanglog/auth/domain/oauthprovider/OauthProviders.java b/backend/src/main/java/hanglog/login/domain/OauthProviders.java similarity index 94% rename from backend/src/main/java/hanglog/auth/domain/oauthprovider/OauthProviders.java rename to backend/src/main/java/hanglog/login/domain/OauthProviders.java index 253311594..accbe063d 100644 --- a/backend/src/main/java/hanglog/auth/domain/oauthprovider/OauthProviders.java +++ b/backend/src/main/java/hanglog/login/domain/OauthProviders.java @@ -1,4 +1,4 @@ -package hanglog.auth.domain.oauthprovider; +package hanglog.login.domain; import static hanglog.global.exception.ExceptionCode.NOT_SUPPORTED_OAUTH_SERVICE; diff --git a/backend/src/main/java/hanglog/auth/domain/oauthuserinfo/OauthUserInfo.java b/backend/src/main/java/hanglog/login/domain/OauthUserInfo.java similarity index 73% rename from backend/src/main/java/hanglog/auth/domain/oauthuserinfo/OauthUserInfo.java rename to backend/src/main/java/hanglog/login/domain/OauthUserInfo.java index c6377e704..84cd4b0ac 100644 --- a/backend/src/main/java/hanglog/auth/domain/oauthuserinfo/OauthUserInfo.java +++ b/backend/src/main/java/hanglog/login/domain/OauthUserInfo.java @@ -1,4 +1,4 @@ -package hanglog.auth.domain.oauthuserinfo; +package hanglog.login.domain; public interface OauthUserInfo { String getSocialLoginId(); diff --git a/backend/src/main/java/hanglog/auth/domain/RefreshToken.java b/backend/src/main/java/hanglog/login/domain/RefreshToken.java similarity index 94% rename from backend/src/main/java/hanglog/auth/domain/RefreshToken.java rename to backend/src/main/java/hanglog/login/domain/RefreshToken.java index e0da5f34e..9bad8c43f 100644 --- a/backend/src/main/java/hanglog/auth/domain/RefreshToken.java +++ b/backend/src/main/java/hanglog/login/domain/RefreshToken.java @@ -1,4 +1,4 @@ -package hanglog.auth.domain; +package hanglog.login.domain; import static lombok.AccessLevel.PROTECTED; diff --git a/backend/src/main/java/hanglog/login/domain/repository/RefreshTokenRepository.java b/backend/src/main/java/hanglog/login/domain/repository/RefreshTokenRepository.java new file mode 100644 index 000000000..5d5f218e8 --- /dev/null +++ b/backend/src/main/java/hanglog/login/domain/repository/RefreshTokenRepository.java @@ -0,0 +1,22 @@ +package hanglog.login.domain.repository; + +import hanglog.login.domain.RefreshToken; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface RefreshTokenRepository extends JpaRepository { + + Optional findByToken(final String token); + + boolean existsByToken(final String token); + + @Modifying + @Query(""" + DELETE FROM RefreshToken refreshToken + WHERE refreshToken.memberId = :memberId + """) + void deleteByMemberId(@Param("memberId") final Long memberId); +} diff --git a/backend/src/main/java/hanglog/auth/dto/AccessTokenResponse.java b/backend/src/main/java/hanglog/login/dto/AccessTokenResponse.java similarity index 90% rename from backend/src/main/java/hanglog/auth/dto/AccessTokenResponse.java rename to backend/src/main/java/hanglog/login/dto/AccessTokenResponse.java index 612ffd3de..fbb3b5410 100644 --- a/backend/src/main/java/hanglog/auth/dto/AccessTokenResponse.java +++ b/backend/src/main/java/hanglog/login/dto/AccessTokenResponse.java @@ -1,4 +1,4 @@ -package hanglog.auth.dto; +package hanglog.login.dto; import static lombok.AccessLevel.PRIVATE; diff --git a/backend/src/main/java/hanglog/auth/dto/LoginRequest.java b/backend/src/main/java/hanglog/login/dto/LoginRequest.java similarity index 90% rename from backend/src/main/java/hanglog/auth/dto/LoginRequest.java rename to backend/src/main/java/hanglog/login/dto/LoginRequest.java index e58a82420..001c07dc4 100644 --- a/backend/src/main/java/hanglog/auth/dto/LoginRequest.java +++ b/backend/src/main/java/hanglog/login/dto/LoginRequest.java @@ -1,4 +1,4 @@ -package hanglog.auth.dto; +package hanglog.login.dto; import static lombok.AccessLevel.PRIVATE; diff --git a/backend/src/main/java/hanglog/auth/domain/BearerAuthorizationExtractor.java b/backend/src/main/java/hanglog/login/infrastructure/BearerAuthorizationExtractor.java similarity index 93% rename from backend/src/main/java/hanglog/auth/domain/BearerAuthorizationExtractor.java rename to backend/src/main/java/hanglog/login/infrastructure/BearerAuthorizationExtractor.java index 0c4e10c23..cdf3e861b 100644 --- a/backend/src/main/java/hanglog/auth/domain/BearerAuthorizationExtractor.java +++ b/backend/src/main/java/hanglog/login/infrastructure/BearerAuthorizationExtractor.java @@ -1,4 +1,4 @@ -package hanglog.auth.domain; +package hanglog.login.infrastructure; import static hanglog.global.exception.ExceptionCode.INVALID_ACCESS_TOKEN; diff --git a/backend/src/main/java/hanglog/auth/domain/JwtProvider.java b/backend/src/main/java/hanglog/login/infrastructure/JwtProvider.java similarity index 98% rename from backend/src/main/java/hanglog/auth/domain/JwtProvider.java rename to backend/src/main/java/hanglog/login/infrastructure/JwtProvider.java index 83db43192..cc490c4c3 100644 --- a/backend/src/main/java/hanglog/auth/domain/JwtProvider.java +++ b/backend/src/main/java/hanglog/login/infrastructure/JwtProvider.java @@ -1,4 +1,4 @@ -package hanglog.auth.domain; +package hanglog.login.infrastructure; import static hanglog.global.exception.ExceptionCode.EXPIRED_PERIOD_ACCESS_TOKEN; import static hanglog.global.exception.ExceptionCode.EXPIRED_PERIOD_REFRESH_TOKEN; @@ -7,6 +7,7 @@ import hanglog.global.exception.ExpiredPeriodJwtException; import hanglog.global.exception.InvalidJwtException; +import hanglog.login.domain.MemberTokens; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Header; diff --git a/backend/src/main/java/hanglog/auth/domain/oauthprovider/GoogleOauthProvider.java b/backend/src/main/java/hanglog/login/infrastructure/oauthprovider/GoogleOauthProvider.java similarity index 93% rename from backend/src/main/java/hanglog/auth/domain/oauthprovider/GoogleOauthProvider.java rename to backend/src/main/java/hanglog/login/infrastructure/oauthprovider/GoogleOauthProvider.java index babdee7e8..bb8a03a80 100644 --- a/backend/src/main/java/hanglog/auth/domain/oauthprovider/GoogleOauthProvider.java +++ b/backend/src/main/java/hanglog/login/infrastructure/oauthprovider/GoogleOauthProvider.java @@ -1,12 +1,13 @@ -package hanglog.auth.domain.oauthprovider; +package hanglog.login.infrastructure.oauthprovider; import static hanglog.global.exception.ExceptionCode.INVALID_AUTHORIZATION_CODE; import static hanglog.global.exception.ExceptionCode.NOT_SUPPORTED_OAUTH_SERVICE; -import hanglog.auth.domain.OauthAccessToken; -import hanglog.auth.domain.oauthuserinfo.GoogleUserInfo; -import hanglog.auth.domain.oauthuserinfo.OauthUserInfo; import hanglog.global.exception.AuthException; +import hanglog.login.domain.OauthAccessToken; +import hanglog.login.domain.OauthProvider; +import hanglog.login.domain.OauthUserInfo; +import hanglog.login.infrastructure.oauthuserinfo.GoogleUserInfo; import java.util.Optional; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; diff --git a/backend/src/main/java/hanglog/auth/domain/oauthprovider/KakaoOauthProvider.java b/backend/src/main/java/hanglog/login/infrastructure/oauthprovider/KakaoOauthProvider.java similarity index 93% rename from backend/src/main/java/hanglog/auth/domain/oauthprovider/KakaoOauthProvider.java rename to backend/src/main/java/hanglog/login/infrastructure/oauthprovider/KakaoOauthProvider.java index 3c1e1a9aa..aec5f80d7 100644 --- a/backend/src/main/java/hanglog/auth/domain/oauthprovider/KakaoOauthProvider.java +++ b/backend/src/main/java/hanglog/login/infrastructure/oauthprovider/KakaoOauthProvider.java @@ -1,12 +1,13 @@ -package hanglog.auth.domain.oauthprovider; +package hanglog.login.infrastructure.oauthprovider; import static hanglog.global.exception.ExceptionCode.INVALID_AUTHORIZATION_CODE; import static hanglog.global.exception.ExceptionCode.NOT_SUPPORTED_OAUTH_SERVICE; -import hanglog.auth.domain.OauthAccessToken; -import hanglog.auth.domain.oauthuserinfo.KakaoUserInfo; import hanglog.global.exception.AuthException; -import hanglog.auth.domain.oauthuserinfo.OauthUserInfo; +import hanglog.login.domain.OauthAccessToken; +import hanglog.login.domain.OauthProvider; +import hanglog.login.domain.OauthUserInfo; +import hanglog.login.infrastructure.oauthuserinfo.KakaoUserInfo; import java.util.Optional; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; diff --git a/backend/src/main/java/hanglog/auth/domain/oauthuserinfo/GoogleUserInfo.java b/backend/src/main/java/hanglog/login/infrastructure/oauthuserinfo/GoogleUserInfo.java similarity index 87% rename from backend/src/main/java/hanglog/auth/domain/oauthuserinfo/GoogleUserInfo.java rename to backend/src/main/java/hanglog/login/infrastructure/oauthuserinfo/GoogleUserInfo.java index f16852f58..71009070c 100644 --- a/backend/src/main/java/hanglog/auth/domain/oauthuserinfo/GoogleUserInfo.java +++ b/backend/src/main/java/hanglog/login/infrastructure/oauthuserinfo/GoogleUserInfo.java @@ -1,8 +1,9 @@ -package hanglog.auth.domain.oauthuserinfo; +package hanglog.login.infrastructure.oauthuserinfo; import static lombok.AccessLevel.PRIVATE; import com.fasterxml.jackson.annotation.JsonProperty; +import hanglog.login.domain.OauthUserInfo; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; diff --git a/backend/src/main/java/hanglog/auth/domain/oauthuserinfo/KakaoUserInfo.java b/backend/src/main/java/hanglog/login/infrastructure/oauthuserinfo/KakaoUserInfo.java similarity index 90% rename from backend/src/main/java/hanglog/auth/domain/oauthuserinfo/KakaoUserInfo.java rename to backend/src/main/java/hanglog/login/infrastructure/oauthuserinfo/KakaoUserInfo.java index 667197126..66dc88bca 100644 --- a/backend/src/main/java/hanglog/auth/domain/oauthuserinfo/KakaoUserInfo.java +++ b/backend/src/main/java/hanglog/login/infrastructure/oauthuserinfo/KakaoUserInfo.java @@ -1,8 +1,9 @@ -package hanglog.auth.domain.oauthuserinfo; +package hanglog.login.infrastructure.oauthuserinfo; import static lombok.AccessLevel.PRIVATE; import com.fasterxml.jackson.annotation.JsonProperty; +import hanglog.login.domain.OauthUserInfo; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; diff --git a/backend/src/main/java/hanglog/auth/presentation/AuthController.java b/backend/src/main/java/hanglog/login/presentation/LoginController.java similarity index 80% rename from backend/src/main/java/hanglog/auth/presentation/AuthController.java rename to backend/src/main/java/hanglog/login/presentation/LoginController.java index de115eb04..6df1671d2 100644 --- a/backend/src/main/java/hanglog/auth/presentation/AuthController.java +++ b/backend/src/main/java/hanglog/login/presentation/LoginController.java @@ -1,4 +1,4 @@ -package hanglog.auth.presentation; +package hanglog.login.presentation; import static org.springframework.http.HttpHeaders.SET_COOKIE; import static org.springframework.http.HttpStatus.CREATED; @@ -6,10 +6,10 @@ import hanglog.auth.Auth; import hanglog.auth.MemberOnly; import hanglog.auth.domain.Accessor; -import hanglog.auth.domain.MemberTokens; -import hanglog.auth.dto.AccessTokenResponse; -import hanglog.auth.dto.LoginRequest; -import hanglog.auth.service.AuthService; +import hanglog.login.domain.MemberTokens; +import hanglog.login.dto.AccessTokenResponse; +import hanglog.login.dto.LoginRequest; +import hanglog.login.service.LoginService; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseCookie; @@ -24,11 +24,11 @@ @RestController @RequiredArgsConstructor -public class AuthController { +public class LoginController { public static final int COOKIE_AGE_SECONDS = 604800; - private final AuthService authService; + private final LoginService loginService; @PostMapping("/login/{provider}") public ResponseEntity login( @@ -36,7 +36,7 @@ public ResponseEntity login( @RequestBody final LoginRequest loginRequest, final HttpServletResponse response ) { - final MemberTokens memberTokens = authService.login(provider, loginRequest.getCode()); + final MemberTokens memberTokens = loginService.login(provider, loginRequest.getCode()); final ResponseCookie cookie = ResponseCookie.from("refresh-token", memberTokens.getRefreshToken()) .maxAge(COOKIE_AGE_SECONDS) .sameSite("None") @@ -53,7 +53,7 @@ public ResponseEntity extendLogin( @CookieValue("refresh-token") final String refreshToken, @RequestHeader("Authorization") final String authorizationHeader ) { - final String renewalRefreshToken = authService.renewalAccessToken(refreshToken, authorizationHeader); + final String renewalRefreshToken = loginService.renewalAccessToken(refreshToken, authorizationHeader); return ResponseEntity.status(CREATED).body(new AccessTokenResponse(renewalRefreshToken)); } @@ -62,14 +62,14 @@ public ResponseEntity extendLogin( public ResponseEntity logout( @Auth final Accessor accessor, @CookieValue("refresh-token") final String refreshToken) { - authService.removeRefreshToken(refreshToken); + loginService.removeRefreshToken(refreshToken); return ResponseEntity.noContent().build(); } @DeleteMapping("/account") @MemberOnly public ResponseEntity deleteAccount(@Auth final Accessor accessor) { - authService.deleteAccount(accessor.getMemberId()); + loginService.deleteAccount(accessor.getMemberId()); return ResponseEntity.noContent().build(); } } diff --git a/backend/src/main/java/hanglog/auth/service/AuthService.java b/backend/src/main/java/hanglog/login/service/LoginService.java similarity index 72% rename from backend/src/main/java/hanglog/auth/service/AuthService.java rename to backend/src/main/java/hanglog/login/service/LoginService.java index 57a186526..c9352dfb2 100644 --- a/backend/src/main/java/hanglog/auth/service/AuthService.java +++ b/backend/src/main/java/hanglog/login/service/LoginService.java @@ -1,39 +1,47 @@ -package hanglog.auth.service; +package hanglog.login.service; import static hanglog.global.exception.ExceptionCode.FAIL_TO_GENERATE_RANDOM_NICKNAME; import static hanglog.global.exception.ExceptionCode.FAIL_TO_VALIDATE_TOKEN; import static hanglog.global.exception.ExceptionCode.INVALID_REFRESH_TOKEN; -import hanglog.auth.domain.BearerAuthorizationExtractor; -import hanglog.auth.domain.JwtProvider; -import hanglog.auth.domain.MemberTokens; -import hanglog.auth.domain.RefreshToken; -import hanglog.auth.domain.oauthprovider.OauthProvider; -import hanglog.auth.domain.oauthprovider.OauthProviders; -import hanglog.auth.domain.oauthuserinfo.OauthUserInfo; -import hanglog.auth.domain.repository.RefreshTokenRepository; +import hanglog.community.domain.repository.PublishedTripRepository; import hanglog.global.exception.AuthException; +import hanglog.login.domain.MemberTokens; +import hanglog.login.domain.OauthProvider; +import hanglog.login.domain.OauthProviders; +import hanglog.login.domain.OauthUserInfo; +import hanglog.login.domain.RefreshToken; +import hanglog.login.domain.repository.RefreshTokenRepository; +import hanglog.login.infrastructure.BearerAuthorizationExtractor; +import hanglog.login.infrastructure.JwtProvider; import hanglog.member.domain.Member; +import hanglog.member.domain.MemberDeleteEvent; import hanglog.member.domain.repository.MemberRepository; -import hanglog.trip.domain.repository.TripRepository; +import hanglog.trip.domain.repository.CustomTripRepository; +import hanglog.trip.domain.repository.SharedTripRepository; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Transactional @RequiredArgsConstructor -public class AuthService { +public class LoginService { private static final int MAX_TRY_COUNT = 5; private static final int FOUR_DIGIT_RANGE = 10000; + private final RefreshTokenRepository refreshTokenRepository; + private final PublishedTripRepository publishedTripRepository; private final MemberRepository memberRepository; + private final CustomTripRepository customTripRepository; + private final SharedTripRepository sharedTripRepository; private final OauthProviders oauthProviders; - private final RefreshTokenRepository refreshTokenRepository; - private final TripRepository tripRepository; private final JwtProvider jwtProvider; private final BearerAuthorizationExtractor bearerExtractor; + private final ApplicationEventPublisher publisher; public MemberTokens login(final String providerName, final String code) { final OauthProvider provider = oauthProviders.mapping(providerName); @@ -66,7 +74,7 @@ private Member createMember(final String socialLoginId, final String nickname, f throw new AuthException(FAIL_TO_GENERATE_RANDOM_NICKNAME); } - public String generateRandomFourDigitCode() { + private String generateRandomFourDigitCode() { final int randomNumber = (int) (Math.random() * FOUR_DIGIT_RANGE); return String.format("%04d", randomNumber); } @@ -89,8 +97,10 @@ public void removeRefreshToken(final String refreshToken) { } public void deleteAccount(final Long memberId) { - refreshTokenRepository.deleteByMemberId(memberId); - tripRepository.deleteAllByMemberId(memberId); - memberRepository.deleteById(memberId); + final List tripIds = customTripRepository.findTripIdsByMemberId(memberId); + publishedTripRepository.deleteByTripIds(tripIds); + sharedTripRepository.deleteByTripIds(tripIds); + memberRepository.deleteByMemberId(memberId); + publisher.publishEvent(new MemberDeleteEvent(tripIds, memberId)); } } diff --git a/backend/src/main/java/hanglog/member/domain/MemberDeleteEvent.java b/backend/src/main/java/hanglog/member/domain/MemberDeleteEvent.java new file mode 100644 index 000000000..0c73614da --- /dev/null +++ b/backend/src/main/java/hanglog/member/domain/MemberDeleteEvent.java @@ -0,0 +1,13 @@ +package hanglog.member.domain; + +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class MemberDeleteEvent { + + private final List tripIds; + private final Long memberId; +} diff --git a/backend/src/main/java/hanglog/member/domain/repository/MemberRepository.java b/backend/src/main/java/hanglog/member/domain/repository/MemberRepository.java index 2d4c9d933..5d4ca20c3 100644 --- a/backend/src/main/java/hanglog/member/domain/repository/MemberRepository.java +++ b/backend/src/main/java/hanglog/member/domain/repository/MemberRepository.java @@ -3,10 +3,21 @@ import hanglog.member.domain.Member; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface MemberRepository extends JpaRepository { Optional findBySocialLoginId(String socialLoginId); boolean existsByNickname(String nickname); + + @Modifying + @Query(""" + UPDATE Member member + SET member.status = 'DELETED' + WHERE member.id = :memberId + """) + void deleteByMemberId(@Param("memberId") final Long memberId); } diff --git a/backend/src/main/java/hanglog/share/dto/response/WriterResponse.java b/backend/src/main/java/hanglog/member/dto/response/WriterResponse.java similarity index 92% rename from backend/src/main/java/hanglog/share/dto/response/WriterResponse.java rename to backend/src/main/java/hanglog/member/dto/response/WriterResponse.java index 6213e9708..25f6d8bf7 100644 --- a/backend/src/main/java/hanglog/share/dto/response/WriterResponse.java +++ b/backend/src/main/java/hanglog/member/dto/response/WriterResponse.java @@ -1,4 +1,4 @@ -package hanglog.share.dto.response; +package hanglog.member.dto.response; import static lombok.AccessLevel.PRIVATE; diff --git a/backend/src/main/java/hanglog/member/service/MemberService.java b/backend/src/main/java/hanglog/member/service/MemberService.java index a018a3f66..5907c07cd 100644 --- a/backend/src/main/java/hanglog/member/service/MemberService.java +++ b/backend/src/main/java/hanglog/member/service/MemberService.java @@ -1,14 +1,19 @@ package hanglog.member.service; import static hanglog.global.exception.ExceptionCode.DUPLICATED_MEMBER_NICKNAME; +import static hanglog.global.exception.ExceptionCode.INVALID_IMAGE_URL; import static hanglog.global.exception.ExceptionCode.NOT_FOUND_MEMBER_ID; import hanglog.global.exception.BadRequestException; +import hanglog.image.domain.S3ImageEvent; import hanglog.member.domain.Member; import hanglog.member.domain.repository.MemberRepository; import hanglog.member.dto.request.MyPageRequest; import hanglog.member.dto.response.MyPageResponse; +import java.net.MalformedURLException; +import java.net.URL; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,7 +22,10 @@ @Transactional public class MemberService { + private static final String IMAGE_URL_HOST = "image.hanglog.com"; + private final MemberRepository memberRepository; + private final ApplicationEventPublisher publisher; @Transactional(readOnly = true) public MyPageResponse getMyPageInfo(final Long memberId) { @@ -33,19 +41,34 @@ public void updateMyPageInfo(final Long memberId, final MyPageRequest myPageRequ if (member.isNicknameChanged(myPageRequest.getNickname())) { checkDuplicatedNickname(myPageRequest.getNickname()); } - final Member updateMember = new Member( memberId, member.getSocialLoginId(), myPageRequest.getNickname(), myPageRequest.getImageUrl() ); + deleteOriginalImage(member.getImageUrl(), updateMember.getImageUrl()); memberRepository.save(updateMember); } private void checkDuplicatedNickname(final String nickname) { - if(memberRepository.existsByNickname(nickname)) { + if (memberRepository.existsByNickname(nickname)) { throw new BadRequestException(DUPLICATED_MEMBER_NICKNAME); } } + + private void deleteOriginalImage(final String originalUrl, final String updatedUrl) { + if (originalUrl.equals(updatedUrl)) { + return; + } + try { + final URL targetUrl = new URL(originalUrl); + if (targetUrl.getHost().equals(IMAGE_URL_HOST)) { + final String targetName = originalUrl.substring(originalUrl.lastIndexOf("/") + 1); + publisher.publishEvent(new S3ImageEvent(targetName)); + } + } catch (final MalformedURLException e) { + throw new BadRequestException(INVALID_IMAGE_URL); + } + } } diff --git a/backend/src/main/java/hanglog/share/domain/repository/SharedTripRepository.java b/backend/src/main/java/hanglog/share/domain/repository/SharedTripRepository.java deleted file mode 100644 index 8a1eff960..000000000 --- a/backend/src/main/java/hanglog/share/domain/repository/SharedTripRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package hanglog.share.domain.repository; - -import hanglog.share.domain.SharedTrip; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface SharedTripRepository extends JpaRepository { - - Optional findBySharedCode(final String sharedCode); -} diff --git a/backend/src/main/java/hanglog/share/dto/response/SharedTripCodeResponse.java b/backend/src/main/java/hanglog/share/dto/response/SharedTripCodeResponse.java deleted file mode 100644 index 9e0f1404b..000000000 --- a/backend/src/main/java/hanglog/share/dto/response/SharedTripCodeResponse.java +++ /dev/null @@ -1,19 +0,0 @@ -package hanglog.share.dto.response; - -import hanglog.share.domain.SharedTrip; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public class SharedTripCodeResponse { - - private final String sharedCode; - - public static SharedTripCodeResponse of(final SharedTrip sharedTrip) { - if (!sharedTrip.isShared()) { - return new SharedTripCodeResponse(null); - } - return new SharedTripCodeResponse(sharedTrip.getSharedCode()); - } -} diff --git a/backend/src/main/java/hanglog/share/presentation/SharedTripController.java b/backend/src/main/java/hanglog/share/presentation/SharedTripController.java deleted file mode 100644 index 9af93acc0..000000000 --- a/backend/src/main/java/hanglog/share/presentation/SharedTripController.java +++ /dev/null @@ -1,57 +0,0 @@ -package hanglog.share.presentation; - -import hanglog.auth.Auth; -import hanglog.auth.domain.Accessor; -import hanglog.expense.dto.response.TripExpenseResponse; -import hanglog.expense.service.ExpenseService; -import hanglog.share.dto.request.SharedTripStatusRequest; -import hanglog.share.dto.response.SharedTripCodeResponse; -import hanglog.share.service.SharedTripService; -import hanglog.trip.dto.response.TripDetailResponse; -import hanglog.trip.service.TripService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -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.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - - -@RestController -@RequiredArgsConstructor -public class SharedTripController { - - private final SharedTripService sharedTripService; - private final TripService tripService; - private final ExpenseService expenseService; - - @GetMapping("/shared-trips/{sharedCode}") - public ResponseEntity getSharedTrip(@PathVariable final String sharedCode) { - final Long tripId = sharedTripService.getTripId(sharedCode); - final TripDetailResponse tripDetailResponse = sharedTripService.getSharedTripDetail(tripId); - return ResponseEntity.ok().body(tripDetailResponse); - } - - @PatchMapping("/trips/{tripId}/share") - public ResponseEntity updateSharedStatus( - @Auth final Accessor accessor, - @PathVariable final Long tripId, - @RequestBody @Valid final SharedTripStatusRequest sharedTripStatusRequest - ) { - tripService.validateTripByMember(accessor.getMemberId(), tripId); - final SharedTripCodeResponse sharedTripCodeResponse = sharedTripService.updateSharedTripStatus( - tripId, - sharedTripStatusRequest - ); - return ResponseEntity.ok().body(sharedTripCodeResponse); - } - - @GetMapping("/shared-trips/{sharedCode}/expense") - public ResponseEntity getSharedExpenses(@PathVariable final String sharedCode) { - final Long tripId = sharedTripService.getTripId(sharedCode); - final TripExpenseResponse tripExpenseResponse = expenseService.getAllExpenses(tripId); - return ResponseEntity.ok().body(tripExpenseResponse); - } -} diff --git a/backend/src/main/java/hanglog/trip/domain/DayLog.java b/backend/src/main/java/hanglog/trip/domain/DayLog.java index c3dadb523..c3c6646bc 100644 --- a/backend/src/main/java/hanglog/trip/domain/DayLog.java +++ b/backend/src/main/java/hanglog/trip/domain/DayLog.java @@ -16,7 +16,9 @@ import jakarta.persistence.OrderBy; import java.time.LocalDate; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.SQLDelete; @@ -47,7 +49,7 @@ public class DayLog extends BaseEntity { @OneToMany(mappedBy = "dayLog", cascade = REMOVE, orphanRemoval = true) @OrderBy(value = "ordinal ASC") - private List items = new ArrayList<>(); + private Set items = new HashSet<>(); public DayLog( final Long id, @@ -60,7 +62,7 @@ public DayLog( this.title = title; this.ordinal = ordinal; this.trip = trip; - this.items = items; + this.items = new HashSet<>(items); } public DayLog( @@ -83,4 +85,12 @@ public LocalDate calculateDate() { final LocalDate startDate = trip.getStartDate(); return startDate.plusDays((long) ordinal - DEFAULT_DAY); } + + public void addItem(final Item item) { + items.add(item); + } + + public List getItems() { + return new ArrayList<>(items); + } } diff --git a/backend/src/main/java/hanglog/expense/domain/DayLogExpense.java b/backend/src/main/java/hanglog/trip/domain/DayLogExpense.java similarity index 74% rename from backend/src/main/java/hanglog/expense/domain/DayLogExpense.java rename to backend/src/main/java/hanglog/trip/domain/DayLogExpense.java index d44398d92..e84fcc80c 100644 --- a/backend/src/main/java/hanglog/expense/domain/DayLogExpense.java +++ b/backend/src/main/java/hanglog/trip/domain/DayLogExpense.java @@ -1,6 +1,6 @@ -package hanglog.expense.domain; +package hanglog.trip.domain; -import hanglog.trip.domain.DayLog; +import hanglog.expense.domain.Amount; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/backend/src/main/java/hanglog/image/domain/Image.java b/backend/src/main/java/hanglog/trip/domain/Image.java similarity index 87% rename from backend/src/main/java/hanglog/image/domain/Image.java rename to backend/src/main/java/hanglog/trip/domain/Image.java index dbbe68073..5455503fe 100644 --- a/backend/src/main/java/hanglog/image/domain/Image.java +++ b/backend/src/main/java/hanglog/trip/domain/Image.java @@ -1,12 +1,10 @@ -package hanglog.image.domain; +package hanglog.trip.domain; -import static jakarta.persistence.CascadeType.PERSIST; import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; import hanglog.global.BaseEntity; -import hanglog.trip.domain.Item; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -32,7 +30,7 @@ public class Image extends BaseEntity { @Column(nullable = false, unique = true) private String name; - @ManyToOne(fetch = LAZY, cascade = {PERSIST}) + @ManyToOne(fetch = LAZY) @JoinColumn(name = "item_id") private Item item; diff --git a/backend/src/main/java/hanglog/trip/domain/Item.java b/backend/src/main/java/hanglog/trip/domain/Item.java index e540c552e..2074c2089 100644 --- a/backend/src/main/java/hanglog/trip/domain/Item.java +++ b/backend/src/main/java/hanglog/trip/domain/Item.java @@ -4,7 +4,6 @@ import static hanglog.global.exception.ExceptionCode.INVALID_EXPENSE_UNDER_MIN; import static hanglog.global.exception.ExceptionCode.INVALID_RATING; import static hanglog.global.type.StatusType.USABLE; -import static jakarta.persistence.CascadeType.MERGE; import static jakarta.persistence.CascadeType.PERSIST; import static jakarta.persistence.CascadeType.REMOVE; import static jakarta.persistence.EnumType.STRING; @@ -18,7 +17,6 @@ import hanglog.global.exception.BadRequestException; import hanglog.global.exception.InvalidDomainException; import hanglog.global.type.StatusType; -import hanglog.image.domain.Image; import hanglog.trip.domain.type.ItemType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -68,19 +66,19 @@ public class Item extends BaseEntity { private String memo; - @OneToOne(fetch = LAZY, cascade = {PERSIST, MERGE, REMOVE}, orphanRemoval = true) + @OneToOne(fetch = LAZY, cascade = REMOVE) @JoinColumn(name = "place_id") private Place place; - @ManyToOne(fetch = LAZY, cascade = {PERSIST}) + @ManyToOne(fetch = LAZY, cascade = PERSIST) @JoinColumn(name = "day_log_id", nullable = false) private DayLog dayLog; - @OneToOne(fetch = LAZY, cascade = {PERSIST, MERGE, REMOVE}, orphanRemoval = true) + @OneToOne(fetch = LAZY, cascade = REMOVE) @JoinColumn(name = "expense_id") private Expense expense; - @OneToMany(mappedBy = "item", fetch = LAZY, cascade = {PERSIST, MERGE, REMOVE}, orphanRemoval = true) + @OneToMany(mappedBy = "item", fetch = LAZY, cascade = REMOVE) private List images = new ArrayList<>(); public Item( @@ -198,4 +196,8 @@ private boolean isInvalidRatingFormat(final Double rating) { public void changeOrdinal(final int ordinal) { this.ordinal = ordinal; } + + public boolean isSpot() { + return itemType.isSpot(); + } } diff --git a/backend/src/main/java/hanglog/trip/domain/Place.java b/backend/src/main/java/hanglog/trip/domain/Place.java index e6b3fa1b0..6fc5eb44c 100644 --- a/backend/src/main/java/hanglog/trip/domain/Place.java +++ b/backend/src/main/java/hanglog/trip/domain/Place.java @@ -13,6 +13,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import java.math.BigDecimal; +import java.util.Objects; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -55,4 +56,22 @@ public Place( this.longitude = longitude; this.category = category; } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Place place)) { + return false; + } + return Objects.equals(name, place.name) && Objects.equals(latitude, place.latitude) + && Objects.equals(longitude, place.longitude) && Objects.equals(category, + place.category); + } + + @Override + public int hashCode() { + return Objects.hash(name, latitude, longitude, category); + } } diff --git a/backend/src/main/java/hanglog/trip/domain/PublishDeleteEvent.java b/backend/src/main/java/hanglog/trip/domain/PublishDeleteEvent.java new file mode 100644 index 000000000..d77d39e9c --- /dev/null +++ b/backend/src/main/java/hanglog/trip/domain/PublishDeleteEvent.java @@ -0,0 +1,11 @@ +package hanglog.trip.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class PublishDeleteEvent { + + private final Long tripId; +} diff --git a/backend/src/main/java/hanglog/trip/domain/PublishEvent.java b/backend/src/main/java/hanglog/trip/domain/PublishEvent.java new file mode 100644 index 000000000..360b48375 --- /dev/null +++ b/backend/src/main/java/hanglog/trip/domain/PublishEvent.java @@ -0,0 +1,11 @@ +package hanglog.trip.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class PublishEvent { + + private final Long tripId; +} diff --git a/backend/src/main/java/hanglog/share/domain/SharedTrip.java b/backend/src/main/java/hanglog/trip/domain/SharedTrip.java similarity index 97% rename from backend/src/main/java/hanglog/share/domain/SharedTrip.java rename to backend/src/main/java/hanglog/trip/domain/SharedTrip.java index e60571779..bc35fbdff 100644 --- a/backend/src/main/java/hanglog/share/domain/SharedTrip.java +++ b/backend/src/main/java/hanglog/trip/domain/SharedTrip.java @@ -1,4 +1,4 @@ -package hanglog.share.domain; +package hanglog.trip.domain; import static hanglog.global.exception.ExceptionCode.FAIL_SHARE_CODE_HASH; import static jakarta.persistence.FetchType.LAZY; @@ -7,7 +7,6 @@ import hanglog.global.BaseEntity; import hanglog.global.exception.InvalidDomainException; -import hanglog.trip.domain.Trip; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; diff --git a/backend/src/main/java/hanglog/trip/domain/Trip.java b/backend/src/main/java/hanglog/trip/domain/Trip.java index 27c3477e2..3cc79ffd8 100644 --- a/backend/src/main/java/hanglog/trip/domain/Trip.java +++ b/backend/src/main/java/hanglog/trip/domain/Trip.java @@ -1,20 +1,19 @@ package hanglog.trip.domain; import static hanglog.global.type.StatusType.USABLE; -import static hanglog.image.util.ImageUrlConverter.convertUrlToName; import static jakarta.persistence.CascadeType.MERGE; import static jakarta.persistence.CascadeType.PERSIST; import static jakarta.persistence.CascadeType.REMOVE; import static jakarta.persistence.EnumType.STRING; import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.GenerationType.IDENTITY; +import static java.util.Objects.requireNonNullElse; import static lombok.AccessLevel.PROTECTED; -import hanglog.community.domain.type.PublishedStatusType; import hanglog.global.BaseEntity; import hanglog.member.domain.Member; -import hanglog.share.domain.SharedTrip; -import hanglog.share.domain.type.SharedStatusType; +import hanglog.trip.domain.type.PublishedStatusType; +import hanglog.trip.domain.type.SharedStatusType; import hanglog.trip.dto.request.TripUpdateRequest; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -28,8 +27,10 @@ import jakarta.persistence.OrderBy; import java.time.LocalDate; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.SQLDelete; @@ -77,7 +78,7 @@ public class Trip extends BaseEntity { @OneToMany(mappedBy = "trip", cascade = {PERSIST, REMOVE, MERGE}, orphanRemoval = true) @OrderBy(value = "ordinal ASC") - private List dayLogs = new ArrayList<>(); + private Set dayLogs = new HashSet<>(); @OneToOne(mappedBy = "trip", cascade = {PERSIST, REMOVE, MERGE}, orphanRemoval = true) private SharedTrip sharedTrip; @@ -104,7 +105,7 @@ public Trip( this.endDate = endDate; this.description = description; this.sharedTrip = sharedTrip; - this.dayLogs = dayLogs; + this.dayLogs = new HashSet<>(dayLogs); this.sharedStatus = sharedStatus; this.publishedStatus = publishedStatus; } @@ -127,17 +128,14 @@ public static Trip of(final Member member, final String title, final LocalDate s public void update(final TripUpdateRequest updateRequest) { this.title = updateRequest.getTitle(); - this.imageName = updateImageUrl(updateRequest.getImageUrl()); + this.imageName = updateImageName(updateRequest.getImageName()); this.startDate = updateRequest.getStartDate(); this.endDate = updateRequest.getEndDate(); this.description = updateRequest.getDescription(); } - private String updateImageUrl(final String imageUrl) { - if (imageUrl == null) { - return DEFAULT_IMAGE_NAME; - } - return convertUrlToName(imageUrl); + private String updateImageName(final String imageName) { + return requireNonNullElse(imageName, DEFAULT_IMAGE_NAME); } public Optional getSharedCode() { @@ -169,4 +167,16 @@ public void changePublishedStatus(final Boolean isPublished) { public Boolean isWriter(final Long memberId) { return this.member.getId().equals(memberId); } + + public void addDayLog(final DayLog dayLog) { + dayLogs.add(dayLog); + } + + public void removeDayLog(final DayLog dayLog) { + dayLogs.remove(dayLog); + } + + public List getDayLogs() { + return new ArrayList<>(dayLogs); + } } diff --git a/backend/src/main/java/hanglog/trip/domain/TripDeleteEvent.java b/backend/src/main/java/hanglog/trip/domain/TripDeleteEvent.java new file mode 100644 index 000000000..8233af111 --- /dev/null +++ b/backend/src/main/java/hanglog/trip/domain/TripDeleteEvent.java @@ -0,0 +1,11 @@ +package hanglog.trip.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class TripDeleteEvent { + + private final Long tripId; +} diff --git a/backend/src/main/java/hanglog/trip/domain/repository/CustomDayLogRepository.java b/backend/src/main/java/hanglog/trip/domain/repository/CustomDayLogRepository.java new file mode 100644 index 000000000..f205a9a2d --- /dev/null +++ b/backend/src/main/java/hanglog/trip/domain/repository/CustomDayLogRepository.java @@ -0,0 +1,13 @@ +package hanglog.trip.domain.repository; + +import hanglog.trip.domain.DayLog; +import java.util.List; + +public interface CustomDayLogRepository { + + void saveAll(final List dayLogs); + + List findDayLogIdsByTripId(final Long tripId); + + List findDayLogIdsByTripIds(final List tripIds); +} diff --git a/backend/src/main/java/hanglog/trip/domain/repository/CustomImageRepository.java b/backend/src/main/java/hanglog/trip/domain/repository/CustomImageRepository.java new file mode 100644 index 000000000..b50f634a4 --- /dev/null +++ b/backend/src/main/java/hanglog/trip/domain/repository/CustomImageRepository.java @@ -0,0 +1,11 @@ +package hanglog.trip.domain.repository; + +import hanglog.trip.domain.Image; +import java.util.List; + +public interface CustomImageRepository { + + void saveAll(final List images); + + void deleteAll(final List images); +} diff --git a/backend/src/main/java/hanglog/trip/domain/repository/CustomItemRepository.java b/backend/src/main/java/hanglog/trip/domain/repository/CustomItemRepository.java new file mode 100644 index 000000000..28968c3b3 --- /dev/null +++ b/backend/src/main/java/hanglog/trip/domain/repository/CustomItemRepository.java @@ -0,0 +1,11 @@ +package hanglog.trip.domain.repository; + +import hanglog.trip.dto.ItemElement; +import java.util.List; + +public interface CustomItemRepository { + + List findItemIdsByDayLogIds(final List dayLogIds); + + void updateOrdinals(final List orderedItemIds); +} diff --git a/backend/src/main/java/hanglog/trip/domain/repository/CustomTripCityRepository.java b/backend/src/main/java/hanglog/trip/domain/repository/CustomTripCityRepository.java new file mode 100644 index 000000000..669cd43b0 --- /dev/null +++ b/backend/src/main/java/hanglog/trip/domain/repository/CustomTripCityRepository.java @@ -0,0 +1,9 @@ +package hanglog.trip.domain.repository; + +import hanglog.city.domain.City; +import java.util.List; + +public interface CustomTripCityRepository { + + void saveAll(final List cities, final Long tripId); +} diff --git a/backend/src/main/java/hanglog/trip/domain/repository/CustomTripRepository.java b/backend/src/main/java/hanglog/trip/domain/repository/CustomTripRepository.java new file mode 100644 index 000000000..7eeebf198 --- /dev/null +++ b/backend/src/main/java/hanglog/trip/domain/repository/CustomTripRepository.java @@ -0,0 +1,8 @@ +package hanglog.trip.domain.repository; + +import java.util.List; + +public interface CustomTripRepository { + + List findTripIdsByMemberId(final Long memberId); +} diff --git a/backend/src/main/java/hanglog/trip/domain/repository/DayLogRepository.java b/backend/src/main/java/hanglog/trip/domain/repository/DayLogRepository.java index e17c8c42e..15bf6d250 100644 --- a/backend/src/main/java/hanglog/trip/domain/repository/DayLogRepository.java +++ b/backend/src/main/java/hanglog/trip/domain/repository/DayLogRepository.java @@ -1,7 +1,41 @@ package hanglog.trip.domain.repository; import hanglog.trip.domain.DayLog; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface DayLogRepository extends JpaRepository { + + @Query(""" + SELECT dayLog + FROM DayLog dayLog + LEFT JOIN FETCH dayLog.items items + WHERE dayLog.id = :dayLogId + """) + Optional findWithItemsById(@Param("dayLogId") final Long dayLogId); + + @Query(""" + SELECT dayLog + FROM DayLog dayLog + LEFT JOIN FETCH dayLog.items items + LEFT JOIN FETCH items.images images + LEFT JOIN FETCH items.expense expense + LEFT JOIN FETCH items.place place + LEFT JOIN FETCH expense.category expense_category + LEFT JOIN FETCH place.category place_category + WHERE dayLog.id = :dayLogId + """) + Optional findWithItemDetailsById(@Param("dayLogId") final Long dayLogId); + + @Modifying + @Query(""" + UPDATE DayLog dayLog + SET dayLog.status = 'DELETED' + WHERE dayLog.id IN :dayLogIds + """) + void deleteByIds(@Param("dayLogIds") final List dayLogIds); } diff --git a/backend/src/main/java/hanglog/trip/domain/repository/ImageRepository.java b/backend/src/main/java/hanglog/trip/domain/repository/ImageRepository.java new file mode 100644 index 000000000..e2f07fe6c --- /dev/null +++ b/backend/src/main/java/hanglog/trip/domain/repository/ImageRepository.java @@ -0,0 +1,27 @@ +package hanglog.trip.domain.repository; + +import hanglog.trip.domain.Image; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ImageRepository extends JpaRepository { + + @Modifying + @Query(""" + UPDATE Image image + SET image.status = 'DELETED' + WHERE image.item.id = :itemId + """) + void deleteByItemId(@Param("itemId") final Long itemId); + + @Modifying + @Query(""" + UPDATE Image image + SET image.status = 'DELETED' + WHERE image.item.id IN :itemIds + """) + void deleteByItemIds(@Param("itemIds") final List itemIds); +} diff --git a/backend/src/main/java/hanglog/trip/domain/repository/ItemRepository.java b/backend/src/main/java/hanglog/trip/domain/repository/ItemRepository.java index 63bfbe434..6fae22dbe 100644 --- a/backend/src/main/java/hanglog/trip/domain/repository/ItemRepository.java +++ b/backend/src/main/java/hanglog/trip/domain/repository/ItemRepository.java @@ -1,7 +1,40 @@ package hanglog.trip.domain.repository; import hanglog.trip.domain.Item; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface ItemRepository extends JpaRepository { + + @Query(""" + SELECT item + FROM Item item + LEFT JOIN FETCH item.images images + LEFT JOIN FETCH item.expense expense + LEFT JOIN FETCH item.place place + LEFT JOIN FETCH expense.category expense_category + LEFT JOIN FETCH place.category place_category + WHERE item.id = :itemId + """) + Optional findById(@Param("itemId") final Long itemId); + + @Modifying + @Query(""" + UPDATE Item item + SET item.status = 'DELETED' + WHERE item.id = :itemId + """) + void deleteById(@Param("itemId") final Long itemId); + + @Modifying + @Query(""" + UPDATE Item item + SET item.status = 'DELETED' + WHERE item.id IN :itemIds + """) + void deleteByIds(@Param("itemIds") final List itemIds); } diff --git a/backend/src/main/java/hanglog/trip/domain/repository/PlaceRepository.java b/backend/src/main/java/hanglog/trip/domain/repository/PlaceRepository.java index fd7c3a7c5..de8346969 100644 --- a/backend/src/main/java/hanglog/trip/domain/repository/PlaceRepository.java +++ b/backend/src/main/java/hanglog/trip/domain/repository/PlaceRepository.java @@ -1,7 +1,27 @@ package hanglog.trip.domain.repository; import hanglog.trip.domain.Place; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface PlaceRepository extends JpaRepository { + + @Modifying + @Query(""" + UPDATE Place place + SET place.status = 'DELETED' + WHERE place.id = :id + """) + void deleteById(@Param("id") final Long id); + + @Modifying + @Query(""" + UPDATE Place place + SET place.status = 'DELETED' + WHERE place.id IN :placeIds + """) + void deleteByIds(@Param("placeIds") final List placeIds); } diff --git a/backend/src/main/java/hanglog/trip/domain/repository/PublishedTripRepository.java b/backend/src/main/java/hanglog/trip/domain/repository/PublishedTripRepository.java deleted file mode 100644 index 229d04896..000000000 --- a/backend/src/main/java/hanglog/trip/domain/repository/PublishedTripRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package hanglog.trip.domain.repository; - -import hanglog.community.domain.PublishedTrip; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface PublishedTripRepository extends JpaRepository { - - void deleteByTripId(final Long tripId); - - boolean existsByTripId(final Long tripId); - - Optional findByTripId(final Long tripId); -} diff --git a/backend/src/main/java/hanglog/trip/domain/repository/SharedTripRepository.java b/backend/src/main/java/hanglog/trip/domain/repository/SharedTripRepository.java new file mode 100644 index 000000000..9bd99f5d7 --- /dev/null +++ b/backend/src/main/java/hanglog/trip/domain/repository/SharedTripRepository.java @@ -0,0 +1,36 @@ +package hanglog.trip.domain.repository; + +import hanglog.trip.domain.SharedTrip; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface SharedTripRepository extends JpaRepository { + + @Query(""" + SELECT sharedTrip + FROM SharedTrip sharedTrip + LEFT JOIN FETCH sharedTrip.trip trip + WHERE sharedTrip.sharedCode = :sharedCode + """) + Optional findBySharedCode(final String sharedCode); + + @Modifying + @Query(""" + UPDATE SharedTrip sharedTrip + SET sharedTrip.status = 'DELETED' + WHERE sharedTrip.trip.id = :tripId + """) + void deleteByTripId(@Param("tripId") final Long tripId); + + @Modifying + @Query(""" + UPDATE SharedTrip sharedTrip + SET sharedTrip.status = 'DELETED' + WHERE sharedTrip.trip.id IN :tripIds + """) + void deleteByTripIds(@Param("tripIds") final List tripIds); +} diff --git a/backend/src/main/java/hanglog/trip/domain/repository/TripCityRepository.java b/backend/src/main/java/hanglog/trip/domain/repository/TripCityRepository.java index 46ef73019..e0224c698 100644 --- a/backend/src/main/java/hanglog/trip/domain/repository/TripCityRepository.java +++ b/backend/src/main/java/hanglog/trip/domain/repository/TripCityRepository.java @@ -1,12 +1,27 @@ package hanglog.trip.domain.repository; import hanglog.trip.domain.TripCity; +import hanglog.trip.dto.TripCityElement; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface TripCityRepository extends JpaRepository { - List findByTripId(Long tripId); + @Modifying + @Query(""" + UPDATE TripCity tripCity + SET tripCity.status = 'DELETED' + WHERE tripCity.trip.id = :tripId + """) + void deleteAllByTripId(@Param("tripId") final Long tripId); - void deleteAllByTripId(Long tripId); + @Query(""" + SELECT new hanglog.trip.dto.TripCityElement (tc.trip.id, tc.city) + FROM TripCity tc + WHERE tc.trip.id IN :tripIds + """) + List findTripIdAndCitiesByTripIds(@Param("tripIds") final List tripIds); } diff --git a/backend/src/main/java/hanglog/trip/domain/repository/TripRepository.java b/backend/src/main/java/hanglog/trip/domain/repository/TripRepository.java index 8e50b35b8..f94de79b2 100644 --- a/backend/src/main/java/hanglog/trip/domain/repository/TripRepository.java +++ b/backend/src/main/java/hanglog/trip/domain/repository/TripRepository.java @@ -1,11 +1,12 @@ package hanglog.trip.domain.repository; -import hanglog.community.domain.type.PublishedStatusType; import hanglog.trip.domain.Trip; +import hanglog.trip.domain.type.PublishedStatusType; import java.util.List; import java.util.Optional; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -19,20 +20,56 @@ public interface TripRepository extends JpaRepository { @Query("SELECT trip FROM Trip trip LEFT JOIN FETCH trip.sharedTrip WHERE trip.member.id = :memberId") List findAllByMemberId(@Param("memberId") final Long memberId); - @Query("SELECT trip FROM Trip trip " - + "LEFT JOIN PublishedTrip publishedTrip ON publishedTrip.trip = trip " - + "WHERE trip.publishedStatus = 'PUBLISHED'") + @Query(""" + SELECT trip FROM Trip trip + LEFT JOIN PublishedTrip publishedTrip ON publishedTrip.tripId = trip.id + LEFT JOIN FETCH trip.sharedTrip sharedTrip + LEFT JOIN FETCH trip.member member + WHERE trip.publishedStatus = 'PUBLISHED' + """) List findPublishedTripByPageable(final Pageable pageable); - @Query("SELECT trip FROM Trip trip " - + "LEFT JOIN Likes likes ON likes.tripId = trip.id " - + "WHERE trip.publishedStatus = 'PUBLISHED' " - + "GROUP BY trip.id " - + "ORDER BY COUNT(likes.tripId) DESC") + @Query(""" + SELECT trip FROM Trip trip + LEFT JOIN Likes likes ON likes.tripId = trip.id + LEFT JOIN FETCH trip.sharedTrip sharedTrip + WHERE trip.publishedStatus = 'PUBLISHED' + GROUP BY trip.id + ORDER BY COUNT(likes.tripId) DESC + """) List findTripsOrderByLikesCount(final Pageable pageable); Long countTripByPublishedStatus(final PublishedStatusType publishedStatusType); - void deleteAllByMemberId(final Long memberId); + @Query(""" + SELECT trip + FROM Trip trip + LEFT JOIN FETCH trip.member member + LEFT JOIN FETCH trip.sharedTrip sharedTrip + LEFT JOIN FETCH trip.dayLogs dayLogs + LEFT JOIN FETCH dayLogs.items items + LEFT JOIN FETCH items.images images + LEFT JOIN FETCH items.expense expense + LEFT JOIN FETCH items.place place + LEFT JOIN FETCH expense.category expense_category + LEFT JOIN FETCH place.category place_category + WHERE dayLogs.trip.id = :tripId + """) + Optional findById(@Param("tripId") final Long tripId); + @Modifying + @Query(""" + UPDATE Trip trip + SET trip.status = 'DELETED' + WHERE trip.id = :tripId + """) + void deleteById(@Param("tripId") final Long tripId); + + @Modifying + @Query(""" + UPDATE Trip trip + SET trip.status = 'DELETED' + WHERE trip.member.id = :memberId + """) + void deleteByMemberId(@Param("memberId") final Long memberId); } diff --git a/backend/src/main/java/hanglog/community/domain/type/PublishedStatusType.java b/backend/src/main/java/hanglog/trip/domain/type/PublishedStatusType.java similarity index 86% rename from backend/src/main/java/hanglog/community/domain/type/PublishedStatusType.java rename to backend/src/main/java/hanglog/trip/domain/type/PublishedStatusType.java index cf230e61f..2574c52dd 100644 --- a/backend/src/main/java/hanglog/community/domain/type/PublishedStatusType.java +++ b/backend/src/main/java/hanglog/trip/domain/type/PublishedStatusType.java @@ -1,4 +1,4 @@ -package hanglog.community.domain.type; +package hanglog.trip.domain.type; public enum PublishedStatusType { diff --git a/backend/src/main/java/hanglog/share/domain/type/SharedStatusType.java b/backend/src/main/java/hanglog/trip/domain/type/SharedStatusType.java similarity index 86% rename from backend/src/main/java/hanglog/share/domain/type/SharedStatusType.java rename to backend/src/main/java/hanglog/trip/domain/type/SharedStatusType.java index 53ecf37c8..2931ecc59 100644 --- a/backend/src/main/java/hanglog/share/domain/type/SharedStatusType.java +++ b/backend/src/main/java/hanglog/trip/domain/type/SharedStatusType.java @@ -1,4 +1,4 @@ -package hanglog.share.domain.type; +package hanglog.trip.domain.type; public enum SharedStatusType { diff --git a/backend/src/main/java/hanglog/trip/dto/ItemElement.java b/backend/src/main/java/hanglog/trip/dto/ItemElement.java new file mode 100644 index 000000000..189f9665b --- /dev/null +++ b/backend/src/main/java/hanglog/trip/dto/ItemElement.java @@ -0,0 +1,13 @@ +package hanglog.trip.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ItemElement { + + private final Long itemId; + private final Long placeId; + private final Long expenseId; +} diff --git a/backend/src/main/java/hanglog/trip/dto/TripCityElement.java b/backend/src/main/java/hanglog/trip/dto/TripCityElement.java new file mode 100644 index 000000000..fb6af75e0 --- /dev/null +++ b/backend/src/main/java/hanglog/trip/dto/TripCityElement.java @@ -0,0 +1,13 @@ +package hanglog.trip.dto; + +import hanglog.city.domain.City; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TripCityElement { + + private final Long tripId; + private final City city; +} diff --git a/backend/src/main/java/hanglog/trip/dto/TripCityElements.java b/backend/src/main/java/hanglog/trip/dto/TripCityElements.java new file mode 100644 index 000000000..712713fe0 --- /dev/null +++ b/backend/src/main/java/hanglog/trip/dto/TripCityElements.java @@ -0,0 +1,24 @@ +package hanglog.trip.dto; + +import hanglog.city.domain.City; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class TripCityElements { + + private final List elements; + + public Map> toCityMap() { + final Map> map = new HashMap<>(); + for (final TripCityElement tripCityElement : elements) { + final Long tripId = tripCityElement.getTripId(); + final City city = tripCityElement.getCity(); + map.computeIfAbsent(tripId, k -> new ArrayList<>()).add(city); + } + return map; + } +} diff --git a/backend/src/main/java/hanglog/trip/dto/request/ItemRequest.java b/backend/src/main/java/hanglog/trip/dto/request/ItemRequest.java index c6120949d..ddfd50360 100644 --- a/backend/src/main/java/hanglog/trip/dto/request/ItemRequest.java +++ b/backend/src/main/java/hanglog/trip/dto/request/ItemRequest.java @@ -34,8 +34,8 @@ public class ItemRequest { @NotNull(message = "여행 아이템이 속한 데이 로그를 입력해주세요.") private final Long dayLogId; - @Size(max = 10, message = "여행 아이템의 이미지는 최대 10개까지 첨부할 수 있습니다.") - private final List imageUrls; + @Size(max = 5, message = "여행 아이템의 이미지는 최대 5개까지 첨부할 수 있습니다.") + private final List imageNames; @Valid private final PlaceRequest place; @@ -50,7 +50,7 @@ public ItemRequest( final Double rating, final String memo, final Long dayLogId, - final List imageUrls, + final List imageNames, final PlaceRequest place, final ExpenseRequest expense ) { @@ -62,7 +62,7 @@ public ItemRequest( this.rating = rating; this.memo = memo; this.dayLogId = dayLogId; - this.imageUrls = imageUrls; + this.imageNames = imageNames; this.place = place; this.expense = expense; } diff --git a/backend/src/main/java/hanglog/trip/dto/request/ItemUpdateRequest.java b/backend/src/main/java/hanglog/trip/dto/request/ItemUpdateRequest.java index ef309119a..fdbd5558d 100644 --- a/backend/src/main/java/hanglog/trip/dto/request/ItemUpdateRequest.java +++ b/backend/src/main/java/hanglog/trip/dto/request/ItemUpdateRequest.java @@ -35,8 +35,8 @@ public class ItemUpdateRequest { @NotNull(message = "여행 아이템이 속한 데이 로그를 입력해주세요.") private final Long dayLogId; - @Size(max = 10, message = "여행 아이템의 이미지는 최대 10개까지 첨부할 수 있습니다.") - private final List imageUrls; + @Size(max = 5, message = "여행 아이템의 이미지는 최대 5개까지 첨부할 수 있습니다.") + private final List imageNames; @NotNull(message = "장소의 업데이트 여부를 입력해 주세요.") private final Boolean isPlaceUpdated; @@ -53,7 +53,7 @@ public ItemUpdateRequest( final Double rating, final String memo, final Long dayLogId, - final List imageUrls, + final List imageNames, final Boolean isPlaceUpdated, final PlaceRequest place, final ExpenseRequest expense @@ -67,7 +67,7 @@ public ItemUpdateRequest( this.rating = rating; this.memo = memo; this.dayLogId = dayLogId; - this.imageUrls = imageUrls; + this.imageNames = imageNames; this.isPlaceUpdated = isPlaceUpdated; this.place = place; this.expense = expense; diff --git a/backend/src/main/java/hanglog/share/dto/request/SharedTripStatusRequest.java b/backend/src/main/java/hanglog/trip/dto/request/SharedStatusRequest.java similarity index 82% rename from backend/src/main/java/hanglog/share/dto/request/SharedTripStatusRequest.java rename to backend/src/main/java/hanglog/trip/dto/request/SharedStatusRequest.java index f6b94b0e0..c4f851a2e 100644 --- a/backend/src/main/java/hanglog/share/dto/request/SharedTripStatusRequest.java +++ b/backend/src/main/java/hanglog/trip/dto/request/SharedStatusRequest.java @@ -1,4 +1,4 @@ -package hanglog.share.dto.request; +package hanglog.trip.dto.request; import static lombok.AccessLevel.PRIVATE; @@ -10,7 +10,7 @@ @Getter @AllArgsConstructor @NoArgsConstructor(access = PRIVATE) -public class SharedTripStatusRequest { +public class SharedStatusRequest { @NotNull(message = "공유 상태를 선택해주세요.") private Boolean sharedStatus; diff --git a/backend/src/main/java/hanglog/trip/dto/request/TripCreateRequest.java b/backend/src/main/java/hanglog/trip/dto/request/TripCreateRequest.java index 20282abc5..887d26a66 100644 --- a/backend/src/main/java/hanglog/trip/dto/request/TripCreateRequest.java +++ b/backend/src/main/java/hanglog/trip/dto/request/TripCreateRequest.java @@ -3,7 +3,6 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; import java.time.LocalDate; import java.util.List; import lombok.AllArgsConstructor; diff --git a/backend/src/main/java/hanglog/trip/dto/request/TripUpdateRequest.java b/backend/src/main/java/hanglog/trip/dto/request/TripUpdateRequest.java index 6b0be4edf..86c366dbd 100644 --- a/backend/src/main/java/hanglog/trip/dto/request/TripUpdateRequest.java +++ b/backend/src/main/java/hanglog/trip/dto/request/TripUpdateRequest.java @@ -17,7 +17,7 @@ public class TripUpdateRequest { @Size(max = 50, message = "여행 제목은 50자를 넘을 수 없습니다.") private final String title; - private final String imageUrl; + private final String imageName; @NotNull(message = "여행 시작 날짜를 입력해 주세요.") @DateTimeFormat(pattern = "yyyy-MM-dd") diff --git a/backend/src/main/java/hanglog/expense/dto/response/DayLogExpenseResponse.java b/backend/src/main/java/hanglog/trip/dto/response/DayLogLedgerResponse.java similarity index 78% rename from backend/src/main/java/hanglog/expense/dto/response/DayLogExpenseResponse.java rename to backend/src/main/java/hanglog/trip/dto/response/DayLogLedgerResponse.java index 6563b59ff..247edbbaf 100644 --- a/backend/src/main/java/hanglog/expense/dto/response/DayLogExpenseResponse.java +++ b/backend/src/main/java/hanglog/trip/dto/response/DayLogLedgerResponse.java @@ -1,6 +1,6 @@ -package hanglog.expense.dto.response; +package hanglog.trip.dto.response; -import hanglog.expense.domain.DayLogExpense; +import hanglog.trip.domain.DayLogExpense; import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; @@ -9,7 +9,7 @@ @Getter @RequiredArgsConstructor -public class DayLogExpenseResponse { +public class DayLogLedgerResponse { private final Long id; private final Integer ordinal; @@ -17,13 +17,13 @@ public class DayLogExpenseResponse { private final BigDecimal totalAmount; private final List items; - public static DayLogExpenseResponse of(final DayLogExpense dayLogExpense) { + public static DayLogLedgerResponse of(final DayLogExpense dayLogExpense) { final List itemResponses = dayLogExpense.getDayLog().getItems().stream() .filter(dayLog -> dayLog.getExpense() != null) .map(ItemDetailResponse::of) .toList(); - return new DayLogExpenseResponse( + return new DayLogLedgerResponse( dayLogExpense.getDayLog().getId(), dayLogExpense.getDayLog().getOrdinal(), dayLogExpense.getDayLog().calculateDate(), diff --git a/backend/src/main/java/hanglog/image/dto/ImagesResponse.java b/backend/src/main/java/hanglog/trip/dto/response/ImagesResponse.java similarity index 60% rename from backend/src/main/java/hanglog/image/dto/ImagesResponse.java rename to backend/src/main/java/hanglog/trip/dto/response/ImagesResponse.java index b46c7a392..4d7d75c8e 100644 --- a/backend/src/main/java/hanglog/image/dto/ImagesResponse.java +++ b/backend/src/main/java/hanglog/trip/dto/response/ImagesResponse.java @@ -1,8 +1,6 @@ -package hanglog.image.dto; +package hanglog.trip.dto.response; -import static hanglog.image.util.ImageUrlConverter.convertNameToUrl; - -import hanglog.image.domain.Image; +import hanglog.trip.domain.Image; import java.util.List; import lombok.AllArgsConstructor; import lombok.Getter; @@ -13,12 +11,12 @@ @NoArgsConstructor public class ImagesResponse { - private List imageUrls; + private List imageNames; public static ImagesResponse of(final List images) { return new ImagesResponse( images.stream() - .map(image -> convertNameToUrl(image.getName())) + .map(Image::getName) .toList()); } } diff --git a/backend/src/main/java/hanglog/expense/dto/response/ItemDetailResponse.java b/backend/src/main/java/hanglog/trip/dto/response/ItemDetailResponse.java similarity index 86% rename from backend/src/main/java/hanglog/expense/dto/response/ItemDetailResponse.java rename to backend/src/main/java/hanglog/trip/dto/response/ItemDetailResponse.java index 73b198672..57daafd22 100644 --- a/backend/src/main/java/hanglog/expense/dto/response/ItemDetailResponse.java +++ b/backend/src/main/java/hanglog/trip/dto/response/ItemDetailResponse.java @@ -1,5 +1,6 @@ -package hanglog.expense.dto.response; +package hanglog.trip.dto.response; +import hanglog.expense.dto.response.ItemExpenseResponse; import hanglog.trip.domain.Item; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/backend/src/main/java/hanglog/trip/dto/response/ItemResponse.java b/backend/src/main/java/hanglog/trip/dto/response/ItemResponse.java index ceedbcffa..bfc9eed10 100644 --- a/backend/src/main/java/hanglog/trip/dto/response/ItemResponse.java +++ b/backend/src/main/java/hanglog/trip/dto/response/ItemResponse.java @@ -1,10 +1,8 @@ package hanglog.trip.dto.response; -import static hanglog.image.util.ImageUrlConverter.convertNameToUrl; - import hanglog.expense.domain.Expense; import hanglog.expense.dto.response.ItemExpenseResponse; -import hanglog.image.domain.Image; +import hanglog.trip.domain.Image; import hanglog.trip.domain.Item; import hanglog.trip.domain.Place; import java.util.List; @@ -21,7 +19,7 @@ public class ItemResponse { private final Integer ordinal; private final Double rating; private final String memo; - private final List imageUrls; + private final List imageNames; private final PlaceResponse place; private final ItemExpenseResponse expense; @@ -33,7 +31,7 @@ public static ItemResponse of(final Item item) { item.getOrdinal(), item.getRating(), item.getMemo(), - getImageUrls(item.getImages()), + getImageNames(item.getImages()), getPlaceResponse(item.getPlace()), getExpenseResponse(item.getExpense()) ); @@ -53,9 +51,9 @@ private static ItemExpenseResponse getExpenseResponse(final Expense expense) { return ItemExpenseResponse.of(expense); } - private static List getImageUrls(final List images) { + private static List getImageNames(final List images) { return images.stream() - .map(image -> convertNameToUrl(image.getName())) + .map(Image::getName) .toList(); } } diff --git a/backend/src/main/java/hanglog/expense/dto/response/TripExpenseResponse.java b/backend/src/main/java/hanglog/trip/dto/response/LedgerResponse.java similarity index 68% rename from backend/src/main/java/hanglog/expense/dto/response/TripExpenseResponse.java rename to backend/src/main/java/hanglog/trip/dto/response/LedgerResponse.java index 8909312bc..a23cf6ac6 100644 --- a/backend/src/main/java/hanglog/expense/dto/response/TripExpenseResponse.java +++ b/backend/src/main/java/hanglog/trip/dto/response/LedgerResponse.java @@ -1,13 +1,15 @@ -package hanglog.expense.dto.response; +package hanglog.trip.dto.response; +import hanglog.city.domain.City; import hanglog.city.dto.response.CityResponse; import hanglog.currency.domain.Currency; import hanglog.expense.domain.Amount; import hanglog.expense.domain.CategoryExpense; -import hanglog.expense.domain.DayLogExpense; +import hanglog.expense.dto.response.CategoryExpenseResponse; +import hanglog.expense.dto.response.ExchangeRateResponse; +import hanglog.trip.domain.DayLogExpense; import hanglog.trip.domain.Trip; -import hanglog.trip.domain.TripCity; import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; @@ -16,7 +18,7 @@ @Getter @RequiredArgsConstructor -public class TripExpenseResponse { +public class LedgerResponse { private final Long id; private final String title; @@ -26,29 +28,29 @@ public class TripExpenseResponse { private final BigDecimal totalAmount; private final List categories; private final ExchangeRateResponse exchangeRate; - private final List dayLogs; + private final List dayLogs; - public static TripExpenseResponse of( + public static LedgerResponse of( final Trip trip, final Amount totalAmount, - final List tripCities, + final List cities, final List categoryExpenses, final Currency currency, final List dayLogExpenses ) { - final List cityExpenseResponses = tripCities.stream() - .map(tripCity -> CityResponse.of(tripCity.getCity())) + final List cityExpenseResponses = cities.stream() + .map(CityResponse::of) .toList(); final List categoryExpenseResponses = categoryExpenses.stream() .map(CategoryExpenseResponse::of) .toList(); - final List dayLogExpenseResponses = dayLogExpenses.stream() - .map(DayLogExpenseResponse::of) + final List dayLogLedgerRespons = dayLogExpenses.stream() + .map(DayLogLedgerResponse::of) .toList(); - return new TripExpenseResponse( + return new LedgerResponse( trip.getId(), trip.getTitle(), trip.getStartDate(), @@ -57,7 +59,7 @@ public static TripExpenseResponse of( totalAmount.getValue(), categoryExpenseResponses, ExchangeRateResponse.of(currency), - dayLogExpenseResponses + dayLogLedgerRespons ); } } diff --git a/backend/src/main/java/hanglog/trip/dto/response/SharedCodeResponse.java b/backend/src/main/java/hanglog/trip/dto/response/SharedCodeResponse.java new file mode 100644 index 000000000..2dfe66a5c --- /dev/null +++ b/backend/src/main/java/hanglog/trip/dto/response/SharedCodeResponse.java @@ -0,0 +1,19 @@ +package hanglog.trip.dto.response; + +import hanglog.trip.domain.SharedTrip; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class SharedCodeResponse { + + private final String sharedCode; + + public static SharedCodeResponse of(final SharedTrip sharedTrip) { + if (!sharedTrip.isShared()) { + return new SharedCodeResponse(null); + } + return new SharedCodeResponse(sharedTrip.getSharedCode()); + } +} diff --git a/backend/src/main/java/hanglog/trip/dto/response/SharedTripDetailResponse.java b/backend/src/main/java/hanglog/trip/dto/response/SharedTripDetailResponse.java new file mode 100644 index 000000000..efa2a1b9d --- /dev/null +++ b/backend/src/main/java/hanglog/trip/dto/response/SharedTripDetailResponse.java @@ -0,0 +1,53 @@ +package hanglog.trip.dto.response; + +import static lombok.AccessLevel.PRIVATE; + +import hanglog.city.domain.City; +import hanglog.city.dto.response.CityWithPositionResponse; +import hanglog.member.dto.response.WriterResponse; +import hanglog.trip.domain.Trip; +import java.time.LocalDate; +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor(access = PRIVATE) +public class SharedTripDetailResponse { + + private final Long id; + private final List cities; + private final WriterResponse writer; + private final String title; + private final LocalDate startDate; + private final LocalDate endDate; + private final String description; + private final String imageName; + private final String sharedCode; + private final List dayLogs; + + public static SharedTripDetailResponse of(final Trip trip, final List cities) { + final List dayLogResponses = trip.getDayLogs().stream() + .map(DayLogResponse::of) + .toList(); + + final List cityWithPositionResponses = cities.stream() + .map(CityWithPositionResponse::of) + .toList(); + + final String sharedCode = trip.getSharedCode().orElse(null); + + return new SharedTripDetailResponse( + trip.getId(), + cityWithPositionResponses, + WriterResponse.of(trip.getMember()), + trip.getTitle(), + trip.getStartDate(), + trip.getEndDate(), + trip.getDescription(), + trip.getImageName(), + sharedCode, + dayLogResponses + ); + } +} diff --git a/backend/src/main/java/hanglog/trip/dto/response/TripDetailResponse.java b/backend/src/main/java/hanglog/trip/dto/response/TripDetailResponse.java index 2b7395273..6f6408c57 100644 --- a/backend/src/main/java/hanglog/trip/dto/response/TripDetailResponse.java +++ b/backend/src/main/java/hanglog/trip/dto/response/TripDetailResponse.java @@ -1,6 +1,5 @@ package hanglog.trip.dto.response; -import static hanglog.image.util.ImageUrlConverter.convertNameToUrl; import static hanglog.trip.dto.response.type.TripType.PERSONAL; import static hanglog.trip.dto.response.type.TripType.PUBLISHED; import static hanglog.trip.dto.response.type.TripType.SHARED; @@ -8,7 +7,7 @@ import hanglog.city.domain.City; import hanglog.city.dto.response.CityWithPositionResponse; -import hanglog.share.dto.response.WriterResponse; +import hanglog.member.dto.response.WriterResponse; import hanglog.trip.domain.DayLog; import hanglog.trip.domain.Trip; import hanglog.trip.dto.response.type.TripType; @@ -31,7 +30,7 @@ public class TripDetailResponse { private final LocalDate startDate; private final LocalDate endDate; private final String description; - private final String imageUrl; + private final String imageName; private final Boolean isLike; private final Long likeCount; private final LocalDateTime publishedDate; @@ -50,7 +49,7 @@ public static TripDetailResponse personalTrip(final Trip trip, final List trip.getStartDate(), trip.getEndDate(), trip.getDescription(), - convertNameToUrl(trip.getImageName()), + trip.getImageName(), null, null, null, @@ -71,7 +70,7 @@ public static TripDetailResponse sharedTrip(final Trip trip, final List ci trip.getStartDate(), trip.getEndDate(), trip.getDescription(), - convertNameToUrl(trip.getImageName()), + trip.getImageName(), null, null, null, @@ -99,7 +98,7 @@ public static TripDetailResponse publishedTrip( trip.getStartDate(), trip.getEndDate(), trip.getDescription(), - convertNameToUrl(trip.getImageName()), + trip.getImageName(), isLike, likeCount, publishedDate, diff --git a/backend/src/main/java/hanglog/trip/dto/response/TripResponse.java b/backend/src/main/java/hanglog/trip/dto/response/TripResponse.java index 63fb17ccd..c6b83bb1e 100644 --- a/backend/src/main/java/hanglog/trip/dto/response/TripResponse.java +++ b/backend/src/main/java/hanglog/trip/dto/response/TripResponse.java @@ -1,6 +1,5 @@ package hanglog.trip.dto.response; -import static hanglog.image.util.ImageUrlConverter.convertNameToUrl; import static lombok.AccessLevel.PRIVATE; import hanglog.city.domain.City; @@ -21,7 +20,7 @@ public class TripResponse { private final LocalDate startDate; private final LocalDate endDate; private final String description; - private final String imageUrl; + private final String imageName; public static TripResponse of(final Trip trip, final List cities) { final List cityResponses = cities.stream() @@ -35,7 +34,7 @@ public static TripResponse of(final Trip trip, final List cities) { trip.getStartDate(), trip.getEndDate(), trip.getDescription(), - convertNameToUrl(trip.getImageName()) + trip.getImageName() ); } } diff --git a/backend/src/main/java/hanglog/trip/infrastructure/CustomDayLogRepositoryImpl.java b/backend/src/main/java/hanglog/trip/infrastructure/CustomDayLogRepositoryImpl.java new file mode 100644 index 000000000..e436f247b --- /dev/null +++ b/backend/src/main/java/hanglog/trip/infrastructure/CustomDayLogRepositoryImpl.java @@ -0,0 +1,67 @@ +package hanglog.trip.infrastructure; + +import hanglog.trip.domain.DayLog; +import hanglog.trip.domain.repository.CustomDayLogRepository; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class CustomDayLogRepositoryImpl implements CustomDayLogRepository { + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + @Override + public void saveAll(final List dayLogs) { + final String sql = """ + INSERT INTO day_log (created_at, modified_at, ordinal, status, title, trip_id) + VALUES (:createdAt, :modifiedAt, :ordinal, :status, :title, :tripId) + """; + namedParameterJdbcTemplate.batchUpdate(sql, getDayLogToSqlParameterSources(dayLogs)); + } + + @Override + public List findDayLogIdsByTripId(final Long tripId) { + final String sql = """ + SELECT d.id + FROM day_log d + WHERE d.trip_id = :tripId + """; + final MapSqlParameterSource parameters = new MapSqlParameterSource(); + parameters.addValue("tripId", tripId); + return namedParameterJdbcTemplate.queryForList(sql, parameters, Long.class); + } + + @Override + public List findDayLogIdsByTripIds(final List tripIds) { + final String sql = """ + SELECT d.id + FROM day_log d + WHERE d.trip_id IN (:tripIds) + """; + final MapSqlParameterSource parameters = new MapSqlParameterSource(); + parameters.addValue("tripIds", tripIds); + return namedParameterJdbcTemplate.queryForList(sql, parameters, Long.class); + } + + private MapSqlParameterSource[] getDayLogToSqlParameterSources(final List dayLogs) { + return dayLogs.stream() + .map(this::getDayLogToSqlParameterSource) + .toArray(MapSqlParameterSource[]::new); + } + + private MapSqlParameterSource getDayLogToSqlParameterSource(final DayLog dayLog) { + final LocalDateTime now = LocalDateTime.now(); + return new MapSqlParameterSource() + .addValue("createdAt", now) + .addValue("modifiedAt", now) + .addValue("ordinal", dayLog.getOrdinal()) + .addValue("status", dayLog.getStatus().name()) + .addValue("title", dayLog.getTitle()) + .addValue("tripId", dayLog.getTrip().getId()); + } +} diff --git a/backend/src/main/java/hanglog/trip/infrastructure/CustomImageRepositoryImpl.java b/backend/src/main/java/hanglog/trip/infrastructure/CustomImageRepositoryImpl.java new file mode 100644 index 000000000..5d66cfc55 --- /dev/null +++ b/backend/src/main/java/hanglog/trip/infrastructure/CustomImageRepositoryImpl.java @@ -0,0 +1,58 @@ +package hanglog.trip.infrastructure; + +import hanglog.trip.domain.Image; +import hanglog.trip.domain.repository.CustomImageRepository; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class CustomImageRepositoryImpl implements CustomImageRepository { + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + @Override + public void saveAll(final List images) { + final String sql = """ + INSERT INTO image (created_at, modified_at, item_id, name, status) + VALUES (:createdAt, :modifiedAt, :itemId, :name, :status) + """; + namedParameterJdbcTemplate.batchUpdate(sql, getImageToSqlParameterSources(images)); + } + + @Override + public void deleteAll(final List images) { + final String sql = """ + DELETE FROM image WHERE id IN (:imageIds) + """; + namedParameterJdbcTemplate.update(sql, getDeletedImageIdsSqlParameterSources(images)); + } + + private SqlParameterSource getDeletedImageIdsSqlParameterSources(final List images) { + final List imageIds = images.stream() + .map(Image::getId) + .toList(); + return new MapSqlParameterSource("imageIds", imageIds); + } + + private MapSqlParameterSource[] getImageToSqlParameterSources(final List images) { + return images.stream() + .map(this::getImageToSqlParameterSource) + .toArray(MapSqlParameterSource[]::new); + } + + private MapSqlParameterSource getImageToSqlParameterSource(final Image image) { + final LocalDateTime now = LocalDateTime.now(); + return new MapSqlParameterSource() + .addValue("createdAt", now) + .addValue("modifiedAt", now) + .addValue("itemId", image.getItem().getId()) + .addValue("name", image.getName()) + .addValue("status", image.getStatus().name()); + } +} diff --git a/backend/src/main/java/hanglog/trip/infrastructure/CustomItemRepositoryImpl.java b/backend/src/main/java/hanglog/trip/infrastructure/CustomItemRepositoryImpl.java new file mode 100644 index 000000000..8ef84799b --- /dev/null +++ b/backend/src/main/java/hanglog/trip/infrastructure/CustomItemRepositoryImpl.java @@ -0,0 +1,60 @@ +package hanglog.trip.infrastructure; + +import hanglog.trip.domain.repository.CustomItemRepository; +import hanglog.trip.dto.ItemElement; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class CustomItemRepositoryImpl implements CustomItemRepository { + + private static final RowMapper elementRowMapper = (rs, rowNum) -> new ItemElement( + rs.getLong(1), + rs.getLong(2), + rs.getLong(3) + ); + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + private final JdbcTemplate jdbcTemplate; + + @Override + public List findItemIdsByDayLogIds(final List dayLogIds) { + final String sql = """ + SELECT i.id, i.expense_id, i.place_id + FROM item i + WHERE i.day_log_id IN (:dayLogIds) + """; + final MapSqlParameterSource parameters = new MapSqlParameterSource(); + parameters.addValue("dayLogIds", dayLogIds); + return namedParameterJdbcTemplate.query(sql, parameters, elementRowMapper); + } + + @Override + public void updateOrdinals(final List orderedItemIds) { + final String sql = "UPDATE item SET ordinal = :newOrdinal WHERE id = :itemId"; + final SqlParameterSource[] sqlParameterSources = getUpdateOrdinalsSqlParameterSources(orderedItemIds); + namedParameterJdbcTemplate.batchUpdate(sql, sqlParameterSources); + } + + private SqlParameterSource[] getUpdateOrdinalsSqlParameterSources(final List orderedItemIds) { + final SqlParameterSource[] sqlParameterSources = new MapSqlParameterSource[orderedItemIds.size()]; + for (int i = 0; i < orderedItemIds.size(); i++) { + final Long itemId = orderedItemIds.get(i); + final int newOrdinal = i + 1; + final Map sqlParameterSource = new HashMap<>(); + sqlParameterSource.put("newOrdinal", newOrdinal); + sqlParameterSource.put("itemId", itemId); + sqlParameterSources[i] = new MapSqlParameterSource(sqlParameterSource); + } + return sqlParameterSources; + } +} diff --git a/backend/src/main/java/hanglog/trip/infrastructure/CustomTripCityRepositoryImpl.java b/backend/src/main/java/hanglog/trip/infrastructure/CustomTripCityRepositoryImpl.java new file mode 100644 index 000000000..192873471 --- /dev/null +++ b/backend/src/main/java/hanglog/trip/infrastructure/CustomTripCityRepositoryImpl.java @@ -0,0 +1,44 @@ +package hanglog.trip.infrastructure; + +import static hanglog.global.type.StatusType.USABLE; + +import hanglog.city.domain.City; +import hanglog.trip.domain.repository.CustomTripCityRepository; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class CustomTripCityRepositoryImpl implements CustomTripCityRepository { + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + @Override + public void saveAll(final List cities, final Long tripId) { + final String sql = """ + INSERT INTO trip_city (city_id, created_at, modified_at, status, trip_id) + VALUES (:cityId, :createdAt, :modifiedAt, :status, :tripId) + """; + namedParameterJdbcTemplate.batchUpdate(sql, getTripCityToSqlParameterSources(cities, tripId)); + } + + private MapSqlParameterSource[] getTripCityToSqlParameterSources(final List cities, final Long tripId) { + return cities.stream() + .map(city -> getTripCityToSqlParameterSource(city, tripId)) + .toArray(MapSqlParameterSource[]::new); + } + + private MapSqlParameterSource getTripCityToSqlParameterSource(final City city, final Long tripId) { + final LocalDateTime now = LocalDateTime.now(); + return new MapSqlParameterSource() + .addValue("cityId", city.getId()) + .addValue("createdAt", now) + .addValue("modifiedAt", now) + .addValue("status", USABLE.name()) + .addValue("tripId", tripId); + } +} diff --git a/backend/src/main/java/hanglog/trip/infrastructure/CustomTripRepositoryImpl.java b/backend/src/main/java/hanglog/trip/infrastructure/CustomTripRepositoryImpl.java new file mode 100644 index 000000000..0335ad763 --- /dev/null +++ b/backend/src/main/java/hanglog/trip/infrastructure/CustomTripRepositoryImpl.java @@ -0,0 +1,24 @@ +package hanglog.trip.infrastructure; + +import hanglog.trip.domain.repository.CustomTripRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class CustomTripRepositoryImpl implements CustomTripRepository { + + private final JdbcTemplate jdbcTemplate; + + @Override + public List findTripIdsByMemberId(final Long memberId) { + final String sql = """ + SELECT t.id + FROM trip t + WHERE t.member_id = ? + """; + return jdbcTemplate.queryForList(sql, Long.class, memberId); + } +} diff --git a/backend/src/main/java/hanglog/image/presentation/ImageController.java b/backend/src/main/java/hanglog/trip/presentation/ImageController.java similarity index 72% rename from backend/src/main/java/hanglog/image/presentation/ImageController.java rename to backend/src/main/java/hanglog/trip/presentation/ImageController.java index 03230b8eb..1cc600499 100644 --- a/backend/src/main/java/hanglog/image/presentation/ImageController.java +++ b/backend/src/main/java/hanglog/trip/presentation/ImageController.java @@ -1,7 +1,7 @@ -package hanglog.image.presentation; +package hanglog.trip.presentation; -import hanglog.image.dto.ImagesResponse; -import hanglog.image.service.ImageService; +import hanglog.trip.dto.response.ImagesResponse; +import hanglog.trip.service.ImageService; import java.net.URI; import java.util.List; import lombok.RequiredArgsConstructor; @@ -22,7 +22,7 @@ public class ImageController { @PostMapping public ResponseEntity uploadImage(@RequestPart final List images) { final ImagesResponse imagesResponse = imageService.save(images); - final String firstImageUrl = imagesResponse.getImageUrls().get(0); - return ResponseEntity.created(URI.create(firstImageUrl)).body(imagesResponse); + final String firstImageName = imagesResponse.getImageNames().get(0); + return ResponseEntity.created(URI.create(firstImageName)).body(imagesResponse); } } diff --git a/backend/src/main/java/hanglog/trip/presentation/SharedTripController.java b/backend/src/main/java/hanglog/trip/presentation/SharedTripController.java new file mode 100644 index 000000000..ef237ba9d --- /dev/null +++ b/backend/src/main/java/hanglog/trip/presentation/SharedTripController.java @@ -0,0 +1,34 @@ +package hanglog.trip.presentation; + +import hanglog.trip.dto.response.LedgerResponse; +import hanglog.trip.dto.response.TripDetailResponse; +import hanglog.trip.service.LedgerService; +import hanglog.trip.service.SharedTripService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +@RequiredArgsConstructor +public class SharedTripController { + + private final SharedTripService sharedTripService; + private final LedgerService ledgerService; + + @GetMapping("/shared-trips/{sharedCode}") + public ResponseEntity getSharedTrip(@PathVariable final String sharedCode) { + final Long tripId = sharedTripService.getTripId(sharedCode); + final TripDetailResponse tripDetailResponse = sharedTripService.getSharedTripDetail(tripId); + return ResponseEntity.ok().body(tripDetailResponse); + } + + @GetMapping("/shared-trips/{sharedCode}/expense") + public ResponseEntity getSharedExpenses(@PathVariable final String sharedCode) { + final Long tripId = sharedTripService.getTripId(sharedCode); + final LedgerResponse ledgerResponse = ledgerService.getAllExpenses(tripId); + return ResponseEntity.ok().body(ledgerResponse); + } +} diff --git a/backend/src/main/java/hanglog/trip/presentation/TripController.java b/backend/src/main/java/hanglog/trip/presentation/TripController.java index ee22c0ec2..90a0d965b 100644 --- a/backend/src/main/java/hanglog/trip/presentation/TripController.java +++ b/backend/src/main/java/hanglog/trip/presentation/TripController.java @@ -4,10 +4,14 @@ import hanglog.auth.MemberOnly; import hanglog.auth.domain.Accessor; import hanglog.trip.dto.request.PublishedStatusRequest; +import hanglog.trip.dto.request.SharedStatusRequest; import hanglog.trip.dto.request.TripCreateRequest; import hanglog.trip.dto.request.TripUpdateRequest; +import hanglog.trip.dto.response.LedgerResponse; +import hanglog.trip.dto.response.SharedCodeResponse; import hanglog.trip.dto.response.TripDetailResponse; import hanglog.trip.dto.response.TripResponse; +import hanglog.trip.service.LedgerService; import hanglog.trip.service.TripService; import jakarta.validation.Valid; import java.net.URI; @@ -30,6 +34,7 @@ public class TripController { private final TripService tripService; + private final LedgerService ledgerService; @PostMapping @MemberOnly @@ -76,6 +81,31 @@ public ResponseEntity deleteTrip(@Auth final Accessor accessor, @PathVaria return ResponseEntity.noContent().build(); } + @GetMapping("/{tripId}/expense") + @MemberOnly + public ResponseEntity getExpenses( + @Auth final Accessor accessor, + @PathVariable final Long tripId + ) { + tripService.validateTripByMember(accessor.getMemberId(), tripId); + final LedgerResponse ledgerResponse = ledgerService.getAllExpenses(tripId); + return ResponseEntity.ok().body(ledgerResponse); + } + + @PatchMapping("/{tripId}/share") + public ResponseEntity updateSharedStatus( + @Auth final Accessor accessor, + @PathVariable final Long tripId, + @RequestBody @Valid final SharedStatusRequest sharedStatusRequest + ) { + tripService.validateTripByMember(accessor.getMemberId(), tripId); + final SharedCodeResponse sharedCodeResponse = tripService.updateSharedTripStatus( + tripId, + sharedStatusRequest + ); + return ResponseEntity.ok().body(sharedCodeResponse); + } + @PatchMapping("/{tripId}/publish") @MemberOnly public ResponseEntity updatePublishedStatus( diff --git a/backend/src/main/java/hanglog/trip/service/DayLogService.java b/backend/src/main/java/hanglog/trip/service/DayLogService.java index d60d0f382..8867c2805 100644 --- a/backend/src/main/java/hanglog/trip/service/DayLogService.java +++ b/backend/src/main/java/hanglog/trip/service/DayLogService.java @@ -5,13 +5,12 @@ import static hanglog.global.exception.ExceptionCode.INVALID_ORDERED_ITEM_IDS; import static hanglog.global.exception.ExceptionCode.NOT_FOUND_DAY_LOG_ID; import static hanglog.global.exception.ExceptionCode.NOT_FOUND_TRIP_ID; -import static hanglog.global.exception.ExceptionCode.NOT_FOUND_TRIP_ITEM_ID; import hanglog.global.exception.BadRequestException; import hanglog.trip.domain.DayLog; import hanglog.trip.domain.Item; +import hanglog.trip.domain.repository.CustomItemRepository; import hanglog.trip.domain.repository.DayLogRepository; -import hanglog.trip.domain.repository.ItemRepository; import hanglog.trip.dto.request.DayLogUpdateTitleRequest; import hanglog.trip.dto.request.ItemsOrdinalUpdateRequest; import hanglog.trip.dto.response.DayLogResponse; @@ -29,7 +28,7 @@ public class DayLogService { private final DayLogRepository dayLogRepository; - private final ItemRepository itemRepository; + private final CustomItemRepository customItemRepository; @Transactional(readOnly = true) public DayLogResponse getById(final Long id) { @@ -41,7 +40,7 @@ public DayLogResponse getById(final Long id) { } public void updateTitle(final Long id, final DayLogUpdateTitleRequest request) { - final DayLog dayLog = dayLogRepository.findById(id) + final DayLog dayLog = dayLogRepository.findWithItemsById(id) .orElseThrow(() -> new BadRequestException(NOT_FOUND_DAY_LOG_ID)); validateAlreadyDeleted(dayLog); @@ -62,13 +61,13 @@ private void validateAlreadyDeleted(final DayLog dayLog) { } public void updateOrdinalOfItems(final Long dayLogId, final ItemsOrdinalUpdateRequest itemsOrdinalUpdateRequest) { - final DayLog dayLog = dayLogRepository.findById(dayLogId) + final DayLog dayLog = dayLogRepository.findWithItemsById(dayLogId) .orElseThrow(() -> new BadRequestException(NOT_FOUND_DAY_LOG_ID)); final List items = dayLog.getItems(); final List orderedItemIds = itemsOrdinalUpdateRequest.getItemIds(); validateOrderedItemIds(items, orderedItemIds); - changeOrdinalOfItemsByOrderedItemIds(orderedItemIds); + customItemRepository.updateOrdinals(orderedItemIds); } private void validateOrderedItemIds(final List items, final List orderedItemIds) { @@ -81,14 +80,4 @@ private void validateOrderedItemIds(final List items, final List ord throw new BadRequestException(INVALID_ORDERED_ITEM_IDS); } } - - private void changeOrdinalOfItemsByOrderedItemIds(final List orderedItemIds) { - int ordinal = 1; - - for (final Long itemId : orderedItemIds) { - final Item item = itemRepository.findById(itemId) - .orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ITEM_ID)); - item.changeOrdinal(ordinal++); - } - } } diff --git a/backend/src/main/java/hanglog/trip/service/ImageService.java b/backend/src/main/java/hanglog/trip/service/ImageService.java new file mode 100644 index 000000000..9bd3c9748 --- /dev/null +++ b/backend/src/main/java/hanglog/trip/service/ImageService.java @@ -0,0 +1,53 @@ +package hanglog.trip.service; + +import static hanglog.global.exception.ExceptionCode.EMPTY_IMAGE_LIST; +import static hanglog.global.exception.ExceptionCode.EXCEED_IMAGE_LIST_SIZE; + +import hanglog.global.exception.ImageException; +import hanglog.image.domain.ImageFile; +import hanglog.image.domain.S3ImageEvent; +import hanglog.image.infrastructure.ImageUploader; +import hanglog.trip.dto.response.ImagesResponse; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +@RequiredArgsConstructor +public class ImageService { + + private static final int MAX_IMAGE_LIST_SIZE = 5; + private static final int EMPTY_LIST_SIZE = 0; + + private final ImageUploader imageUploader; + private final ApplicationEventPublisher publisher; + + public ImagesResponse save(final List images) { + validateSizeOfImages(images); + final List imageFiles = images.stream() + .map(ImageFile::new) + .toList(); + final List imageNames = uploadImages(imageFiles); + return new ImagesResponse(imageNames); + } + + private List uploadImages(final List imageFiles) { + try { + return imageUploader.uploadImages(imageFiles); + } catch (final ImageException e) { + imageFiles.forEach(imageFile -> publisher.publishEvent(new S3ImageEvent(imageFile.getHashedName()))); + throw e; + } + } + + private void validateSizeOfImages(final List images) { + if (images.size() > MAX_IMAGE_LIST_SIZE) { + throw new ImageException(EXCEED_IMAGE_LIST_SIZE); + } + if (images.size() == EMPTY_LIST_SIZE) { + throw new ImageException(EMPTY_IMAGE_LIST); + } + } +} diff --git a/backend/src/main/java/hanglog/trip/service/ItemService.java b/backend/src/main/java/hanglog/trip/service/ItemService.java index 969eaa5a6..3dfef494c 100644 --- a/backend/src/main/java/hanglog/trip/service/ItemService.java +++ b/backend/src/main/java/hanglog/trip/service/ItemService.java @@ -4,21 +4,24 @@ import static hanglog.global.exception.ExceptionCode.NOT_FOUND_CATEGORY_ID; import static hanglog.global.exception.ExceptionCode.NOT_FOUND_DAY_LOG_ID; import static hanglog.global.exception.ExceptionCode.NOT_FOUND_TRIP_ITEM_ID; -import static hanglog.image.util.ImageUrlConverter.convertUrlToName; import hanglog.category.domain.Category; import hanglog.category.domain.repository.CategoryRepository; import hanglog.currency.domain.type.CurrencyType; import hanglog.expense.domain.Amount; import hanglog.expense.domain.Expense; +import hanglog.expense.domain.repository.ExpenseRepository; import hanglog.global.exception.BadRequestException; -import hanglog.image.domain.Image; -import hanglog.image.domain.repository.ImageRepository; +import hanglog.image.domain.S3ImageEvent; import hanglog.trip.domain.DayLog; +import hanglog.trip.domain.Image; import hanglog.trip.domain.Item; import hanglog.trip.domain.Place; +import hanglog.trip.domain.repository.CustomImageRepository; import hanglog.trip.domain.repository.DayLogRepository; +import hanglog.trip.domain.repository.ImageRepository; import hanglog.trip.domain.repository.ItemRepository; +import hanglog.trip.domain.repository.PlaceRepository; import hanglog.trip.domain.type.ItemType; import hanglog.trip.dto.request.ExpenseRequest; import hanglog.trip.dto.request.ItemRequest; @@ -27,6 +30,7 @@ import hanglog.trip.dto.response.ItemResponse; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -38,26 +42,33 @@ public class ItemService { private final ItemRepository itemRepository; private final CategoryRepository categoryRepository; private final DayLogRepository dayLogRepository; + private final PlaceRepository placeRepository; + private final ExpenseRepository expenseRepository; private final ImageRepository imageRepository; + private final CustomImageRepository customImageRepository; + private final ApplicationEventPublisher publisher; public Long save(final Long tripId, final ItemRequest itemRequest) { - // TODO: 유저 인가 로직 필요 - final DayLog dayLog = dayLogRepository.findById(itemRequest.getDayLogId()) + final DayLog dayLog = dayLogRepository.findWithItemsById(itemRequest.getDayLogId()) .orElseThrow(() -> new BadRequestException(NOT_FOUND_DAY_LOG_ID)); validateAssociationTripAndDayLog(tripId, dayLog); + final List images = makeImages(itemRequest); final Item item = new Item( ItemType.getItemTypeByIsSpot(itemRequest.getItemType()), itemRequest.getTitle(), - getNewItemOrdinal(dayLog.getId()), + getNewItemOrdinal(dayLog), itemRequest.getRating(), itemRequest.getMemo(), makePlace(itemRequest.getPlace()), dayLog, makeExpense(itemRequest.getExpense()), - makeImages(itemRequest) + images ); - return itemRepository.save(item).getId(); + final Item savedItem = itemRepository.save(item); + images.forEach(image -> image.setItem(savedItem)); + customImageRepository.saveAll(images); + return savedItem.getId(); } private void validateAssociationTripAndDayLog(final Long tripId, final DayLog dayLog) { @@ -68,30 +79,20 @@ private void validateAssociationTripAndDayLog(final Long tripId, final DayLog da } private List makeImages(final ItemRequest itemRequest) { - final List images = itemRequest.getImageUrls().stream() - .map(imageUrl -> new Image(convertUrlToName(imageUrl))) + return itemRequest.getImageNames().stream() + .map(Image::new) .toList(); - - return imageRepository.saveAll(images); } public void update(final Long tripId, final Long itemId, final ItemUpdateRequest itemUpdateRequest) { - final DayLog dayLog = dayLogRepository.findById(itemUpdateRequest.getDayLogId()) + final DayLog dayLog = dayLogRepository.findWithItemDetailsById(itemUpdateRequest.getDayLogId()) .orElseThrow(() -> new BadRequestException(NOT_FOUND_DAY_LOG_ID)); validateAssociationTripAndDayLog(tripId, dayLog); - final Item item = itemRepository.findById(itemId) + final Item item = dayLog.getItems().stream() + .filter(target -> target.getId().equals(itemId)) + .findFirst() .orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ITEM_ID)); - - Place updatedPlace = item.getPlace(); - if (itemUpdateRequest.getIsPlaceUpdated() || isChangedToSpot(itemUpdateRequest, item)) { - updatedPlace = makePlace(itemUpdateRequest.getPlace()); - } - - if (item.getItemType() == ItemType.SPOT && !itemUpdateRequest.getItemType()) { - updatedPlace = null; - } - final Item updatedItem = new Item( itemId, ItemType.getItemTypeByIsSpot(itemUpdateRequest.getItemType()), @@ -99,27 +100,57 @@ public void update(final Long tripId, final Long itemId, final ItemUpdateRequest item.getOrdinal(), itemUpdateRequest.getRating(), itemUpdateRequest.getMemo(), - updatedPlace, + makeUpdatedPlace(itemUpdateRequest, item), dayLog, - makeExpense(itemUpdateRequest.getExpense()), - makeUpdatedImages(itemUpdateRequest, item.getImages()) + makeUpdatedExpense(itemUpdateRequest.getExpense(), item.getExpense()), + makeUpdatedImages(itemUpdateRequest, item) ); - itemRepository.save(updatedItem); } - private boolean isChangedToSpot(final ItemUpdateRequest itemUpdateRequest, final Item item) { - return item.getItemType().equals(ItemType.NON_SPOT) && itemUpdateRequest.getPlace() != null; + private Place makeUpdatedPlace(final ItemUpdateRequest itemUpdateRequest, final Item item) { + final Place originalPlace = item.getPlace(); + if (isPlaceNotUpdated(itemUpdateRequest, item)) { + return originalPlace; + } + final Place updatedPlace = createPlaceByPlaceRequest(itemUpdateRequest.getPlace()); + deleteNotUsedPlace(originalPlace); + return saveNewlyUpdatedPlace(updatedPlace); + } + + boolean isPlaceNotUpdated(final ItemUpdateRequest itemUpdateRequest, final Item item) { + if (!item.isSpot() && !itemUpdateRequest.getIsPlaceUpdated()) { + return true; + } + return item.isSpot() && !itemUpdateRequest.getIsPlaceUpdated() && itemUpdateRequest.getItemType(); + } + + private void deleteNotUsedPlace(final Place place) { + if (place == null) { + return; + } + placeRepository.delete(place); + } + + private Place saveNewlyUpdatedPlace(final Place updatedPlace) { + if (updatedPlace == null) { + return null; + } + return placeRepository.save(updatedPlace); } private Place makePlace(final PlaceRequest placeRequest) { - if (placeRequest == null) { + final Place place = createPlaceByPlaceRequest(placeRequest); + if (place == null) { return null; } - return createPlaceByPlaceRequest(placeRequest); + return placeRepository.save(place); } private Place createPlaceByPlaceRequest(final PlaceRequest placeRequest) { + if (placeRequest == null) { + return null; + } return new Place( placeRequest.getName(), placeRequest.getLatitude(), @@ -136,35 +167,87 @@ private Category findCategoryByApiCategory(final List apiCategory) { return categories.get(0); } - private List makeUpdatedImages(final ItemUpdateRequest itemUpdateRequest, final List originalImages) { - final List updatedImages = itemUpdateRequest.getImageUrls().stream() - .map(imageUrl -> makeUpdatedImage(imageUrl, originalImages)) + private List makeUpdatedImages(final ItemUpdateRequest itemUpdateRequest, final Item item) { + final List originalImages = item.getImages(); + final List updatedImages = itemUpdateRequest.getImageNames().stream() + .map(imageName -> makeUpdatedImage(imageName, originalImages, item)) .toList(); - return imageRepository.saveAll(updatedImages); + deleteNotUsedImages(originalImages, updatedImages); + saveNewlyUpdatedImages(originalImages, updatedImages); + return updatedImages; } - private Image makeUpdatedImage(final String imageUrl, final List originalImages) { - final String imageName = convertUrlToName(imageUrl); - + private Image makeUpdatedImage(final String imageName, final List originalImages, final Item item) { return originalImages.stream() .filter(originalImage -> originalImage.getName().equals(imageName)) .findAny() - .orElseGet(() -> new Image(imageName)); + .orElseGet(() -> new Image(imageName, item)); + } + + private void saveNewlyUpdatedImages(final List originalImages, final List updatedImages) { + final List newImages = updatedImages.stream() + .filter(image -> !originalImages.contains(image)) + .toList(); + customImageRepository.saveAll(newImages); + } + + private void deleteNotUsedImages(final List originalImages, final List updatedImages) { + final List deletedImages = originalImages.stream() + .filter(image -> !updatedImages.contains(image)) + .toList(); + if (deletedImages.isEmpty()) { + return; + } + customImageRepository.deleteAll(deletedImages); + deletedImages.forEach(image -> publisher.publishEvent(new S3ImageEvent(image.getName()))); + } + + private Expense makeUpdatedExpense(final ExpenseRequest expenseRequest, final Expense originalExpense) { + final Expense updatedExpense = createExpenseByExpenseRequest(expenseRequest); + if (isExpenseNotUpdated(updatedExpense, originalExpense)) { + return originalExpense; + } + deleteNotUsedExpense(originalExpense); + return saveNewlyUpdatedExpense(updatedExpense); + } + + private void deleteNotUsedExpense(final Expense expense) { + if (expense == null) { + return; + } + expenseRepository.delete(expense); + } + + private boolean isExpenseNotUpdated(final Expense updatedExpense, final Expense originalExpense) { + if (updatedExpense == null && originalExpense == null) { + return true; + } + return updatedExpense != null && updatedExpense.equals(originalExpense); + } + + private Expense saveNewlyUpdatedExpense(final Expense updatedExpense) { + if (updatedExpense == null) { + return null; + } + return expenseRepository.save(updatedExpense); } private Expense makeExpense(final ExpenseRequest expenseRequest) { - if (expenseRequest == null) { + final Expense expense = createExpenseByExpenseRequest(expenseRequest); + if (expense == null) { return null; } - return createExpenseByExpenseRequest(expenseRequest); + return expenseRepository.save(expense); } private Expense createExpenseByExpenseRequest(final ExpenseRequest expenseRequest) { + if (expenseRequest == null) { + return null; + } final Category expenseCategory = categoryRepository.findById(expenseRequest.getCategoryId()) .orElseThrow(() -> new BadRequestException(NOT_FOUND_CATEGORY_ID)); final String currency = CurrencyType.getMappedCurrencyType(expenseRequest.getCurrency()).getCode(); - return new Expense( currency, new Amount(expenseRequest.getAmount()), @@ -172,19 +255,28 @@ private Expense createExpenseByExpenseRequest(final ExpenseRequest expenseReques ); } - private int getNewItemOrdinal(final Long dayLogId) { - return dayLogRepository.findById(dayLogId) - .orElseThrow(() -> new BadRequestException(NOT_FOUND_DAY_LOG_ID)) - .getItems() - .size() + 1; + private int getNewItemOrdinal(final DayLog dayLog) { + return dayLog.getItems().size() + 1; } public void delete(final Long itemId) { final Item item = itemRepository.findById(itemId) .orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ITEM_ID)); - itemRepository.delete(item); + + if (!item.getImages().isEmpty()) { + imageRepository.deleteByItemId(itemId); + } + if (item.getPlace() != null) { + placeRepository.deleteById(item.getPlace().getId()); + } + if (item.getExpense() != null) { + expenseRepository.deleteById(item.getExpense().getId()); + } + itemRepository.deleteById(itemId); + item.getImages().forEach(image -> publisher.publishEvent(new S3ImageEvent(image.getName()))); } + @Transactional(readOnly = true) public List getItems() { return itemRepository.findAll().stream() .map(ItemResponse::of) diff --git a/backend/src/main/java/hanglog/expense/service/ExpenseService.java b/backend/src/main/java/hanglog/trip/service/LedgerService.java similarity index 64% rename from backend/src/main/java/hanglog/expense/service/ExpenseService.java rename to backend/src/main/java/hanglog/trip/service/LedgerService.java index 17fcfe93b..5415a20c7 100644 --- a/backend/src/main/java/hanglog/expense/service/ExpenseService.java +++ b/backend/src/main/java/hanglog/trip/service/LedgerService.java @@ -1,25 +1,26 @@ -package hanglog.expense.service; +package hanglog.trip.service; import static hanglog.global.exception.ExceptionCode.NOT_FOUND_CURRENCY_DATA; import static hanglog.global.exception.ExceptionCode.NOT_FOUND_TRIP_ID; import hanglog.category.domain.Category; +import hanglog.category.domain.ExpenseCategories; import hanglog.category.domain.repository.CategoryRepository; +import hanglog.city.domain.City; +import hanglog.city.domain.repository.CityRepository; import hanglog.currency.domain.Currency; +import hanglog.currency.domain.OldestCurrency; import hanglog.currency.domain.repository.CurrencyRepository; import hanglog.currency.domain.type.CurrencyType; import hanglog.expense.domain.Amount; import hanglog.expense.domain.CategoryExpense; -import hanglog.expense.domain.DayLogExpense; import hanglog.expense.domain.Expense; -import hanglog.expense.dto.response.TripExpenseResponse; import hanglog.global.exception.BadRequestException; import hanglog.trip.domain.DayLog; -import hanglog.trip.domain.Item; +import hanglog.trip.domain.DayLogExpense; import hanglog.trip.domain.Trip; -import hanglog.trip.domain.TripCity; -import hanglog.trip.domain.repository.TripCityRepository; import hanglog.trip.domain.repository.TripRepository; +import hanglog.trip.dto.response.LedgerResponse; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -30,23 +31,24 @@ @Service @RequiredArgsConstructor -@Transactional(readOnly = true) -public class ExpenseService { +@Transactional +public class LedgerService { private final TripRepository tripRepository; private final CurrencyRepository currencyRepository; - private final TripCityRepository tripCityRepository; + private final CityRepository cityRepository; private final CategoryRepository categoryRepository; - public TripExpenseResponse getAllExpenses(final Long tripId) { + @Transactional(readOnly = true) + public LedgerResponse getAllExpenses(final Long tripId) { final Trip trip = tripRepository.findById(tripId) .orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ID)); final Currency currency = currencyRepository.findTopByDateLessThanEqualOrderByDateDesc(trip.getStartDate()) - .orElse(findOldestCurrency()); + .orElseGet(this::findOldestCurrency); final Map dayLogAmounts = getDayLogAmounts(trip.getDayLogs()); final Map categoryAmounts = getCategoryAmounts(); - final List tripCities = tripCityRepository.findByTripId(tripId); + final List cities = cityRepository.findCitiesByTripId(tripId); for (final DayLog dayLog : trip.getDayLogs()) { calculateAmounts(dayLog, currency, dayLogAmounts, categoryAmounts); @@ -56,17 +58,17 @@ public TripExpenseResponse getAllExpenses(final Long tripId) { final List categoryExpenses = categoryAmounts.entrySet().stream() .map(entry -> new CategoryExpense(entry.getKey(), entry.getValue(), totalAmount)) - .sorted((o1,o2) -> o2.getAmount().compareTo(o1.getAmount())) + .sorted((o1, o2) -> o2.getAmount().compareTo(o1.getAmount())) .toList(); final List dayLogExpenses = dayLogAmounts.entrySet().stream() .map(entry -> new DayLogExpense(entry.getKey(), entry.getValue())) .toList(); - return TripExpenseResponse.of( + return LedgerResponse.of( trip, totalAmount, - tripCities, + cities, categoryExpenses, currency, dayLogExpenses @@ -74,8 +76,14 @@ public TripExpenseResponse getAllExpenses(final Long tripId) { } private Currency findOldestCurrency() { - return currencyRepository.findTopByOrderByDateAsc() + final Optional oldestCurrency = OldestCurrency.get(); + if (oldestCurrency.isPresent()) { + return oldestCurrency.get(); + } + final Currency currency = currencyRepository.findTopByOrderByDateAsc() .orElseThrow(() -> new BadRequestException(NOT_FOUND_CURRENCY_DATA)); + OldestCurrency.init(currency); + return currency; } private void calculateAmounts( @@ -84,19 +92,17 @@ private void calculateAmounts( final Map dayLogAmounts, final Map categoryAmounts ) { - for (final Item item : dayLog.getItems()) { - final Optional expense = Optional.ofNullable(item.getExpense()); - - if (expense.isPresent()) { - final Amount KRWAmount = changeToKRW(expense.get(), currency); - - final Amount dayLogAmount = dayLogAmounts.get(dayLog); - dayLogAmounts.put(dayLog, dayLogAmount.add(KRWAmount)); - - final Amount categoryAmount = categoryAmounts.get(expense.get().getCategory()); - categoryAmounts.put(expense.get().getCategory(), categoryAmount.add(KRWAmount)); - } - } + dayLog.getItems().stream() + .map(item -> Optional.ofNullable(item.getExpense())) + .filter(Optional::isPresent) + .forEach(expense -> { + final Amount KRWAmount = changeToKRW(expense.get(), currency); + final Amount dayLogAmount = dayLogAmounts.get(dayLog); + dayLogAmounts.put(dayLog, dayLogAmount.add(KRWAmount)); + final Category category = expense.get().getCategory(); + final Amount categoryAmount = categoryAmounts.get(category); + categoryAmounts.put(category, categoryAmount.add(KRWAmount)); + }); } private Amount changeToKRW(final Expense expense, final Currency currency) { @@ -119,11 +125,20 @@ private Map getDayLogAmounts(final List dayLogs) { } private Map getCategoryAmounts() { - final List categories = categoryRepository.findExpenseCategory(); final Map categoryAmounts = new LinkedHashMap<>(); - for (final Category category : categories) { + for (final Category category : findCategories()) { categoryAmounts.put(category, Amount.ZERO); } return categoryAmounts; } + + private List findCategories() { + final Optional> expenseCategories = ExpenseCategories.get(); + if (expenseCategories.isPresent()) { + return expenseCategories.get(); + } + final List categories = categoryRepository.findExpenseCategory(); + ExpenseCategories.init(categories); + return categories; + } } diff --git a/backend/src/main/java/hanglog/share/service/SharedTripService.java b/backend/src/main/java/hanglog/trip/service/SharedTripService.java similarity index 50% rename from backend/src/main/java/hanglog/share/service/SharedTripService.java rename to backend/src/main/java/hanglog/trip/service/SharedTripService.java index 9eaeea570..66f8e57e2 100644 --- a/backend/src/main/java/hanglog/share/service/SharedTripService.java +++ b/backend/src/main/java/hanglog/trip/service/SharedTripService.java @@ -1,22 +1,18 @@ -package hanglog.share.service; +package hanglog.trip.service; import static hanglog.global.exception.ExceptionCode.INVALID_SHARE_CODE; import static hanglog.global.exception.ExceptionCode.NOT_FOUND_SHARED_CODE; import static hanglog.global.exception.ExceptionCode.NOT_FOUND_TRIP_ID; import hanglog.city.domain.City; +import hanglog.city.domain.repository.CityRepository; import hanglog.global.exception.BadRequestException; -import hanglog.share.domain.SharedTrip; -import hanglog.share.domain.repository.SharedTripRepository; -import hanglog.share.dto.request.SharedTripStatusRequest; -import hanglog.share.dto.response.SharedTripCodeResponse; +import hanglog.trip.domain.SharedTrip; import hanglog.trip.domain.Trip; -import hanglog.trip.domain.TripCity; -import hanglog.trip.domain.repository.TripCityRepository; +import hanglog.trip.domain.repository.SharedTripRepository; import hanglog.trip.domain.repository.TripRepository; import hanglog.trip.dto.response.TripDetailResponse; import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,8 +23,8 @@ public class SharedTripService { private final SharedTripRepository sharedTripRepository; - private final TripCityRepository tripCityRepository; private final TripRepository tripRepository; + private final CityRepository cityRepository; @Transactional(readOnly = true) public Long getTripId(final String sharedCode) { @@ -42,36 +38,11 @@ public Long getTripId(final String sharedCode) { return sharedTrip.getTrip().getId(); } + @Transactional(readOnly = true) public TripDetailResponse getSharedTripDetail(final Long tripId) { final Trip trip = tripRepository.findById(tripId) .orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ID)); - final List cities = getCitiesByTripId(tripId); + final List cities = cityRepository.findCitiesByTripId(tripId); return TripDetailResponse.sharedTrip(trip, cities); } - - private List getCitiesByTripId(final Long tripId) { - return tripCityRepository.findByTripId(tripId).stream() - .map(TripCity::getCity) - .toList(); - } - - public SharedTripCodeResponse updateSharedTripStatus( - final Long tripId, - final SharedTripStatusRequest sharedTripStatusRequest - ) { - final Trip trip = tripRepository.findTripById(tripId) - .orElse(findTripWithNoFetch(tripId)); - - final SharedTrip sharedTrip = Optional.ofNullable(trip.getSharedTrip()) - .orElseGet(() -> SharedTrip.createdBy(trip)); - - sharedTrip.changeSharedStatus(sharedTripStatusRequest.getSharedStatus()); - sharedTripRepository.save(sharedTrip); - return SharedTripCodeResponse.of(sharedTrip); - } - - private Trip findTripWithNoFetch(final Long tripId) { - return tripRepository.findById(tripId) - .orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ID)); - } } diff --git a/backend/src/main/java/hanglog/trip/service/TripService.java b/backend/src/main/java/hanglog/trip/service/TripService.java index 67d4a3d49..046945a3e 100644 --- a/backend/src/main/java/hanglog/trip/service/TripService.java +++ b/backend/src/main/java/hanglog/trip/service/TripService.java @@ -7,27 +7,37 @@ import hanglog.city.domain.City; import hanglog.city.domain.repository.CityRepository; -import hanglog.community.domain.PublishedTrip; -import hanglog.community.domain.type.PublishedStatusType; import hanglog.global.exception.AuthException; import hanglog.global.exception.BadRequestException; +import hanglog.image.domain.S3ImageEvent; import hanglog.member.domain.Member; import hanglog.member.domain.repository.MemberRepository; import hanglog.trip.domain.DayLog; +import hanglog.trip.domain.PublishDeleteEvent; +import hanglog.trip.domain.PublishEvent; +import hanglog.trip.domain.SharedTrip; import hanglog.trip.domain.Trip; -import hanglog.trip.domain.TripCity; -import hanglog.trip.domain.repository.PublishedTripRepository; +import hanglog.trip.domain.TripDeleteEvent; +import hanglog.trip.domain.repository.CustomDayLogRepository; +import hanglog.trip.domain.repository.CustomTripCityRepository; +import hanglog.trip.domain.repository.SharedTripRepository; import hanglog.trip.domain.repository.TripCityRepository; import hanglog.trip.domain.repository.TripRepository; +import hanglog.trip.domain.type.PublishedStatusType; import hanglog.trip.dto.request.PublishedStatusRequest; +import hanglog.trip.dto.request.SharedStatusRequest; import hanglog.trip.dto.request.TripCreateRequest; import hanglog.trip.dto.request.TripUpdateRequest; +import hanglog.trip.dto.response.SharedCodeResponse; import hanglog.trip.dto.response.TripDetailResponse; import hanglog.trip.dto.response.TripResponse; import java.time.temporal.ChronoUnit; import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -42,7 +52,10 @@ public class TripService { private final CityRepository cityRepository; private final TripCityRepository tripCityRepository; private final MemberRepository memberRepository; - private final PublishedTripRepository publishedTripRepository; + private final SharedTripRepository sharedTripRepository; + private final CustomDayLogRepository customDayLogRepository; + private final CustomTripCityRepository customTripCityRepository; + private final ApplicationEventPublisher publisher; public void validateTripByMember(final Long memberId, final Long tripId) { if (!tripRepository.existsByMemberIdAndId(memberId, tripId)) { @@ -53,30 +66,24 @@ public void validateTripByMember(final Long memberId, final Long tripId) { public Long save(final Long memberId, final TripCreateRequest tripCreateRequest) { final Member member = memberRepository.findById(memberId) .orElseThrow(() -> new BadRequestException(NOT_FOUND_MEMBER_ID)); - final List cites = tripCreateRequest.getCityIds().stream() - .map(cityId -> cityRepository.findById(cityId) - .orElseThrow(() -> new BadRequestException(NOT_FOUND_CITY_ID))) - .toList(); + + final List cities = cityRepository.findCitiesByIds(tripCreateRequest.getCityIds()); + if (cities.size() != tripCreateRequest.getCityIds().size()) { + throw new BadRequestException(NOT_FOUND_CITY_ID); + } final Trip newTrip = Trip.of( member, - generateInitialTitle(cites), + generateInitialTitle(cities), tripCreateRequest.getStartDate(), tripCreateRequest.getEndDate() ); - saveTripCities(cites, newTrip); - saveDayLogs(newTrip); final Trip trip = tripRepository.save(newTrip); + customTripCityRepository.saveAll(cities, trip.getId()); + saveDayLogs(trip); return trip.getId(); } - private void saveTripCities(final List cites, final Trip trip) { - final List tripCities = cites.stream() - .map(city -> new TripCity(trip, city)) - .toList(); - tripCityRepository.saveAll(tripCities); - } - private void saveDayLogs(final Trip savedTrip) { final int days = (int) ChronoUnit.DAYS.between( savedTrip.getStartDate(), @@ -85,9 +92,10 @@ private void saveDayLogs(final Trip savedTrip) { final List dayLogs = IntStream.rangeClosed(1, days + 1) .mapToObj(ordinal -> DayLog.generateEmpty(ordinal, savedTrip)) .toList(); - savedTrip.getDayLogs().addAll(dayLogs); + customDayLogRepository.saveAll(dayLogs); } + @Transactional(readOnly = true) public List getAllTrips(final Long memberId) { final List trips = tripRepository.findAllByMemberId(memberId); return trips.stream() @@ -96,23 +104,18 @@ public List getAllTrips(final Long memberId) { } private TripResponse getTripResponse(final Trip trip) { - final List cities = getCitiesByTripId(trip.getId()); + final List cities = cityRepository.findCitiesByTripId(trip.getId()); return TripResponse.of(trip, cities); } - + + @Transactional(readOnly = true) public TripDetailResponse getTripDetail(final Long tripId) { final Trip trip = tripRepository.findById(tripId) .orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ID)); - final List cities = getCitiesByTripId(tripId); + final List cities = cityRepository.findCitiesByTripId(tripId); return TripDetailResponse.personalTrip(trip, cities); } - private List getCitiesByTripId(final Long tripId) { - return tripCityRepository.findByTripId(tripId).stream() - .map(TripCity::getCity) - .toList(); - } - public void update(final Long tripId, final TripUpdateRequest updateRequest) { final Trip trip = tripRepository.findById(tripId) .orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ID)); @@ -121,16 +124,17 @@ public void update(final Long tripId, final TripUpdateRequest updateRequest) { .orElseThrow(() -> new BadRequestException(NOT_FOUND_CITY_ID))) .toList(); - updateTripCities(tripId, trip, cities); + updateTripCities(tripId, cities); updateDayLog(updateRequest, trip); + updateImage(trip.getImageName(), updateRequest.getImageName()); trip.update(updateRequest); tripRepository.save(trip); } - private void updateTripCities(final Long tripId, final Trip trip, final List cities) { + private void updateTripCities(final Long tripId, final List cities) { // TODO: 전체 삭제 후 지우는 로직 말고 다른 방법으로 리팩토링 필요 tripCityRepository.deleteAllByTripId(tripId); - saveTripCities(cities, trip); + customTripCityRepository.saveAll(cities, tripId); } private void updateDayLog(final TripUpdateRequest updateRequest, final Trip trip) { @@ -147,6 +151,13 @@ private void updateDayLog(final TripUpdateRequest updateRequest, final Trip trip } } + private void updateImage(final String originalImageName, final String updateImageName) { + if (originalImageName.equals(updateImageName)) { + return; + } + publisher.publishEvent(new S3ImageEvent(originalImageName)); + } + private void updateDayLogByPeriod(final Trip trip, final int currentPeriod, final int requestPeriod) { final DayLog extraDayLog = trip.getDayLogs().get(currentPeriod); if (currentPeriod < requestPeriod) { @@ -162,26 +173,54 @@ private void addEmptyDayLogs(final Trip trip, final int currentPeriod, final int final List emptyDayLogs = IntStream.range(currentPeriod, requestPeriod) .mapToObj(ordinal -> DayLog.generateEmpty(ordinal + 1, trip)) .toList(); - trip.getDayLogs().addAll(emptyDayLogs); + emptyDayLogs.forEach(trip::addDayLog); } private void removeRemainingDayLogs(final Trip trip, final int currentPeriod, final int requestPeriod) { - trip.getDayLogs() - .removeIf(dayLog -> dayLog.getOrdinal() >= requestPeriod + 1 && dayLog.getOrdinal() <= currentPeriod); + trip.getDayLogs().stream() + .filter(getDayLogOutOfPeriod(currentPeriod, requestPeriod)) + .forEach(trip::removeDayLog); + } + + private Predicate getDayLogOutOfPeriod(final int currentPeriod, final int requestPeriod) { + return dayLog -> dayLog.getOrdinal() >= requestPeriod + 1 && dayLog.getOrdinal() <= currentPeriod; } public void delete(final Long tripId) { - final Trip trip = tripRepository.findById(tripId) - .orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ID)); - tripRepository.delete(trip); - tripCityRepository.deleteAllByTripId(tripId); - publishedTripRepository.deleteByTripId(tripId); + if (!tripRepository.existsById(tripId)) { + throw new BadRequestException(NOT_FOUND_TRIP_ID); + } + + publisher.publishEvent(new PublishDeleteEvent(tripId)); + sharedTripRepository.deleteByTripId(tripId); + tripRepository.deleteById(tripId); + publisher.publishEvent(new TripDeleteEvent(tripId)); } private String generateInitialTitle(final List cites) { return cites.get(0).getName() + TITLE_POSTFIX; } + public SharedCodeResponse updateSharedTripStatus( + final Long tripId, + final SharedStatusRequest sharedStatusRequest + ) { + final Trip trip = tripRepository.findTripById(tripId) + .orElse(findTripWithNoFetch(tripId)); + + final SharedTrip sharedTrip = Optional.ofNullable(trip.getSharedTrip()) + .orElseGet(() -> SharedTrip.createdBy(trip)); + + sharedTrip.changeSharedStatus(sharedStatusRequest.getSharedStatus()); + sharedTripRepository.save(sharedTrip); + return SharedCodeResponse.of(sharedTrip); + } + + private Trip findTripWithNoFetch(final Long tripId) { + return tripRepository.findById(tripId) + .orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ID)); + } + public void updatePublishedStatus(final Long tripId, final PublishedStatusRequest publishedStatusRequest) { final Trip trip = tripRepository.findById(tripId) .orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ID)); @@ -207,9 +246,6 @@ private void unpublishTrip(final Trip trip) { private void publishTrip(final Trip trip) { trip.changePublishedStatus(true); - if (!publishedTripRepository.existsByTripId(trip.getId())) { - final PublishedTrip publishedTrip = new PublishedTrip(trip); - publishedTripRepository.save(publishedTrip); - } + publisher.publishEvent(new PublishEvent(trip.getId())); } } diff --git a/backend/src/main/resources/info-appender.xml b/backend/src/main/resources/info-appender.xml index 2af1e7482..bdb572f3a 100644 --- a/backend/src/main/resources/info-appender.xml +++ b/backend/src/main/resources/info-appender.xml @@ -8,8 +8,7 @@ - "timestamp": "%date{yyyy-MM-dd HH:mm}", - ${CONSOLE_LOG_PATTERN}\n + [%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5level ${PID:-} --- [%15.15thread] %-40.40logger{36} : %msg%n%n utf8 diff --git a/backend/src/test/java/hanglog/auth/domain/MemberFixture.java b/backend/src/test/java/hanglog/auth/domain/MemberFixture.java deleted file mode 100644 index 1aa78f761..000000000 --- a/backend/src/test/java/hanglog/auth/domain/MemberFixture.java +++ /dev/null @@ -1,33 +0,0 @@ -package hanglog.auth.domain; - -public class MemberFixture { - - public static final String GOOGLE_USER_INFO_JSON_STRING = - "{\"id\":\"google_id\"," - + "\"email\":\"test@test.com\"," - + "\"verified_email\":true," - + "\"name\":\"google_test\"," - + "\"given_name\":\"google_test\"," - + "\"family_name\":\"google_test\"," - + "\"picture\":\"google_image_url\"," - + "\"locale\":\"ko\"}"; - public static final String KAKAO_USER_INFO_JSON_STRING = - "{\"id\":\"kakao_id\"," - + "\"connected_at\":\"2023-07-13T01:18:52Z\"," - + "\"properties\":{" - + "\"nickname\":\"kakao_test\"," - + "\"profile_image\":\"kakao_image_url\"," - + "\"thumbnail_image\":\"test_thumbnail_image\"}," - + "\"kakao_account\":{\"profile_nickname_needs_agreement\":false," - + "\"profile_image_needs_agreement\":false," - + "\"profile\":{" - + "\"nickname\":\"kakao_test\"," - + "\"thumbnail_image_url\":\"kakao_image_url\"," - + "\"is_default_image\":false}," - + "\"profile_image_url\":\"test_thumbnail_image\"," - + "\"has_email\":true," - + "\"email_needs_agreement\":false," - + "\"is_email_valid\":true," - + "\"is_email_verified\":true," - + "\"email\":\"fruturum@nate.com\"}}"; -} diff --git a/backend/src/test/java/hanglog/community/presentation/CommunityControllerTest.java b/backend/src/test/java/hanglog/community/presentation/CommunityControllerTest.java index c4943c1da..3984ef23c 100644 --- a/backend/src/test/java/hanglog/community/presentation/CommunityControllerTest.java +++ b/backend/src/test/java/hanglog/community/presentation/CommunityControllerTest.java @@ -6,6 +6,7 @@ import static hanglog.global.restdocs.RestDocsConfiguration.field; import static hanglog.trip.fixture.CityFixture.LONDON; import static hanglog.trip.fixture.CityFixture.PARIS; +import static hanglog.trip.fixture.CityFixture.TOKYO; import static hanglog.trip.fixture.DayLogFixture.EXPENSE_LONDON_DAYLOG; import static hanglog.trip.fixture.TripFixture.LONDON_TO_JAPAN; import static hanglog.trip.fixture.TripFixture.LONDON_TRIP; @@ -24,20 +25,18 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import hanglog.auth.domain.MemberTokens; import hanglog.city.domain.City; import hanglog.community.dto.response.CommunityTripListResponse; import hanglog.community.dto.response.CommunityTripResponse; import hanglog.community.dto.response.RecommendTripListResponse; import hanglog.community.service.CommunityService; import hanglog.expense.domain.CategoryExpense; -import hanglog.expense.domain.DayLogExpense; -import hanglog.expense.dto.response.TripExpenseResponse; -import hanglog.expense.service.ExpenseService; import hanglog.global.ControllerTest; -import hanglog.trip.domain.TripCity; +import hanglog.login.domain.MemberTokens; +import hanglog.trip.domain.DayLogExpense; +import hanglog.trip.dto.response.LedgerResponse; import hanglog.trip.dto.response.TripDetailResponse; -import hanglog.trip.fixture.CityFixture; +import hanglog.trip.service.LedgerService; import jakarta.servlet.http.Cookie; import java.time.LocalDateTime; import java.util.List; @@ -70,7 +69,7 @@ class CommunityControllerTest extends ControllerTest { private CommunityService communityService; @MockBean - private ExpenseService expenseService; + private LedgerService ledgerService; @BeforeEach void setUp() { @@ -113,7 +112,7 @@ private ResultActions performGetExpense(final int tripId) throws Exception { @Test void getTripsByPage() throws Exception { // given - when(communityService.getTripsByPage(any(), any())) + when(communityService.getCommunityTripsByPage(any(), any())) .thenReturn(new CommunityTripListResponse( List.of(CommunityTripResponse.of(LONDON_TRIP, CITIES, true, 1L)), 1L @@ -169,10 +168,10 @@ void getTripsByPage() throws Exception { .type(JsonFieldType.BOOLEAN) .description("좋아요 유무") .attributes(field("constraint", "boolean (true : 좋아요)")), - fieldWithPath("trips[].imageUrl") + fieldWithPath("trips[].imageName") .type(JsonFieldType.STRING) .description("여행 대표 이미지") - .attributes(field("constraint", "이미지 URL")), + .attributes(field("constraint", "이미지 이름")), fieldWithPath("trips[].cities") .type(JsonFieldType.ARRAY) .description("여행 도시 배열") @@ -268,10 +267,10 @@ void getRecommendTrips() throws Exception { .type(JsonFieldType.BOOLEAN) .description("좋아요 유무") .attributes(field("constraint", "boolean (true : 좋아요)")), - fieldWithPath("trips[].imageUrl") + fieldWithPath("trips[].imageName") .type(JsonFieldType.STRING) .description("여행 대표 이미지") - .attributes(field("constraint", "이미지 URL")), + .attributes(field("constraint", "이미지 이름")), fieldWithPath("trips[].cities") .type(JsonFieldType.ARRAY) .description("여행 도시 배열") @@ -363,10 +362,10 @@ void getTrip() throws Exception { .type(JsonFieldType.STRING) .description("여행 요약") .attributes(field("constraint", "200자 이하의 문자열")), - fieldWithPath("imageUrl") + fieldWithPath("imageName") .type(JsonFieldType.STRING) .description("여행 대표 이미지") - .attributes(field("constraint", "이미지 URL")), + .attributes(field("constraint", "이미지 이름")), fieldWithPath("sharedCode") .type(JsonFieldType.STRING) .description("공유 코드") @@ -457,11 +456,11 @@ void getTrip() throws Exception { @Test void getExpenses() throws Exception { // given - when(expenseService.getAllExpenses(any())).thenReturn( - TripExpenseResponse.of( + when(ledgerService.getAllExpenses(any())).thenReturn( + LedgerResponse.of( LONDON_TO_JAPAN, AMOUNT_20000, - List.of(new TripCity(LONDON_TRIP, LONDON), new TripCity(LONDON_TRIP, CityFixture.TOKYO)), + List.of(LONDON, TOKYO), List.of(new CategoryExpense(EXPENSE_CATEGORIES.get(1), AMOUNT_20000, AMOUNT_20000)), DEFAULT_CURRENCY, List.of(new DayLogExpense(EXPENSE_LONDON_DAYLOG, AMOUNT_20000)) diff --git a/backend/src/test/java/hanglog/community/presentation/LikeControllerTest.java b/backend/src/test/java/hanglog/community/presentation/LikeControllerTest.java index c5fd17e17..ffff8a020 100644 --- a/backend/src/test/java/hanglog/community/presentation/LikeControllerTest.java +++ b/backend/src/test/java/hanglog/community/presentation/LikeControllerTest.java @@ -14,10 +14,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import hanglog.auth.domain.MemberTokens; -import hanglog.community.dto.request.LikeRequest; -import hanglog.community.service.LikeService; import hanglog.global.ControllerTest; +import hanglog.like.dto.request.LikeRequest; +import hanglog.like.presentation.LikeController; +import hanglog.like.service.LikeService; +import hanglog.login.domain.MemberTokens; import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/backend/src/test/java/hanglog/expense/fixture/TripExpenseFixture.java b/backend/src/test/java/hanglog/expense/fixture/TripExpenseFixture.java index d39daf92e..c1745ef62 100644 --- a/backend/src/test/java/hanglog/expense/fixture/TripExpenseFixture.java +++ b/backend/src/test/java/hanglog/expense/fixture/TripExpenseFixture.java @@ -5,12 +5,12 @@ import static hanglog.expense.fixture.ExpenseFixture.USD_100_ACCOMMODATION_EXPENSE; import static hanglog.integration.IntegrationFixture.MEMBER; -import hanglog.community.domain.type.PublishedStatusType; -import hanglog.share.domain.type.SharedStatusType; import hanglog.trip.domain.DayLog; import hanglog.trip.domain.Item; import hanglog.trip.domain.Trip; import hanglog.trip.domain.type.ItemType; +import hanglog.trip.domain.type.PublishedStatusType; +import hanglog.trip.domain.type.SharedStatusType; import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; @@ -91,12 +91,12 @@ public class TripExpenseFixture { private static void addDayLogsToTrip(final Trip trip, final List dayLogs) { dayLogs.stream() .filter(dayLog -> !trip.getDayLogs().contains(dayLog)) - .forEachOrdered(dayLog -> trip.getDayLogs().add(dayLog)); + .forEachOrdered(trip::addDayLog); } private static void addItemsToDayLog(final DayLog dayLog, final List items) { items.stream() .filter(item -> !dayLog.getItems().contains(item)) - .forEachOrdered(item -> dayLog.getItems().add(item)); + .forEachOrdered(dayLog::addItem); } } diff --git a/backend/src/test/java/hanglog/expense/presentation/ExpenseControllerTest.java b/backend/src/test/java/hanglog/expense/presentation/ExpenseControllerTest.java deleted file mode 100644 index 8cec19f58..000000000 --- a/backend/src/test/java/hanglog/expense/presentation/ExpenseControllerTest.java +++ /dev/null @@ -1,241 +0,0 @@ -package hanglog.expense.presentation; - -import static hanglog.category.fixture.CategoryFixture.EXPENSE_CATEGORIES; -import static hanglog.expense.fixture.AmountFixture.AMOUNT_20000; -import static hanglog.expense.fixture.CurrencyFixture.DEFAULT_CURRENCY; -import static hanglog.global.restdocs.RestDocsConfiguration.field; -import static hanglog.trip.fixture.CityFixture.LONDON; -import static hanglog.trip.fixture.CityFixture.TOKYO; -import static hanglog.trip.fixture.DayLogFixture.EXPENSE_LONDON_DAYLOG; -import static hanglog.trip.fixture.TripFixture.LONDON_TO_JAPAN; -import static hanglog.trip.fixture.TripFixture.LONDON_TRIP; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.when; -import static org.springframework.http.HttpHeaders.AUTHORIZATION; -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import hanglog.auth.domain.MemberTokens; -import hanglog.expense.domain.CategoryExpense; -import hanglog.expense.domain.DayLogExpense; -import hanglog.expense.dto.response.TripExpenseResponse; -import hanglog.expense.service.ExpenseService; -import hanglog.global.ControllerTest; -import hanglog.trip.domain.TripCity; -import hanglog.trip.service.TripService; -import jakarta.servlet.http.Cookie; -import java.util.List; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; -import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.test.web.servlet.ResultActions; - -@WebMvcTest(ExpenseController.class) -@MockBean(JpaMetamodelMappingContext.class) -class ExpenseControllerTest extends ControllerTest { - - private static final MemberTokens MEMBER_TOKENS = new MemberTokens("refreshToken", "accessToken"); - private static final Cookie COOKIE = new Cookie("refresh-token", MEMBER_TOKENS.getRefreshToken()); - - @MockBean - private ExpenseService expenseService; - - @MockBean - private TripService tripService; - - private ResultActions performGetRequest(final int tripId) throws Exception { - return mockMvc.perform( - get("/trips/{tripId}/expense", tripId) - .header(AUTHORIZATION, MEMBER_TOKENS.getAccessToken()) - .cookie(COOKIE) - .contentType(APPLICATION_JSON)); - } - - @DisplayName("모든 경비를 가져온다.") - @Test - void getExpenses() throws Exception { - // given - given(refreshTokenRepository.existsByToken(any())).willReturn(true); - doNothing().when(jwtProvider).validateTokens(any()); - given(jwtProvider.getSubject(any())).willReturn("1"); - doNothing().when(tripService).validateTripByMember(anyLong(), anyLong()); - - final TripExpenseResponse tripExpenseResponse = TripExpenseResponse.of( - LONDON_TO_JAPAN, - AMOUNT_20000, - List.of(new TripCity(LONDON_TRIP, LONDON), new TripCity(LONDON_TRIP, TOKYO)), - List.of(new CategoryExpense(EXPENSE_CATEGORIES.get(1), AMOUNT_20000, AMOUNT_20000)), - DEFAULT_CURRENCY, - List.of(new DayLogExpense(EXPENSE_LONDON_DAYLOG, AMOUNT_20000)) - ); - - when(expenseService.getAllExpenses(1L)).thenReturn(tripExpenseResponse); - - // when - final ResultActions resultActions = performGetRequest(1); - - // then - resultActions.andDo( - restDocs.document( - pathParameters( - parameterWithName("tripId") - .description("여행 ID") - ), - responseFields( - fieldWithPath("id") - .type(JsonFieldType.NUMBER) - .description("여행 ID") - .attributes(field("constraint", "양의 정수")), - fieldWithPath("title") - .type(JsonFieldType.STRING) - .description("여행 제목") - .attributes(field("constraint", "50자 이하의 문자열")), - fieldWithPath("startDate") - .type(JsonFieldType.STRING) - .description("여행 시작 날짜") - .attributes(field("constraint", "yyyy-MM-dd")), - fieldWithPath("endDate") - .type(JsonFieldType.STRING) - .description("여행 종료 날짜") - .attributes(field("constraint", "yyyy-MM-dd")), - fieldWithPath("cities") - .type(JsonFieldType.ARRAY) - .description("도시 목록") - .attributes(field("constraint", "1개 이상의 city")), - fieldWithPath("cities[].id") - .type(JsonFieldType.NUMBER) - .description("도시 ID") - .attributes(field("constraint", "양의 정수")), - fieldWithPath("cities[].name") - .type(JsonFieldType.STRING) - .description("도시 명") - .attributes(field("constraint", "20자 이하의 문자열")), - fieldWithPath("totalAmount") - .type(JsonFieldType.NUMBER) - .description("총 경비") - .attributes(field("constraint", "양의 정수")), - fieldWithPath("categories") - .type(JsonFieldType.ARRAY) - .description("카테고리별 경비 목록") - .attributes(field("constraint", "카테고리 배열")), - fieldWithPath("categories[].category") - .type(JsonFieldType.OBJECT) - .description("카테고리") - .attributes(field("constraint", "카테고리")), - fieldWithPath("categories[].category.id") - .type(JsonFieldType.NUMBER) - .description("카테고리 ID") - .attributes(field("constraint", "양의 정수")), - fieldWithPath("categories[].category.name") - .type(JsonFieldType.STRING) - .description("카테고리 명") - .attributes(field("constraint", "2자의 문자열")), - fieldWithPath("categories[].amount") - .type(JsonFieldType.NUMBER) - .description("카테고리 경비") - .attributes(field("constraint", "양의 정수")), - fieldWithPath("categories[].percentage") - .type(JsonFieldType.NUMBER) - .description("카테고리 백분율") - .attributes(field("constraint", "100이하의 백분율")), - fieldWithPath("exchangeRate") - .type(JsonFieldType.OBJECT) - .description("적용된 환율") - .attributes(field("constraint", "적용된 환율")), - fieldWithPath("exchangeRate.date") - .type(JsonFieldType.STRING) - .description("환율 날짜") - .attributes(field("constraint", "yyyy-MM-dd")), - fieldWithPath("exchangeRate.currencyRates") - .type(JsonFieldType.ARRAY) - .description("통화별 환율") - .attributes(field("constraint", "currency")), - fieldWithPath("exchangeRate.currencyRates[].currency") - .type(JsonFieldType.STRING) - .description("통화") - .attributes(field("constraint", "3자의 문자열")), - fieldWithPath("exchangeRate.currencyRates[].rate") - .type(JsonFieldType.NUMBER) - .description("환율") - .attributes(field("constraint", "양의 유리수")), - fieldWithPath("dayLogs") - .type(JsonFieldType.ARRAY) - .description("날짜별 여행 기록 배열") - .attributes(field("constraint", "2개 이상의 데이 로그")), - fieldWithPath("dayLogs[].id") - .type(JsonFieldType.NUMBER) - .description("날짜별 기록 ID") - .attributes(field("constraint", "양의 정수")), - fieldWithPath("dayLogs[].ordinal") - .type(JsonFieldType.NUMBER) - .description("여행에서의 날짜 순서") - .attributes(field("constraint", "양의 정수")), - fieldWithPath("dayLogs[].date") - .type(JsonFieldType.STRING) - .description("실제 날짜") - .attributes(field("constraint", "yyyy-MM-dd")), - fieldWithPath("dayLogs[].totalAmount") - .type(JsonFieldType.NUMBER) - .description("날짜별 총 경비") - .attributes(field("constraint", "양의 정수")), - fieldWithPath("dayLogs[].items") - .type(JsonFieldType.ARRAY) - .description("아이템 목록") - .attributes(field("constraint", "배열")), - fieldWithPath("dayLogs[].items[].id") - .type(JsonFieldType.NUMBER) - .description("아이템 ID") - .attributes(field("constraint", "양의 정수")), - fieldWithPath("dayLogs[].items[].title") - .type(JsonFieldType.STRING) - .description("아이템 제목") - .attributes(field("constraint", "50자 이하의 문자열")), - fieldWithPath("dayLogs[].items[].ordinal") - .type(JsonFieldType.NUMBER) - .description("아이템의 배치 순서") - .attributes(field("constraint", "양의 정수")), - fieldWithPath("dayLogs[].items[].expense") - .type(JsonFieldType.OBJECT) - .description("아이템 경비") - .attributes(field("constraint", "아이템 경비")), - fieldWithPath("dayLogs[].items[].expense.id") - .type(JsonFieldType.NUMBER) - .description("경비 ID") - .attributes(field("constraint", "양의 정수")), - fieldWithPath("dayLogs[].items[].expense.currency") - .type(JsonFieldType.STRING) - .description("경비 통화") - .attributes(field("constraint", "3자의 문자열")), - fieldWithPath("dayLogs[].items[].expense.amount") - .type(JsonFieldType.NUMBER) - .description("경비") - .attributes(field("constraint", "양의 유리수")), - fieldWithPath("dayLogs[].items[].expense.category") - .type(JsonFieldType.OBJECT) - .description("경비 카테고리") - .attributes(field("constraint", "id와 이름")), - fieldWithPath("dayLogs[].items[].expense.category.id") - .type(JsonFieldType.NUMBER) - .description("카테고리 ID") - .attributes(field("constraint", "양의 정수")), - fieldWithPath("dayLogs[].items[].expense.category.name") - .type(JsonFieldType.STRING) - .description("카테고리 명") - .attributes(field("constraint", "2자 문자열")) - ) - ) - ) - .andExpect(status().isOk()); - } -} diff --git a/backend/src/test/java/hanglog/expense/service/ExpenseServiceTest.java b/backend/src/test/java/hanglog/expense/service/LedgerServiceTest.java similarity index 85% rename from backend/src/test/java/hanglog/expense/service/ExpenseServiceTest.java rename to backend/src/test/java/hanglog/expense/service/LedgerServiceTest.java index 7fc600161..841181bd5 100644 --- a/backend/src/test/java/hanglog/expense/service/ExpenseServiceTest.java +++ b/backend/src/test/java/hanglog/expense/service/LedgerServiceTest.java @@ -17,18 +17,20 @@ import static hanglog.trip.fixture.TripFixture.LONDON_TRIP; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; import hanglog.category.domain.repository.CategoryRepository; +import hanglog.city.domain.City; +import hanglog.city.domain.repository.CityRepository; import hanglog.currency.domain.repository.CurrencyRepository; import hanglog.expense.domain.Amount; import hanglog.expense.domain.CategoryExpense; -import hanglog.expense.domain.DayLogExpense; -import hanglog.expense.dto.response.TripExpenseResponse; import hanglog.expense.fixture.ExchangeableExpenseFixture.ExchangeableExpense; -import hanglog.trip.domain.TripCity; -import hanglog.trip.domain.repository.TripCityRepository; +import hanglog.trip.domain.DayLogExpense; import hanglog.trip.domain.repository.TripRepository; +import hanglog.trip.dto.response.LedgerResponse; +import hanglog.trip.service.LedgerService; import java.math.BigDecimal; import java.util.Arrays; import java.util.List; @@ -44,10 +46,10 @@ @ExtendWith(MockitoExtension.class) @Transactional -class ExpenseServiceTest { +class LedgerServiceTest { @InjectMocks - private ExpenseService expenseService; + private LedgerService ledgerService; @Mock private TripRepository tripRepository; @@ -56,7 +58,7 @@ class ExpenseServiceTest { private CurrencyRepository currencyRepository; @Mock - private TripCityRepository tripCityRepository; + private CityRepository cityRepository; @Mock private CategoryRepository categoryRepository; @@ -65,17 +67,14 @@ class ExpenseServiceTest { @Test void getAllExpenses() { // given - final List tripCities = List.of( - new TripCity(TRIP_FOR_EXPENSE, LONDON), - new TripCity(TRIP_FOR_EXPENSE, TOKYO) - ); + final List cities = List.of(LONDON, TOKYO); when(tripRepository.findById(1L)) .thenReturn(Optional.of(TRIP_FOR_EXPENSE)); - when(currencyRepository.findTopByOrderByDateAsc()) + lenient().when(currencyRepository.findTopByOrderByDateAsc()) .thenReturn(Optional.of(DEFAULT_CURRENCY)); - when(tripCityRepository.findByTripId(1L)) - .thenReturn(tripCities); - when(categoryRepository.findExpenseCategory()) + when(cityRepository.findCitiesByTripId(1L)) + .thenReturn(cities); + lenient().when(categoryRepository.findExpenseCategory()) .thenReturn(EXPENSE_CATEGORIES); final Amount day1Amount = getTotalAmount(Arrays.asList(KRW_100_FOOD, EUR_100_SHOPPING)); @@ -86,10 +85,10 @@ void getAllExpenses() { USD_100_ACCOMMODATION )); - final TripExpenseResponse expected = TripExpenseResponse.of( + final LedgerResponse expected = LedgerResponse.of( TRIP_FOR_EXPENSE, totalAmount, - tripCities, + cities, List.of( new CategoryExpense( SHOPPING, @@ -118,7 +117,7 @@ void getAllExpenses() { .toList(); // when - final TripExpenseResponse actual = expenseService.getAllExpenses(1L); + final LedgerResponse actual = ledgerService.getAllExpenses(1L); final List actualCategories = actual.getCategories().stream() .filter(categoryExpenseResponse -> !categoryExpenseResponse.getAmount().equals(BigDecimal.ZERO)) .map(categoryExpenseResponse -> categoryExpenseResponse.getCategory().getName()) @@ -127,6 +126,7 @@ void getAllExpenses() { assertSoftly(softly -> { softly.assertThat(actual) .usingRecursiveComparison() + .ignoringCollectionOrder() .ignoringFields("categories") .isEqualTo(expected); softly.assertThat(actual.getCategories()) @@ -152,13 +152,13 @@ void getNoExpenseTrip() { .thenReturn(Optional.of(LONDON_TRIP)); when(currencyRepository.findTopByOrderByDateAsc()) .thenReturn(Optional.of(DEFAULT_CURRENCY)); - when(tripCityRepository.findByTripId(1L)) + when(cityRepository.findCitiesByTripId(1L)) .thenReturn(List.of()); when(categoryRepository.findExpenseCategory()) .thenReturn(EXPENSE_CATEGORIES); // when - final TripExpenseResponse actual = expenseService.getAllExpenses(1L); + final LedgerResponse actual = ledgerService.getAllExpenses(1L); // then assertThat(actual).extracting("categories").asList().hasSize(6); diff --git a/backend/src/test/java/hanglog/global/ControllerTest.java b/backend/src/test/java/hanglog/global/ControllerTest.java index 44c9594b4..9bde7335e 100644 --- a/backend/src/test/java/hanglog/global/ControllerTest.java +++ b/backend/src/test/java/hanglog/global/ControllerTest.java @@ -2,11 +2,11 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -import hanglog.auth.AuthArgumentResolver; -import hanglog.auth.domain.BearerAuthorizationExtractor; -import hanglog.auth.domain.JwtProvider; -import hanglog.auth.domain.repository.RefreshTokenRepository; import hanglog.global.restdocs.RestDocsConfiguration; +import hanglog.login.LoginArgumentResolver; +import hanglog.login.domain.repository.RefreshTokenRepository; +import hanglog.login.infrastructure.BearerAuthorizationExtractor; +import hanglog.login.infrastructure.JwtProvider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -31,7 +31,7 @@ public abstract class ControllerTest { protected MockMvc mockMvc; @Autowired - protected AuthArgumentResolver authArgumentResolver; + protected LoginArgumentResolver loginArgumentResolver; @MockBean protected JwtProvider jwtProvider; diff --git a/backend/src/test/java/hanglog/global/config/EventListenerTestConfig.java b/backend/src/test/java/hanglog/global/config/EventListenerTestConfig.java new file mode 100644 index 000000000..d7c929573 --- /dev/null +++ b/backend/src/test/java/hanglog/global/config/EventListenerTestConfig.java @@ -0,0 +1,15 @@ +package hanglog.global.config; + +import hanglog.community.domain.repository.PublishedTripRepository; +import hanglog.listener.PublishEventListener; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class EventListenerTestConfig { + + @Bean + public PublishEventListener publishEventListener(PublishedTripRepository publishedTripRepository) { + return new PublishEventListener(publishedTripRepository); + } +} diff --git a/backend/src/test/java/hanglog/image/domain/ImageFileTest.java b/backend/src/test/java/hanglog/image/domain/ImageFileTest.java index 364f67d30..fddd811d3 100644 --- a/backend/src/test/java/hanglog/image/domain/ImageFileTest.java +++ b/backend/src/test/java/hanglog/image/domain/ImageFileTest.java @@ -4,9 +4,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import hanglog.global.exception.ImageException; -import java.io.FileInputStream; import java.io.IOException; -import java.nio.file.Path; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockMultipartFile; diff --git a/backend/src/test/java/hanglog/image/util/ImageUrlConverterTest.java b/backend/src/test/java/hanglog/image/util/ImageUrlConverterTest.java deleted file mode 100644 index 8ca4cb319..000000000 --- a/backend/src/test/java/hanglog/image/util/ImageUrlConverterTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package hanglog.image.util; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import hanglog.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 ImageUrlConverterTest { - - @DisplayName("URL로 이미지의 이름을 파싱한다.") - @Test - void convertUrlToName() { - // given - final String url = "https://hanglog.com/img/test.png"; - final String expected = "test.png"; - - // when & then - assertThat(ImageUrlConverter.convertUrlToName(url)).isEqualTo(expected); - } - - @DisplayName("URL의 형식이 잘못된 경우 예외가 발생한다.") - @ParameterizedTest() - @ValueSource(strings = { - "", - "test.png", - "invalid/https://hanglog.com/img/test.png", - "https://hanglog.com/img/https://hanglog.com/img/test.png" - }) - void convertUrlToName(final String url) { - // when & then - assertThatThrownBy(() -> ImageUrlConverter.convertUrlToName(url)) - .isInstanceOf(BadRequestException.class) - .extracting("code") - .isEqualTo(5005); - } - - - @DisplayName("이미지의 이름으로 URL을 생성한다.") - @Test - void convertNameToUrl() { - // given - final String name = "test.png"; - final String expected = "https://hanglog.com/img/test.png"; - - // when & then - assertThat(ImageUrlConverter.convertNameToUrl(name)).isEqualTo(expected); - } -} diff --git a/backend/src/test/java/hanglog/integration/IntegrationFixture.java b/backend/src/test/java/hanglog/integration/IntegrationFixture.java index 7977c1f04..f211ffa43 100644 --- a/backend/src/test/java/hanglog/integration/IntegrationFixture.java +++ b/backend/src/test/java/hanglog/integration/IntegrationFixture.java @@ -6,9 +6,9 @@ import hanglog.city.domain.City; import hanglog.expense.domain.Amount; import hanglog.expense.domain.Expense; -import hanglog.image.domain.Image; import hanglog.member.domain.Member; import hanglog.trip.domain.DayLog; +import hanglog.trip.domain.Image; import hanglog.trip.domain.Item; import hanglog.trip.domain.Place; import hanglog.trip.domain.Trip; @@ -147,4 +147,14 @@ public class IntegrationFixture { new Expense("gbp", new Amount(0.0), CULTURE_CATEGORY), List.of(DEFAULT_IMAGE) ); + + static { + addDayLogsToTrip(LAHGON_TRIP, List.of(DAY_LOG_1)); + } + + private static void addDayLogsToTrip(final Trip trip, final List dayLogs) { + dayLogs.stream() + .filter(dayLog -> !trip.getDayLogs().contains(dayLog)) + .forEachOrdered(trip::addDayLog); + } } diff --git a/backend/src/test/java/hanglog/integration/controller/IntegrationTest.java b/backend/src/test/java/hanglog/integration/controller/IntegrationTest.java index 24088c170..29bb7d350 100644 --- a/backend/src/test/java/hanglog/integration/controller/IntegrationTest.java +++ b/backend/src/test/java/hanglog/integration/controller/IntegrationTest.java @@ -1,9 +1,9 @@ package hanglog.integration.controller; -import hanglog.auth.domain.JwtProvider; -import hanglog.auth.domain.MemberTokens; -import hanglog.auth.domain.RefreshToken; -import hanglog.auth.domain.repository.RefreshTokenRepository; +import hanglog.login.domain.MemberTokens; +import hanglog.login.domain.RefreshToken; +import hanglog.login.domain.repository.RefreshTokenRepository; +import hanglog.login.infrastructure.JwtProvider; import hanglog.member.domain.Member; import hanglog.member.domain.repository.MemberRepository; import io.restassured.RestAssured; diff --git a/backend/src/test/java/hanglog/integration/controller/ItemIntegrationTest.java b/backend/src/test/java/hanglog/integration/controller/ItemIntegrationTest.java index d39d021d6..31af4e10b 100644 --- a/backend/src/test/java/hanglog/integration/controller/ItemIntegrationTest.java +++ b/backend/src/test/java/hanglog/integration/controller/ItemIntegrationTest.java @@ -11,13 +11,13 @@ import static org.springframework.http.HttpStatus.CREATED; import static org.springframework.http.HttpStatus.NO_CONTENT; -import hanglog.auth.domain.MemberTokens; import hanglog.category.domain.Category; import hanglog.category.domain.repository.CategoryRepository; import hanglog.category.dto.CategoryResponse; import hanglog.currency.domain.type.CurrencyType; import hanglog.expense.dto.response.ItemExpenseResponse; import hanglog.global.exception.BadRequestException; +import hanglog.login.domain.MemberTokens; import hanglog.trip.dto.request.ExpenseRequest; import hanglog.trip.dto.request.ItemRequest; import hanglog.trip.dto.request.ItemUpdateRequest; @@ -40,7 +40,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -public class ItemIntegrationTest extends IntegrationTest { +class ItemIntegrationTest extends IntegrationTest { @Autowired private CategoryRepository categoryRepository; @@ -83,7 +83,7 @@ void createItem_NonSpot() { 1, itemRequest.getRating(), itemRequest.getMemo(), - itemRequest.getImageUrls(), + itemRequest.getImageNames(), null, null ); @@ -138,7 +138,7 @@ void createItem_Spot() { 1, itemRequest.getRating(), itemRequest.getMemo(), - itemRequest.getImageUrls(), + itemRequest.getImageNames(), expectedPlaceResponse, expectedItemExpenseResponse ); @@ -156,9 +156,9 @@ void createItem_Spot() { ); } - @DisplayName("NonSpot인 아이템을 Spot으로 수정한다.") + @DisplayName("Spot인 아이템을 NonSpot으로 수정한다.") @Test - void updateItem_NonSpotToSpot() { + void updateItem_SpotToNonSpot() { // when final ItemRequest itemRequest = getSpotItemRequest(); final ExtractableResponse createResponse = requestCreateItem(memberTokens, tripId, itemRequest); @@ -215,7 +215,7 @@ void updateItem_changePlace() { 4.5, "updated memo", dayLogId, - List.of("https://hanglog.com/img/test1.png", "https://hanglog.com/img/test2.png"), + List.of("test1.png", "test2.png"), true, updatedPlaceRequest, getExpenseRequest() @@ -239,9 +239,9 @@ void updateItem_changePlace() { ); } - @DisplayName("Spot에서 NonSpot으로 수정한다.") + @DisplayName("NonSpot에서 Spot으로 수정한다.") @Test - void updateItem_SpotToNonSpot() { + void updateItem_NonSpotToSpot() { // when final ItemRequest itemRequest = getNonSpotItemRequest(); final ExtractableResponse createResponse = requestCreateItem(memberTokens, tripId, itemRequest); @@ -253,8 +253,8 @@ void updateItem_SpotToNonSpot() { 4.5, "updated memo", dayLogId, - List.of("https://hanglog.com/img/test1.png", "https://hanglog.com/img/test2.png"), - false, + List.of("test1.png", "test2.png"), + true, getPlaceRequest(), getExpenseRequest() ); @@ -402,7 +402,7 @@ private ItemRequest getSpotItemRequest() { 5.0, "memo", dayLogId, - List.of("https://hanglog.com/img/test1.png", "https://hanglog.com/img/test2.png"), + List.of("test1.png", "test2.png"), getPlaceRequest(), getExpenseRequest() ); @@ -455,7 +455,7 @@ private ItemResponse createMockIdResponseBy(final Integer ordinal, final ItemUpd ordinal, itemUpdateRequest.getRating(), itemUpdateRequest.getMemo(), - itemUpdateRequest.getImageUrls(), + itemUpdateRequest.getImageNames(), createMockIdPlaceResponseBy(itemUpdateRequest.getPlace()), createMockIdExpenseResponseBy(itemUpdateRequest.getExpense()) ); diff --git a/backend/src/test/java/hanglog/integration/controller/SharedTripIntegrationTest.java b/backend/src/test/java/hanglog/integration/controller/SharedTripIntegrationTest.java index 0d7f643f7..396e6e455 100644 --- a/backend/src/test/java/hanglog/integration/controller/SharedTripIntegrationTest.java +++ b/backend/src/test/java/hanglog/integration/controller/SharedTripIntegrationTest.java @@ -9,8 +9,8 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.springframework.http.HttpHeaders.AUTHORIZATION; -import hanglog.auth.domain.MemberTokens; -import hanglog.share.dto.request.SharedTripStatusRequest; +import hanglog.login.domain.MemberTokens; +import hanglog.trip.dto.request.SharedStatusRequest; import hanglog.trip.dto.request.TripCreateRequest; import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; @@ -39,7 +39,7 @@ private ExtractableResponse requestUpdateSharedTripStatus(final boolea .header(HttpHeaders.AUTHORIZATION, "Bearer " + memberTokens.getAccessToken()) .cookies("refresh-token", memberTokens.getRefreshToken()) - .body(new SharedTripStatusRequest(status)) + .body(new SharedStatusRequest(status)) .contentType(JSON) .when().patch("/trips/{tripId}/share", tripId) .then().log().all() diff --git a/backend/src/test/java/hanglog/integration/controller/TripIntegrationTest.java b/backend/src/test/java/hanglog/integration/controller/TripIntegrationTest.java index c692bb1fc..233260e14 100644 --- a/backend/src/test/java/hanglog/integration/controller/TripIntegrationTest.java +++ b/backend/src/test/java/hanglog/integration/controller/TripIntegrationTest.java @@ -8,7 +8,7 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.springframework.http.HttpHeaders.AUTHORIZATION; -import hanglog.auth.domain.MemberTokens; +import hanglog.login.domain.MemberTokens; import hanglog.trip.dto.request.TripCreateRequest; import hanglog.trip.dto.request.TripUpdateRequest; import hanglog.trip.dto.response.TripDetailResponse; diff --git a/backend/src/test/java/hanglog/integration/service/AuthServiceIntegrationTest.java b/backend/src/test/java/hanglog/integration/service/AuthServiceIntegrationTest.java deleted file mode 100644 index 7553fd9e9..000000000 --- a/backend/src/test/java/hanglog/integration/service/AuthServiceIntegrationTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package hanglog.integration.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; - -import hanglog.auth.domain.BearerAuthorizationExtractor; -import hanglog.auth.domain.JwtProvider; -import hanglog.auth.domain.oauthprovider.OauthProviders; -import hanglog.auth.domain.repository.RefreshTokenRepository; -import hanglog.auth.service.AuthService; -import hanglog.trip.domain.repository.TripRepository; -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; - -@Import({ - AuthService.class, - OauthProviders.class, - JwtProvider.class, - BearerAuthorizationExtractor.class -}) -class AuthServiceIntegrationTest extends ServiceIntegrationTest { - - @Autowired - private OauthProviders oauthProviders; - @Autowired - private RefreshTokenRepository refreshTokenRepository; - @Autowired - private TripRepository tripRepository; - @Autowired - private JwtProvider jwtProvider; - @Autowired - private BearerAuthorizationExtractor bearerExtractor; - @Autowired - private AuthService authService; - - @DisplayName("멤버를 삭제한다.") - @Test - void deleteAccount() { - // when & then - assertThat(memberRepository.findById(member.getId()).isPresent()).isTrue(); - assertDoesNotThrow(() -> authService.deleteAccount(member.getId())); - assertThat(memberRepository.findById(member.getId()).isEmpty()).isTrue(); - } -} diff --git a/backend/src/test/java/hanglog/integration/service/CommunityServiceIntegrationTest.java b/backend/src/test/java/hanglog/integration/service/CommunityServiceIntegrationTest.java index a5c34dfba..70fbc81d6 100644 --- a/backend/src/test/java/hanglog/integration/service/CommunityServiceIntegrationTest.java +++ b/backend/src/test/java/hanglog/integration/service/CommunityServiceIntegrationTest.java @@ -8,10 +8,13 @@ import hanglog.community.dto.response.CommunityTripListResponse; import hanglog.community.dto.response.CommunityTripResponse; import hanglog.community.service.CommunityService; -import hanglog.expense.service.ExpenseService; +import hanglog.global.config.EventListenerTestConfig; import hanglog.trip.dto.request.PublishedStatusRequest; import hanglog.trip.dto.request.TripCreateRequest; import hanglog.trip.dto.response.TripDetailResponse; +import hanglog.trip.infrastructure.CustomDayLogRepositoryImpl; +import hanglog.trip.infrastructure.CustomTripCityRepositoryImpl; +import hanglog.trip.service.LedgerService; import hanglog.trip.service.TripService; import java.time.LocalDate; import java.util.List; @@ -26,13 +29,17 @@ @Import({ TripService.class, CommunityService.class, - ExpenseService.class, - RecommendStrategies.class + LedgerService.class, + RecommendStrategies.class, + CustomDayLogRepositoryImpl.class, + CustomTripCityRepositoryImpl.class, + EventListenerTestConfig.class }) -public class CommunityServiceIntegrationTest extends ServiceIntegrationTest { +class CommunityServiceIntegrationTest extends ServiceIntegrationTest { @Autowired private TripService tripService; + @Autowired private CommunityService communityService; @@ -51,7 +58,10 @@ void setTrips() { void getTripsByPage() { // when final Pageable pageable = PageRequest.of(1, 10, DESC, "publishedTrip.id"); - final CommunityTripListResponse response = communityService.getTripsByPage(Accessor.member(1L), pageable); + final CommunityTripListResponse response = communityService.getCommunityTripsByPage( + Accessor.member(1L), + pageable + ); final List tripResponses = response.getTrips(); // then diff --git a/backend/src/test/java/hanglog/integration/service/LikeServiceIntegrationTest.java b/backend/src/test/java/hanglog/integration/service/LikeServiceIntegrationTest.java index e8754efc9..1d5b71f59 100644 --- a/backend/src/test/java/hanglog/integration/service/LikeServiceIntegrationTest.java +++ b/backend/src/test/java/hanglog/integration/service/LikeServiceIntegrationTest.java @@ -3,9 +3,11 @@ import static hanglog.integration.IntegrationFixture.TRIP_CREATE_REQUEST; import static org.assertj.core.api.SoftAssertions.assertSoftly; -import hanglog.community.domain.repository.LikeRepository; -import hanglog.community.dto.request.LikeRequest; -import hanglog.community.service.LikeService; +import hanglog.like.dto.request.LikeRequest; +import hanglog.like.repository.LikeRepository; +import hanglog.like.service.LikeService; +import hanglog.trip.infrastructure.CustomDayLogRepositoryImpl; +import hanglog.trip.infrastructure.CustomTripCityRepositoryImpl; import hanglog.trip.service.TripService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -14,7 +16,9 @@ @Import({ TripService.class, - LikeService.class + LikeService.class, + CustomDayLogRepositoryImpl.class, + CustomTripCityRepositoryImpl.class }) class LikeServiceIntegrationTest extends ServiceIntegrationTest { diff --git a/backend/src/test/java/hanglog/integration/service/LoginServiceIntegrationTest.java b/backend/src/test/java/hanglog/integration/service/LoginServiceIntegrationTest.java new file mode 100644 index 000000000..d2a773bea --- /dev/null +++ b/backend/src/test/java/hanglog/integration/service/LoginServiceIntegrationTest.java @@ -0,0 +1,66 @@ +package hanglog.integration.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import hanglog.community.domain.repository.PublishedTripRepository; +import hanglog.login.domain.OauthProviders; +import hanglog.login.domain.repository.RefreshTokenRepository; +import hanglog.login.infrastructure.BearerAuthorizationExtractor; +import hanglog.login.infrastructure.JwtProvider; +import hanglog.login.service.LoginService; +import hanglog.member.domain.repository.MemberRepository; +import hanglog.trip.domain.repository.SharedTripRepository; +import hanglog.trip.domain.repository.TripRepository; +import hanglog.trip.infrastructure.CustomTripRepositoryImpl; +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Import; + +@Import({ + LoginService.class, + OauthProviders.class, + JwtProvider.class, + BearerAuthorizationExtractor.class, + CustomTripRepositoryImpl.class +}) +class LoginServiceIntegrationTest extends ServiceIntegrationTest { + + @Autowired + private OauthProviders oauthProviders; + @Autowired + private RefreshTokenRepository refreshTokenRepository; + @Autowired + private TripRepository tripRepository; + @Autowired + private JwtProvider jwtProvider; + @Autowired + private BearerAuthorizationExtractor bearerExtractor; + @Autowired + private LoginService loginService; + @Autowired + private PublishedTripRepository publishedTripRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired + private SharedTripRepository sharedTripRepository; + @Autowired + private ApplicationEventPublisher publisher; + @Autowired + private EntityManager entityManager; + + @DisplayName("멤버를 삭제한다.") + @Test + void deleteAccount() { + // when & then + assertThat(memberRepository.findById(member.getId())).isPresent(); + assertDoesNotThrow(() -> loginService.deleteAccount(member.getId())); + entityManager.flush(); + entityManager.clear(); + + assertThat(memberRepository.findById(member.getId())).isEmpty(); + } +} diff --git a/backend/src/test/java/hanglog/integration/service/TripServiceIntegrationTest.java b/backend/src/test/java/hanglog/integration/service/TripServiceIntegrationTest.java index d34e1ea00..0e4073a90 100644 --- a/backend/src/test/java/hanglog/integration/service/TripServiceIntegrationTest.java +++ b/backend/src/test/java/hanglog/integration/service/TripServiceIntegrationTest.java @@ -23,7 +23,10 @@ import hanglog.trip.dto.request.TripUpdateRequest; import hanglog.trip.dto.response.TripDetailResponse; import hanglog.trip.dto.response.TripResponse; +import hanglog.trip.infrastructure.CustomDayLogRepositoryImpl; +import hanglog.trip.infrastructure.CustomTripCityRepositoryImpl; import hanglog.trip.service.TripService; +import jakarta.persistence.EntityManager; import java.time.LocalDate; import java.util.Arrays; import java.util.List; @@ -33,7 +36,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; -@Import(TripService.class) +@Import({ + TripService.class, + CustomTripCityRepositoryImpl.class, + CustomDayLogRepositoryImpl.class +}) class TripServiceIntegrationTest extends ServiceIntegrationTest { @Autowired @@ -48,6 +55,9 @@ class TripServiceIntegrationTest extends ServiceIntegrationTest { @Autowired private TripService tripService; + @Autowired + private EntityManager entityManager; + @BeforeEach void setUp() { tripRepository.deleteAll(); @@ -170,6 +180,8 @@ void update() { void update_IncreaseDayLogs() { // given final Long tripId = tripService.save(member.getId(), TRIP_CREATE_REQUEST); + entityManager.flush(); + entityManager.clear(); final String updatedTitle = "수정된 여행 제목"; final String updatedDescription = "매번 색다르고 즐거운 서유럽 여행"; @@ -211,6 +223,8 @@ void update_IncreaseDayLogs() { void update_DecreaseDayLogs() { // given final Long tripId = tripService.save(member.getId(), TRIP_CREATE_REQUEST); + entityManager.flush(); + entityManager.clear(); final String updatedTitle = "수정된 여행 제목"; final String updatedDescription = "매번 색다르고 즐거운 서유럽 여행"; diff --git a/backend/src/test/java/hanglog/auth/domain/JwtProviderTest.java b/backend/src/test/java/hanglog/login/infrastructure/JwtProviderTest.java similarity index 99% rename from backend/src/test/java/hanglog/auth/domain/JwtProviderTest.java rename to backend/src/test/java/hanglog/login/infrastructure/JwtProviderTest.java index f8947d845..04f1c4467 100644 --- a/backend/src/test/java/hanglog/auth/domain/JwtProviderTest.java +++ b/backend/src/test/java/hanglog/login/infrastructure/JwtProviderTest.java @@ -1,4 +1,4 @@ -package hanglog.auth.domain; +package hanglog.login.infrastructure; import static hanglog.global.exception.ExceptionCode.EXPIRED_PERIOD_ACCESS_TOKEN; import static hanglog.global.exception.ExceptionCode.EXPIRED_PERIOD_REFRESH_TOKEN; @@ -11,6 +11,7 @@ import hanglog.global.exception.AuthException; import hanglog.global.exception.ExpiredPeriodJwtException; import hanglog.global.exception.InvalidJwtException; +import hanglog.login.domain.MemberTokens; import io.jsonwebtoken.Header; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; diff --git a/backend/src/test/java/hanglog/auth/presentation/AuthControllerTest.java b/backend/src/test/java/hanglog/login/presentation/LoginControllerTest.java similarity index 92% rename from backend/src/test/java/hanglog/auth/presentation/AuthControllerTest.java rename to backend/src/test/java/hanglog/login/presentation/LoginControllerTest.java index 4afc098d6..16ccbb8c4 100644 --- a/backend/src/test/java/hanglog/auth/presentation/AuthControllerTest.java +++ b/backend/src/test/java/hanglog/login/presentation/LoginControllerTest.java @@ -1,4 +1,4 @@ -package hanglog.auth.presentation; +package hanglog.login.presentation; import static hanglog.global.restdocs.RestDocsConfiguration.field; import static org.assertj.core.api.Assertions.assertThat; @@ -23,11 +23,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import hanglog.auth.domain.MemberTokens; -import hanglog.auth.dto.AccessTokenResponse; -import hanglog.auth.dto.LoginRequest; -import hanglog.auth.service.AuthService; import hanglog.global.ControllerTest; +import hanglog.login.domain.MemberTokens; +import hanglog.login.dto.AccessTokenResponse; +import hanglog.login.dto.LoginRequest; +import hanglog.login.service.LoginService; import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -42,10 +42,10 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; -@WebMvcTest(AuthController.class) +@WebMvcTest(LoginController.class) @MockBean(JpaMetamodelMappingContext.class) @AutoConfigureRestDocs -class AuthControllerTest extends ControllerTest { +class LoginControllerTest extends ControllerTest { private final static String GOOGLE_PROVIDER = "google"; private final static String REFRESH_TOKEN = "refreshToken"; @@ -56,7 +56,7 @@ class AuthControllerTest extends ControllerTest { private ObjectMapper objectMapper; @MockBean - private AuthService authService; + private LoginService loginService; @DisplayName("로그인을 할 수 있다.") @Test @@ -65,7 +65,7 @@ void login() throws Exception { final LoginRequest loginRequest = new LoginRequest("code"); final MemberTokens memberTokens = new MemberTokens(REFRESH_TOKEN, ACCESS_TOKEN); - when(authService.login(anyString(), anyString())) + when(loginService.login(anyString(), anyString())) .thenReturn(memberTokens); final ResultActions resultActions = mockMvc.perform(post("/login/{provider}", GOOGLE_PROVIDER) @@ -113,7 +113,7 @@ void extendLogin() throws Exception { final MemberTokens memberTokens = new MemberTokens(REFRESH_TOKEN, RENEW_ACCESS_TOKEN); final Cookie cookie = new Cookie("refresh-token", memberTokens.getRefreshToken()); - when(authService.renewalAccessToken(REFRESH_TOKEN, ACCESS_TOKEN)) + when(loginService.renewalAccessToken(REFRESH_TOKEN, ACCESS_TOKEN)) .thenReturn(RENEW_ACCESS_TOKEN); // when @@ -161,7 +161,7 @@ void logout() throws Exception { given(refreshTokenRepository.existsByToken(any())).willReturn(true); doNothing().when(jwtProvider).validateTokens(any()); given(jwtProvider.getSubject(any())).willReturn("1"); - doNothing().when(authService).removeRefreshToken(anyString()); + doNothing().when(loginService).removeRefreshToken(anyString()); final MemberTokens memberTokens = new MemberTokens(REFRESH_TOKEN, RENEW_ACCESS_TOKEN); final Cookie cookie = new Cookie("refresh-token", memberTokens.getRefreshToken()); @@ -186,7 +186,7 @@ void logout() throws Exception { )); // then - verify(authService).removeRefreshToken(anyString()); + verify(loginService).removeRefreshToken(anyString()); } @@ -197,7 +197,7 @@ void deleteAccount() throws Exception { given(refreshTokenRepository.existsByToken(any())).willReturn(true); doNothing().when(jwtProvider).validateTokens(any()); given(jwtProvider.getSubject(any())).willReturn("1"); - doNothing().when(authService).deleteAccount(anyLong()); + doNothing().when(loginService).deleteAccount(anyLong()); final MemberTokens memberTokens = new MemberTokens(REFRESH_TOKEN, RENEW_ACCESS_TOKEN); final Cookie cookie = new Cookie("refresh-token", memberTokens.getRefreshToken()); @@ -222,6 +222,6 @@ void deleteAccount() throws Exception { )); // then - verify(authService).deleteAccount(anyLong()); + verify(loginService).deleteAccount(anyLong()); } } diff --git a/backend/src/test/java/hanglog/member/controller/MemberControllerTest.java b/backend/src/test/java/hanglog/member/controller/MemberControllerTest.java index 70517233c..53ea41a02 100644 --- a/backend/src/test/java/hanglog/member/controller/MemberControllerTest.java +++ b/backend/src/test/java/hanglog/member/controller/MemberControllerTest.java @@ -22,8 +22,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import hanglog.auth.domain.MemberTokens; import hanglog.global.ControllerTest; +import hanglog.login.domain.MemberTokens; import hanglog.member.dto.request.MyPageRequest; import hanglog.member.dto.response.MyPageResponse; import hanglog.member.presentation.MemberController; diff --git a/backend/src/test/java/hanglog/member/event/DeleteEventListenerTest.java b/backend/src/test/java/hanglog/member/event/DeleteEventListenerTest.java new file mode 100644 index 000000000..c2ad68496 --- /dev/null +++ b/backend/src/test/java/hanglog/member/event/DeleteEventListenerTest.java @@ -0,0 +1,117 @@ +package hanglog.member.event; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.anyList; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import hanglog.expense.domain.repository.ExpenseRepository; +import hanglog.listener.DeleteEventListener; +import hanglog.login.domain.repository.RefreshTokenRepository; +import hanglog.member.domain.MemberDeleteEvent; +import hanglog.trip.domain.TripDeleteEvent; +import hanglog.trip.domain.repository.CustomDayLogRepository; +import hanglog.trip.domain.repository.CustomItemRepository; +import hanglog.trip.domain.repository.DayLogRepository; +import hanglog.trip.domain.repository.ImageRepository; +import hanglog.trip.domain.repository.ItemRepository; +import hanglog.trip.domain.repository.PlaceRepository; +import hanglog.trip.domain.repository.TripCityRepository; +import hanglog.trip.domain.repository.TripRepository; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DeleteEventListenerTest { + + @Mock + private CustomDayLogRepository customDayLogRepository; + @Mock + private CustomItemRepository customItemRepository; + @Mock + private PlaceRepository placeRepository; + @Mock + private ExpenseRepository expenseRepository; + @Mock + private ImageRepository imageRepository; + @Mock + private ItemRepository itemRepository; + @Mock + private DayLogRepository dayLogRepository; + @Mock + private TripRepository tripRepository; + @Mock + private RefreshTokenRepository refreshTokenRepository; + @Mock + private TripCityRepository tripCityRepository; + @InjectMocks + private DeleteEventListener listener; + + @DisplayName("deleteMember 메서드에서 올바르게 레포지토리의 메서드를 호출한다.") + @Test + void deleteMember() { + // given + final MemberDeleteEvent event = new MemberDeleteEvent(List.of(1L, 2L, 3L), 1L); + + when(customDayLogRepository.findDayLogIdsByTripIds(event.getTripIds())).thenReturn(new ArrayList<>()); + when(customItemRepository.findItemIdsByDayLogIds(anyList())).thenReturn(new ArrayList<>()); + doNothing().when(placeRepository).deleteByIds(anyList()); + doNothing().when(expenseRepository).deleteByIds(anyList()); + doNothing().when(imageRepository).deleteByItemIds(anyList()); + doNothing().when(itemRepository).deleteByIds(anyList()); + doNothing().when(dayLogRepository).deleteByIds(anyList()); + doNothing().when(tripRepository).deleteByMemberId(anyLong()); + doNothing().when(refreshTokenRepository).deleteByMemberId(anyLong()); + + // when + listener.deleteMember(event); + + // then + verify(customDayLogRepository, times(1)).findDayLogIdsByTripIds(event.getTripIds()); + verify(customItemRepository, times(1)).findItemIdsByDayLogIds(anyList()); + verify(placeRepository, times(1)).deleteByIds(anyList()); + verify(expenseRepository, times(1)).deleteByIds(anyList()); + verify(imageRepository, times(1)).deleteByItemIds(anyList()); + verify(itemRepository, times(1)).deleteByIds(anyList()); + verify(dayLogRepository, times(1)).deleteByIds(anyList()); + verify(tripRepository, times(1)).deleteByMemberId(anyLong()); + verify(refreshTokenRepository, times(1)).deleteByMemberId(anyLong()); + } + + @DisplayName("deleteTrip 메서드에서 올바르게 레포지토리의 메서드를 호출한다.") + @Test + void deleteTrip() { + // given + final TripDeleteEvent event = new TripDeleteEvent(1L); + + when(customDayLogRepository.findDayLogIdsByTripId(event.getTripId())).thenReturn(new ArrayList<>()); + when(customItemRepository.findItemIdsByDayLogIds(anyList())).thenReturn(new ArrayList<>()); + doNothing().when(placeRepository).deleteByIds(anyList()); + doNothing().when(expenseRepository).deleteByIds(anyList()); + doNothing().when(imageRepository).deleteByItemIds(anyList()); + doNothing().when(itemRepository).deleteByIds(anyList()); + doNothing().when(dayLogRepository).deleteByIds(anyList()); + doNothing().when(tripCityRepository).deleteAllByTripId(anyLong()); + + // when + listener.deleteTrip(event); + + // then + verify(customDayLogRepository, times(1)).findDayLogIdsByTripId(event.getTripId()); + verify(customItemRepository, times(1)).findItemIdsByDayLogIds(anyList()); + verify(placeRepository, times(1)).deleteByIds(anyList()); + verify(expenseRepository, times(1)).deleteByIds(anyList()); + verify(imageRepository, times(1)).deleteByItemIds(anyList()); + verify(itemRepository, times(1)).deleteByIds(anyList()); + verify(dayLogRepository, times(1)).deleteByIds(anyList()); + verify(tripCityRepository, times(1)).deleteAllByTripId(anyLong()); + } +} diff --git a/backend/src/test/java/hanglog/share/service/SharedTripServiceTest.java b/backend/src/test/java/hanglog/share/service/SharedTripServiceTest.java deleted file mode 100644 index 704d7dca5..000000000 --- a/backend/src/test/java/hanglog/share/service/SharedTripServiceTest.java +++ /dev/null @@ -1,130 +0,0 @@ -package hanglog.share.service; - -import static hanglog.global.exception.ExceptionCode.INVALID_SHARE_CODE; -import static hanglog.global.exception.ExceptionCode.NOT_FOUND_SHARED_CODE; -import static hanglog.global.exception.ExceptionCode.NOT_FOUND_TRIP_ID; -import static hanglog.share.fixture.ShareFixture.SHARED_TRIP; -import static hanglog.share.fixture.ShareFixture.TRIP_HAS_SHARED_TRIP; -import static hanglog.share.fixture.ShareFixture.UNSHARED_TRIP; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; - -import hanglog.global.exception.BadRequestException; -import hanglog.share.domain.repository.SharedTripRepository; -import hanglog.share.dto.request.SharedTripStatusRequest; -import hanglog.share.dto.response.SharedTripCodeResponse; -import hanglog.trip.domain.repository.TripRepository; -import java.util.Optional; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.transaction.annotation.Transactional; - -@ExtendWith(MockitoExtension.class) -@Transactional -class SharedTripServiceTest { - - @InjectMocks - private SharedTripService sharedTripService; - - @Mock - private SharedTripRepository sharedTripRepository; - - @Mock - private TripRepository tripRepository; - - @DisplayName("공유된 여행을 조회한다.") - @Test - void getSharedTrip() { - // given - given(sharedTripRepository.findBySharedCode(anyString())) - .willReturn(Optional.of(SHARED_TRIP)); - - // when - final Long tripId = sharedTripService.getTripId("sharedCode"); - - //then - assertThat(tripId).usingRecursiveComparison() - .isEqualTo(1L); - } - - @DisplayName("비공유 상태의 여행 조회시 실패한다.") - @Test - void getSharedTrip_UnsharedFail() { - // given - given(sharedTripRepository.findBySharedCode(anyString())) - .willReturn(Optional.of(UNSHARED_TRIP)); - - // when & then - assertThatThrownBy(() -> sharedTripService.getTripId("sharedCode")) - .isInstanceOf(BadRequestException.class) - .extracting("code") - .isEqualTo(INVALID_SHARE_CODE.getCode()); - } - - @DisplayName("존재하지 않는 코드로 조회시 실패한다.") - @Test - void getSharedTrip_NoExistCode() { - // given - given(sharedTripRepository.findBySharedCode(anyString())) - .willReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> sharedTripService.getTripId("noExistSharedCode")) - .isInstanceOf(BadRequestException.class) - .extracting("code") - .isEqualTo(NOT_FOUND_SHARED_CODE.getCode()); - } - - @DisplayName("여행의 공유 허용상태로 변경한다.") - @Test - void updateSharedStatus() { - // given - final SharedTripStatusRequest sharedTripStatusRequest = new SharedTripStatusRequest(true); - given(tripRepository.findById(anyLong())) - .willReturn(Optional.of(TRIP_HAS_SHARED_TRIP)); - - // when - final SharedTripCodeResponse actual = sharedTripService.updateSharedTripStatus(1L, sharedTripStatusRequest); - - //then - assertThat(actual).usingRecursiveComparison() - .isEqualTo(new SharedTripCodeResponse(SHARED_TRIP.getSharedCode())); - } - - @DisplayName("공유 허용을 처음 할 경우 새로운 공유 code를 생성한다.") - @Test - void updateSharedStatus_CreateSharedTrip() { - // given - final SharedTripStatusRequest sharedTripStatusRequest = new SharedTripStatusRequest(true); - given(tripRepository.findById(anyLong())) - .willReturn(Optional.of(TRIP_HAS_SHARED_TRIP)); - - // when - final SharedTripCodeResponse actual = sharedTripService.updateSharedTripStatus(1L, sharedTripStatusRequest); - - //then - assertThat(actual.getSharedCode()).isNotNull(); - } - - @DisplayName("존재하지 않는 여행의 공유 상태 변경은 예외처리한다.") - @Test - void updateSharedStatus_NotExistTripFail() { - // given - final SharedTripStatusRequest sharedTripStatusRequest = new SharedTripStatusRequest(true); -// given(tripRepository.findById(anyLong())) -// .willReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> sharedTripService.updateSharedTripStatus(1L, sharedTripStatusRequest)) - .isInstanceOf(BadRequestException.class) - .extracting("code") - .isEqualTo(NOT_FOUND_TRIP_ID.getCode()); - } -} diff --git a/backend/src/test/java/hanglog/trip/fixture/DayLogFixture.java b/backend/src/test/java/hanglog/trip/fixture/DayLogFixture.java index 937b8dbcc..101d063af 100644 --- a/backend/src/test/java/hanglog/trip/fixture/DayLogFixture.java +++ b/backend/src/test/java/hanglog/trip/fixture/DayLogFixture.java @@ -1,16 +1,16 @@ package hanglog.trip.fixture; import static hanglog.integration.IntegrationFixture.MEMBER; -import static hanglog.trip.fixture.ItemFixture.AIRPLANE_ITEM; -import static hanglog.trip.fixture.ItemFixture.JAPAN_HOTEL; -import static hanglog.trip.fixture.ItemFixture.LONDON_EYE_ITEM; -import hanglog.community.domain.type.PublishedStatusType; -import hanglog.share.domain.type.SharedStatusType; import hanglog.trip.domain.DayLog; +import hanglog.trip.domain.Item; import hanglog.trip.domain.Trip; +import hanglog.trip.domain.type.ItemType; +import hanglog.trip.domain.type.PublishedStatusType; +import hanglog.trip.domain.type.SharedStatusType; import java.time.LocalDate; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public final class DayLogFixture { @@ -66,13 +66,63 @@ public final class DayLogFixture { "경비 확인 런던 여행", 1, TRIP, - List.of(LONDON_EYE_ITEM, AIRPLANE_ITEM) + new ArrayList<>() ); public static final DayLog EXPENSE_JAPAN_DAYLOG = new DayLog( 1L, "경비 확인 런던 여행", 2, TRIP, - List.of(JAPAN_HOTEL) + new ArrayList<>() ); + + public static final Item LONDON_EYE_ITEM = new Item( + 1L, + ItemType.SPOT, + "런던 아이", + 1, + 4.5, + "런던 아이 메모", + PlaceFixture.LONDON_EYE, + EXPENSE_LONDON_DAYLOG, + ExpenseFixture.EURO_10000 + ); + public static final Item AIRPLANE_ITEM = new Item( + 3L, + ItemType.NON_SPOT, + "비행기", + 3, + 4.5, + "런던에서 탄 비행기", + EXPENSE_LONDON_DAYLOG, + ExpenseFixture.EURO_10000 + ); + public static final Item JAPAN_HOTEL = new Item( + 4L, + ItemType.NON_SPOT, + "호텔", + 3, + 4.5, + "일본에서 묵은 호텔", + EXPENSE_JAPAN_DAYLOG, + ExpenseFixture.JPY_10000 + ); + + static { + addDayLogsToTrip(TRIP, Arrays.asList(EXPENSE_LONDON_DAYLOG, EXPENSE_JAPAN_DAYLOG)); + addItemsToDayLog(EXPENSE_LONDON_DAYLOG, List.of(LONDON_EYE_ITEM, AIRPLANE_ITEM)); + addItemsToDayLog(EXPENSE_JAPAN_DAYLOG, List.of(JAPAN_HOTEL)); + } + + private static void addDayLogsToTrip(final Trip trip, final List dayLogs) { + dayLogs.stream() + .filter(dayLog -> !trip.getDayLogs().contains(dayLog)) + .forEachOrdered(trip::addDayLog); + } + + private static void addItemsToDayLog(final DayLog dayLog, final List items) { + items.stream() + .filter(item -> !dayLog.getItems().contains(item)) + .forEachOrdered(dayLog::addItem); + } } diff --git a/backend/src/test/java/hanglog/trip/fixture/ItemFixture.java b/backend/src/test/java/hanglog/trip/fixture/ItemFixture.java index c589e93f9..9a64f8885 100644 --- a/backend/src/test/java/hanglog/trip/fixture/ItemFixture.java +++ b/backend/src/test/java/hanglog/trip/fixture/ItemFixture.java @@ -3,6 +3,7 @@ import hanglog.trip.domain.DayLog; import hanglog.trip.domain.Item; import hanglog.trip.domain.type.ItemType; +import java.util.List; public final class ItemFixture { @@ -45,11 +46,12 @@ public final class ItemFixture { DAYLOG_FOR_ITEM_FIXTURE, ExpenseFixture.EURO_10000 ); + public static final Item JAPAN_HOTEL = new Item( 4L, ItemType.NON_SPOT, "호텔", - 3, + 4, 4.5, "일본에서 묵은 호텔", new DayLog( @@ -59,4 +61,14 @@ public final class ItemFixture { ), ExpenseFixture.JPY_10000 ); + + static { + addItemsToDayLog(DAYLOG_FOR_ITEM_FIXTURE, List.of(LONDON_EYE_ITEM, AIRPLANE_ITEM, TAXI_ITEM, JAPAN_HOTEL)); + } + + private static void addItemsToDayLog(final DayLog dayLog, final List items) { + items.stream() + .filter(item -> !dayLog.getItems().contains(item)) + .forEachOrdered(dayLog::addItem); + } } diff --git a/backend/src/test/java/hanglog/share/fixture/ShareFixture.java b/backend/src/test/java/hanglog/trip/fixture/ShareFixture.java similarity index 83% rename from backend/src/test/java/hanglog/share/fixture/ShareFixture.java rename to backend/src/test/java/hanglog/trip/fixture/ShareFixture.java index 79984fe4f..66529769c 100644 --- a/backend/src/test/java/hanglog/share/fixture/ShareFixture.java +++ b/backend/src/test/java/hanglog/trip/fixture/ShareFixture.java @@ -1,16 +1,17 @@ -package hanglog.share.fixture; +package hanglog.trip.fixture; import static hanglog.integration.IntegrationFixture.MEMBER; import hanglog.city.domain.City; -import hanglog.community.domain.type.PublishedStatusType; -import hanglog.share.domain.SharedTrip; -import hanglog.share.domain.type.SharedStatusType; import hanglog.trip.domain.DayLog; +import hanglog.trip.domain.SharedTrip; import hanglog.trip.domain.Trip; +import hanglog.trip.domain.type.PublishedStatusType; +import hanglog.trip.domain.type.SharedStatusType; import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class ShareFixture { @@ -101,8 +102,12 @@ public class ShareFixture { ); static { - final List dayLogs = TRIP_SHARE.getDayLogs(); - dayLogs.add(LONDON_DAYLOG_1); - dayLogs.add(LONDON_DAYLOG_2); + addDayLogsToTrip(TRIP_SHARE, Arrays.asList(LONDON_DAYLOG_1, LONDON_DAYLOG_2)); + } + + private static void addDayLogsToTrip(final Trip trip, final List dayLogs) { + dayLogs.stream() + .filter(dayLog -> !trip.getDayLogs().contains(dayLog)) + .forEachOrdered(trip::addDayLog); } } diff --git a/backend/src/test/java/hanglog/trip/fixture/TripFixture.java b/backend/src/test/java/hanglog/trip/fixture/TripFixture.java index 5fda51509..a439ef2cc 100644 --- a/backend/src/test/java/hanglog/trip/fixture/TripFixture.java +++ b/backend/src/test/java/hanglog/trip/fixture/TripFixture.java @@ -7,9 +7,9 @@ import static hanglog.trip.fixture.DayLogFixture.LONDON_DAYLOG_2; import static hanglog.trip.fixture.DayLogFixture.LONDON_DAYLOG_EXTRA; -import hanglog.community.domain.type.PublishedStatusType; -import hanglog.share.domain.type.SharedStatusType; import hanglog.trip.domain.Trip; +import hanglog.trip.domain.type.PublishedStatusType; +import hanglog.trip.domain.type.SharedStatusType; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; diff --git a/backend/src/test/java/hanglog/trip/presentation/CityControllerTest.java b/backend/src/test/java/hanglog/trip/presentation/CityControllerTest.java index 6509e3969..e64fa77fe 100644 --- a/backend/src/test/java/hanglog/trip/presentation/CityControllerTest.java +++ b/backend/src/test/java/hanglog/trip/presentation/CityControllerTest.java @@ -10,10 +10,10 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import hanglog.city.presentation.CityController; import hanglog.city.dto.response.CityResponse; -import hanglog.global.ControllerTest; +import hanglog.city.presentation.CityController; import hanglog.city.service.CityService; +import hanglog.global.ControllerTest; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/hanglog/trip/presentation/DayLogControllerTest.java b/backend/src/test/java/hanglog/trip/presentation/DayLogControllerTest.java index 2b707d9bf..baec33add 100644 --- a/backend/src/test/java/hanglog/trip/presentation/DayLogControllerTest.java +++ b/backend/src/test/java/hanglog/trip/presentation/DayLogControllerTest.java @@ -18,8 +18,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import hanglog.auth.domain.MemberTokens; import hanglog.global.ControllerTest; +import hanglog.login.domain.MemberTokens; import hanglog.trip.dto.request.DayLogUpdateTitleRequest; import hanglog.trip.dto.request.ItemsOrdinalUpdateRequest; import hanglog.trip.dto.response.DayLogResponse; diff --git a/backend/src/test/java/hanglog/image/presentation/ImageControllerTest.java b/backend/src/test/java/hanglog/trip/presentation/ImageControllerTest.java similarity index 92% rename from backend/src/test/java/hanglog/image/presentation/ImageControllerTest.java rename to backend/src/test/java/hanglog/trip/presentation/ImageControllerTest.java index 4b23c9a25..b9c7dd3f2 100644 --- a/backend/src/test/java/hanglog/image/presentation/ImageControllerTest.java +++ b/backend/src/test/java/hanglog/trip/presentation/ImageControllerTest.java @@ -1,4 +1,4 @@ -package hanglog.image.presentation; +package hanglog.trip.presentation; import static hanglog.global.restdocs.RestDocsConfiguration.field; import static org.mockito.ArgumentMatchers.any; @@ -13,8 +13,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import hanglog.global.ControllerTest; -import hanglog.image.dto.ImagesResponse; -import hanglog.image.service.ImageService; +import hanglog.trip.dto.response.ImagesResponse; +import hanglog.trip.service.ImageService; import java.io.FileInputStream; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -59,10 +59,10 @@ void uploadImage() throws Exception { partWithName("images").description("이미지 파일, 최대 5개, 개당 최대 10MB") ), responseFields( - fieldWithPath("imageUrls") + fieldWithPath("imageNames") .type(JsonFieldType.ARRAY) - .description("저장된 이미지 url 배열") - .attributes(field("constraint", "nginx 주소 + 해싱된 이름")) + .description("저장된 이미지 name 배열") + .attributes(field("constraint", "해싱된 이름")) ) )); } diff --git a/backend/src/test/java/hanglog/trip/presentation/ItemControllerTest.java b/backend/src/test/java/hanglog/trip/presentation/ItemControllerTest.java index 5ad3943f7..22b60e060 100644 --- a/backend/src/test/java/hanglog/trip/presentation/ItemControllerTest.java +++ b/backend/src/test/java/hanglog/trip/presentation/ItemControllerTest.java @@ -21,8 +21,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import hanglog.auth.domain.MemberTokens; import hanglog.global.ControllerTest; +import hanglog.login.domain.MemberTokens; import hanglog.trip.dto.request.ExpenseRequest; import hanglog.trip.dto.request.ItemRequest; import hanglog.trip.dto.request.ItemUpdateRequest; @@ -109,7 +109,7 @@ void createItem() throws Exception { 5.0, "에펠탑을 방문", 1L, - List.of("imageUrl"), + List.of("imageName"), placeRequest, expenseRequest ); @@ -150,9 +150,9 @@ void createItem() throws Exception { .type(JsonFieldType.NUMBER) .description("날짜 ID") .attributes(field("constraint", "양의 정수")), - fieldWithPath("imageUrls") + fieldWithPath("imageNames") .type(JsonFieldType.ARRAY) - .description("여행 아이템 이미지 URL 배열") + .description("여행 아이템 이미지 이름 배열") .attributes(field("constraint", "URL 배열")), fieldWithPath("place.name") .type(JsonFieldType.STRING) @@ -209,7 +209,7 @@ void updateItem() throws Exception { 4.5, "에펠탑을 방문", 1L, - List.of("imageUrl"), + List.of("imageName"), true, placeRequest, expenseRequest @@ -251,9 +251,9 @@ void updateItem() throws Exception { .type(JsonFieldType.NUMBER) .description("날짜 ID") .attributes(field("constraint", "양의 정수")), - fieldWithPath("imageUrls") + fieldWithPath("imageNames") .type(JsonFieldType.ARRAY) - .description("여행 아이템 이미지 URL 배열") + .description("여행 아이템 이미지 이름 배열") .attributes(field("constraint", "URL 배열")), fieldWithPath("isPlaceUpdated") .type(JsonFieldType.BOOLEAN) diff --git a/backend/src/test/java/hanglog/share/presentation/SharedTripControllerTest.java b/backend/src/test/java/hanglog/trip/presentation/SharedTripControllerTest.java similarity index 81% rename from backend/src/test/java/hanglog/share/presentation/SharedTripControllerTest.java rename to backend/src/test/java/hanglog/trip/presentation/SharedTripControllerTest.java index e8cf362e8..d0b978966 100644 --- a/backend/src/test/java/hanglog/share/presentation/SharedTripControllerTest.java +++ b/backend/src/test/java/hanglog/trip/presentation/SharedTripControllerTest.java @@ -1,48 +1,34 @@ -package hanglog.share.presentation; +package hanglog.trip.presentation; import static hanglog.category.fixture.CategoryFixture.EXPENSE_CATEGORIES; import static hanglog.expense.fixture.AmountFixture.AMOUNT_20000; import static hanglog.expense.fixture.CurrencyFixture.DEFAULT_CURRENCY; import static hanglog.global.restdocs.RestDocsConfiguration.field; -import static hanglog.share.fixture.ShareFixture.BEIJING; -import static hanglog.share.fixture.ShareFixture.CALIFORNIA; -import static hanglog.share.fixture.ShareFixture.TOKYO; -import static hanglog.share.fixture.ShareFixture.TRIP_SHARE; import static hanglog.trip.fixture.CityFixture.LONDON; import static hanglog.trip.fixture.DayLogFixture.EXPENSE_LONDON_DAYLOG; import static hanglog.trip.fixture.TripFixture.LONDON_TO_JAPAN; -import static hanglog.trip.fixture.TripFixture.LONDON_TRIP; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; -import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import hanglog.auth.domain.MemberTokens; import hanglog.expense.domain.CategoryExpense; -import hanglog.expense.domain.DayLogExpense; -import hanglog.expense.dto.response.TripExpenseResponse; -import hanglog.expense.service.ExpenseService; import hanglog.global.ControllerTest; -import hanglog.share.dto.request.SharedTripStatusRequest; -import hanglog.share.dto.response.SharedTripCodeResponse; -import hanglog.share.service.SharedTripService; -import hanglog.trip.domain.TripCity; +import hanglog.login.domain.MemberTokens; +import hanglog.trip.domain.DayLogExpense; +import hanglog.trip.dto.response.LedgerResponse; import hanglog.trip.dto.response.TripDetailResponse; import hanglog.trip.fixture.CityFixture; +import hanglog.trip.fixture.ShareFixture; +import hanglog.trip.service.LedgerService; +import hanglog.trip.service.SharedTripService; import hanglog.trip.service.TripService; import jakarta.servlet.http.Cookie; import java.util.List; @@ -73,7 +59,7 @@ class SharedTripControllerTest extends ControllerTest { private TripService tripService; @MockBean - private ExpenseService expenseService; + private LedgerService ledgerService; @DisplayName("ShareCode로 단일 여행을 조회한다.") @Test @@ -82,7 +68,7 @@ void getSharedTrip() throws Exception { when(sharedTripService.getTripId(anyString())) .thenReturn(1L); when(sharedTripService.getSharedTripDetail(anyLong())) - .thenReturn(TripDetailResponse.sharedTrip(TRIP_SHARE, List.of(CALIFORNIA, TOKYO, BEIJING))); + .thenReturn(TripDetailResponse.sharedTrip(ShareFixture.TRIP_SHARE, List.of(ShareFixture.CALIFORNIA, ShareFixture.TOKYO, ShareFixture.BEIJING))); // when mockMvc.perform(get("/shared-trips/{sharedCode}", "xxxxxx").contentType(APPLICATION_JSON)) @@ -133,10 +119,10 @@ void getSharedTrip() throws Exception { .type(JsonFieldType.STRING) .description("여행 요약") .attributes(field("constraint", "200자 이하의 문자열")), - fieldWithPath("imageUrl") + fieldWithPath("imageName") .type(JsonFieldType.STRING) .description("여행 대표 이미지") - .attributes(field("constraint", "이미지 URL")), + .attributes(field("constraint", "이미지 이름")), fieldWithPath("sharedCode") .type(JsonFieldType.STRING) .description("공유 코드") @@ -211,72 +197,6 @@ void getSharedTrip() throws Exception { .andReturn(); } - @DisplayName("공유 상태를 변경한다") - @Test - void updateSharedStatus() throws Exception { - // given - final SharedTripStatusRequest sharedStatusRequest = new SharedTripStatusRequest(true); - final SharedTripCodeResponse sharedCodeResponse = new SharedTripCodeResponse("sharedCode"); - when(sharedTripService.updateSharedTripStatus(anyLong(), any(SharedTripStatusRequest.class))) - .thenReturn(sharedCodeResponse); - given(refreshTokenRepository.existsByToken(any())).willReturn(true); - doNothing().when(jwtProvider).validateTokens(any()); - given(jwtProvider.getSubject(any())).willReturn("1"); - doNothing().when(tripService).validateTripByMember(anyLong(), anyLong()); - - // when & then - mockMvc.perform(patch("/trips/{tripId}/share", 1) - .header(AUTHORIZATION, MEMBER_TOKENS.getAccessToken()) - .cookie(COOKIE) - .contentType(APPLICATION_JSON) - .content(objectMapper.writeValueAsString(sharedStatusRequest))) - .andExpect(status().isOk()) - .andDo(restDocs.document( - pathParameters( - parameterWithName("tripId") - .description("여행 ID") - ), - requestFields( - fieldWithPath("sharedStatus") - .type(JsonFieldType.BOOLEAN) - .description("공유 유무") - .attributes(field("constraint", "공유시: true, 비공유시: false")) - ), - responseFields( - fieldWithPath("sharedCode") - .type(JsonFieldType.STRING) - .description("공유 코드") - .attributes(field("constraint", "공유시: 문자열 비공유시: null")) - .optional() - ) - ) - ) - .andReturn(); - } - - @DisplayName("공유 상태가 없는 공유 수정 요청은 예외처리한다.") - @Test - void getSharedTrip_NullSharedStatus() throws Exception { - // given - final SharedTripStatusRequest sharedStatusRequest = new SharedTripStatusRequest(null); - final SharedTripCodeResponse sharedCodeResponse = new SharedTripCodeResponse("xxxxxx"); - when(sharedTripService.updateSharedTripStatus(anyLong(), any(SharedTripStatusRequest.class))) - .thenReturn(sharedCodeResponse); - given(refreshTokenRepository.existsByToken(any())).willReturn(true); - doNothing().when(jwtProvider).validateTokens(any()); - given(jwtProvider.getSubject(any())).willReturn("1"); - doNothing().when(tripService).validateTripByMember(anyLong(), anyLong()); - - // when & then - mockMvc.perform(patch("/trips/{tripId}/share", 1) - .header(AUTHORIZATION, MEMBER_TOKENS.getAccessToken()) - .cookie(COOKIE) - .contentType(APPLICATION_JSON) - .content(objectMapper.writeValueAsString(sharedStatusRequest))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("공유 상태를 선택해주세요.")); - } - @DisplayName("ShareCode로 여행에 대한 가계부를 조회한다.") @Test void getSharedExpenses() throws Exception { @@ -284,17 +204,17 @@ void getSharedExpenses() throws Exception { when(sharedTripService.getTripId(anyString())) .thenReturn(1L); - final TripExpenseResponse tripExpenseResponse = TripExpenseResponse.of( + final LedgerResponse ledgerResponse = LedgerResponse.of( LONDON_TO_JAPAN, AMOUNT_20000, - List.of(new TripCity(LONDON_TRIP, LONDON), new TripCity(LONDON_TRIP, CityFixture.TOKYO)), + List.of(LONDON, CityFixture.TOKYO), List.of(new CategoryExpense(EXPENSE_CATEGORIES.get(1), AMOUNT_20000, AMOUNT_20000)), DEFAULT_CURRENCY, List.of(new DayLogExpense(EXPENSE_LONDON_DAYLOG, AMOUNT_20000)) ); // when - when(expenseService.getAllExpenses(1L)).thenReturn(tripExpenseResponse); + when(ledgerService.getAllExpenses(1L)).thenReturn(ledgerResponse); // then mockMvc.perform(get("/shared-trips/{sharedCode}/expense", "xxxxxx").contentType(APPLICATION_JSON)) diff --git a/backend/src/test/java/hanglog/trip/presentation/TripControllerTest.java b/backend/src/test/java/hanglog/trip/presentation/TripControllerTest.java index dc0b81a5e..2e119ab6e 100644 --- a/backend/src/test/java/hanglog/trip/presentation/TripControllerTest.java +++ b/backend/src/test/java/hanglog/trip/presentation/TripControllerTest.java @@ -1,8 +1,14 @@ package hanglog.trip.presentation; +import static hanglog.category.fixture.CategoryFixture.EXPENSE_CATEGORIES; +import static hanglog.expense.fixture.AmountFixture.AMOUNT_20000; +import static hanglog.expense.fixture.CurrencyFixture.DEFAULT_CURRENCY; import static hanglog.global.restdocs.RestDocsConfiguration.field; import static hanglog.trip.fixture.CityFixture.LONDON; import static hanglog.trip.fixture.CityFixture.PARIS; +import static hanglog.trip.fixture.CityFixture.TOKYO; +import static hanglog.trip.fixture.DayLogFixture.EXPENSE_LONDON_DAYLOG; +import static hanglog.trip.fixture.TripFixture.LONDON_TO_JAPAN; import static hanglog.trip.fixture.TripFixture.LONDON_TRIP; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -32,14 +38,20 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import hanglog.auth.domain.MemberTokens; import hanglog.city.domain.City; +import hanglog.expense.domain.CategoryExpense; import hanglog.global.ControllerTest; +import hanglog.login.domain.MemberTokens; +import hanglog.trip.domain.DayLogExpense; import hanglog.trip.dto.request.PublishedStatusRequest; +import hanglog.trip.dto.request.SharedStatusRequest; import hanglog.trip.dto.request.TripCreateRequest; import hanglog.trip.dto.request.TripUpdateRequest; +import hanglog.trip.dto.response.LedgerResponse; +import hanglog.trip.dto.response.SharedCodeResponse; import hanglog.trip.dto.response.TripDetailResponse; import hanglog.trip.dto.response.TripResponse; +import hanglog.trip.service.LedgerService; import hanglog.trip.service.TripService; import jakarta.servlet.http.Cookie; import java.time.LocalDate; @@ -72,6 +84,9 @@ class TripControllerTest extends ControllerTest { @MockBean private TripService tripService; + @MockBean + private LedgerService ledgerService; + @BeforeEach void setUp() { given(refreshTokenRepository.existsByToken(any())).willReturn(true); @@ -129,6 +144,14 @@ private ResultActions performDeleteRequest() throws Exception { .contentType(APPLICATION_JSON)); } + private ResultActions performGetLedgerRequest(final int tripId) throws Exception { + return mockMvc.perform( + get("/trips/{tripId}/expense", tripId) + .header(AUTHORIZATION, MEMBER_TOKENS.getAccessToken()) + .cookie(COOKIE) + .contentType(APPLICATION_JSON)); + } + @DisplayName("단일 여행을 생성할 수 있다.") @Test void createTrip() throws Exception { @@ -306,10 +329,10 @@ void getTrip() throws Exception { .type(JsonFieldType.STRING) .description("여행 요약") .attributes(field("constraint", "200자 이하의 문자열")), - fieldWithPath("imageUrl") + fieldWithPath("imageName") .type(JsonFieldType.STRING) .description("여행 대표 이미지") - .attributes(field("constraint", "이미지 URL")), + .attributes(field("constraint", "이미지 이름")), fieldWithPath("sharedCode") .type(JsonFieldType.STRING) .description("공유 코드") @@ -427,10 +450,10 @@ void getTrips() throws Exception { .type(JsonFieldType.STRING) .description("여행 요약") .attributes(field("constraint", "200자 이하의 문자열")), - fieldWithPath("[].imageUrl") + fieldWithPath("[].imageName") .type(JsonFieldType.STRING) .description("여행 대표 이미지") - .attributes(field("constraint", "이미지 URL")), + .attributes(field("constraint", "이미지 이름")), fieldWithPath("[].cities") .type(JsonFieldType.ARRAY) .description("여행 도시 배열") @@ -465,7 +488,7 @@ void updateTrip() throws Exception { final TripUpdateRequest updateRequest = new TripUpdateRequest( "변경된 타이틀", - "https://hanglog.com/img/default-image.png", + "default-image.png", LocalDate.of(2023, 7, 2), LocalDate.of(2023, 7, 7), "추가된 여행 설명", @@ -490,10 +513,10 @@ void updateTrip() throws Exception { .type(JsonFieldType.STRING) .description("여행 제목") .attributes(field("constraint", "50자 이하의 문자열")), - fieldWithPath("imageUrl") + fieldWithPath("imageName") .type(JsonFieldType.STRING) .description("여행 이미지") - .attributes(field("constraint", "이미지 URL")), + .attributes(field("constraint", "이미지 이름")), fieldWithPath("startDate") .type(JsonFieldType.STRING) .description("여행 시작 날짜") @@ -524,7 +547,7 @@ void updateTrip_TitleNull() throws Exception { final TripUpdateRequest badRequest = new TripUpdateRequest( null, - "https://hanglog.com/img/default-image.png", + "default-image.png", LocalDate.of(2023, 7, 2), LocalDate.of(2023, 7, 7), "추가된 여행 설명", @@ -550,7 +573,7 @@ void updateTrip_TitleOverMax() throws Exception { final String updatedTitle = "1" + "1234567890".repeat(5); final TripUpdateRequest badRequest = new TripUpdateRequest( updatedTitle, - "https://hanglog.com/img/default-image.png", + "default-image.png", LocalDate.of(2023, 7, 2), LocalDate.of(2023, 7, 7), "추가된 여행 설명", @@ -576,7 +599,7 @@ void updateTrip_DescriptionOverMax() throws Exception { final String updateDescription = "1" + "1234567890".repeat(20); final TripUpdateRequest badRequest = new TripUpdateRequest( "updatedTitle", - "https://hanglog.com/img/default-image.png", + "default-image.png", LocalDate.of(2023, 7, 2), LocalDate.of(2023, 7, 7), updateDescription, @@ -600,7 +623,7 @@ void updateTrip_StartDateNull() throws Exception { final TripUpdateRequest badRequest = new TripUpdateRequest( "변경된 타이틀", - "https://hanglog.com/img/default-image.png", + "default-image.png", null, LocalDate.of(2023, 7, 7), "추가된 여행 설명", @@ -624,7 +647,7 @@ void updateTrip_EndDateNull() throws Exception { final TripUpdateRequest badRequest = new TripUpdateRequest( "변경된 타이틀", - "https://hanglog.com/img/default-image.png", + "default-image.png", LocalDate.of(2023, 7, 1), null, "추가된 여행 설명", @@ -648,7 +671,7 @@ void updateTrip_CityIdsNull() throws Exception { final TripUpdateRequest badRequest = new TripUpdateRequest( "변경된 타이틀", - "https://hanglog.com/img/default-image.png", + "default-image.png", LocalDate.of(2023, 7, 1), LocalDate.of(2023, 7, 7), "추가된 여행 설명", @@ -672,7 +695,7 @@ void updateTrip_CityIdsEmpty() throws Exception { final TripUpdateRequest badRequest = new TripUpdateRequest( "변경된 타이틀", - "https://hanglog.com/img/default-image.png", + "default-image.png", LocalDate.of(2023, 7, 1), LocalDate.of(2023, 7, 7), "추가된 여행 설명", @@ -709,6 +732,72 @@ void deleteTrip() throws Exception { )); } + @DisplayName("공유 상태를 변경한다") + @Test + void updateSharedStatus() throws Exception { + // given + final SharedStatusRequest sharedStatusRequest = new SharedStatusRequest(true); + final SharedCodeResponse sharedCodeResponse = new SharedCodeResponse("sharedCode"); + when(tripService.updateSharedTripStatus(anyLong(), any(SharedStatusRequest.class))) + .thenReturn(sharedCodeResponse); + given(refreshTokenRepository.existsByToken(any())).willReturn(true); + doNothing().when(jwtProvider).validateTokens(any()); + given(jwtProvider.getSubject(any())).willReturn("1"); + doNothing().when(tripService).validateTripByMember(anyLong(), anyLong()); + + // when & then + mockMvc.perform(patch("/trips/{tripId}/share", 1) + .header(AUTHORIZATION, MEMBER_TOKENS.getAccessToken()) + .cookie(COOKIE) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(sharedStatusRequest))) + .andExpect(status().isOk()) + .andDo(restDocs.document( + pathParameters( + parameterWithName("tripId") + .description("여행 ID") + ), + requestFields( + fieldWithPath("sharedStatus") + .type(JsonFieldType.BOOLEAN) + .description("공유 유무") + .attributes(field("constraint", "공유시: true, 비공유시: false")) + ), + responseFields( + fieldWithPath("sharedCode") + .type(JsonFieldType.STRING) + .description("공유 코드") + .attributes(field("constraint", "공유시: 문자열 비공유시: null")) + .optional() + ) + ) + ) + .andReturn(); + } + + @DisplayName("공유 상태가 없는 공유 수정 요청은 예외처리한다.") + @Test + void getSharedTrip_NullSharedStatus() throws Exception { + // given + final SharedStatusRequest sharedStatusRequest = new SharedStatusRequest(null); + final SharedCodeResponse sharedCodeResponse = new SharedCodeResponse("xxxxxx"); + when(tripService.updateSharedTripStatus(anyLong(), any(SharedStatusRequest.class))) + .thenReturn(sharedCodeResponse); + given(refreshTokenRepository.existsByToken(any())).willReturn(true); + doNothing().when(jwtProvider).validateTokens(any()); + given(jwtProvider.getSubject(any())).willReturn("1"); + doNothing().when(tripService).validateTripByMember(anyLong(), anyLong()); + + // when & then + mockMvc.perform(patch("/trips/{tripId}/share", 1) + .header(AUTHORIZATION, MEMBER_TOKENS.getAccessToken()) + .cookie(COOKIE) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(sharedStatusRequest))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("공유 상태를 선택해주세요.")); + } + @DisplayName("커뮤니티 공개 상태를 변경한다") @Test void updatePublishedStatus() throws Exception { @@ -740,4 +829,181 @@ void updatePublishedStatus() throws Exception { ) .andReturn(); } + + @DisplayName("특정 여행의 가계부를 가져온다.") + @Test + void getLedger() throws Exception { + // given + given(refreshTokenRepository.existsByToken(any())).willReturn(true); + doNothing().when(jwtProvider).validateTokens(any()); + given(jwtProvider.getSubject(any())).willReturn("1"); + doNothing().when(tripService).validateTripByMember(anyLong(), anyLong()); + + final LedgerResponse ledgerResponse = LedgerResponse.of( + LONDON_TO_JAPAN, + AMOUNT_20000, + List.of(LONDON, TOKYO), + List.of(new CategoryExpense(EXPENSE_CATEGORIES.get(1), AMOUNT_20000, AMOUNT_20000)), + DEFAULT_CURRENCY, + List.of(new DayLogExpense(EXPENSE_LONDON_DAYLOG, AMOUNT_20000)) + ); + + when(ledgerService.getAllExpenses(1L)).thenReturn(ledgerResponse); + + // when + final ResultActions resultActions = performGetLedgerRequest(1); + + // then + resultActions.andDo( + restDocs.document( + pathParameters( + parameterWithName("tripId") + .description("여행 ID") + ), + responseFields( + fieldWithPath("id") + .type(JsonFieldType.NUMBER) + .description("여행 ID") + .attributes(field("constraint", "양의 정수")), + fieldWithPath("title") + .type(JsonFieldType.STRING) + .description("여행 제목") + .attributes(field("constraint", "50자 이하의 문자열")), + fieldWithPath("startDate") + .type(JsonFieldType.STRING) + .description("여행 시작 날짜") + .attributes(field("constraint", "yyyy-MM-dd")), + fieldWithPath("endDate") + .type(JsonFieldType.STRING) + .description("여행 종료 날짜") + .attributes(field("constraint", "yyyy-MM-dd")), + fieldWithPath("cities") + .type(JsonFieldType.ARRAY) + .description("도시 목록") + .attributes(field("constraint", "1개 이상의 city")), + fieldWithPath("cities[].id") + .type(JsonFieldType.NUMBER) + .description("도시 ID") + .attributes(field("constraint", "양의 정수")), + fieldWithPath("cities[].name") + .type(JsonFieldType.STRING) + .description("도시 명") + .attributes(field("constraint", "20자 이하의 문자열")), + fieldWithPath("totalAmount") + .type(JsonFieldType.NUMBER) + .description("총 경비") + .attributes(field("constraint", "양의 정수")), + fieldWithPath("categories") + .type(JsonFieldType.ARRAY) + .description("카테고리별 경비 목록") + .attributes(field("constraint", "카테고리 배열")), + fieldWithPath("categories[].category") + .type(JsonFieldType.OBJECT) + .description("카테고리") + .attributes(field("constraint", "카테고리")), + fieldWithPath("categories[].category.id") + .type(JsonFieldType.NUMBER) + .description("카테고리 ID") + .attributes(field("constraint", "양의 정수")), + fieldWithPath("categories[].category.name") + .type(JsonFieldType.STRING) + .description("카테고리 명") + .attributes(field("constraint", "2자의 문자열")), + fieldWithPath("categories[].amount") + .type(JsonFieldType.NUMBER) + .description("카테고리 경비") + .attributes(field("constraint", "양의 정수")), + fieldWithPath("categories[].percentage") + .type(JsonFieldType.NUMBER) + .description("카테고리 백분율") + .attributes(field("constraint", "100이하의 백분율")), + fieldWithPath("exchangeRate") + .type(JsonFieldType.OBJECT) + .description("적용된 환율") + .attributes(field("constraint", "적용된 환율")), + fieldWithPath("exchangeRate.date") + .type(JsonFieldType.STRING) + .description("환율 날짜") + .attributes(field("constraint", "yyyy-MM-dd")), + fieldWithPath("exchangeRate.currencyRates") + .type(JsonFieldType.ARRAY) + .description("통화별 환율") + .attributes(field("constraint", "currency")), + fieldWithPath("exchangeRate.currencyRates[].currency") + .type(JsonFieldType.STRING) + .description("통화") + .attributes(field("constraint", "3자의 문자열")), + fieldWithPath("exchangeRate.currencyRates[].rate") + .type(JsonFieldType.NUMBER) + .description("환율") + .attributes(field("constraint", "양의 유리수")), + fieldWithPath("dayLogs") + .type(JsonFieldType.ARRAY) + .description("날짜별 여행 기록 배열") + .attributes(field("constraint", "2개 이상의 데이 로그")), + fieldWithPath("dayLogs[].id") + .type(JsonFieldType.NUMBER) + .description("날짜별 기록 ID") + .attributes(field("constraint", "양의 정수")), + fieldWithPath("dayLogs[].ordinal") + .type(JsonFieldType.NUMBER) + .description("여행에서의 날짜 순서") + .attributes(field("constraint", "양의 정수")), + fieldWithPath("dayLogs[].date") + .type(JsonFieldType.STRING) + .description("실제 날짜") + .attributes(field("constraint", "yyyy-MM-dd")), + fieldWithPath("dayLogs[].totalAmount") + .type(JsonFieldType.NUMBER) + .description("날짜별 총 경비") + .attributes(field("constraint", "양의 정수")), + fieldWithPath("dayLogs[].items") + .type(JsonFieldType.ARRAY) + .description("아이템 목록") + .attributes(field("constraint", "배열")), + fieldWithPath("dayLogs[].items[].id") + .type(JsonFieldType.NUMBER) + .description("아이템 ID") + .attributes(field("constraint", "양의 정수")), + fieldWithPath("dayLogs[].items[].title") + .type(JsonFieldType.STRING) + .description("아이템 제목") + .attributes(field("constraint", "50자 이하의 문자열")), + fieldWithPath("dayLogs[].items[].ordinal") + .type(JsonFieldType.NUMBER) + .description("아이템의 배치 순서") + .attributes(field("constraint", "양의 정수")), + fieldWithPath("dayLogs[].items[].expense") + .type(JsonFieldType.OBJECT) + .description("아이템 경비") + .attributes(field("constraint", "아이템 경비")), + fieldWithPath("dayLogs[].items[].expense.id") + .type(JsonFieldType.NUMBER) + .description("경비 ID") + .attributes(field("constraint", "양의 정수")), + fieldWithPath("dayLogs[].items[].expense.currency") + .type(JsonFieldType.STRING) + .description("경비 통화") + .attributes(field("constraint", "3자의 문자열")), + fieldWithPath("dayLogs[].items[].expense.amount") + .type(JsonFieldType.NUMBER) + .description("경비") + .attributes(field("constraint", "양의 유리수")), + fieldWithPath("dayLogs[].items[].expense.category") + .type(JsonFieldType.OBJECT) + .description("경비 카테고리") + .attributes(field("constraint", "id와 이름")), + fieldWithPath("dayLogs[].items[].expense.category.id") + .type(JsonFieldType.NUMBER) + .description("카테고리 ID") + .attributes(field("constraint", "양의 정수")), + fieldWithPath("dayLogs[].items[].expense.category.name") + .type(JsonFieldType.STRING) + .description("카테고리 명") + .attributes(field("constraint", "2자 문자열")) + ) + ) + ) + .andExpect(status().isOk()); + } } diff --git a/backend/src/test/java/hanglog/trip/service/CityServiceTest.java b/backend/src/test/java/hanglog/trip/service/CityServiceTest.java index 895b6410b..f813e106e 100644 --- a/backend/src/test/java/hanglog/trip/service/CityServiceTest.java +++ b/backend/src/test/java/hanglog/trip/service/CityServiceTest.java @@ -5,9 +5,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; -import hanglog.city.service.CityService; -import hanglog.city.dto.response.CityResponse; import hanglog.city.domain.repository.CityRepository; +import hanglog.city.dto.response.CityResponse; +import hanglog.city.service.CityService; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/hanglog/trip/service/DayLogServiceTest.java b/backend/src/test/java/hanglog/trip/service/DayLogServiceTest.java index bcc80c55f..59c2b3087 100644 --- a/backend/src/test/java/hanglog/trip/service/DayLogServiceTest.java +++ b/backend/src/test/java/hanglog/trip/service/DayLogServiceTest.java @@ -4,15 +4,13 @@ import static hanglog.trip.fixture.DayLogFixture.UPDATED_LONDON_DAYLOG; import static hanglog.trip.fixture.ItemFixture.DAYLOG_FOR_ITEM_FIXTURE; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; import hanglog.trip.domain.DayLog; -import hanglog.trip.domain.Item; +import hanglog.trip.domain.repository.CustomItemRepository; import hanglog.trip.domain.repository.DayLogRepository; -import hanglog.trip.domain.repository.ItemRepository; import hanglog.trip.dto.request.DayLogUpdateTitleRequest; import hanglog.trip.dto.request.ItemsOrdinalUpdateRequest; import hanglog.trip.dto.response.DayLogResponse; @@ -36,7 +34,7 @@ class DayLogServiceTest { private DayLogRepository dayLogRepository; @Mock - private ItemRepository itemRepository; + private CustomItemRepository customItemRepository; @DisplayName("날짜별 여행을 조회할 수 있다.") @Test @@ -65,7 +63,7 @@ void updateTitle() { // given final DayLogUpdateTitleRequest request = new DayLogUpdateTitleRequest("updated"); - given(dayLogRepository.findById(1L)) + given(dayLogRepository.findWithItemsById(1L)) .willReturn(Optional.of(LONDON_DAYLOG_1)); given(dayLogRepository.save(any(DayLog.class))) .willReturn(UPDATED_LONDON_DAYLOG); @@ -74,7 +72,7 @@ void updateTitle() { dayLogService.updateTitle(LONDON_DAYLOG_1.getId(), request); // then - verify(dayLogRepository).findById(UPDATED_LONDON_DAYLOG.getId()); + verify(dayLogRepository).findWithItemsById(UPDATED_LONDON_DAYLOG.getId()); verify(dayLogRepository).save(any(DayLog.class)); } @@ -82,25 +80,14 @@ void updateTitle() { @Test void updateItemOrdinals() { // given - final ItemsOrdinalUpdateRequest request = new ItemsOrdinalUpdateRequest(List.of(3L, 2L, 1L)); - given(dayLogRepository.findById(1L)) + final ItemsOrdinalUpdateRequest request = new ItemsOrdinalUpdateRequest(List.of(4L, 3L, 2L, 1L)); + given(dayLogRepository.findWithItemsById(1L)) .willReturn(Optional.of(DAYLOG_FOR_ITEM_FIXTURE)); - given(itemRepository.findById(1L)) - .willReturn(Optional.ofNullable(DAYLOG_FOR_ITEM_FIXTURE.getItems().get(0))); - given(itemRepository.findById(2L)) - .willReturn(Optional.ofNullable(DAYLOG_FOR_ITEM_FIXTURE.getItems().get(1))); - given(itemRepository.findById(3L)) - .willReturn(Optional.ofNullable(DAYLOG_FOR_ITEM_FIXTURE.getItems().get(2))); // when dayLogService.updateOrdinalOfItems(1L, request); // then - final List items = DAYLOG_FOR_ITEM_FIXTURE.getItems(); - assertSoftly(softly -> { - softly.assertThat(items.get(0).getOrdinal()).isEqualTo(3); - softly.assertThat(items.get(1).getOrdinal()).isEqualTo(2); - softly.assertThat(items.get(2).getOrdinal()).isEqualTo(1); - }); + verify(customItemRepository).updateOrdinals(any()); } } diff --git a/backend/src/test/java/hanglog/image/service/ImageServiceTest.java b/backend/src/test/java/hanglog/trip/service/ImageServiceTest.java similarity index 98% rename from backend/src/test/java/hanglog/image/service/ImageServiceTest.java rename to backend/src/test/java/hanglog/trip/service/ImageServiceTest.java index 4f7666023..feb665c7c 100644 --- a/backend/src/test/java/hanglog/image/service/ImageServiceTest.java +++ b/backend/src/test/java/hanglog/trip/service/ImageServiceTest.java @@ -1,4 +1,4 @@ -package hanglog.image.service; +package hanglog.trip.service; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/backend/src/test/java/hanglog/trip/service/ItemServiceTest.java b/backend/src/test/java/hanglog/trip/service/ItemServiceTest.java index 62832123c..6e56f6801 100644 --- a/backend/src/test/java/hanglog/trip/service/ItemServiceTest.java +++ b/backend/src/test/java/hanglog/trip/service/ItemServiceTest.java @@ -5,17 +5,20 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import hanglog.category.domain.Category; import hanglog.category.domain.repository.CategoryRepository; import hanglog.category.fixture.CategoryFixture; +import hanglog.expense.domain.repository.ExpenseRepository; import hanglog.global.exception.BadRequestException; -import hanglog.image.domain.repository.ImageRepository; import hanglog.trip.domain.DayLog; import hanglog.trip.domain.Item; +import hanglog.trip.domain.repository.CustomImageRepository; import hanglog.trip.domain.repository.DayLogRepository; import hanglog.trip.domain.repository.ItemRepository; +import hanglog.trip.domain.repository.PlaceRepository; import hanglog.trip.domain.type.ItemType; import hanglog.trip.dto.request.ExpenseRequest; import hanglog.trip.dto.request.ItemRequest; @@ -45,13 +48,19 @@ class ItemServiceTest { private ItemRepository itemRepository; @Mock - private CategoryRepository categoryRepository; + private CustomImageRepository customImageRepository; @Mock - private DayLogRepository dayLogRepository; + private PlaceRepository placeRepository; + + @Mock + private ExpenseRepository expenseRepository; @Mock - private ImageRepository imageRepository; + private CategoryRepository categoryRepository; + + @Mock + private DayLogRepository dayLogRepository; @DisplayName("새롭게 생성한 여행 아이템의 id를 반환한다.") @Test @@ -70,17 +79,18 @@ void save() { 4.5, "에펠탑을 방문", 1L, - List.of("https://hanglog.com/img/imageName.png"), + List.of("imageName.png"), placeRequest, expenseRequest ); given(itemRepository.save(any())) .willReturn(ItemFixture.LONDON_EYE_ITEM); - given(dayLogRepository.findById(any())) + given(dayLogRepository.findWithItemsById(any())) .willReturn(Optional.of(new DayLog("첫날", 1, TripFixture.LONDON_TRIP))); given(categoryRepository.findById(any())) .willReturn(Optional.of(new Category(1L, "문화", "culture"))); + doNothing().when(customImageRepository).saveAll(any()); // when final Long actualId = itemService.save(1L, itemRequest); @@ -107,12 +117,12 @@ void save_NotContainBaseUrl() { 4.5, "에펠탑을 방문", 1L, - List.of("https://invalid-url/img/imageName.png"), + List.of("imageName.png"), placeRequest, expenseRequest ); - given(dayLogRepository.findById(any())) + given(dayLogRepository.findWithItemsById(any())) .willReturn(Optional.of(new DayLog("첫날", 1, TripFixture.LONDON_TRIP))); // when & then @@ -136,12 +146,12 @@ void save_InvalidParsedUrl() { 4.5, "에펠탑을 방문", 1L, - List.of("https://hanglog.com/img/https://hanglog.com/img/imageName.png"), + List.of("imageName.png"), placeRequest, expenseRequest ); - given(dayLogRepository.findById(any())) + given(dayLogRepository.findWithItemsById(any())) .willReturn(Optional.of(new DayLog("첫날", 1, TripFixture.LONDON_TRIP))); // when & then @@ -159,20 +169,18 @@ void update_PlaceNotChange() { 4.5, "에펠탑을 방문", 1L, - List.of("https://hanglog.com/img/imageName.png"), + List.of("imageName.png"), false, null, expenseRequest ); + final DayLog dayLog = new DayLog("첫날", 1, TripFixture.LONDON_TRIP); + dayLog.addItem(ItemFixture.LONDON_EYE_ITEM); - given(itemRepository.save(any())) - .willReturn(ItemFixture.LONDON_EYE_ITEM); - given(itemRepository.findById(any())) - .willReturn(Optional.of(ItemFixture.LONDON_EYE_ITEM)); given(categoryRepository.findById(any())) .willReturn(Optional.of(CategoryFixture.EXPENSE_CATEGORIES.get(1))); - given(dayLogRepository.findById(any())) - .willReturn(Optional.of(new DayLog("첫날", 1, TripFixture.LONDON_TRIP))); + given(dayLogRepository.findWithItemDetailsById(any())) + .willReturn(Optional.of(dayLog)); // when itemService.update(1L, 1L, itemUpdateRequest); @@ -198,20 +206,18 @@ void update_PlaceChange() { 4.5, "에펠탑을 방문", 1L, - List.of("https://hanglog.com/img/imageName.png"), + List.of("imageName.png"), false, placeRequest, expenseRequest ); - given(itemRepository.save(any())) - .willReturn(ItemFixture.LONDON_EYE_ITEM); - given(itemRepository.findById(any())) - .willReturn(Optional.of(ItemFixture.LONDON_EYE_ITEM)); + final DayLog dayLog = new DayLog("첫날", 1, TripFixture.LONDON_TRIP); + dayLog.addItem(ItemFixture.LONDON_EYE_ITEM); given(categoryRepository.findById(any())) .willReturn(Optional.of(CategoryFixture.EXPENSE_CATEGORIES.get(1))); - given(dayLogRepository.findById(any())) - .willReturn(Optional.of(new DayLog("첫날", 1, TripFixture.LONDON_TRIP))); + given(dayLogRepository.findWithItemDetailsById(any())) + .willReturn(Optional.of(dayLog)); // when itemService.update(1L, 1L, itemUpdateRequest); @@ -246,7 +252,7 @@ void delete() { itemService.delete(itemForDelete.getId()); // then - verify(itemRepository).delete(any()); + verify(itemRepository).deleteById(any()); } @DisplayName("모든 여행 아이템의 Response를 반환한다.") diff --git a/backend/src/test/java/hanglog/trip/service/SharedTripServiceTest.java b/backend/src/test/java/hanglog/trip/service/SharedTripServiceTest.java new file mode 100644 index 000000000..9bf7be4f5 --- /dev/null +++ b/backend/src/test/java/hanglog/trip/service/SharedTripServiceTest.java @@ -0,0 +1,74 @@ +package hanglog.trip.service; + +import static hanglog.global.exception.ExceptionCode.INVALID_SHARE_CODE; +import static hanglog.global.exception.ExceptionCode.NOT_FOUND_SHARED_CODE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; + +import hanglog.global.exception.BadRequestException; +import hanglog.trip.domain.repository.SharedTripRepository; +import hanglog.trip.fixture.ShareFixture; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.transaction.annotation.Transactional; + +@ExtendWith(MockitoExtension.class) +@Transactional +class SharedTripServiceTest { + + @InjectMocks + private SharedTripService sharedTripService; + + @Mock + private SharedTripRepository sharedTripRepository; + + @DisplayName("공유된 여행을 조회한다.") + @Test + void getSharedTrip() { + // given + given(sharedTripRepository.findBySharedCode(anyString())) + .willReturn(Optional.of(ShareFixture.SHARED_TRIP)); + + // when + final Long tripId = sharedTripService.getTripId("sharedCode"); + + //then + assertThat(tripId).usingRecursiveComparison() + .isEqualTo(1L); + } + + @DisplayName("비공유 상태의 여행 조회시 실패한다.") + @Test + void getSharedTrip_UnsharedFail() { + // given + given(sharedTripRepository.findBySharedCode(anyString())) + .willReturn(Optional.of(ShareFixture.UNSHARED_TRIP)); + + // when & then + assertThatThrownBy(() -> sharedTripService.getTripId("sharedCode")) + .isInstanceOf(BadRequestException.class) + .extracting("code") + .isEqualTo(INVALID_SHARE_CODE.getCode()); + } + + @DisplayName("존재하지 않는 코드로 조회시 실패한다.") + @Test + void getSharedTrip_NoExistCode() { + // given + given(sharedTripRepository.findBySharedCode(anyString())) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> sharedTripService.getTripId("noExistSharedCode")) + .isInstanceOf(BadRequestException.class) + .extracting("code") + .isEqualTo(NOT_FOUND_SHARED_CODE.getCode()); + } +} diff --git a/backend/src/test/java/hanglog/trip/service/TripServiceTest.java b/backend/src/test/java/hanglog/trip/service/TripServiceTest.java index 6af5c70fb..3ea8007bd 100644 --- a/backend/src/test/java/hanglog/trip/service/TripServiceTest.java +++ b/backend/src/test/java/hanglog/trip/service/TripServiceTest.java @@ -4,30 +4,38 @@ import static hanglog.integration.IntegrationFixture.MEMBER; import static hanglog.trip.fixture.CityFixture.LONDON; import static hanglog.trip.fixture.CityFixture.PARIS; +import static hanglog.trip.fixture.ShareFixture.SHARED_TRIP; +import static hanglog.trip.fixture.ShareFixture.TRIP_HAS_SHARED_TRIP; import static hanglog.trip.fixture.TripFixture.LONDON_TRIP; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import hanglog.city.domain.repository.CityRepository; import hanglog.community.domain.PublishedTrip; -import hanglog.community.domain.type.PublishedStatusType; +import hanglog.community.domain.repository.PublishedTripRepository; import hanglog.global.exception.BadRequestException; import hanglog.member.domain.repository.MemberRepository; -import hanglog.share.domain.type.SharedStatusType; import hanglog.trip.domain.DayLog; import hanglog.trip.domain.Trip; -import hanglog.trip.domain.TripCity; -import hanglog.trip.domain.repository.PublishedTripRepository; +import hanglog.trip.domain.repository.CustomDayLogRepository; +import hanglog.trip.domain.repository.CustomTripCityRepository; +import hanglog.trip.domain.repository.SharedTripRepository; import hanglog.trip.domain.repository.TripCityRepository; import hanglog.trip.domain.repository.TripRepository; +import hanglog.trip.domain.type.PublishedStatusType; +import hanglog.trip.domain.type.SharedStatusType; import hanglog.trip.dto.request.PublishedStatusRequest; +import hanglog.trip.dto.request.SharedStatusRequest; import hanglog.trip.dto.request.TripCreateRequest; import hanglog.trip.dto.request.TripUpdateRequest; +import hanglog.trip.dto.response.SharedCodeResponse; import hanglog.trip.dto.response.TripDetailResponse; import java.time.LocalDate; import java.util.ArrayList; @@ -41,6 +49,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.transaction.annotation.Transactional; @ExtendWith(MockitoExtension.class) @@ -53,6 +62,9 @@ class TripServiceTest { @Mock private TripRepository tripRepository; + @Mock + private SharedTripRepository sharedTripRepository; + @Mock private CityRepository cityRepository; @@ -65,6 +77,15 @@ class TripServiceTest { @Mock private PublishedTripRepository publishedTripRepository; + @Mock + private CustomDayLogRepository customDayLogRepository; + + @Mock + private CustomTripCityRepository customTripCityRepository; + + @Mock + private ApplicationEventPublisher publisher; + @DisplayName("MemberId와 TripId로 여행이 존재하는지 검증한다.") @Test void validateTripByMember() { @@ -88,14 +109,13 @@ void save() { List.of(1L, 2L) ); - given(cityRepository.findById(1L)) - .willReturn(Optional.of(LONDON)); - given(cityRepository.findById(2L)) - .willReturn(Optional.of(PARIS)); - given(tripRepository.save(any(Trip.class))) - .willReturn(LONDON_TRIP); given(memberRepository.findById(anyLong())) .willReturn(Optional.of(MEMBER)); + given(tripRepository.save(any(Trip.class))) + .willReturn(LONDON_TRIP); + given(cityRepository.findCitiesByIds(anyList())) + .willReturn(List.of(PARIS, LONDON)); + doNothing().when(customDayLogRepository).saveAll(any()); // when final Long actualId = tripService.save(MEMBER.getId(), tripCreateRequest); @@ -115,12 +135,10 @@ void save_UnCorrectCites() { invalidCities ); - given(cityRepository.findById(1L)) - .willReturn(Optional.of(LONDON)); - given(cityRepository.findById(3L)) - .willThrow(new BadRequestException(NOT_FOUND_TRIP_ID)); given(memberRepository.findById(anyLong())) .willReturn(Optional.of(MEMBER)); + given(cityRepository.findCitiesByIds(anyList())) + .willThrow(new BadRequestException(NOT_FOUND_TRIP_ID)); // when & then assertThatThrownBy(() -> tripService.save(MEMBER.getId(), tripCreateRequest)) @@ -136,8 +154,8 @@ void getTrip() { given(tripRepository.findById(1L)) .willReturn(Optional.of(LONDON_TRIP)); - given(tripCityRepository.findByTripId(1L)) - .willReturn(List.of(new TripCity(LONDON_TRIP, PARIS), new TripCity(LONDON_TRIP, LONDON))); + given(cityRepository.findCitiesByTripId(anyLong())) + .willReturn(List.of(PARIS, LONDON)); // when final TripDetailResponse actual = tripService.getTripDetail(1L); @@ -153,7 +171,7 @@ void update() { // given final TripUpdateRequest updateRequest = new TripUpdateRequest( "변경된 타이틀", - "https://hanglog.com/img/default-image.png", + "default-image.png", LocalDate.of(2023, 7, 2), LocalDate.of(2023, 7, 5), "추가된 여행 설명", @@ -166,6 +184,7 @@ void update() { .willReturn(Optional.of(PARIS)); given(cityRepository.findById(2L)) .willReturn(Optional.of(LONDON)); + doNothing().when(customTripCityRepository).saveAll(any(), anyLong()); // when tripService.update(LONDON_TRIP.getId(), updateRequest); @@ -181,8 +200,7 @@ void delete_InvalidTripId() { // given final Long invalidTripId = 2L; - given(tripRepository.findById(invalidTripId)) - .willThrow(new BadRequestException(NOT_FOUND_TRIP_ID)); + given(tripRepository.existsById(invalidTripId)).willReturn(false); // when & then assertThatThrownBy(() -> tripService.delete(invalidTripId)) @@ -191,6 +209,50 @@ void delete_InvalidTripId() { .isEqualTo(1001); } + @DisplayName("여행의 공유 허용상태로 변경한다.") + @Test + void updateSharedStatus() { + // given + final SharedStatusRequest sharedStatusRequest = new SharedStatusRequest(true); + given(tripRepository.findById(anyLong())) + .willReturn(Optional.of(TRIP_HAS_SHARED_TRIP)); + + // when + final SharedCodeResponse actual = tripService.updateSharedTripStatus(1L, sharedStatusRequest); + + //then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(new SharedCodeResponse(SHARED_TRIP.getSharedCode())); + } + + @DisplayName("공유 허용을 처음 할 경우 새로운 공유 code를 생성한다.") + @Test + void updateSharedStatus_CreateSharedTrip() { + // given + final SharedStatusRequest sharedStatusRequest = new SharedStatusRequest(true); + given(tripRepository.findById(anyLong())) + .willReturn(Optional.of(TRIP_HAS_SHARED_TRIP)); + + // when + final SharedCodeResponse actual = tripService.updateSharedTripStatus(1L, sharedStatusRequest); + + //then + assertThat(actual.getSharedCode()).isNotNull(); + } + + @DisplayName("존재하지 않는 여행의 공유 상태 변경은 예외처리한다.") + @Test + void updateSharedStatus_NotExistTripFail() { + // given + final SharedStatusRequest sharedStatusRequest = new SharedStatusRequest(true); + + // when & then + assertThatThrownBy(() -> tripService.updateSharedTripStatus(1L, sharedStatusRequest)) + .isInstanceOf(BadRequestException.class) + .extracting("code") + .isEqualTo(NOT_FOUND_TRIP_ID.getCode()); + } + @DisplayName("비공개인 Trip을 공개로 변경한다.") @Test void updatePublishedStatus_FirstPublished() { @@ -198,15 +260,13 @@ void updatePublishedStatus_FirstPublished() { LONDON_TRIP.changePublishedStatus(false); given(tripRepository.findById(LONDON_TRIP.getId())) .willReturn(Optional.of(LONDON_TRIP)); - given(publishedTripRepository.existsByTripId(LONDON_TRIP.getId())) - .willReturn(false); + final PublishedStatusRequest publishedStatusRequest = new PublishedStatusRequest(true); // when tripService.updatePublishedStatus(LONDON_TRIP.getId(), publishedStatusRequest); // then - verify(publishedTripRepository).save(any(PublishedTrip.class)); assertThat(LONDON_TRIP.getPublishedStatus()).isEqualTo(PublishedStatusType.PUBLISHED); } @@ -215,12 +275,10 @@ void updatePublishedStatus_FirstPublished() { void updatePublishedStatus_NotFirstPublished() { // given LONDON_TRIP.changePublishedStatus(false); - final PublishedTrip publishedTrip = new PublishedTrip(1L, LONDON_TRIP); + final PublishedTrip publishedTrip = new PublishedTrip(1L, LONDON_TRIP.getId()); publishedTrip.changeStatusToDeleted(); given(tripRepository.findById(LONDON_TRIP.getId())) .willReturn(Optional.of(LONDON_TRIP)); - given(publishedTripRepository.existsByTripId(LONDON_TRIP.getId())) - .willReturn(true); final PublishedStatusRequest publishedStatusRequest = new PublishedStatusRequest(true); // when @@ -268,7 +326,7 @@ void setUp() { 2L, MEMBER, "파리 여행", - "https://hanglog.com/img/default-image.png", + "default-image.png", LocalDate.of(2023, 7, 1), LocalDate.of(2023, 7, 3), "", @@ -282,7 +340,7 @@ void setUp() { void changeDate(final int startDay, final int endDay) { updateRequest = new TripUpdateRequest( "변경된 타이틀", - "https://hanglog.com/img/default-image.png", + "default-image.png", LocalDate.of(2023, 7, startDay), LocalDate.of(2023, 7, endDay), "추가된 여행 설명", @@ -292,7 +350,7 @@ void changeDate(final int startDay, final int endDay) { final Trip updatedTrip = new Trip( trip.getId(), MEMBER, - updateRequest.getImageUrl(), + updateRequest.getImageName(), updateRequest.getTitle(), updateRequest.getStartDate(), updateRequest.getEndDate(), @@ -326,7 +384,10 @@ void update_SamePeriod() { assertSoftly( softly -> { softly.assertThat(trip.getDayLogs().size()).isEqualTo(4); - softly.assertThat(trip.getDayLogs()).containsExactly(dayLog1, dayLog2, dayLog3, extraDayLog); + softly.assertThat(trip.getDayLogs()) + .usingRecursiveComparison() + .ignoringCollectionOrder() + .isEqualTo(List.of(dayLog1, dayLog2, dayLog3, extraDayLog)); } ); } @@ -365,7 +426,10 @@ void update_DecreasePeriod() { assertSoftly( softly -> { softly.assertThat(actualDayLogs.size()).isEqualTo(3); - softly.assertThat(actualDayLogs).containsExactly(dayLog1, dayLog2, extraDayLog); + softly.assertThat(actualDayLogs) + .usingRecursiveComparison() + .ignoringCollectionOrder() + .isEqualTo(List.of(dayLog1, dayLog2, extraDayLog)); } ); } @@ -410,6 +474,7 @@ void update_IncreasePeriod() { softly.assertThat(actualDayLogs) .usingRecursiveComparison() .ignoringFields("trip") + .ignoringCollectionOrder() .isEqualTo(List.of( dayLog1, dayLog2, diff --git a/frontend/config/webpack.common.js b/frontend/config/webpack.common.js index 3fe40ac64..8b2b784e1 100644 --- a/frontend/config/webpack.common.js +++ b/frontend/config/webpack.common.js @@ -3,6 +3,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const Dotenv = require('dotenv-webpack'); const webpack = require('webpack'); const { convertToAbsolutePath } = require('./webpackUtil'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); module.exports = { entry: convertToAbsolutePath('src/index.tsx'), @@ -11,7 +12,10 @@ module.exports = { { test: /\.(js|jsx|ts|tsx)$/, exclude: /node_modules/, - use: ['ts-loader'], + loader: 'esbuild-loader', + options: { + target: 'es2021', + }, }, { test: /\.svg$/i, @@ -67,5 +71,6 @@ module.exports = { favicon: convertToAbsolutePath('public/favicon.ico'), }), new Dotenv(), + new ForkTsCheckerWebpackPlugin(), ], }; diff --git a/frontend/cypress/e2e/trip.cy.ts b/frontend/cypress/e2e/trip.cy.ts index c2c2b1e6f..d34b4a6bb 100644 --- a/frontend/cypress/e2e/trip.cy.ts +++ b/frontend/cypress/e2e/trip.cy.ts @@ -57,13 +57,9 @@ describe('여행 수정 페이지', () => { cy.get('li[role="tab"]').first().click(); cy.fixture('trip.json').then((expectedData) => { - const { title, items } = expectedData.dayLogs[0]; + const { title } = expectedData.dayLogs[0]; cy.findByPlaceholderText('소제목').should('have.value', title); - - items.forEach((item: TripItemData) => { - cy.get('p').contains(item.title); - }); }); }); @@ -161,11 +157,10 @@ describe('여행 정보 수정', () => { cy.fixture('trip.json').then((expectedData) => { cy.findByRole('dialog').find('img').should('exist'); - cy.findByRole('dialog').find(`img[src="${expectedData.imageUrl}"]`).should('not.exist'); + cy.findByRole('dialog').find(`img[src="${expectedData.imageName}"]`).should('not.exist'); }); }); - // 로컬에서는 잘 돌아가는데, github action으로는 안 됨.... it.skip('여행 정보 수정 모달에서 여행 정보를 수정하면 변경된 여행 정보를 여행 정보 수정 페이지에서 볼 수 있다.', () => { cy.findByText('여행 정보 수정').click(); diff --git a/frontend/cypress/fixtures/communityTrips.json b/frontend/cypress/fixtures/communityTrips.json index ae41147d0..4b2e9f495 100644 --- a/frontend/cypress/fixtures/communityTrips.json +++ b/frontend/cypress/fixtures/communityTrips.json @@ -2,7 +2,7 @@ { "id": 1, "title": "런던1 여행2", - "imageUrl": "https://res.klook.com/image/upload/Mobile/City/n9sn4fajwa1skldmdeex.jpg", + "imageName": "default-image.png", "authorNickname": "양파쿵야", "cities": [ { @@ -23,7 +23,7 @@ { "id": 2, "title": "파리2 여행", - "imageUrl": "https://images.squarespace-cdn.com/content/v1/586ebc34d482e9c69268b69a/1624386887478-9Z3XA27D8WFVDWKW00QS/20201230173806551_JRT8E1VC.png", + "imageName": "default-image.png", "authorNickname": "양파쿵야", "cities": [ { @@ -44,7 +44,7 @@ { "id": 3, "title": "서울 여행4", - "imageUrl": "https://a.cdn-hotels.com/gdcs/production144/d992/418cd5c1-7f91-4c44-9f39-3016b033eaa1.jpg?impolicy=fcrop&w=800&h=533&q=medium", + "imageName": "default-image.png", "authorNickname": "양파쿵야", "cities": [ { @@ -65,7 +65,7 @@ { "id": 4, "title": "도쿄 여행6", - "imageUrl": "https://www.jamonfive.com/files/attach/images/1032/015/002/aa092a675ca23b89e818562389d62c12.jpg", + "imageName": "default-image.png", "authorNickname": "양파쿵야", "cities": [ { @@ -86,7 +86,7 @@ { "id": 5, "title": "런던 여행7", - "imageUrl": "https://www.discoverlosangeles.com/sites/default/files/images/2023-02/IMG_0410-Edit-3.jpg?width=1600&height=1200&fit=crop&quality=78&auto=webp", + "imageName": "default-image.png", "authorNickname": "양파쿵야", "cities": [ { @@ -107,7 +107,7 @@ { "id": 6, "title": "런던 여행142", - "imageUrl": "https://res.klook.com/image/upload/Mobile/City/n9sn4fajwa1skldmdeex.jpg", + "imageName": "default-image.png", "authorNickname": "양파쿵야", "cities": [ { @@ -128,7 +128,7 @@ { "id": 7, "title": "파리 여행23445", - "imageUrl": "https://images.squarespace-cdn.com/content/v1/586ebc34d482e9c69268b69a/1624386887478-9Z3XA27D8WFVDWKW00QS/20201230173806551_JRT8E1VC.png", + "imageName": "default-image.png", "authorNickname": "양파쿵야", "cities": [ { @@ -149,7 +149,7 @@ { "id": 8, "title": "서울2345 여행", - "imageUrl": "https://a.cdn-hotels.com/gdcs/production144/d992/418cd5c1-7f91-4c44-9f39-3016b033eaa1.jpg?impolicy=fcrop&w=800&h=533&q=medium", + "imageName": "default-image.png", "authorNickname": "양파쿵야", "cities": [ { @@ -170,7 +170,7 @@ { "id": 9, "title": "도쿄 여행5678", - "imageUrl": "https://www.jamonfive.com/files/attach/images/1032/015/002/aa092a675ca23b89e818562389d62c12.jpg", + "imageName": "default-image.png", "authorNickname": "양파쿵야", "cities": [ { @@ -191,7 +191,7 @@ { "id": 10, "title": "런던 여행4567458", - "imageUrl": "https://www.discoverlosangeles.com/sites/default/files/images/2023-02/IMG_0410-Edit-3.jpg?width=1600&height=1200&fit=crop&quality=78&auto=webp", + "imageName": "default-image.png", "authorNickname": "양파쿵야", "cities": [ { diff --git a/frontend/cypress/fixtures/recommendedTrips.json b/frontend/cypress/fixtures/recommendedTrips.json index 1dcdb614d..d1677338d 100644 --- a/frontend/cypress/fixtures/recommendedTrips.json +++ b/frontend/cypress/fixtures/recommendedTrips.json @@ -4,7 +4,7 @@ { "id": 1, "title": "런던 여행", - "imageUrl": "https://res.klook.com/image/upload/Mobile/City/n9sn4fajwa1skldmdeex.jpg", + "imageName": "default-image.png", "authorNickname": "양파쿵야", "cities": [ { @@ -18,14 +18,14 @@ ], "startDate": "2023-06-13", "endDate": "2023-06-20", - "description": "어쩌구 저쩌구 좌충우돌좌충우돌좌충우돌좌충우돌좌충우돌좌충우돌 라곤의 런던 여행기", + "description": "어쩌구 저쩌구", "likeCount": 123, "isLike": true }, { "id": 2, "title": "파리 여행", - "imageUrl": "https://images.squarespace-cdn.com/content/v1/586ebc34d482e9c69268b69a/1624386887478-9Z3XA27D8WFVDWKW00QS/20201230173806551_JRT8E1VC.png", + "imageName": "default-image.png", "authorNickname": "양파쿵야", "cities": [ { @@ -46,7 +46,7 @@ { "id": 3, "title": "서울 여행", - "imageUrl": "https://a.cdn-hotels.com/gdcs/production144/d992/418cd5c1-7f91-4c44-9f39-3016b033eaa1.jpg?impolicy=fcrop&w=800&h=533&q=medium", + "imageName": "default-image.png", "authorNickname": "양파쿵야", "cities": [ { @@ -67,7 +67,7 @@ { "id": 4, "title": "도쿄 여행", - "imageUrl": "https://www.jamonfive.com/files/attach/images/1032/015/002/aa092a675ca23b89e818562389d62c12.jpg", + "imageName": "default-image.png", "authorNickname": "양파쿵야", "cities": [ { @@ -88,7 +88,7 @@ { "id": 5, "title": "런던2 여행3", - "imageUrl": "https://www.discoverlosangeles.com/sites/default/files/images/2023-02/IMG_0410-Edit-3.jpg?width=1600&height=1200&fit=crop&quality=78&auto=webp", + "imageName": "default-image.png", "authorNickname": "양파쿵야", "cities": [ { diff --git a/frontend/cypress/fixtures/trip.json b/frontend/cypress/fixtures/trip.json index cd8d36236..59c201188 100644 --- a/frontend/cypress/fixtures/trip.json +++ b/frontend/cypress/fixtures/trip.json @@ -4,7 +4,7 @@ "startDate": "2023-07-01", "endDate": "2023-07-03", "description": "라곤의 좌충우돌 유럽 여행기", - "imageUrl": "https://a.cdn-hotels.com/gdcs/production153/d1371/e6c1f55e-51ac-41d5-8c63-2d0c63faf59e.jpg", + "imageName": "default-image.png", "cities": [ { "id": 1, @@ -58,12 +58,7 @@ "name": "관광" } }, - "imageUrls": [ - "https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg", - "https://e3.365dm.com/17/10/2048x1152/skynews-piccadilly-piccadilly-circus_4131587.jpg", - "https://dynamic-media-cdn.tripadvisor.com/media/photo-o/15/9e/a5/6f/regent-str.jpg?w=1200&h=-1&s=1", - "https://flashbak.com/wp-content/uploads/2017/01/Piccadilly-by-Night-London-1960-by-Elmar-Ludwig.jpg" - ] + "imageNames": ["default-image2.png", "default-image.png"] }, { "id": 2, @@ -83,7 +78,7 @@ } }, "expense": null, - "imageUrls": ["https://pbs.twimg.com/media/D8sOPlgXUAACySN.jpg"] + "imageNames": ["default-image.png"] }, { "id": 3, @@ -102,18 +97,18 @@ "name": "교통" } }, - "imageUrls": [] + "imageNames": [] }, { "id": 10, "itemType": true, - "title": "covertGarden", + "title": "default-image.png", "ordinal": 1, "rating": 4.5, "memo": null, "place": { "id": 1, - "name": "covertGarden", + "name": "default-image.png", "latitude": 51.5117, "longitude": -0.1226, "category": { @@ -130,9 +125,7 @@ "name": "음식" } }, - "imageUrls": [ - "https://lh3.googleusercontent.com/p/AF1QipOirgDoMN4u1mwho_3Nh_4hS6IehGlewiHtcQLI=s1360-w1360-h1020" - ] + "imageNames": ["default-image.png"] } ] }, @@ -168,9 +161,7 @@ "name": "음식" } }, - "imageUrls": [ - "https://lh3.googleusercontent.com/p/AF1QipOirgDoMN4u1mwho_3Nh_4hS6IehGlewiHtcQLI=s1360-w1360-h1020" - ] + "imageNames": ["default-image.png"] }, { "id": 4, @@ -189,9 +180,7 @@ "name": "교통" } }, - "imageUrls": [ - "https://www.thetrainline.com/cms/media/4626/southeastern-javelin-train-st-pancras-intl-desktop_1x.jpg" - ] + "imageNames": ["default-image.png"] } ] }, diff --git a/frontend/cypress/fixtures/trips.json b/frontend/cypress/fixtures/trips.json index 9c5dffe9f..61f1a5bc3 100644 --- a/frontend/cypress/fixtures/trips.json +++ b/frontend/cypress/fixtures/trips.json @@ -2,7 +2,7 @@ { "id": 1, "title": "런던 여행", - "imageUrl": "https://res.klook.com/image/upload/Mobile/City/n9sn4fajwa1skldmdeex.jpg", + "imageName": "default-image.png", "cities": [ { "id": 1, @@ -24,7 +24,7 @@ { "id": 2, "title": "서울 여행", - "imageUrl": "https://images.squarespace-cdn.com/content/v1/586ebc34d482e9c69268b69a/1624386887478-9Z3XA27D8WFVDWKW00QS/20201230173806551_JRT8E1VC.png", + "imageName": "default-image.png", "cities": [ { "id": 2, @@ -40,7 +40,7 @@ { "id": 3, "title": "부산 여행", - "imageUrl": "https://a.cdn-hotels.com/gdcs/production144/d992/418cd5c1-7f91-4c44-9f39-3016b033eaa1.jpg?impolicy=fcrop&w=800&h=533&q=medium", + "imageName": "default-image.png", "cities": [ { "id": 3, @@ -56,7 +56,7 @@ { "id": 4, "title": "바르셀로나 여행", - "imageUrl": "https://www.jamonfive.com/files/attach/images/1032/015/002/aa092a675ca23b89e818562389d62c12.jpg", + "imageName": "default-image.png", "cities": [ { "id": 2, @@ -78,7 +78,7 @@ { "id": 5, "title": "로스앤젤레스 여행", - "imageUrl": "https://www.discoverlosangeles.com/sites/default/files/images/2023-02/IMG_0410-Edit-3.jpg?width=1600&height=1200&fit=crop&quality=78&auto=webp", + "imageName": "default-image.png", "cities": [ { "id": 6, @@ -100,7 +100,7 @@ { "id": 6, "title": "리옹 여행", - "imageUrl": null, + "imageName": "default-image.png", "cities": [ { "id": 53, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c7c1f9f14..8c7bf7ddd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -50,6 +50,7 @@ "cross-env": "^7.0.3", "cypress": "^12.17.3", "dotenv-webpack": "^8.0.1", + "esbuild-loader": "^4.0.2", "eslint": "^8.44.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^8.8.0", @@ -59,10 +60,12 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-storybook": "^0.6.13", + "fork-ts-checker-webpack-plugin": "^9.0.0", "html-webpack-plugin": "^5.5.3", "msw": "^1.2.3", "msw-storybook-addon": "^1.8.0", "prettier": "^2.8.8", + "speed-measure-webpack-plugin": "^1.5.0", "storybook": "^7.2.2", "ts-loader": "^9.4.4", "typescript": "^5.1.6", @@ -4644,6 +4647,104 @@ "integrity": "sha512-6sfo1qTulpVbkxECP+AVrHV9OoJqhzCsfTNp5NIG+enM4HyM3HvZCO798WShIXBN0+QtDIcutJCjsVYnQP5rIQ==", "dev": true }, + "node_modules/@storybook/builder-webpack5/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/fork-ts-checker-webpack-plugin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", + "integrity": "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^7.0.1", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@storybook/builder-webpack5/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4671,6 +4772,18 @@ "node": ">=10" } }, + "node_modules/@storybook/builder-webpack5/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@storybook/builder-webpack5/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -11064,6 +11177,432 @@ "@esbuild/win32-x64": "0.18.20" } }, + "node_modules/esbuild-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-4.0.2.tgz", + "integrity": "sha512-kj88m0yrtTEJDeUEF+3TZsq7t9VPzQQj7UmXAzUbIaipoYSrd0UxKAcg4l9CBgP8uVoploiw+nKr8DIv6Y9gXw==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.0", + "get-tsconfig": "^4.7.0", + "loader-utils": "^2.0.4", + "webpack-sources": "^1.4.3" + }, + "funding": { + "url": "https://github.com/esbuild-kit/esbuild-loader?sponsor=1" + }, + "peerDependencies": { + "webpack": "^4.40.0 || ^5.0.0" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/android-arm": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.4.tgz", + "integrity": "sha512-uBIbiYMeSsy2U0XQoOGVVcpIktjLMEKa7ryz2RLr7L/vTnANNEsPVAh4xOv7ondGz6ac1zVb0F8Jx20rQikffQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/android-arm64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.4.tgz", + "integrity": "sha512-mRsi2vJsk4Bx/AFsNBqOH2fqedxn5L/moT58xgg51DjX1la64Z3Npicut2VbhvDFO26qjWtPMsVxCd80YTFVeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/android-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.4.tgz", + "integrity": "sha512-4iPufZ1TMOD3oBlGFqHXBpa3KFT46aLl6Vy7gwed0ZSYgHaZ/mihbYb4t7Z9etjkC9Al3ZYIoOaHrU60gcMy7g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.4.tgz", + "integrity": "sha512-Lviw8EzxsVQKpbS+rSt6/6zjn9ashUZ7Tbuvc2YENgRl0yZTktGlachZ9KMJUsVjZEGFVu336kl5lBgDN6PmpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/darwin-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.4.tgz", + "integrity": "sha512-YHbSFlLgDwglFn0lAO3Zsdrife9jcQXQhgRp77YiTDja23FrC2uwnhXMNkAucthsf+Psr7sTwYEryxz6FPAVqw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.4.tgz", + "integrity": "sha512-vz59ijyrTG22Hshaj620e5yhs2dU1WJy723ofc+KUgxVCM6zxQESmWdMuVmUzxtGqtj5heHyB44PjV/HKsEmuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.4.tgz", + "integrity": "sha512-3sRbQ6W5kAiVQRBWREGJNd1YE7OgzS0AmOGjDmX/qZZecq8NFlQsQH0IfXjjmD0XtUYqr64e0EKNFjMUlPL3Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/linux-arm": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.4.tgz", + "integrity": "sha512-z/4ArqOo9EImzTi4b6Vq+pthLnepFzJ92BnofU1jgNlcVb+UqynVFdoXMCFreTK7FdhqAzH0vmdwW5373Hm9pg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/linux-arm64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.4.tgz", + "integrity": "sha512-ZWmWORaPbsPwmyu7eIEATFlaqm0QGt+joRE9sKcnVUG3oBbr/KYdNE2TnkzdQwX6EDRdg/x8Q4EZQTXoClUqqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/linux-ia32": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.4.tgz", + "integrity": "sha512-EGc4vYM7i1GRUIMqRZNCTzJh25MHePYsnQfKDexD8uPTCm9mK56NIL04LUfX2aaJ+C9vyEp2fJ7jbqFEYgO9lQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/linux-loong64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.4.tgz", + "integrity": "sha512-WVhIKO26kmm8lPmNrUikxSpXcgd6HDog0cx12BUfA2PkmURHSgx9G6vA19lrlQOMw+UjMZ+l3PpbtzffCxFDRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.4.tgz", + "integrity": "sha512-keYY+Hlj5w86hNp5JJPuZNbvW4jql7c1eXdBUHIJGTeN/+0QFutU3GrS+c27L+NTmzi73yhtojHk+lr2+502Mw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.4.tgz", + "integrity": "sha512-tQ92n0WMXyEsCH4m32S21fND8VxNiVazUbU4IUGVXQpWiaAxOBvtOtbEt3cXIV3GEBydYsY8pyeRMJx9kn3rvw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.4.tgz", + "integrity": "sha512-tRRBey6fG9tqGH6V75xH3lFPpj9E8BH+N+zjSUCnFOX93kEzqS0WdyJHkta/mmJHn7MBaa++9P4ARiU4ykjhig==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/linux-s390x": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.4.tgz", + "integrity": "sha512-152aLpQqKZYhThiJ+uAM4PcuLCAOxDsCekIbnGzPKVBRUDlgaaAfaUl5NYkB1hgY6WN4sPkejxKlANgVcGl9Qg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/linux-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.4.tgz", + "integrity": "sha512-Mi4aNA3rz1BNFtB7aGadMD0MavmzuuXNTaYL6/uiYIs08U7YMPETpgNn5oue3ICr+inKwItOwSsJDYkrE9ekVg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.4.tgz", + "integrity": "sha512-9+Wxx1i5N/CYo505CTT7T+ix4lVzEdz0uCoYGxM5JDVlP2YdDC1Bdz+Khv6IbqmisT0Si928eAxbmGkcbiuM/A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.4.tgz", + "integrity": "sha512-MFsHleM5/rWRW9EivFssop+OulYVUoVcqkyOkjiynKBCGBj9Lihl7kh9IzrreDyXa4sNkquei5/DTP4uCk25xw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/sunos-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.4.tgz", + "integrity": "sha512-6Xq8SpK46yLvrGxjp6HftkDwPP49puU4OF0hEL4dTxqCbfx09LyrbUj/D7tmIRMj5D5FCUPksBbxyQhp8tmHzw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/win32-arm64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.4.tgz", + "integrity": "sha512-PkIl7Jq4mP6ke7QKwyg4fD4Xvn8PXisagV/+HntWoDEdmerB2LTukRZg728Yd1Fj+LuEX75t/hKXE2Ppk8Hh1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/win32-ia32": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.4.tgz", + "integrity": "sha512-ga676Hnvw7/ycdKB53qPusvsKdwrWzEyJ+AtItHGoARszIqvjffTwaaW3b2L6l90i7MO9i+dlAW415INuRhSGg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/@esbuild/win32-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.4.tgz", + "integrity": "sha512-HP0GDNla1T3ZL8Ko/SHAS2GgtjOg+VmWnnYLhuTksr++EnduYB0f3Y2LzHsUwb2iQ13JGoY6G3R8h6Du/WG6uA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-loader/node_modules/esbuild": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.4.tgz", + "integrity": "sha512-x7jL0tbRRpv4QUyuDMjONtWFciygUxWaUM1kMX2zWxI0X2YWOt7MSA0g4UdeSiHM8fcYVzpQhKYOycZwxTdZkA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.19.4", + "@esbuild/android-arm64": "0.19.4", + "@esbuild/android-x64": "0.19.4", + "@esbuild/darwin-arm64": "0.19.4", + "@esbuild/darwin-x64": "0.19.4", + "@esbuild/freebsd-arm64": "0.19.4", + "@esbuild/freebsd-x64": "0.19.4", + "@esbuild/linux-arm": "0.19.4", + "@esbuild/linux-arm64": "0.19.4", + "@esbuild/linux-ia32": "0.19.4", + "@esbuild/linux-loong64": "0.19.4", + "@esbuild/linux-mips64el": "0.19.4", + "@esbuild/linux-ppc64": "0.19.4", + "@esbuild/linux-riscv64": "0.19.4", + "@esbuild/linux-s390x": "0.19.4", + "@esbuild/linux-x64": "0.19.4", + "@esbuild/netbsd-x64": "0.19.4", + "@esbuild/openbsd-x64": "0.19.4", + "@esbuild/sunos-x64": "0.19.4", + "@esbuild/win32-arm64": "0.19.4", + "@esbuild/win32-ia32": "0.19.4", + "@esbuild/win32-x64": "0.19.4" + } + }, + "node_modules/esbuild-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esbuild-loader/node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, "node_modules/esbuild-plugin-alias": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/esbuild-plugin-alias/-/esbuild-plugin-alias-0.2.1.tgz", @@ -12496,9 +13035,9 @@ } }, "node_modules/fork-ts-checker-webpack-plugin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", - "integrity": "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.0.tgz", + "integrity": "sha512-Kw3JjsfGs0piB0V2Em8gCuo51O3p4KyCOK0Tn8X57oq2mSNBrMmONALRBw5frcmWsOVU7iELXXsJ+FVxJeQuhA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.16.7", @@ -12881,6 +13420,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/getos": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", @@ -17971,6 +18522,15 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -18578,6 +19138,12 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -18686,6 +19252,73 @@ "wbuf": "^1.7.3" } }, + "node_modules/speed-measure-webpack-plugin": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.5.0.tgz", + "integrity": "sha512-Re0wX5CtM6gW7bZA64ONOfEPEhwbiSF/vz6e2GvadjuaPrQcHTQdRGsD8+BE7iUOysXH8tIenkPCQBEcspXsNg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "webpack": "^1 || ^2 || ^3 || ^4 || ^5" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/speed-measure-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -24277,6 +24910,77 @@ "integrity": "sha512-6sfo1qTulpVbkxECP+AVrHV9OoJqhzCsfTNp5NIG+enM4HyM3HvZCO798WShIXBN0+QtDIcutJCjsVYnQP5rIQ==", "dev": true }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "fork-ts-checker-webpack-plugin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", + "integrity": "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^7.0.1", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "dependencies": { + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -24295,6 +24999,15 @@ "lru-cache": "^6.0.0" } }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -29093,6 +29806,220 @@ "@esbuild/win32-x64": "0.18.20" } }, + "esbuild-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-4.0.2.tgz", + "integrity": "sha512-kj88m0yrtTEJDeUEF+3TZsq7t9VPzQQj7UmXAzUbIaipoYSrd0UxKAcg4l9CBgP8uVoploiw+nKr8DIv6Y9gXw==", + "dev": true, + "requires": { + "esbuild": "^0.19.0", + "get-tsconfig": "^4.7.0", + "loader-utils": "^2.0.4", + "webpack-sources": "^1.4.3" + }, + "dependencies": { + "@esbuild/android-arm": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.4.tgz", + "integrity": "sha512-uBIbiYMeSsy2U0XQoOGVVcpIktjLMEKa7ryz2RLr7L/vTnANNEsPVAh4xOv7ondGz6ac1zVb0F8Jx20rQikffQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.4.tgz", + "integrity": "sha512-mRsi2vJsk4Bx/AFsNBqOH2fqedxn5L/moT58xgg51DjX1la64Z3Npicut2VbhvDFO26qjWtPMsVxCd80YTFVeg==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.4.tgz", + "integrity": "sha512-4iPufZ1TMOD3oBlGFqHXBpa3KFT46aLl6Vy7gwed0ZSYgHaZ/mihbYb4t7Z9etjkC9Al3ZYIoOaHrU60gcMy7g==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.4.tgz", + "integrity": "sha512-Lviw8EzxsVQKpbS+rSt6/6zjn9ashUZ7Tbuvc2YENgRl0yZTktGlachZ9KMJUsVjZEGFVu336kl5lBgDN6PmpA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.4.tgz", + "integrity": "sha512-YHbSFlLgDwglFn0lAO3Zsdrife9jcQXQhgRp77YiTDja23FrC2uwnhXMNkAucthsf+Psr7sTwYEryxz6FPAVqw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.4.tgz", + "integrity": "sha512-vz59ijyrTG22Hshaj620e5yhs2dU1WJy723ofc+KUgxVCM6zxQESmWdMuVmUzxtGqtj5heHyB44PjV/HKsEmuQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.4.tgz", + "integrity": "sha512-3sRbQ6W5kAiVQRBWREGJNd1YE7OgzS0AmOGjDmX/qZZecq8NFlQsQH0IfXjjmD0XtUYqr64e0EKNFjMUlPL3Cw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.4.tgz", + "integrity": "sha512-z/4ArqOo9EImzTi4b6Vq+pthLnepFzJ92BnofU1jgNlcVb+UqynVFdoXMCFreTK7FdhqAzH0vmdwW5373Hm9pg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.4.tgz", + "integrity": "sha512-ZWmWORaPbsPwmyu7eIEATFlaqm0QGt+joRE9sKcnVUG3oBbr/KYdNE2TnkzdQwX6EDRdg/x8Q4EZQTXoClUqqA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.4.tgz", + "integrity": "sha512-EGc4vYM7i1GRUIMqRZNCTzJh25MHePYsnQfKDexD8uPTCm9mK56NIL04LUfX2aaJ+C9vyEp2fJ7jbqFEYgO9lQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.4.tgz", + "integrity": "sha512-WVhIKO26kmm8lPmNrUikxSpXcgd6HDog0cx12BUfA2PkmURHSgx9G6vA19lrlQOMw+UjMZ+l3PpbtzffCxFDRg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.4.tgz", + "integrity": "sha512-keYY+Hlj5w86hNp5JJPuZNbvW4jql7c1eXdBUHIJGTeN/+0QFutU3GrS+c27L+NTmzi73yhtojHk+lr2+502Mw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.4.tgz", + "integrity": "sha512-tQ92n0WMXyEsCH4m32S21fND8VxNiVazUbU4IUGVXQpWiaAxOBvtOtbEt3cXIV3GEBydYsY8pyeRMJx9kn3rvw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.4.tgz", + "integrity": "sha512-tRRBey6fG9tqGH6V75xH3lFPpj9E8BH+N+zjSUCnFOX93kEzqS0WdyJHkta/mmJHn7MBaa++9P4ARiU4ykjhig==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.4.tgz", + "integrity": "sha512-152aLpQqKZYhThiJ+uAM4PcuLCAOxDsCekIbnGzPKVBRUDlgaaAfaUl5NYkB1hgY6WN4sPkejxKlANgVcGl9Qg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.4.tgz", + "integrity": "sha512-Mi4aNA3rz1BNFtB7aGadMD0MavmzuuXNTaYL6/uiYIs08U7YMPETpgNn5oue3ICr+inKwItOwSsJDYkrE9ekVg==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.4.tgz", + "integrity": "sha512-9+Wxx1i5N/CYo505CTT7T+ix4lVzEdz0uCoYGxM5JDVlP2YdDC1Bdz+Khv6IbqmisT0Si928eAxbmGkcbiuM/A==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.4.tgz", + "integrity": "sha512-MFsHleM5/rWRW9EivFssop+OulYVUoVcqkyOkjiynKBCGBj9Lihl7kh9IzrreDyXa4sNkquei5/DTP4uCk25xw==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.4.tgz", + "integrity": "sha512-6Xq8SpK46yLvrGxjp6HftkDwPP49puU4OF0hEL4dTxqCbfx09LyrbUj/D7tmIRMj5D5FCUPksBbxyQhp8tmHzw==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.4.tgz", + "integrity": "sha512-PkIl7Jq4mP6ke7QKwyg4fD4Xvn8PXisagV/+HntWoDEdmerB2LTukRZg728Yd1Fj+LuEX75t/hKXE2Ppk8Hh1w==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.4.tgz", + "integrity": "sha512-ga676Hnvw7/ycdKB53qPusvsKdwrWzEyJ+AtItHGoARszIqvjffTwaaW3b2L6l90i7MO9i+dlAW415INuRhSGg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.4.tgz", + "integrity": "sha512-HP0GDNla1T3ZL8Ko/SHAS2GgtjOg+VmWnnYLhuTksr++EnduYB0f3Y2LzHsUwb2iQ13JGoY6G3R8h6Du/WG6uA==", + "dev": true, + "optional": true + }, + "esbuild": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.4.tgz", + "integrity": "sha512-x7jL0tbRRpv4QUyuDMjONtWFciygUxWaUM1kMX2zWxI0X2YWOt7MSA0g4UdeSiHM8fcYVzpQhKYOycZwxTdZkA==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.19.4", + "@esbuild/android-arm64": "0.19.4", + "@esbuild/android-x64": "0.19.4", + "@esbuild/darwin-arm64": "0.19.4", + "@esbuild/darwin-x64": "0.19.4", + "@esbuild/freebsd-arm64": "0.19.4", + "@esbuild/freebsd-x64": "0.19.4", + "@esbuild/linux-arm": "0.19.4", + "@esbuild/linux-arm64": "0.19.4", + "@esbuild/linux-ia32": "0.19.4", + "@esbuild/linux-loong64": "0.19.4", + "@esbuild/linux-mips64el": "0.19.4", + "@esbuild/linux-ppc64": "0.19.4", + "@esbuild/linux-riscv64": "0.19.4", + "@esbuild/linux-s390x": "0.19.4", + "@esbuild/linux-x64": "0.19.4", + "@esbuild/netbsd-x64": "0.19.4", + "@esbuild/openbsd-x64": "0.19.4", + "@esbuild/sunos-x64": "0.19.4", + "@esbuild/win32-arm64": "0.19.4", + "@esbuild/win32-ia32": "0.19.4", + "@esbuild/win32-x64": "0.19.4" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + } + } + }, "esbuild-plugin-alias": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/esbuild-plugin-alias/-/esbuild-plugin-alias-0.2.1.tgz", @@ -30165,9 +31092,9 @@ "dev": true }, "fork-ts-checker-webpack-plugin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", - "integrity": "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.0.tgz", + "integrity": "sha512-Kw3JjsfGs0piB0V2Em8gCuo51O3p4KyCOK0Tn8X57oq2mSNBrMmONALRBw5frcmWsOVU7iELXXsJ+FVxJeQuhA==", "dev": true, "requires": { "@babel/code-frame": "^7.16.7", @@ -30443,6 +31370,15 @@ "get-intrinsic": "^1.1.1" } }, + "get-tsconfig": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "dev": true, + "requires": { + "resolve-pkg-maps": "^1.0.0" + } + }, "getos": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", @@ -34176,6 +35112,12 @@ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true }, + "resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true + }, "restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -34658,6 +35600,12 @@ } } }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -34752,6 +35700,51 @@ "wbuf": "^1.7.3" } }, + "speed-measure-webpack-plugin": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.5.0.tgz", + "integrity": "sha512-Re0wX5CtM6gW7bZA64ONOfEPEhwbiSF/vz6e2GvadjuaPrQcHTQdRGsD8+BE7iUOysXH8tIenkPCQBEcspXsNg==", + "dev": true, + "requires": { + "chalk": "^4.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5ddfe286a..a26b4af86 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -76,6 +76,7 @@ "cross-env": "^7.0.3", "cypress": "^12.17.3", "dotenv-webpack": "^8.0.1", + "esbuild-loader": "^4.0.2", "eslint": "^8.44.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^8.8.0", @@ -85,10 +86,12 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-storybook": "^0.6.13", + "fork-ts-checker-webpack-plugin": "^9.0.0", "html-webpack-plugin": "^5.5.3", "msw": "^1.2.3", "msw-storybook-addon": "^1.8.0", "prettier": "^2.8.8", + "speed-measure-webpack-plugin": "^1.5.0", "storybook": "^7.2.2", "ts-loader": "^9.4.4", "typescript": "^5.1.6", diff --git a/frontend/src/api/interceptors.ts b/frontend/src/api/interceptors.ts index e164d9f5b..feb1f447d 100644 --- a/frontend/src/api/interceptors.ts +++ b/frontend/src/api/interceptors.ts @@ -72,5 +72,5 @@ export const handleAPIError = (error: AxiosError) => { throw new HTTPError(HTTP_STATUS_CODE.INTERNAL_SERVER_ERROR, data.message); } - throw new HTTPError(status, data.message); + throw new HTTPError(status, data.message, data.code); }; diff --git a/frontend/src/assets/svg/empty-like.svg b/frontend/src/assets/svg/empty-like.svg index 01126ae79..c636b96da 100644 --- a/frontend/src/assets/svg/empty-like.svg +++ b/frontend/src/assets/svg/empty-like.svg @@ -1,3 +1,3 @@ - - - + + + \ No newline at end of file diff --git a/frontend/src/components/common/TripInformation/TripButtons/TripButtons.tsx b/frontend/src/components/common/TripInformation/TripButtons/TripButtons.tsx index 82e023614..4b425bb68 100644 --- a/frontend/src/components/common/TripInformation/TripButtons/TripButtons.tsx +++ b/frontend/src/components/common/TripInformation/TripButtons/TripButtons.tsx @@ -12,32 +12,34 @@ import { import TripShareButton from '@components/common/TripInformation/TripShareButton/TripShareButton'; import { useDeleteTripMutation } from '@hooks/api/useDeleteTripMutation'; +import { useTrip } from '@hooks/trip/useTrip'; -import type { TripData } from '@type/trip'; +import type { TripData, TripTypeData } from '@type/trip'; import { PATH } from '@constants/path'; +import { TRIP_TYPE } from '@constants/trip'; import BinIcon from '@assets/svg/bin-icon.svg'; import EditIcon from '@assets/svg/edit-icon.svg'; interface TripButtonsProps { + tripType: TripTypeData; tripId: string; sharedCode: TripData['sharedCode']; - isShared: boolean; - isPublished?: boolean; publishState: boolean; } -export const TripButtons = ({ - tripId, - sharedCode, - isShared, - isPublished = false, - publishState, -}: TripButtonsProps) => { +export const TripButtons = ({ tripType, tripId, sharedCode, publishState }: TripButtonsProps) => { + const isPersonal = tripType === TRIP_TYPE.PERSONAL; + const isPublished = tripType === TRIP_TYPE.PUBLISHED; + const navigate = useNavigate(); const deleteTripMutation = useDeleteTripMutation(); + const { + tripData: { isWriter }, + } = useTrip(tripType, tripId); + const { isOpen: isDeleteModalOpen, close: closeDeleteModal, @@ -49,12 +51,12 @@ export const TripButtons = ({ }; const goToExpensePage = () => { - if (isPublished) { + if (tripType === TRIP_TYPE.PUBLISHED) { navigate(PATH.COMMUNITY_EXPENSE(tripId)); return; } - if (isShared) { + if (tripType === TRIP_TYPE.SHARED) { navigate(PATH.SHARE_EXPENSE(tripId)); return; } @@ -66,7 +68,7 @@ export const TripButtons = ({ deleteTripMutation.mutate( { tripId }, { - onSuccess: () => navigate(PATH.ROOT), + onSuccess: () => navigate(PATH.MY_TRIPS), } ); }; @@ -76,7 +78,7 @@ export const TripButtons = ({ - {!isShared && ( + {isPersonal && ( <> @@ -97,6 +99,11 @@ export const TripButtons = ({ )} + {isPublished && isWriter && ( + + )} ); }; diff --git a/frontend/src/components/common/TripInformation/TripInformation.tsx b/frontend/src/components/common/TripInformation/TripInformation.tsx index 880d7aad1..e16133675 100644 --- a/frontend/src/components/common/TripInformation/TripInformation.tsx +++ b/frontend/src/components/common/TripInformation/TripInformation.tsx @@ -23,9 +23,12 @@ import { useTrip } from '@hooks/trip/useTrip'; import { mediaQueryMobileState } from '@store/mediaQuery'; +import { convertToImageUrl } from '@utils/convertImage'; import { formatDate } from '@utils/formatter'; -import type { TripTypeData } from '@type/trip'; +import type { TripData, TripTypeData } from '@type/trip'; + +import { TRIP_TYPE } from '@constants/trip'; import DefaultThumbnail from '@assets/png/trip-information_default-thumbnail.png'; @@ -33,22 +36,22 @@ interface TripInformationProps { tripType: TripTypeData; tripId: string; isEditable?: boolean; - isShared?: boolean; - isPublished?: boolean; + initialTripData?: TripData; } const TripInformation = ({ isEditable = true, - isShared = false, - isPublished = false, tripId, tripType, + initialTripData, }: TripInformationProps) => { const isMobile = useRecoilValue(mediaQueryMobileState); + const isPublished = tripType === TRIP_TYPE.PUBLISHED; + const { tripData: savedTripData } = useTrip(tripType, tripId); - const { isOpen: isEditModalOpen, close: closeEditModal, open: openEditModal } = useOverlay(); + const tripData = initialTripData || savedTripData; - const { tripData } = useTrip(tripType, tripId); + const { isOpen: isEditModalOpen, close: closeEditModal, open: openEditModal } = useOverlay(); const [likeCount, setLikeCount] = useState(tripData.likeCount); @@ -61,7 +64,10 @@ const TripInformation = ({
- 여행 대표 이미지 + 여행 대표 이미지 @@ -91,7 +97,7 @@ const TripInformation = ({ 작성자 이미지 {tripData.writer.nickname} @@ -114,10 +120,9 @@ const TripInformation = ({ ) : ( )} diff --git a/frontend/src/components/common/TripItem/EditMenu/EditMenu.tsx b/frontend/src/components/common/TripItem/EditMenu/EditMenu.tsx index 2deb54d70..7548958d7 100644 --- a/frontend/src/components/common/TripItem/EditMenu/EditMenu.tsx +++ b/frontend/src/components/common/TripItem/EditMenu/EditMenu.tsx @@ -67,7 +67,7 @@ const EditMenu = ({ tripId, dayLogId, hasImage, imageHeight, ...information }: E } : null, memo: information.memo, - imageUrls: information.imageUrls, + imageNames: information.imageNames, }} /> )} diff --git a/frontend/src/components/common/TripItem/TripItem.tsx b/frontend/src/components/common/TripItem/TripItem.tsx index 5dfc33499..519a5451f 100644 --- a/frontend/src/components/common/TripItem/TripItem.tsx +++ b/frontend/src/components/common/TripItem/TripItem.tsx @@ -26,6 +26,7 @@ import useResizeImage from '@hooks/trip/useResizeImage'; import { mediaQueryMobileState } from '@store/mediaQuery'; +import { convertToImageUrl, convertToImageUrls } from '@utils/convertImage'; import { formatNumberToMoney } from '@utils/formatter'; import type { TripItemData } from '@type/tripItem'; @@ -82,7 +83,7 @@ const TripItem = ({ onDragEnd={isEditable ? handleDragEnd : undefined} >
- {information.imageUrls.length > 0 && ( + {information.imageNames.length > 0 && ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
1} - showDots={information.imageUrls.length > 1} - images={information.imageUrls} + showArrows={information.imageNames.length > 1} + showDots={information.imageNames.length > 1} + images={convertToImageUrls(information.imageNames)} />
)} @@ -130,7 +131,7 @@ const TripItem = ({ 0} + hasImage={information.imageNames.length > 0} imageHeight={height} {...information} /> @@ -143,13 +144,13 @@ const TripItem = ({ height={500} isDraggable={false} showNavigationOnHover={!isMobile} - showArrows={information.imageUrls.length > 1} - showDots={information.imageUrls.length > 1} - images={information.imageUrls} + showArrows={information.imageNames.length > 1} + showDots={information.imageNames.length > 1} + images={convertToImageUrls(information.imageNames)} > - {information.imageUrls.map((imageUrl) => ( + {information.imageNames.map((imageName) => (
- 이미지 + 이미지
))} diff --git a/frontend/src/components/expense/ExpenseCategories/ExpenseCategories.tsx b/frontend/src/components/expense/ExpenseCategories/ExpenseCategories.tsx index 70b1ab0dd..cd7547009 100644 --- a/frontend/src/components/expense/ExpenseCategories/ExpenseCategories.tsx +++ b/frontend/src/components/expense/ExpenseCategories/ExpenseCategories.tsx @@ -7,15 +7,17 @@ import { useExpense } from '@hooks/expense/useExpense'; import { formatDate, formatNumberToMoney } from '@utils/formatter'; -import { CURRENCY_ICON, DEFAULT_CURRENCY } from '@constants/trip'; +import type { TripTypeData } from '@type/trip'; + +import { CURRENCY_ICON, DEFAULT_CURRENCY, TRIP_TYPE } from '@constants/trip'; interface ExpenseCategoriesProps { tripId: string; - isShared: boolean; + tripType: TripTypeData; } -const ExpenseCategories = ({ tripId, isShared }: ExpenseCategoriesProps) => { - const { categoryExpenseData } = useExpense(tripId); +const ExpenseCategories = ({ tripId, tripType }: ExpenseCategoriesProps) => { + const { categoryExpenseData } = useExpense(tripId, tripType); const { selected: selectedCategoryId, handleSelectClick: handleCategoryIdSelectClick } = useSelect(categoryExpenseData.categoryItems[0].category.id); @@ -57,7 +59,7 @@ const ExpenseCategories = ({ tripId, isShared }: ExpenseCategoriesProps) => { ) : ( - + )} ); diff --git a/frontend/src/components/expense/ExpenseCategoryInformation/ExpenseCategoryInformation.tsx b/frontend/src/components/expense/ExpenseCategoryInformation/ExpenseCategoryInformation.tsx index 2f8b31e46..5c8ed6cee 100644 --- a/frontend/src/components/expense/ExpenseCategoryInformation/ExpenseCategoryInformation.tsx +++ b/frontend/src/components/expense/ExpenseCategoryInformation/ExpenseCategoryInformation.tsx @@ -11,14 +11,17 @@ import { useExpense } from '@hooks/expense/useExpense'; import { formatNumberToMoney } from '@utils/formatter'; +import type { TripTypeData } from '@type/trip'; + import { CURRENCY_ICON, DEFAULT_CURRENCY } from '@constants/trip'; interface ExpenseCategoryInformationProps { tripId: string; + tripType: TripTypeData; } -const ExpenseCategoryInformation = ({ tripId }: ExpenseCategoryInformationProps) => { - const { expenseData } = useExpense(tripId); +const ExpenseCategoryInformation = ({ tripId, tripType }: ExpenseCategoryInformationProps) => { + const { expenseData } = useExpense(tripId, tripType); return (
    diff --git a/frontend/src/components/expense/ExpenseDates/ExpenseDates.tsx b/frontend/src/components/expense/ExpenseDates/ExpenseDates.tsx index 8140d6c93..0d0442824 100644 --- a/frontend/src/components/expense/ExpenseDates/ExpenseDates.tsx +++ b/frontend/src/components/expense/ExpenseDates/ExpenseDates.tsx @@ -7,15 +7,17 @@ import { useExpense } from '@hooks/expense/useExpense'; import { formatDate, formatMonthDate, formatNumberToMoney } from '@utils/formatter'; -import { CURRENCY_ICON, DEFAULT_CURRENCY } from '@constants/trip'; +import type { TripTypeData } from '@type/trip'; + +import { CURRENCY_ICON, DEFAULT_CURRENCY, TRIP_TYPE } from '@constants/trip'; interface ExpenseDatesProps { tripId: string; - isShared: boolean; + tripType: TripTypeData; } -const ExpenseDates = ({ tripId, isShared }: ExpenseDatesProps) => { - const { expenseData, dates } = useExpense(tripId); +const ExpenseDates = ({ tripId, tripType }: ExpenseDatesProps) => { + const { expenseData, dates } = useExpense(tripId, tripType); const { selected: selectedDayLogId, handleSelectClick: handleDayLogIdSelectClick } = useSelect( expenseData.dayLogs[0].id @@ -75,7 +77,7 @@ const ExpenseDates = ({ tripId, isShared }: ExpenseDatesProps) => { ) : ( - + )} ); diff --git a/frontend/src/components/expense/ExpenseInformation/ExpenseInformation.tsx b/frontend/src/components/expense/ExpenseInformation/ExpenseInformation.tsx index ea8477eb6..57645c036 100644 --- a/frontend/src/components/expense/ExpenseInformation/ExpenseInformation.tsx +++ b/frontend/src/components/expense/ExpenseInformation/ExpenseInformation.tsx @@ -17,28 +17,30 @@ import { mediaQueryMobileState } from '@store/mediaQuery'; import { formatDate } from '@utils/formatter'; +import type { TripTypeData } from '@type/trip'; + import { PATH } from '@constants/path'; +import { TRIP_TYPE } from '@constants/trip'; interface ExpenseInformationProps { tripId: string; - isShared: boolean; - isPublished: boolean; + tripType: TripTypeData; } -const ExpenseInformation = ({ tripId, isShared, isPublished }: ExpenseInformationProps) => { +const ExpenseInformation = ({ tripId, tripType }: ExpenseInformationProps) => { const navigate = useNavigate(); const isMobile = useRecoilValue(mediaQueryMobileState); - const { expenseData } = useExpense(tripId); + const { expenseData } = useExpense(tripId, tripType); const goToTripPage = () => { - if (isPublished) { + if (tripType === TRIP_TYPE.PUBLISHED) { navigate(PATH.COMMUNITY_TRIP(tripId)); return; } - if (isShared) { + if (tripType === TRIP_TYPE.SHARED) { navigate(PATH.SHARE_TRIP(tripId)); return; } diff --git a/frontend/src/components/expense/ExpenseListSection/ExpenseListSection.tsx b/frontend/src/components/expense/ExpenseListSection/ExpenseListSection.tsx index 71fd9d03c..43b57a3d3 100644 --- a/frontend/src/components/expense/ExpenseListSection/ExpenseListSection.tsx +++ b/frontend/src/components/expense/ExpenseListSection/ExpenseListSection.tsx @@ -11,14 +11,16 @@ import { import { mediaQueryMobileState } from '@store/mediaQuery'; +import type { TripTypeData } from '@type/trip'; + import { EXPENSE_LIST_FILTERS } from '@constants/expense'; interface ExpenseListProps { tripId: string; - isShared: boolean; + tripType: TripTypeData; } -const ExpenseListSection = ({ tripId, isShared }: ExpenseListProps) => { +const ExpenseListSection = ({ tripId, tripType }: ExpenseListProps) => { const isMobile = useRecoilValue(mediaQueryMobileState); const { selected: selectedFilter, handleSelectClick: handleFilterSelectClick } = useSelect( @@ -47,9 +49,9 @@ const ExpenseListSection = ({ tripId, isShared }: ExpenseListProps) => { {selectedFilter === EXPENSE_LIST_FILTERS.DAY_LOG ? ( - + ) : ( - + )} ); diff --git a/frontend/src/components/expense/TotalExpenseSection/TotalExpenseSection.tsx b/frontend/src/components/expense/TotalExpenseSection/TotalExpenseSection.tsx index 909c6808a..28f027d00 100644 --- a/frontend/src/components/expense/TotalExpenseSection/TotalExpenseSection.tsx +++ b/frontend/src/components/expense/TotalExpenseSection/TotalExpenseSection.tsx @@ -16,23 +16,24 @@ import { mediaQueryMobileState } from '@store/mediaQuery'; import { formatNumberToMoney } from '@utils/formatter'; +import type { TripTypeData } from '@type/trip'; + import { CURRENCY_ICON, DEFAULT_CURRENCY } from '@constants/trip'; import { EXPENSE_CATEGORY_CHART_SIZE, EXPENSE_CATEGORY_CHART_STROKE_WIDTH } from '@constants/ui'; interface TotalExpenseSectionProps { tripId: string; - isShared: boolean; - isPublished: boolean; + tripType: TripTypeData; } -const TotalExpenseSection = ({ tripId, isShared, isPublished }: TotalExpenseSectionProps) => { +const TotalExpenseSection = ({ tripId, tripType }: TotalExpenseSectionProps) => { const isMobile = useRecoilValue(mediaQueryMobileState); - const { expenseData, categoryChartData } = useExpense(tripId); + const { expenseData, categoryChartData } = useExpense(tripId, tripType); return (
    - + 총 경비 :{' '} @@ -52,7 +53,7 @@ const TotalExpenseSection = ({ tripId, isShared, isPublished }: TotalExpenseSect strokeWidth={EXPENSE_CATEGORY_CHART_STROKE_WIDTH} /> - +
    ); }; diff --git a/frontend/src/components/layout/Header/Header.tsx b/frontend/src/components/layout/Header/Header.tsx index 26c5bd798..475bca759 100644 --- a/frontend/src/components/layout/Header/Header.tsx +++ b/frontend/src/components/layout/Header/Header.tsx @@ -29,7 +29,7 @@ const Header = () => { return (
    - + { > 커뮤니티 - { inputRef.current?.click(); }; - const handleImageUrlsChange = useCallback( + const handleImageUrlChange = useCallback( (imageUrl: string) => { updateInputValue('imageUrl', imageUrl); }, @@ -57,7 +56,7 @@ const EditUserProfileForm = ({ initialData }: EditUserProfileForm) => { const { isImageUploading, uploadedImageUrl, handleImageUpload } = useSingleImageUpload({ initialImageUrl: initialData.imageUrl, - onSuccess: handleImageUrlsChange, + updateFormImage: handleImageUrlChange, }); return ( diff --git a/frontend/src/components/trip/TripInfoEditModal/ImageInput/ImageInput.tsx b/frontend/src/components/trip/TripInfoEditModal/ImageInput/ImageInput.tsx deleted file mode 100644 index 98066fffa..000000000 --- a/frontend/src/components/trip/TripInfoEditModal/ImageInput/ImageInput.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { useCallback } from 'react'; - -import { ImageUploadInput } from 'hang-log-design-system'; - -import { useMultipleImageUpload } from '@hooks/common/useMultipleImageUpload'; - -interface ImageInputProps { - initialImage: string | null; - updateCoverImage: (imageUrl: string) => void; -} - -const ImageInput = ({ initialImage, updateCoverImage }: ImageInputProps) => { - const handleImageUrlsChange = useCallback( - (imageUrls: string[]) => { - updateCoverImage(imageUrls[0]); - }, - [updateCoverImage] - ); - - const { - uploadedImageUrls: uploadedImageUrl, - handleImageUpload, - handleImageRemoval, - } = useMultipleImageUpload({ - initialImageUrls: initialImage === null ? [] : [initialImage], - onSuccess: handleImageUrlsChange, - }); - - return ( - - ); -}; - -export default ImageInput; diff --git a/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.style.ts b/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.style.ts index a2bca61af..7822709be 100644 --- a/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.style.ts +++ b/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.style.ts @@ -7,7 +7,7 @@ export const wrapperStyling = css({ '@media screen and (max-width: 600px)': { width: `calc(100vw - ${Theme.spacer.spacing4})`, - height: `calc(100vh - ${Theme.spacer.spacing9})`, + height: '80vh', }, }); @@ -17,12 +17,9 @@ export const formStyling = css({ flexDirection: 'column', gap: Theme.spacer.spacing3, - '> button': { - width: '100%', - }, - '@media screen and (max-width: 600px)': { width: `calc(100vw - ${Theme.spacer.spacing7})`, + marginBottom: Theme.spacer.spacing6, overflowY: 'auto', '-ms-overflow-style': 'none', @@ -54,3 +51,13 @@ export const textareaStyling = css({ resize: 'none', fontFamily: 'none', }); + +export const buttonStyling = css({ + width: '100%', + + '@media screen and (max-width: 600px)': { + position: 'absolute', + width: '89%', + bottom: Theme.spacer.spacing3, + }, +}); diff --git a/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.tsx b/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.tsx index fb7940c61..65b2f9807 100644 --- a/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.tsx +++ b/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.tsx @@ -1,15 +1,26 @@ -import { Button, Flex, Input, Modal, SupportingText, Textarea } from 'hang-log-design-system'; +import { useCallback } from 'react'; + +import { + Button, + Flex, + ImageUploadInput, + Input, + Modal, + SupportingText, + Textarea, +} from 'hang-log-design-system'; import CitySearchBar from '@components/common/CitySearchBar/CitySearchBar'; import DateInput from '@components/common/DateInput/DateInput'; -import ImageInput from '@components/trip/TripInfoEditModal/ImageInput/ImageInput'; import { + buttonStyling, dateInputSupportingText, formStyling, textareaStyling, wrapperStyling, } from '@components/trip/TripInfoEditModal/TripInfoEditModal.style'; +import { useMultipleImageUpload } from '@hooks/common/useMultipleImageUpload'; import { useTripEditForm } from '@hooks/trip/useTripEditForm'; import type { TripData } from '@type/trip'; @@ -35,6 +46,25 @@ const TripInfoEditModal = ({ isOpen, onClose, ...information }: TripInfoEditModa handleSubmit, } = useTripEditForm(information, onClose); + const handleImageNameChange = useCallback( + (imageNames: string[]) => { + if (imageNames.length === 0) { + updateCoverImage(null); + return; + } + + updateCoverImage(imageNames[0]); + }, + [updateCoverImage] + ); + + const { imageUrls, isImageUploading, handleImageUpload, handleImageRemoval } = + useMultipleImageUpload({ + initialImageNames: information.imageName === null ? [] : [information.imageName], + updateFormImage: handleImageNameChange, + maxUploadCount: 1, + }); + return ( - - + + ); diff --git a/frontend/src/components/trip/TripItemAddModal/TripItemAddModal.style.ts b/frontend/src/components/trip/TripItemAddModal/TripItemAddModal.style.ts index 805e26353..2cc08e949 100644 --- a/frontend/src/components/trip/TripItemAddModal/TripItemAddModal.style.ts +++ b/frontend/src/components/trip/TripItemAddModal/TripItemAddModal.style.ts @@ -8,7 +8,7 @@ export const wrapperStyling = css({ '@media screen and (max-width: 600px)': { width: `calc(100vw - ${Theme.spacer.spacing4})`, - height: `calc(100vh - ${Theme.spacer.spacing9})`, + height: `80%`, }, }); @@ -17,12 +17,9 @@ export const formStyling = css({ flexDirection: 'column', gap: Theme.spacer.spacing4, - '& > button': { - width: '100%', - }, - '@media screen and (max-width: 600px)': { width: `calc(100vw - ${Theme.spacer.spacing7})`, + marginBottom: Theme.spacer.spacing6, overflowY: 'auto', '-ms-overflow-style': 'none', @@ -35,3 +32,13 @@ export const formStyling = css({ }, }, }); + +export const buttonStyling = css({ + width: '100%', + + '@media screen and (max-width: 600px)': { + position: 'absolute', + width: '89%', + bottom: Theme.spacer.spacing3, + }, +}); diff --git a/frontend/src/components/trip/TripItemAddModal/TripItemAddModal.tsx b/frontend/src/components/trip/TripItemAddModal/TripItemAddModal.tsx index 04df1a7a6..1b8063925 100644 --- a/frontend/src/components/trip/TripItemAddModal/TripItemAddModal.tsx +++ b/frontend/src/components/trip/TripItemAddModal/TripItemAddModal.tsx @@ -13,6 +13,7 @@ import PlaceInput from '@components/trip/TripItemAddModal/PlaceInput/PlaceInput' import StarRatingInput from '@components/trip/TripItemAddModal/StarRatingInput/StarRatingInput'; import TitleInput from '@components/trip/TripItemAddModal/TitleInput/TitleInput'; import { + buttonStyling, formStyling, wrapperStyling, } from '@components/trip/TripItemAddModal/TripItemAddModal.style'; @@ -59,9 +60,9 @@ const TripItemAddModal = ({ onSuccess: onClose, }); - const handleImageUrlsChange = useCallback( - (imageUrls: string[]) => { - updateInputValue('imageUrls', imageUrls); + const handleImageNamesChange = useCallback( + (imageNames: string[]) => { + updateInputValue('imageNames', imageNames); }, [updateInputValue] ); @@ -70,22 +71,34 @@ const TripItemAddModal = ({ createToast('이미지는 최대 5개 업로드할 수 있습니다.'); }; - const { isImageUploading, uploadedImageUrls, handleImageUpload, handleImageRemoval } = + const { isImageUploading, imageUrls, handleImageUpload, handleImageRemoval } = useMultipleImageUpload({ - initialImageUrls: tripItemInformation.imageUrls, - onSuccess: handleImageUrlsChange, + initialImageNames: tripItemInformation.imageNames, + updateFormImage: handleImageNamesChange, onError: handleImageUploadError, }); return ( - +
    - + - diff --git a/frontend/src/components/trips/CommunityTripsItem/CommunityTripsItem.style.ts b/frontend/src/components/trips/CommunityTripsItem/CommunityTripsItem.style.ts index 576fe9209..475acbdc7 100644 --- a/frontend/src/components/trips/CommunityTripsItem/CommunityTripsItem.style.ts +++ b/frontend/src/components/trips/CommunityTripsItem/CommunityTripsItem.style.ts @@ -46,7 +46,7 @@ export const nameStyling = css({ export const badgeBoxStyling = css({ width: `calc((100vw - 196px) / 5)`, - minHeight: '22px', + minHeight: '24px', marginTop: Theme.spacer.spacing3, marginBottom: Theme.spacer.spacing2, @@ -68,7 +68,7 @@ export const badgeBoxStyling = css({ }, }); -export const durationAndDescriptionStyling = css({ +export const durationStyling = css({ marginBottom: Theme.spacer.spacing1, '@media screen and (max-width: 600px)': { @@ -76,19 +76,18 @@ export const durationAndDescriptionStyling = css({ }, }); -export const durationTextStyling = css({ +export const descriptionStyling = css({ display: '-webkit-box', + '-webkit-line-clamp': '2', + '-webkit-box-orient': 'vertical', marginTop: '4px', width: '100%', - color: Theme.color.gray700, - - textOverflow: 'ellipsis', overflow: 'hidden', - wordBreak: 'break-word', - '-webkit-line-clamp': '2', - '-webkit-box-orient': 'vertical', + textOverflow: 'ellipsis', + wordBreak: 'break-all', + whiteSpace: 'pre-wrap', }); export const skeletonDurationTextStyling = css({ diff --git a/frontend/src/components/trips/CommunityTripsItem/CommunityTripsItem.tsx b/frontend/src/components/trips/CommunityTripsItem/CommunityTripsItem.tsx index 0e6d61586..90d782b95 100644 --- a/frontend/src/components/trips/CommunityTripsItem/CommunityTripsItem.tsx +++ b/frontend/src/components/trips/CommunityTripsItem/CommunityTripsItem.tsx @@ -10,13 +10,15 @@ import { boxStyling, clickableLikeStyling, communityItemInfoStyling, - durationAndDescriptionStyling, + descriptionStyling, + durationStyling, imageStyling, informationStyling, likeCountBoxStyling, nameStyling, } from '@components/trips/CommunityTripsItem/CommunityTripsItem.style'; +import { convertToImageUrl } from '@utils/convertImage'; import { formatDate } from '@utils/formatter'; import type { CommunityTripsItemData } from '@type/trips'; @@ -36,7 +38,7 @@ const CommunityTripsItem = ({ index, trip }: CommunityTripsItemProps) => { const { id, - imageUrl, + imageName, title, cities, startDate, @@ -47,7 +49,7 @@ const CommunityTripsItem = ({ index, trip }: CommunityTripsItemProps) => { likeCount: initialLikeCount, } = trip; - const coverImage = imageUrl; + const coverImage = imageName; const duration = `${formatDate(startDate)} - ${formatDate(endDate)}`; const [likeCount, setLikeCount] = useState(initialLikeCount); @@ -72,7 +74,11 @@ const CommunityTripsItem = ({ index, trip }: CommunityTripsItemProps) => { likeCount={likeCount} css={clickableLikeStyling} /> - {`${title} + {`${title} @@ -83,16 +89,16 @@ const CommunityTripsItem = ({ index, trip }: CommunityTripsItemProps) => { {title} - + {duration} - + {description} - 작성자 이미지 + 작성자 이미지 {writer.nickname} diff --git a/frontend/src/components/trips/TripsHeader/TripsHeader.style.ts b/frontend/src/components/trips/TripsHeader/TripsHeader.style.ts index 8a8335cf8..5287af240 100644 --- a/frontend/src/components/trips/TripsHeader/TripsHeader.style.ts +++ b/frontend/src/components/trips/TripsHeader/TripsHeader.style.ts @@ -2,6 +2,19 @@ import { css } from '@emotion/react'; import { Theme } from 'hang-log-design-system'; +export const containerStyling = css({ + width: '100%', + marginTop: Theme.spacer.spacing3, + marginBottom: Theme.spacer.spacing4, + + justifyContent: 'space-between', + alignItems: 'center', + + '@media screen and (max-width: 600px)': { + justifyContent: 'center', + }, +}); + export const imageStyling = css({ width: '854px', marginLeft: Theme.spacer.spacing6, @@ -15,16 +28,15 @@ export const imageStyling = css({ }, }); -export const headingStyling = () => - css({ - margin: `0 ${Theme.spacer.spacing9}`, +export const headingStyling = css({ + margin: `0 ${Theme.spacer.spacing9}`, - fontWeight: 400, - wordBreak: 'keep-all', + fontWeight: 400, + wordBreak: 'keep-all', - '@media screen and (max-width: 600px)': { - margin: `0 ${Theme.spacer.spacing5}`, - padding: '48px 0 16px 0', - fontSize: Theme.heading.medium.fontSize, - }, - }); + '@media screen and (max-width: 600px)': { + margin: `0 ${Theme.spacer.spacing5}`, + padding: '48px 0 16px 0', + fontSize: Theme.heading.medium.fontSize, + }, +}); diff --git a/frontend/src/components/trips/TripsItem/TripsItem.style.ts b/frontend/src/components/trips/TripsItem/TripsItem.style.ts index bb01fdb2e..a11b051e0 100644 --- a/frontend/src/components/trips/TripsItem/TripsItem.style.ts +++ b/frontend/src/components/trips/TripsItem/TripsItem.style.ts @@ -45,7 +45,7 @@ export const nameStyling = css({ export const badgeBoxStyling = css({ width: `calc((100vw - 196px) / 5)`, - minHeight: '22px', + minHeight: '24px', marginTop: Theme.spacer.spacing3, marginBottom: Theme.spacer.spacing2, @@ -81,7 +81,8 @@ export const descriptionStyling = css({ overflow: 'hidden', textOverflow: 'ellipsis', - wordBreak: 'break-word', + wordBreak: 'break-all', + whiteSpace: 'pre-wrap', }); export const skeletonDurationTextStyling = css({ diff --git a/frontend/src/components/trips/TripsItem/TripsItem.tsx b/frontend/src/components/trips/TripsItem/TripsItem.tsx index bfab41c31..f3cd87ff3 100644 --- a/frontend/src/components/trips/TripsItem/TripsItem.tsx +++ b/frontend/src/components/trips/TripsItem/TripsItem.tsx @@ -10,6 +10,8 @@ import { nameStyling, } from '@components/trips/TripsItem/TripsItem.style'; +import { convertToImageUrl } from '@utils/convertImage'; + import type { CityData } from '@type/city'; import { PATH } from '@constants/path'; @@ -47,7 +49,7 @@ const TripsItem = ({ onClick={() => navigate(PATH.TRIP(String(id)))} > {`${itemName} diff --git a/frontend/src/components/trips/TripsItemList/TripsItemList.tsx b/frontend/src/components/trips/TripsItemList/TripsItemList.tsx index 690435cf4..60de53d52 100644 --- a/frontend/src/components/trips/TripsItemList/TripsItemList.tsx +++ b/frontend/src/components/trips/TripsItemList/TripsItemList.tsx @@ -57,7 +57,7 @@ const TripsItemList = ({ trips, order, changeSelect }: TripsItemListProps) => { { - const { data } = useQuery(['city'], getCity); + const { data } = useQuery(['city'], getCity, { + cacheTime: 24 * 60 * 60 * 60 * 1000, + staleTime: Infinity, + }); return { cityData: data! }; }; diff --git a/frontend/src/hooks/api/useCommunityTripQuery.ts b/frontend/src/hooks/api/useCommunityTripQuery.ts index ed1d9bdcb..d3f49a734 100644 --- a/frontend/src/hooks/api/useCommunityTripQuery.ts +++ b/frontend/src/hooks/api/useCommunityTripQuery.ts @@ -13,12 +13,8 @@ import type { TripData } from '@type/trip'; export const useCommunityTripQuery = (tripId: string) => { const isLoggedIn = useRecoilValue(isLoggedInState); - const { data } = useQuery( - ['PUBLISHED', tripId], - () => getCommunityTrip(tripId, isLoggedIn), - { - cacheTime: 0, - } + const { data } = useQuery(['PUBLISHED', tripId], () => + getCommunityTrip(tripId, isLoggedIn) ); return { tripData: data! }; diff --git a/frontend/src/hooks/api/useExpenseQuery.ts b/frontend/src/hooks/api/useExpenseQuery.ts index 5a433ae05..e13fe7d04 100644 --- a/frontend/src/hooks/api/useExpenseQuery.ts +++ b/frontend/src/hooks/api/useExpenseQuery.ts @@ -7,24 +7,31 @@ import { getExpense } from '@api/expense/getExpense'; import { getSharedExpense } from '@api/expense/getSharedExpense'; import type { ExpenseData } from '@type/expense'; +import type { TripTypeData } from '@type/trip'; -export const useExpenseQuery = ( - tripId: string, - { isShared, isPublished }: { isShared: boolean; isPublished: boolean } -) => { +import { TRIP_TYPE } from '@constants/trip'; + +export const useExpenseQuery = (tripId: string, tripType: TripTypeData) => { const queryFn = { expense: () => getExpense(tripId), }; - if (isPublished) { + if (tripType === TRIP_TYPE.PUBLISHED) { queryFn.expense = () => getCommunityTripExpense(tripId); } - if (isShared && !isPublished) { + if (tripType === TRIP_TYPE.SHARED) { queryFn.expense = () => getSharedExpense(tripId); } - const { data } = useQuery(['expense', tripId], queryFn.expense); + const { data } = useQuery( + [`${tripType}expense`, tripId], + queryFn.expense, + { + cacheTime: 5 * 60 * 1000, + staleTime: 60 * 1000, + } + ); return { expenseData: data! }; }; diff --git a/frontend/src/hooks/api/useImageMutation.ts b/frontend/src/hooks/api/useImageMutation.ts index 3296075ba..ae76d0bc5 100644 --- a/frontend/src/hooks/api/useImageMutation.ts +++ b/frontend/src/hooks/api/useImageMutation.ts @@ -14,6 +14,9 @@ export const useImageMutation = () => { const imageMutation = useMutation({ mutationFn: postImage, + onSuccess: () => { + createToast('이미지 업로드에 성공했습니다', 'success'); + }, onError: (error: ErrorResponseData) => { if (error.code && error.code > ERROR_CODE.TOKEN_ERROR_RANGE) { handleTokenError(); diff --git a/frontend/src/hooks/api/useRecommendedTripsQuery.ts b/frontend/src/hooks/api/useRecommendedTripsQuery.ts index cfd0521a9..f678618cf 100644 --- a/frontend/src/hooks/api/useRecommendedTripsQuery.ts +++ b/frontend/src/hooks/api/useRecommendedTripsQuery.ts @@ -7,12 +7,8 @@ import { getRecommendedTrips } from '@api/trips/getRecommendedTrips'; import type { RecommendedTripsData } from '@type/trips'; export const useRecommendedTripsQuery = (isLoggedIn: boolean) => { - const { data } = useQuery( - ['recommendedTrips'], - () => getRecommendedTrips(isLoggedIn), - { - cacheTime: 0, - } + const { data } = useQuery(['recommendedTrips'], () => + getRecommendedTrips(isLoggedIn) ); return { tripsData: data! }; diff --git a/frontend/src/hooks/api/useTripEditMutation.ts b/frontend/src/hooks/api/useTripEditMutation.ts index d7902a628..6b6bd8bc2 100644 --- a/frontend/src/hooks/api/useTripEditMutation.ts +++ b/frontend/src/hooks/api/useTripEditMutation.ts @@ -7,6 +7,7 @@ import type { ErrorResponseData } from '@api/interceptors'; import { putTrip } from '@api/trip/putTrip'; import { ERROR_CODE } from '@constants/api'; +import { TRIP_TYPE } from '@constants/trip'; export const useTripEditMutation = () => { const queryClient = useQueryClient(); @@ -17,7 +18,7 @@ export const useTripEditMutation = () => { const tripMutation = useMutation({ mutationFn: putTrip, onSuccess: (_, { tripId }) => { - queryClient.invalidateQueries({ queryKey: ['PERSONAL', tripId] }); + queryClient.invalidateQueries({ queryKey: [TRIP_TYPE.PERSONAL, tripId] }); }, onError: (error: ErrorResponseData) => { if (error.code && error.code > ERROR_CODE.TOKEN_ERROR_RANGE) { diff --git a/frontend/src/hooks/api/useTripQuery.ts b/frontend/src/hooks/api/useTripQuery.ts index d93cb2bea..f05df8a50 100644 --- a/frontend/src/hooks/api/useTripQuery.ts +++ b/frontend/src/hooks/api/useTripQuery.ts @@ -30,7 +30,8 @@ export const useTripQuery = (tripType: TripTypeData, tripId: string) => { } const { data } = useQuery([tripType, tripId], queryFn.trip, { - cacheTime: 0, + cacheTime: 5 * 60 * 1000, + staleTime: 60 * 1000, }); return { tripData: data! }; diff --git a/frontend/src/hooks/api/useTripShareStatusMutation.ts b/frontend/src/hooks/api/useTripShareStatusMutation.ts index 0d35bd964..ec65e1560 100644 --- a/frontend/src/hooks/api/useTripShareStatusMutation.ts +++ b/frontend/src/hooks/api/useTripShareStatusMutation.ts @@ -1,5 +1,3 @@ -import { TRIP_TYPE } from '@/constants/trip'; - import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useToast } from '@hooks/common/useToast'; @@ -9,6 +7,7 @@ import type { ErrorResponseData } from '@api/interceptors'; import { patchTripSharedStatus } from '@api/trip/patchTripShareStatus'; import { ERROR_CODE } from '@constants/api'; +import { TRIP_TYPE } from '@constants/trip'; export const useTripShareStatusMutation = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/api/useUserInfoMutation.ts b/frontend/src/hooks/api/useUserInfoMutation.ts index 631cfd13c..5ed7ef354 100644 --- a/frontend/src/hooks/api/useUserInfoMutation.ts +++ b/frontend/src/hooks/api/useUserInfoMutation.ts @@ -22,6 +22,12 @@ export const useUserInfoMutation = () => { createToast('정보를 성공적으로 수정했습니다!', 'success'); }, onError: (error: ErrorResponseData) => { + if (error.code && error.code === ERROR_CODE.DUPLICATED_NICKNAME) { + createToast('중복된 닉네임이 존재합니다.', 'default'); + + return; + } + if (error.code && error.code > ERROR_CODE.TOKEN_ERROR_RANGE) { handleTokenError(); diff --git a/frontend/src/hooks/api/useUserInfoQuery.ts b/frontend/src/hooks/api/useUserInfoQuery.ts index 791e648d0..8127626f4 100644 --- a/frontend/src/hooks/api/useUserInfoQuery.ts +++ b/frontend/src/hooks/api/useUserInfoQuery.ts @@ -7,7 +7,10 @@ import { getUserInfo } from '@api/member/getUserInfo'; import type { UserData } from '@type/member'; export const useUserInfoQuery = () => { - const { data } = useQuery(['userInfo'], getUserInfo); + const { data } = useQuery(['userInfo'], getUserInfo, { + cacheTime: 60 * 60 * 60 * 1000, + staleTime: 60 * 60 * 60 * 1000, + }); return { userInfoData: data! }; }; diff --git a/frontend/src/hooks/common/useMultipleImageUpload.ts b/frontend/src/hooks/common/useMultipleImageUpload.ts index c2e0c3f54..48f183758 100644 --- a/frontend/src/hooks/common/useMultipleImageUpload.ts +++ b/frontend/src/hooks/common/useMultipleImageUpload.ts @@ -1,123 +1,138 @@ import type { ChangeEvent } from 'react'; -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import imageCompression from 'browser-image-compression'; import { useImageMutation } from '@hooks/api/useImageMutation'; -import { useToast } from '@hooks/common/useToast'; + +import { convertToImageNames, convertToImageUrls } from '@utils/convertImage'; import { IMAGE_COMPRESSION_OPTIONS } from '@constants/image'; import { TRIP_ITEM_ADD_MAX_IMAGE_UPLOAD_COUNT } from '@constants/ui'; interface UseMultipleImageUploadParams { - initialImageUrls: string[]; + initialImageNames: string[]; maxUploadCount?: number; - handleInitialImage?: (images: string[]) => void; - onSuccess?: CallableFunction; + updateFormImage?: CallableFunction; onError?: CallableFunction; } export const useMultipleImageUpload = ({ - initialImageUrls, + initialImageNames, maxUploadCount = TRIP_ITEM_ADD_MAX_IMAGE_UPLOAD_COUNT, - onSuccess, + updateFormImage, onError, }: UseMultipleImageUploadParams) => { const imageMutation = useImageMutation(); const isImageUploading = imageMutation.isLoading; - const { createToast } = useToast(); - const [uploadedImageUrls, setUploadedImageUrls] = useState(initialImageUrls); + const initialImageUrls = convertToImageUrls([...initialImageNames]); - const handleImageUpload = useCallback( - async (event: ChangeEvent) => { - const originalImageFiles = event.target.files; + const [imageUrls, setImageUrls] = useState(initialImageUrls); + const uploadedImageNames = useMemo(() => [...initialImageNames], [initialImageNames]); - if (!originalImageFiles) return; + const compressImages = useCallback(async (originalImageFiles: FileList): Promise => { + const imageFiles: File[] = []; - if (originalImageFiles.length + uploadedImageUrls.length > maxUploadCount) { - onError?.(); + try { + await Promise.all( + [...originalImageFiles].map(async (file) => { + const compressedImageFile = await imageCompression(file, IMAGE_COMPRESSION_OPTIONS); - return; - } - - const prevImageUrls = uploadedImageUrls; - - setUploadedImageUrls((prevImageUrls) => { - const newImageUrls = [...originalImageFiles].map((file) => URL.createObjectURL(file)); + const fileName = file.name; + const fileType = compressedImageFile.type; + const convertedFile = new File([compressedImageFile], fileName, { type: fileType }); - return [...prevImageUrls, ...newImageUrls]; - }); - - const imageFiles: File[] = []; - - try { - await Promise.all( - [...originalImageFiles].map(async (file) => { - const compressedImageFile = await imageCompression(file, IMAGE_COMPRESSION_OPTIONS); - - const fileName = file.name; - const fileType = compressedImageFile.type; - const convertedFile = new File([compressedImageFile], fileName, { type: fileType }); + imageFiles.push(convertedFile); + }) + ); + } catch (e) { + imageFiles.push(...originalImageFiles); + } - imageFiles.push(convertedFile); - }) - ); - } catch (e) { - imageFiles.push(...originalImageFiles); - } + return imageFiles; + }, []); - const imageUploadFormData = new FormData(); + const convertToImageFormData = useCallback( + async (imageFiles: FileList) => { + const compressedImages = await compressImages(imageFiles); + const imageFormData = new FormData(); - [...imageFiles].forEach((file) => { - imageUploadFormData.append('images', file); + compressedImages.forEach((file) => { + imageFormData.append('images', file); }); + return imageFormData; + }, + [compressImages] + ); + + const postImageNames = useCallback( + async (images: FormData) => { imageMutation.mutate( - { images: imageUploadFormData }, + { images }, { - onSuccess: ({ imageUrls }) => { + onSuccess: ({ imageNames }) => { if (maxUploadCount === 1) { - onSuccess?.([...imageUrls]); - createToast('이미지 업로드에 성공했습니다', 'success'); + updateFormImage?.([...imageNames]); return; } - onSuccess?.([...initialImageUrls, ...imageUrls]); - createToast('이미지 업로드에 성공했습니다', 'success'); + uploadedImageNames.push(...imageNames); + + updateFormImage?.(uploadedImageNames); }, onError: () => { - setUploadedImageUrls(prevImageUrls); + setImageUrls(initialImageUrls); }, } ); + }, + [imageMutation, maxUploadCount, uploadedImageNames, updateFormImage, initialImageUrls] + ); + + const handleImageUpload = useCallback( + async (event: ChangeEvent) => { + const originalImageFiles = event.target.files; + + if (!originalImageFiles) return; + + if (originalImageFiles.length + imageUrls.length > maxUploadCount) { + onError?.(); + + return; + } + + // 화면에 보여지는 이미지 url로 변경 + 업데이트 + setImageUrls((prevImageUrls) => { + const newImageUrls = [...originalImageFiles].map((file) => URL.createObjectURL(file)); + + return [...prevImageUrls, ...newImageUrls]; + }); + + const imageFormData = await convertToImageFormData(originalImageFiles); + postImageNames(imageFormData); // eslint-disable-next-line no-param-reassign event.target.value = ''; }, - [ - createToast, - imageMutation, - initialImageUrls, - maxUploadCount, - onError, - onSuccess, - uploadedImageUrls, - ] + [imageUrls, maxUploadCount, convertToImageFormData, postImageNames, onError] ); const handleImageRemoval = useCallback( (selectedImageUrl: string) => () => { - setUploadedImageUrls((prevImageUrls) => { + setImageUrls((prevImageUrls) => { const updatedImageUrls = prevImageUrls.filter((imageUrl) => imageUrl !== selectedImageUrl); - onSuccess?.(updatedImageUrls); + + const imageNames = convertToImageNames(updatedImageUrls); + updateFormImage?.(imageNames); return updatedImageUrls; }); }, - [onSuccess] + [updateFormImage] ); - return { isImageUploading, uploadedImageUrls, handleImageUpload, handleImageRemoval }; + return { isImageUploading, imageUrls, handleImageUpload, handleImageRemoval }; }; diff --git a/frontend/src/hooks/common/useSingleImageUpload.ts b/frontend/src/hooks/common/useSingleImageUpload.ts index 81b86a21b..e16db7aa7 100644 --- a/frontend/src/hooks/common/useSingleImageUpload.ts +++ b/frontend/src/hooks/common/useSingleImageUpload.ts @@ -4,66 +4,79 @@ import { useCallback, useState } from 'react'; import imageCompression from 'browser-image-compression'; import { useImageMutation } from '@hooks/api/useImageMutation'; -import { useToast } from '@hooks/common/useToast'; + +import { convertToImageUrl } from '@utils/convertImage'; import { IMAGE_COMPRESSION_OPTIONS } from '@constants/image'; interface UseSingleImageUploadParams { initialImageUrl: string | null; - onSuccess?: CallableFunction; + updateFormImage?: CallableFunction; } export const useSingleImageUpload = ({ initialImageUrl, - onSuccess, + updateFormImage, }: UseSingleImageUploadParams) => { const imageMutation = useImageMutation(); const isImageUploading = imageMutation.isLoading; - const { createToast } = useToast(); - const [uploadedImageUrl, setUploadedImageUrl] = useState(initialImageUrl); - const handleImageUpload = useCallback( - async (event: ChangeEvent) => { - const originalImageFile = event.target.files?.[0]; + const compressImage = useCallback(async (originalImageFile: File) => { + let imageFile: File; - if (!originalImageFile) return; + try { + const compressedImageFile = await imageCompression( + originalImageFile, + IMAGE_COMPRESSION_OPTIONS + ); - const prevImageUrl = uploadedImageUrl; + const fileName = originalImageFile.name; - setUploadedImageUrl(URL.createObjectURL(originalImageFile)); + const fileType = compressedImageFile.type; + + imageFile = new File([compressedImageFile], fileName, { type: fileType }); + } catch (e) { + imageFile = originalImageFile; + } + + return imageFile; + }, []); - let imageFile: File; + const convertToImageFormData = useCallback( + async (imageFile: File) => { + const compressedImage = await compressImage(imageFile); - try { - const compressedImageFile = await imageCompression( - originalImageFile, + const imageUploadFormData = new FormData(); + imageUploadFormData.append('images', compressedImage); - IMAGE_COMPRESSION_OPTIONS - ); + return imageUploadFormData; + }, + [compressImage] + ); + + const handleImageUpload = useCallback( + async (event: ChangeEvent) => { + const originalImageFile = event.target.files?.[0]; - const fileName = originalImageFile.name; + if (!originalImageFile) return; - const fileType = compressedImageFile.type; + const prevImageName = uploadedImageUrl; - imageFile = new File([compressedImageFile], fileName, { type: fileType }); - } catch (e) { - imageFile = originalImageFile; - } + setUploadedImageUrl(URL.createObjectURL(originalImageFile)); - const imageUploadFormData = new FormData(); - imageUploadFormData.append('images', imageFile); + const images = await convertToImageFormData(originalImageFile); imageMutation.mutate( - { images: imageUploadFormData }, + { images }, { - onSuccess: ({ imageUrls }) => { - onSuccess?.(imageUrls[0]); - createToast('이미지 업로드에 성공했습니다', 'success'); + onSuccess: ({ imageNames }) => { + const imageUrl = convertToImageUrl(imageNames[0]); + updateFormImage?.(imageUrl); }, onError: () => { - setUploadedImageUrl(prevImageUrl); + setUploadedImageUrl(prevImageName); }, } ); @@ -71,13 +84,13 @@ export const useSingleImageUpload = ({ // eslint-disable-next-line no-param-reassign event.target.value = ''; }, - [createToast, imageMutation, onSuccess, uploadedImageUrl] + [convertToImageFormData, imageMutation, updateFormImage, uploadedImageUrl] ); const handleImageRemoval = useCallback(() => { setUploadedImageUrl(null); - onSuccess?.(null); - }, [onSuccess]); + updateFormImage?.(null); + }, [updateFormImage]); return { isImageUploading, uploadedImageUrl, handleImageUpload, handleImageRemoval }; }; diff --git a/frontend/src/hooks/expense/useExpense.ts b/frontend/src/hooks/expense/useExpense.ts index 78f761ac1..f8d782040 100644 --- a/frontend/src/hooks/expense/useExpense.ts +++ b/frontend/src/hooks/expense/useExpense.ts @@ -5,13 +5,14 @@ import { useQueryClient } from '@tanstack/react-query'; import type { Segment } from '@components/common/DonutChart/DonutChart'; import type { ExpenseData, ExpenseItemData } from '@type/expense'; +import type { TripTypeData } from '@type/trip'; import { EXPENSE_CHART_COLORS } from '@constants/expense'; -export const useExpense = (tripId: string) => { +export const useExpense = (tripId: string, tripType: TripTypeData) => { const queryClient = useQueryClient(); - const expenseData = queryClient.getQueryData(['expense', tripId])!; + const expenseData = queryClient.getQueryData([`${tripType}expense`, tripId])!; const dates = expenseData.dayLogs.map((data) => ({ id: data.id, diff --git a/frontend/src/hooks/trip/useAddTripItemForm.ts b/frontend/src/hooks/trip/useAddTripItemForm.ts index f7a3539e3..90c1c4cd6 100644 --- a/frontend/src/hooks/trip/useAddTripItemForm.ts +++ b/frontend/src/hooks/trip/useAddTripItemForm.ts @@ -44,7 +44,7 @@ export const useAddTripItemForm = ({ rating: null, expense: null, memo: null, - imageUrls: [], + imageNames: [], } ); const [isTitleError, setIsTitleError] = useState(false); @@ -107,7 +107,6 @@ export const useAddTripItemForm = ({ return; } - if (!itemId) { addTripItemMutation.mutate( { diff --git a/frontend/src/hooks/trip/useTripEditForm.ts b/frontend/src/hooks/trip/useTripEditForm.ts index 4dc8ca0c9..7890a459e 100644 --- a/frontend/src/hooks/trip/useTripEditForm.ts +++ b/frontend/src/hooks/trip/useTripEditForm.ts @@ -16,7 +16,7 @@ export const useTripEditForm = ( startDate, endDate, description, - imageUrl, + imageName, }: Omit< TripData, | 'tripType' @@ -37,7 +37,7 @@ export const useTripEditForm = ( startDate, endDate, }); - const [tripInfo, setTripInfo] = useState({ title, description, imageUrl, ...cityDateInfo }); + const [tripInfo, setTripInfo] = useState({ title, description, imageName, ...cityDateInfo }); const [isCityError, setIsCityError] = useState(false); const [isTitleError, setIsTitleError] = useState(false); const tripEditMutation = useTripEditMutation(); @@ -55,9 +55,9 @@ export const useTripEditForm = ( }); }; - const updateCoverImage = (imageUrl: string) => { + const updateCoverImage = (imageName: string | null) => { setTripInfo((prevTripInfo) => { - return { ...prevTripInfo, imageUrl }; + return { ...prevTripInfo, imageName }; }); }; @@ -89,7 +89,7 @@ export const useTripEditForm = ( tripEditMutation.mutate( { ...tripInfo, - imageUrl: tripInfo.imageUrl, + imageName: tripInfo.imageName, tripId: String(tripId), startDate: tripInfo.startDate!, endDate: tripInfo.endDate!, diff --git a/frontend/src/mocks/data/communityTrip.ts b/frontend/src/mocks/data/communityTrip.ts index 4b84f9b07..134483918 100644 --- a/frontend/src/mocks/data/communityTrip.ts +++ b/frontend/src/mocks/data/communityTrip.ts @@ -18,8 +18,7 @@ export const communityTrip: TripData = { startDate: '2023-07-01', endDate: '2023-07-03', description: '라곤의 좌충우돌 유럽 여행기', - imageUrl: - 'https://a.cdn-hotels.com/gdcs/production153/d1371/e6c1f55e-51ac-41d5-8c63-2d0c63faf59e.jpg', + imageName: 'default-image.png', cities: [ { id: 1, @@ -73,12 +72,7 @@ export const communityTrip: TripData = { name: '관광', }, }, - imageUrls: [ - 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', - 'https://e3.365dm.com/17/10/2048x1152/skynews-piccadilly-piccadilly-circus_4131587.jpg', - 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/15/9e/a5/6f/regent-str.jpg?w=1200&h=-1&s=1', - 'https://flashbak.com/wp-content/uploads/2017/01/Piccadilly-by-Night-London-1960-by-Elmar-Ludwig.jpg', - ], + imageNames: ['default-image2.png', 'default-image.png'], }, { id: 2, @@ -98,7 +92,7 @@ export const communityTrip: TripData = { }, }, expense: null, - imageUrls: ['https://pbs.twimg.com/media/D8sOPlgXUAACySN.jpg'], + imageNames: ['default-image.png'], }, { id: 3, @@ -117,7 +111,7 @@ export const communityTrip: TripData = { name: '교통', }, }, - imageUrls: [], + imageNames: [], }, { id: 10, @@ -145,9 +139,7 @@ export const communityTrip: TripData = { name: '음식', }, }, - imageUrls: [ - 'https://lh3.googleusercontent.com/p/AF1QipOirgDoMN4u1mwho_3Nh_4hS6IehGlewiHtcQLI=s1360-w1360-h1020', - ], + imageNames: ['default-image.png'], }, ], }, @@ -183,9 +175,7 @@ export const communityTrip: TripData = { name: '음식', }, }, - imageUrls: [ - 'https://lh3.googleusercontent.com/p/AF1QipOirgDoMN4u1mwho_3Nh_4hS6IehGlewiHtcQLI=s1360-w1360-h1020', - ], + imageNames: ['default-image.png'], }, { id: 4, @@ -204,9 +194,7 @@ export const communityTrip: TripData = { name: '교통', }, }, - imageUrls: [ - 'https://www.thetrainline.com/cms/media/4626/southeastern-javelin-train-st-pancras-intl-desktop_1x.jpg', - ], + imageNames: ['default-image.png'], }, ], }, diff --git a/frontend/src/mocks/data/communityTripsData.ts b/frontend/src/mocks/data/communityTripsData.ts index c7f2f326c..9029625ed 100644 --- a/frontend/src/mocks/data/communityTripsData.ts +++ b/frontend/src/mocks/data/communityTripsData.ts @@ -4,10 +4,10 @@ export const communityTripsData = { { id: 1, title: '런던1 여행2', - imageUrl: 'https://res.klook.com/image/upload/Mobile/City/n9sn4fajwa1skldmdeex.jpg', + imageName: 'default-image.png', writer: { nickname: '양파쿵야', - imagUrl: + imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', }, cities: [ @@ -29,11 +29,10 @@ export const communityTripsData = { { id: 2, title: '파리2 여행', - imageUrl: - 'https://images.squarespace-cdn.com/content/v1/586ebc34d482e9c69268b69a/1624386887478-9Z3XA27D8WFVDWKW00QS/20201230173806551_JRT8E1VC.png', + imageName: 'default-image.png', writer: { nickname: '양파쿵야', - imagUrl: + imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', }, cities: [ @@ -55,11 +54,10 @@ export const communityTripsData = { { id: 3, title: '서울 여행4', - imageUrl: - 'https://a.cdn-hotels.com/gdcs/production144/d992/418cd5c1-7f91-4c44-9f39-3016b033eaa1.jpg?impolicy=fcrop&w=800&h=533&q=medium', + imageName: 'default-image.png', writer: { nickname: '양파쿵야', - imagUrl: + imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', }, cities: [ @@ -81,11 +79,10 @@ export const communityTripsData = { { id: 4, title: '도쿄 여행6', - imageUrl: - 'https://www.jamonfive.com/files/attach/images/1032/015/002/aa092a675ca23b89e818562389d62c12.jpg', + imageName: 'default-image.png', writer: { nickname: '양파쿵야', - imagUrl: + imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', }, cities: [ @@ -107,11 +104,10 @@ export const communityTripsData = { { id: 5, title: '런던 여행7', - imageUrl: - 'https://www.discoverlosangeles.com/sites/default/files/images/2023-02/IMG_0410-Edit-3.jpg?width=1600&height=1200&fit=crop&quality=78&auto=webp', + imageName: 'default-image.png', writer: { nickname: '양파쿵야', - imagUrl: + imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', }, cities: [ @@ -133,10 +129,10 @@ export const communityTripsData = { { id: 6, title: '런던 여행142', - imageUrl: 'https://res.klook.com/image/upload/Mobile/City/n9sn4fajwa1skldmdeex.jpg', + imageName: 'default-image.png', writer: { nickname: '양파쿵야', - imagUrl: + imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', }, cities: [ @@ -158,11 +154,10 @@ export const communityTripsData = { { id: 7, title: '파리 여행23445', - imageUrl: - 'https://images.squarespace-cdn.com/content/v1/586ebc34d482e9c69268b69a/1624386887478-9Z3XA27D8WFVDWKW00QS/20201230173806551_JRT8E1VC.png', + imageName: 'default-image.png', writer: { nickname: '양파쿵야', - imagUrl: + imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', }, cities: [ @@ -184,11 +179,10 @@ export const communityTripsData = { { id: 8, title: '서울2345 여행', - imageUrl: - 'https://a.cdn-hotels.com/gdcs/production144/d992/418cd5c1-7f91-4c44-9f39-3016b033eaa1.jpg?impolicy=fcrop&w=800&h=533&q=medium', + imageName: 'default-image.png', writer: { nickname: '양파쿵야', - imagUrl: + imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', }, cities: [ @@ -210,11 +204,10 @@ export const communityTripsData = { { id: 9, title: '도쿄 여행5678', - imageUrl: - 'https://www.jamonfive.com/files/attach/images/1032/015/002/aa092a675ca23b89e818562389d62c12.jpg', + imageName: 'default-image.png', writer: { nickname: '양파쿵야', - imagUrl: + imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', }, cities: [ @@ -236,11 +229,10 @@ export const communityTripsData = { { id: 10, title: '런던 여행4567458', - imageUrl: - 'https://www.discoverlosangeles.com/sites/default/files/images/2023-02/IMG_0410-Edit-3.jpg?width=1600&height=1200&fit=crop&quality=78&auto=webp', + imageName: 'default-image.png', writer: { nickname: '양파쿵야', - imagUrl: + imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', }, cities: [ diff --git a/frontend/src/mocks/data/image.ts b/frontend/src/mocks/data/image.ts index 4355c8ecc..1d3624a76 100644 --- a/frontend/src/mocks/data/image.ts +++ b/frontend/src/mocks/data/image.ts @@ -1,6 +1,5 @@ export const images = [ - 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', - 'https://e3.365dm.com/17/10/2048x1152/skynews-piccadilly-piccadilly-circus_4131587.jpg', - 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/15/9e/a5/6f/regent-str.jpg?w=1200&h=-1&s=1', - 'https://flashbak.com/wp-content/uploads/2017/01/Piccadilly-by-Night-London-1960-by-Elmar-Ludwig.jpg', + 'Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', + 'regent-str.jpg', + 'Piccadilly-by-Night-London-1960-by-Elmar-Ludwig.jpg', ]; diff --git a/frontend/src/mocks/data/myTrips.ts b/frontend/src/mocks/data/myTrips.ts index d481b003f..25c1fa231 100644 --- a/frontend/src/mocks/data/myTrips.ts +++ b/frontend/src/mocks/data/myTrips.ts @@ -2,7 +2,7 @@ export const trips = [ { id: 1, title: '런던 여행', - imageUrl: 'https://res.klook.com/image/upload/Mobile/City/n9sn4fajwa1skldmdeex.jpg', + imageName: 'default-image.png', cities: [ { id: 1, @@ -25,8 +25,7 @@ export const trips = [ { id: 2, title: '서울 여행', - imageUrl: - 'https://images.squarespace-cdn.com/content/v1/586ebc34d482e9c69268b69a/1624386887478-9Z3XA27D8WFVDWKW00QS/20201230173806551_JRT8E1VC.png', + imageName: 'default-image.png', cities: [ { id: 2, @@ -42,8 +41,7 @@ export const trips = [ { id: 3, title: '부산 여행', - imageUrl: - 'https://a.cdn-hotels.com/gdcs/production144/d992/418cd5c1-7f91-4c44-9f39-3016b033eaa1.jpg?impolicy=fcrop&w=800&h=533&q=medium', + imageName: 'default-image.png', cities: [ { id: 3, @@ -59,8 +57,7 @@ export const trips = [ { id: 4, title: '바르셀로나 여행', - imageUrl: - 'https://www.jamonfive.com/files/attach/images/1032/015/002/aa092a675ca23b89e818562389d62c12.jpg', + imageName: 'default-image.png', cities: [ { id: 2, @@ -82,8 +79,7 @@ export const trips = [ { id: 5, title: '로스앤젤레스 여행', - imageUrl: - 'https://www.discoverlosangeles.com/sites/default/files/images/2023-02/IMG_0410-Edit-3.jpg?width=1600&height=1200&fit=crop&quality=78&auto=webp', + imageName: 'default-image.png', cities: [ { id: 6, @@ -105,7 +101,7 @@ export const trips = [ { id: 6, title: '리옹 여행', - imageUrl: null, + imageName: null, cities: [ { id: 53, diff --git a/frontend/src/mocks/data/recommendedTripsData.ts b/frontend/src/mocks/data/recommendedTripsData.ts index 86327c55b..b14e25220 100644 --- a/frontend/src/mocks/data/recommendedTripsData.ts +++ b/frontend/src/mocks/data/recommendedTripsData.ts @@ -4,10 +4,10 @@ export const recommendedTripsData = { { id: 1, title: '런던 여행', - imageUrl: 'https://res.klook.com/image/upload/Mobile/City/n9sn4fajwa1skldmdeex.jpg', + imageName: 'default-image.png', writer: { nickname: '양파쿵야', - imagUrl: + imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', }, cities: [ @@ -22,19 +22,17 @@ export const recommendedTripsData = { ], startDate: '2023-06-13', endDate: '2023-06-20', - description: - '어쩌구 저쩌구 좌충우돌좌충우돌좌충우돌좌충우돌좌충우돌좌충우돌 라곤의 런던 여행기', + description: '어쩌구 저쩌구', likeCount: 123, isLike: true, }, { id: 2, title: '파리 여행', - imageUrl: - 'https://images.squarespace-cdn.com/content/v1/586ebc34d482e9c69268b69a/1624386887478-9Z3XA27D8WFVDWKW00QS/20201230173806551_JRT8E1VC.png', + imageName: 'default-image.png', writer: { nickname: '양파쿵야', - imagUrl: + imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', }, cities: [ @@ -56,11 +54,10 @@ export const recommendedTripsData = { { id: 3, title: '서울 여행', - imageUrl: - 'https://a.cdn-hotels.com/gdcs/production144/d992/418cd5c1-7f91-4c44-9f39-3016b033eaa1.jpg?impolicy=fcrop&w=800&h=533&q=medium', + imageName: 'default-image.png', writer: { nickname: '양파쿵야', - imagUrl: + imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', }, cities: [ @@ -82,11 +79,10 @@ export const recommendedTripsData = { { id: 4, title: '도쿄 여행', - imageUrl: - 'https://www.jamonfive.com/files/attach/images/1032/015/002/aa092a675ca23b89e818562389d62c12.jpg', + imageName: 'default-image.png', writer: { nickname: '양파쿵야', - imagUrl: + imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', }, cities: [ @@ -108,11 +104,10 @@ export const recommendedTripsData = { { id: 5, title: '런던2 여행3', - imageUrl: - 'https://www.discoverlosangeles.com/sites/default/files/images/2023-02/IMG_0410-Edit-3.jpg?width=1600&height=1200&fit=crop&quality=78&auto=webp', + imageName: 'default-image.png', writer: { nickname: '양파쿵야', - imagUrl: + imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', }, cities: [ diff --git a/frontend/src/mocks/data/sharedTrip.ts b/frontend/src/mocks/data/sharedTrip.ts index c0e137330..59f8233c1 100644 --- a/frontend/src/mocks/data/sharedTrip.ts +++ b/frontend/src/mocks/data/sharedTrip.ts @@ -17,8 +17,7 @@ export const sharedTrip: TripData = { likeCount: null, isWriter: null, publishedDate: null, - imageUrl: - 'https://a.cdn-hotels.com/gdcs/production153/d1371/e6c1f55e-51ac-41d5-8c63-2d0c63faf59e.jpg', + imageName: 'default-image.png', cities: [ { id: 1, @@ -72,12 +71,7 @@ export const sharedTrip: TripData = { name: '관광', }, }, - imageUrls: [ - 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', - 'https://e3.365dm.com/17/10/2048x1152/skynews-piccadilly-piccadilly-circus_4131587.jpg', - 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/15/9e/a5/6f/regent-str.jpg?w=1200&h=-1&s=1', - 'https://flashbak.com/wp-content/uploads/2017/01/Piccadilly-by-Night-London-1960-by-Elmar-Ludwig.jpg', - ], + imageNames: ['default-image2.png', 'default-image.png'], }, { id: 2, @@ -97,7 +91,7 @@ export const sharedTrip: TripData = { }, }, expense: null, - imageUrls: ['https://pbs.twimg.com/media/D8sOPlgXUAACySN.jpg'], + imageNames: ['default-image.png'], }, { id: 3, @@ -116,7 +110,7 @@ export const sharedTrip: TripData = { name: '교통', }, }, - imageUrls: [], + imageNames: [], }, { id: 10, @@ -144,9 +138,7 @@ export const sharedTrip: TripData = { name: '음식', }, }, - imageUrls: [ - 'https://lh3.googleusercontent.com/p/AF1QipOirgDoMN4u1mwho_3Nh_4hS6IehGlewiHtcQLI=s1360-w1360-h1020', - ], + imageNames: ['default-image.png'], }, ], }, @@ -182,9 +174,7 @@ export const sharedTrip: TripData = { name: '음식', }, }, - imageUrls: [ - 'https://lh3.googleusercontent.com/p/AF1QipOirgDoMN4u1mwho_3Nh_4hS6IehGlewiHtcQLI=s1360-w1360-h1020', - ], + imageNames: ['default-image.png'], }, { id: 4, @@ -203,9 +193,7 @@ export const sharedTrip: TripData = { name: '교통', }, }, - imageUrls: [ - 'https://www.thetrainline.com/cms/media/4626/southeastern-javelin-train-st-pancras-intl-desktop_1x.jpg', - ], + imageNames: ['default-image.png'], }, ], }, diff --git a/frontend/src/mocks/data/trip.ts b/frontend/src/mocks/data/trip.ts index 861cf78b5..17938657e 100644 --- a/frontend/src/mocks/data/trip.ts +++ b/frontend/src/mocks/data/trip.ts @@ -17,8 +17,7 @@ export const trip: TripData = { likeCount: null, isWriter: null, publishedDate: null, - imageUrl: - 'https://a.cdn-hotels.com/gdcs/production153/d1371/e6c1f55e-51ac-41d5-8c63-2d0c63faf59e.jpg', + imageName: 'default-image.png', cities: [ { id: 1, @@ -72,12 +71,7 @@ export const trip: TripData = { name: '관광', }, }, - imageUrls: [ - 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', - 'https://e3.365dm.com/17/10/2048x1152/skynews-piccadilly-piccadilly-circus_4131587.jpg', - 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/15/9e/a5/6f/regent-str.jpg?w=1200&h=-1&s=1', - 'https://flashbak.com/wp-content/uploads/2017/01/Piccadilly-by-Night-London-1960-by-Elmar-Ludwig.jpg', - ], + imageNames: ['default-image2.png', 'default-image.png'], }, { id: 2, @@ -97,7 +91,7 @@ export const trip: TripData = { }, }, expense: null, - imageUrls: ['https://pbs.twimg.com/media/D8sOPlgXUAACySN.jpg'], + imageNames: ['default-image.png'], }, { id: 3, @@ -116,7 +110,7 @@ export const trip: TripData = { name: '교통', }, }, - imageUrls: [], + imageNames: [], }, { id: 10, @@ -144,9 +138,7 @@ export const trip: TripData = { name: '음식', }, }, - imageUrls: [ - 'https://lh3.googleusercontent.com/p/AF1QipOirgDoMN4u1mwho_3Nh_4hS6IehGlewiHtcQLI=s1360-w1360-h1020', - ], + imageNames: ['default-image.png'], }, ], }, @@ -182,9 +174,7 @@ export const trip: TripData = { name: '음식', }, }, - imageUrls: [ - 'https://lh3.googleusercontent.com/p/AF1QipOirgDoMN4u1mwho_3Nh_4hS6IehGlewiHtcQLI=s1360-w1360-h1020', - ], + imageNames: ['default-image.png'], }, { id: 4, @@ -203,9 +193,7 @@ export const trip: TripData = { name: '교통', }, }, - imageUrls: [ - 'https://www.thetrainline.com/cms/media/4626/southeastern-javelin-train-st-pancras-intl-desktop_1x.jpg', - ], + imageNames: ['default-image.png'], }, ], }, diff --git a/frontend/src/mocks/handlers/image.ts b/frontend/src/mocks/handlers/image.ts index 8ec18abe3..724612300 100644 --- a/frontend/src/mocks/handlers/image.ts +++ b/frontend/src/mocks/handlers/image.ts @@ -9,7 +9,7 @@ export const imageHandlers = [ return res( ctx.status(HTTP_STATUS_CODE.CREATED), ctx.delay(2000), - ctx.json({ imageUrls: images }) + ctx.json({ imageNames: images }) ); }), ]; diff --git a/frontend/src/mocks/handlers/myTrips.ts b/frontend/src/mocks/handlers/myTrips.ts index 766a7ff71..8b371853b 100644 --- a/frontend/src/mocks/handlers/myTrips.ts +++ b/frontend/src/mocks/handlers/myTrips.ts @@ -33,7 +33,7 @@ export const myTripsHandlers = [ trip.startDate = response.startDate; trip.endDate = response.endDate; trip.description = response.description; - trip.imageUrl = response.imageUrl; + trip.imageName = response.imageName; return res(ctx.status(HTTP_STATUS_CODE.NO_CONTENT)); }), diff --git a/frontend/src/mocks/handlers/tripItem.ts b/frontend/src/mocks/handlers/tripItem.ts index 4ceda5c73..872cec452 100644 --- a/frontend/src/mocks/handlers/tripItem.ts +++ b/frontend/src/mocks/handlers/tripItem.ts @@ -39,7 +39,7 @@ export const tripItemHandlers = [ }, } : null, - imageUrls: [], + imageNames: response.imageNames, }; trip.dayLogs[0].items.push(newTripItem); @@ -105,7 +105,7 @@ export const tripItemHandlers = [ }, } : null, - imageUrls: response.imageUrls, + imageNames: response.imageNames, }; return res(ctx.status(HTTP_STATUS_CODE.NO_CONTENT)); diff --git a/frontend/src/pages/CommunityTripPage/CommunityTripPage.tsx b/frontend/src/pages/CommunityTripPage/CommunityTripPage.tsx index 62366407c..ad6c07e90 100644 --- a/frontend/src/pages/CommunityTripPage/CommunityTripPage.tsx +++ b/frontend/src/pages/CommunityTripPage/CommunityTripPage.tsx @@ -27,13 +27,7 @@ const CommunityTripPage = () => { return (
    - + { +const ExpensePage = ({ tripType = 'PERSONAL' }: ExpensePageProps) => { const { tripId } = useParams(); if (!tripId) throw new Error('존재하지 않는 tripId 입니다.'); const isMobile = useRecoilValue(mediaQueryMobileState); - useExpenseQuery(tripId, { isShared, isPublished }); + useExpenseQuery(tripId, tripType); return ( - + {isMobile && } - + ); }; diff --git a/frontend/src/pages/MyTripsPage/MyTripsPage.tsx b/frontend/src/pages/MyTripsPage/MyTripsPage.tsx index 88e00e5ed..13b3ad625 100644 --- a/frontend/src/pages/MyTripsPage/MyTripsPage.tsx +++ b/frontend/src/pages/MyTripsPage/MyTripsPage.tsx @@ -9,7 +9,7 @@ import TripsItemList from '@components/trips/TripsItemList/TripsItemList'; import { useTripsQuery } from '@hooks/api/useTripsQuery'; -import { sortByStartDate } from '@utils/sort'; +import { sortByNewest, sortByStartDate } from '@utils/sort'; import { ORDER_BY_DATE, ORDER_BY_REGISTRATION } from '@constants/order'; import { PATH } from '@constants/path'; @@ -21,7 +21,9 @@ const TripsPage = () => { useSelect(ORDER_BY_REGISTRATION); const sortedTrips = - sortSelected === ORDER_BY_DATE ? tripsData?.slice().sort(sortByStartDate) : tripsData; + sortSelected === ORDER_BY_DATE + ? tripsData?.slice().sort(sortByStartDate) + : tripsData?.slice().sort(sortByNewest); return ( <> diff --git a/frontend/src/pages/SharedPage/SharedTripPage.tsx b/frontend/src/pages/SharedPage/SharedTripPage.tsx index ab7e06a74..e52b70ec4 100644 --- a/frontend/src/pages/SharedPage/SharedTripPage.tsx +++ b/frontend/src/pages/SharedPage/SharedTripPage.tsx @@ -27,7 +27,7 @@ const SharedTripPage = () => { return (
    - + { return (
    - + { const [isDaylogShown, setIsDaylogShown] = useState(true); const { tripId } = useParams(); @@ -40,13 +38,7 @@ const TripMobilePage = ({ tripType }: { tripType: TripTypeData }) => { return (
    - +
    {dates.map((date, index) => { diff --git a/frontend/src/router/AppRouter.tsx b/frontend/src/router/AppRouter.tsx index 821a79eac..8acbd884c 100644 --- a/frontend/src/router/AppRouter.tsx +++ b/frontend/src/router/AppRouter.tsx @@ -118,7 +118,7 @@ const AppRouter = () => { path: PATH.SHARE_EXPENSE(':tripId'), element: ( }> - + ), }, @@ -138,7 +138,7 @@ const AppRouter = () => { path: PATH.COMMUNITY_EXPENSE(':tripId'), element: ( }> - + ), }, diff --git a/frontend/src/stories/expense/ExpenseCategories.stories.tsx b/frontend/src/stories/expense/ExpenseCategories.stories.tsx index d310f262b..74b78d027 100644 --- a/frontend/src/stories/expense/ExpenseCategories.stories.tsx +++ b/frontend/src/stories/expense/ExpenseCategories.stories.tsx @@ -9,11 +9,11 @@ const meta = { component: ExpenseCategories, args: { tripId: '1', - isShared: false, + tripType: 'PERSONAL', }, decorators: [ (Story) => { - useExpenseQuery('1', { isShared: false, isPublished: false }); + useExpenseQuery('1', 'PERSONAL'); return ; }, diff --git a/frontend/src/stories/expense/ExpenseCategoryInformation.stories.tsx b/frontend/src/stories/expense/ExpenseCategoryInformation.stories.tsx index 44856d24b..ec5552629 100644 --- a/frontend/src/stories/expense/ExpenseCategoryInformation.stories.tsx +++ b/frontend/src/stories/expense/ExpenseCategoryInformation.stories.tsx @@ -9,10 +9,11 @@ const meta = { component: ExpenseCategoryInformation, args: { tripId: '1', + tripType: 'PERSONAL', }, decorators: [ (Story) => { - useExpenseQuery('1', { isShared: false, isPublished: false }); + useExpenseQuery('1', 'PERSONAL'); return ; }, diff --git a/frontend/src/stories/expense/ExpenseDates.stories.tsx b/frontend/src/stories/expense/ExpenseDates.stories.tsx index 735f7a17e..c2b652c12 100644 --- a/frontend/src/stories/expense/ExpenseDates.stories.tsx +++ b/frontend/src/stories/expense/ExpenseDates.stories.tsx @@ -9,11 +9,11 @@ const meta = { component: ExpenseDates, args: { tripId: '1', - isShared: false, + tripType: 'PERSONAL', }, decorators: [ (Story) => { - useExpenseQuery('1', { isShared: false, isPublished: false }); + useExpenseQuery('1', 'PERSONAL'); return ; }, diff --git a/frontend/src/stories/expense/ExpenseInformation.stories.tsx b/frontend/src/stories/expense/ExpenseInformation.stories.tsx index 095448062..ec305bb08 100644 --- a/frontend/src/stories/expense/ExpenseInformation.stories.tsx +++ b/frontend/src/stories/expense/ExpenseInformation.stories.tsx @@ -9,12 +9,11 @@ const meta = { component: ExpenseInformation, args: { tripId: '1', - isShared: false, - isPublished: false, + tripType: 'PERSONAL', }, decorators: [ (Story) => { - useExpenseQuery('1', { isShared: false, isPublished: false }); + useExpenseQuery('1', 'PERSONAL'); return ; }, diff --git a/frontend/src/stories/expense/ExpenseListSection.stories.tsx b/frontend/src/stories/expense/ExpenseListSection.stories.tsx index b06d27e10..7af13be3e 100644 --- a/frontend/src/stories/expense/ExpenseListSection.stories.tsx +++ b/frontend/src/stories/expense/ExpenseListSection.stories.tsx @@ -10,7 +10,7 @@ const meta = { }, args: { tripId: '1', - isShared: false, + tripType: 'PERSONAL', }, } satisfies Meta; diff --git a/frontend/src/stories/expense/TotalExpenseSection.stories.tsx b/frontend/src/stories/expense/TotalExpenseSection.stories.tsx index 041f76b5a..71f509e21 100644 --- a/frontend/src/stories/expense/TotalExpenseSection.stories.tsx +++ b/frontend/src/stories/expense/TotalExpenseSection.stories.tsx @@ -12,12 +12,11 @@ const meta = { }, args: { tripId: '1', - isShared: false, - isPublished: false, + tripType: 'PERSONAL', }, decorators: [ (Story) => { - useExpenseQuery('1', { isShared: false, isPublished: false }); + useExpenseQuery('1', 'PERSONAL'); return ; }, diff --git a/frontend/src/stories/trip/TripItemAddModal.stories.tsx b/frontend/src/stories/trip/TripItemAddModal.stories.tsx index 40b142694..04360a8cf 100644 --- a/frontend/src/stories/trip/TripItemAddModal.stories.tsx +++ b/frontend/src/stories/trip/TripItemAddModal.stories.tsx @@ -48,7 +48,7 @@ export const WithInitialData: Story = { categoryId: item.expense!.id, }, memo: item.memo, - imageUrls: item.imageUrls, + imageNames: item.imageNames, }, }, }; diff --git a/frontend/src/stories/trips/TripsItem.stories.tsx b/frontend/src/stories/trips/TripsItem.stories.tsx index 9f2bf2167..6edfef504 100644 --- a/frontend/src/stories/trips/TripsItem.stories.tsx +++ b/frontend/src/stories/trips/TripsItem.stories.tsx @@ -14,7 +14,7 @@ const meta = { index: 0, itemName: trips[0].title, cityTags: trips[0].cities, - coverImage: trips[0].imageUrl, + coverImage: trips[0].imageName, duration: `${formatDate(trips[0].startDate)} - ${formatDate(trips[0].endDate)}`, description: trips[0].description, }, diff --git a/frontend/src/types/image.ts b/frontend/src/types/image.ts index 4d2e2792c..bb32196bb 100644 --- a/frontend/src/types/image.ts +++ b/frontend/src/types/image.ts @@ -1,3 +1,3 @@ export interface ImageData { - imageUrls: string[]; + imageNames: string[]; } diff --git a/frontend/src/types/trip.ts b/frontend/src/types/trip.ts index 71dc635e0..9c3abb24f 100644 --- a/frontend/src/types/trip.ts +++ b/frontend/src/types/trip.ts @@ -10,7 +10,7 @@ export interface TripData { startDate: string; endDate: string; description: string | null; - imageUrl: string | null; + imageName: string | null; cities: CityData[]; dayLogs: DayLogData[]; writer: { diff --git a/frontend/src/types/tripItem.ts b/frontend/src/types/tripItem.ts index 5de608747..3a354cb74 100644 --- a/frontend/src/types/tripItem.ts +++ b/frontend/src/types/tripItem.ts @@ -34,7 +34,7 @@ export interface TripItemData { memo: string | null; place: PlaceData | null; expense: ExpenseData | null; - imageUrls: string[]; + imageNames: string[]; } export type TripItemCategory = '장소' | '기타'; @@ -57,5 +57,5 @@ export interface TripItemFormData { categoryId: number; } | null; memo: string | null; - imageUrls: string[]; + imageNames: string[]; } diff --git a/frontend/src/types/trips.ts b/frontend/src/types/trips.ts index e5dc0da73..3b20e9fe5 100644 --- a/frontend/src/types/trips.ts +++ b/frontend/src/types/trips.ts @@ -3,7 +3,7 @@ import type { CityData } from '@type/city'; export interface TripsData { id: number; title: string; - imageUrl: string | null; + imageName: string | null; cities: CityData[]; startDate: string; endDate: string; diff --git a/frontend/src/utils/convertImage.ts b/frontend/src/utils/convertImage.ts new file mode 100644 index 000000000..c8b75b599 --- /dev/null +++ b/frontend/src/utils/convertImage.ts @@ -0,0 +1,13 @@ +export const convertToImageUrl = (imageName: string | null) => { + return `${process.env.IMAGE_BASEURL}${imageName}`; +}; + +export const convertToImageUrls = (imageNames: string[]) => { + return [...imageNames]?.map((imageName) => `${process.env.IMAGE_BASEURL}${imageName}`); +}; + +export const convertToImageNames = (imageUrls: string[]) => { + return [...imageUrls]?.map((imageUrl) => + imageUrl.replace('blob:', '').replace(`${process.env.IMAGE_BASEURL}`, '') + ); +}; diff --git a/frontend/src/utils/sort.ts b/frontend/src/utils/sort.ts index 7405395d2..bf40dcb00 100644 --- a/frontend/src/utils/sort.ts +++ b/frontend/src/utils/sort.ts @@ -4,5 +4,12 @@ export const sortByStartDate = (a: TripsData, b: TripsData) => { const dateA = new Date(a.startDate); const dateB = new Date(b.startDate); - return dateA.getTime() - dateB.getTime(); + return dateB.getTime() - dateA.getTime(); +}; + +export const sortByNewest = (a: TripsData, b: TripsData) => { + const tripA = a.id; + const tripB = b.id; + + return tripB - tripA; };