Skip to content

Commit

Permalink
Merge pull request #222 from BudgetBuddiesTeam/feat/#213
Browse files Browse the repository at this point in the history
[feat] FAQ 검색기능 추가
  • Loading branch information
wnd01jun authored Nov 18, 2024
2 parents cb05ac5 + d78df08 commit c86667b
Show file tree
Hide file tree
Showing 18 changed files with 479 additions and 5 deletions.
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly('io.jsonwebtoken:jjwt-jackson:0.11.5') // jackson으로 jwt 파싱

// querydsl 관련
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public enum ErrorStatus implements BaseErrorCode {

_OTP_NOT_VALID(HttpStatus.BAD_REQUEST, "OTP4001", "인증번호가 유효하지 않습니다."),
_PHONE_NUMBER_NOT_VALID(HttpStatus.BAD_REQUEST, "AUTH4001", "전화번호 형식이 유효하지 않습니다. (예: 01012341234)"),
_FAQ_NOT_FOUND(HttpStatus.NOT_FOUND, "FAQ4004", "해당 FAQ가 존재하지 않습니다.");
_FAQ_NOT_FOUND(HttpStatus.NOT_FOUND, "FAQ4004", "해당 FAQ가 존재하지 않습니다."),
_SEARCH_KEYWORD_NOT_FOUND(HttpStatus.NOT_FOUND, "SEARCH_KEYWORD4004", "해당 SearchKeyword가 존재하지 않습니다."),
_FAQ_KEYWORD_NOT_FOUND(HttpStatus.NOT_FOUND, "FAQ_KEYWORD4004", "해당 FaqKeyword가 존재하지 않습니다.");


private HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import jakarta.validation.Valid;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

Expand Down Expand Up @@ -70,7 +71,7 @@ ApiResponse<?> postFaq(@ExistUser @RequestParam Long userId,

}
)
ApiResponse<?> findByPaging(Pageable pageable);
ApiResponse<?> findByPaging(Pageable pageable, String SearchCondition);

@Operation(summary = "[User] FAQ 수정 API", description = "FAQ를 수정하는 API입니다.",
requestBody = @RequestBody(
Expand Down Expand Up @@ -112,4 +113,9 @@ ApiResponse<?> modifyFaq(@PathVariable @ExistFaq Long faqId,
}
)
ApiResponse<?> deleteFaq(@ExistFaq Long faqId);

ApiResponse<?> addKeyword(@ExistFaq Long faqId, Long searchKeywordId);

ApiResponse<?> deleteKeyword(@ExistFaq Long faqId, Long searchKeywordId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
import com.bbteam.budgetbuddies.domain.faq.validation.ExistFaq;
import com.bbteam.budgetbuddies.domain.user.validation.ExistUser;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Null;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.lang.Nullable;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

Expand All @@ -37,8 +39,9 @@ public ApiResponse<FaqResponseDto.FaqFindResponse> findFaq(@PathVariable @ExistF

@Override
@GetMapping("/all")
public ApiResponse<Page<FaqResponseDto.FaqFindResponse>> findByPaging(@ParameterObject Pageable pageable) {
return ApiResponse.onSuccess(faqService.findAllWithPaging(pageable));
public ApiResponse<Page<FaqResponseDto.FaqFindResponse>> findByPaging(@ParameterObject Pageable pageable,
@RequestParam @Nullable String searchCondition) {
return ApiResponse.onSuccess(faqService.searchFaq(pageable, searchCondition));
}

@Override
Expand All @@ -54,4 +57,16 @@ public ApiResponse<String> deleteFaq(@PathVariable @ExistFaq Long faqId) {
faqService.deleteFaq(faqId);
return ApiResponse.onSuccess("Delete Success");
}

@Override
@PostMapping("/{faqId}/keyword")
public ApiResponse<?> addKeyword(@PathVariable @ExistFaq Long faqId, @RequestParam Long searchKeywordId) {
return ApiResponse.onSuccess(faqService.addKeyword(faqId, searchKeywordId));
}

@Override
@DeleteMapping("/{faqId}/keyword")
public ApiResponse<?> deleteKeyword(@PathVariable @ExistFaq Long faqId, @RequestParam Long searchKeywordId) {
return ApiResponse.onSuccess(faqService.removeKeyword(faqId, searchKeywordId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import com.bbteam.budgetbuddies.domain.faq.entity.Faq;
import org.springframework.data.jpa.repository.JpaRepository;

public interface FaqRepository extends JpaRepository<Faq, Long> {
public interface FaqRepository extends JpaRepository<Faq, Long>, FaqSearchRepository {


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.bbteam.budgetbuddies.domain.faq.repository;

import com.bbteam.budgetbuddies.domain.faq.entity.Faq;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface FaqSearchRepository {

Page<Faq> searchFaq(Pageable pageable, String searchCondition);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.bbteam.budgetbuddies.domain.faq.repository;

import com.bbteam.budgetbuddies.domain.faq.entity.Faq;
import com.bbteam.budgetbuddies.domain.searchkeyword.domain.QSearchKeyword;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;

import java.util.List;

import static com.bbteam.budgetbuddies.domain.faq.entity.QFaq.*;
import static com.bbteam.budgetbuddies.domain.faqkeyword.domain.QFaqKeyword.*;
import static com.bbteam.budgetbuddies.domain.searchkeyword.domain.QSearchKeyword.*;

public class FaqSearchRepositoryImpl implements FaqSearchRepository{

private final JPAQueryFactory queryFactory;

public FaqSearchRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}

@Override
public Page<Faq> searchFaq(Pageable pageable, String searchCondition) {
List<Faq> result = queryFactory.select(faq)
.from(faq)
.where(faq.id.in(
JPAExpressions
.select(faqKeyword.faq.id)
.from(faqKeyword)
.join(searchKeyword).on(keywordMatch(searchCondition))
))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

Long total = queryFactory.select(faq.count())
.from(faq)
.where(faq.id.in(
JPAExpressions
.select(faqKeyword.faq.id)
.from(faqKeyword)
.join(searchKeyword).on(keywordMatch(searchCondition))
))
.fetchOne();

return new PageImpl<>(result, pageable, total);

}

private BooleanExpression keywordMatch(String searchCondition) {
return searchCondition != null ? searchKeyword.keyword.contains(searchCondition) : null;
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.bbteam.budgetbuddies.domain.faq.dto.FaqRequestDto;
import com.bbteam.budgetbuddies.domain.faq.dto.FaqResponseDto;
import com.bbteam.budgetbuddies.domain.faqkeyword.dto.FaqKeywordResponseDto;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

Expand All @@ -11,9 +12,15 @@ public interface FaqService {

Page<FaqResponseDto.FaqFindResponse> findAllWithPaging(Pageable pageable);

Page<FaqResponseDto.FaqFindResponse> searchFaq(Pageable pageable, String searchCondition);

FaqResponseDto.FaqPostResponse postFaq(FaqRequestDto.FaqPostRequest dto, Long userId);

FaqResponseDto.FaqModifyResponse modifyFaq(FaqRequestDto.FaqModifyRequest dto, Long faqId);

String deleteFaq(Long faqId);

FaqKeywordResponseDto addKeyword(Long faqId, Long searchKeywordId);

String removeKeyword(Long faqId, Long searchKeywordId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
import com.bbteam.budgetbuddies.domain.faq.dto.FaqResponseDto;
import com.bbteam.budgetbuddies.domain.faq.entity.Faq;
import com.bbteam.budgetbuddies.domain.faq.repository.FaqRepository;
import com.bbteam.budgetbuddies.domain.faqkeyword.domain.FaqKeyword;
import com.bbteam.budgetbuddies.domain.faqkeyword.dto.FaqKeywordResponseDto;
import com.bbteam.budgetbuddies.domain.faqkeyword.repository.FaqKeywordRepository;
import com.bbteam.budgetbuddies.domain.searchkeyword.domain.SearchKeyword;
import com.bbteam.budgetbuddies.domain.searchkeyword.repository.SearchKeywordRepository;
import com.bbteam.budgetbuddies.domain.user.entity.User;
import com.bbteam.budgetbuddies.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
Expand All @@ -22,6 +27,8 @@ public class FaqServiceImpl implements FaqService{

private final FaqRepository faqRepository;
private final UserRepository userRepository;
private final FaqKeywordRepository faqKeywordRepository;
private final SearchKeywordRepository searchKeywordRepository;

@Override
public FaqResponseDto.FaqFindResponse findOneFaq(Long faqId) {
Expand Down Expand Up @@ -62,4 +69,36 @@ public String deleteFaq(Long faqId) {
private Faq findFaq(Long faqId) {
return faqRepository.findById(faqId).orElseThrow(() -> new GeneralException(ErrorStatus._FAQ_NOT_FOUND));
}

@Override
public Page<FaqResponseDto.FaqFindResponse> searchFaq(Pageable pageable, String searchCondition) {
return faqRepository.searchFaq(pageable, searchCondition).map(FaqConverter::entityToFind);
}

@Override
@Transactional
public FaqKeywordResponseDto addKeyword(Long faqId, Long searchKeywordId) {
Faq faq = findFaq(faqId);
SearchKeyword searchKeyword = searchKeywordRepository.findById(searchKeywordId).orElseThrow(() -> new GeneralException(ErrorStatus._SEARCH_KEYWORD_NOT_FOUND));

FaqKeyword faqKeyword = FaqKeyword.builder()
.searchKeyword(searchKeyword)
.faq(faq)
.build();

faqKeywordRepository.save(faqKeyword);
return FaqKeywordResponseDto.toDto(faqKeyword);
}

@Override
@Transactional
public String removeKeyword(Long faqId, Long searchKeywordId) {
Faq faq = findFaq(faqId);
SearchKeyword searchKeyword = searchKeywordRepository.findById(searchKeywordId).orElseThrow(() -> new GeneralException(ErrorStatus._SEARCH_KEYWORD_NOT_FOUND));

FaqKeyword faqKeyword = faqKeywordRepository.findByFaqAndSearchKeyword(faq, searchKeyword).orElseThrow(() -> new GeneralException(ErrorStatus._FAQ_KEYWORD_NOT_FOUND));
faqKeywordRepository.delete(faqKeyword);

return "ok";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.bbteam.budgetbuddies.domain.faqkeyword.domain;

import com.bbteam.budgetbuddies.common.BaseEntity;
import com.bbteam.budgetbuddies.domain.faq.entity.Faq;
import com.bbteam.budgetbuddies.domain.searchkeyword.domain.SearchKeyword;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.*;
import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@SuperBuilder
public class FaqKeyword extends BaseEntity {

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "faq_id")
@NotFound(action = NotFoundAction.IGNORE)
private Faq faq;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "search_keyword_id")
@NotFound(action = NotFoundAction.IGNORE)
private SearchKeyword searchKeyword;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.bbteam.budgetbuddies.domain.faqkeyword.dto;

import com.bbteam.budgetbuddies.domain.faqkeyword.domain.FaqKeyword;
import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public class FaqKeywordResponseDto {

private Long faqId;
private Long searchKeywordId;

private String faqTitle;
private String keyword;

public static FaqKeywordResponseDto toDto(FaqKeyword faqKeyword) {
return new FaqKeywordResponseDto(faqKeyword.getFaq().getId(), faqKeyword.getSearchKeyword().getId(),
faqKeyword.getFaq().getTitle(), faqKeyword.getSearchKeyword().getKeyword());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.bbteam.budgetbuddies.domain.faqkeyword.repository;

import com.bbteam.budgetbuddies.domain.faq.entity.Faq;
import com.bbteam.budgetbuddies.domain.faqkeyword.domain.FaqKeyword;
import com.bbteam.budgetbuddies.domain.searchkeyword.domain.SearchKeyword;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface FaqKeywordRepository extends JpaRepository<FaqKeyword, Long> {
Optional<FaqKeyword> findByFaqAndSearchKeyword(Faq faq, SearchKeyword searchKeyword);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.bbteam.budgetbuddies.domain.searchkeyword.controller;

import com.bbteam.budgetbuddies.apiPayload.ApiResponse;
import com.bbteam.budgetbuddies.domain.searchkeyword.domain.SearchKeyword;
import com.bbteam.budgetbuddies.domain.searchkeyword.service.SearchKeywordService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/search-keyword")
public class SearchKeywordController {

private final SearchKeywordService searchKeywordService;

@PostMapping("")
public ApiResponse<SearchKeyword> saveKeyword(String keyword) {

return ApiResponse.onSuccess(searchKeywordService.saveKeyword(keyword));
}

@GetMapping("")
public ApiResponse<SearchKeyword> findOne(Long searchKeywordId) {
return ApiResponse.onSuccess(searchKeywordService.findOne(searchKeywordId));
}

@GetMapping("/all")
public ApiResponse<Page<SearchKeyword>> findAll(Pageable pageable) {
return ApiResponse.onSuccess(searchKeywordService.findAll(pageable));
}

@PutMapping("")
public ApiResponse<SearchKeyword> modifyOne(Long searchKeywordId, String newKeyword) {
return ApiResponse.onSuccess((searchKeywordService.modifyOne(searchKeywordId, newKeyword)));
}

@DeleteMapping("")
public ApiResponse<String> deleteOne(Long searchKeywordId) {
searchKeywordService.deleteOne(searchKeywordId);
return ApiResponse.onSuccess("OK");
}




}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.bbteam.budgetbuddies.domain.searchkeyword.domain;

import com.bbteam.budgetbuddies.common.BaseEntity;
import jakarta.persistence.Entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SearchKeyword extends BaseEntity {

private String keyword;

public void changeKeyword(String newKeyword) {
this.keyword = newKeyword;
}

}
Loading

0 comments on commit c86667b

Please sign in to comment.