From 9a4c1e7fc71311e6884fdd92826893688320bf95 Mon Sep 17 00:00:00 2001 From: e_wha Date: Fri, 18 Apr 2025 18:43:17 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EA=B4=80=EA=B4=91=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=97=91=EC=85=80=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20API=20=EB=B0=8F=20=ED=8E=98=EC=9D=B4=EC=A7=95,=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 39 +++++ .../ElasticSearchEngineApplication.java | 4 + .../common/BaseResponseStatus.java | 40 +++++ .../common/CommonResponse.java | 44 ++++++ .../config/QuerydslConfig.java | 15 ++ .../config/SwaggerConfig.java | 25 +++ .../controller/TourController.java | 80 ++++++++++ .../elasticsearchengine/domain/Tour.java | 48 ++++++ .../dto/ExcelSaveTourListResponseDto.java | 15 ++ .../elasticsearchengine/dto/TourListDto.java | 19 +++ .../dto/TourListResponseDto.java | 15 ++ .../dto/TourSaveRequestDto.java | 39 +++++ .../repository/TourQueryRepository.java | 17 ++ .../repository/TourQueryRepositoryImpl.java | 118 ++++++++++++++ .../repository/TourRepository.java | 18 +++ .../service/TourService.java | 17 ++ .../service/TourServiceImpl.java | 146 ++++++++++++++++++ .../vo/ExcelSaveTourListResponseVo.java | 24 +++ .../vo/TourListResponseVo.java | 26 ++++ src/main/resources/application.properties | 13 ++ 20 files changed, 762 insertions(+) create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/common/BaseResponseStatus.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/common/CommonResponse.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/config/QuerydslConfig.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/config/SwaggerConfig.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/controller/TourController.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/domain/Tour.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/dto/ExcelSaveTourListResponseDto.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/dto/TourListDto.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/dto/TourListResponseDto.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/dto/TourSaveRequestDto.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/repository/TourQueryRepository.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/repository/TourQueryRepositoryImpl.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/repository/TourRepository.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/service/TourService.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/service/TourServiceImpl.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/vo/ExcelSaveTourListResponseVo.java create mode 100644 src/main/java/com/elasticsearch/elasticsearchengine/vo/TourListResponseVo.java diff --git a/build.gradle b/build.gradle index b1e3f0a..f938c0b 100644 --- a/build.gradle +++ b/build.gradle @@ -27,14 +27,53 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' +<<<<<<< HEAD +======= + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + //xlsx 처리 + implementation 'org.apache.poi:poi:5.2.3' + implementation 'org.apache.poi:poi-ooxml:5.2.3' + +>>>>>>> 7ffefd2 (feat: 관광 정보 엑셀 저장 기능 API 및 페이징, 검색 조회 기능 API) compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +<<<<<<< HEAD +======= + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' // Swagger +>>>>>>> 7ffefd2 (feat: 관광 정보 엑셀 저장 기능 API 및 페이징, 검색 조회 기능 API) } tasks.named('test') { useJUnitPlatform() } +<<<<<<< HEAD +======= + +//QueryDSL +def querydslDir = "$buildDir/generated/querydsl" + +sourceSets { + main { + java { + srcDirs += querydslDir + } + } +} + +tasks.withType(JavaCompile).configureEach { + options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir) +} + +dependencies { + // 이미 있더라도, 확실하게 다음을 포함해줘야 함 + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta" + annotationProcessor "jakarta.persistence:jakarta.persistence-api:3.1.0" + annotationProcessor "jakarta.annotation:jakarta.annotation-api:2.1.1" +} +>>>>>>> 7ffefd2 (feat: 관광 정보 엑셀 저장 기능 API 및 페이징, 검색 조회 기능 API) diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/ElasticSearchEngineApplication.java b/src/main/java/com/elasticsearch/elasticsearchengine/ElasticSearchEngineApplication.java index c40666b..2fa17d7 100644 --- a/src/main/java/com/elasticsearch/elasticsearchengine/ElasticSearchEngineApplication.java +++ b/src/main/java/com/elasticsearch/elasticsearchengine/ElasticSearchEngineApplication.java @@ -11,3 +11,7 @@ public static void main(String[] args) { } } +<<<<<<< HEAD +======= + +>>>>>>> 7ffefd2 (feat: 관광 정보 엑셀 저장 기능 API 및 페이징, 검색 조회 기능 API) diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/common/BaseResponseStatus.java b/src/main/java/com/elasticsearch/elasticsearchengine/common/BaseResponseStatus.java new file mode 100644 index 0000000..e9d9753 --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/common/BaseResponseStatus.java @@ -0,0 +1,40 @@ +package com.elasticsearch.elasticsearchengine.common; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; + +@Getter +@RequiredArgsConstructor +public enum BaseResponseStatus { + + /** + * 200: 요청 성공 + **/ + SUCCESS(HttpStatus.OK, true, 200, "요청에 성공했습니다."), + NONE_DATA(HttpStatus.OK, true, 200, "조회할 게시물이 없습니다."), + + /** + * 400 : security 에러 + */ + WRONG_PAGE_NUM_MIN(HttpStatus.BAD_REQUEST, false, 400, "잘못된 페이지 번호입니다. (최소 1 이상)"), + WRONG_PARAM(HttpStatus.BAD_REQUEST, false, 400, "잘못된 요청 (필수 값 누락 또는 잘못된 입력)"), + WRONG_PAGE_NUM_MAX(HttpStatus.BAD_REQUEST, false, 400, "잘못된 size 값입니다. (1~100)"), + /** + * 500 : security 에러 + */ + WRONG_SERVER(HttpStatus.INTERNAL_SERVER_ERROR, false, 500, "서버 내부 오류가 발생"), + + /** + * 900: 기타 에러 + */ + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, false, 900, "Internal server error"), + NO_EXIST_IMAGE(HttpStatus.NOT_FOUND, false, 901, "존재하지 않는 이미지 입니다"); + + private final HttpStatusCode httpStatusCode; + private final boolean isSuccess; + private final int code; + private final String message; + +} \ No newline at end of file diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/common/CommonResponse.java b/src/main/java/com/elasticsearch/elasticsearchengine/common/CommonResponse.java new file mode 100644 index 0000000..7e927b9 --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/common/CommonResponse.java @@ -0,0 +1,44 @@ +package com.elasticsearch.elasticsearchengine.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; + +@Data +@Builder +public class CommonResponse { + private boolean success; + private String message; + private T data; + private Error error; + public static CommonResponse success(String message) { + return new CommonResponse<>(true, message, null,null); + } + + public static CommonResponse success(String message, T data) { + return new CommonResponse<>(true, message, data,null); + } + + public static CommonResponse success(T data) { + return new CommonResponse<>(true, null, data,null); + } + + public static CommonResponse fail(BaseResponseStatus code, String message) { + return new CommonResponse<>(false, message, null, new Error(code)); + } + + public static CommonResponse fail(BaseResponseStatus code) { + return new CommonResponse<>(false, code.getMessage(), null, new Error(code)); + } + + public static CommonResponse fail(BaseResponseStatus code, T data, String message) { + return new CommonResponse<>(false, message, data, new Error(code)); + } + + @Getter + @AllArgsConstructor + static class Error { + private BaseResponseStatus code; + } +} diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/config/QuerydslConfig.java b/src/main/java/com/elasticsearch/elasticsearchengine/config/QuerydslConfig.java new file mode 100644 index 0000000..c5ad78d --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/config/QuerydslConfig.java @@ -0,0 +1,15 @@ +package com.elasticsearch.elasticsearchengine.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuerydslConfig { + + @Bean + public JPAQueryFactory jpaQueryFactory(EntityManager em) { + return new JPAQueryFactory(em); + } +} \ No newline at end of file diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/config/SwaggerConfig.java b/src/main/java/com/elasticsearch/elasticsearchengine/config/SwaggerConfig.java new file mode 100644 index 0000000..207fbba --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/config/SwaggerConfig.java @@ -0,0 +1,25 @@ +package com.elasticsearch.elasticsearchengine.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + //http://localhost:8080/swagger-ui/index.html + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .components(new Components()) + .info(info()); + } + + private Info info() { + return new Info() + .title("Postgresql 기반 RestAPI") + .description("개발자들을 위한 문서입니다.") + .version("1.0"); + } +} diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/controller/TourController.java b/src/main/java/com/elasticsearch/elasticsearchengine/controller/TourController.java new file mode 100644 index 0000000..0a5070e --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/controller/TourController.java @@ -0,0 +1,80 @@ +package com.elasticsearch.elasticsearchengine.controller; + +import com.elasticsearch.elasticsearchengine.common.CommonResponse; +import com.elasticsearch.elasticsearchengine.dto.ExcelSaveTourListResponseDto; +import com.elasticsearch.elasticsearchengine.dto.TourListResponseDto; +import com.elasticsearch.elasticsearchengine.service.TourService; +import com.elasticsearch.elasticsearchengine.vo.ExcelSaveTourListResponseVo; +import com.elasticsearch.elasticsearchengine.vo.TourListResponseVo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + +import static com.elasticsearch.elasticsearchengine.common.BaseResponseStatus.INTERNAL_SERVER_ERROR; + +@RequiredArgsConstructor +@RestController +@RequestMapping("tour") +@Tag(name = "tour_controller", description = "관광정보관리시스템") +public class TourController { + + private final TourService tourService; + + @PostMapping(value = "/save", consumes = "multipart/form-data") + @Operation(summary = "관광 정보 등록 (엑셀 삽입)", description = "엑셀에 등록된 관광 정보를 등록합니다.") + public CommonResponse excelSaveTourList( + @RequestPart("file") + @io.swagger.v3.oas.annotations.Parameter( + description = "관광 정보가 삽입된 엑셀 파일", + required = true + ) MultipartFile file + ) { + try { + ExcelSaveTourListResponseDto responseDto = tourService.excelSaveTourList(file); + ExcelSaveTourListResponseVo responseVo = ExcelSaveTourListResponseVo.dtoToVo(responseDto); + return CommonResponse.success("관광정보가 성공적으로 등록되었습니다.", responseVo); + } catch (IOException e) { + return CommonResponse.fail(INTERNAL_SERVER_ERROR, "파일 처리 중 오류가 발생했습니다."); + } + } + + + @GetMapping(value = "/find") + @Operation(summary = "관광 정보 조회", description = "페이지 번호, 관광 타입 번호, 시군구코드, 서브 카테고리, 태그를 기준으로 관광 정보를 조회합니다.") + @Parameters({ + @Parameter(name = "page", description = "페이지 번호, 기본 값 : 1"), + @Parameter(name = "contentTypeId", description = "관광 타입 번호"), + @Parameter(name = "sigunguCode", description = "시군구코드"), + @Parameter(name = "sideCategory", description = "서브 카테고리"), + @Parameter(name = "tags", description = "태그"), + }) + public CommonResponse findTourList( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "contentTypeId") String contentTypeId, + @RequestParam(name = "sigunguCode", required = false) String sigunguCode, + @RequestParam(name = "sideCategory", required = false) String sideCategory, + @RequestParam(name = "tags", required = false) List tags + ) { + Page TourListPage = tourService.findByMultiCode(page, contentTypeId, sigunguCode, sideCategory, tags); + + if (TourListPage.isEmpty()) { + return CommonResponse.success("관광 정보", TourListResponseVo.builder() + .totalList(0) + .currentPage(page) + .totalPages(0) + .tourListDto(null) + .build()); + } + + return CommonResponse.success("관광 정보", + TourListResponseVo.dtoToVo(TourListPage.getContent().get(0))); + } +} diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/domain/Tour.java b/src/main/java/com/elasticsearch/elasticsearchengine/domain/Tour.java new file mode 100644 index 0000000..89633df --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/domain/Tour.java @@ -0,0 +1,48 @@ +package com.elasticsearch.elasticsearchengine.domain; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDate; + +@Builder +@Data +@AllArgsConstructor +@NoArgsConstructor +@Entity(name = "tour") +public class Tour { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int TourId; + + @Column(nullable = false, unique = true) + private int contentId; + + private String contentTypeId; + + private String title; + + private String addr; + + private String zipCode; + + private String areaCode; + + private String sigunguCode; + + private String category; + + private String sideCategory; + + private String tags; + + private String thumbnail; + + private double mapx; + + private double mapy; + + private LocalDate createdTime; + + private LocalDate modifiedTime; +} diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/dto/ExcelSaveTourListResponseDto.java b/src/main/java/com/elasticsearch/elasticsearchengine/dto/ExcelSaveTourListResponseDto.java new file mode 100644 index 0000000..a8bd6e4 --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/dto/ExcelSaveTourListResponseDto.java @@ -0,0 +1,15 @@ +package com.elasticsearch.elasticsearchengine.dto; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Builder +@Data +public class ExcelSaveTourListResponseDto { + + private int successCount; + private int failedCount; + private List errors; +} diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/dto/TourListDto.java b/src/main/java/com/elasticsearch/elasticsearchengine/dto/TourListDto.java new file mode 100644 index 0000000..002677d --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/dto/TourListDto.java @@ -0,0 +1,19 @@ +package com.elasticsearch.elasticsearchengine.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TourListDto { + private int contentId; + private String title; + private String thumbnail; + private String tags; +} diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/dto/TourListResponseDto.java b/src/main/java/com/elasticsearch/elasticsearchengine/dto/TourListResponseDto.java new file mode 100644 index 0000000..d940958 --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/dto/TourListResponseDto.java @@ -0,0 +1,15 @@ +package com.elasticsearch.elasticsearchengine.dto; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class TourListResponseDto { + private int totalList; + private int currentPage; + private int totalPages; + private List tourListDto; +} \ No newline at end of file diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/dto/TourSaveRequestDto.java b/src/main/java/com/elasticsearch/elasticsearchengine/dto/TourSaveRequestDto.java new file mode 100644 index 0000000..2a6ff4a --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/dto/TourSaveRequestDto.java @@ -0,0 +1,39 @@ +package com.elasticsearch.elasticsearchengine.dto; + +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class TourSaveRequestDto { + + private int contentId; + + private String contentTypeId; + + private String title; + + private String addr; + + private String zipCode; + + private String areaCode; + + private String sigunguCode; + + private String category; + + private String sideCategory; + + private String tags; + + private String thumbnail; + + private double mapx; + + private double mapy; + + private LocalDate createdTime; + + private LocalDate modifiedTime; +} diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/repository/TourQueryRepository.java b/src/main/java/com/elasticsearch/elasticsearchengine/repository/TourQueryRepository.java new file mode 100644 index 0000000..d8004e7 --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/repository/TourQueryRepository.java @@ -0,0 +1,17 @@ +package com.elasticsearch.elasticsearchengine.repository; + +import com.elasticsearch.elasticsearchengine.domain.Tour; +import com.elasticsearch.elasticsearchengine.dto.TourListDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface TourQueryRepository { + + Page findByMultiCode(String contentTypeId, String sigunguCode, String sideCategory, List tags, Pageable pageable); + + //자연용 + Page findMultiCodeNature(String contentTypeId, String category, String sigunguCode, String sideCategory, List tags, Pageable pageable); +} diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/repository/TourQueryRepositoryImpl.java b/src/main/java/com/elasticsearch/elasticsearchengine/repository/TourQueryRepositoryImpl.java new file mode 100644 index 0000000..1a00a6b --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/repository/TourQueryRepositoryImpl.java @@ -0,0 +1,118 @@ +package com.elasticsearch.elasticsearchengine.repository; + +import com.elasticsearch.elasticsearchengine.domain.QTour; +import com.elasticsearch.elasticsearchengine.domain.Tour; +import com.elasticsearch.elasticsearchengine.dto.TourListDto; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.expression.spel.ast.Projection; + +import java.util.List; + + +@RequiredArgsConstructor +public class TourQueryRepositoryImpl implements TourQueryRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public Page findByMultiCode(String contentTypeId, String sigunguCode, String sideCategory, List tags, Pageable pageable) { + QTour tour = QTour.tour; + + BooleanBuilder builder = new BooleanBuilder(); + + builder.and(tour.contentTypeId.eq(contentTypeId)); + + if (sigunguCode != null && !sigunguCode.isBlank()) { + builder.and(tour.sigunguCode.eq(sigunguCode)); + } + + if (sideCategory != null && !sideCategory.isBlank()) { + builder.and(tour.sideCategory.eq(sideCategory)); + } + + if (tags != null && !tags.isEmpty()) { + BooleanBuilder tagBuilder = new BooleanBuilder(); + for (String tag : tags) { + tagBuilder.or(tour.tags.contains(tag)); // tags LIKE %tag% + } + builder.and(tagBuilder); + } + + List content = queryFactory + .select(Projections.constructor( + TourListDto.class, + tour.contentId, + tour.title, + tour.thumbnail, + tour.tags + )) + .from(tour) + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = queryFactory + .select(tour.count()) + .from(tour) + .where(builder) + .fetchOne(); + + return new PageImpl<>(content, pageable, total == null ? 0 : total); + } + + @Override + public Page findMultiCodeNature(String contentTypeId, String category, String sigunguCode, String sideCategory, List tags, Pageable pageable) { + QTour tour = QTour.tour; + + BooleanBuilder builder = new BooleanBuilder(); + + builder.and(tour.contentTypeId.eq(contentTypeId)); + builder.and(tour.category.eq(category)); + + if (sigunguCode != null && !sigunguCode.isBlank()) { + builder.and(tour.sigunguCode.eq(sigunguCode)); + } + + if (sideCategory != null && !sideCategory.isBlank()) { + builder.and(tour.sideCategory.eq(sideCategory)); + } + + if (tags != null && !tags.isEmpty()) { + BooleanBuilder tagBuilder = new BooleanBuilder(); + for (String tag : tags) { + tagBuilder.or(tour.tags.contains(tag)); // tags LIKE %tag% + } + builder.and(tagBuilder); + } + + List content = queryFactory + .select(Projections.constructor( + TourListDto.class, + tour.contentId.stringValue(), + tour.title, + tour.thumbnail, + Expressions.stringTemplate("function('string_to_array', {0}, ',')", tour.tags) + )) + .from(tour) + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = queryFactory + .select(tour.count()) + .from(tour) + .where(builder) + .fetchOne(); + + return new PageImpl<>(content, pageable, total == null ? 0 : total); + } +} diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/repository/TourRepository.java b/src/main/java/com/elasticsearch/elasticsearchengine/repository/TourRepository.java new file mode 100644 index 0000000..f63d76b --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/repository/TourRepository.java @@ -0,0 +1,18 @@ +package com.elasticsearch.elasticsearchengine.repository; + +import com.elasticsearch.elasticsearchengine.domain.Tour; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + + +public interface TourRepository extends JpaRepository , TourQueryRepository { + + Page findByContentTypeId(String contentTypeId, Pageable pageable); + + //자연용 + Page findByContentTypeIdAndCategory(String contentTypeId, String category, Pageable pageable); + + + +} diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/service/TourService.java b/src/main/java/com/elasticsearch/elasticsearchengine/service/TourService.java new file mode 100644 index 0000000..d970429 --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/service/TourService.java @@ -0,0 +1,17 @@ +package com.elasticsearch.elasticsearchengine.service; + +import com.elasticsearch.elasticsearchengine.dto.ExcelSaveTourListResponseDto; +import com.elasticsearch.elasticsearchengine.dto.TourListResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + +public interface TourService { + + public ExcelSaveTourListResponseDto excelSaveTourList(MultipartFile file) throws IOException; + public Page findBycontentTypeId(int page, String contentTypeId); + public Page findByMultiCode(int page, String contentTypeId, String sigunguCode, String sideCategory, List tags); +} diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/service/TourServiceImpl.java b/src/main/java/com/elasticsearch/elasticsearchengine/service/TourServiceImpl.java new file mode 100644 index 0000000..db62b4c --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/service/TourServiceImpl.java @@ -0,0 +1,146 @@ +package com.elasticsearch.elasticsearchengine.service; + +import com.elasticsearch.elasticsearchengine.domain.Tour; +import com.elasticsearch.elasticsearchengine.dto.ExcelSaveTourListResponseDto; +import com.elasticsearch.elasticsearchengine.dto.TourListDto; +import com.elasticsearch.elasticsearchengine.dto.TourListResponseDto; +import com.elasticsearch.elasticsearchengine.repository.TourRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import org.apache.poi.ss.usermodel.*; + +import java.io.IOException; +import java.io.InputStream; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +public class TourServiceImpl implements TourService { + + private final TourRepository tourRepository; + + + @Override + @Transactional + public ExcelSaveTourListResponseDto excelSaveTourList(MultipartFile file) throws IOException { + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + + List TourToSave = new ArrayList<>(); + List errors = new ArrayList<>(); + int successCount = 0; + int failedCount = 0; + + + try (InputStream inputStream = file.getInputStream(); Workbook workbook = WorkbookFactory.create(inputStream)) { + Sheet sheet = workbook.getSheetAt(0); + for (Row row : sheet) { + if (row.getRowNum() == 0) continue; + + String contentId = getCellValue(row.getCell(0)); + String contentTypeId = getCellValue(row.getCell(1)); + String title = getCellValue(row.getCell(2)); + String addr = getCellValue(row.getCell(3)); + String zipCode = getCellValue(row.getCell(4)); + String areaCode = getCellValue(row.getCell(5)); + String sigunguCode = getCellValue(row.getCell(6)); + String category = getCellValue(row.getCell(7)); + String sideCategory = getCellValue(row.getCell(8)); + String tags = getCellValue(row.getCell(9)); + String thumbnail = getCellValue(row.getCell(10)); + String mapx = getCellValue(row.getCell(11)); + String mapy = getCellValue(row.getCell(12)); + String createdTime = getCellValue(row.getCell(13)); + String modifiedTime = getCellValue(row.getCell(14)); + + TourToSave.add(Tour.builder() + .contentId(Integer.parseInt(contentId)) + .contentTypeId(contentTypeId) + .title(title) + .addr(addr) + .zipCode(zipCode) + .areaCode(areaCode) + .sigunguCode(sigunguCode) + .category(category) + .sideCategory(sideCategory) + .tags(tags) + .thumbnail(thumbnail) + .mapx(Double.parseDouble(mapx)) + .mapy(Double.parseDouble(mapy)) + .createdTime(LocalDate.parse(createdTime, formatter)) + .modifiedTime(LocalDate.parse(modifiedTime, formatter)) + .build() + ); + successCount++; + } + } + + tourRepository.saveAll(TourToSave); + + return ExcelSaveTourListResponseDto.builder() + .successCount(successCount) + .failedCount(failedCount) + .errors(errors) + .build(); + } + + private String getCellValue(Cell cell) { + if (cell == null) return ""; + return switch (cell.getCellType()) { + case STRING -> cell.getStringCellValue().trim(); + case NUMERIC -> String.valueOf((long) cell.getNumericCellValue()); + case BOOLEAN -> String.valueOf(cell.getBooleanCellValue()); + default -> ""; + }; + } + + @Override + public Page findBycontentTypeId(int page, String contentTypeId) { + PageRequest pageable = PageRequest.of(page, 12); + Page TourPage; + + TourPage = tourRepository.findByContentTypeId(contentTypeId, pageable); + + if (TourPage.isEmpty()) { + return Page.empty(); + } + + return TourPage.map(Tour -> TourListResponseDto.builder() + .totalList((int)TourPage.getTotalElements()) + .currentPage(TourPage.getNumber() + 1) + .totalPages(TourPage.getTotalPages()) + .build()); + } + + @Override + public Page findByMultiCode(int page, String contentTypeId, String sigunguCode, String sideCategory, List tags) { + PageRequest pageable = PageRequest.of(page, 12); + Page TourPage; + + TourPage = tourRepository.findByMultiCode(contentTypeId, sigunguCode, sideCategory ,tags ,pageable); + + if (TourPage.isEmpty()) { + return Page.empty(); + } + + TourListResponseDto responseDto = TourListResponseDto.builder() + .totalList((int) TourPage.getTotalElements()) + .currentPage(TourPage.getNumber() + 1) + .totalPages(TourPage.getTotalPages()) + .tourListDto(TourPage.toList()) + .build(); + + return new PageImpl<>(List.of(responseDto), pageable, 1); + } +} \ No newline at end of file diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/vo/ExcelSaveTourListResponseVo.java b/src/main/java/com/elasticsearch/elasticsearchengine/vo/ExcelSaveTourListResponseVo.java new file mode 100644 index 0000000..7d88263 --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/vo/ExcelSaveTourListResponseVo.java @@ -0,0 +1,24 @@ +package com.elasticsearch.elasticsearchengine.vo; + +import com.elasticsearch.elasticsearchengine.dto.ExcelSaveTourListResponseDto; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Builder +@Data +public class ExcelSaveTourListResponseVo { + + private int successCount; + private int errorCount; + private List errors; + + public static ExcelSaveTourListResponseVo dtoToVo(ExcelSaveTourListResponseDto excelSaveTourListResponseDto) { + return ExcelSaveTourListResponseVo.builder() + .successCount(excelSaveTourListResponseDto.getSuccessCount()) + .errorCount(excelSaveTourListResponseDto.getFailedCount()) + .errors(excelSaveTourListResponseDto.getErrors()) + .build(); + } +} diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/vo/TourListResponseVo.java b/src/main/java/com/elasticsearch/elasticsearchengine/vo/TourListResponseVo.java new file mode 100644 index 0000000..d4c0375 --- /dev/null +++ b/src/main/java/com/elasticsearch/elasticsearchengine/vo/TourListResponseVo.java @@ -0,0 +1,26 @@ +package com.elasticsearch.elasticsearchengine.vo; + +import com.elasticsearch.elasticsearchengine.dto.TourListDto; +import com.elasticsearch.elasticsearchengine.dto.TourListResponseDto; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class TourListResponseVo { + private int totalList; + private int currentPage; + private int totalPages; + private List tourListDto; + + public static TourListResponseVo dtoToVo(TourListResponseDto tourListResponseDto) { + return TourListResponseVo.builder() + .totalList(tourListResponseDto.getTotalList()) + .currentPage(tourListResponseDto.getCurrentPage()) + .totalPages(tourListResponseDto.getTotalPages()) + .tourListDto(tourListResponseDto.getTourListDto()) + .build(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b4ddbad..42246df 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,14 @@ spring.application.name=elasticSearchEngine +<<<<<<< HEAD +======= + +#postgresql +spring.datasource.url=jdbc:postgresql://localhost:5432/postgres +spring.datasource.username=postgres +spring.datasource.password=postgres +spring.datasource.driver-class-name=org.postgresql.Driver + +#jpa +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +>>>>>>> 7ffefd2 (feat: 관광 정보 엑셀 저장 기능 API 및 페이징, 검색 조회 기능 API) From 60c6aa3b3d908788cb464e0dda5073627ff07a98 Mon Sep 17 00:00:00 2001 From: e_wha <55922263+e-wha@users.noreply.github.com> Date: Tue, 22 Apr 2025 09:50:28 +0900 Subject: [PATCH 2/3] Update build.gradle --- build.gradle | 9 --------- 1 file changed, 9 deletions(-) diff --git a/build.gradle b/build.gradle index f938c0b..f9ff51d 100644 --- a/build.gradle +++ b/build.gradle @@ -27,32 +27,24 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' -<<<<<<< HEAD -======= implementation 'org.springframework.boot:spring-boot-starter-data-jpa' //xlsx 처리 implementation 'org.apache.poi:poi:5.2.3' implementation 'org.apache.poi:poi-ooxml:5.2.3' ->>>>>>> 7ffefd2 (feat: 관광 정보 엑셀 저장 기능 API 및 페이징, 검색 조회 기능 API) compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' -<<<<<<< HEAD -======= implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' // Swagger ->>>>>>> 7ffefd2 (feat: 관광 정보 엑셀 저장 기능 API 및 페이징, 검색 조회 기능 API) } tasks.named('test') { useJUnitPlatform() } -<<<<<<< HEAD -======= //QueryDSL def querydslDir = "$buildDir/generated/querydsl" @@ -76,4 +68,3 @@ dependencies { annotationProcessor "jakarta.persistence:jakarta.persistence-api:3.1.0" annotationProcessor "jakarta.annotation:jakarta.annotation-api:2.1.1" } ->>>>>>> 7ffefd2 (feat: 관광 정보 엑셀 저장 기능 API 및 페이징, 검색 조회 기능 API) From 4750b2bd0b222a64d5c15c11f0386d1d9181c299 Mon Sep 17 00:00:00 2001 From: e_wha <55922263+e-wha@users.noreply.github.com> Date: Tue, 22 Apr 2025 09:50:43 +0900 Subject: [PATCH 3/3] Update ElasticSearchEngineApplication.java --- .../elasticsearchengine/ElasticSearchEngineApplication.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/com/elasticsearch/elasticsearchengine/ElasticSearchEngineApplication.java b/src/main/java/com/elasticsearch/elasticsearchengine/ElasticSearchEngineApplication.java index 2fa17d7..c40666b 100644 --- a/src/main/java/com/elasticsearch/elasticsearchengine/ElasticSearchEngineApplication.java +++ b/src/main/java/com/elasticsearch/elasticsearchengine/ElasticSearchEngineApplication.java @@ -11,7 +11,3 @@ public static void main(String[] args) { } } -<<<<<<< HEAD -======= - ->>>>>>> 7ffefd2 (feat: 관광 정보 엑셀 저장 기능 API 및 페이징, 검색 조회 기능 API)