diff --git a/.dockerignore b/.dockerignore index 9f4c740..fbe99aa 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1 @@ -db/ \ No newline at end of file +docker/db/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 41220b4..b3fb8b7 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,7 @@ out/ .vscode/ .idea .env -/db +/docker/db !/resources/db/*.sql mongodb diff --git a/build.gradle b/build.gradle index d855e35..22e5e98 100644 --- a/build.gradle +++ b/build.gradle @@ -28,8 +28,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' - implementation 'co.elastic.clients:elasticsearch-java:8.14.0' implementation 'jakarta.json:jakarta.json-api:2.1.3' implementation 'org.eclipse.parsson:parsson:1.1.5' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' @@ -47,6 +45,7 @@ dependencies { implementation 'io.github.resilience4j:resilience4j-reactor:2.2.0' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' implementation 'io.github.cdimascio:java-dotenv:5.2.2' + implementation 'org.modelmapper:modelmapper:3.1.1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9' @@ -82,15 +81,3 @@ tasks.named('test') { clean { delete file('src/main/generated') } - - -// bootRun 자동 컴파일 설정 (핫 리로딩) -tasks.named('bootRun') { - dependsOn 'classes' - - doFirst { - println "starting bootRun with auto-compile..." - } -} - - diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 29f0bb2..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,50 +0,0 @@ -version: "3.8" - -services: - db: - image: mysql:8.4 - container_name: db - platform: linux/amd64 - environment: - MYSQL_USER: artrip - MYSQL_ROOT_PASSWORD: artrip1! - MYSQL_DATABASE: artrip - MYSQL_PASSWORD: artrip1! - ports: - - "33069:3306" - volumes: - - ./db/data:/var/lib/mysql - - ./db/conf:/etc/mysql/conf.d - - redis: - image: redis:7.2 - container_name: redis - restart: always - ports: - - "63799:6379" - volumes: - - redis-data:/data - - elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0 - container_name: elastic - environment: - - discovery.type=single-node - - xpack.security.enabled=false - - xpack.security.http.ssl.enabled=false - - ES_JAVA_OPTS=-Xms1g -Xmx1g - ports: - - "9200:9200" - - "9300:9300" - volumes: - - ./elasticsearch/data:/usr/share/elasticsearch/data - healthcheck: - test: [ "CMD", "curl", "-f", "http://localhost:9200/_cluster/health" ] - interval: 10s - timeout: 5s - retries: 5 - start_period: 30s - - -volumes: - redis-data: \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 8a096be..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM --platform=linux/amd64 gradle:8.5-jdk17-alpine AS builder - -WORKDIR /app - -COPY . /app - -RUN gradle build -x test --no-daemon - -FROM bellsoft/liberica-openjdk-alpine:17 AS develop - -WORKDIR /app - -RUN apk add --no-cache curl netcat-openbsd bash - -ENV SPRING_PROFILES_ACTIVE=local - -ENTRYPOINT ["sh", "-c", "./gradlew bootRun --no-daemon"] - -FROM bellsoft/liberica-openjdk-alpine:17 AS production - -WORKDIR /app - -RUN apk add --no-cache curl netcat-openbsd - -COPY --from=builder /app/build/libs/*.jar /app/artrip.jar - -ENV SPRING_PROFILES_ACTIVE=prod - -ENTRYPOINT ["java", "-jar", "/app/artrip.jar"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..69b2317 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,30 @@ +version: "3.8" + +services: + db: + image: mysql:8.4 + container_name: db + platform: linux/amd64 + environment: + MYSQL_USER: artrip + MYSQL_ROOT_PASSWORD: artrip1! + MYSQL_DATABASE: artrip + MYSQL_PASSWORD: artrip1! + ports: + - "33069:3306" + volumes: + - ./db/data:/var/lib/mysql + - ./db/conf:/etc/mysql/conf.d + + redis: + image: redis:7.2 + container_name: redis + restart: always + ports: + - "63799:6379" + volumes: + - redis-data:/data + + +volumes: + redis-data: \ No newline at end of file diff --git a/src/main/java/org/atdev/artrip/config/ElasticsearchConfig.java b/src/main/java/org/atdev/artrip/config/ElasticsearchConfig.java deleted file mode 100644 index aa94135..0000000 --- a/src/main/java/org/atdev/artrip/config/ElasticsearchConfig.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.atdev.artrip.config; - -import co.elastic.clients.elasticsearch.ElasticsearchClient; -import co.elastic.clients.json.jackson.JacksonJsonpMapper; -import co.elastic.clients.transport.ElasticsearchTransport; -import co.elastic.clients.transport.rest_client.RestClientTransport; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import org.apache.http.HttpHost; -import org.elasticsearch.client.RestClient; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class ElasticsearchConfig { - - @Bean - public RestClient restClient() { - return RestClient.builder(new HttpHost("localhost", 9200)).build(); - } - - @Bean - public ElasticsearchTransport elasticsearchTransport(RestClient restClient) { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.registerModule(new JavaTimeModule()); - objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - - return new RestClientTransport(restClient, new JacksonJsonpMapper(objectMapper)); - } - - @Bean - public ElasticsearchClient elasticsearchClient(ElasticsearchTransport transport) { - return new ElasticsearchClient(transport); - } -} diff --git a/src/main/java/org/atdev/artrip/config/ElasticsearchIndexConfig.java b/src/main/java/org/atdev/artrip/config/ElasticsearchIndexConfig.java deleted file mode 100644 index 8505929..0000000 --- a/src/main/java/org/atdev/artrip/config/ElasticsearchIndexConfig.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.atdev.artrip.config; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.atdev.artrip.elastic.service.ExhibitIndexService; -import org.springframework.boot.CommandLineRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; - -@Configuration -@RequiredArgsConstructor -@Slf4j -public class ElasticsearchIndexConfig { - - private final ExhibitIndexService exhibitIndexService; - - @Bean - @Profile("!test") - public CommandLineRunner initializeElasticsearchIndex(){ - return args -> { - log.info("Initializing Elasticsearch index"); - - try { - exhibitIndexService.createAndApplyIndex(); - int indexedCount = exhibitIndexService.indexAllExhibits(); - log.info("Indexed {} exhibits into Elasticsearch", indexedCount); - - } catch (Exception e) { - log.error("Error initializing Elasticsearch index", e); - } - }; - } -} diff --git a/src/main/java/org/atdev/artrip/config/WebConfig.java b/src/main/java/org/atdev/artrip/config/WebConfig.java index 05810b2..5b43f8d 100644 --- a/src/main/java/org/atdev/artrip/config/WebConfig.java +++ b/src/main/java/org/atdev/artrip/config/WebConfig.java @@ -1,11 +1,20 @@ package org.atdev.artrip.config; +import lombok.RequiredArgsConstructor; +import org.atdev.artrip.global.resolver.LoginUserIdArgumentResolver; import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.List; + @Configuration +@RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { + + private final LoginUserIdArgumentResolver userIdArgumentResolver; + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") @@ -13,4 +22,10 @@ public void addCorsMappings(CorsRegistry registry) { .allowedMethods("OPTIONS","GET", "POST", "PUT", "PATCH", "DELETE") .allowCredentials(true); } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(userIdArgumentResolver); + } + } \ No newline at end of file diff --git a/src/main/java/org/atdev/artrip/controller/AdminExhibitController.java b/src/main/java/org/atdev/artrip/controller/AdminExhibitController.java index 91783ec..5c51f21 100644 --- a/src/main/java/org/atdev/artrip/controller/AdminExhibitController.java +++ b/src/main/java/org/atdev/artrip/controller/AdminExhibitController.java @@ -17,7 +17,6 @@ @RestController @RequiredArgsConstructor @RequestMapping("/admin/exhibits") -@Slf4j @Tag(name = "Admin - Exhibit", description = "관리자 전시 관리 API") public class AdminExhibitController { @@ -37,7 +36,6 @@ public class AdminExhibitController { ) @GetMapping public CommonResponse> getExhibitList(Criteria cri) { - log.info("Admin getting exhibit list: {}", cri); PagingResponseDTO result = adminExhibitService.getExhibitList(cri); @@ -47,7 +45,6 @@ public CommonResponse> getExhibitLis @Operation(summary = "전시 상세 조회", description = "특정 전시의 상세 정보를 조회합니다.") @GetMapping("/{exhibitId}") public CommonResponse getExhibit(@PathVariable Long exhibitId) { - log.info("Admin getting exhibit : {}", exhibitId); AdminExhibitResponse result = adminExhibitService.getExhibit(exhibitId); @@ -57,7 +54,6 @@ public CommonResponse getExhibit(@PathVariable Long exhibi @Operation(summary = "전시 등록", description = "새로운 전시를 등록합니다.") @PostMapping public CommonResponse createExhibit(@RequestBody CreateExhibitRequest request) { - log.info("Admin creating exhibit: title = {}", request.getTitle()); Long exhibitId = adminExhibitService.createExhibit(request); @@ -67,7 +63,6 @@ public CommonResponse createExhibit(@RequestBody CreateExhibitRequest requ @Operation(summary = "전시 수정", description = "특정 전시를 수정합니다.") @PutMapping("/{exhibitId}") public CommonResponse updateExhibit(@PathVariable Long exhibitId, @RequestBody UpdateExhibitRequest request){ - log.info("Admin updating exhibit: {}", request.getTitle()); Long updatedId = adminExhibitService.updateExhibit(exhibitId, request); @@ -77,7 +72,6 @@ public CommonResponse updateExhibit(@PathVariable Long exhibitId, @Request @Operation(summary = "전시 삭제", description = "특정 전시를 삭제합니다.") @DeleteMapping("/{exhibitId}") public CommonResponse deleteExhibit(@PathVariable Long exhibitId) { - log.info("Admin deleting exhibit: {}", exhibitId); adminExhibitService.deleteExhibit(exhibitId); return CommonResponse.onSuccess(null); diff --git a/src/main/java/org/atdev/artrip/controller/AdminExhibitHallController.java b/src/main/java/org/atdev/artrip/controller/AdminExhibitHallController.java index 63174b7..437d2cc 100644 --- a/src/main/java/org/atdev/artrip/controller/AdminExhibitHallController.java +++ b/src/main/java/org/atdev/artrip/controller/AdminExhibitHallController.java @@ -17,7 +17,6 @@ @RestController @RequestMapping("/admin/exhibit-halls") @RequiredArgsConstructor -@Slf4j @Tag(name = "Admin - ExhibitHall", description = "관리자 전시관 관리 API") public class AdminExhibitHallController { @@ -37,7 +36,6 @@ public class AdminExhibitHallController { ) @GetMapping public CommonResponse> getExhibitHallList(Criteria cri) { - log.info("Admin getting exhibit hall : {}" , cri); PagingResponseDTO result = adminExhibitHallService.getExhibitHallList(cri); @@ -47,7 +45,6 @@ public CommonResponse> getExhibitHall @Operation(summary = "전시관 상세 조회", description = "전시관 ID로 전시관 상세 조회") @GetMapping("/{exhibitHallId}") public CommonResponse getExhibitHall(@PathVariable Long exhibitHallId) { - log.info("Admin getting exhibit hall : {}" , exhibitHallId); ExhibitHallResponse result = adminExhibitHallService.getExhibitHall(exhibitHallId); @@ -57,7 +54,6 @@ public CommonResponse getExhibitHall(@PathVariable Long exh @Operation(summary = "전시관 등록" ) @PostMapping public CommonResponse createExhibitHall(@RequestBody CreateExhibitHallRequest request) { - log.info("Admin creating exhibit hall : {}" , request); Long result = adminExhibitHallService.createExhibitHall(request); @@ -67,7 +63,6 @@ public CommonResponse createExhibitHall(@RequestBody CreateExhibitHallRequ @Operation(summary = "전시관 수정" ) @PutMapping("/{exhibitHallId}") public CommonResponse updateExhibitHall(@PathVariable Long exhibitHallId, @RequestBody UpdateExhibitHallRequest request) { - log.info("Admin updating exhibit hall : {}, {}", exhibitHallId, request); Long result = adminExhibitHallService.updateExhibitHall(exhibitHallId, request); @@ -77,7 +72,6 @@ public CommonResponse updateExhibitHall(@PathVariable Long exhibitHallId, @Operation(summary = "전시관 삭제" ) @DeleteMapping("/{exhibitHallId}") public CommonResponse deleteExhibitHall(@PathVariable Long exhibitHallId) { - log.info("Admin deleting exhibit hall : {}" , exhibitHallId); adminExhibitHallService.deleteExhibitHall(exhibitHallId); diff --git a/src/main/java/org/atdev/artrip/controller/AuthController.java b/src/main/java/org/atdev/artrip/controller/AuthController.java index 64e3331..b2dba43 100644 --- a/src/main/java/org/atdev/artrip/controller/AuthController.java +++ b/src/main/java/org/atdev/artrip/controller/AuthController.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import org.atdev.artrip.controller.dto.request.ReissueRequest; import org.atdev.artrip.global.apipayload.code.status.UserErrorCode; +import org.atdev.artrip.global.resolver.LoginUser; import org.atdev.artrip.service.AuthService; import org.atdev.artrip.controller.dto.request.SocialLoginRequest; import org.atdev.artrip.controller.dto.response.SocialLoginResponse; @@ -13,8 +14,6 @@ import org.atdev.artrip.global.apipayload.code.status.CommonErrorCode; import org.atdev.artrip.global.swagger.ApiErrorResponses; 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.*; @RestController @@ -110,9 +109,7 @@ public ResponseEntity> socialLogin(@RequestB @Operation(summary = "isFirstLogin값 반전 api") @PostMapping("/complete") public ResponseEntity completeOnboarding( - @AuthenticationPrincipal UserDetails userDetails) { - - long userId = Long.parseLong(userDetails.getUsername()); + @LoginUser Long userId) { authService.completeOnboarding(userId); diff --git a/src/main/java/org/atdev/artrip/controller/ExhibitController.java b/src/main/java/org/atdev/artrip/controller/ExhibitController.java index 4ba9f41..5e57812 100644 --- a/src/main/java/org/atdev/artrip/controller/ExhibitController.java +++ b/src/main/java/org/atdev/artrip/controller/ExhibitController.java @@ -2,12 +2,12 @@ import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; -import org.atdev.artrip.controller.dto.response.ExhibitDetailResponse; +import org.atdev.artrip.controller.dto.response.*; +import org.atdev.artrip.global.resolver.LoginUser; +import org.atdev.artrip.global.s3.service.S3Service; import org.atdev.artrip.service.ExhibitService; import org.atdev.artrip.controller.dto.request.ExhibitFilterRequest; -import org.atdev.artrip.controller.dto.response.FilterResponse; import org.atdev.artrip.service.HomeService; -import org.atdev.artrip.controller.dto.response.RegionResponse; import org.atdev.artrip.global.apipayload.CommonResponse; import org.atdev.artrip.global.apipayload.code.status.CommonErrorCode; import org.atdev.artrip.global.apipayload.code.status.HomeErrorCode; @@ -15,23 +15,20 @@ import org.atdev.artrip.global.swagger.ApiErrorResponses; import org.springdoc.core.annotations.ParameterObject; 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.format.DateTimeFormatter; import java.util.List; @RestController @RequiredArgsConstructor -@RequestMapping("/exhibit") +@RequestMapping("/exhibits") public class ExhibitController { private final HomeService homeService; private final ExhibitService exhibitService; + private final S3Service s3Service; - private Long getUserId(UserDetails userDetails) { - return userDetails != null ? Long.parseLong(userDetails.getUsername()) : null; - } @Operation(summary = "장르 조회", description = "키워드 장르 데이터 전체 조회") @ApiErrorResponses( common = {CommonErrorCode._BAD_REQUEST, CommonErrorCode._UNAUTHORIZED}, @@ -51,11 +48,10 @@ public ResponseEntity>> getGenres(){ @GetMapping("/{id}") public ResponseEntity> getExhibit( @PathVariable Long id, - @AuthenticationPrincipal UserDetails userDetails, + @LoginUser Long userId, @ParameterObject ImageResizeRequest resize ){ - Long userId = getUserId(userDetails); ExhibitDetailResponse exhibit= exhibitService.getExhibitDetail(id, userId, resize); return ResponseEntity.ok(CommonResponse.onSuccess(exhibit)); @@ -87,20 +83,49 @@ public ResponseEntity>> getDomestic(){ } - @Operation(summary = "전시 조건 필터 전체 조회",description = "기간, 지역, 장르, 전시 스타일 필터 조회 - null 시 전체선택") + @Operation(summary = "전시 검색 및 필터링",description = "기간, 지역, 장르, 전시 스타일, 키워드 필터 조회 - null 시 전체선택") @ApiErrorResponses( common = {CommonErrorCode._BAD_REQUEST, CommonErrorCode._UNAUTHORIZED}, home = {HomeErrorCode._HOME_INVALID_DATE_RANGE, HomeErrorCode._HOME_UNRECOGNIZED_REGION, HomeErrorCode._HOME_EXHIBIT_NOT_FOUND} ) - @PostMapping("/filter") - public ResponseEntity getDomesticFilter(@RequestBody ExhibitFilterRequest dto, - @RequestParam(required = false) Long cursor, - @RequestParam(defaultValue = "20") Long size, - @AuthenticationPrincipal UserDetails userDetails) { - Long userId = getUserId(userDetails); - FilterResponse exhibits = homeService.getFilterExhibit(dto, size, cursor,userId); - - return ResponseEntity.ok(exhibits); - + @GetMapping + public CommonResponse> getExhibit( + @ModelAttribute ExhibitFilterRequest request, + @ModelAttribute ImageResizeRequest resizeRequest, + @LoginUser Long userId) { + + CursorPaginationResponse serviceResult = homeService.findExhibits(request, userId); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + + List data = serviceResult.getData().stream() + .map(result -> { + String period = result.startDate().format(formatter) + " - " + result.endDate().format(formatter); + String resizedUrl = s3Service.buildResizeUrl( + result.posterUrl(), + resizeRequest.w(), + resizeRequest.h(), + resizeRequest.f() + ); + return HomeListResponse.builder() + .exhibit_id(result.exhibitId()) + .title(result.title()) + .posterUrl(resizedUrl) + .status(result.status()) + .exhibitPeriod(period) + .hallName(result.hallName()) + .regionName(result.region()) + .countryName(result.country()) + .isFavorite(result.isFavorite()) + .build(); + }).toList(); + + CursorPaginationResponse result = CursorPaginationResponse.of( + data, + serviceResult.isHasNext(), + serviceResult.getNextCursor() + ); + + return CommonResponse.onSuccess(result); } } diff --git a/src/main/java/org/atdev/artrip/controller/FavoriteController.java b/src/main/java/org/atdev/artrip/controller/FavoriteController.java index 3a8f6fc..fb41ab2 100644 --- a/src/main/java/org/atdev/artrip/controller/FavoriteController.java +++ b/src/main/java/org/atdev/artrip/controller/FavoriteController.java @@ -3,17 +3,15 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.atdev.artrip.controller.dto.response.CalenderResponse; import org.atdev.artrip.controller.dto.response.FavoriteResponse; +import org.atdev.artrip.global.resolver.LoginUser; import org.atdev.artrip.service.FavoriteExhibitService; import org.atdev.artrip.global.apipayload.CommonResponse; import org.atdev.artrip.global.apipayload.code.status.CommonErrorCode; import org.atdev.artrip.global.apipayload.code.status.FavoriteErrorCode; import org.atdev.artrip.global.swagger.ApiErrorResponses; import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; @@ -21,7 +19,6 @@ import java.util.List; import java.util.Map; -@Slf4j @RestController @RequiredArgsConstructor @RequestMapping("favorites") @@ -37,12 +34,9 @@ public class FavoriteController { ) @PostMapping("/{exhibitId}") public CommonResponse addFavorite( - @AuthenticationPrincipal UserDetails userDetails, + @LoginUser Long userId, @PathVariable Long exhibitId) { - Long userId = Long.parseLong(userDetails.getUsername()); - log.info("Adding favorite for userId: {} , exhibit {}", userId, exhibitId); - FavoriteResponse response = favoriteExhibitService.addFavorite(userId, exhibitId); return CommonResponse.onSuccess(response); } @@ -54,12 +48,9 @@ public CommonResponse addFavorite( ) @DeleteMapping("/{exhibitId}") public CommonResponse removeFavorite( - @AuthenticationPrincipal UserDetails userDetails, + @LoginUser Long userId, @PathVariable Long exhibitId) { - Long userId = Long.parseLong(userDetails.getUsername()); - log.info("Removing favorite for userId: {} , exhibit {}", userId, exhibitId); - favoriteExhibitService.removeFavorite(userId, exhibitId); return CommonResponse.onSuccess(null); } @@ -71,10 +62,7 @@ public CommonResponse removeFavorite( ) @GetMapping public CommonResponse> getAllFavorites( - @AuthenticationPrincipal UserDetails userDetails) { - - Long userId = Long.parseLong(userDetails.getUsername()); - log.info("Getting all favorites for userId: {}", userId); + @LoginUser Long userId) { List favorites = favoriteExhibitService.getAllFavorites(userId); @@ -90,13 +78,10 @@ public CommonResponse> getAllFavorites( ) @GetMapping("/date") public CommonResponse> getFavoritesByDate( - @AuthenticationPrincipal UserDetails userDetails, + @LoginUser Long userId, @RequestParam("data") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { - Long userId = Long.parseLong(userDetails.getUsername()); - log.info("Getting all favorites for userId: {}, date: {}", userId, date); - List favorites = favoriteExhibitService.getFavoritesByDate(userId, date); return CommonResponse.onSuccess(favorites); } @@ -110,12 +95,11 @@ public CommonResponse> getFavoritesByDate( ) @GetMapping("/country") public CommonResponse> getFavoritesByCountry( - @AuthenticationPrincipal UserDetails userDetails, + @LoginUser Long userId, @RequestParam String country) { - Long userId = Long.parseLong(userDetails.getUsername()); - log.info("Getting all favorites for userId: {}, country: {}", userId, country); List favorites = favoriteExhibitService.getFavoritesByCountry(userId, country); + return CommonResponse.onSuccess(favorites); } @@ -128,13 +112,10 @@ public CommonResponse> getFavoritesByCountry( ) @GetMapping("/calendar") public CommonResponse getCalenderDates( - @AuthenticationPrincipal UserDetails userDetails, + @LoginUser Long userId, @RequestParam int year, @RequestParam int month) { - Long userId = Long.parseLong(userDetails.getUsername()); - log.info("Getting calendar dates for userId: {}, year: {}, month: {}", userId, year, month); - CalenderResponse response = favoriteExhibitService.getCalenderDates(userId, year, month); return CommonResponse.onSuccess(response); } @@ -148,10 +129,7 @@ public CommonResponse getCalenderDates( ) @GetMapping("/countries") public CommonResponse> getFavoriteCountries( - @AuthenticationPrincipal UserDetails userDetails) { - - Long userId = Long.parseLong(userDetails.getUsername()); - log.info("Getting favorite countries for userId: {}", userId); + @LoginUser Long userId) { List countries = favoriteExhibitService.getFavoriteCountries(userId); return CommonResponse.onSuccess(countries); @@ -166,9 +144,8 @@ public CommonResponse> getFavoriteCountries( ) @GetMapping("/check/{exhibitId}") public CommonResponse> checkFavorite( - @AuthenticationPrincipal UserDetails userDetails, + @LoginUser Long userId, @PathVariable Long exhibitId) { - Long userId = Long.parseLong(userDetails.getUsername()); boolean isFavorite = favoriteExhibitService.isFavorite(userId, exhibitId); diff --git a/src/main/java/org/atdev/artrip/controller/HomeController.java b/src/main/java/org/atdev/artrip/controller/HomeController.java index 78a57ec..b6073b9 100644 --- a/src/main/java/org/atdev/artrip/controller/HomeController.java +++ b/src/main/java/org/atdev/artrip/controller/HomeController.java @@ -4,6 +4,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.atdev.artrip.controller.dto.response.HomeListResponse; +import org.atdev.artrip.global.resolver.LoginUser; import org.atdev.artrip.service.HomeService; import org.atdev.artrip.controller.dto.request.GenreRandomRequest; import org.atdev.artrip.controller.dto.request.PersonalizedRequest; @@ -16,8 +17,6 @@ import org.atdev.artrip.global.swagger.ApiErrorResponses; import org.springdoc.core.annotations.ParameterObject; 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.util.List; @@ -51,12 +50,10 @@ public class HomeController { ) @PostMapping("/personalized/random") public ResponseEntity>> getRandomPersonalized( - @AuthenticationPrincipal UserDetails userDetails, + @LoginUser Long userId, @Valid @RequestBody PersonalizedRequest requestDto, @ParameterObject ImageResizeRequest resize){ - long userId = Long.parseLong(userDetails.getUsername()); - List exhibits= homeService.getRandomPersonalized(userId, requestDto, resize); return ResponseEntity.ok(CommonResponse.onSuccess(exhibits)); @@ -85,12 +82,10 @@ public ResponseEntity>> getRandomPersonali @PostMapping("/schedule") public ResponseEntity>> getRandomSchedule( @Valid @RequestBody ScheduleRandomRequest request, - @AuthenticationPrincipal UserDetails userDetails, + @LoginUser Long userId, @ParameterObject ImageResizeRequest resize){ - Long userId = Long.parseLong(userDetails.getUsername()); - List exhibits= homeService.getRandomSchedule(request, userId, resize); return ResponseEntity.ok(CommonResponse.onSuccess(exhibits)); @@ -121,12 +116,9 @@ public ResponseEntity>> getRandomSchedule( @PostMapping("/genre/random") public ResponseEntity>> getRandomExhibits( @Valid @RequestBody GenreRandomRequest request, - @AuthenticationPrincipal UserDetails userDetails, + @LoginUser Long userId, @ParameterObject ImageResizeRequest resize){ - - Long userId = Long.parseLong(userDetails.getUsername()); - List exhibits = homeService.getRandomGenre(request, userId, resize); return ResponseEntity.ok(CommonResponse.onSuccess(exhibits)); } @@ -154,11 +146,9 @@ public ResponseEntity>> getRandomExhibits( @PostMapping("recommend/today") public ResponseEntity>> getTodayRecommendations( @Valid @RequestBody TodayRandomRequest request, - @AuthenticationPrincipal UserDetails userDetails, + @LoginUser Long userId, @ParameterObject ImageResizeRequest resize){ - Long userId = Long.parseLong(userDetails.getUsername()); - List exhibits = homeService.getRandomToday(request, userId, resize); return ResponseEntity.ok(CommonResponse.onSuccess(exhibits)); diff --git a/src/main/java/org/atdev/artrip/controller/ReviewController.java b/src/main/java/org/atdev/artrip/controller/ReviewController.java index 202629d..afe59af 100644 --- a/src/main/java/org/atdev/artrip/controller/ReviewController.java +++ b/src/main/java/org/atdev/artrip/controller/ReviewController.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; +import org.atdev.artrip.global.resolver.LoginUser; import org.atdev.artrip.service.ReviewService; import org.atdev.artrip.controller.dto.request.ReviewCreateRequest; import org.atdev.artrip.controller.dto.response.ExhibitReviewSliceResponse; @@ -15,8 +16,6 @@ import org.atdev.artrip.global.swagger.ApiErrorResponses; import org.springdoc.core.annotations.ParameterObject; 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 org.springframework.web.multipart.MultipartFile; @@ -38,9 +37,7 @@ public class ReviewController { public ResponseEntity> CreateReview(@PathVariable Long exhibitId, @RequestPart(value = "images",required = false) List images, @RequestPart(value = "request") ReviewCreateRequest request, - @AuthenticationPrincipal UserDetails userDetails){ - - Long userId = Long.valueOf(userDetails.getUsername()); + @LoginUser Long userId){ ReviewResponse review = reviewService.createReview(exhibitId, request, images, userId); @@ -56,9 +53,7 @@ public ResponseEntity> CreateReview(@PathVariable public ResponseEntity> UpdateReview(@PathVariable Long reviewId, @RequestPart(value = "images",required = false) List images, @RequestPart("request") ReviewUpdateRequest request, - @AuthenticationPrincipal UserDetails userDetails){ - - Long userId = Long.valueOf(userDetails.getUsername()); + @LoginUser Long userId ){ ReviewResponse review = reviewService.updateReview(reviewId, request, images, userId); @@ -72,9 +67,7 @@ public ResponseEntity> UpdateReview(@PathVariable ) @DeleteMapping("/{reviewId}") public ResponseEntity> DeleteReview(@PathVariable Long reviewId, - @AuthenticationPrincipal UserDetails userDetails){ - - Long userId = Long.valueOf(userDetails.getUsername()); + @LoginUser Long userId){ reviewService.deleteReview(reviewId, userId); @@ -90,11 +83,9 @@ public ResponseEntity> DeleteReview(@PathVariable Long re public ResponseEntity> getAllReview( @RequestParam(required = false) Long cursor, @RequestParam(defaultValue = "10") int size, - @AuthenticationPrincipal UserDetails userDetails, + @LoginUser Long userId, @ParameterObject ImageResizeRequest resize) { - Long userId = Long.valueOf(userDetails.getUsername()); - ReviewSliceResponse response = reviewService.getAllReview(userId, cursor, size, resize); return ResponseEntity.ok(CommonResponse.onSuccess(response)); diff --git a/src/main/java/org/atdev/artrip/controller/SearchController.java b/src/main/java/org/atdev/artrip/controller/SearchController.java deleted file mode 100644 index 3c5ad7b..0000000 --- a/src/main/java/org/atdev/artrip/controller/SearchController.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.atdev.artrip.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.atdev.artrip.global.apipayload.code.status.SearchErrorCode; -import org.atdev.artrip.service.SearchHistoryService; -import org.atdev.artrip.global.apipayload.CommonResponse; -import org.atdev.artrip.controller.dto.response.ExhibitSearchResponse; -import org.atdev.artrip.service.ExhibitSearchService; -import org.atdev.artrip.global.apipayload.code.status.CommonErrorCode; -import org.atdev.artrip.global.apipayload.exception.GeneralException; -import org.atdev.artrip.global.swagger.ApiErrorResponses; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/search") -@Slf4j -@Tag(name = "Search-controller", description = "전시회 검색 및 검색어 관리 API") -public class SearchController { - - private final ExhibitSearchService exhibitSearchService; - private final SearchHistoryService searchHistoryService; - - @Operation(summary = "전시회 검색", description = "키워드로 전시회를 검색합니다.") - @ApiErrorResponses( - common = {CommonErrorCode._UNAUTHORIZED, CommonErrorCode._BAD_REQUEST}, - search = {SearchErrorCode._SEARCH_EXHIBIT_NOT_FOUND, SearchErrorCode._SEARCH_KEYWORD_INVALID} - ) - @GetMapping("/exhibits") - public CommonResponse> searchExhibits( - @RequestParam String keyword, - @AuthenticationPrincipal UserDetails userDetails) { - - Long userId = extractUserId(userDetails); - List results = exhibitSearchService.searchExhibits(keyword, userId); - return CommonResponse.onSuccess(results); - } - - @Operation(summary = "최근 검색어 조회", description = "사용자의 최근 검색어 10개를 조회합니다.") - @ApiErrorResponses( - common = {CommonErrorCode._UNAUTHORIZED, CommonErrorCode._BAD_REQUEST}, - search = {SearchErrorCode._SEARCH_HISTORY_NOT_FOUND} - ) - @GetMapping("/history") - public CommonResponse> getRecentKeywords( - @AuthenticationPrincipal UserDetails userDetails) { - - Long userId = extractUserId(userDetails); - List keywords = searchHistoryService.findRecent(userId); - return CommonResponse.onSuccess(keywords); - } - - @Operation(summary = "검색어 삭제", description = "사용자의 특정 검색어를 삭제합니다.") - @ApiErrorResponses( - common = {CommonErrorCode._UNAUTHORIZED, CommonErrorCode._BAD_REQUEST}, - search = {SearchErrorCode._SEARCH_HISTORY_NOT_FOUND} - ) - @DeleteMapping("/history") - public CommonResponse deleteKeywords( - @RequestParam String keyword, - @AuthenticationPrincipal UserDetails userDetails) { - - Long userId = extractUserId(userDetails); - searchHistoryService.remove(userId, keyword); - return CommonResponse.onSuccess(null); - } - - @Operation(summary = "검색어 전체 삭제", description = "사용자의 모든 검색어를 삭제합니다.") - @ApiErrorResponses( - common = {CommonErrorCode._UNAUTHORIZED, CommonErrorCode._BAD_REQUEST}, - search = {SearchErrorCode._SEARCH_HISTORY_NOT_FOUND, SearchErrorCode._SEARCH_TOO_FREQUENT} - ) - @DeleteMapping("/history/all") - public CommonResponse deleteAllKeywords( - @AuthenticationPrincipal UserDetails userDetails) { - - Long userId = extractUserId(userDetails); - searchHistoryService.removeAll(userId); - - return CommonResponse.onSuccess(null); - } - - private Long extractUserId(UserDetails userDetails) { - return userDetails != null ? Long.parseLong(userDetails.getUsername()) : null; - } - - @Operation(summary = "추천 검색어", description = "전체 사용자가 많이 검색한 인기 키워드를 조회합니다.") - @ApiErrorResponses( - common = {CommonErrorCode._UNAUTHORIZED, CommonErrorCode._BAD_REQUEST}, - search = {SearchErrorCode._SEARCH_RECOMMENDATION_NOT_FOUND} - ) - @GetMapping("/recommendations") - public CommonResponse> getRecommendations( - @AuthenticationPrincipal UserDetails userDetails - ) { - if (userDetails == null ) { - throw new GeneralException(CommonErrorCode._UNAUTHORIZED); - } - List keywords = searchHistoryService.findPopularKeywords(); - return CommonResponse.onSuccess(keywords); - } - -} diff --git a/src/main/java/org/atdev/artrip/controller/StatusController.java b/src/main/java/org/atdev/artrip/controller/StatusController.java deleted file mode 100644 index 3e7bec0..0000000 --- a/src/main/java/org/atdev/artrip/controller/StatusController.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.atdev.artrip.controller; - -import org.atdev.artrip.controller.dto.response.StatusResponse; -import org.atdev.artrip.global.apipayload.CommonResponse; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@RestController -public class StatusController { - - private static final Map users = new HashMap<>(); - static { - users.put(1L, "aa"); - users.put(2L, "bb"); - } - @GetMapping("/") - public StatusResponse getStatus() { - return new StatusResponse("201", "greeting"); - } - - @GetMapping("/a") - public ResponseEntity>> getAllUsers() { - List allUsers = new ArrayList<>(users.values()); - return ResponseEntity.ok(CommonResponse.onSuccess(allUsers)); - } -} diff --git a/src/main/java/org/atdev/artrip/controller/UserController.java b/src/main/java/org/atdev/artrip/controller/UserController.java index 26b06c6..123e86c 100644 --- a/src/main/java/org/atdev/artrip/controller/UserController.java +++ b/src/main/java/org/atdev/artrip/controller/UserController.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.atdev.artrip.controller.dto.response.ExhibitRecentResponse; import org.atdev.artrip.global.apipayload.code.status.UserErrorCode; +import org.atdev.artrip.global.resolver.LoginUser; import org.atdev.artrip.service.UserService; import org.atdev.artrip.controller.dto.request.NicknameRequest; import org.atdev.artrip.controller.dto.response.MypageResponse; @@ -16,8 +17,6 @@ import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.MediaType; 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 org.springframework.web.multipart.MultipartFile; @@ -37,11 +36,9 @@ public class UserController { user = {UserErrorCode._PROFILE_IMAGE_NOT_EXIST, UserErrorCode._USER_NOT_FOUND} ) public ResponseEntity> getUpdateImage( - @AuthenticationPrincipal UserDetails userDetails, + @LoginUser Long userId, @RequestPart("image") MultipartFile image){ - Long userId = Long.parseLong(userDetails.getUsername()); - userService.updateProfileImg(userId,image); return ResponseEntity.ok(CommonResponse.onSuccess("프로필 이미지 생성")); @@ -54,9 +51,7 @@ public ResponseEntity> getUpdateImage( user = {UserErrorCode._PROFILE_IMAGE_NOT_EXIST, UserErrorCode._USER_NOT_FOUND} ) public ResponseEntity> getDeleteImage( - @AuthenticationPrincipal UserDetails userDetails){ - - Long userId = Long.parseLong(userDetails.getUsername()); + @LoginUser Long userId){ userService.deleteProfileImg(userId); @@ -70,11 +65,9 @@ public ResponseEntity> getDeleteImage( user = {UserErrorCode._DUPLICATE_NICKNAME, UserErrorCode._USER_NOT_FOUND, UserErrorCode._NICKNAME_BAD_REQUEST} ) public ResponseEntity> updateNickname( - @AuthenticationPrincipal UserDetails user, + @LoginUser Long userId, @RequestBody NicknameRequest dto) { - Long userId = Long.valueOf(user.getUsername()); - NicknameResponse response = userService.updateNickName(userId, dto); return ResponseEntity.ok(CommonResponse.onSuccess(response)); @@ -87,11 +80,9 @@ public ResponseEntity> updateNickname( // user = {UserErrorCode._USER_NOT_FOUND} // ) public ResponseEntity> getMypage( - @AuthenticationPrincipal UserDetails user, + @LoginUser Long userId, @ParameterObject ImageResizeRequest resize) { - Long userId = Long.valueOf(user.getUsername()); - MypageResponse response = userService.getMypage(userId, resize); return ResponseEntity.ok(CommonResponse.onSuccess(response)); @@ -105,9 +96,7 @@ public ResponseEntity> getMypage( exhibit = {ExhibitErrorCode._EXHIBIT_NOT_FOUND} ) public ResponseEntity>> getRecentExhibit( - @AuthenticationPrincipal UserDetails userDetails){ - - Long userId = Long.valueOf(userDetails.getUsername()); + @LoginUser Long userId){ List responses = userService.getRecentViews(userId); diff --git a/src/main/java/org/atdev/artrip/controller/UserKeywordController.java b/src/main/java/org/atdev/artrip/controller/UserKeywordController.java index a35c6a0..49576fa 100644 --- a/src/main/java/org/atdev/artrip/controller/UserKeywordController.java +++ b/src/main/java/org/atdev/artrip/controller/UserKeywordController.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; +import org.atdev.artrip.global.resolver.LoginUser; import org.atdev.artrip.service.KeywordService; import org.atdev.artrip.controller.dto.request.KeywordRequest; import org.atdev.artrip.controller.dto.response.KeywordResponse; @@ -10,8 +11,6 @@ import org.atdev.artrip.global.apipayload.code.status.KeywordErrorCode; import org.atdev.artrip.global.swagger.ApiErrorResponses; 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.util.List; @@ -30,11 +29,9 @@ public class UserKeywordController { ) @PostMapping("/keywords") public ResponseEntity> saveUserKeywords( - @AuthenticationPrincipal UserDetails userDetails, + @LoginUser Long userId, @RequestBody KeywordRequest request) { - Long userId = Long.parseLong(userDetails.getUsername()); // subject → userId형변환 - keywordService.saveUserKeywords(userId, request.getKeywordIds()); return ResponseEntity.ok(CommonResponse.onSuccess(null)); } @@ -57,9 +54,8 @@ public ResponseEntity>> getAllKeywords() { ) @GetMapping("/keywords") public ResponseEntity>> getUserKeywords( - @AuthenticationPrincipal UserDetails userDetails) { + @LoginUser Long userId) { - Long userId = Long.parseLong(userDetails.getUsername()); List keywords = keywordService.getUserKeywords(userId); return ResponseEntity.ok(CommonResponse.onSuccess(keywords)); } diff --git a/src/main/java/org/atdev/artrip/controller/dto/request/ExhibitFilterRequest.java b/src/main/java/org/atdev/artrip/controller/dto/request/ExhibitFilterRequest.java index 9f51120..10341c4 100644 --- a/src/main/java/org/atdev/artrip/controller/dto/request/ExhibitFilterRequest.java +++ b/src/main/java/org/atdev/artrip/controller/dto/request/ExhibitFilterRequest.java @@ -1,7 +1,10 @@ package org.atdev.artrip.controller.dto.request; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.atdev.artrip.constants.SortType; import java.time.LocalDate; @@ -9,6 +12,8 @@ @Builder @Data +@NoArgsConstructor +@AllArgsConstructor public class ExhibitFilterRequest { private LocalDate startDate; @@ -24,4 +29,12 @@ public class ExhibitFilterRequest { private SortType sortType; + private Long cursor; + + private Long size; + + public Long getSize() { + return size != null && size > 0 ? size : 20L; + } + } diff --git a/src/main/java/org/atdev/artrip/controller/dto/request/ImageResizeRequest.java b/src/main/java/org/atdev/artrip/controller/dto/request/ImageResizeRequest.java index 1bc5627..dcc22d5 100644 --- a/src/main/java/org/atdev/artrip/controller/dto/request/ImageResizeRequest.java +++ b/src/main/java/org/atdev/artrip/controller/dto/request/ImageResizeRequest.java @@ -1,22 +1,15 @@ package org.atdev.artrip.controller.dto.request; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -public class ImageResizeRequest { - - @Schema(description = "width", defaultValue = "100") - private Integer w; - - @Schema(description = "height", defaultValue = "100") - private Integer h; - - @Schema(defaultValue = "webp") - private String f = "webp"; - +public record ImageResizeRequest( + Integer w, + Integer h, + String f +) { + + public ImageResizeRequest { + w = (w == null || w <= 0) ? 100 : w; + h = (h == null || h <= 0) ? 100 : h; + + f = (f == null || f.isBlank()) ? "webp" : f; + } } diff --git a/src/main/java/org/atdev/artrip/controller/dto/response/CursorPaginationResponse.java b/src/main/java/org/atdev/artrip/controller/dto/response/CursorPaginationResponse.java new file mode 100644 index 0000000..3492305 --- /dev/null +++ b/src/main/java/org/atdev/artrip/controller/dto/response/CursorPaginationResponse.java @@ -0,0 +1,25 @@ +package org.atdev.artrip.controller.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +@AllArgsConstructor +public class CursorPaginationResponse { + + private List data; + private boolean hasNext; + private Long nextCursor; + + public static CursorPaginationResponse of(List data, boolean hasNext, Long nextCursor) { + return CursorPaginationResponse.builder() + .data(data) + .hasNext(hasNext) + .nextCursor(nextCursor) + .build(); + } +} diff --git a/src/main/java/org/atdev/artrip/controller/dto/response/ExhibitSearchResponse.java b/src/main/java/org/atdev/artrip/controller/dto/response/ExhibitSearchResponse.java deleted file mode 100644 index d64ff69..0000000 --- a/src/main/java/org/atdev/artrip/controller/dto/response/ExhibitSearchResponse.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.atdev.artrip.controller.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import org.atdev.artrip.constants.Status; -import org.atdev.artrip.elastic.document.KeywordInfo; - -import java.math.BigDecimal; -import java.util.List; - -@Data -@Builder -@AllArgsConstructor -public class ExhibitSearchResponse { - - private Long id; - private String title; - private String description; - - private String startDate; - private String endDate; - - private Status status; - private String posterUrl; - private String ticketUrl; - - private BigDecimal latitude; - private BigDecimal longitude; - - private List keywords; -} \ No newline at end of file diff --git a/src/main/java/org/atdev/artrip/controller/dto/response/ExhibitSearchResult.java b/src/main/java/org/atdev/artrip/controller/dto/response/ExhibitSearchResult.java new file mode 100644 index 0000000..4e502e4 --- /dev/null +++ b/src/main/java/org/atdev/artrip/controller/dto/response/ExhibitSearchResult.java @@ -0,0 +1,22 @@ +package org.atdev.artrip.controller.dto.response; + +import lombok.Builder; +import org.atdev.artrip.constants.Status; + +import java.time.LocalDate; + +@Builder +public record ExhibitSearchResult( + Long exhibitId, + String title, + String posterUrl, + Status status, + String hallName, + String region, + String country, + LocalDate startDate, + LocalDate endDate, + boolean isFavorite + + ) { +} diff --git a/src/main/java/org/atdev/artrip/controller/dto/response/FilterResponse.java b/src/main/java/org/atdev/artrip/controller/dto/response/FilterResponse.java index 7667973..69a9eff 100644 --- a/src/main/java/org/atdev/artrip/controller/dto/response/FilterResponse.java +++ b/src/main/java/org/atdev/artrip/controller/dto/response/FilterResponse.java @@ -15,4 +15,12 @@ public class FilterResponse { private boolean hasNext; private Long nextCursor; + public static FilterResponse of (List data, boolean hasNext, Long nextCursor) { + return FilterResponse.builder() + .exhibits(data) + .hasNext(hasNext) + .nextCursor(nextCursor) + .build(); + } + } \ No newline at end of file diff --git a/src/main/java/org/atdev/artrip/converter/HomeConverter.java b/src/main/java/org/atdev/artrip/converter/HomeConverter.java index 6390137..e963cad 100644 --- a/src/main/java/org/atdev/artrip/converter/HomeConverter.java +++ b/src/main/java/org/atdev/artrip/converter/HomeConverter.java @@ -35,7 +35,7 @@ public FilterResponse toFilterResponse(Slice slice, Set favorites ? slice.getContent().get(slice.getContent().size() - 1).getExhibitId() : null; - return new FilterResponse(postDtos, slice.hasNext(), nextCursor); + return FilterResponse.of(postDtos, slice.hasNext(), nextCursor); } diff --git a/src/main/java/org/atdev/artrip/converter/ReviewConverter.java b/src/main/java/org/atdev/artrip/converter/ReviewConverter.java index bd87dc4..427be00 100644 --- a/src/main/java/org/atdev/artrip/converter/ReviewConverter.java +++ b/src/main/java/org/atdev/artrip/converter/ReviewConverter.java @@ -89,10 +89,7 @@ public ReviewResponse toReviewResponse(Review review) { public void updateReviewFromDto(Review review, ReviewUpdateRequest request) { if (request.getContent() != null) { - review.setContent(request.getContent()); - } - if (request.getDate() != null) { - review.setVisitDate(request.getDate()); + review.updateContent(request.getContent(), LocalDateTime.now()); } } diff --git a/src/main/java/org/atdev/artrip/domain/exhibit/Exhibit.java b/src/main/java/org/atdev/artrip/domain/exhibit/Exhibit.java index 26f6170..b2cdb2f 100644 --- a/src/main/java/org/atdev/artrip/domain/exhibit/Exhibit.java +++ b/src/main/java/org/atdev/artrip/domain/exhibit/Exhibit.java @@ -14,7 +14,6 @@ @Entity @Table(name = "exhibit") -@EntityListeners(ExhibitEntityListener.class) @Getter @Setter @NoArgsConstructor diff --git a/src/main/java/org/atdev/artrip/domain/exhibit/ExhibitEntityListener.java b/src/main/java/org/atdev/artrip/domain/exhibit/ExhibitEntityListener.java deleted file mode 100644 index 3a38cde..0000000 --- a/src/main/java/org/atdev/artrip/domain/exhibit/ExhibitEntityListener.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.atdev.artrip.domain.exhibit; - -import jakarta.persistence.PostPersist; -import jakarta.persistence.PostRemove; -import jakarta.persistence.PostUpdate; -import lombok.extern.slf4j.Slf4j; -import org.atdev.artrip.elastic.service.ExhibitIndexService; -import org.atdev.artrip.global.apipayload.code.status.ExhibitErrorCode; -import org.atdev.artrip.global.apipayload.exception.GeneralException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -public class ExhibitEntityListener { - - private static ApplicationContext context; - - @Autowired - public void setApplicationContext(ApplicationContext applicationContext) { - context = applicationContext; - } - - @PostPersist - public void onPostPersist(Exhibit exhibit) { - log.info("ExhibitEntityListener - ID={}, Title={}", - exhibit.getExhibitId(), exhibit.getTitle()); - - try { - ExhibitIndexService indexService = context.getBean(ExhibitIndexService.class); - indexService.indexExhibit(exhibit); - } catch (Exception e) { - log.error(e.getMessage(), e); - throw new GeneralException(ExhibitErrorCode._EXHIBIT_NOT_FOUND); - } - } - - @PostUpdate - public void onPostUpdate(Exhibit exhibit) { - log.info("Exhibit DB Update - ID={}, Title={}", - exhibit.getExhibitId(), exhibit.getTitle()); - - try { - ExhibitIndexService indexService = context.getBean(ExhibitIndexService.class); - indexService.indexExhibit(exhibit); - } catch (Exception e) { - log.error(e.getMessage(), e); - throw new GeneralException(ExhibitErrorCode._EXHIBIT_NOT_FOUND); - } - } - - @PostRemove - public void onPostRemove(Exhibit exhibit) { - log.info("Exhibit DB Delete - ID={}, Title={}", - exhibit.getExhibitId(), exhibit.getTitle()); - - try { - ExhibitIndexService indexService = context.getBean(ExhibitIndexService.class); - indexService.deleteExhibit(exhibit.getExhibitId()); - } catch (Exception e) { - log.error(e.getMessage(), e); - throw new GeneralException(ExhibitErrorCode._EXHIBIT_NOT_FOUND); - } - } -} diff --git a/src/main/java/org/atdev/artrip/domain/keyword/Keyword.java b/src/main/java/org/atdev/artrip/domain/keyword/Keyword.java index 0f62cb5..59255bb 100644 --- a/src/main/java/org/atdev/artrip/domain/keyword/Keyword.java +++ b/src/main/java/org/atdev/artrip/domain/keyword/Keyword.java @@ -9,7 +9,6 @@ @Entity @Table(name = "keyword") @Getter -@Setter @NoArgsConstructor @AllArgsConstructor @Builder diff --git a/src/main/java/org/atdev/artrip/domain/keyword/UserKeyword.java b/src/main/java/org/atdev/artrip/domain/keyword/UserKeyword.java index cb954bc..f254b99 100644 --- a/src/main/java/org/atdev/artrip/domain/keyword/UserKeyword.java +++ b/src/main/java/org/atdev/artrip/domain/keyword/UserKeyword.java @@ -9,7 +9,6 @@ @Entity @Table(name = "user_keyword") @Getter -@Setter @NoArgsConstructor @AllArgsConstructor @Builder diff --git a/src/main/java/org/atdev/artrip/domain/notification/Notification.java b/src/main/java/org/atdev/artrip/domain/notification/Notification.java index a6a5e15..ad017d9 100644 --- a/src/main/java/org/atdev/artrip/domain/notification/Notification.java +++ b/src/main/java/org/atdev/artrip/domain/notification/Notification.java @@ -9,7 +9,6 @@ @Entity @Table(name = "notification") @Getter -@Setter @NoArgsConstructor @AllArgsConstructor @Builder diff --git a/src/main/java/org/atdev/artrip/domain/review/ReviewImage.java b/src/main/java/org/atdev/artrip/domain/review/ReviewImage.java index 975220f..085fa04 100644 --- a/src/main/java/org/atdev/artrip/domain/review/ReviewImage.java +++ b/src/main/java/org/atdev/artrip/domain/review/ReviewImage.java @@ -7,7 +7,6 @@ @Entity @Getter -@Setter @NoArgsConstructor @AllArgsConstructor @Builder diff --git a/src/main/java/org/atdev/artrip/domain/search/SearchHistory.java b/src/main/java/org/atdev/artrip/domain/search/SearchHistory.java index da3927c..565eab9 100644 --- a/src/main/java/org/atdev/artrip/domain/search/SearchHistory.java +++ b/src/main/java/org/atdev/artrip/domain/search/SearchHistory.java @@ -10,7 +10,6 @@ @Entity @Table(name = "search_history") @Getter -@Setter @NoArgsConstructor @AllArgsConstructor @Builder diff --git a/src/main/java/org/atdev/artrip/domain/stamp/Stamp.java b/src/main/java/org/atdev/artrip/domain/stamp/Stamp.java index 1c73a62..a38bcc7 100644 --- a/src/main/java/org/atdev/artrip/domain/stamp/Stamp.java +++ b/src/main/java/org/atdev/artrip/domain/stamp/Stamp.java @@ -9,7 +9,6 @@ @Entity @Table(name = "stamp") @Getter -@Setter @NoArgsConstructor @AllArgsConstructor @Builder diff --git a/src/main/java/org/atdev/artrip/elastic/document/ExhibitDocument.java b/src/main/java/org/atdev/artrip/elastic/document/ExhibitDocument.java deleted file mode 100644 index 8c37bfe..0000000 --- a/src/main/java/org/atdev/artrip/elastic/document/ExhibitDocument.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.atdev.artrip.elastic.document; - -import com.fasterxml.jackson.annotation.JsonFormat; -import jakarta.persistence.Id; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.atdev.artrip.constants.Status; -import org.springframework.data.elasticsearch.annotations.DateFormat; -import org.springframework.data.elasticsearch.annotations.Document; -import org.springframework.data.elasticsearch.annotations.Field; -import org.springframework.data.elasticsearch.annotations.FieldType; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.List; - - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Document(indexName = "exhibits") -public class ExhibitDocument { - - @Id - private Long id; - - private String title; - private String description; - - @Field(type = FieldType.Date, format = DateFormat.date) - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy.MM.dd") - private LocalDate startDate; - - @Field(type = FieldType.Date, format = DateFormat.date) - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy.MM.dd") - private LocalDate endDate; - - private Status status; - private String posterUrl; - private String ticketUrl; - - private BigDecimal latitude; - private BigDecimal longitude; - - private List keywords; - -} diff --git a/src/main/java/org/atdev/artrip/elastic/document/KeywordInfo.java b/src/main/java/org/atdev/artrip/elastic/document/KeywordInfo.java deleted file mode 100644 index abd9cb5..0000000 --- a/src/main/java/org/atdev/artrip/elastic/document/KeywordInfo.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.atdev.artrip.elastic.document; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.atdev.artrip.constants.KeywordType; -import org.springframework.data.elasticsearch.annotations.Field; -import org.springframework.data.elasticsearch.annotations.FieldType; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class KeywordInfo { - - @Field(type = FieldType.Keyword) - private String name; - - @Field(type = FieldType.Keyword) - private KeywordType type; -} diff --git a/src/main/java/org/atdev/artrip/elastic/service/ExhibitIndexService.java b/src/main/java/org/atdev/artrip/elastic/service/ExhibitIndexService.java deleted file mode 100644 index 76b5358..0000000 --- a/src/main/java/org/atdev/artrip/elastic/service/ExhibitIndexService.java +++ /dev/null @@ -1,214 +0,0 @@ -package org.atdev.artrip.elastic.service; - -import co.elastic.clients.elasticsearch.ElasticsearchClient; -import co.elastic.clients.elasticsearch._types.analysis.TokenChar; -import co.elastic.clients.elasticsearch.core.BulkRequest; -import co.elastic.clients.elasticsearch.core.BulkResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.atdev.artrip.repository.ExhibitRepository; -import org.atdev.artrip.elastic.document.KeywordInfo; -import org.atdev.artrip.elastic.document.ExhibitDocument; -import org.atdev.artrip.domain.exhibit.Exhibit; -import org.atdev.artrip.global.apipayload.code.status.CommonErrorCode; -import org.atdev.artrip.global.apipayload.code.status.ElasticErrorCode; -import org.atdev.artrip.global.apipayload.exception.GeneralException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.io.IOException; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -@Slf4j -public class ExhibitIndexService { - - private final ElasticsearchClient esClient; - private final ExhibitRepository exhibitRepository; - private final String EXHIBIT_INDEX = "exhibits"; - - private ExhibitDocument convertToDocument(Exhibit exhibit) { - - List keywordInfos = exhibit.getKeywords().stream() - .map(keyword -> KeywordInfo.builder() - .name(keyword.getName()) - .type(keyword.getType()) - .build()) - .collect(Collectors.toList()); - - ExhibitDocument.ExhibitDocumentBuilder builder = ExhibitDocument.builder() - .id(exhibit.getExhibitId()) - .title(exhibit.getTitle()) - .description(exhibit.getDescription()) - .startDate(exhibit.getStartDate()) - .endDate(exhibit.getEndDate()) - .status(exhibit.getStatus()) - .posterUrl(exhibit.getPosterUrl()) - .ticketUrl(exhibit.getTicketUrl()) - .latitude(Optional.ofNullable(exhibit.getExhibitHall()) - .map(hall -> hall.getLatitude()) - .orElse(null)) - .longitude(Optional.ofNullable(exhibit.getExhibitHall()) - .map(hall -> hall.getLongitude()) - .orElse(null)) - .keywords(keywordInfos); - - return builder.build(); - } - - public void createAndApplyIndex() { - try { - if (esClient.indices().exists(r -> r.index(EXHIBIT_INDEX)).value()) { - esClient.indices().delete(r -> r.index(EXHIBIT_INDEX)); - log.info("Existing index deleted : {}", EXHIBIT_INDEX); - } - - esClient.indices().create(c -> c - .index(EXHIBIT_INDEX) - .settings(s -> s - .maxNgramDiff(9) - .analysis(a -> a - .tokenizer("edge_ngram_tokenizer", t -> t - .definition(d -> d - .edgeNgram(en -> en - .minGram(1) - .maxGram(10) - .tokenChars(TokenChar.Letter, TokenChar.Digit) - ) - ) - ) - .filter("ngram_filter", f -> f - .definition(d -> d - .ngram(ng -> ng - .minGram(1) - .maxGram(10) - ) - ) - ) - .analyzer("edge_ngram_analyzer", an -> an - .custom(ca -> ca - .tokenizer("edge_ngram_tokenizer") - .filter("lowercase") - ) - ) - .analyzer("ngram_nori_analyzer", an -> an - .custom(ca -> ca - .tokenizer("standard") - .filter("lowercase", "ngram_filter") - ) - ) - ) - ) - .mappings(m -> m - .properties("title", p -> p - .text(t -> t - .analyzer("edge_ngram_analyzer") - .searchAnalyzer("standard") - .fields("nori", f -> f.text(t2 -> t2.analyzer("ngram_nori_analyzer"))) - .fields("keyword", f -> f.keyword(k -> k)))) - .properties("description", p -> p.text(t -> t - .analyzer("edge_ngram_analyzer") - .searchAnalyzer("standard") - .fields("nori", f -> f.text(t2 -> t2.analyzer("ngram_nori_analyzer"))))) - .properties("startDate", p -> p.date(d -> d.format("yyyy.MM.dd"))) - .properties("endDate", p -> p.date(d -> d.format("yyyy.MM.dd"))) - .properties("status", p -> p.keyword(k -> k)) - .properties("posterUrl", p -> p.keyword(k -> k)) - .properties("ticketUrl", p -> p.keyword(k -> k)) - .properties("keywords", p -> p - .nested(n -> n - .properties("name", np -> np - .text(t -> t - .analyzer("edge_ngram_analyzer") - .searchAnalyzer("standard") - .fields("nori", f -> f.text(t2 -> t2.analyzer("ngram_nori_analyzer"))) - .fields("keyword", f -> f.keyword(k -> k)) - ) - ) - .properties("type", np -> np.keyword(k -> k)) - ) - ) - ) - ); - - log.info("index '{}' created with Nori And Edge Ngram analyzer", EXHIBIT_INDEX); - } catch (IOException e) { - log.error("Error createing or applying index settings", e); - if (e.getMessage().contains("nori_tokenizer")) { - throw new GeneralException(ElasticErrorCode._ES_ANALYZER_CONFIG_FAILED); - } - throw new GeneralException(ElasticErrorCode._ES_INDEX_CREATE_FAILED); - } - } - - @Transactional(readOnly = true) - public int indexAllExhibits() { - try { - List exhibits = exhibitRepository.findAllWithKeywords(); - int count = exhibits.size(); - - if (exhibits.isEmpty()) { - log.warn("No exhibits found"); - return 0; - } - - List documents = exhibits.stream() - .map(this::convertToDocument) - .collect(Collectors.toList()); - - BulkRequest.Builder bulkBuilder = new BulkRequest.Builder(); - for (ExhibitDocument doc : documents) { - bulkBuilder.operations(op -> op - .index(idx -> idx - .index(EXHIBIT_INDEX) - .id(String.valueOf(doc.getId())) - .document(doc))); - } - BulkResponse response = esClient.bulk(bulkBuilder.build()); - - if (response.errors()) { - log.error("Bulk indexing failed with errors : {} items failed" , response.items().stream().filter(i -> i.error() != null).count()); - response.items().stream() - .filter(i -> i.error() != null) - .limit(5) - .forEach(i -> log.error("item error : {}", i.error().reason())); - - } - if (response.errors()) { - throw new GeneralException(ElasticErrorCode._ES_BULK_PARTIAL_FAILED); - } - return count; - } catch (Exception e) { - log.error("Elasticsearch indexing error : {}", e.getClass().getName(), e ); - throw new GeneralException(ElasticErrorCode._ES_BULK_INDEX_FAILED); - } - } - - @Transactional - public void indexExhibit(Exhibit exhibit) { - try { - ExhibitDocument doc = convertToDocument(exhibit); - esClient.index(i -> i - .index(EXHIBIT_INDEX) - .id(String.valueOf(doc.getId())) - .document(doc)); - } catch (Exception e) { - log.error("Indexing failed : {}", e.getMessage(), e); - throw new GeneralException(CommonErrorCode._INTERNAL_SERVER_ERROR); - } - } - - public void deleteExhibit(Long exhibitId) { - try { - esClient.delete(d -> d - .index(EXHIBIT_INDEX) - .id(String.valueOf(exhibitId))); - } catch (Exception e) { - log.error("Delete Failed: {}", e.getMessage(), e); - throw new GeneralException(CommonErrorCode._INTERNAL_SERVER_ERROR); - } - } -} diff --git a/src/main/java/org/atdev/artrip/elastic/service/ScheduledIndexService.java b/src/main/java/org/atdev/artrip/elastic/service/ScheduledIndexService.java deleted file mode 100644 index b714577..0000000 --- a/src/main/java/org/atdev/artrip/elastic/service/ScheduledIndexService.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.atdev.artrip.elastic.service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.atdev.artrip.repository.ExhibitRepository; -import org.atdev.artrip.global.apipayload.code.status.ExhibitErrorCode; -import org.atdev.artrip.global.apipayload.exception.GeneralException; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - - -@Service -@RequiredArgsConstructor -@Slf4j -public class ScheduledIndexService { - - private final ExhibitIndexService indexService; - private final ExhibitRepository exhibitRepository; - - @Scheduled(cron = "0 0 2 * * * ") - public void reindexAllExhibits() { - log.info("Reindexing all exhibits"); - - try { - int count = indexService.indexAllExhibits(); - - } catch (Exception e) { - log.error("Error during reindexing: {}", e.getMessage(), e); - throw new GeneralException(ExhibitErrorCode._EXHIBIT_NOT_FOUND); - } - } - - @Scheduled(fixedDelay = 600000) // 10 minutes - public void reindexRecentlyExhibits() { - - log.info("Reindexing recently exhibits"); - - try { - // Todo: 최근 1시간 이내 수정된 데이터만 조회해서 재색인 - -// List recentExhibits = exhibitRepository.findByUpdatedAtAfter( -// LocalDateTime.now().minusHours(1)); -// recentExhibits.forEach(indexService::indexExhibit); - } catch (Exception e) { - log.error("Error during reindexing: {}", e.getMessage(), e); - throw new GeneralException(ExhibitErrorCode._EXHIBIT_NOT_FOUND); - } - } -} diff --git a/src/main/java/org/atdev/artrip/external/culturalapi/cultureinfo/service/CultureInfoSyncService.java b/src/main/java/org/atdev/artrip/external/culturalapi/cultureinfo/service/CultureInfoSyncService.java index ee64aa6..2a3c2fc 100644 --- a/src/main/java/org/atdev/artrip/external/culturalapi/cultureinfo/service/CultureInfoSyncService.java +++ b/src/main/java/org/atdev/artrip/external/culturalapi/cultureinfo/service/CultureInfoSyncService.java @@ -8,7 +8,6 @@ import org.atdev.artrip.domain.exhibitHall.ExhibitHall; import org.atdev.artrip.repository.ExhibitHallRepository; import org.atdev.artrip.domain.keyword.Keyword; -import org.atdev.artrip.elastic.service.ExhibitIndexService; import org.atdev.artrip.external.culturalapi.cultureinfo.client.CultureInfoApiClient; import org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response.CultureInfoDetailItem; import org.atdev.artrip.external.culturalapi.cultureinfo.web.dto.response.CultureInfoDetailResponse; @@ -38,7 +37,6 @@ public class CultureInfoSyncService { private final ExhibitRepository exhibitRepository; private final ExhibitHallRepository exhibitHallRepository; private final KeywordMatchingService keywordMatchingService; - private final ExhibitIndexService exhibitIndexService; private final S3Service s3Service; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); @@ -186,14 +184,12 @@ private void processItem(CultureInfoItem item, Map hallCach updateExhibit(existing.get(), exhibit); matchAndSaveKeywords(existing.get(), item, detailItem); - exhibitIndexService.indexExhibit(existing.get()); result.incrementUpdated(); } else { Exhibit saved = exhibitRepository.save(exhibit); matchAndSaveKeywords(saved, item, detailItem); exhibitRepository.save(saved); - exhibitIndexService.indexExhibit(saved); result.incrementInserted(); } } diff --git a/src/main/java/org/atdev/artrip/external/culturalapi/cultureinfo/web/controller/CultureInfoSyncController.java b/src/main/java/org/atdev/artrip/external/culturalapi/cultureinfo/web/controller/CultureInfoSyncController.java index 7327855..1c2e2a0 100644 --- a/src/main/java/org/atdev/artrip/external/culturalapi/cultureinfo/web/controller/CultureInfoSyncController.java +++ b/src/main/java/org/atdev/artrip/external/culturalapi/cultureinfo/web/controller/CultureInfoSyncController.java @@ -2,13 +2,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.atdev.artrip.elastic.service.ExhibitIndexService; import org.atdev.artrip.external.culturalapi.cultureinfo.service.CultureInfoSyncService; import org.atdev.artrip.global.apipayload.CommonResponse; import org.springframework.web.bind.annotation.*; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; import java.util.Map; @Slf4j @@ -18,7 +15,6 @@ public class CultureInfoSyncController { private final CultureInfoSyncService CultureInfoSyncService; - private final ExhibitIndexService exhibitIndexService; @GetMapping("/exhibits") public CommonResponse> syncByPeriod( @@ -59,10 +55,4 @@ public CommonResponse> syncAll() { "failed", result.getFailed() )); } - - @PostMapping("/reindex") - public CommonResponse reindexES() { - int count = exhibitIndexService.indexAllExhibits(); - return CommonResponse.onSuccess(count); - } } diff --git a/src/main/java/org/atdev/artrip/global/apipayload/code/status/ElasticErrorCode.java b/src/main/java/org/atdev/artrip/global/apipayload/code/status/ElasticErrorCode.java deleted file mode 100644 index da69548..0000000 --- a/src/main/java/org/atdev/artrip/global/apipayload/code/status/ElasticErrorCode.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.atdev.artrip.global.apipayload.code.status; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.atdev.artrip.global.apipayload.code.BaseErrorCode; -import org.springframework.http.HttpStatus; - -@Getter -@AllArgsConstructor -public enum ElasticErrorCode implements BaseErrorCode { - - _ES_CONNECTION_FAILED(HttpStatus.SERVICE_UNAVAILABLE, "ES503-CONNECTION_FAILED", "Elasticsearch 서버에 연결할 수 없습니다."), - _ES_TIMEOUT(HttpStatus.SERVICE_UNAVAILABLE, "ES503-TIMEOUT", "Elasticsearch 요청 시간이 초과되었습니다."), - _ES_CLUSTER_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "ES503-CLUSTER_UNAVAILABLE", "Elasticsearch 클러스터를 사용할 수 없습니다."), - - _ES_INDEX_CREATE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ES500-INDEX_CREATE_FAILED", "Elasticsearch 인덱스 생성에 실패했습니다."), - _ES_INDEX_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ES500-INDEX_DELETE_FAILED", "Elasticsearch 인덱스 삭제에 실패했습니다."), - _ES_INDEX_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "ES500-INDEX_NOT_FOUND", "Elasticsearch 인덱스를 찾을 수 없습니다."), - _ES_INDEX_EXISTS_CHECK_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ES500-INDEX_EXISTS_CHECK_FAILED", "인덱스 존재 여부 확인에 실패했습니다."), - - _ES_DOCUMENT_INDEX_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ES500-DOCUMENT_INDEX_FAILED", "문서 인덱싱에 실패했습니다."), - _ES_BULK_INDEX_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ES500-BULK_INDEX_FAILED", "벌크 인덱싱에 실패했습니다."), - _ES_BULK_PARTIAL_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ES500-BULK_PARTIAL_FAILED", "일부 문서 인덱싱에 실패했습니다."), - _ES_DOCUMENT_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ES500-DOCUMENT_DELETE_FAILED", "문서 삭제에 실패했습니다."), - _ES_DOCUMENT_UPDATE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ES500-DOCUMENT_UPDATE_FAILED", "문서 업데이트에 실패했습니다."), - - _ES_SEARCH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ES500-SEARCH_FAILED", "Elasticsearch 검색에 실패했습니다."), - _ES_QUERY_EXECUTION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ES500-QUERY_EXECUTION_FAILED", "검색 쿼리 실행에 실패했습니다."), - - _ES_ANALYZER_CONFIG_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ES500-ANALYZER_CONFIG_FAILED", "분석기 설정에 실패했습니다. (Nori 플러그인 확인 필요)"), - _ES_MAPPING_CONFIG_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ES500-MAPPING_CONFIG_FAILED", "매핑 설정에 실패했습니다."), - _ES_SETTINGS_CONFIG_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ES500-SETTINGS_CONFIG_FAILED", "인덱스 설정에 실패했습니다."), - - _ES_INVALID_QUERY(HttpStatus.BAD_REQUEST, "ES400-INVALID_QUERY", "잘못된 검색 쿼리입니다."), - _ES_INVALID_DOCUMENT(HttpStatus.BAD_REQUEST, "ES400-INVALID_DOCUMENT", "잘못된 문서 형식입니다."), - _ES_INVALID_INDEX_NAME(HttpStatus.BAD_REQUEST, "ES400-INVALID_INDEX_NAME", "잘못된 인덱스 이름입니다."), - _ES_INVALID_MAPPING(HttpStatus.BAD_REQUEST, "ES400-INVALID_MAPPING", "잘못된 매핑 정의입니다."), - - _ES_DOCUMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "ES404-DOCUMENT_NOT_FOUND", "요청한 문서를 찾을 수 없습니다."), - _ES_SEARCH_NO_RESULT(HttpStatus.NOT_FOUND, "ES404-SEARCH_NO_RESULT", "검색 결과가 없습니다."); - - private final HttpStatus httpStatus; - private final String code; - private final String message; -} diff --git a/src/main/java/org/atdev/artrip/global/resolver/LoginUser.java b/src/main/java/org/atdev/artrip/global/resolver/LoginUser.java new file mode 100644 index 0000000..290c29f --- /dev/null +++ b/src/main/java/org/atdev/artrip/global/resolver/LoginUser.java @@ -0,0 +1,15 @@ +package org.atdev.artrip.global.resolver; + +import io.swagger.v3.oas.annotations.Parameter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +@Parameter(hidden = true) +public @interface LoginUser { + +} diff --git a/src/main/java/org/atdev/artrip/global/resolver/LoginUserIdArgumentResolver.java b/src/main/java/org/atdev/artrip/global/resolver/LoginUserIdArgumentResolver.java new file mode 100644 index 0000000..ce9e813 --- /dev/null +++ b/src/main/java/org/atdev/artrip/global/resolver/LoginUserIdArgumentResolver.java @@ -0,0 +1,45 @@ +package org.atdev.artrip.global.resolver; + +import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +public class LoginUserIdArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(LoginUser.class) && parameter.getParameterType().equals(Long.class); + } + + @Override + public Object resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory webDataBinderFactory + ) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated() || authentication.getPrincipal().equals("anonymousUser")) { + return null; + } + + Object principal = authentication.getPrincipal(); + + if (principal instanceof UserDetails userDetails) { + try { + return Long.parseLong(userDetails.getUsername()); + } catch (NumberFormatException e) { + return null; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/atdev/artrip/repository/ExhibitDocumentRepository.java b/src/main/java/org/atdev/artrip/repository/ExhibitDocumentRepository.java deleted file mode 100644 index bf6df3b..0000000 --- a/src/main/java/org/atdev/artrip/repository/ExhibitDocumentRepository.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.atdev.artrip.repository; - -import org.atdev.artrip.constants.KeywordType; -import org.atdev.artrip.elastic.document.ExhibitDocument; -import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; - -import java.util.List; - -public interface ExhibitDocumentRepository extends ElasticsearchRepository { - - List findByTitleContainingOrKeywordsNameContaining(String title, String keywordName); - - List findByKeywordsType(KeywordType type); - - List findByKeywordsNameContaining(String keywordName); - -} diff --git a/src/main/java/org/atdev/artrip/repository/ExhibitRepository.java b/src/main/java/org/atdev/artrip/repository/ExhibitRepository.java index 7e44f92..9ab0274 100644 --- a/src/main/java/org/atdev/artrip/repository/ExhibitRepository.java +++ b/src/main/java/org/atdev/artrip/repository/ExhibitRepository.java @@ -2,6 +2,7 @@ import org.atdev.artrip.domain.exhibit.Exhibit; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -45,14 +46,9 @@ WHERE status IN ('ONGOING', 'ENDING_SOON') """, nativeQuery = true) int updateFinishedStatus(); - - @Query("SELECT DISTINCT e FROM Exhibit e LEFT JOIN FETCH e.keywords WHERE e.exhibitId = :id") Optional findByIdWithKeywords(@Param("id") Long id); - @Query("SELECT DISTINCT e FROM Exhibit e LEFT JOIN FETCH e.keywords") - List findAllWithKeywords(); - Page findByDescriptionContaining(String description, Pageable pageable); long countByExhibitHall_ExhibitHallId(Long exhibitHallId); @@ -62,4 +58,5 @@ WHERE status IN ('ONGOING', 'ENDING_SOON') // 패치조인 전시홀 전시관 @Query("select e from Exhibit e join fetch e.exhibitHall where e.exhibitId in :ids") List findAllByIdWithHall(@Param("ids") List ids); + } diff --git a/src/main/java/org/atdev/artrip/repository/ExhibitRepositoryCustom.java b/src/main/java/org/atdev/artrip/repository/ExhibitRepositoryCustom.java index 4ed78d1..88c838f 100644 --- a/src/main/java/org/atdev/artrip/repository/ExhibitRepositoryCustom.java +++ b/src/main/java/org/atdev/artrip/repository/ExhibitRepositoryCustom.java @@ -16,4 +16,5 @@ public interface ExhibitRepositoryCustom { List findRandomExhibits(RandomExhibitRequest condition); + Slice searchByKeyword(String title, Long cursor, Long size); } diff --git a/src/main/java/org/atdev/artrip/repository/ExhibitRepositoryImpl.java b/src/main/java/org/atdev/artrip/repository/ExhibitRepositoryImpl.java index 6c95c7c..9b5edb8 100644 --- a/src/main/java/org/atdev/artrip/repository/ExhibitRepositoryImpl.java +++ b/src/main/java/org/atdev/artrip/repository/ExhibitRepositoryImpl.java @@ -23,6 +23,8 @@ import java.util.List; import java.util.Set; +import static org.atdev.artrip.domain.exhibit.QExhibit.exhibit; + @Repository @RequiredArgsConstructor public class ExhibitRepositoryImpl implements ExhibitRepositoryCustom{ @@ -32,7 +34,7 @@ public class ExhibitRepositoryImpl implements ExhibitRepositoryCustom{ @Override public Slice findExhibitByFilters(ExhibitFilterRequest dto, Long size, Long cursorId) { - QExhibit e = QExhibit.exhibit; + QExhibit e = exhibit; QExhibitHall h = QExhibitHall.exhibitHall; QKeyword k = QKeyword.keyword; @@ -47,7 +49,7 @@ public Slice findExhibitByFilters(ExhibitFilterRequest dto, Long size, List content = queryFactory .selectDistinct(e) .from(e) - .join(e.exhibitHall, h) + .join(e.exhibitHall, h).fetchJoin() .leftJoin(e.keywords, k) .where( e.status.ne(Status.FINISHED), @@ -69,17 +71,17 @@ public Slice findExhibitByFilters(ExhibitFilterRequest dto, Long size, content.remove(size.intValue()); return new SliceImpl<>(content, PageRequest.of(0, size.intValue()), hasNext); - }// 페이지 개념은 사용 x + } @Override public List findRandomExhibits(RandomExhibitRequest c) { - QExhibit e = QExhibit.exhibit; + QExhibit e = exhibit; QExhibitHall h = QExhibitHall.exhibitHall; QKeyword k = QKeyword.keyword; return queryFactory - .selectDistinct(Projections.constructor(// select 순서와 DTO 생성자 파라미터 순서를 1:1 매핑함! + .selectDistinct(Projections.constructor( HomeListResponse.class, e.exhibitId, e.title, @@ -121,11 +123,11 @@ private BooleanExpression cursorCondition(Exhibit cursor, SortType sortType, QEx .or(e.favoriteCount.eq(cursor.getFavoriteCount()) .and(e.exhibitId.lt(cursor.getExhibitId()))); - case LATEST -> e.startDate.lt(cursor.getStartDate())//< + case LATEST -> e.startDate.lt(cursor.getStartDate()) .or(e.startDate.eq(cursor.getStartDate()) .and(e.exhibitId.lt(cursor.getExhibitId()))); - default -> e.endDate.gt(cursor.getEndDate())//> + default -> e.endDate.gt(cursor.getEndDate()) .or(e.endDate.eq(cursor.getEndDate()) .and(e.exhibitId.lt(cursor.getExhibitId()))); }; @@ -179,11 +181,11 @@ private BooleanExpression isDomesticEq(Boolean isDomestic) { } private BooleanExpression countryEq(String country) { - return country == null ? null : QExhibitHall.exhibitHall.country.eq(country); + return country == null || country.isBlank() ? null : QExhibitHall.exhibitHall.country.eq(country); } private BooleanExpression regionEq(String region) { - return region == null ? null : QExhibitHall.exhibitHall.region.eq(region); + return region == null || region.isBlank() ? null : QExhibitHall.exhibitHall.region.eq(region); } private BooleanExpression genreIn(Set genres) { @@ -201,8 +203,44 @@ private BooleanExpression styleIn(Set styles) { private BooleanExpression findDate(LocalDate date){ if (date == null) return null; - return QExhibit.exhibit.startDate.loe(date)//<= - .and(QExhibit.exhibit.endDate.goe(date));//>= + return exhibit.startDate.loe(date) + .and(exhibit.endDate.goe(date)); + } + + private BooleanExpression keywordSearch(String keyword, QExhibit e, QExhibitHall h, QKeyword k) { + if (keyword == null || keyword.isBlank()) { + return null; + } + + return e.title.containsIgnoreCase(keyword) + .or(e.description.containsIgnoreCase(keyword)) + .or(h.name.containsIgnoreCase(keyword)) + .or(k.name.containsIgnoreCase(keyword)); + } + + @Override + public Slice searchByKeyword(String keywords, Long cursor, Long size) { + QExhibitHall exhibitHall = QExhibitHall.exhibitHall; + QKeyword keyword = QKeyword.keyword; + List content = queryFactory + .selectDistinct(exhibit) + .from(exhibit) + .leftJoin(exhibit.exhibitHall, exhibitHall).fetchJoin() + .join(exhibit.keywords, keyword) + .where( + keywords != null ? keyword.name.contains(keywords) : null, + cursor != null ? exhibit.exhibitId.lt(cursor) : null + ) + .limit(size + 1) + .orderBy(exhibit.exhibitId.desc()) + .fetch(); + + boolean hasNext = false; + if (content.size() > size) { + content.remove(size.intValue()); + hasNext = true; + } + return new SliceImpl<>(content, PageRequest.ofSize(size.intValue()),hasNext); } } diff --git a/src/main/java/org/atdev/artrip/service/AdminExhibitHallService.java b/src/main/java/org/atdev/artrip/service/AdminExhibitHallService.java index 233c657..ee83ed4 100644 --- a/src/main/java/org/atdev/artrip/service/AdminExhibitHallService.java +++ b/src/main/java/org/atdev/artrip/service/AdminExhibitHallService.java @@ -23,7 +23,6 @@ @Service @RequiredArgsConstructor -@Slf4j public class AdminExhibitHallService { private final ExhibitHallRepository exhibitHallRepository; @@ -32,7 +31,6 @@ public class AdminExhibitHallService { @Transactional(readOnly = true) public PagingResponseDTO getExhibitHallList(Criteria cri) { - log.info("Admin getting exhibit hall list: {}", cri); Pageable pageable = cri.toPageable(); Page hallPage; @@ -54,7 +52,6 @@ public PagingResponseDTO getExhibitHallList(Criteria cr @Transactional(readOnly = true) public ExhibitHallResponse getExhibitHall(Long exhibitHallId) { - log.info("Admin getting exhibit hall : {}", exhibitHallId); ExhibitHall hall = exhibitHallRepository.findById(exhibitHallId).orElseThrow(() -> new GeneralException(ExhibitErrorCode._EXHIBIT_HALL_NOT_FOUND)); @@ -65,7 +62,6 @@ public ExhibitHallResponse getExhibitHall(Long exhibitHallId) { @Transactional public Long createExhibitHall(CreateExhibitHallRequest request) { - log.info("Admin creating exhibit hall: {}", request); ExhibitHall exhibitHall = ExhibitHall.builder() .name(request.getName()) @@ -85,38 +81,17 @@ public Long createExhibitHall(CreateExhibitHallRequest request) { ExhibitHall savedHall = exhibitHallRepository.save(exhibitHall); - log.info("Exhibit hall saved: {}", savedHall); - return savedHall.getExhibitHallId(); } @Transactional public Long updateExhibitHall(Long exhibitHallId, UpdateExhibitHallRequest request) { - log.info("Admin updating exhibit hall: {}, {}", exhibitHallId, request); - - ExhibitHall hall = exhibitHallRepository.findById(exhibitHallId).orElseThrow(() -> new GeneralException(ExhibitErrorCode._EXHIBIT_NOT_FOUND)); - - if (request.getName() != null) {hall.setName(request.getName());} - if (request.getAddress() != null) {hall.setAddress(request.getAddress());} - if (request.getCountry() != null) {hall.setCountry(request.getCountry());} - if (request.getRegion() != null) {hall.setRegion(request.getRegion());} - if (request.getPhone() != null) {hall.setPhone(request.getPhone());} - if (request.getHomepageUrl() != null) {hall.setHomepageUrl(request.getHomepageUrl());} - if (request.getOpeningHours() != null) {hall.setOpeningHours(request.getOpeningHours());} - if (request.getIsDomestic() != null) {hall.setIsDomestic(request.getIsDomestic());} - if (request.getClosedDays() != null) {hall.setClosedDays(request.getClosedDays());} - hall.setUpdatedAt(LocalDateTime.now()); - ExhibitHall savedHall = exhibitHallRepository.save(hall); - - log.info("ExhibitHall updated: id={}, name={}", savedHall.getExhibitHallId(), savedHall.getName()); - - return savedHall.getExhibitHallId(); + return null; } @Transactional public void deleteExhibitHall(Long exhibitHallId) { - log.info("Admin deleting exhibit hall: {}", exhibitHallId); if (!exhibitHallRepository.existsById(exhibitHallId)) { throw new GeneralException(ExhibitErrorCode._EXHIBIT_HALL_NOT_FOUND); @@ -129,7 +104,6 @@ public void deleteExhibitHall(Long exhibitHallId) { exhibitHallRepository.deleteById(exhibitHallId); - log.info("Exhibit hall deleted: {}", exhibitHallId); } diff --git a/src/main/java/org/atdev/artrip/service/AdminExhibitService.java b/src/main/java/org/atdev/artrip/service/AdminExhibitService.java index a4a7a4e..9198d0c 100644 --- a/src/main/java/org/atdev/artrip/service/AdminExhibitService.java +++ b/src/main/java/org/atdev/artrip/service/AdminExhibitService.java @@ -1,7 +1,8 @@ package org.atdev.artrip.service; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import org.atdev.artrip.global.apipayload.code.status.CommonErrorCode; +import org.atdev.artrip.global.apipayload.code.status.ExhibitErrorCode; import org.atdev.artrip.global.page.Criteria; import org.atdev.artrip.global.page.PagingResponseDTO; import org.atdev.artrip.converter.AdminExhibitConverter; @@ -15,9 +16,6 @@ import org.atdev.artrip.repository.ExhibitHallRepository; import org.atdev.artrip.domain.keyword.Keyword; import org.atdev.artrip.repository.KeywordRepository; -import org.atdev.artrip.elastic.service.ExhibitIndexService; -import org.atdev.artrip.global.apipayload.code.status.CommonErrorCode; -import org.atdev.artrip.global.apipayload.code.status.ExhibitErrorCode; import org.atdev.artrip.global.apipayload.exception.GeneralException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -29,18 +27,15 @@ @Service @RequiredArgsConstructor -@Slf4j public class AdminExhibitService { private final ExhibitRepository exhibitRepository; private final ExhibitHallRepository exhibitHallRepository; private final KeywordRepository keywordRepository; - private final ExhibitIndexService exhibitIndexService; private final AdminExhibitConverter adminExhibitConverter; @Transactional(readOnly = true) public PagingResponseDTO getExhibitList(Criteria cri) { - log.info("Admin Getting Exhibit List : {}", cri); Pageable pageable = cri.toPageable(); @@ -59,7 +54,6 @@ public PagingResponseDTO getExhibitList(Criteria cri) @Transactional(readOnly = true) public AdminExhibitResponse getExhibit (Long exhibitId) { - log.info("Admin Getting Exhibit : {}", exhibitId); Exhibit exhibit = exhibitRepository.findByIdWithKeywords(exhibitId) .orElseThrow(() -> new GeneralException(ExhibitErrorCode._EXHIBIT_NOT_FOUND)); @@ -69,7 +63,6 @@ public AdminExhibitResponse getExhibit (Long exhibitId) { @Transactional public Long createExhibit(CreateExhibitRequest request) { - log.info("Admin Creating exhibit : title={}", request); ExhibitHall exhibitHall = exhibitHallRepository.findById(request.getExhibitHallId()) .orElseThrow(() -> new GeneralException(ExhibitErrorCode._EXHIBIT_HALL_NOT_FOUND)); @@ -96,24 +89,14 @@ public Long createExhibit(CreateExhibitRequest request) { .build(); exhibit.getKeywords().addAll(keywords); - Exhibit savedExhibit = exhibitRepository.save(exhibit); - - log.info("Exhibit created : id={}, keywords.size={} ", - savedExhibit.getExhibitId(), - savedExhibit.getKeywords().size()); - try { - exhibitIndexService.indexExhibit(savedExhibit); - log.info("Exhibit indexing successfully : id={} ",savedExhibit.getExhibitId()); } catch (Exception e) { - log.error("Exhibit indexing failed", e); } - return savedExhibit.getExhibitId(); + return null; } @Transactional public Long updateExhibit(Long exhibitId, UpdateExhibitRequest request) { - log.info("Admin Updating Exhibit : {}", exhibitId); Exhibit exhibit = exhibitRepository.findByIdWithKeywords(exhibitId) .orElseThrow(() -> new GeneralException(ExhibitErrorCode._EXHIBIT_NOT_FOUND)); @@ -149,12 +132,10 @@ public Long updateExhibit(Long exhibitId, UpdateExhibitRequest request) { Exhibit savedExhibit = exhibitRepository.save(exhibit); + // TODO: 업데이트 할 내용 추가 try { - exhibitIndexService.indexExhibit(savedExhibit); - }catch (Exception e) { - log.error("Admin Exhibit Update Error", e.getMessage()); throw new GeneralException(CommonErrorCode._INTERNAL_SERVER_ERROR); } @@ -163,7 +144,6 @@ public Long updateExhibit(Long exhibitId, UpdateExhibitRequest request) { @Transactional public void deleteExhibit(Long exhibitId) { - log.info("Admin Deleting Exhibit : {}", exhibitId); if (!exhibitRepository.existsById(exhibitId)) { throw new GeneralException(ExhibitErrorCode._EXHIBIT_NOT_FOUND); @@ -171,10 +151,9 @@ public void deleteExhibit(Long exhibitId) { exhibitRepository.deleteById(exhibitId); + // TODO: delete 할 code 추가 try { - exhibitIndexService.deleteExhibit(exhibitId); } catch (Exception e) { - log.error("Admin Exhibit Deletion Error", e.getMessage()); throw new GeneralException(CommonErrorCode._INTERNAL_SERVER_ERROR); } } diff --git a/src/main/java/org/atdev/artrip/service/ExhibitSearchService.java b/src/main/java/org/atdev/artrip/service/ExhibitSearchService.java deleted file mode 100644 index 3c6b996..0000000 --- a/src/main/java/org/atdev/artrip/service/ExhibitSearchService.java +++ /dev/null @@ -1,134 +0,0 @@ -package org.atdev.artrip.service; - -import co.elastic.clients.elasticsearch.ElasticsearchClient; -import co.elastic.clients.elasticsearch._types.SortOrder; -import co.elastic.clients.elasticsearch._types.query_dsl.Query; -import co.elastic.clients.elasticsearch.core.SearchRequest; -import co.elastic.clients.elasticsearch.core.SearchResponse; -import co.elastic.clients.elasticsearch.core.search.Hit; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.atdev.artrip.controller.dto.response.ExhibitSearchResponse; -import org.atdev.artrip.elastic.document.ExhibitDocument; -import org.atdev.artrip.global.apipayload.code.status.SearchErrorCode; -import org.atdev.artrip.global.apipayload.exception.GeneralException; -import org.springframework.stereotype.Service; - -import java.io.IOException; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -@Slf4j -public class ExhibitSearchService { - - private final ElasticsearchClient esClient; - private final SearchHistoryService searchHistoryService; - - private final static String EXHIBIT_INDEX = "exhibits"; - - public List searchExhibits(String keyword, Long userId) { - log.info("Searching exhibits - user: {}, keyword: {}", userId, keyword); - - if (keyword == null || keyword.trim().isEmpty()) { - throw new GeneralException(SearchErrorCode._SEARCH_KEYWORD_EMPTY); - } - - // 글자 길이 검증 로직 필요 시 활성화 예정 -// keyword = keyword.trim(); -// -// if (keyword.length() < 2) { -// throw new GeneralException(SearchErrorCode._SEARCH_KEYWORD_TOO_SHORT); -// } -// -// if (keyword.length() > 50) { -// throw new GeneralException(SearchErrorCode._SEARCH_KEYWORD_TOO_LONG); -// } - if (userId != null) { - try { - searchHistoryService.create(userId, keyword); - }catch (Exception e) { - log.error("Error Saving search history", e); - } - } - - try { - Query multiMatchQuery = Query.of(q -> q - .multiMatch(m -> m - .query(keyword) - .fields( - "title^3", - "title.nori^2", - "description^1", - "description.nori^1" - ) - .fuzziness("AUTO") - ) - ); - Query nestedKeywordQuery = Query.of(q -> q - .nested(n -> n - .path("keywords") - .query(nq -> nq - .multiMatch(m -> m - .query(keyword) - .fields( - "keywords.name^3", - "keywords.name.nori^2" - ) - .fuzziness("AUTO") - ) - ) - ) - ); - - Query combinedQuery = Query.of(q -> q - .bool(b -> b - .should(multiMatchQuery) - .should(nestedKeywordQuery) - .minimumShouldMatch("1") - ) - ); - - SearchRequest searchRequest = SearchRequest.of(r -> r - .index(EXHIBIT_INDEX) - .query(combinedQuery) - .size(100) - .sort(s -> s.score(sc -> sc.order(SortOrder.Desc))) - ); - - SearchResponse response = esClient.search(searchRequest, ExhibitDocument.class); - - List results = response.hits().hits().stream() - .map(Hit::source) - .map(this::convertToExhibitResponse) - .collect(Collectors.toList()); - - return results; - - } catch (IOException e) { - log.error("Elasticsearch search error", e); - throw new GeneralException(SearchErrorCode._SEARCH_EXHIBIT_NOT_FOUND); - } - } - - private ExhibitSearchResponse convertToExhibitResponse(ExhibitDocument doc) { - DateTimeFormatter ftt = DateTimeFormatter.ofPattern("yyyy.MM.dd"); - - return ExhibitSearchResponse.builder() - .id(doc.getId()) - .title(doc.getTitle()) - .description(doc.getDescription()) - .startDate(doc.getStartDate() != null ? doc.getStartDate().format(ftt) : null) - .endDate(doc.getEndDate() != null ? doc.getEndDate().format(ftt) : null) - .status(doc.getStatus()) - .posterUrl(doc.getPosterUrl()) - .ticketUrl(doc.getTicketUrl()) - .keywords(doc.getKeywords()) - .latitude(doc.getLatitude()) - .longitude(doc.getLongitude()) - .build(); - } - -} diff --git a/src/main/java/org/atdev/artrip/service/ExhibitService.java b/src/main/java/org/atdev/artrip/service/ExhibitService.java index e371d58..8fa183b 100644 --- a/src/main/java/org/atdev/artrip/service/ExhibitService.java +++ b/src/main/java/org/atdev/artrip/service/ExhibitService.java @@ -29,7 +29,7 @@ public ExhibitDetailResponse getExhibitDetail(Long exhibitId, Long userId, Image Exhibit exhibit = exhibitRepository.findById(exhibitId) .orElseThrow(() -> new GeneralException(ExhibitErrorCode._EXHIBIT_NOT_FOUND)); - String resizedPosterUrl = s3Service.buildResizeUrl(exhibit.getPosterUrl(), resize.getW(), resize.getH(), resize.getF()); + String resizedPosterUrl = s3Service.buildResizeUrl(exhibit.getPosterUrl(), resize.w(), resize.h(), resize.f()); boolean isFavorite = false; if (userId != null ) { @@ -41,5 +41,4 @@ public ExhibitDetailResponse getExhibitDetail(Long exhibitId, Long userId, Image return homeConverter.toHomeExhibitResponse(exhibit, isFavorite, resizedPosterUrl); } - } diff --git a/src/main/java/org/atdev/artrip/service/HomeService.java b/src/main/java/org/atdev/artrip/service/HomeService.java index a90d35b..adcc8d5 100644 --- a/src/main/java/org/atdev/artrip/service/HomeService.java +++ b/src/main/java/org/atdev/artrip/service/HomeService.java @@ -1,14 +1,15 @@ package org.atdev.artrip.service; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.atdev.artrip.controller.dto.request.*; +import org.atdev.artrip.controller.dto.response.CursorPaginationResponse; +import org.atdev.artrip.controller.dto.response.ExhibitSearchResult; +import org.atdev.artrip.domain.exhibitHall.ExhibitHall; import org.atdev.artrip.repository.UserRepository; import org.atdev.artrip.domain.exhibit.Exhibit; import org.atdev.artrip.repository.ExhibitHallRepository; import org.atdev.artrip.repository.FavoriteExhibitRepository; import org.atdev.artrip.converter.HomeConverter; -import org.atdev.artrip.controller.dto.response.FilterResponse; import org.atdev.artrip.repository.ExhibitRepository; import org.atdev.artrip.controller.dto.response.HomeListResponse; @@ -25,11 +26,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.format.DateTimeFormatter; import java.util.*; @Service @RequiredArgsConstructor -@Slf4j public class HomeService { private final ExhibitRepository exhibitRepository; @@ -79,12 +80,36 @@ public List getRegions() { } - //필터 전체 조회 - public FilterResponse getFilterExhibit(ExhibitFilterRequest dto, Long size, Long cursorId, Long userId) { + @Transactional(readOnly = true) + public CursorPaginationResponse findExhibits(ExhibitFilterRequest request, Long userId) { - Slice slice = exhibitRepository.findExhibitByFilters(dto, size, cursorId); + Slice slice = exhibitRepository.findExhibitByFilters(request, request.getSize(), request.getCursor()); Set favoriteIds = getFavoriteIds(userId); - return homeConverter.toFilterResponse(slice, favoriteIds); + + List data = slice.getContent().stream() + .map(exhibit -> { + ExhibitHall hall = exhibit.getExhibitHall(); + String hallName = (hall != null) ? hall.getName() : null; + String region = (hall != null) ? hall.getRegion() : null; + String country = (hall != null) ? hall.getCountry() : null; + + return ExhibitSearchResult.builder() + .exhibitId(exhibit.getExhibitId()) + .title(exhibit.getTitle()) + .posterUrl(exhibit.getPosterUrl()) + .status(exhibit.getStatus()) + .startDate(exhibit.getStartDate()) + .endDate(exhibit.getEndDate()) + .hallName(hallName) + .country(country) + .region(region) + .isFavorite(favoriteIds.contains(exhibit.getExhibitId())) + .build(); + }).toList(); + + Long nextCursor = (slice.hasNext() && !data.isEmpty()) ? slice.getContent().get(slice.getContent().size() - 1).getExhibitId() : null; + + return CursorPaginationResponse.of(data, slice.hasNext(), nextCursor); } // 사용자 맞춤 전시 랜덤 추천 @@ -108,7 +133,7 @@ public List getRandomPersonalized(Long userId, PersonalizedReq List results = exhibitRepository.findRandomExhibits(filter); results.forEach(r -> r.setPosterUrl( - s3Service.buildResizeUrl(r.getPosterUrl(), resize.getW(), resize.getH(), resize.getF()) + s3Service.buildResizeUrl(r.getPosterUrl(), resize.w(), resize.h(), resize.f()) )); Set favoriteIds = getFavoriteIds(userId); @@ -124,7 +149,7 @@ public List getRandomSchedule(ScheduleRandomRequest request, L List results = exhibitRepository.findRandomExhibits(filter); results.forEach(r -> r.setPosterUrl( - s3Service.buildResizeUrl(r.getPosterUrl(), resize.getW(), resize.getH(), resize.getF()) + s3Service.buildResizeUrl(r.getPosterUrl(), resize.w(), resize.h(), resize.f()) )); Set favoriteIds = getFavoriteIds(userId); @@ -141,7 +166,7 @@ public List getRandomGenre(GenreRandomRequest request, Long us List results = exhibitRepository.findRandomExhibits(filter); results.forEach(r -> r.setPosterUrl( - s3Service.buildResizeUrl(r.getPosterUrl(), resize.getW(), resize.getH(), resize.getF()) + s3Service.buildResizeUrl(r.getPosterUrl(), resize.w(), resize.h(), resize.f()) )); Set favoriteIds = getFavoriteIds(userId); @@ -158,7 +183,7 @@ public List getRandomToday(TodayRandomRequest request, Long us List results = exhibitRepository.findRandomExhibits(filter); results.forEach(r -> r.setPosterUrl( - s3Service.buildResizeUrl(r.getPosterUrl(), resize.getW(), resize.getH(), resize.getF()) + s3Service.buildResizeUrl(r.getPosterUrl(), resize.w(), resize.h(), resize.f()) )); Set favoriteIds = getFavoriteIds(userId); diff --git a/src/main/java/org/atdev/artrip/service/ReviewService.java b/src/main/java/org/atdev/artrip/service/ReviewService.java index 51d073f..742d0f8 100644 --- a/src/main/java/org/atdev/artrip/service/ReviewService.java +++ b/src/main/java/org/atdev/artrip/service/ReviewService.java @@ -163,7 +163,7 @@ public ReviewSliceResponse getAllReview(Long userId, Long cursor, int size, Imag .toList(); summaries.forEach(r -> r.setThumbnailUrl( - s3Service.buildResizeUrl(r.getThumbnailUrl(), resize.getW(), resize.getH(), resize.getF()) + s3Service.buildResizeUrl(r.getThumbnailUrl(), resize.w(), resize.h(), resize.f()) )); return new ReviewSliceResponse(summaries, nextCursor, slice.hasNext()); @@ -192,7 +192,7 @@ public ExhibitReviewSliceResponse getExhibitReview(Long exhibitId, Long cursor, .toList(); summaries.forEach(r -> r.setThumbnailUrl( - s3Service.buildResizeUrl(r.getThumbnailUrl(), resize.getW(), resize.getH(), resize.getF()) + s3Service.buildResizeUrl(r.getThumbnailUrl(), resize.w(), resize.h(), resize.f()) )); return new ExhibitReviewSliceResponse(summaries, nextCursor, slice.hasNext(),totalCount); diff --git a/src/main/java/org/atdev/artrip/service/SearchHistoryService.java b/src/main/java/org/atdev/artrip/service/SearchHistoryService.java deleted file mode 100644 index d2fdebb..0000000 --- a/src/main/java/org/atdev/artrip/service/SearchHistoryService.java +++ /dev/null @@ -1,152 +0,0 @@ -package org.atdev.artrip.service; - -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.atdev.artrip.domain.auth.User; -import org.atdev.artrip.global.apipayload.code.status.SearchErrorCode; -import org.atdev.artrip.repository.UserRepository; -import org.atdev.artrip.domain.search.SearchHistory; -import org.atdev.artrip.repository.SearchHistoryRepository; -import org.atdev.artrip.global.apipayload.code.status.CommonErrorCode; -import org.atdev.artrip.global.apipayload.exception.GeneralException; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Set; - -@Service -@RequiredArgsConstructor -@Slf4j -public class SearchHistoryService { - - private final SearchHistoryRepository searchHistoryRepository; - private final UserRepository userRepository; - private final StringRedisTemplate recommendRedisTemplate; - - private static final String POPULAR_KEYWORDS_KEY = "search:popular_keywords"; - - @PostConstruct - public void initRedis() { - try { - log.info("Redis cache with popular keywords"); - List keywords = searchHistoryRepository.findPopularKeywords(); - - if (!keywords.isEmpty()) { - recommendRedisTemplate.delete(POPULAR_KEYWORDS_KEY); - syncToRedis(keywords); - } - } catch (Exception e) { - log.error("redis cache filed : {}", e.getMessage(), e); - } - } - - @Scheduled(fixedRate = 300000) - public void syncToRedis() { - try { - List keywords = searchHistoryRepository.findPopularKeywords(); - - if (!keywords.isEmpty()) { - recommendRedisTemplate.delete(POPULAR_KEYWORDS_KEY); - syncToRedis(keywords); - } - } catch (Exception e) { - log.error("redis cache filed : {}", e.getMessage(), e); - } - } - - @Transactional - public void create(Long userId, String keyword) { - //TODO: 배포 전 로그 레벨 조정 - try { - log.debug("Saving search history for userId: {}, keyword: {}", userId, keyword); - - User user = userRepository.findById(userId).orElseThrow(() -> new GeneralException(CommonErrorCode._INTERNAL_SERVER_ERROR)); - - SearchHistory history = SearchHistory.builder() - .user(user) - .content(keyword) - .createdAt(LocalDateTime.now()) - .build(); - searchHistoryRepository.save(history); - - recommendRedisTemplate.opsForZSet().incrementScore(POPULAR_KEYWORDS_KEY, keyword, 1); - - } catch (Exception e) { - log.error(e.getMessage(), e); - } - } - - public List findRecent(Long userId) { - //TODO: 배포 전 로그 레벨 조정 - log.info("Searching for userId: {}", userId); - return searchHistoryRepository.findTop10ByUser_UserIdOrderByCreatedAtDesc(userId) - .stream() - .map(SearchHistory::getContent) - .distinct() - .toList(); - } - - @Transactional - public void remove(Long userId, String keyword) { - try { - log.debug("Deleting search history for userId: {}, keyword: {}", userId, keyword); - searchHistoryRepository.deleteByUserIdAndContent(userId, keyword); - - } catch (Exception e) { - log.error(e.getMessage(), e); - throw new GeneralException(SearchErrorCode._SEARCH_HISTORY_NOT_FOUND); - } - } - - @Transactional - public void removeAll(Long userId) { - try { - log.debug("Deleting all search history for userId: {}", userId); - searchHistoryRepository.deleteByUserId(userId); - } catch (Exception e) { - log.error(e.getMessage(), e); - throw new GeneralException(SearchErrorCode._SEARCH_EXHIBIT_NOT_FOUND); - } - } - - public List findPopularKeywords() { - try { - Set redisResult = recommendRedisTemplate.opsForZSet() - .reverseRange(POPULAR_KEYWORDS_KEY, 0, 4); - - if (redisResult != null && !redisResult.isEmpty()) { - log.debug("Popular keywords from Redis: {}", redisResult); - return redisResult.stream().toList(); - } - log.debug("Redis empty, falling back to MySQL"); - List mysqlResult = searchHistoryRepository.findPopularKeywords(); - - if (!mysqlResult.isEmpty()) { - syncToRedis(mysqlResult); - } - - return mysqlResult; - } catch (Exception e) { - log.error("Error finding popular keywrods from Redis: {}", e.getMessage(), e ); - return searchHistoryRepository.findPopularKeywords(); - } - } - - private void syncToRedis(List keywords) { - try { - for (int i = 0; i < keywords.size(); i++) { - double score = keywords.size() - i; - recommendRedisTemplate.opsForZSet().add(POPULAR_KEYWORDS_KEY, keywords.get(i), score); - } - log.debug("Synced {} keywords to Redis", keywords.size()); - } catch (Exception e) { - log.error("error syncing to Redis: {}", e.getMessage(), e); - } - } - -} diff --git a/src/main/java/org/atdev/artrip/service/UserService.java b/src/main/java/org/atdev/artrip/service/UserService.java index 82ef21a..e3376c9 100644 --- a/src/main/java/org/atdev/artrip/service/UserService.java +++ b/src/main/java/org/atdev/artrip/service/UserService.java @@ -143,7 +143,7 @@ public MypageResponse getMypage(Long userId, ImageResizeRequest resize){ User user = userRepository.findById(userId) .orElseThrow(()-> new GeneralException(UserErrorCode._USER_NOT_FOUND)); - String profileImage = s3Service.buildResizeUrl(user.getProfileImageUrl(), resize.getW(), resize.getH(), resize.getF()); + String profileImage = s3Service.buildResizeUrl(user.getProfileImageUrl(), resize.w(), resize.h(), resize.f()); return new MypageResponse(user.getNickName(), profileImage, user.getEmail()); } diff --git a/src/test/java/org/atdev/artrip/service/SearchServiceTest.java b/src/test/java/org/atdev/artrip/service/SearchServiceTest.java new file mode 100644 index 0000000..f983945 --- /dev/null +++ b/src/test/java/org/atdev/artrip/service/SearchServiceTest.java @@ -0,0 +1,94 @@ +package org.atdev.artrip.service; + +import org.atdev.artrip.controller.dto.request.ExhibitFilterRequest; +import org.atdev.artrip.controller.dto.request.ImageResizeRequest; +import org.atdev.artrip.controller.dto.response.CursorPaginationResponse; +import org.atdev.artrip.controller.dto.response.HomeListResponse; +import org.atdev.artrip.domain.exhibit.Exhibit; +import org.atdev.artrip.domain.exhibitHall.ExhibitHall; +import org.atdev.artrip.global.s3.service.S3Service; +import org.atdev.artrip.repository.ExhibitRepository; +import org.atdev.artrip.repository.FavoriteExhibitRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.time.LocalDate; +import java.util.Collections; +import java.util.List; + +@ExtendWith(MockitoExtension.class) +public class SearchServiceTest { + + @InjectMocks + HomeService homeService; + + @Mock + ExhibitRepository exhibitRepository; + + @Mock + private S3Service s3Service; + + @Mock + private FavoriteExhibitRepository favoriteExhibitRepository; + + @DisplayName("키워드 전시 검색 결과 조회") + @Test + void searchKeyword() { + + // Given + ExhibitFilterRequest request = new ExhibitFilterRequest(); + request.setKeyword("현대 전시"); + + Long userId = 1L; + ImageResizeRequest resizeRequest = new ImageResizeRequest(100, 100, "webp"); + String mockResizedUrl = "https://resized-url.com?w=100&h100"; + + ExhibitHall mockExhibitHall = ExhibitHall.builder() + .name("Hangar Y") + .isDomestic(false) + .region("뮈동") + .build(); + + Exhibit mockExhibit = Exhibit.builder() + .exhibitId(1L) + .title("Matisse – Soulages") + .posterUrl(mockResizedUrl) + .exhibitHall(mockExhibitHall) + .startDate(LocalDate.now()) + .endDate(LocalDate.now().plusDays(10)) + .build(); + + List content = List.of(mockExhibit); + Slice slice = new SliceImpl<>(content, PageRequest.of(0, 10), false); + + when(exhibitRepository.findExhibitByFilters(any(), any(), any())) + .thenReturn(slice); + when(s3Service.buildResizeUrl(any(),any(),any(),any())) + .thenReturn(mockResizedUrl); + when(favoriteExhibitRepository.findActiveExhibitIds(anyLong())) + .thenReturn(Collections.emptySet()); + + // when + CursorPaginationResponse result = homeService.findExhibits(request, resizeRequest, userId); + + // then + HomeListResponse actualDto = result.getData().get(0); + Assertions.assertNotNull(result); + Assertions.assertEquals(1, result.getData().size()); + Assertions.assertEquals("Matisse – Soulages", actualDto.getTitle()); + Assertions.assertEquals(mockResizedUrl, actualDto.getPosterUrl()); + + } + +}