Skip to content

Commit

Permalink
[feat #27] 질문 게시글 전체 조회 API (#34)
Browse files Browse the repository at this point in the history
* [feat] : 게시글 상호작용 엔티티 연관관계 매핑 삭제

* [feat] : 게시글 상호작용 엔티티 builder 삭제 후 정적 팩토리 메서드 추가

* [feat] : 게시글 상호작용 엔티티 수량 증가 메서드 추가

* [chore] : querydsl 의존성 추가

* [chore] : 256 인코딩 삭제

* [chore] : s3 API 스웨거 명세

* [rename] : 질문글 dto 경로 변경

* [feat] : 질문글 검색 DTO 추가

* [feat] : 질문글 검색 요청 DTO 추가

* [feat] : JobGroup 필드 추가 및 변환함수 추가

* [chore] : querydsl 설정 파일  추가

* [test] : repository test 추상 클래스에 querydsl 설정 파일 포함

* [feat] : 직군 및 검색어 필터링 동적 쿼리 작성

* [test] : 직군 및 검색어 필터링 동적 쿼리 테스트

* [feat] : request dto 채택 여부 필드 null 허용

* [test] : questionPostFixture 객체 추가

* [test] : 질문글 키워드 필터링 repository test

* [feat] : 응답 dto 채택 여부 필드 추가

* [style] : 응답 dto 필드명 변경

* [feat] : 질문글 entity<->dto 매퍼 추가

* [feat] : 질문글 검색 비즈니스 로직 추가

* [feat] : 질문글 검색 API 로직 추가

* [feat] : 질문글 검색 API http method 변경

* [feat] : 질문글 검색 채택여부 필터링 추가

* [test] : 질문글 검색 채택여부 필터링 테스트

* [test] : 질문글 검색 통합 테스트

* [style] : 코드 리포멧팅

* [style] : dto 팩토리 메서드 네이밍 변경

* [feat] : dto 검증 로직 추가

* [test] : dto 검증 로직 테스트

* [chore] : yml 디코딩 추가
  • Loading branch information
hyun2371 authored Aug 13, 2024
1 parent d0deb05 commit e424872
Show file tree
Hide file tree
Showing 24 changed files with 484 additions and 40 deletions.
24 changes: 24 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,30 @@ dependencies {

//swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'

// 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"
}

// Querydsl
def generated = 'src/main/generated'

// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(generated))
}

// java source set 에 querydsl QClass 위치 추가
sourceSets {
main.java.srcDirs += [generated]
}

// gradle clean 시에 QClass 디렉토리 삭제
clean {
delete file(generated)
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.dnd.gongmuin.answer.dto;

import com.dnd.gongmuin.question_post.dto.MemberInfo;
import com.dnd.gongmuin.question_post.dto.response.MemberInfo;

public record AnswerDetailResponse(
Long answerId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.dnd.gongmuin.answer.domain.Answer;
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.question_post.dto.MemberInfo;
import com.dnd.gongmuin.question_post.dto.response.MemberInfo;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/dnd/gongmuin/common/config/QueryDslConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.dnd.gongmuin.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.querydsl.jpa.impl.JPAQueryFactory;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;

@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
5 changes: 2 additions & 3 deletions src/main/java/com/dnd/gongmuin/mail/service/MailService.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,15 @@
@RequiredArgsConstructor
public class MailService {

@Value("${spring.mail.auth-code-expiration-millis}")
private long authCodeExpirationMillis;
private static final String SUBJECT = "[공무인] 공무원 인증 메일입니다.";
private static final String AUTH_CODE_PREFIX = "AuthCode ";
private static final String TEXT = "인증 코드는 다음과 같습니다.\n ";

private final JavaMailSender mailSender;
private final AuthCodeGenerator authCodeGenerator;
private final RedisUtil redisUtil;
private final MemberRepository memberRepository;
@Value("${spring.mail.auth-code-expiration-millis}")
private long authCodeExpirationMillis;

public SendMailResponse sendEmail(SendMailRequest request) {
String targetEmail = request.targetEmail();
Expand Down
13 changes: 11 additions & 2 deletions src/main/java/com/dnd/gongmuin/member/domain/JobGroup.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dnd.gongmuin.member.domain;

import java.util.Arrays;
import java.util.List;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
Expand All @@ -9,17 +10,25 @@
@RequiredArgsConstructor
public enum JobGroup {

ENGINEERING("공업"); // TODO: 7/20/24 필드 추가
ENGINEERING("공업"),
ADMINISTRATION("행정"),
MACHINE("기계");

private final String label;

public static JobGroup of(String input) {
public static JobGroup from(String input) {
return Arrays.stream(values())
.filter(group -> group.isEqual(input))
.findAny()
.orElseThrow(IllegalArgumentException::new);
}

public static List<JobGroup> from(List<String> labels) {
return labels.stream()
.map(JobGroup::from)
.toList();
}

private boolean isEqual(String input) {
return input.equals(this.label);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ private void updateAdditionalInfo(AdditionalInfoRequest request, Member findMemb
findMember.updateAdditionalInfo(
request.nickname(),
request.officialEmail(),
JobGroup.of(request.jobGroup()),
JobGroup.from(request.jobGroup()),
JobCategory.of(request.jobCategory())
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
package com.dnd.gongmuin.post_interaction.domain;

import static jakarta.persistence.ConstraintMode.*;
import static jakarta.persistence.FetchType.*;

import com.dnd.gongmuin.common.entity.TimeBaseEntity;
import com.dnd.gongmuin.question_post.domain.QuestionPost;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

Expand All @@ -38,15 +30,19 @@ public class PostInteractionCount extends TimeBaseEntity {
@Column(name = "type")
private InteractionType type;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "question_post_id",
nullable = false,
foreignKey = @ForeignKey(NO_CONSTRAINT))
private QuestionPost questionPost;
@Column(name = "question_post_id")
private Long questionPostId;

@Builder
public PostInteractionCount(InteractionType type, QuestionPost questionPost) {
private PostInteractionCount(InteractionType type, Long questionPostId) {
this.type = type;
this.questionPost = questionPost;
this.questionPostId = questionPostId;
}

public static PostInteractionCount of(InteractionType type, Long questionPostId) {
return new PostInteractionCount(type, questionPostId);
}

private void increaseTotalCount() {
totalCount++;
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package com.dnd.gongmuin.question_post.controller;

import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.dnd.gongmuin.common.dto.PageResponse;
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.question_post.dto.QuestionPostDetailResponse;
import com.dnd.gongmuin.question_post.dto.RegisterQuestionPostRequest;
import com.dnd.gongmuin.question_post.dto.request.QuestionPostSearchCondition;
import com.dnd.gongmuin.question_post.dto.request.RegisterQuestionPostRequest;
import com.dnd.gongmuin.question_post.dto.response.QuestionPostDetailResponse;
import com.dnd.gongmuin.question_post.dto.response.QuestionPostSimpleResponse;
import com.dnd.gongmuin.question_post.service.QuestionPostService;

import io.swagger.v3.oas.annotations.Operation;
Expand Down Expand Up @@ -48,4 +53,16 @@ public ResponseEntity<QuestionPostDetailResponse> getQuestionPostById(
QuestionPostDetailResponse response = questionPostService.getQuestionPostById(questionPostId);
return ResponseEntity.ok(response);
}

@Operation(summary = "질문글 검색 API", description = "질문글을 키워드로 검색하고 정렬, 필터링을 한다.")
@ApiResponse(useReturnTypeSchema = true)
@GetMapping("/search")
public ResponseEntity<PageResponse<QuestionPostSimpleResponse>> searchQuestionPost(
@Valid @ModelAttribute QuestionPostSearchCondition condition,
Pageable pageable
) {
PageResponse<QuestionPostSimpleResponse> response = questionPostService.searchQuestionPost(
condition, pageable);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.question_post.domain.QuestionPost;
import com.dnd.gongmuin.question_post.domain.QuestionPostImage;
import com.dnd.gongmuin.question_post.dto.request.RegisterQuestionPostRequest;
import com.dnd.gongmuin.question_post.dto.response.MemberInfo;
import com.dnd.gongmuin.question_post.dto.response.QuestionPostDetailResponse;
import com.dnd.gongmuin.question_post.dto.response.QuestionPostSimpleResponse;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
Expand All @@ -14,7 +18,7 @@
public class QuestionPostMapper {

public static QuestionPost toQuestionPost(RegisterQuestionPostRequest request, Member member) {
JobGroup jobGroup = JobGroup.of(request.targetJobGroup());
JobGroup jobGroup = JobGroup.from(request.targetJobGroup());
List<QuestionPostImage> images = request.imageUrls().stream()
.map(QuestionPostImage::from)
.toList();
Expand All @@ -39,4 +43,16 @@ public static QuestionPostDetailResponse toQuestionPostDetailResponse(QuestionPo
questionPost.getCreatedAt().toString()
);
}

public static QuestionPostSimpleResponse toQuestionPostSimpleResponse(QuestionPost questionPost) {
return new QuestionPostSimpleResponse(
questionPost.getId(),
questionPost.getTitle(),
questionPost.getContent(),
questionPost.getJobGroup().getLabel(),
questionPost.getReward(),
questionPost.getCreatedAt().toString(),
questionPost.getIsChosen()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.dnd.gongmuin.question_post.dto.request;

import java.util.List;

import jakarta.validation.constraints.Size;

public record QuestionPostSearchCondition(
String keyword,
@Size(max = 3, message = "직군은 3개까지 선택 가능합니다.")
List<String> jobGroups,
Boolean isChosen
) {
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.dnd.gongmuin.question_post.dto;
package com.dnd.gongmuin.question_post.dto.request;

import java.util.List;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.dnd.gongmuin.question_post.dto;
package com.dnd.gongmuin.question_post.dto.response;

public record MemberInfo(
Long memberId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.dnd.gongmuin.question_post.dto;
package com.dnd.gongmuin.question_post.dto.response;

import java.util.List;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.dnd.gongmuin.question_post.dto.response;

public record QuestionPostSimpleResponse(
Long questionPostId,
String title,
String content,
String jobGroup,
int reward,
String createdAt,
boolean isChosen
// TODO: 8/11/24 북마크 수, 추천수 추가
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.dnd.gongmuin.question_post.repository;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;

import com.dnd.gongmuin.question_post.domain.QuestionPost;
import com.dnd.gongmuin.question_post.dto.request.QuestionPostSearchCondition;

public interface QuestionPostQueryRepository {
Slice<QuestionPost> searchQuestionPosts(QuestionPostSearchCondition condition, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.dnd.gongmuin.question_post.repository;

import static com.dnd.gongmuin.question_post.domain.QQuestionPost.*;

import java.util.List;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.stereotype.Repository;

import com.dnd.gongmuin.member.domain.JobGroup;
import com.dnd.gongmuin.question_post.domain.QuestionPost;
import com.dnd.gongmuin.question_post.dto.request.QuestionPostSearchCondition;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class QuestionPostQueryRepositoryImpl implements QuestionPostQueryRepository {

private final JPAQueryFactory queryFactory;

@Override
public Slice<QuestionPost> searchQuestionPosts(QuestionPostSearchCondition condition, Pageable pageable) {
List<QuestionPost> content = queryFactory.select(questionPost)
.from(questionPost)
.where(
keywordContains(condition.keyword()),
jobGroupContains(condition.jobGroups()),
isChosenEq(condition.isChosen())
)
.limit(pageable.getPageSize() + 1L)
.offset(pageable.getOffset())
.fetch();
boolean hasNext = hasNext(pageable.getPageSize(), content);
return new SliceImpl<>(content, pageable, hasNext);
}

private BooleanExpression isChosenEq(Boolean isChosen) {
if (isChosen == null) {
return null;
}
if (Boolean.TRUE.equals(isChosen)) {
return questionPost.isChosen.eq(Boolean.TRUE);
} else {
return questionPost.isChosen.eq(Boolean.FALSE);
}
}

private BooleanExpression jobGroupContains(List<String> jobGroups) {
if (jobGroups == null || jobGroups.isEmpty())
return null; // 직군 필터링 선택 안할 때
List<JobGroup> selectedJobGroups = JobGroup.from(jobGroups); // string -> enum
return questionPost.jobGroup.in(selectedJobGroups);
}

private BooleanExpression keywordContains(String keyword) {
return keyword != null ? questionPost.title.contains(keyword) : null;
}

private boolean hasNext(int pageSize, List<QuestionPost> questionPosts) {
if (questionPosts.size() <= pageSize) {
return false;
}
questionPosts.remove(pageSize);
return true;
}
}
Loading

0 comments on commit e424872

Please sign in to comment.