Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] FAQ 검색기능 추가 #222

Merged
merged 11 commits into from
Nov 18, 2024
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));
}
Comment on lines +67 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 API는 키워드를 통해 FAQ를 지우는 메소드인걸까요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FAQ와 연결된 searchKeyword mapping을 지우는 메소드 입니다!

}
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