Skip to content

Commit

Permalink
Merge pull request #112 from Korea-Certified-Store/develop
Browse files Browse the repository at this point in the history
Develop to Main 릴리즈 (#111)
sungjindev authored Feb 21, 2024
2 parents 2deeaf9 + 2f9682c commit a8452af
Showing 21 changed files with 707 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/action-develop-cd.yml
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ on:
branches:
- develop

# 코드의 내용을 이 파일을 실행하여 action을 수행하는 주체(Github Actions에서 사용하는 VM)가 읽을 수 있도록 권한을 설정
# 코드의 내용을 이 파일을 실행하여 action을 수행하는 주체(Github Actions에서 사용하는 VM)가 읽을 수 있도록 권한을 설정
permissions:
contents: read

5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -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 {
75 changes: 75 additions & 0 deletions src/main/java/com/nainga/nainga/domain/report/api/ReportApi.java
Original file line number Diff line number Diff line change
@@ -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 = "사용자의 신규 가게 등록 요청에 대한 제보를 서버에 저장합니다.<br><br>" +
"[Request Body]<br>" +
"storeName: 등록 요청하는 가게 이름<br>" +
"formattedAddress: 등록 요청하는 가게 주소<br>" +
"certifications: 가게가 가지고 있는 인증제들의 이름을 담은 리스트. 착한가격업소, 모범음식점, 안심식당이 아닌 경우 예외 발생<br>" +
"[Response Body]<br>" +
"등록된 reportId<br>")
@PostMapping("api/report/newStore/v1")
public Result<Long> 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 = "사용자의 특정 가게에 대한 정보 수정 혹은 삭제 요청에 대한 제보를 서버에 저장합니다.<br><br>" +
"[Request Body]<br>" +
"dtype: 제보 종류를 구분하기 위한 값. fix는 수정 요청, del은 삭제 요청. fix나 del이 아닌 경우 예외 발생<br>" +
"storeId: 수정 혹은 삭제를 요청하는 가게 id<br>" +
"contents: 제보 내용<br>" +
"[Response Body]<br>" +
"등록된 reportId<br>")
@PostMapping("api/report/specificStore/v1")
public Result<Long> 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를 가지고 사용자 제보 내용을 조회합니다.<br><br>" +
"[Request Body]<br>" +
"reportId: 검색할 사용자 제보의 reportId. 유효하지 않은 reportId의 경우 예외 발생<br>" +
"[Response Body]<br>" +
"해당 reportId로 검색된 사용자 제보 내용<br>")
@GetMapping("api/report/byId/v1")
public Result<Report> 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에 있는 모든 사용자 제보 내용을 조회합니다.<br><br>" +
"[Response Body]<br>" +
"DB에 있는 모든 사용자 제보 내용<br>")
@GetMapping("api/report/all/v1")
public Result<List<Report>> findAll() {
List<Report> reports = reportService.findAll();
return new Result<>(Result.CODE_SUCCESS, Result.MESSAGE_OK, reports);
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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> report = reportRepository.findById(id);
if (report.isEmpty()) {
throw new GlobalException(ReportErrorCode.INVALID_REPORT_ID); //잘못된 reportId로 검색하는 경우에 Custom GlobalException 처리
} else {
return report.get();
}
}

public List<Report> findAll() { //DB에 있는 모든 Report 조회
return reportRepository.findAll();
}
}
Original file line number Diff line number Diff line change
@@ -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<Report> findById(Long id) {
List<Report> result = em.createQuery("select r from Report r where r.id = :id", Report.class)
.setParameter("id", id)
.getResultList();
return result.stream().findAny();
}

public List<Report> findAll() {
return em.createQuery("select r from Report r", Report.class)
.getResultList();
}
}
Original file line number Diff line number Diff line change
@@ -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; //신고 내용
}
Original file line number Diff line number Diff line change
@@ -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; //신고 내용
}
Original file line number Diff line number Diff line change
@@ -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<String> certifications; //가게가 가지고 있는 인증제 이름 리스트
}
16 changes: 16 additions & 0 deletions src/main/java/com/nainga/nainga/domain/report/domain/Report.java
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<String> certifications; //가게가 가지고 있는 인증제 이름 리스트

public SaveNewStoreReportRequest(String storeName, String formattedAddress, List<String> certifications) {
this.storeName = storeName;
this.formattedAddress = formattedAddress;
this.certifications = certifications;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/nainga/nainga/domain/store/api/StoreApi.java
Original file line number Diff line number Diff line change
@@ -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<CreateDividedGoodPriceStoresResponse> 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개까지 검색어를 자동으로 완성하여 반환해줍니다.<br><br>" +
"[Request Body]<br>" +
"searchKeyword: 사용자의 검색 키워드<br>" +
"[Response Body]<br>" +
"자동으로 완성된 최대 10개의 검색어<br>")
@GetMapping("api/store/autocorrect/v1")
public Result<List<String>> autocorrect(@RequestParam(value = "searchKeyword") String searchKeyword) {
List<String> autocorrectResult = storeService.autocorrect(searchKeyword);
return new Result<>(Result.CODE_SUCCESS, Result.MESSAGE_OK, autocorrectResult);
}
}

Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<String> autocorrect(String keyword) { //검색어 자동 완성 로직
// Set<String> 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<String> 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<String> autocorrect(String keyword) { //검색어 자동 완성 기능 관련 로직
// Long index = redisSortedSetService.findFromSortedSet(keyword); //사용자가 입력한 검색어를 바탕으로 Redis에서 조회한 결과 매칭되는 index
//
// if (index == null) {
// return new ArrayList<>(); //만약 사용자 검색어 바탕으로 자동 완성 검색어를 만들 수 없으면 Empty Array 리턴
// }
//
// Set<String> 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개의 키워드들
// }
}
Original file line number Diff line number Diff line change
@@ -43,4 +43,16 @@ public List<Store> findAll() {
return em.createQuery("select s from Store s", Store.class)
.getResultList();
}

public List<String> findAllDisplayName() {
return em.createQuery("select s.displayName from Store s", String.class)
.getResultList();
}

public List<String> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String, String> hashOperations;
// private final RedisTemplate<String, String> redisTemplate;
//
// public RedisHashService(RedisTemplate<String, String> 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<String> findAllValuesContainingSearchKeyword(String searchKeyword) {
// //Redis에서는 case insensitive한 검색을 지원하는 내장 모듈이 없으므로 searchKeyword는 모두 소문자로 통일하여 검색하도록 구현
// //당연히 초기 Redis에 field를 저장할 때도 모두 소문자로 변형하여 저장했고 원본 문자열은 value에 저장!
// Set<String> result = new HashSet<>(); //searchKeyword를 포함하는 원래 가게 이름들의 리스트. 최대 maxSize개까지 저장. 중복 허용하지 않고, 자동 사전순 정렬하기 위해 사용
// final int maxSize = 10; //최대 검색어 자동 완성 개수
//
// ScanOptions scanOptions = ScanOptions.scanOptions().match("*" + searchKeyword + "*").build(); //searchKeyword를 포함하는지를 검사하기 위한 scanOption
// Cursor<Map.Entry<String, String>> cursor = hashOperations.scan(key, scanOptions); //기존 Redis Keys 로직의 성능 이슈를 해결하기 위해 10개 단위로 끊어서 조회하는 Scan 기능 사용
//
// while (cursor.hasNext()) { //끊어서 조회하다보니 while loop로 조회
// Map.Entry<String, String> entry = cursor.next();
// result.add(entry.getValue());
//
// if(result.size() >= maxSize) //maxSize에 도달하면 scan 중단
// break;
// }
// cursor.close();
// return result;
// }
//
// public void removeAllOfHash() {
// redisTemplate.delete(key);
// }
//}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String> findAllValuesAfterIndexFromSortedSet(Long index) {
// return redisTemplate.opsForZSet().range(key, index, index + 200); //전체를 다 불러오기 보다는 200개 정도만 가져와도 자동 완성을 구현하는 데 무리가 없으므로 200개로 rough하게 설정
// }
//
// public void removeAllOfSortedSet() {
// redisTemplate.opsForZSet().removeRange(key, 0, -1);
// }
//}
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion src/main/resources/backend-submodule
Original file line number Diff line number Diff line change
@@ -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<Report> reports = reportService.findAll();
List<Long> reportIds = reports.stream().map(Report::getId).toList();

//then
assertArrayEquals(reportIds.toArray(), List.of(report1Id, report2Id).toArray());

}

}
Original file line number Diff line number Diff line change
@@ -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<String> result1 = storeService.autocorrect("^");
List<String> result2 = storeService.autocorrect("^^");

//then
assertArrayEquals(result1.toArray(), List.of("^", "^^", "^*^").toArray());
assertArrayEquals(result2.toArray(), List.of("^^").toArray());
}
}
2 changes: 1 addition & 1 deletion src/test/resources/backend-submodule

0 comments on commit a8452af

Please sign in to comment.