diff --git a/.github/workflows/action-develop-cd.yml b/.github/workflows/action-develop-cd.yml
index e654d6ad..8d607c04 100644
--- a/.github/workflows/action-develop-cd.yml
+++ b/.github/workflows/action-develop-cd.yml
@@ -6,7 +6,7 @@ on:
branches:
- develop
- # 코드의 내용을 이 파일을 실행하여 action을 수행하는 주체(Github Actions에서 사용하는 VM)가 읽을 수 있도록 권한을 설정
+ # 코드의 내용을 이 파일을 실행하여 action을 수행하는 주체(Github Actions에서 사용하는 VM)가 읽을 수 있도록 권한을 설정
permissions:
contents: read
diff --git a/build.gradle b/build.gradle
index d7c05c48..de8f8cff 100644
--- a/build.gradle
+++ b/build.gradle
@@ -56,9 +56,12 @@ dependencies {
//spring doc 추가
dependencies {
- implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
}
+ //Validation을 위해 추가! Spring Boot 2.3버전 이후부터는 web 의존성안에 있던 validation 관련 package가 아예 모듈로 빠짐
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+
}
test {
diff --git a/src/main/java/com/nainga/nainga/domain/report/api/ReportApi.java b/src/main/java/com/nainga/nainga/domain/report/api/ReportApi.java
new file mode 100644
index 00000000..7e5c4c3b
--- /dev/null
+++ b/src/main/java/com/nainga/nainga/domain/report/api/ReportApi.java
@@ -0,0 +1,75 @@
+package com.nainga.nainga.domain.report.api;
+
+import com.nainga.nainga.domain.report.application.ReportService;
+import com.nainga.nainga.domain.report.domain.Report;
+import com.nainga.nainga.domain.report.dto.SaveNewStoreReportRequest;
+import com.nainga.nainga.domain.report.dto.SaveSpecificStoreReportRequest;
+import com.nainga.nainga.global.util.Result;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequiredArgsConstructor
+public class ReportApi {
+ private final ReportService reportService;
+
+ //사용자의 신규 가게 등록 요청에 대한 제보를 저장
+ @Tag(name = "[New] 사용자 제보")
+ @Operation(summary = "사용자의 신규 가게 등록 요청에 대한 제보를 서버에 저장", description = "사용자의 신규 가게 등록 요청에 대한 제보를 서버에 저장합니다.
" +
+ "[Request Body]
" +
+ "storeName: 등록 요청하는 가게 이름
" +
+ "formattedAddress: 등록 요청하는 가게 주소
" +
+ "certifications: 가게가 가지고 있는 인증제들의 이름을 담은 리스트. 착한가격업소, 모범음식점, 안심식당이 아닌 경우 예외 발생
" +
+ "[Response Body]
" +
+ "등록된 reportId
")
+ @PostMapping("api/report/newStore/v1")
+ public Result saveNewStoreReport(@Valid @RequestBody SaveNewStoreReportRequest saveNewStoreReportRequest) {
+ Long reportId = reportService.saveNewStoreReport(saveNewStoreReportRequest);
+ return new Result<>(Result.CODE_SUCCESS, Result.MESSAGE_OK, reportId);
+ }
+
+ //사용자의 특정 가게에 대한 수정, 삭제 요청 정보를 저장
+ @Tag(name = "[New] 사용자 제보")
+ @Operation(summary = "사용자의 특정 가게에 대한 정보 수정 혹은 삭제 요청에 대한 제보를 서버에 저장", description = "사용자의 특정 가게에 대한 정보 수정 혹은 삭제 요청에 대한 제보를 서버에 저장합니다.
" +
+ "[Request Body]
" +
+ "dtype: 제보 종류를 구분하기 위한 값. fix는 수정 요청, del은 삭제 요청. fix나 del이 아닌 경우 예외 발생
" +
+ "storeId: 수정 혹은 삭제를 요청하는 가게 id
" +
+ "contents: 제보 내용
" +
+ "[Response Body]
" +
+ "등록된 reportId
")
+ @PostMapping("api/report/specificStore/v1")
+ public Result saveSpecificStoreReport(@Valid @RequestBody SaveSpecificStoreReportRequest saveSpecificStoreReportRequest) {
+ Long reportId = reportService.saveSpecificStoreReport(saveSpecificStoreReportRequest);
+ return new Result<>(Result.CODE_SUCCESS, Result.MESSAGE_OK, reportId);
+ }
+
+ //reportId를 가지고 사용자 제보 내용 조회
+ @Tag(name = "[New] 사용자 제보")
+ @Operation(summary = "reportId를 가지고 사용자 제보 내용 조회", description = "reportId를 가지고 사용자 제보 내용을 조회합니다.
" +
+ "[Request Body]
" +
+ "reportId: 검색할 사용자 제보의 reportId. 유효하지 않은 reportId의 경우 예외 발생
" +
+ "[Response Body]
" +
+ "해당 reportId로 검색된 사용자 제보 내용
")
+ @GetMapping("api/report/byId/v1")
+ public Result findById(@NotNull @RequestParam(value = "reportId") Long reportId) {
+ Report report = reportService.findById(reportId);
+ return new Result<>(Result.CODE_SUCCESS, Result.MESSAGE_OK, report);
+ }
+
+ //DB에 있는 모든 사용자 제보 내용 조회
+ @Tag(name = "[New] 사용자 제보")
+ @Operation(summary = "DB에 있는 모든 사용자 제보 내용 조회", description = "DB에 있는 모든 사용자 제보 내용을 조회합니다.
" +
+ "[Response Body]
" +
+ "DB에 있는 모든 사용자 제보 내용
")
+ @GetMapping("api/report/all/v1")
+ public Result> findAll() {
+ List reports = reportService.findAll();
+ return new Result<>(Result.CODE_SUCCESS, Result.MESSAGE_OK, reports);
+ }
+}
diff --git a/src/main/java/com/nainga/nainga/domain/report/application/ReportService.java b/src/main/java/com/nainga/nainga/domain/report/application/ReportService.java
new file mode 100644
index 00000000..c06197b2
--- /dev/null
+++ b/src/main/java/com/nainga/nainga/domain/report/application/ReportService.java
@@ -0,0 +1,76 @@
+package com.nainga.nainga.domain.report.application;
+
+import com.nainga.nainga.domain.report.dao.ReportRepository;
+import com.nainga.nainga.domain.report.domain.DelSpecificStoreReport;
+import com.nainga.nainga.domain.report.domain.FixSpecificStoreReport;
+import com.nainga.nainga.domain.report.domain.NewStoreReport;
+import com.nainga.nainga.domain.report.domain.Report;
+import com.nainga.nainga.domain.report.dto.SaveNewStoreReportRequest;
+import com.nainga.nainga.domain.report.dto.SaveSpecificStoreReportRequest;
+import com.nainga.nainga.global.exception.GlobalException;
+import com.nainga.nainga.global.exception.ReportErrorCode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Optional;
+
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class ReportService {
+ private final ReportRepository reportRepository;
+
+ @Transactional
+ public Long saveNewStoreReport(SaveNewStoreReportRequest saveNewStoreReportRequest) throws GlobalException { //사용자의 신규 가게 등록 요청
+ List certificationList = List.of("착한가격업소", "모범음식점", "안심식당"); //현재 App에서 사용중인 Certification 목록
+ for (String certification : saveNewStoreReportRequest.getCertifications()) {
+ if (!certificationList.contains(certification)) {
+ throw new GlobalException(ReportErrorCode.INVALID_CERTIFICATION); //잘못된 인증제 값이 들어온 것이므로 예외 발생
+ }
+ }
+
+ NewStoreReport newStoreReport = NewStoreReport.builder()
+ .storeName(saveNewStoreReportRequest.getStoreName())
+ .formattedAddress(saveNewStoreReportRequest.getFormattedAddress())
+ .certifications(saveNewStoreReportRequest.getCertifications())
+ .build();
+
+ return reportRepository.save(newStoreReport);
+ }
+
+ @Transactional
+ public Long saveSpecificStoreReport(SaveSpecificStoreReportRequest saveSpecificStoreReportRequest) throws GlobalException { //사용자의 특정 가게에 대한 수정, 삭제 요청
+ if (saveSpecificStoreReportRequest.getDtype().equals("fix")) {
+ FixSpecificStoreReport fixSpecificStoreReport = FixSpecificStoreReport.builder()
+ .storeId(saveSpecificStoreReportRequest.getStoreId())
+ .contents(saveSpecificStoreReportRequest.getContents())
+ .build();
+
+ return reportRepository.save(fixSpecificStoreReport);
+ } else if (saveSpecificStoreReportRequest.getDtype().equals("del")) {
+ DelSpecificStoreReport delSpecificStoreReport = DelSpecificStoreReport.builder()
+ .storeId(saveSpecificStoreReportRequest.getStoreId())
+ .contents(saveSpecificStoreReportRequest.getContents())
+ .build();
+
+ return reportRepository.save(delSpecificStoreReport);
+ } else {
+ throw new GlobalException(ReportErrorCode.INVALID_DTYPE); //잘못된 DTYPE이 들어왔을 경우에 Custom GlobalException 처리
+ }
+ }
+
+ public Report findById(Long id) throws GlobalException { //reportId를 가지고 report를 DB에서 조회하는 로직
+ Optional report = reportRepository.findById(id);
+ if (report.isEmpty()) {
+ throw new GlobalException(ReportErrorCode.INVALID_REPORT_ID); //잘못된 reportId로 검색하는 경우에 Custom GlobalException 처리
+ } else {
+ return report.get();
+ }
+ }
+
+ public List findAll() { //DB에 있는 모든 Report 조회
+ return reportRepository.findAll();
+ }
+}
diff --git a/src/main/java/com/nainga/nainga/domain/report/dao/ReportRepository.java b/src/main/java/com/nainga/nainga/domain/report/dao/ReportRepository.java
new file mode 100644
index 00000000..5b6c3982
--- /dev/null
+++ b/src/main/java/com/nainga/nainga/domain/report/dao/ReportRepository.java
@@ -0,0 +1,33 @@
+package com.nainga.nainga.domain.report.dao;
+
+import com.nainga.nainga.domain.report.domain.Report;
+import jakarta.persistence.EntityManager;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+@RequiredArgsConstructor
+public class ReportRepository {
+
+ private final EntityManager em;
+
+ public Long save(Report report) {
+ em.persist(report);
+ return report.getId();
+ }
+
+ public Optional findById(Long id) {
+ List result = em.createQuery("select r from Report r where r.id = :id", Report.class)
+ .setParameter("id", id)
+ .getResultList();
+ return result.stream().findAny();
+ }
+
+ public List findAll() {
+ return em.createQuery("select r from Report r", Report.class)
+ .getResultList();
+ }
+}
diff --git a/src/main/java/com/nainga/nainga/domain/report/domain/DelSpecificStoreReport.java b/src/main/java/com/nainga/nainga/domain/report/domain/DelSpecificStoreReport.java
new file mode 100644
index 00000000..a2649b52
--- /dev/null
+++ b/src/main/java/com/nainga/nainga/domain/report/domain/DelSpecificStoreReport.java
@@ -0,0 +1,16 @@
+package com.nainga.nainga.domain.report.domain;
+
+import jakarta.persistence.DiscriminatorValue;
+import jakarta.persistence.Entity;
+import lombok.*;
+
+@Entity
+@Getter
+@Builder
+@DiscriminatorValue("del")
+@AllArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class DelSpecificStoreReport extends Report {
+ private Long storeId; //가게 id
+ private String contents; //신고 내용
+}
diff --git a/src/main/java/com/nainga/nainga/domain/report/domain/FixSpecificStoreReport.java b/src/main/java/com/nainga/nainga/domain/report/domain/FixSpecificStoreReport.java
new file mode 100644
index 00000000..19d4b088
--- /dev/null
+++ b/src/main/java/com/nainga/nainga/domain/report/domain/FixSpecificStoreReport.java
@@ -0,0 +1,16 @@
+package com.nainga.nainga.domain.report.domain;
+
+import jakarta.persistence.DiscriminatorValue;
+import jakarta.persistence.Entity;
+import lombok.*;
+
+@Entity
+@Getter
+@Builder
+@DiscriminatorValue("fix")
+@AllArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class FixSpecificStoreReport extends Report {
+ private Long storeId; //가게 id
+ private String contents; //신고 내용
+}
diff --git a/src/main/java/com/nainga/nainga/domain/report/domain/NewStoreReport.java b/src/main/java/com/nainga/nainga/domain/report/domain/NewStoreReport.java
new file mode 100644
index 00000000..af19f012
--- /dev/null
+++ b/src/main/java/com/nainga/nainga/domain/report/domain/NewStoreReport.java
@@ -0,0 +1,21 @@
+package com.nainga.nainga.domain.report.domain;
+
+import jakarta.persistence.DiscriminatorValue;
+import jakarta.persistence.ElementCollection;
+import jakarta.persistence.Entity;
+import lombok.*;
+
+import java.util.List;
+
+@Entity
+@Getter
+@Builder
+@DiscriminatorValue("new")
+@AllArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class NewStoreReport extends Report {
+ private String storeName; //가게 이름
+ private String formattedAddress; //가게 주소
+ @ElementCollection
+ private List certifications; //가게가 가지고 있는 인증제 이름 리스트
+}
diff --git a/src/main/java/com/nainga/nainga/domain/report/domain/Report.java b/src/main/java/com/nainga/nainga/domain/report/domain/Report.java
new file mode 100644
index 00000000..71c03f7d
--- /dev/null
+++ b/src/main/java/com/nainga/nainga/domain/report/domain/Report.java
@@ -0,0 +1,16 @@
+package com.nainga.nainga.domain.report.domain;
+
+import jakarta.persistence.*;
+import lombok.*;
+
+@Entity
+@Getter
+@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
+@DiscriminatorColumn(name = "dtype")
+public abstract class Report {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "report_id")
+ private Long id;
+}
diff --git a/src/main/java/com/nainga/nainga/domain/report/dto/SaveNewStoreReportRequest.java b/src/main/java/com/nainga/nainga/domain/report/dto/SaveNewStoreReportRequest.java
new file mode 100644
index 00000000..64ae7ee0
--- /dev/null
+++ b/src/main/java/com/nainga/nainga/domain/report/dto/SaveNewStoreReportRequest.java
@@ -0,0 +1,27 @@
+package com.nainga.nainga.domain.report.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class SaveNewStoreReportRequest {
+ @NotEmpty
+ @Schema(defaultValue = "가게 이름", description = "새로 등록하고자 하는 가게 이름")
+ private String storeName; //가게 이름
+ @NotEmpty
+ @Schema(defaultValue = "주소", description = "새로 등록하고자 하는 가게 주소")
+ private String formattedAddress; //가게 주소
+ @NotEmpty
+ @Schema(defaultValue = "[\"착한가격업소\", \"모범음식점\", \"안심식당\"]", description = "새로 등록할 가게가 가지고 있는 인증제들의 이름을 담은 리스트. 착한가격업소, 모범음식점, 안심식당이 아닌 경우 예외 발생")
+ private List certifications; //가게가 가지고 있는 인증제 이름 리스트
+
+ public SaveNewStoreReportRequest(String storeName, String formattedAddress, List certifications) {
+ this.storeName = storeName;
+ this.formattedAddress = formattedAddress;
+ this.certifications = certifications;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/nainga/nainga/domain/report/dto/SaveSpecificStoreReportRequest.java b/src/main/java/com/nainga/nainga/domain/report/dto/SaveSpecificStoreReportRequest.java
new file mode 100644
index 00000000..9f251106
--- /dev/null
+++ b/src/main/java/com/nainga/nainga/domain/report/dto/SaveSpecificStoreReportRequest.java
@@ -0,0 +1,25 @@
+package com.nainga.nainga.domain.report.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class SaveSpecificStoreReportRequest {
+ @NotEmpty
+ @Schema(defaultValue = "fix/del", description = "제보 종류를 구분하기 위한 값. fix는 수정 요청, del은 삭제 요청.")
+ private String dtype; //Report 종류를 구분하기 위한 type
+ @NotNull
+ @Schema(defaultValue = "0", description = "수정 혹은 삭제를 요청하는 가게 id")
+ private Long storeId; //가게 id
+ @NotEmpty
+ @Schema(defaultValue = "제보 내용", description = "제보 내용")
+ private String contents; //신고 내용
+
+ public SaveSpecificStoreReportRequest(String dtype, Long storeId, String contents) {
+ this.dtype = dtype;
+ this.storeId = storeId;
+ this.contents = contents;
+ }
+}
diff --git a/src/main/java/com/nainga/nainga/domain/store/api/StoreApi.java b/src/main/java/com/nainga/nainga/domain/store/api/StoreApi.java
index a9a8b5af..dc5177fc 100644
--- a/src/main/java/com/nainga/nainga/domain/store/api/StoreApi.java
+++ b/src/main/java/com/nainga/nainga/domain/store/api/StoreApi.java
@@ -3,9 +3,11 @@
import com.nainga.nainga.domain.store.application.GoodPriceGoogleMapStoreService;
import com.nainga.nainga.domain.store.application.MobeomGoogleMapStoreService;
import com.nainga.nainga.domain.store.application.SafeGoogleMapStoreService;
+import com.nainga.nainga.domain.store.application.StoreService;
import com.nainga.nainga.domain.store.dto.CreateDividedGoodPriceStoresResponse;
import com.nainga.nainga.domain.store.dto.CreateDividedMobeomStoresResponse;
import com.nainga.nainga.domain.store.dto.CreateDividedSafeStoresResponse;
+import com.nainga.nainga.domain.storecertification.dto.StoreCertificationsByLocationResponse;
import com.nainga.nainga.global.util.Result;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
@@ -15,12 +17,15 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
+import java.util.List;
+
@RestController
@RequiredArgsConstructor
public class StoreApi {
private final MobeomGoogleMapStoreService mobeomGoogleMapStoreService;
private final SafeGoogleMapStoreService safeGoogleMapStoreService;
private final GoodPriceGoogleMapStoreService goodPriceGoogleMapStoreService;
+ private final StoreService storeService;
@Hidden
@Tag(name = "초기 Data 생성")
@@ -78,5 +83,18 @@ public Result createDividedGoodPriceStores
System.out.println("response = " + response); //편하게 콘솔 로그에서 확인하기 위한 용도
return new Result<>(Result.CODE_SUCCESS, Result.MESSAGE_OK, response);
}
+
+ //검색어를 이용해 가게 이름에 대해 검색하여 나온 검색 결과를 바탕으로 검색어를 자동 완성해서 최대 10개의 자동 완성된 검색어를 리턴
+ @Tag(name = "[New] 검색어 자동 완성")
+ @Operation(summary = "사용자의 검색 키워드를 바탕으로 검색어 자동 완성", description = "사용자의 검색 키워드를 바탕으로 DB에서 매칭되는 가게 이름을 조회하여 최대 10개까지 검색어를 자동으로 완성하여 반환해줍니다.
" +
+ "[Request Body]
" +
+ "searchKeyword: 사용자의 검색 키워드
" +
+ "[Response Body]
" +
+ "자동으로 완성된 최대 10개의 검색어
")
+ @GetMapping("api/store/autocorrect/v1")
+ public Result> autocorrect(@RequestParam(value = "searchKeyword") String searchKeyword) {
+ List autocorrectResult = storeService.autocorrect(searchKeyword);
+ return new Result<>(Result.CODE_SUCCESS, Result.MESSAGE_OK, autocorrectResult);
+ }
}
diff --git a/src/main/java/com/nainga/nainga/domain/store/application/StoreService.java b/src/main/java/com/nainga/nainga/domain/store/application/StoreService.java
new file mode 100644
index 00000000..ed025544
--- /dev/null
+++ b/src/main/java/com/nainga/nainga/domain/store/application/StoreService.java
@@ -0,0 +1,100 @@
+package com.nainga.nainga.domain.store.application;
+
+import com.nainga.nainga.domain.store.dao.StoreRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class StoreService {
+ private final StoreRepository storeRepository;
+
+ public List autocorrect(String keyword) { //검색어 자동 완성 로직
+ return storeRepository.findAllBySearchKeyword(keyword);
+ }
+
+// private final RedisSortedSetService redisSortedSetService;
+// private final RedisHashService redisHashService;
+
+ /*
+ Redis Hash 자료 구조를 활용한 새로운 검색어 자동 완성 로직입니다.
+ 아래 로직은 Case insensitive하게 검색될 수 있도록 구현하였습니다.
+ 지금은 MySQL이 성능이 더 좋아서 사용하지 않습니다.
+ */
+
+// @PostConstruct
+// public void init() { //이 Service Bean이 생성된 이후에 검색어 자동 완성 기능을 위한 데이터들을 Redis에 저장 (Redis는 인메모리 DB라 휘발성을 띄기 때문)
+// redisHashService.removeAllOfHash();
+// saveAllDisplayName(storeRepository.findAllDisplayName()); //모든 가게명을 소문자로 변환한 것을 field, 원래 가게 이름을 value로 매핑시켜서 Redis Hash에 저장
+// }
+
+// public void saveAllDisplayName(List allDisplayName) {
+// ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); //병렬 처리를 위한 스레드풀을 생성하는 과정
+//
+// for (String displayName : allDisplayName) {
+// executorService.submit(() -> { //submit 메서드를 사용해서 병렬 처리할 작업 추가
+// //Redis 내장 기능이 아닌 case insensitive 조회를 구현하기 위해 소문자로 변환한 field값과 원래 string인 value의 쌍을 Hash에 저장
+// redisHashService.addToHash(displayName.toLowerCase(), displayName); //case insensitive하게 검색어 자동 완성 기능을 직접 구현하기 위해 소문자로 통일해서 저장
+// });
+// }
+// executorService.shutdown(); //작업이 모두 완료되면 스레드풀을 종료
+// }
+
+// public List autocorrect(String keyword) { //검색어 자동 완성 로직
+// Set allValuesContainingSearchKeyword = redisHashService.findAllValuesContainingSearchKeyword(keyword); //case insensitive하게 serachKeyword를 포함하는 가게 이름 최대 10개 반환
+// if(allValuesContainingSearchKeyword.isEmpty())
+// return new ArrayList<>();
+// else
+// return new ArrayList<>(allValuesContainingSearchKeyword); //자동 완성 결과가 존재하면 ArrayList로 변환하여 리턴
+// }
+
+ /*
+ 아래 주석처리 된 코드는 초기 검색어 자동 완성 로직입니다.
+ 검색어 자동 완성에 대한 요구 사항이 앞에서부터 매칭되는 글자가 아닌 Contains 개념으로 바뀌어서 주석 처리하여 임시로 남겨놓았습니다.
+ 지금은 MySQL이 성능이 더 좋기 때문에 사용하지 않는 코드입니다.
+ */
+
+// private String suffix = "*"; //검색어 자동 완성 기능에서 실제 노출될 수 있는 완벽한 형태의 단어를 구분하기 위한 접미사
+// private int maxSize = 10; //검색어 자동 완성 기능 최대 개수
+//
+// @PostConstruct
+// public void init() { //이 Service Bean이 생성된 이후에 검색어 자동 완성 기능을 위한 데이터들을 Redis에 저장 (Redis는 인메모리 DB라 휘발성을 띄기 때문)
+// redisSortedSetService.removeAllOfSortedSet();
+// saveAllSubstring(storeRepository.findAllDisplayName()); //MySQL DB에 저장된 모든 가게명을 음절 단위로 잘라 모든 Substring을 Redis에 저장해주는 로직
+// }
+//
+// public void saveAllSubstring(List allDisplayName) {
+// ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); //병렬 처리를 위한 스레드풀을 생성하는 과정
+//
+// for (String displayName : allDisplayName) {
+// executorService.submit(() -> { //submit 메서드를 사용해서 병렬 처리할 작업 추가
+// redisSortedSetService.addToSortedSet(displayName + suffix);
+//
+// for (int i = displayName.length(); i > 0; --i) {
+// redisSortedSetService.addToSortedSet(displayName.substring(0, i));
+// }
+// });
+// }
+// executorService.shutdown(); //작업이 모두 완료되면 스레드풀을 종료
+// }
+//
+// public List autocorrect(String keyword) { //검색어 자동 완성 기능 관련 로직
+// Long index = redisSortedSetService.findFromSortedSet(keyword); //사용자가 입력한 검색어를 바탕으로 Redis에서 조회한 결과 매칭되는 index
+//
+// if (index == null) {
+// return new ArrayList<>(); //만약 사용자 검색어 바탕으로 자동 완성 검색어를 만들 수 없으면 Empty Array 리턴
+// }
+//
+// Set allValuesAfterIndexFromSortedSet = redisSortedSetService.findAllValuesAfterIndexFromSortedSet(index); //사용자 검색어 이후로 정렬된 Redis 데이터들 가져오기
+//
+// return allValuesAfterIndexFromSortedSet.stream()
+// .filter(value -> value.endsWith(suffix) && value.startsWith(keyword))
+// .map(value -> StringUtils.removeEnd(value, suffix))
+// .limit(maxSize)
+// .toList(); //자동 완성을 통해 만들어진 최대 maxSize개의 키워드들
+// }
+}
diff --git a/src/main/java/com/nainga/nainga/domain/store/dao/StoreRepository.java b/src/main/java/com/nainga/nainga/domain/store/dao/StoreRepository.java
index fb5be1d9..b6ead007 100644
--- a/src/main/java/com/nainga/nainga/domain/store/dao/StoreRepository.java
+++ b/src/main/java/com/nainga/nainga/domain/store/dao/StoreRepository.java
@@ -43,4 +43,16 @@ public List findAll() {
return em.createQuery("select s from Store s", Store.class)
.getResultList();
}
+
+ public List findAllDisplayName() {
+ return em.createQuery("select s.displayName from Store s", String.class)
+ .getResultList();
+ }
+
+ public List findAllBySearchKeyword(String searchKeyword) {
+ return em.createQuery("select s.displayName from Store s where s.displayName like concat('%', :searchKeyword, '%')", String.class)
+ .setParameter("searchKeyword", searchKeyword)
+ .setMaxResults(10)
+ .getResultList();
+ }
}
diff --git a/src/main/java/com/nainga/nainga/global/application/RedisHashService.java b/src/main/java/com/nainga/nainga/global/application/RedisHashService.java
new file mode 100644
index 00000000..61e322c9
--- /dev/null
+++ b/src/main/java/com/nainga/nainga/global/application/RedisHashService.java
@@ -0,0 +1,54 @@
+//package com.nainga.nainga.global.application;
+//
+//import org.springframework.data.redis.core.Cursor;
+//import org.springframework.data.redis.core.HashOperations;
+//import org.springframework.data.redis.core.RedisTemplate;
+//import org.springframework.data.redis.core.ScanOptions;
+//import org.springframework.stereotype.Service;
+//
+//import java.util.HashSet;
+//import java.util.Map;
+//import java.util.Set;
+//import java.util.TreeSet;
+//
+//@Service
+//public class RedisHashService {
+// private final HashOperations hashOperations;
+// private final RedisTemplate redisTemplate;
+//
+// public RedisHashService(RedisTemplate redisTemplate) {
+// this.hashOperations = redisTemplate.opsForHash();
+// this.redisTemplate = redisTemplate;
+// }
+//
+// private String key = "autocorrect"; //검색어 자동 완성을 위한 Redis 데이터
+//
+// //Hash에 field-value 쌍을 추가하는 메서드
+// public void addToHash(String field, String value) {
+// hashOperations.put(key, field, value);
+// }
+//
+// public Set findAllValuesContainingSearchKeyword(String searchKeyword) {
+// //Redis에서는 case insensitive한 검색을 지원하는 내장 모듈이 없으므로 searchKeyword는 모두 소문자로 통일하여 검색하도록 구현
+// //당연히 초기 Redis에 field를 저장할 때도 모두 소문자로 변형하여 저장했고 원본 문자열은 value에 저장!
+// Set result = new HashSet<>(); //searchKeyword를 포함하는 원래 가게 이름들의 리스트. 최대 maxSize개까지 저장. 중복 허용하지 않고, 자동 사전순 정렬하기 위해 사용
+// final int maxSize = 10; //최대 검색어 자동 완성 개수
+//
+// ScanOptions scanOptions = ScanOptions.scanOptions().match("*" + searchKeyword + "*").build(); //searchKeyword를 포함하는지를 검사하기 위한 scanOption
+// Cursor> cursor = hashOperations.scan(key, scanOptions); //기존 Redis Keys 로직의 성능 이슈를 해결하기 위해 10개 단위로 끊어서 조회하는 Scan 기능 사용
+//
+// while (cursor.hasNext()) { //끊어서 조회하다보니 while loop로 조회
+// Map.Entry entry = cursor.next();
+// result.add(entry.getValue());
+//
+// if(result.size() >= maxSize) //maxSize에 도달하면 scan 중단
+// break;
+// }
+// cursor.close();
+// return result;
+// }
+//
+// public void removeAllOfHash() {
+// redisTemplate.delete(key);
+// }
+//}
diff --git a/src/main/java/com/nainga/nainga/global/application/RedisSortedSetService.java b/src/main/java/com/nainga/nainga/global/application/RedisSortedSetService.java
new file mode 100644
index 00000000..52a575b9
--- /dev/null
+++ b/src/main/java/com/nainga/nainga/global/application/RedisSortedSetService.java
@@ -0,0 +1,34 @@
+//package com.nainga.nainga.global.application;
+//
+//import lombok.RequiredArgsConstructor;
+//import org.springframework.data.redis.core.RedisCallback;
+//import org.springframework.data.redis.core.RedisTemplate;
+//import org.springframework.data.redis.core.ZSetOperations;
+//import org.springframework.stereotype.Service;
+//
+//import java.util.List;
+//import java.util.Set;
+//
+//@Service
+//@RequiredArgsConstructor
+//public class RedisSortedSetService { //검색어 자동 완성을 구현할 때 사용하는 Redis의 SortedSet 관련 서비스 레이어
+// private final RedisTemplate redisTemplate;
+// private String key = "autocorrect"; //검색어 자동 완성을 위한 Redis 데이터
+// private int score = 0; //Score는 딱히 필요 없으므로 하나로 통일
+//
+// public void addToSortedSet(String value) { //Redis SortedSet에 추가
+// redisTemplate.opsForZSet().add(key, value, score);
+// }
+//
+// public Long findFromSortedSet(String value) { //Redis SortedSet에서 Value를 찾아 인덱스를 반환
+// return redisTemplate.opsForZSet().rank(key, value);
+// }
+//
+// public Set findAllValuesAfterIndexFromSortedSet(Long index) {
+// return redisTemplate.opsForZSet().range(key, index, index + 200); //전체를 다 불러오기 보다는 200개 정도만 가져와도 자동 완성을 구현하는 데 무리가 없으므로 200개로 rough하게 설정
+// }
+//
+// public void removeAllOfSortedSet() {
+// redisTemplate.opsForZSet().removeRange(key, 0, -1);
+// }
+//}
diff --git a/src/main/java/com/nainga/nainga/global/exception/ReportErrorCode.java b/src/main/java/com/nainga/nainga/global/exception/ReportErrorCode.java
new file mode 100644
index 00000000..b2603b79
--- /dev/null
+++ b/src/main/java/com/nainga/nainga/global/exception/ReportErrorCode.java
@@ -0,0 +1,19 @@
+package com.nainga.nainga.global.exception;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+
+/*
+ Report와 관련된 도메인에서 발생할 수 있는 ErrorCode를 정의합니다.
+ */
+@Getter
+@RequiredArgsConstructor
+public enum ReportErrorCode implements ErrorCode {
+ INVALID_DTYPE(HttpStatus.NOT_FOUND, "There is a wrong dtype. You can only use a dtype such as fix or del."),
+ INVALID_CERTIFICATION(HttpStatus.NOT_FOUND, "There is a wrong certification. You can only use certifications such as 착한가격업소, 모범음식점, 안심식당."),
+ INVALID_REPORT_ID(HttpStatus.NOT_FOUND, "There is a wrong reportId.");
+
+ private final HttpStatus httpStatus;
+ private final String message;
+}
diff --git a/src/main/resources/backend-submodule b/src/main/resources/backend-submodule
index 864e24ee..c741a715 160000
--- a/src/main/resources/backend-submodule
+++ b/src/main/resources/backend-submodule
@@ -1 +1 @@
-Subproject commit 864e24eecb92e71b9c8988e9684e4faf9802dcd8
+Subproject commit c741a71530d128886428736a347039c2d406bebc
diff --git a/src/test/java/com/nainga/nainga/domain/report/application/ReportServiceTest.java b/src/test/java/com/nainga/nainga/domain/report/application/ReportServiceTest.java
new file mode 100644
index 00000000..73ff0101
--- /dev/null
+++ b/src/test/java/com/nainga/nainga/domain/report/application/ReportServiceTest.java
@@ -0,0 +1,109 @@
+package com.nainga.nainga.domain.report.application;
+
+import com.nainga.nainga.domain.report.dao.ReportRepository;
+import com.nainga.nainga.domain.report.domain.DelSpecificStoreReport;
+import com.nainga.nainga.domain.report.domain.FixSpecificStoreReport;
+import com.nainga.nainga.domain.report.domain.NewStoreReport;
+import com.nainga.nainga.domain.report.domain.Report;
+import com.nainga.nainga.domain.report.dto.SaveNewStoreReportRequest;
+import com.nainga.nainga.domain.report.dto.SaveSpecificStoreReportRequest;
+import com.nainga.nainga.global.exception.GlobalException;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+@SpringBootTest
+@Transactional
+class ReportServiceTest {
+ @Autowired
+ private ReportRepository reportRepository;
+
+ @Autowired
+ private ReportService reportService;
+
+ @Test
+ public void saveNewStoreReport() throws Exception {
+ //given
+ SaveNewStoreReportRequest saveNewStoreReportRequest1 = new SaveNewStoreReportRequest("가게1", "주소1", List.of("착한가격업소", "모범음식점")); //정상적인 테스트 케이스
+ SaveNewStoreReportRequest saveNewStoreReportRequest2 = new SaveNewStoreReportRequest("가게2", "주소2", List.of("착한가격업")); //인증제 이름이 잘못되었을 때
+
+ //when
+ Long report1Id = reportService.saveNewStoreReport(saveNewStoreReportRequest1);
+ NewStoreReport report = (NewStoreReport) reportService.findById(report1Id);
+
+ //then
+ assertArrayEquals(report.getCertifications().toArray(), saveNewStoreReportRequest1.getCertifications().toArray());
+ assertThat(report.getStoreName()).isEqualTo(saveNewStoreReportRequest1.getStoreName());
+ assertThat(report.getFormattedAddress()).isEqualTo(saveNewStoreReportRequest1.getFormattedAddress());
+ assertThatThrownBy(() -> reportService.saveNewStoreReport(saveNewStoreReportRequest2)) //잘못된 인증제라서 예외가 터져야함
+ .isInstanceOf(GlobalException.class);
+ }
+
+ @Test
+ public void saveSpecificStoreReport() throws Exception {
+ //given
+ SaveSpecificStoreReportRequest saveSpecificStoreReportRequest1 = new SaveSpecificStoreReportRequest("del", 123L, "내용1"); //정상적인 테스트 케이스
+ SaveSpecificStoreReportRequest saveSpecificStoreReportRequest2 = new SaveSpecificStoreReportRequest("fix", 1234L, "내용2"); //정상적인 테스트 케이스
+ SaveSpecificStoreReportRequest saveSpecificStoreReportRequest3 = new SaveSpecificStoreReportRequest("xxx", 12345L, "내용3"); //잘못된 dtype
+
+ //when
+ Long report1Id = reportService.saveSpecificStoreReport(saveSpecificStoreReportRequest1);
+ Long report2Id = reportService.saveSpecificStoreReport(saveSpecificStoreReportRequest2);
+ DelSpecificStoreReport report1 = (DelSpecificStoreReport) reportService.findById(report1Id);
+ FixSpecificStoreReport report2 = (FixSpecificStoreReport) reportService.findById(report2Id);
+
+ //then
+ assertThat(report1.getStoreId()).isEqualTo(saveSpecificStoreReportRequest1.getStoreId());
+ assertThat(report2.getContents()).isEqualTo(saveSpecificStoreReportRequest2.getContents());
+ assertThatThrownBy(() -> reportService.saveSpecificStoreReport(saveSpecificStoreReportRequest3)) //잘못된 dtype이라서 예외가 터져야함
+ .isInstanceOf(GlobalException.class);
+ }
+
+ @Test
+ public void findById() throws Exception {
+ //given
+ SaveNewStoreReportRequest saveNewStoreReportRequest1 = new SaveNewStoreReportRequest("가게1", "주소1", List.of("착한가격업소", "모범음식점")); //정상적인 테스트 케이스
+ SaveSpecificStoreReportRequest saveSpecificStoreReportRequest1 = new SaveSpecificStoreReportRequest("del", 123L, "내용1"); //정상적인 테스트 케이스
+
+ //when
+ Long report1Id = reportService.saveNewStoreReport(saveNewStoreReportRequest1);
+ Long report2Id = reportService.saveSpecificStoreReport(saveSpecificStoreReportRequest1);
+ NewStoreReport report1 = (NewStoreReport) reportService.findById(report1Id);
+ DelSpecificStoreReport report2 = (DelSpecificStoreReport) reportService.findById(report2Id);
+
+ //then
+ assertThat(report1.getFormattedAddress()).isEqualTo(saveNewStoreReportRequest1.getFormattedAddress());
+ assertArrayEquals(report1.getCertifications().toArray(), saveNewStoreReportRequest1.getCertifications().toArray());
+ assertThat(report1.getStoreName()).isEqualTo(saveNewStoreReportRequest1.getStoreName());
+ assertThat(report2.getContents()).isEqualTo(saveSpecificStoreReportRequest1.getContents());
+ assertThat(report2.getStoreId()).isEqualTo(saveSpecificStoreReportRequest1.getStoreId());
+ }
+
+ @Test
+ public void findAll() throws Exception {
+ //given
+ SaveNewStoreReportRequest saveNewStoreReportRequest1 = new SaveNewStoreReportRequest("가게1", "주소1", List.of("착한가격업소", "모범음식점")); //정상적인 테스트 케이스
+ SaveSpecificStoreReportRequest saveSpecificStoreReportRequest1 = new SaveSpecificStoreReportRequest("del", 123L, "내용1"); //정상적인 테스트 케이스
+
+ //when
+ Long report1Id = reportService.saveNewStoreReport(saveNewStoreReportRequest1);
+ Long report2Id = reportService.saveSpecificStoreReport(saveSpecificStoreReportRequest1);
+
+ List reports = reportService.findAll();
+ List reportIds = reports.stream().map(Report::getId).toList();
+
+ //then
+ assertArrayEquals(reportIds.toArray(), List.of(report1Id, report2Id).toArray());
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/nainga/nainga/domain/store/application/StoreServiceTest.java b/src/test/java/com/nainga/nainga/domain/store/application/StoreServiceTest.java
new file mode 100644
index 00000000..cf87f9c0
--- /dev/null
+++ b/src/test/java/com/nainga/nainga/domain/store/application/StoreServiceTest.java
@@ -0,0 +1,49 @@
+package com.nainga.nainga.domain.store.application;
+
+import com.nainga.nainga.domain.store.dao.StoreRepository;
+import com.nainga.nainga.domain.store.domain.Store;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@SpringBootTest
+@Transactional
+class StoreServiceTest {
+
+ @Autowired
+ StoreService storeService;
+
+ @Autowired
+ StoreRepository storeRepository;
+
+ @Test
+ public void autocorrect() throws Exception { //검색어 자동 완성 기능에 대한 테스트
+ //given
+ Store store1 = Store.builder()
+ .displayName("^")
+ .build();
+ Store store2 = Store.builder()
+ .displayName("^^")
+ .build();
+ Store store3 = Store.builder()
+ .displayName("^*^")
+ .build();
+ storeRepository.save(store1);
+ storeRepository.save(store2);
+ storeRepository.save(store3);
+
+ //when
+ List result1 = storeService.autocorrect("^");
+ List result2 = storeService.autocorrect("^^");
+
+ //then
+ assertArrayEquals(result1.toArray(), List.of("^", "^^", "^*^").toArray());
+ assertArrayEquals(result2.toArray(), List.of("^^").toArray());
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/backend-submodule b/src/test/resources/backend-submodule
index 732770ba..c741a715 160000
--- a/src/test/resources/backend-submodule
+++ b/src/test/resources/backend-submodule
@@ -1 +1 @@
-Subproject commit 732770ba3e8a25abfb4212a7a75175249e2c099e
+Subproject commit c741a71530d128886428736a347039c2d406bebc