diff --git a/.gitignore b/.gitignore index 1d78857..39e0b93 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,9 @@ application-stage.yml application-prod.yml application-local.yml !/docker/elasticsearch +/src/main/resources/firebase/ + +## FCM +secrets/ +secrets/ +**/firebase*.json diff --git a/build.gradle b/build.gradle index 8d0b61c..64251d2 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.flywaydb:flyway-core' implementation 'org.flywaydb:flyway-mysql' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0' + implementation 'io.github.resilience4j:resilience4j-reactor:2.2.0' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9' @@ -66,6 +70,10 @@ dependencies { annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + //FCM + implementation 'com.google.firebase:firebase-admin:9.2.0' + } tasks.named('test') { diff --git a/src/main/java/org/atdev/artrip/domain/Enum/KeywordType.java b/src/main/java/org/atdev/artrip/domain/Enum/KeywordType.java index 2ae42fe..1a732fc 100644 --- a/src/main/java/org/atdev/artrip/domain/Enum/KeywordType.java +++ b/src/main/java/org/atdev/artrip/domain/Enum/KeywordType.java @@ -1,6 +1,6 @@ -package org.atdev.artrip.domain.Enum; + package org.atdev.artrip.domain.Enum; -public enum KeywordType { - GENRE, - STYLE -} + public enum KeywordType { + GENRE, + STYLE + } diff --git a/src/main/java/org/atdev/artrip/domain/Enum/Platform.java b/src/main/java/org/atdev/artrip/domain/Enum/Platform.java new file mode 100644 index 0000000..ea2e8ff --- /dev/null +++ b/src/main/java/org/atdev/artrip/domain/Enum/Platform.java @@ -0,0 +1,6 @@ +package org.atdev.artrip.domain.Enum; + +public enum Platform { + ANDROID, + IOS +} \ No newline at end of file diff --git a/src/main/java/org/atdev/artrip/domain/Notification.java b/src/main/java/org/atdev/artrip/domain/Notification.java index 5cf54c6..c209b78 100644 --- a/src/main/java/org/atdev/artrip/domain/Notification.java +++ b/src/main/java/org/atdev/artrip/domain/Notification.java @@ -7,7 +7,7 @@ import java.time.LocalDateTime; @Entity -@Table(name = "notification", schema = "art_dev") +@Table(name = "notification") @Getter @Setter @NoArgsConstructor diff --git a/src/main/java/org/atdev/artrip/domain/RecentExhibit.java b/src/main/java/org/atdev/artrip/domain/RecentExhibit.java index 5e8d573..152496c 100644 --- a/src/main/java/org/atdev/artrip/domain/RecentExhibit.java +++ b/src/main/java/org/atdev/artrip/domain/RecentExhibit.java @@ -8,7 +8,7 @@ import java.time.LocalDateTime; @Entity -@Table(name = "recent_exhibit", schema = "art_dev") +@Table(name = "recent_exhibit") @Data public class RecentExhibit { @Id diff --git a/src/main/java/org/atdev/artrip/domain/Stamp.java b/src/main/java/org/atdev/artrip/domain/Stamp.java index d83b05c..153f7b9 100644 --- a/src/main/java/org/atdev/artrip/domain/Stamp.java +++ b/src/main/java/org/atdev/artrip/domain/Stamp.java @@ -7,7 +7,7 @@ import java.time.LocalDateTime; @Entity -@Table(name = "stamp", schema = "art_dev") +@Table(name = "stamp") @Getter @Setter @NoArgsConstructor diff --git a/src/main/java/org/atdev/artrip/domain/admin/exhibit/dto/CreateExhibitRequest.java b/src/main/java/org/atdev/artrip/domain/admin/exhibit/dto/CreateExhibitRequest.java index 2f73200..9ae5a41 100644 --- a/src/main/java/org/atdev/artrip/domain/admin/exhibit/dto/CreateExhibitRequest.java +++ b/src/main/java/org/atdev/artrip/domain/admin/exhibit/dto/CreateExhibitRequest.java @@ -10,29 +10,17 @@ @Data public class CreateExhibitRequest { + private Long exhibitHallId; private String title; private String description; - - private Long exhibitHallId; - private String exhibitHallName; - private String address; - private String country; - private String region; - private String phone; + private String ticketUrl; + private String posterUrl; private LocalDateTime startDate; private LocalDateTime endDate; - private String openingHours; - private Status status; - private String posterUrl; // 이미지 URL - private String ticketUrl; - - private BigDecimal latitude; - private BigDecimal longitude; - private List keywordIds; } diff --git a/src/main/java/org/atdev/artrip/domain/admin/exhibit/dto/ExhibitAdminResponse.java b/src/main/java/org/atdev/artrip/domain/admin/exhibit/dto/ExhibitAdminResponse.java index ac94474..5286180 100644 --- a/src/main/java/org/atdev/artrip/domain/admin/exhibit/dto/ExhibitAdminResponse.java +++ b/src/main/java/org/atdev/artrip/domain/admin/exhibit/dto/ExhibitAdminResponse.java @@ -30,9 +30,6 @@ public class ExhibitAdminResponse { private String posterUrl; private String ticketUrl; - private BigDecimal latitude; - private BigDecimal longitude; - private List keywords; private LocalDateTime createdAt; diff --git a/src/main/java/org/atdev/artrip/domain/admin/exhibit/service/AdminExhibitService.java b/src/main/java/org/atdev/artrip/domain/admin/exhibit/service/AdminExhibitService.java index 18e8feb..23f7bd8 100644 --- a/src/main/java/org/atdev/artrip/domain/admin/exhibit/service/AdminExhibitService.java +++ b/src/main/java/org/atdev/artrip/domain/admin/exhibit/service/AdminExhibitService.java @@ -71,17 +71,10 @@ public ExhibitAdminResponse getExhibit (Long exhibitId) { public Long createExhibit(CreateExhibitRequest request) { log.info("Admin Creating exhibit : title={}", request); - ExhibitHall exhibitHall = getOrCreateExhibitHall( - request.getExhibitHallId(), - request.getExhibitHallName(), - request.getAddress(), - request.getCountry(), - request.getRegion(), - request.getPhone(), - request.getOpeningHours()); + ExhibitHall exhibitHall = exhibitHallRepository.findById(request.getExhibitHallId()) + .orElseThrow(() -> new GeneralException(ExhibitError._EXHIBIT_HALL_NOT_FOUND)); List keywords = List.of(); - if (request.getKeywordIds() != null && !request.getKeywordIds().isEmpty()) { keywords = keywordRepository.findAllById(request.getKeywordIds()); if (keywords.size() != request.getKeywordIds().size()) { @@ -98,8 +91,6 @@ public Long createExhibit(CreateExhibitRequest request) { .status(request.getStatus()) .ticketUrl(request.getTicketUrl()) .posterUrl(request.getPosterUrl()) - .latitude(request.getLatitude()) - .longitude(request.getLongitude()) .createdAt(LocalDateTime.now()) .updatedAt(LocalDateTime.now()) .build(); @@ -242,8 +233,6 @@ public ExhibitAdminResponse convertToAdminResponse(Exhibit exhibit) { .status(exhibit.getStatus()) .posterUrl(exhibit.getPosterUrl()) .ticketUrl(exhibit.getTicketUrl()) - .latitude(exhibit.getLatitude()) - .longitude(exhibit.getLongitude()) .keywords(exhibit.getKeywords().stream() .map(this::convertToKeywordInfo) .collect(Collectors.toList())) diff --git a/src/main/java/org/atdev/artrip/domain/admin/exhibitHall/dto/CreateExhibitHallRequest.java b/src/main/java/org/atdev/artrip/domain/admin/exhibitHall/dto/CreateExhibitHallRequest.java index 22483a9..7e0c3a5 100644 --- a/src/main/java/org/atdev/artrip/domain/admin/exhibitHall/dto/CreateExhibitHallRequest.java +++ b/src/main/java/org/atdev/artrip/domain/admin/exhibitHall/dto/CreateExhibitHallRequest.java @@ -2,6 +2,8 @@ import lombok.Data; +import java.math.BigDecimal; + @Data public class CreateExhibitHallRequest { @@ -14,4 +16,6 @@ public class CreateExhibitHallRequest { private String openingHours; private Boolean isDomestic; private String closedDays; + private BigDecimal longitude; + private BigDecimal latitude; } diff --git a/src/main/java/org/atdev/artrip/domain/admin/exhibitHall/dto/ExhibitHallResponse.java b/src/main/java/org/atdev/artrip/domain/admin/exhibitHall/dto/ExhibitHallResponse.java index 45b5b75..b2cd2e4 100644 --- a/src/main/java/org/atdev/artrip/domain/admin/exhibitHall/dto/ExhibitHallResponse.java +++ b/src/main/java/org/atdev/artrip/domain/admin/exhibitHall/dto/ExhibitHallResponse.java @@ -3,6 +3,8 @@ import lombok.Builder; import lombok.Data; +import java.math.BigDecimal; + @Data @Builder public class ExhibitHallResponse { @@ -18,5 +20,7 @@ public class ExhibitHallResponse { private Boolean isDomestic; private Long exhibitCount; private String closedDays; + private BigDecimal longitude; + private BigDecimal latitude; } diff --git a/src/main/java/org/atdev/artrip/domain/admin/exhibitHall/service/AdminExhibitHallService.java b/src/main/java/org/atdev/artrip/domain/admin/exhibitHall/service/AdminExhibitHallService.java index 0bc37c5..c6374ab 100644 --- a/src/main/java/org/atdev/artrip/domain/admin/exhibitHall/service/AdminExhibitHallService.java +++ b/src/main/java/org/atdev/artrip/domain/admin/exhibitHall/service/AdminExhibitHallService.java @@ -71,6 +71,8 @@ public Long createExhibitHall(CreateExhibitHallRequest request) { .openingHours(request.getOpeningHours()) .closedDays(request.getClosedDays()) .isDomestic(request.getIsDomestic()) + .longitude(request.getLongitude()) + .latitude(request.getLatitude()) .createdAt(LocalDateTime.now()) .updatedAt(LocalDateTime.now()) .build(); @@ -153,6 +155,8 @@ private ExhibitHallResponse convertToResponse(ExhibitHall hall) { .isDomestic(hall.getIsDomestic()) .exhibitCount(exhibitCount) .closedDays(hall.getClosedDays()) + .latitude(hall.getLatitude()) + .longitude(hall.getLongitude()) .build(); } } diff --git a/src/main/java/org/atdev/artrip/domain/auth/Oauth/CustomOAuth2UserService.java b/src/main/java/org/atdev/artrip/domain/auth/Oauth/CustomOAuth2UserService.java index a8c8f78..8b5da37 100644 --- a/src/main/java/org/atdev/artrip/domain/auth/Oauth/CustomOAuth2UserService.java +++ b/src/main/java/org/atdev/artrip/domain/auth/Oauth/CustomOAuth2UserService.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import org.atdev.artrip.domain.Enum.Provider; import org.atdev.artrip.domain.Enum.Role; -import org.atdev.artrip.domain.SocialAccounts; +import org.atdev.artrip.domain.auth.data.SocialAccounts; import org.atdev.artrip.domain.auth.data.User; import org.atdev.artrip.domain.auth.repository.UserRepository; import org.springframework.security.core.authority.SimpleGrantedAuthority; diff --git a/src/main/java/org/atdev/artrip/domain/SocialAccounts.java b/src/main/java/org/atdev/artrip/domain/auth/data/SocialAccounts.java similarity index 88% rename from src/main/java/org/atdev/artrip/domain/SocialAccounts.java rename to src/main/java/org/atdev/artrip/domain/auth/data/SocialAccounts.java index 07c4d2f..4cfb745 100644 --- a/src/main/java/org/atdev/artrip/domain/SocialAccounts.java +++ b/src/main/java/org/atdev/artrip/domain/auth/data/SocialAccounts.java @@ -1,15 +1,14 @@ -package org.atdev.artrip.domain; +package org.atdev.artrip.domain.auth.data; import jakarta.persistence.*; import lombok.*; import org.atdev.artrip.domain.Enum.Provider; -import org.atdev.artrip.domain.auth.data.User; import org.hibernate.annotations.CreationTimestamp; import java.time.LocalDateTime; @Entity -@Table(name = "social_accounts", schema = "art_dev") +@Table(name = "social_accounts") @Getter @Setter @NoArgsConstructor diff --git a/src/main/java/org/atdev/artrip/domain/auth/data/User.java b/src/main/java/org/atdev/artrip/domain/auth/data/User.java index f3d9762..aee24f1 100644 --- a/src/main/java/org/atdev/artrip/domain/auth/data/User.java +++ b/src/main/java/org/atdev/artrip/domain/auth/data/User.java @@ -4,12 +4,9 @@ import jakarta.validation.constraints.Email; import lombok.*; import org.atdev.artrip.domain.Enum.Role; -import org.atdev.artrip.domain.SocialAccounts; -import org.hibernate.annotations.CreationTimestamp; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import java.sql.Timestamp; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -48,6 +45,7 @@ public class User { @Column(name = "push_token") private String pushToken; + @Builder.Default @Column(nullable = false) private boolean onboardingCompleted=false; diff --git a/src/main/java/org/atdev/artrip/domain/auth/jwt/JwtProvider.java b/src/main/java/org/atdev/artrip/domain/auth/jwt/JwtProvider.java index e2ea329..d627501 100644 --- a/src/main/java/org/atdev/artrip/domain/auth/jwt/JwtProvider.java +++ b/src/main/java/org/atdev/artrip/domain/auth/jwt/JwtProvider.java @@ -6,7 +6,6 @@ import io.jsonwebtoken.security.Keys; import lombok.extern.slf4j.Slf4j; import org.atdev.artrip.domain.auth.jwt.exception.JwtAuthenticationException; -import org.atdev.artrip.domain.auth.web.dto.ReissueRequest; import org.atdev.artrip.global.apipayload.code.status.UserError; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; diff --git a/src/main/java/org/atdev/artrip/domain/auth/repository/SocialRepository.java b/src/main/java/org/atdev/artrip/domain/auth/repository/SocialRepository.java index 082adf4..1bfc473 100644 --- a/src/main/java/org/atdev/artrip/domain/auth/repository/SocialRepository.java +++ b/src/main/java/org/atdev/artrip/domain/auth/repository/SocialRepository.java @@ -1,6 +1,6 @@ package org.atdev.artrip.domain.auth.repository; -import org.atdev.artrip.domain.SocialAccounts; +import org.atdev.artrip.domain.auth.data.SocialAccounts; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/org/atdev/artrip/domain/auth/service/AuthService.java b/src/main/java/org/atdev/artrip/domain/auth/service/AuthService.java index 87220ff..74160b0 100644 --- a/src/main/java/org/atdev/artrip/domain/auth/service/AuthService.java +++ b/src/main/java/org/atdev/artrip/domain/auth/service/AuthService.java @@ -14,16 +14,16 @@ import lombok.extern.slf4j.Slf4j; import org.atdev.artrip.domain.Enum.Provider; import org.atdev.artrip.domain.Enum.Role; -import org.atdev.artrip.domain.SocialAccounts; +import org.atdev.artrip.domain.auth.data.SocialAccounts; import org.atdev.artrip.domain.auth.data.User; import org.atdev.artrip.domain.auth.jwt.JwtGenerator; import org.atdev.artrip.domain.auth.jwt.JwtProvider; import org.atdev.artrip.domain.auth.jwt.JwtToken; import org.atdev.artrip.domain.auth.jwt.repository.RefreshTokenRedisRepository; import org.atdev.artrip.domain.auth.repository.UserRepository; -import org.atdev.artrip.domain.auth.web.dto.ReissueRequest; -import org.atdev.artrip.domain.auth.web.dto.SocialLoginResponse; -import org.atdev.artrip.domain.auth.web.dto.SocialUserInfo; +import org.atdev.artrip.domain.auth.web.dto.request.ReissueRequest; +import org.atdev.artrip.domain.auth.web.dto.response.SocialLoginResponse; +import org.atdev.artrip.domain.auth.web.dto.response.SocialUserInfo; import org.atdev.artrip.global.apipayload.code.status.UserError; import org.atdev.artrip.global.apipayload.exception.GeneralException; import org.springframework.beans.factory.annotation.Value; @@ -34,7 +34,6 @@ import java.net.URL; import java.security.interfaces.RSAPublicKey; import java.util.List; -import java.util.Optional; @Slf4j @Service diff --git a/src/main/java/org/atdev/artrip/domain/auth/web/controller/AuthController.java b/src/main/java/org/atdev/artrip/domain/auth/web/controller/AuthController.java index 79416a6..499fdc7 100644 --- a/src/main/java/org/atdev/artrip/domain/auth/web/controller/AuthController.java +++ b/src/main/java/org/atdev/artrip/domain/auth/web/controller/AuthController.java @@ -1,19 +1,13 @@ package org.atdev.artrip.domain.auth.web.controller; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; -import org.atdev.artrip.domain.auth.jwt.JwtToken; -import org.atdev.artrip.domain.auth.jwt.repository.RefreshTokenRedisRepository; import org.atdev.artrip.domain.auth.service.AuthService; -import org.atdev.artrip.domain.auth.web.dto.ReissueRequest; -import org.atdev.artrip.domain.auth.web.dto.SocialLoginRequest; -import org.atdev.artrip.domain.auth.web.dto.SocialLoginResponse; +import org.atdev.artrip.domain.auth.web.dto.request.ReissueRequest; +import org.atdev.artrip.domain.auth.web.dto.request.SocialLoginRequest; +import org.atdev.artrip.domain.auth.web.dto.response.SocialLoginResponse; import org.atdev.artrip.global.apipayload.CommonResponse; import org.atdev.artrip.global.apipayload.code.status.CommonError; import org.atdev.artrip.global.apipayload.code.status.UserError; diff --git a/src/main/java/org/atdev/artrip/domain/auth/web/dto/ReissueRequest.java b/src/main/java/org/atdev/artrip/domain/auth/web/dto/request/ReissueRequest.java similarity index 63% rename from src/main/java/org/atdev/artrip/domain/auth/web/dto/ReissueRequest.java rename to src/main/java/org/atdev/artrip/domain/auth/web/dto/request/ReissueRequest.java index 557f4ef..ff40d03 100644 --- a/src/main/java/org/atdev/artrip/domain/auth/web/dto/ReissueRequest.java +++ b/src/main/java/org/atdev/artrip/domain/auth/web/dto/request/ReissueRequest.java @@ -1,4 +1,4 @@ -package org.atdev.artrip.domain.auth.web.dto; +package org.atdev.artrip.domain.auth.web.dto.request; import lombok.Data; diff --git a/src/main/java/org/atdev/artrip/domain/auth/web/dto/SocialLoginRequest.java b/src/main/java/org/atdev/artrip/domain/auth/web/dto/request/SocialLoginRequest.java similarity index 69% rename from src/main/java/org/atdev/artrip/domain/auth/web/dto/SocialLoginRequest.java rename to src/main/java/org/atdev/artrip/domain/auth/web/dto/request/SocialLoginRequest.java index 7b67023..dfe5e4e 100644 --- a/src/main/java/org/atdev/artrip/domain/auth/web/dto/SocialLoginRequest.java +++ b/src/main/java/org/atdev/artrip/domain/auth/web/dto/request/SocialLoginRequest.java @@ -1,4 +1,4 @@ -package org.atdev.artrip.domain.auth.web.dto; +package org.atdev.artrip.domain.auth.web.dto.request; import lombok.Data; diff --git a/src/main/java/org/atdev/artrip/domain/auth/web/dto/SocialLoginResponse.java b/src/main/java/org/atdev/artrip/domain/auth/web/dto/response/SocialLoginResponse.java similarity index 79% rename from src/main/java/org/atdev/artrip/domain/auth/web/dto/SocialLoginResponse.java rename to src/main/java/org/atdev/artrip/domain/auth/web/dto/response/SocialLoginResponse.java index c65a579..529265b 100644 --- a/src/main/java/org/atdev/artrip/domain/auth/web/dto/SocialLoginResponse.java +++ b/src/main/java/org/atdev/artrip/domain/auth/web/dto/response/SocialLoginResponse.java @@ -1,4 +1,4 @@ -package org.atdev.artrip.domain.auth.web.dto; +package org.atdev.artrip.domain.auth.web.dto.response; import lombok.AllArgsConstructor; diff --git a/src/main/java/org/atdev/artrip/domain/auth/web/dto/SocialUserInfo.java b/src/main/java/org/atdev/artrip/domain/auth/web/dto/response/SocialUserInfo.java similarity index 78% rename from src/main/java/org/atdev/artrip/domain/auth/web/dto/SocialUserInfo.java rename to src/main/java/org/atdev/artrip/domain/auth/web/dto/response/SocialUserInfo.java index b2d3fa4..8920ef2 100644 --- a/src/main/java/org/atdev/artrip/domain/auth/web/dto/SocialUserInfo.java +++ b/src/main/java/org/atdev/artrip/domain/auth/web/dto/response/SocialUserInfo.java @@ -1,4 +1,4 @@ -package org.atdev.artrip.domain.auth.web.dto; +package org.atdev.artrip.domain.auth.web.dto.response; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/org/atdev/artrip/domain/exhibit/data/Exhibit.java b/src/main/java/org/atdev/artrip/domain/exhibit/data/Exhibit.java index 1cc5549..b4bbd2d 100644 --- a/src/main/java/org/atdev/artrip/domain/exhibit/data/Exhibit.java +++ b/src/main/java/org/atdev/artrip/domain/exhibit/data/Exhibit.java @@ -13,7 +13,7 @@ import java.util.Set; @Entity -@Table(name = "exhibit", schema = "art_dev") +@Table(name = "exhibit") @EntityListeners(ExhibitEntityListener.class) @Getter @Setter @@ -59,14 +59,6 @@ public class Exhibit { @Column(name = "updated_at") private LocalDateTime updatedAt; - @Column(name = "latitude") - private BigDecimal latitude; - - @Column(name = "longitude") - private BigDecimal longitude; - - //enum 생성 정렬 등등 - @ManyToMany @Builder.Default @JoinTable( diff --git a/src/main/java/org/atdev/artrip/domain/exhibit/reponse/ExhibitDetailResponse.java b/src/main/java/org/atdev/artrip/domain/exhibit/reponse/ExhibitDetailResponse.java index 8728563..bbe12d7 100644 --- a/src/main/java/org/atdev/artrip/domain/exhibit/reponse/ExhibitDetailResponse.java +++ b/src/main/java/org/atdev/artrip/domain/exhibit/reponse/ExhibitDetailResponse.java @@ -4,6 +4,8 @@ import lombok.Getter; import org.atdev.artrip.domain.Enum.Status; +import java.math.BigDecimal; + @Getter @Builder public class ExhibitDetailResponse { @@ -21,4 +23,7 @@ public class ExhibitDetailResponse { private String hallAddress; private String hallOpeningHours; private String hallPhone; + private Double hallLatitude; + private Double hallLongitude; + } diff --git a/src/main/java/org/atdev/artrip/domain/exhibit/repository/ExhibitRepository.java b/src/main/java/org/atdev/artrip/domain/exhibit/repository/ExhibitRepository.java index 2a7f882..bcb71a5 100644 --- a/src/main/java/org/atdev/artrip/domain/exhibit/repository/ExhibitRepository.java +++ b/src/main/java/org/atdev/artrip/domain/exhibit/repository/ExhibitRepository.java @@ -20,17 +20,6 @@ public interface ExhibitRepository extends JpaRepository,ExhibitRepositoryCustom{ - @Query(value = """ - SELECT e.* - FROM exhibit e - JOIN exhibit_hall h ON e.exhibit_hall_id = h.exhibit_hall_id - WHERE (:isDomestic IS NULL OR h.is_domestic = :isDomestic) - ORDER BY RAND() - LIMIT :limit - """, nativeQuery = true) - List findRandomExhibits(@Param("limit") int limit, @Param("isDomestic") Boolean isDomestic); - - @Query(value = """ SELECT e.* FROM exhibit e @@ -71,26 +60,6 @@ List findAllByGenreAndDomestic( List findAllGenres(); - @Query(value = """ - SELECT e.* - FROM exhibit e - JOIN exhibit_keyword ek ON e.exhibit_id = ek.exhibit_id - JOIN keyword k ON ek.keyword_id = k.keyword_id - JOIN exhibit_hall h ON e.exhibit_hall_id = h.exhibit_hall_id - WHERE e.end_date >= NOW() - AND (:isDomestic IS NULL OR h.is_domestic = :isDomestic) - AND ((k.type = 'GENRE' AND k.name IN (:genres)) - OR (k.type = 'STYLE' AND k.name IN (:styles))) - ORDER BY RAND() - LIMIT :limit - """, nativeQuery = true) - List findRandomByKeywords( - @Param("genres") Set genres, - @Param("styles") Set styles, - @Param("limit") int limit, - @Param("isDomestic") Boolean isDomestic - ); - @Query(value = """ SELECT e.* FROM exhibit e @@ -128,18 +97,6 @@ WHERE status IN ('ONGOING', 'ENDING_SOON') int updateFinishedStatus(); - @Query(value = """ - SELECT e.* - FROM exhibit e - JOIN exhibit_hall h ON e.exhibit_hall_id = h.exhibit_hall_id - WHERE :date BETWEEN e.start_date AND e.end_date - AND (:isDomestic IS NULL OR h.is_domestic = :isDomestic) - ORDER BY RAND() - LIMIT :limit - """, nativeQuery = true) - List findRandomExhibitsByDate(@Param("isDomestic") Boolean isDomestic, - @Param("date") LocalDate date, - @Param("limit") int limit); @Query(value = """ SELECT e.* @@ -181,5 +138,5 @@ ORDER BY RAND() Page findAllByRegion(@Param("region") String region, Pageable pageable); - + Optional findByTitleAndStartDate(String title, LocalDateTime startDate); } diff --git a/src/main/java/org/atdev/artrip/domain/exhibit/repository/ExhibitRepositoryCustom.java b/src/main/java/org/atdev/artrip/domain/exhibit/repository/ExhibitRepositoryCustom.java index f425a03..a795351 100644 --- a/src/main/java/org/atdev/artrip/domain/exhibit/repository/ExhibitRepositoryCustom.java +++ b/src/main/java/org/atdev/artrip/domain/exhibit/repository/ExhibitRepositoryCustom.java @@ -1,15 +1,21 @@ package org.atdev.artrip.domain.exhibit.repository; import org.atdev.artrip.domain.exhibit.data.Exhibit; -import org.atdev.artrip.domain.exhibit.web.dto.ExhibitFilterDto; -import org.springframework.data.domain.Page; +import org.atdev.artrip.domain.exhibit.web.dto.request.ExhibitFilterRequestDto; +import org.atdev.artrip.domain.home.web.dto.RandomExhibitFilterRequestDto; +import org.atdev.artrip.domain.home.response.HomeListResponse; +import org.atdev.artrip.domain.home.web.dto.RandomExhibitRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface ExhibitRepositoryCustom { - Slice findExhibitByFilters(ExhibitFilterDto filter, Pageable pageable, Long cursorId); + Slice findExhibitByFilters(ExhibitFilterRequestDto filter, Pageable pageable, Long cursorId); + + List findRandomExhibits(RandomExhibitRequest condition); } diff --git a/src/main/java/org/atdev/artrip/domain/exhibit/repository/ExhibitRepositoryImpl.java b/src/main/java/org/atdev/artrip/domain/exhibit/repository/ExhibitRepositoryImpl.java index 2c153c0..b9beda5 100644 --- a/src/main/java/org/atdev/artrip/domain/exhibit/repository/ExhibitRepositoryImpl.java +++ b/src/main/java/org/atdev/artrip/domain/exhibit/repository/ExhibitRepositoryImpl.java @@ -1,21 +1,28 @@ package org.atdev.artrip.domain.exhibit.repository; import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.atdev.artrip.domain.Enum.KeywordType; import org.atdev.artrip.domain.Enum.SortType; import org.atdev.artrip.domain.exhibit.data.Exhibit; import org.atdev.artrip.domain.exhibit.data.QExhibit; -import org.atdev.artrip.domain.exhibit.web.dto.ExhibitFilterDto; +import org.atdev.artrip.domain.exhibit.web.dto.request.ExhibitFilterRequestDto; import org.atdev.artrip.domain.exhibitHall.data.QExhibitHall; import org.atdev.artrip.domain.favortie.data.QFavoriteExhibit; +import org.atdev.artrip.domain.home.response.HomeListResponse; +import org.atdev.artrip.domain.home.web.dto.RandomExhibitRequest; import org.atdev.artrip.domain.keyword.data.QKeyword; import org.springframework.data.domain.*; import org.springframework.stereotype.Repository; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; +import java.util.Set; @Repository @RequiredArgsConstructor @@ -24,7 +31,7 @@ public class ExhibitRepositoryImpl implements ExhibitRepositoryCustom{ private final JPAQueryFactory queryFactory; @Override - public Slice findExhibitByFilters(ExhibitFilterDto dto, Pageable pageable, Long cursorId) { + public Slice findExhibitByFilters(ExhibitFilterRequestDto dto, Pageable pageable, Long cursorId) { QExhibit e = QExhibit.exhibit; QExhibitHall h = QExhibitHall.exhibitHall; @@ -58,12 +65,12 @@ public Slice findExhibitByFilters(ExhibitFilterDto dto, Pageable pageab .leftJoin(f).on(f.exhibit.eq(e)) .where( typeFilter(dto, h), - dateFilter(dto, e), - countryFilter(dto, h), - regionFilter(dto, h), - genreFilter(dto, k), - styleFilter(dto, k), - cursorCondition(cursor, cursorFavoriteCount, dto.getSortType(), e, f) + dateFilter(dto.getStartDate(), dto.getEndDate(),e), + cursorCondition(cursor, cursorFavoriteCount, dto.getSortType(), e, f), + countryEq(dto.getCountry()), + regionEq(dto.getRegion()), + genreIn(dto.getGenres()), + styleIn(dto.getStyles()) ) .orderBy(sortFilter(dto, e, f)) .limit(pageable.getPageSize() + 1) @@ -77,6 +84,42 @@ public Slice findExhibitByFilters(ExhibitFilterDto dto, Pageable pageab return new SliceImpl<>(content, pageable, hasNext); } + @Override + public List findRandomExhibits(RandomExhibitRequest c) { + + QExhibit e = QExhibit.exhibit; + QExhibitHall h = QExhibitHall.exhibitHall; + QKeyword k = QKeyword.keyword; + + return queryFactory + .selectDistinct(Projections.constructor( + HomeListResponse.class, + e.exhibitId, + e.title, + e.posterUrl, + e.status, + Expressions.stringTemplate( + "concat({0}, ' ~ ', {1})", + e.startDate.stringValue(), + e.endDate.stringValue() + ) + )) + .from(e) + .join(e.exhibitHall, h) + .leftJoin(e.keywords, k) + .where( + isDomesticEq(c.getIsDomestic()), + countryEq(c.getCountry()), + regionEq(c.getRegion()), + genreIn(c.getGenres()), + styleIn(c.getStyles()), + findDate(c.getDate()) + ) + .orderBy(Expressions.numberTemplate(Double.class, "RAND()").asc()) + .limit(c.getLimit()) + .fetch(); + } + private BooleanExpression cursorCondition(Exhibit cursor, long cursorFavoriteCount, SortType sortType, QExhibit e, QFavoriteExhibit f) { if (cursor == null) return null; @@ -96,7 +139,7 @@ private BooleanExpression cursorCondition(Exhibit cursor, long cursorFavoriteCo }; } - private OrderSpecifier[] sortFilter(ExhibitFilterDto dto, QExhibit e, QFavoriteExhibit f) { + private OrderSpecifier[] sortFilter(ExhibitFilterRequestDto dto, QExhibit e, QFavoriteExhibit f) { if (dto.getSortType() == null) { return new OrderSpecifier[]{e.createdAt.desc()}; @@ -119,7 +162,7 @@ private OrderSpecifier[] sortFilter(ExhibitFilterDto dto, QExhibit e, QFavori } } - private BooleanExpression typeFilter(ExhibitFilterDto dto, QExhibitHall h) { + private BooleanExpression typeFilter(ExhibitFilterRequestDto dto, QExhibitHall h) { if (dto.getType() == null) return null; if ("DOMESTIC".equalsIgnoreCase(dto.getType())) { @@ -130,39 +173,57 @@ private BooleanExpression typeFilter(ExhibitFilterDto dto, QExhibitHall h) { return null; } - private BooleanExpression dateFilter(ExhibitFilterDto dto, QExhibit e) { + private BooleanExpression dateFilter(LocalDate startDate, LocalDate endDate, QExhibit e) { + BooleanExpression condition = null; - if (dto.getEndDate() != null) { - condition = e.startDate.loe(dto.getEndDate().atTime(23, 59, 59)); + if (startDate == null && endDate == null) return null; + + if (endDate != null) { + condition = e.startDate.loe(endDate.atTime(23, 59, 59)); } - if (dto.getStartDate() != null) { - BooleanExpression endCond = e.endDate.goe(dto.getStartDate().atStartOfDay()); - condition = (condition == null) ? endCond : condition.and(endCond); + if (startDate != null) { + BooleanExpression startCond = e.endDate.goe(startDate.atStartOfDay()); + condition = (condition == null) ? startCond : condition.and(startCond); } return condition; } - private BooleanExpression countryFilter(ExhibitFilterDto dto, QExhibitHall h) { - return dto.getCountry() != null ? h.country.eq(dto.getCountry()) : null; + private BooleanExpression isDomesticEq(Boolean isDomestic) { + return isDomestic == null ? null : QExhibitHall.exhibitHall.isDomestic.eq(isDomestic); + } + + private BooleanExpression countryEq(String country) { + return country == null ? null : QExhibitHall.exhibitHall.country.eq(country); } - private BooleanExpression regionFilter(ExhibitFilterDto dto, QExhibitHall h) { - return dto.getRegion() != null ? h.region.eq(dto.getRegion()) : null; + private BooleanExpression regionEq(String region) { + return region == null ? null : QExhibitHall.exhibitHall.region.eq(region); } - private BooleanExpression genreFilter(ExhibitFilterDto dto, QKeyword k) { - if (dto.getGenres() == null || dto.getGenres().isEmpty()) return null; - return k.type.eq(KeywordType.GENRE) - .and(k.name.in(dto.getGenres())); + private BooleanExpression genreIn(Set genres) { + if (genres == null || genres.isEmpty()) return null; + return QKeyword.keyword.type.eq(KeywordType.GENRE) + .and(QKeyword.keyword.name.in(genres)); } - private BooleanExpression styleFilter(ExhibitFilterDto dto, QKeyword k) { - if (dto.getStyles() == null || dto.getStyles().isEmpty()) return null; - return k.type.eq(KeywordType.STYLE) - .and(k.name.in(dto.getStyles())); + private BooleanExpression styleIn(Set styles) { + if (styles == null || styles.isEmpty()) return null; + return QKeyword.keyword.type.eq(KeywordType.STYLE) + .and(QKeyword.keyword.name.in(styles)); } + private BooleanExpression findDate(LocalDate date){ + if (date == null) return null; + + LocalDateTime dayStart = date.atStartOfDay(); + LocalDateTime dayEnd = date.atTime(23, 59, 59); + + return QExhibit.exhibit.startDate.loe(dayEnd)//<= + .and(QExhibit.exhibit.endDate.goe(dayStart));//>= + } + + } diff --git a/src/main/java/org/atdev/artrip/domain/exhibit/web/controller/ExhibitController.java b/src/main/java/org/atdev/artrip/domain/exhibit/web/controller/ExhibitController.java new file mode 100644 index 0000000..5820381 --- /dev/null +++ b/src/main/java/org/atdev/artrip/domain/exhibit/web/controller/ExhibitController.java @@ -0,0 +1,169 @@ +package org.atdev.artrip.domain.exhibit.web.controller; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.atdev.artrip.domain.exhibit.reponse.ExhibitDetailResponse; +import org.atdev.artrip.domain.exhibit.web.dto.request.ExhibitFilterRequestDto; +import org.atdev.artrip.domain.home.response.FilterResponse; +import org.atdev.artrip.domain.home.response.HomeListResponse; +import org.atdev.artrip.domain.home.service.HomeService; +import org.atdev.artrip.global.apipayload.CommonResponse; +import org.atdev.artrip.global.apipayload.code.status.CommonError; +import org.atdev.artrip.global.apipayload.code.status.HomeError; +import org.atdev.artrip.global.swagger.ApiErrorResponses; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/exhibit") +public class ExhibitController { + + private final HomeService homeService; + + + @Operation(summary = "장르 조회", description = "키워드 장르 데이터 전체 조회") + @ApiErrorResponses( + common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, + home = {HomeError._HOME_GENRE_NOT_FOUND} + ) + @GetMapping("/genre") + public ResponseEntity>> getGenres(){ + List genres = homeService.getAllGenres(); + return ResponseEntity.ok(CommonResponse.onSuccess(genres)); + } + + @Operation(summary = "장르별 전시 조회", description = "true=국내, false=국외") + @ApiErrorResponses( + common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, + home = {HomeError._HOME_GENRE_NOT_FOUND} + ) + @GetMapping("/genre/all") + public ResponseEntity>> getAllExhibits( + @RequestParam String genre, + @RequestParam Boolean isDomestic){ + + List exhibits = homeService.getAllgenreExhibits(genre,isDomestic); + return ResponseEntity.ok(CommonResponse.onSuccess(exhibits)); + } + + @Operation(summary = "전시 상세 조회") + @ApiErrorResponses( + common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, + home = {HomeError._HOME_EXHIBIT_NOT_FOUND} + ) + @GetMapping("/{id}") + public ResponseEntity> getExhibit( + @PathVariable Long id){ + + ExhibitDetailResponse exhibit= homeService.getExhibitDetail(id); + + return ResponseEntity.ok(CommonResponse.onSuccess(exhibit)); + } + + @Operation(summary = "사용자 맞춤 전시 전체 조회") + @ApiErrorResponses( + common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, + home = {HomeError._HOME_EXHIBIT_NOT_FOUND} + ) + @GetMapping("/personalized/all") + public ResponseEntity>> getAllPersonalized( + @AuthenticationPrincipal UserDetails userDetails, + @RequestParam Boolean isDomestic){ + + long userId = Long.parseLong(userDetails.getUsername()); + + List exhibits= homeService.getAllPersonalized(userId,isDomestic); + + return ResponseEntity.ok(CommonResponse.onSuccess(exhibits)); + } + + @Operation(summary = "이번주 전시 일정 전체 조회") + @ApiErrorResponses( + common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, + home = {HomeError._HOME_EXHIBIT_NOT_FOUND} + ) + @GetMapping("/schedule/all") + public ResponseEntity>> getAllSchedule( + @RequestParam(name = "isDomestic", required = false) Boolean isDomestic, + @RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date){ + + List exhibits= homeService.getAllSchedule(isDomestic,date); + + return ResponseEntity.ok(CommonResponse.onSuccess(exhibits)); + } + + @Operation(summary = "해외 국가 목록 조회") + @ApiErrorResponses( + common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED} + ) + @GetMapping("/overseas") + public ResponseEntity>> getOverseas(){ + + List OverseasList = homeService.getOverseas(); + + return ResponseEntity.ok(CommonResponse.onSuccess(OverseasList)); + } + + @Operation(summary = "국내 지역 목록 조회") + @ApiErrorResponses( + common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED} + ) + @GetMapping("/domestic") + public ResponseEntity>> getDomestic(){ + + List domesticList = homeService.getDomestic(); + + return ResponseEntity.ok(CommonResponse.onSuccess(domesticList)); + } + + @Operation(summary = "해외 특정 국가 랜덤 조회",description = "특정 해외 국가 전시데이터 3개 랜덤조회") + @ApiErrorResponses( + common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, + home = {HomeError._HOME_UNRECOGNIZED_REGION, HomeError._HOME_EXHIBIT_NOT_FOUND} + ) + @GetMapping("/overseas/random") + public ResponseEntity>> getRandomOverseas(@RequestParam String country){ + + List random = homeService.getRandomOverseas(country, 3); + + return ResponseEntity.ok(CommonResponse.onSuccess(random)); + } + + @Operation(summary = "국내 지역 전체 조회",description = "국내 지역 전시 전체 조회 1p 당 20개씩 조회.") + @ApiErrorResponses( + common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, + home = {HomeError._HOME_UNRECOGNIZED_REGION, HomeError._HOME_EXHIBIT_NOT_FOUND} + ) + @GetMapping("/domestic/all") + public ResponseEntity>> getRandomDomestic(@RequestParam String region){ + + List random = homeService.getRandomDomestic(region, Pageable.ofSize(20)); + + return ResponseEntity.ok(CommonResponse.onSuccess(random)); + } + + @Operation(summary = "전시 조건 필터",description = "기간, 지역, 장르, 전시 스타일 필터 조회 - null 시 전체선택") + @ApiErrorResponses( + common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, + home = {HomeError._HOME_INVALID_DATE_RANGE, HomeError._HOME_UNRECOGNIZED_REGION, HomeError._HOME_EXHIBIT_NOT_FOUND} + ) + @PostMapping("/filter") + public ResponseEntity getDomesticFilter(@RequestBody ExhibitFilterRequestDto dto, + @RequestParam(required = false) Long cursor, + @PageableDefault(size = 20) Pageable pageable){ + + FilterResponse exhibits = homeService.getFilterExhibit(dto, pageable, cursor); + + return ResponseEntity.ok(exhibits); + + } +} diff --git a/src/main/java/org/atdev/artrip/domain/exhibit/web/dto/ExhibitFilterDto.java b/src/main/java/org/atdev/artrip/domain/exhibit/web/dto/request/ExhibitFilterRequestDto.java similarity index 75% rename from src/main/java/org/atdev/artrip/domain/exhibit/web/dto/ExhibitFilterDto.java rename to src/main/java/org/atdev/artrip/domain/exhibit/web/dto/request/ExhibitFilterRequestDto.java index 6bbe981..9af8dbe 100644 --- a/src/main/java/org/atdev/artrip/domain/exhibit/web/dto/ExhibitFilterDto.java +++ b/src/main/java/org/atdev/artrip/domain/exhibit/web/dto/request/ExhibitFilterRequestDto.java @@ -1,16 +1,15 @@ -package org.atdev.artrip.domain.exhibit.web.dto; +package org.atdev.artrip.domain.exhibit.web.dto.request; import lombok.Builder; import lombok.Data; import org.atdev.artrip.domain.Enum.SortType; -import org.atdev.artrip.domain.keyword.data.Keyword; import java.time.LocalDate; import java.util.Set; @Builder @Data -public class ExhibitFilterDto { +public class ExhibitFilterRequestDto { private LocalDate startDate; private LocalDate endDate; diff --git a/src/main/java/org/atdev/artrip/domain/exhibitHall/data/ExhibitHall.java b/src/main/java/org/atdev/artrip/domain/exhibitHall/data/ExhibitHall.java index 8a7f5c4..45589d2 100644 --- a/src/main/java/org/atdev/artrip/domain/exhibitHall/data/ExhibitHall.java +++ b/src/main/java/org/atdev/artrip/domain/exhibitHall/data/ExhibitHall.java @@ -2,11 +2,13 @@ import jakarta.persistence.*; import lombok.*; + +import java.math.BigDecimal; import java.sql.Timestamp; import java.time.LocalDateTime; @Entity -@Table(name = "exhibit_hall", schema = "art_dev") +@Table(name = "exhibit_hall") @Getter @Setter @NoArgsConstructor @@ -47,6 +49,12 @@ public class ExhibitHall { private Boolean isDomestic; // Byte → Boolean으로 변경, JPA에서 매핑 가능 // true = 국내, false = 해외 + @Column(name = "latitude") + private BigDecimal latitude; + + @Column(name = "longitude") + private BigDecimal longitude; + @Column(name = "created_at", updatable = false) private LocalDateTime createdAt; diff --git a/src/main/java/org/atdev/artrip/domain/exhibitHall/repository/ExhibitHallRepository.java b/src/main/java/org/atdev/artrip/domain/exhibitHall/repository/ExhibitHallRepository.java index c444d2f..064d3a2 100644 --- a/src/main/java/org/atdev/artrip/domain/exhibitHall/repository/ExhibitHallRepository.java +++ b/src/main/java/org/atdev/artrip/domain/exhibitHall/repository/ExhibitHallRepository.java @@ -8,6 +8,7 @@ import org.springframework.data.repository.query.Param; import java.util.List; +import java.util.Optional; public interface ExhibitHallRepository extends JpaRepository { @@ -20,4 +21,5 @@ public interface ExhibitHallRepository extends JpaRepository List findAllDomesticRegions(); + Optional findByName(String placeName); } diff --git a/src/main/java/org/atdev/artrip/domain/favortie/data/FavoriteExhibit.java b/src/main/java/org/atdev/artrip/domain/favortie/data/FavoriteExhibit.java index cbe2502..0e85909 100644 --- a/src/main/java/org/atdev/artrip/domain/favortie/data/FavoriteExhibit.java +++ b/src/main/java/org/atdev/artrip/domain/favortie/data/FavoriteExhibit.java @@ -8,7 +8,7 @@ import java.time.LocalDateTime; @Entity -@Table(name = "favorite_exhibit", schema = "art_dev") +@Table(name = "favorite_exhibit") @Getter @Setter @NoArgsConstructor diff --git a/src/main/java/org/atdev/artrip/domain/home/converter/HomeConverter.java b/src/main/java/org/atdev/artrip/domain/home/converter/HomeConverter.java index a1de300..13851b2 100644 --- a/src/main/java/org/atdev/artrip/domain/home/converter/HomeConverter.java +++ b/src/main/java/org/atdev/artrip/domain/home/converter/HomeConverter.java @@ -2,13 +2,16 @@ import org.atdev.artrip.domain.exhibit.data.Exhibit; import org.atdev.artrip.domain.exhibit.reponse.ExhibitDetailResponse; +import org.atdev.artrip.domain.home.web.dto.RandomExhibitFilterRequestDto; import org.atdev.artrip.domain.home.response.FilterResponse; import org.atdev.artrip.domain.home.response.HomeListResponse; +import org.atdev.artrip.domain.home.web.dto.RandomExhibitRequest; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Component; import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.Set; @Component public class HomeConverter { @@ -48,6 +51,9 @@ public ExhibitDetailResponse toHomeExhibitResponse(Exhibit exhibit) { var hall = exhibit.getExhibitHall(); String period = exhibit.getStartDate().format(formatter) + " ~ " + exhibit.getEndDate().format(formatter); + Double lat = hall.getLatitude() != null ? hall.getLatitude().doubleValue() : null; + Double lng = hall.getLongitude() != null ? hall.getLongitude().doubleValue() : null; + return ExhibitDetailResponse.builder() .exhibitId(exhibit.getExhibitId()) .title(exhibit.getTitle()) @@ -57,11 +63,50 @@ public ExhibitDetailResponse toHomeExhibitResponse(Exhibit exhibit) { .status(exhibit.getStatus()) .exhibitPeriod(period) - .hallName(hall != null ? hall.getName() : null) + .hallName(hall != null ? hall.getName() : null)// exhibit과 exhibithall이 연결되어있지않아도 체크 가능 .hallAddress(hall != null ? hall.getAddress() : null) .hallOpeningHours(hall != null ? hall.getOpeningHours() : null) .hallPhone(hall != null ? hall.getPhone() : null) + .hallLatitude(lat) + .hallLongitude(lng) .build(); } + + public RandomExhibitRequest from(RandomExhibitFilterRequestDto request, Set genres, Set styles) { + + return RandomExhibitRequest.builder() + .isDomestic(request.getIsDomestic()) + .country(normalize(request.getCountry())) + .region(normalize(request.getRegion())) + .date(request.getDate()) + .genres(isEmpty(genres)) + .styles(isEmpty(styles)) + .limit(request.getLimit() != null ? request.getLimit() : 3) + .build(); + } + private String normalize(String value) { + if (value == null) return null; + if ("전체".equals(value)) return null; + return value; + } + + public RandomExhibitRequest from(RandomExhibitFilterRequestDto request) { + return from(request, request.getGenres(), request.getStyles()); + } + + private Set isEmpty(Set value) { + return (value == null || value.isEmpty()) ? null : value; + } + + public RandomExhibitRequest fromGenre(RandomExhibitFilterRequestDto request) { + + return RandomExhibitRequest.builder() + .isDomestic(request.getIsDomestic()) + .country(request.getCountry()) + .region(request.getRegion()) + .singleGenre(request.getSingleGenre() != null ? request.getSingleGenre() : null) + .limit(request.getLimit() != null ? request.getLimit() : 3) + .build(); + } } diff --git a/src/main/java/org/atdev/artrip/domain/home/service/HomeService.java b/src/main/java/org/atdev/artrip/domain/home/service/HomeService.java index 8cb1b3c..38bed0b 100644 --- a/src/main/java/org/atdev/artrip/domain/home/service/HomeService.java +++ b/src/main/java/org/atdev/artrip/domain/home/service/HomeService.java @@ -5,14 +5,15 @@ import org.atdev.artrip.domain.auth.repository.UserRepository; import org.atdev.artrip.domain.exhibit.data.Exhibit; import org.atdev.artrip.domain.exhibit.reponse.ExhibitDetailResponse; -import org.atdev.artrip.domain.exhibit.web.dto.ExhibitFilterDto; +import org.atdev.artrip.domain.exhibit.web.dto.request.ExhibitFilterRequestDto; +import org.atdev.artrip.domain.home.web.dto.RandomExhibitFilterRequestDto; import org.atdev.artrip.domain.exhibitHall.repository.ExhibitHallRepository; import org.atdev.artrip.domain.home.converter.HomeConverter; import org.atdev.artrip.domain.home.response.FilterResponse; -import org.atdev.artrip.domain.home.response.HomeExhibitResponse; import org.atdev.artrip.domain.exhibit.repository.ExhibitRepository; import org.atdev.artrip.domain.home.response.HomeListResponse; +import org.atdev.artrip.domain.home.web.dto.RandomExhibitRequest; import org.atdev.artrip.domain.keyword.data.Keyword; import org.atdev.artrip.domain.keyword.data.UserKeyword; import org.atdev.artrip.domain.keyword.repository.UserKeywordRepository; @@ -23,9 +24,9 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; -import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -41,13 +42,6 @@ public class HomeService { private final HomeConverter homeConverter; - // 오늘 추천 전시 - public List getTodayRecommendedExhibits(Boolean isDomestic) { - return exhibitRepository.findRandomExhibits(3,isDomestic) - .stream() - .map(homeConverter::toHomeExhibitListResponse) - .toList(); - } // // 큐레이션 전시 // public List getCuratedExhibits() { @@ -57,13 +51,6 @@ public List getTodayRecommendedExhibits(Boolean isDomestic) { // .toList(); // } - public List getThemeExhibits(String genre,Boolean isDomestic) { - - return exhibitRepository.findThemeExhibits(genre, 2, isDomestic) - .stream() - .map(homeConverter::toHomeExhibitListResponse) - .toList(); - } public List getAllGenres() { return exhibitRepository.findAllGenres(); @@ -85,32 +72,6 @@ public ExhibitDetailResponse getExhibitDetail(Long exhibitId) { return homeConverter.toHomeExhibitResponse(exhibit); } - public List getPersonalized(Long userId,Boolean isDomestic){ - - if (!userRepository.existsById(userId)) { - throw new GeneralException(UserError._USER_NOT_FOUND); - } - - List userKeywords = userkeywordRepository.findByUser_UserId(userId) - .stream() - .map(UserKeyword::getKeyword) - .toList(); - - Set genres = userKeywords.stream() - .filter(k -> k.getType() == KeywordType.GENRE) - .map(Keyword::getName) - .collect(Collectors.toSet()); - - Set styles = userKeywords.stream() - .filter(k -> k.getType() == KeywordType.STYLE) - .map(Keyword::getName) - .collect(Collectors.toSet()); - - return exhibitRepository.findRandomByKeywords(genres,styles,3, isDomestic) - .stream() - .map(homeConverter::toHomeExhibitListResponse) - .toList(); - } public List getAllPersonalized(Long userId,Boolean isDomestic){ @@ -139,14 +100,6 @@ public List getAllPersonalized(Long userId,Boolean isDomestic) .toList(); } - public List getSchedule(Boolean isDomestic,LocalDate date){ - - return exhibitRepository.findRandomExhibitsByDate(isDomestic,date,2) - .stream() - .map(homeConverter::toHomeExhibitListResponse) - .toList(); - } - public List getAllSchedule(Boolean isDomestic,LocalDate date){ return exhibitRepository.findAllByDate(isDomestic,date) @@ -179,7 +132,7 @@ public List getRandomDomestic(String region, Pageable pageable .toList(); } - public FilterResponse getFilterExhibit(ExhibitFilterDto dto, Pageable pageable, Long cursorId) { + public FilterResponse getFilterExhibit(ExhibitFilterRequestDto dto, Pageable pageable, Long cursorId) { Slice slice = exhibitRepository.findExhibitByFilters(dto, pageable, cursorId); @@ -187,5 +140,63 @@ public FilterResponse getFilterExhibit(ExhibitFilterDto dto, Pageable pageable, } + @Transactional + public List getRandomPersonalized(Long userId, RandomExhibitFilterRequestDto requestDto){ + + if (!userRepository.existsById(userId)) { + throw new GeneralException(UserError._USER_NOT_FOUND); + } + + List userKeywords = userkeywordRepository.findByUser_UserId(userId) + .stream() + .map(UserKeyword::getKeyword) + .toList(); + + Set genres = userKeywords.stream() + .filter(k -> k.getType() == KeywordType.GENRE) + .map(Keyword::getName) + .collect(Collectors.toSet()); + + Set styles = userKeywords.stream() + .filter(k -> k.getType() == KeywordType.STYLE) + .map(Keyword::getName) + .collect(Collectors.toSet()); + + RandomExhibitRequest filter = homeConverter.from( + requestDto, + genres.isEmpty() ? null : genres, + styles.isEmpty() ? null : styles + ); + + + return exhibitRepository.findRandomExhibits(filter); + } + + public List getRandomSchedule(RandomExhibitFilterRequestDto request){ + + RandomExhibitRequest filter = homeConverter.from(request); + + return exhibitRepository.findRandomExhibits(filter); + } + + public List getRandomGenre(RandomExhibitFilterRequestDto request){ + + RandomExhibitRequest filter = homeConverter.fromGenre(request); + + return exhibitRepository.findRandomExhibits(filter); + } + + public List getToday(RandomExhibitFilterRequestDto request){ + + RandomExhibitRequest filter = homeConverter.from(request); +// RandomExhibitFilterRequestDto filter = RandomExhibitFilterRequestDto.builder() +// .isDomestic(isDomestic) +// .country(country) +// .region(region) +// .limit(limit) +// .build(); + + return exhibitRepository.findRandomExhibits(filter); + } } \ No newline at end of file diff --git a/src/main/java/org/atdev/artrip/domain/home/web/controller/HomeController.java b/src/main/java/org/atdev/artrip/domain/home/web/controller/HomeController.java index 5ded96e..51f23d6 100644 --- a/src/main/java/org/atdev/artrip/domain/home/web/controller/HomeController.java +++ b/src/main/java/org/atdev/artrip/domain/home/web/controller/HomeController.java @@ -2,25 +2,23 @@ import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; -import org.atdev.artrip.domain.exhibit.reponse.ExhibitDetailResponse; -import org.atdev.artrip.domain.exhibit.web.dto.ExhibitFilterDto; -import org.atdev.artrip.domain.home.response.FilterResponse; -import org.atdev.artrip.domain.home.response.HomeExhibitResponse; +import org.atdev.artrip.domain.home.web.dto.RandomExhibitFilterRequestDto; +import org.atdev.artrip.domain.home.web.validationgroup.GenreRandomGroup; +import org.atdev.artrip.domain.home.web.validationgroup.ScheduleRandomGroup; import org.atdev.artrip.domain.home.response.HomeListResponse; import org.atdev.artrip.domain.home.service.HomeService; +import org.atdev.artrip.domain.home.web.validationgroup.TodayRandomGroup; +import org.atdev.artrip.domain.home.web.validationgroup.UserCustomGroup; import org.atdev.artrip.global.apipayload.CommonResponse; import org.atdev.artrip.global.apipayload.code.status.CommonError; import org.atdev.artrip.global.apipayload.code.status.HomeError; import org.atdev.artrip.global.swagger.ApiErrorResponses; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; -import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import java.time.LocalDate; import java.util.List; @@ -31,206 +29,132 @@ public class HomeController { private final HomeService homeService; - @Operation(summary = "오늘의 전시 추천", description = "전시데이터 3개 랜덤 조회, true=국내, false=국외") - @ApiErrorResponses( - common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, - home = {HomeError._HOME_EXHIBIT_NOT_FOUND} - ) - @GetMapping("recommend/today") - public ResponseEntity>> getTodayRecommendations( - @RequestParam Boolean isDomestic) { - List exhibits = homeService.getTodayRecommendedExhibits(isDomestic); - return ResponseEntity.ok(CommonResponse.onSuccess(exhibits)); - } - - @Operation(summary = "장르 조회", description = "키워드 장르 데이터 전체 조회") - @ApiErrorResponses( - common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, - home = {HomeError._HOME_GENRE_NOT_FOUND} - ) - @GetMapping("/genre") - public ResponseEntity>> getGenres(){ - List genres = homeService.getAllGenres(); - return ResponseEntity.ok(CommonResponse.onSuccess(genres)); - } - - @Operation(summary = "장르별 랜덤 조회", description = "true=국내, false=국외") - @ApiErrorResponses( - common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, - home = {HomeError._HOME_GENRE_NOT_FOUND} - ) - @GetMapping("/genre/random") - public ResponseEntity>> getRandomExhibits( - @RequestParam String genre, - @RequestParam Boolean isDomestic){ - - List exhibits = homeService.getThemeExhibits(genre,isDomestic); - return ResponseEntity.ok(CommonResponse.onSuccess(exhibits)); - } - - @Operation(summary = "장르별 전시 조회", description = "true=국내, false=국외") - @ApiErrorResponses( - common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, - home = {HomeError._HOME_GENRE_NOT_FOUND} - ) - @GetMapping("/genre/all") - public ResponseEntity>> getAllExhibits( - @RequestParam String genre, - @RequestParam Boolean isDomestic){ - - List exhibits = homeService.getAllgenreExhibits(genre,isDomestic); - return ResponseEntity.ok(CommonResponse.onSuccess(exhibits)); + @Operation(summary = "사용자 맞춤 전시 랜덤 조회", + description = """ + [요청 규칙] + - isDomestic = true (국내) + - region: 필수 + - country: 사용하지 않음 + - isDomestic = false (국외) + - country: 필수 + - region: 사용하지 않음 + + 예시 요청: + { + "isDomestic": true, + "region": "전체" } - - @Operation(summary = "전시 상세 조회") + """) @ApiErrorResponses( - common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, - home = {HomeError._HOME_EXHIBIT_NOT_FOUND} - ) - @GetMapping("/{id}") - public ResponseEntity> getExhibit( - @PathVariable Long id){ - - ExhibitDetailResponse exhibit= homeService.getExhibitDetail(id); - - return ResponseEntity.ok(CommonResponse.onSuccess(exhibit)); - } - - @Operation(summary = "사용자 맞춤 전시 추천") - @ApiErrorResponses( - common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, - home = {HomeError._HOME_EXHIBIT_NOT_FOUND} + common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED} ) - @GetMapping("/personalized") - public ResponseEntity>> getPersonalized( + @PostMapping("/personalized/random") + public ResponseEntity>> getRandomPersonalized( @AuthenticationPrincipal UserDetails userDetails, - @RequestParam Boolean isDomestic){//문자열 형태로 userid뽑아올수있음 + @Validated(UserCustomGroup.class) + @RequestBody RandomExhibitFilterRequestDto requestDto){ long userId = Long.parseLong(userDetails.getUsername()); - List exhibits= homeService.getPersonalized(userId,isDomestic); + List exhibits= homeService.getRandomPersonalized(userId, requestDto); return ResponseEntity.ok(CommonResponse.onSuccess(exhibits)); } - @Operation(summary = "사용자 맞춤 전시 전체 조회") - @ApiErrorResponses( - common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, - home = {HomeError._HOME_EXHIBIT_NOT_FOUND} + @Operation( + summary = "이번주 전시 일정 랜덤 조회", + description = """ + [요청 규칙] + - isDomestic = true (국내) + - region: 필수 + - country: 사용하지 않음 + - isDomestic = false (국외) + - country: 필수 + - region: 사용하지 않음 + - date: 필수 + + 예시 요청: + { + "isDomestic": true, + "region": "전체", + "date": "2025-12-16" + } + """ ) - @GetMapping("/personalized/all") - public ResponseEntity>> getAllPersonalized( - @AuthenticationPrincipal UserDetails userDetails, - @RequestParam Boolean isDomestic){ - - long userId = Long.parseLong(userDetails.getUsername()); + @PostMapping("/schedule") + public ResponseEntity>> getRandomSchedule( + @Validated(ScheduleRandomGroup.class) + @RequestBody RandomExhibitFilterRequestDto request){ - List exhibits= homeService.getAllPersonalized(userId,isDomestic); + List exhibits= homeService.getRandomSchedule(request); return ResponseEntity.ok(CommonResponse.onSuccess(exhibits)); } - @Operation(summary = "이번주 전시 일정 랜덤 조회") + @Operation(summary = "장르별 랜덤 조회", + description = """ + [요청 규칙] + - isDomestic = true (국내) + - region: 필수 + - country: 사용하지 않음 + - isDomestic = false (국외) + - country: 필수 + - region: 사용하지 않음 + - singleGenre: 필수 + + 예시 요청: + { + "isDomestic": true, + "region": "전체", + "singleGenre": "현대 미술" + } + """) @ApiErrorResponses( common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, - home = {HomeError._HOME_EXHIBIT_NOT_FOUND} + home = {HomeError._HOME_GENRE_NOT_FOUND} ) - @GetMapping("/schedule") - public ResponseEntity>> getSchedule( - @RequestParam Boolean isDomestic, - @RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date){ - - List exhibits= homeService.getSchedule(isDomestic,date); + @PostMapping("/genre/random") + public ResponseEntity>> getRandomExhibits( + @Validated(GenreRandomGroup.class) + @RequestBody RandomExhibitFilterRequestDto request){ + List exhibits = homeService.getRandomGenre(request); return ResponseEntity.ok(CommonResponse.onSuccess(exhibits)); } - @Operation(summary = "이번주 전시 일정 전체 조회") + @Operation(summary = "오늘의(국가/지역별) 전시 랜덤 추천", + description = """ + [요청 규칙] + - isDomestic = true (국내) + - region: 필수 + - country: 사용하지 않음 + - isDomestic = false (국외) + - country: 필수 + - region: 사용하지 않음 + + 예시 요청: + { + "isDomestic": true, + "region": "전체" + } + """) @ApiErrorResponses( common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, home = {HomeError._HOME_EXHIBIT_NOT_FOUND} ) - @GetMapping("/schedule/all") - public ResponseEntity>> getAllSchedule( - @RequestParam(name = "isDomestic", required = false) Boolean isDomestic, - @RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date){ + @PostMapping("recommend/today") + public ResponseEntity>> getTodayRecommendations( + @Validated(TodayRandomGroup.class) + @RequestBody RandomExhibitFilterRequestDto request){ - List exhibits= homeService.getAllSchedule(isDomestic,date); + List exhibits = homeService.getToday(request); return ResponseEntity.ok(CommonResponse.onSuccess(exhibits)); } - // @GetMapping("/curation") // public ResponseEntity>> getCuratedExhibits() { // List exhibits = exhibitService.getCuratedExhibits(); // return ResponseEntity.ok(ApiResponse.onSuccess(exhibits)); // } - - @Operation(summary = "해외 국가 목록 조회") - @ApiErrorResponses( - common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED} - ) - @GetMapping("/overseas") - public ResponseEntity>> getOverseas(){ - - List OverseasList = homeService.getOverseas(); - - return ResponseEntity.ok(CommonResponse.onSuccess(OverseasList)); - } - - @Operation(summary = "국내 지역 목록 조회") - @ApiErrorResponses( - common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED} - ) - @GetMapping("/domestic") - public ResponseEntity>> getDomestic(){ - - List domesticList = homeService.getDomestic(); - - return ResponseEntity.ok(CommonResponse.onSuccess(domesticList)); - } - - @Operation(summary = "해외 특정 국가 랜덤 조회",description = "특정 해외 국가 전시데이터 3개 랜덤조회") - @ApiErrorResponses( - common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, - home = {HomeError._HOME_UNRECOGNIZED_REGION, HomeError._HOME_EXHIBIT_NOT_FOUND} - ) - @GetMapping("/overseas/random") - public ResponseEntity>> getRandomOverseas(@RequestParam String country){ - - List random = homeService.getRandomOverseas(country, 3); - - return ResponseEntity.ok(CommonResponse.onSuccess(random)); - } - - @Operation(summary = "국내 지역 전체 조회",description = "국내 지역 전시 전체 조회 1p 당 20개씩 조회.") - @ApiErrorResponses( - common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, - home = {HomeError._HOME_UNRECOGNIZED_REGION, HomeError._HOME_EXHIBIT_NOT_FOUND} - ) - @GetMapping("/domestic/all") - public ResponseEntity>> getRandomDomestic(@RequestParam String region){ - - List random = homeService.getRandomDomestic(region, Pageable.ofSize(20)); - - return ResponseEntity.ok(CommonResponse.onSuccess(random)); - } - - @Operation(summary = "전시 조건 필터",description = "기간, 지역, 장르, 전시 스타일 필터 조회 - null 시 전체선택") - @ApiErrorResponses( - common = {CommonError._BAD_REQUEST, CommonError._UNAUTHORIZED}, - home = {HomeError._HOME_INVALID_DATE_RANGE, HomeError._HOME_UNRECOGNIZED_REGION, HomeError._HOME_EXHIBIT_NOT_FOUND} - ) - @PostMapping("/filter") - public ResponseEntity getDomesticFilter(@RequestBody ExhibitFilterDto dto, - @RequestParam(required = false) Long cursor, - @PageableDefault(size = 20) Pageable pageable){ - - FilterResponse exhibits = homeService.getFilterExhibit(dto, pageable, cursor); - - return ResponseEntity.ok(exhibits); - - } - } diff --git a/src/main/java/org/atdev/artrip/domain/home/web/dto/RandomExhibitFilterRequestDto.java b/src/main/java/org/atdev/artrip/domain/home/web/dto/RandomExhibitFilterRequestDto.java new file mode 100644 index 0000000..9b02dbe --- /dev/null +++ b/src/main/java/org/atdev/artrip/domain/home/web/dto/RandomExhibitFilterRequestDto.java @@ -0,0 +1,67 @@ +package org.atdev.artrip.domain.home.web.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import org.atdev.artrip.domain.home.web.validationgroup.GenreRandomGroup; +import org.atdev.artrip.domain.home.web.validationgroup.ScheduleRandomGroup; +import org.atdev.artrip.domain.home.web.validationgroup.TodayRandomGroup; +import org.atdev.artrip.domain.home.web.validationgroup.UserCustomGroup; + +import java.time.LocalDate; +import java.util.Set; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RandomExhibitFilterRequestDto { + + @NotNull(groups = { + TodayRandomGroup.class, + ScheduleRandomGroup.class, + GenreRandomGroup.class, + UserCustomGroup.class + }) + private Boolean isDomestic; + + private String country; + private String region; + + @NotEmpty(groups = GenreRandomGroup.class) // 장르별 랜덤 조회 + private String singleGenre; + + @Schema(hidden = true) + private Set genres; + @Schema(hidden = true) + private Set styles; + + @NotNull(groups = ScheduleRandomGroup.class) // 이번주 전시 조회 + private LocalDate date; + + @Schema(hidden = true) + private Integer limit; + + @Schema(hidden = true) + @AssertTrue(message = "국내 전시는 region 필수(전체 가능), 국외 전시는 country 필수(전체 가능)이며 둘을 동시에 보낼 수 없습니다.", + groups = {TodayRandomGroup.class, + ScheduleRandomGroup.class, + GenreRandomGroup.class, + UserCustomGroup.class}) + public boolean isDomesticRegionCountryValid() { + if (isDomestic == null) return true; + + boolean hasRegion = region != null && !region.isBlank(); + boolean hasCountry = country != null && !country.isBlank(); + + if (isDomestic) { + return hasRegion && !hasCountry; + } else { + return hasCountry && !hasRegion; + } + } + +} \ No newline at end of file diff --git a/src/main/java/org/atdev/artrip/domain/home/web/dto/RandomExhibitRequest.java b/src/main/java/org/atdev/artrip/domain/home/web/dto/RandomExhibitRequest.java new file mode 100644 index 0000000..faa742f --- /dev/null +++ b/src/main/java/org/atdev/artrip/domain/home/web/dto/RandomExhibitRequest.java @@ -0,0 +1,23 @@ +package org.atdev.artrip.domain.home.web.dto; + +import lombok.*; + +import java.time.LocalDate; +import java.util.Set; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RandomExhibitRequest { + + private Boolean isDomestic; + private String country; + private String region; + private LocalDate date; + private String singleGenre; + private Set genres; + private Set styles; + private int limit; +} diff --git a/src/main/java/org/atdev/artrip/domain/home/web/validationgroup/GenreRandomGroup.java b/src/main/java/org/atdev/artrip/domain/home/web/validationgroup/GenreRandomGroup.java new file mode 100644 index 0000000..873172d --- /dev/null +++ b/src/main/java/org/atdev/artrip/domain/home/web/validationgroup/GenreRandomGroup.java @@ -0,0 +1,4 @@ +package org.atdev.artrip.domain.home.web.validationgroup; + +public interface GenreRandomGroup { +} diff --git a/src/main/java/org/atdev/artrip/domain/home/web/validationgroup/ScheduleRandomGroup.java b/src/main/java/org/atdev/artrip/domain/home/web/validationgroup/ScheduleRandomGroup.java new file mode 100644 index 0000000..f605a98 --- /dev/null +++ b/src/main/java/org/atdev/artrip/domain/home/web/validationgroup/ScheduleRandomGroup.java @@ -0,0 +1,4 @@ +package org.atdev.artrip.domain.home.web.validationgroup; + +public interface ScheduleRandomGroup { +} diff --git a/src/main/java/org/atdev/artrip/domain/home/web/validationgroup/TodayRandomGroup.java b/src/main/java/org/atdev/artrip/domain/home/web/validationgroup/TodayRandomGroup.java new file mode 100644 index 0000000..b9329fd --- /dev/null +++ b/src/main/java/org/atdev/artrip/domain/home/web/validationgroup/TodayRandomGroup.java @@ -0,0 +1,4 @@ +package org.atdev.artrip.domain.home.web.validationgroup; + +public interface TodayRandomGroup { +} diff --git a/src/main/java/org/atdev/artrip/domain/home/web/validationgroup/UserCustomGroup.java b/src/main/java/org/atdev/artrip/domain/home/web/validationgroup/UserCustomGroup.java new file mode 100644 index 0000000..ff0f04d --- /dev/null +++ b/src/main/java/org/atdev/artrip/domain/home/web/validationgroup/UserCustomGroup.java @@ -0,0 +1,4 @@ +package org.atdev.artrip.domain.home.web.validationgroup; + +public interface UserCustomGroup { +} diff --git a/src/main/java/org/atdev/artrip/domain/keyword/data/Keyword.java b/src/main/java/org/atdev/artrip/domain/keyword/data/Keyword.java index 34c8b98..a898316 100644 --- a/src/main/java/org/atdev/artrip/domain/keyword/data/Keyword.java +++ b/src/main/java/org/atdev/artrip/domain/keyword/data/Keyword.java @@ -7,7 +7,7 @@ import java.util.List; @Entity -@Table(name = "keyword", schema = "art_dev") +@Table(name = "keyword") @Getter @Setter @NoArgsConstructor diff --git a/src/main/java/org/atdev/artrip/domain/keyword/data/UserKeyword.java b/src/main/java/org/atdev/artrip/domain/keyword/data/UserKeyword.java index af26938..bcd7966 100644 --- a/src/main/java/org/atdev/artrip/domain/keyword/data/UserKeyword.java +++ b/src/main/java/org/atdev/artrip/domain/keyword/data/UserKeyword.java @@ -7,7 +7,7 @@ import java.time.LocalDateTime; @Entity -@Table(name = "user_keyword", schema = "art_dev") +@Table(name = "user_keyword") @Getter @Setter @NoArgsConstructor diff --git a/src/main/java/org/atdev/artrip/domain/review/data/Review.java b/src/main/java/org/atdev/artrip/domain/review/data/Review.java index 4e67eb6..569f86e 100644 --- a/src/main/java/org/atdev/artrip/domain/review/data/Review.java +++ b/src/main/java/org/atdev/artrip/domain/review/data/Review.java @@ -12,7 +12,7 @@ import java.util.List; @Entity -@Table(name = "review", schema = "art_dev") +@Table(name = "review") @Getter @Setter @NoArgsConstructor diff --git a/src/main/java/org/atdev/artrip/domain/search/data/SearchHistory.java b/src/main/java/org/atdev/artrip/domain/search/data/SearchHistory.java index 9024b7a..aaa7182 100644 --- a/src/main/java/org/atdev/artrip/domain/search/data/SearchHistory.java +++ b/src/main/java/org/atdev/artrip/domain/search/data/SearchHistory.java @@ -9,7 +9,7 @@ import java.time.LocalDateTime; @Entity -@Table(name = "search_history", schema = "art_dev") +@Table(name = "search_history") @Getter @Setter @NoArgsConstructor diff --git a/src/main/java/org/atdev/artrip/domain/search/service/ExhibitSearchService.java b/src/main/java/org/atdev/artrip/domain/search/service/ExhibitSearchService.java index 6afc954..bbf25d8 100644 --- a/src/main/java/org/atdev/artrip/domain/search/service/ExhibitSearchService.java +++ b/src/main/java/org/atdev/artrip/domain/search/service/ExhibitSearchService.java @@ -126,8 +126,6 @@ private ExhibitSearchResponse convertToExhibitResponse(ExhibitDocument doc) { .status(doc.getStatus()) .posterUrl(doc.getPosterUrl()) .ticketUrl(doc.getTicketUrl()) - .latitude(doc.getLatitude()) - .longitude(doc.getLongitude()) .keywords(doc.getKeywords()) .build(); } diff --git a/src/main/java/org/atdev/artrip/elastic/service/ExhibitIndexService.java b/src/main/java/org/atdev/artrip/elastic/service/ExhibitIndexService.java index 13ae73d..cf29da2 100644 --- a/src/main/java/org/atdev/artrip/elastic/service/ExhibitIndexService.java +++ b/src/main/java/org/atdev/artrip/elastic/service/ExhibitIndexService.java @@ -47,8 +47,6 @@ private ExhibitDocument convertToDocument(Exhibit exhibit) { .status(exhibit.getStatus()) .posterUrl(exhibit.getPosterUrl()) .ticketUrl(exhibit.getTicketUrl()) - .latitude(exhibit.getLatitude()) - .longitude(exhibit.getLongitude()) .keywords(keywordInfos); return builder.build(); @@ -113,8 +111,6 @@ public void createAndApplyIndex() { .properties("status", p -> p.keyword(k -> k)) .properties("posterUrl", p -> p.keyword(k -> k)) .properties("ticketUrl", p -> p.keyword(k -> k)) - .properties("latitude", p -> p.float_(f -> f)) - .properties("longitude", p -> p.float_(f -> f)) .properties("keywords", p -> p .nested(n -> n .properties("name", np -> np diff --git a/src/main/java/org/atdev/artrip/external/config/RetryConfig.java b/src/main/java/org/atdev/artrip/external/config/RetryConfig.java new file mode 100644 index 0000000..eb155b9 --- /dev/null +++ b/src/main/java/org/atdev/artrip/external/config/RetryConfig.java @@ -0,0 +1,31 @@ +package org.atdev.artrip.external.config; + +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryRegistry; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class RetryConfig { + + private final RetryRegistry retryRegistry; + + @Bean + public Retry publicDataApiRetry() { + Retry retry = retryRegistry.retry("publicDataApi"); + + retry.getEventPublisher() + .onRetry(event -> log.warn("공공데이터 API 재시도 - 시도 횟수 : {}, 원인 : {}", + event.getNumberOfRetryAttempts(), + event.getLastThrowable().getMessage())) + .onError(event -> log.error("공공데이터 API 실패 - 시도 횟수 : {}", + event.getNumberOfRetryAttempts())) + .onSuccess(event -> log.info("공공데이터 API 성공 - 시도 횟수 : {}", + event.getNumberOfRetryAttempts())); + return retry; + } +} diff --git a/src/main/java/org/atdev/artrip/external/config/WebClientConfig.java b/src/main/java/org/atdev/artrip/external/config/WebClientConfig.java new file mode 100644 index 0000000..c9776f9 --- /dev/null +++ b/src/main/java/org/atdev/artrip/external/config/WebClientConfig.java @@ -0,0 +1,89 @@ +package org.atdev.artrip.external.config; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import io.netty.channel.ChannelOption; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; +import lombok.extern.slf4j.Slf4j; +import org.atdev.artrip.external.publicdata.properties.PublicDataProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.xml.Jaxb2XmlDecoder; +import org.springframework.http.codec.xml.Jaxb2XmlEncoder; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Configuration +public class WebClientConfig { + + @Bean + public WebClient publicDataWebClient(PublicDataProperties properties) { + + XmlMapper xmlMapper = XmlMapper.builder() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .build(); + + HttpClient httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, properties.getConnectTimeout()) + .responseTimeout(Duration.ofMillis(properties.getReadTimeout())) + .doOnConnected(conn -> conn + .addHandlerLast(new ReadTimeoutHandler(properties.getReadTimeout(), TimeUnit.MILLISECONDS)) + .addHandlerLast(new WriteTimeoutHandler(properties.getReadTimeout(), TimeUnit.MILLISECONDS))); + + ExchangeStrategies strategies = ExchangeStrategies.builder() + .codecs(config -> { + config.defaultCodecs().maxInMemorySize(10 * 1024 * 1024); + config.customCodecs().register( + new Jackson2JsonDecoder(xmlMapper, + MediaType.TEXT_XML, + MediaType.APPLICATION_XML, + new MediaType("text", "xml", StandardCharsets.UTF_8)) + ); + config.customCodecs().register( + new Jackson2JsonEncoder(xmlMapper, + MediaType.TEXT_XML, + MediaType.APPLICATION_XML + ) + ); + }).build(); + + return WebClient.builder() + .baseUrl(properties.getBaseUrl()) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .exchangeStrategies(strategies) + .defaultHeader("Accept", MediaType.APPLICATION_XML_VALUE + ", " + MediaType.TEXT_XML_VALUE + ", */*") + .defaultHeader("User-Agent", "ArtTrip/1.0") + .filter(logRequest()) + .filter(logResponse()) + .build(); + } + + private ExchangeFilterFunction logResponse () { + return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { + log.info("공공데이터 API 응답 Status : {}", clientResponse.statusCode()); + return Mono.just(clientResponse); + }); + + } + + private ExchangeFilterFunction logRequest() { + return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> { + log.info("[공공데이터 API 요청] {} {}", clientRequest.method(), clientRequest.url()); + return Mono.just(clientRequest); + }); + } +} diff --git a/src/main/java/org/atdev/artrip/external/publicdata/exhibit/client/BasePublicDataClient.java b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/client/BasePublicDataClient.java new file mode 100644 index 0000000..36c3501 --- /dev/null +++ b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/client/BasePublicDataClient.java @@ -0,0 +1,104 @@ +package org.atdev.artrip.external.publicdata.exhibit.client; + +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryRegistry; +import lombok.extern.slf4j.Slf4j; +import org.atdev.artrip.external.publicdata.exhibit.dto.request.BasePublicDataRequest; +import org.atdev.artrip.external.publicdata.exhibit.dto.response.BasePublicDataItem; +import org.atdev.artrip.external.publicdata.exhibit.dto.response.BasePublicDataResponse; +import org.atdev.artrip.external.publicdata.exhibit.dto.response.PublicDataResponse; +import org.atdev.artrip.external.publicdata.properties.PublicDataProperties; +import org.atdev.artrip.global.apipayload.exception.ExternalApiException; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatusCode; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.util.function.Supplier; + +@Slf4j +public abstract class BasePublicDataClient < + T extends BasePublicDataItem, + R extends BasePublicDataRequest, + S extends PublicDataResponse>{ + + protected final WebClient webClient; + protected final PublicDataProperties properties; + protected final Retry retry; + + protected BasePublicDataClient(WebClient publicDataWebClient, + PublicDataProperties properties, + RetryRegistry retryRegistry) { + this.webClient = publicDataWebClient; + this.properties = properties; + this.retry = retryRegistry.retry("publicDataAPI"); + } + + protected abstract String getApiName(); + + protected abstract String getApiPath(); + + protected abstract ParameterizedTypeReference getResponseTypeRef(); + + protected abstract Class getResponseClass(); + + protected abstract void addRequestParams(UriComponentsBuilder builder, R request); + + protected S fetch(R request) { + Supplier supplier = Retry.decorateSupplier(retry, () -> doFetch(request)); + + try { + return supplier.get(); + } catch (Exception e) { + log.error("[{}] API 호출 실패: {}", getApiName(), e.getMessage()); + throw new ExternalApiException(getApiName(), "API 호출 실패", e); + } + } + + protected S doFetch(R request) { + URI uri = buildUri(request); + + log.debug("[{}] 요청 URI: {}", getApiName(), uri); + + return webClient.get() + .uri(uri) + .retrieve() + .onStatus(HttpStatusCode::isError, response -> + response.bodyToMono(String.class) + .flatMap(body -> Mono.error( + new ExternalApiException(getApiName(), response.statusCode(), body)))) + .bodyToMono(getResponseClass()) + .doOnSuccess(response -> logResponse(response)) + .doOnError(e -> log.error("[{}] 호출 에러: {}", getApiName(), e.getMessage())) + .block(); + } + + protected URI buildUri(R request) { + + UriComponentsBuilder builder = UriComponentsBuilder + .fromHttpUrl(properties.getBaseUrl()) + .path(getApiPath()) + .queryParam("serviceKey", properties.getServiceKey()) + .queryParam("pageNo", request.getPageNo()) + .queryParam("numOfRows", request.getNumOfRows()) + .queryParam("sortStdr", request.getSortStdr()); + + addRequestParams(builder, request); + + return builder.build(true).toUri(); + } + + protected void logResponse(S response) { + if (response.isSuccess()) { + log.info("[{}] 조회 성공 - 총 건수 {} : 페이지 : {}/{}", + getApiName(), + response.getTotalCount(), + response.getCurrentPage(), + response.getTotalPages()); + } else { + log.warn("{} API 응답 실패 - {}" , getApiName(), response.getErrorMessage()); + } + } +} diff --git a/src/main/java/org/atdev/artrip/external/publicdata/exhibit/client/ExhibitApiClient.java b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/client/ExhibitApiClient.java new file mode 100644 index 0000000..8a59932 --- /dev/null +++ b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/client/ExhibitApiClient.java @@ -0,0 +1,84 @@ +package org.atdev.artrip.external.publicdata.exhibit.client; + +import io.github.resilience4j.retry.RetryRegistry; +import lombok.extern.slf4j.Slf4j; +import org.atdev.artrip.external.publicdata.exhibit.dto.request.ExhibitRequest; +import org.atdev.artrip.external.publicdata.exhibit.dto.response.ExhibitItem; +import org.atdev.artrip.external.publicdata.exhibit.dto.response.ExhibitResponse; +import org.atdev.artrip.external.publicdata.properties.PublicDataProperties; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriComponentsBuilder; + +@Slf4j +@Component +public class ExhibitApiClient extends BasePublicDataClient{ + + public ExhibitApiClient( + WebClient publicDataWebClient, + PublicDataProperties properties, + RetryRegistry retryRegistry){ + super(publicDataWebClient, properties, retryRegistry); + } + + @Override + protected String getApiName() { + return "exhibitApi"; + } + + @Override + protected String getApiPath() { + return ""; + } + + @Override + protected ParameterizedTypeReference getResponseTypeRef() { + return new ParameterizedTypeReference<>(){}; + } + + @Override + protected Class getResponseClass() { + return ExhibitResponse.class; + } + + @Override + protected void addRequestParams(UriComponentsBuilder builder, ExhibitRequest request) { + if (request.getFrom() != null) { + builder.queryParam("from", request.getFrom()); + } + + if(request.getTo() != null) { + builder.queryParam("to", request.getTo()); + } + + if (request.getSido() != null) { + builder.queryParam("sido", request.getSido()); + } + + if (request.getRealmCode() != null) { + builder.queryParam("realmCode", request.getRealmCode()); + } + } + + public ExhibitResponse fetchByPeriod(String from, String to, int pageNo) { + ExhibitRequest request = ExhibitRequest.builder() + .serviceKey(properties.getServiceKey()) + .pageNo(pageNo) + .numOfRows(properties.getPageSize()) + .from(from) + .to(to) + .build(); + + return fetch(request); + } + + public ExhibitResponse fetchExhibits(int pageNo) { + ExhibitRequest request = ExhibitRequest.builder() + .serviceKey(properties.getServiceKey()) + .pageNo(pageNo) + .numOfRows(properties.getPageSize()) + .build(); + return fetch(request); + } +} diff --git a/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/request/BasePublicDataRequest.java b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/request/BasePublicDataRequest.java new file mode 100644 index 0000000..f8e2412 --- /dev/null +++ b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/request/BasePublicDataRequest.java @@ -0,0 +1,22 @@ +package org.atdev.artrip.external.publicdata.exhibit.dto.request; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class BasePublicDataRequest { + + protected String serviceKey; + + @Builder.Default + protected int pageNo = 1; + + @Builder.Default + protected int numOfRows = 100; + + @Builder.Default + protected String sortStdr = "1"; +} diff --git a/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/request/ExhibitRequest.java b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/request/ExhibitRequest.java new file mode 100644 index 0000000..24903ba --- /dev/null +++ b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/request/ExhibitRequest.java @@ -0,0 +1,21 @@ +package org.atdev.artrip.external.publicdata.exhibit.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class ExhibitRequest extends BasePublicDataRequest{ + + private String from; + private String to; + private String sido; + private String keyword; + private String realmCode; + +} diff --git a/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/response/BasePublicDataItem.java b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/response/BasePublicDataItem.java new file mode 100644 index 0000000..19d41f3 --- /dev/null +++ b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/response/BasePublicDataItem.java @@ -0,0 +1,10 @@ +package org.atdev.artrip.external.publicdata.exhibit.dto.response; + +public interface BasePublicDataItem { + + String getUniqueId(); + + default boolean isValid() { + return getUniqueId() != null && !getUniqueId().isBlank(); + } +} diff --git a/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/response/BasePublicDataResponse.java b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/response/BasePublicDataResponse.java new file mode 100644 index 0000000..3985675 --- /dev/null +++ b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/response/BasePublicDataResponse.java @@ -0,0 +1,98 @@ +package org.atdev.artrip.external.publicdata.exhibit.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Collections; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +@JacksonXmlRootElement(localName = "response") +public class BasePublicDataResponse { + + @JacksonXmlProperty(localName = "header") + private Header header; + + @JacksonXmlProperty(localName = "body") + private Body body; + + @Getter + @Setter + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Header { + @JacksonXmlProperty(localName = "resultCode") + private String resultCode; + + @JacksonXmlProperty(localName = "resultMsg") + private String resultMsg; + } + + @Getter + @Setter + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Body { + @JacksonXmlProperty(localName = "totalCount") + private int totalCount; + + @JacksonXmlProperty(localName = "pageNo") + private int pageNo; + + @JacksonXmlProperty(localName = "numOfRows") + private int numOfRows; + + @JacksonXmlElementWrapper(localName = "items") + @JacksonXmlProperty(localName = "item") + private List items; + } + + public boolean isSuccess() { + return header != null && "0000".equals(header.getResultCode()); + } + + public String getErrorMessage() { + if (header == null) { + return "응답 헤더가 없습니다."; + } + return String.format("[%s] %s", header.getResultCode(), header.getResultMsg()); + } + + public int getTotalPages() { + if (body == null || body.getNumOfRows() == 0 ) { + return 0; + } + return (int) Math.ceil((double) body.getTotalCount() / body.getNumOfRows()); + } + + public int getCurrentPage() { + return body != null ? body.getPageNo() : 0; + } + + public int getTotalCount() { + return body != null ? body.getTotalCount() : 0; + } + + public List getItems() { + if (body == null || body.getItems() == null) { + return Collections.emptyList(); + } + return body.getItems(); + } + + public boolean hasData() { + return !getItems().isEmpty(); + } + + public boolean isLastPage() { + return getCurrentPage() >= getTotalPages(); + } +} diff --git a/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/response/ExhibitItem.java b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/response/ExhibitItem.java new file mode 100644 index 0000000..2d0a53a --- /dev/null +++ b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/response/ExhibitItem.java @@ -0,0 +1,91 @@ +package org.atdev.artrip.external.publicdata.exhibit.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class ExhibitItem implements BasePublicDataItem{ + + @JacksonXmlProperty(localName = "title") + private String title; + + @JacksonXmlProperty(localName = "collectionDb") + private String collectionDb; // 전시정보 + + @JacksonXmlProperty(localName = "subjectCategory") + private String subjectCategory; // 국내전시/해외전시 + + @JacksonXmlProperty(localName = "rights") + private String rights; // 미술관명 (예: 국립현대미술관) + + @JacksonXmlProperty(localName = "charge") + private String charge; // 요금 + + @JacksonXmlProperty(localName = "venue") + private String venue; // 전시 장소 (예: 1전시실) + + @JacksonXmlProperty(localName = "eventPeriod") + private String eventPeriod; // 기간 (예: 2020-12-17 ~ 2021-04-11) + + @JacksonXmlProperty(localName = "subDescription") + private String subDescription; // 설명 + + @JacksonXmlProperty(localName = "person") + private String person; // 작가 + + @JacksonXmlProperty(localName = "creator") + private String creator; + + @JacksonXmlProperty(localName = "publisher") + private String publisher; + + // place로 사용 (미술관명) + public String getPlace() { + return this.rights; + } + + // 기존 호환성 메서드 + public String getThumbnail() { + return null; // KCISA API에서 이미지 없음 + } + + public String getUrl() { + return null; // KCISA API에서 URL 없음 + } + + public String getPlaceAddr() { + return null; + } + + public String getArea() { + return null; + } + + public String getPhone() { + return null; + } + + public String getGpsX() { + return null; + } + + public String getGpsY() { + return null; + } + + @Override + public String getUniqueId() { + return title + "_" + (eventPeriod != null ? eventPeriod : ""); + } + + @Override + public boolean isValid() { + return title != null && !title.isBlank(); + } +} diff --git a/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/response/ExhibitResponse.java b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/response/ExhibitResponse.java new file mode 100644 index 0000000..87ebc3e --- /dev/null +++ b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/response/ExhibitResponse.java @@ -0,0 +1,102 @@ +package org.atdev.artrip.external.publicdata.exhibit.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Collections; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +@JacksonXmlRootElement(localName = "response") +public class ExhibitResponse implements PublicDataResponse { + + @JacksonXmlProperty(localName = "header") + private Header header; + + @JacksonXmlProperty(localName = "body") + private ExhibitBody body; + + @Getter + @Setter + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Header { + @JacksonXmlProperty(localName = "resultCode") + private String resultCode; + + @JacksonXmlProperty(localName = "resultMsg") + private String resultMsg; + } + + @Getter + @Setter + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ExhibitBody { + @JacksonXmlProperty(localName = "totalCount") + private int totalCount; + + @JacksonXmlProperty(localName = "pageNo") + private int pageNo; + + @JacksonXmlProperty(localName = "numOfRows") + private int numOfRows; + + @JacksonXmlElementWrapper(localName = "items") + @JacksonXmlProperty(localName = "item") + private List items; + } + + @Override + public boolean isSuccess() { + return header != null && "0000".equals(header.getResultCode()); + } + + @Override + public String getErrorMessage() { + if (header == null) { + return "응답 헤더가 없습니다."; + } + return String.format("[%s] %s", header.getResultCode(), header.getResultMsg()); + } + + @Override + public int getTotalPages() { + if (body == null || body.getNumOfRows() == 0) { + return 0; + } + return (int) Math.ceil((double) body.getTotalCount() / body.getNumOfRows()); + } + + @Override + public int getCurrentPage() { + return body != null ? body.getPageNo() : 0; + } + + @Override + public int getTotalCount() { + return body != null ? body.getTotalCount() : 0; + } + + @Override + public List getItems() { + if (body == null || body.getItems() == null) { + return Collections.emptyList(); + } + return body.getItems(); + } + + public List getExhibitsOnly() { + return getItems().stream() + .filter(item -> "전시정보".equals(item.getCollectionDb())) + .toList(); + } +} diff --git a/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/response/PublicDataResponse.java b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/response/PublicDataResponse.java new file mode 100644 index 0000000..6bbceaf --- /dev/null +++ b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/dto/response/PublicDataResponse.java @@ -0,0 +1,26 @@ +package org.atdev.artrip.external.publicdata.exhibit.dto.response; + +import java.util.List; + +public interface PublicDataResponse { + + boolean isSuccess(); + + String getErrorMessage(); + + int getTotalPages(); + + int getCurrentPage(); + + int getTotalCount(); + + List getItems(); + + default boolean hasData() { + return !getItems().isEmpty(); + } + + default boolean isLastPage() { + return getCurrentPage() >= getTotalPages(); + } +} diff --git a/src/main/java/org/atdev/artrip/external/publicdata/exhibit/mapper/ExhibitMapper.java b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/mapper/ExhibitMapper.java new file mode 100644 index 0000000..e7acfa2 --- /dev/null +++ b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/mapper/ExhibitMapper.java @@ -0,0 +1,180 @@ +package org.atdev.artrip.external.publicdata.exhibit.mapper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.atdev.artrip.domain.Enum.Status; +import org.atdev.artrip.domain.exhibit.data.Exhibit; +import org.atdev.artrip.domain.exhibitHall.data.ExhibitHall; +import org.atdev.artrip.external.publicdata.exhibit.dto.response.ExhibitItem; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ExhibitMapper { + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final DateTimeFormatter DATE_FORMATTER_DASH = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + public Exhibit toExhibit(ExhibitItem item) { + try { + LocalDateTime startDate = parseEventPeriod(item.getEventPeriod(), true); + LocalDateTime endDate = parseEventPeriod(item.getEventPeriod(), false); + Status status = calculateStatus(startDate, endDate); + + return Exhibit.builder() + .title(truncate(item.getTitle(), 255)) + .description(buildDescription(item)) + .startDate(startDate) + .endDate(endDate) + .status(status) + .posterUrl(item.getThumbnail()) + .ticketUrl(item.getUrl()) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + } catch (Exception e) { + log.warn("전시 정보 변환 실패 - title: {}, error: {}", item.getTitle(), e.getMessage()); + return null; + } + } + + public ExhibitHall toExhibitHall(ExhibitItem item) { + if (item == null || !StringUtils.hasText(item.getPlace())) { + return null; + } + + return ExhibitHall.builder() + .name(truncate(item.getPlace(), 255)) + .country("대한민국") + .region(parseRegion(item.getArea())) + .address(item.getPlaceAddr()) + .phone(item.getPhone()) + .isDomestic(true) + .latitude(parseCoordinate(item.getGpsX())) + .longitude(parseCoordinate(item.getGpsY())) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + } + + private LocalDateTime parseEventPeriod(String eventPeriod, boolean isStart) { + if (!StringUtils.hasText(eventPeriod)) { + return null; + } + + try { + // "2020-12-17 ~ 2021-04-11" 형식 + if (eventPeriod.contains("~")) { + String[] dates = eventPeriod.split("~"); + if (dates.length != 2) { + return null; + } + String dateStr = isStart ? dates[0].trim() : dates[1].trim(); + return LocalDate.parse(dateStr, DATE_FORMATTER_DASH).atStartOfDay(); + } + + return parseDate(eventPeriod); + } catch (Exception e) { + log.warn("eventPeriod 파싱 실패: {}", eventPeriod); + return null; + } + } + + private LocalDateTime parseDate(String date) { + if (!StringUtils.hasText(date)) { + return null; + } + + try { + if (date.length() == 8) { + return LocalDate.parse(date, DATE_FORMATTER).atStartOfDay(); + } + if (date.contains("-")) { + return LocalDate.parse(date, DATE_FORMATTER_DASH).atStartOfDay(); + } + return null; + } catch (DateTimeParseException e) { + log.warn("날짜 파싱 실패: {}", date); + return null; + } + } + + private BigDecimal parseCoordinate(String coordinate) { + if (!StringUtils.hasText(coordinate)) { + return null; + } + + try { + return new BigDecimal(coordinate.trim()); + } catch (NumberFormatException e) { + log.warn("좌표 파싱 실패: {}", coordinate); + return null; + } + } + + private Status calculateStatus(LocalDateTime startDate, LocalDateTime endDate) { + if (startDate == null || endDate == null) { + return Status.ONGOING; + } + + LocalDateTime now = LocalDateTime.now(); + LocalDateTime threeDaysLater = now.plusDays(3); + + if (now.isBefore(startDate)) { + return Status.UPCOMING; + } else if (now.isAfter(endDate)) { + return Status.FINISHED; + } else if (endDate.isBefore(threeDaysLater)) { + return Status.ENDING_SOON; + } else { + return Status.ONGOING; + } + } + + private String buildDescription(ExhibitItem item) { + StringBuilder sb = new StringBuilder(); + + // 작가 정보 + if (StringUtils.hasText(item.getPerson())) { + sb.append("작가: ").append(item.getPerson()); + } + + // 전시 장소 + if (StringUtils.hasText(item.getVenue())) { + if (sb.length() > 0) sb.append("\n"); + sb.append("장소: ").append(item.getVenue()); + } + + // 설명 + if (StringUtils.hasText(item.getSubDescription())) { + if (sb.length() > 0) sb.append("\n\n"); + sb.append(item.getSubDescription()); + } + + // 요금 + if (StringUtils.hasText(item.getCharge())) { + if (sb.length() > 0) sb.append("\n\n"); + sb.append("관람료: ").append(item.getCharge()); + } + + return truncate(sb.toString(), 2000); + } + + private String parseRegion(String area) { + if (!StringUtils.hasText(area)) return null; + return area.contains(" ") ? area.split(" ")[0] : area; + } + + private String truncate(String str, int length) { + if (!StringUtils.hasText(str)) return null; + return str.length() > length ? str.substring(0, length) : str; + } +} diff --git a/src/main/java/org/atdev/artrip/external/publicdata/exhibit/service/ExhibitSyncService.java b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/service/ExhibitSyncService.java new file mode 100644 index 0000000..bcb9a40 --- /dev/null +++ b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/service/ExhibitSyncService.java @@ -0,0 +1,219 @@ +package org.atdev.artrip.external.publicdata.exhibit.service; + +import jakarta.persistence.EntityManager; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.atdev.artrip.domain.exhibit.data.Exhibit; +import org.atdev.artrip.domain.exhibit.repository.ExhibitRepository; +import org.atdev.artrip.domain.exhibitHall.data.ExhibitHall; +import org.atdev.artrip.domain.exhibitHall.repository.ExhibitHallRepository; +import org.atdev.artrip.external.publicdata.exhibit.client.ExhibitApiClient; +import org.atdev.artrip.external.publicdata.exhibit.dto.response.ExhibitItem; +import org.atdev.artrip.external.publicdata.exhibit.dto.response.ExhibitResponse; +import org.atdev.artrip.external.publicdata.exhibit.mapper.ExhibitMapper; +import org.atdev.artrip.global.apipayload.exception.ExternalApiException; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ExhibitSyncService { + + private final ExhibitApiClient exhibitApiClient; + private final ExhibitMapper exhibitMapper; + private final ExhibitRepository exhibitRepository; + private final ExhibitHallRepository exhibitHallRepository; + private final EntityManager entityManager; + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final int MAX_PAGES = 100; + + @Scheduled(cron = "0 0 3 * * *") + public void scheduledSync() { + log.info("----- 전시 정보 스케줄 동기화 start -----"); + + String from = LocalDate.now().format(DATE_FORMATTER); + String to = LocalDate.now().plusMonths(3).format(DATE_FORMATTER); + + SyncResult result = syncByPeriod(from, to); + + log.info("----- 전시 정보 스케줄 동기화 done -----"); + log.info("result - new: {}, updated: {}, failed: {}", + result.getInserted(), result.getUpdated(), result.getFailed()); + } + + public SyncResult syncAll() { + return executeSync(pageNo -> exhibitApiClient.fetchExhibits(pageNo)); + } + + public SyncResult syncByPeriod(String from, String to) { + return executeSync(pageNo -> exhibitApiClient.fetchByPeriod(from, to, pageNo)); + } + + private SyncResult executeSync(Function fetcher) { + SyncResult totalResult = new SyncResult(); + int pageNo = 1; + + try { + while (pageNo <= MAX_PAGES) { + ExhibitResponse response = fetcher.apply(pageNo); + + if (!response.isSuccess() || !response.hasData()) { + log.info("no date found - {}", pageNo); + break; + } + + SyncResult pageResult = processItems(response.getItems()); + totalResult.merge(pageResult); + + log.debug("페이지 {} 처리 완료 - new: {}, updated: {}, failed: {}", + pageNo, pageResult.getInserted(), pageResult.getUpdated(), pageResult.getFailed()); + + if (response.isLastPage()) { + break; + } + + pageNo++; + Thread.sleep(500); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("동기화 중단됨", e); + } catch (ExternalApiException e) { + log.error("API 요청 failed - page: {}, error: {}", pageNo, e.getMessage()); + } + + log.info("동기화 완료 - new: {}, updated: {}, failed: {}", + totalResult.getInserted(), totalResult.getUpdated(), totalResult.getFailed()); + + return totalResult; + } + + private SyncResult processItems(List items) { + SyncResult result = new SyncResult(); + Map hallCache = new HashMap<>(); + + for (ExhibitItem item : items) { + try { + processItem(item, hallCache, result); + entityManager.flush(); + } catch (Exception e) { + log.warn("처리 실패 - title: {}, error: {}", item.getTitle(), e.getMessage()); + result.incrementFailed(); + entityManager.clear(); + } + } + return result; + } + + private void processItem(ExhibitItem item, Map hallCache, SyncResult result) { + ExhibitHall exhibitHall = getOrCreateExhibitHall(item, hallCache); + + Exhibit exhibit = exhibitMapper.toExhibit(item); + if (exhibit == null) { + result.incrementFailed(); + return; + } + + exhibit.setExhibitHall(exhibitHall); + + Optional existing = exhibitRepository + .findByTitleAndStartDate(exhibit.getTitle(), exhibit.getStartDate()); + + if (existing.isPresent()) { + updateExhibit(existing.get(), exhibit); + result.incrementUpdated(); + } else { + exhibitRepository.save(exhibit); + result.incrementInserted(); + } + } + + private ExhibitHall getOrCreateExhibitHall(ExhibitItem item, Map cache) { + String placeName = item.getPlace(); + if (placeName == null || placeName.isBlank()) { + return null; + } + + if (cache.containsKey(placeName)) { + ExhibitHall cached = cache.get(placeName); + updateCoordinatesIfNeeded(cached, item); + return cached; + } + + Optional existing = exhibitHallRepository.findByName(placeName); + if (existing.isPresent()) { + ExhibitHall hall = existing.get(); + updateCoordinatesIfNeeded(hall, item); + cache.put(placeName, hall); + return hall; + } + + ExhibitHall newHall = exhibitMapper.toExhibitHall(item); + if (newHall != null) { + newHall = exhibitHallRepository.save(newHall); + cache.put(placeName, newHall); + } + + return newHall; + } + + private void updateExhibit(Exhibit existing, Exhibit newData) { + existing.setDescription(newData.getDescription()); + existing.setEndDate(newData.getEndDate()); + existing.setStatus(newData.getStatus()); + existing.setPosterUrl(newData.getPosterUrl()); + existing.setTicketUrl(newData.getTicketUrl()); + existing.setUpdatedAt(LocalDateTime.now()); + } + + private void updateCoordinatesIfNeeded(ExhibitHall hall, ExhibitItem item) { + if (hall.getLatitude() == null && item.getGpsX() != null) { + hall.setLatitude(parseCoordinate(item.getGpsX())); + hall.setLongitude(parseCoordinate(item.getGpsY())); + exhibitHallRepository.save(hall); + } + } + + private BigDecimal parseCoordinate(String coordinate) { + if (coordinate == null || coordinate.isBlank()) { + return null; + } + try { + return new BigDecimal(coordinate.trim()); + } catch (NumberFormatException e) { + log.warn("좌표 파싱 실패: {}", coordinate); + return null; + } + } + + @Getter + public static class SyncResult { + private int inserted = 0; + private int updated = 0; + private int failed = 0; + + public void incrementInserted() { inserted++; } + public void incrementUpdated() { updated++; } + public void incrementFailed() { failed++; } + + public void merge(SyncResult other) { + this.inserted += other.inserted; + this.updated += other.updated; + this.failed += other.failed; + } + } +} diff --git a/src/main/java/org/atdev/artrip/external/publicdata/exhibit/web/controller/ExhibitSyncController.java b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/web/controller/ExhibitSyncController.java new file mode 100644 index 0000000..3fc0330 --- /dev/null +++ b/src/main/java/org/atdev/artrip/external/publicdata/exhibit/web/controller/ExhibitSyncController.java @@ -0,0 +1,25 @@ +package org.atdev.artrip.external.publicdata.exhibit.web.controller; + +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.atdev.artrip.external.publicdata.exhibit.service.ExhibitSyncService; +import org.atdev.artrip.global.apipayload.CommonResponse; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("api/admin/sync") +public class ExhibitSyncController { + + private final ExhibitSyncService exhibitSyncService; + + @PostMapping("/exhibits") + public CommonResponse syncAll() { + ExhibitSyncService.SyncResult result = exhibitSyncService.syncAll(); + return CommonResponse.onSuccess(result); + } +} diff --git a/src/main/java/org/atdev/artrip/external/publicdata/properties/PublicDataProperties.java b/src/main/java/org/atdev/artrip/external/publicdata/properties/PublicDataProperties.java new file mode 100644 index 0000000..b81db4e --- /dev/null +++ b/src/main/java/org/atdev/artrip/external/publicdata/properties/PublicDataProperties.java @@ -0,0 +1,19 @@ +package org.atdev.artrip.external.publicdata.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "external.publicdata") +public class PublicDataProperties { + + private String baseUrl; + private String serviceKey; + private int connectTimeout = 5000; + private int readTimeout = 30000; + private int pageSize = 100; +} diff --git a/src/main/java/org/atdev/artrip/global/apipayload/exception/ExternalApiException.java b/src/main/java/org/atdev/artrip/global/apipayload/exception/ExternalApiException.java new file mode 100644 index 0000000..c43cdb6 --- /dev/null +++ b/src/main/java/org/atdev/artrip/global/apipayload/exception/ExternalApiException.java @@ -0,0 +1,33 @@ +package org.atdev.artrip.global.apipayload.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatusCode; + +@Getter +public class ExternalApiException extends RuntimeException { + + private final String apiName; + private final HttpStatusCode statusCode; + private final String responseBody; + + public ExternalApiException(String apiName, String message) { + super(message); + this.apiName = apiName; + this.statusCode = null; + this.responseBody = null; + } + + public ExternalApiException(String apiName, HttpStatusCode statusCode, String responseBody) { + super(String.format("[%s] API 호출 실패 - Status: %s, Body: %s", apiName, statusCode, responseBody)); + this.apiName = apiName; + this.statusCode = statusCode; + this.responseBody = responseBody; + } + + public ExternalApiException(String apiName, String message, Throwable cause) { + super(message, cause); + this.apiName = apiName; + this.statusCode = null; + this.responseBody = null; + } +} diff --git a/src/main/java/org/atdev/artrip/global/config/FCMConfig.java b/src/main/java/org/atdev/artrip/global/config/FCMConfig.java new file mode 100644 index 0000000..996217c --- /dev/null +++ b/src/main/java/org/atdev/artrip/global/config/FCMConfig.java @@ -0,0 +1,38 @@ +package org.atdev.artrip.global.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.FileInputStream; +import java.io.IOException; + +@Slf4j +@Component +public class FCMConfig { + + @Value("${firebase.credentials.path}") + private String firebaseConfigPath; + + @PostConstruct + public void initializeFirebase() throws IOException { + log.info("[Firebase] Initialization started"); + if (FirebaseApp.getApps().isEmpty()) { + + try (FileInputStream serviceAccount = + new FileInputStream(firebaseConfigPath)) { + + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccount)) + .build(); + + FirebaseApp.initializeApp(options); + } + } + } + +} diff --git a/src/main/resources/db/migration/V1__init_database.sql b/src/main/resources/db/migration/V20251127__init_database.sql similarity index 100% rename from src/main/resources/db/migration/V1__init_database.sql rename to src/main/resources/db/migration/V20251127__init_database.sql diff --git a/src/main/resources/db/migration/V2__add_onboarding_completed_to_user.sql b/src/main/resources/db/migration/V20251209__add_onboarding_completed_to_user.sql similarity index 100% rename from src/main/resources/db/migration/V2__add_onboarding_completed_to_user.sql rename to src/main/resources/db/migration/V20251209__add_onboarding_completed_to_user.sql diff --git a/src/main/resources/db/migration/V20251213__modify_exhibit_hall_and_keyword_schema.sql b/src/main/resources/db/migration/V20251213__modify_exhibit_hall_and_keyword_schema.sql new file mode 100644 index 0000000..fc6977c --- /dev/null +++ b/src/main/resources/db/migration/V20251213__modify_exhibit_hall_and_keyword_schema.sql @@ -0,0 +1,7 @@ +alter table exhibit_hall add Column longitude DECIMAL(10, 7) NULL; +alter table exhibit_hall add Column latitude DECIMAL(10, 7) NULL; + +alter table exhibit drop Column longitude; +alter table exhibit drop Column latitude; + +alter table keyword drop column `group`