From df68ec6bcb7beefaaa65101b0ecc6a49acf2ddef Mon Sep 17 00:00:00 2001 From: Goldbar97 Date: Fri, 30 Aug 2024 18:13:56 +0900 Subject: [PATCH 1/3] chore: querydsl dependencies and logback --- build.gradle | 20 ++++++++++++++++++++ src/main/resources/logback-spring.xml | 4 +++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 52e13fb5..23db931b 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,12 @@ dependencies { // Development developmentOnly 'org.springframework.boot:spring-boot-devtools' + // QueryDSL + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' @@ -70,3 +76,17 @@ dependencies { tasks.named('test') { useJUnitPlatform() } + +def generated = 'src/main/generated' + +tasks.withType(JavaCompile) { + options.getGeneratedSourceOutputDirectory().set(file(generated)) +} + +sourceSets { + main.java.srcDirs += [generated] +} + +clean { + delete file(generated) +} \ No newline at end of file diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 9b45a08a..01103fb5 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -6,7 +6,9 @@ - %d{yyyy-MM-dd HH:mm:ss.SSS}] %highlight(%-5level) $magenta(%-4relative) --- [ %thread{10} ] %cyan(%logger{20}) : %msg%n + %d{yyyy-MM-dd HH:mm:ss.SSS}] %highlight(%-5level) %magenta(%-4relative) --- [ + %thread{10} ] %cyan(%logger{20}) : %msg%n + From fbadbb2dc67262a55bc102864644517e98a845d9 Mon Sep 17 00:00:00 2001 From: Goldbar97 Date: Fri, 30 Aug 2024 18:14:49 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20QueryDSL=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/QueryDslConfig.java | 16 +++ .../CommonJobPostingController.java | 21 ++++ .../repository/JobPostingRepositoryDSL.java | 22 ++++ .../JobPostingRepositoryDSLImpl.java | 108 ++++++++++++++++++ .../service/JobPostingService.java | 56 ++++++++- 5 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/ctrls/auto_enter_view/config/QueryDslConfig.java create mode 100644 src/main/java/com/ctrls/auto_enter_view/repository/JobPostingRepositoryDSL.java create mode 100644 src/main/java/com/ctrls/auto_enter_view/repository/JobPostingRepositoryDSLImpl.java diff --git a/src/main/java/com/ctrls/auto_enter_view/config/QueryDslConfig.java b/src/main/java/com/ctrls/auto_enter_view/config/QueryDslConfig.java new file mode 100644 index 00000000..e1f19a1b --- /dev/null +++ b/src/main/java/com/ctrls/auto_enter_view/config/QueryDslConfig.java @@ -0,0 +1,16 @@ +package com.ctrls.auto_enter_view.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QueryDslConfig { + + @Bean + public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) { + + return new JPAQueryFactory(entityManager); + } +} \ No newline at end of file diff --git a/src/main/java/com/ctrls/auto_enter_view/controller/CommonJobPostingController.java b/src/main/java/com/ctrls/auto_enter_view/controller/CommonJobPostingController.java index b27483cc..b65694e7 100644 --- a/src/main/java/com/ctrls/auto_enter_view/controller/CommonJobPostingController.java +++ b/src/main/java/com/ctrls/auto_enter_view/controller/CommonJobPostingController.java @@ -2,7 +2,11 @@ import com.ctrls.auto_enter_view.dto.common.JobPostingDetailDto; import com.ctrls.auto_enter_view.dto.common.MainJobPostingDto; +import com.ctrls.auto_enter_view.enums.Education; +import com.ctrls.auto_enter_view.enums.JobCategory; +import com.ctrls.auto_enter_view.enums.TechStack; import com.ctrls.auto_enter_view.service.JobPostingService; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -47,4 +51,21 @@ public ResponseEntity getDetailJobPosting( JobPostingDetailDto.Response response = jobPostingService.getJobPostingDetail(jobPostingKey); return ResponseEntity.ok(response); } + + @GetMapping("/search") + public ResponseEntity searchJobPosting( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "24") int size, + @RequestParam(required = false) JobCategory jobCategory, + @RequestParam(required = false) List techStacks, + @RequestParam(required = false) String employmentType, + @RequestParam(required = false) Integer minCareer, + @RequestParam(required = false) Integer maxCareer, + @RequestParam(required = false) Education education + ) { + + MainJobPostingDto.Response response = jobPostingService.searchJobPosting(page, size, + jobCategory, techStacks, employmentType, minCareer, maxCareer, education); + return ResponseEntity.ok(response); + } } \ No newline at end of file diff --git a/src/main/java/com/ctrls/auto_enter_view/repository/JobPostingRepositoryDSL.java b/src/main/java/com/ctrls/auto_enter_view/repository/JobPostingRepositoryDSL.java new file mode 100644 index 00000000..23f295e5 --- /dev/null +++ b/src/main/java/com/ctrls/auto_enter_view/repository/JobPostingRepositoryDSL.java @@ -0,0 +1,22 @@ +package com.ctrls.auto_enter_view.repository; + +import com.ctrls.auto_enter_view.entity.JobPostingEntity; +import com.ctrls.auto_enter_view.enums.Education; +import com.ctrls.auto_enter_view.enums.JobCategory; +import com.ctrls.auto_enter_view.enums.TechStack; +import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface JobPostingRepositoryDSL { + + Page searchJobPosting( + Pageable pageable, + JobCategory jobCategory, + List techStacks, + String employmentType, + Integer minCareer, + Integer maxCareer, + Education education + ); +} \ No newline at end of file diff --git a/src/main/java/com/ctrls/auto_enter_view/repository/JobPostingRepositoryDSLImpl.java b/src/main/java/com/ctrls/auto_enter_view/repository/JobPostingRepositoryDSLImpl.java new file mode 100644 index 00000000..ec11796a --- /dev/null +++ b/src/main/java/com/ctrls/auto_enter_view/repository/JobPostingRepositoryDSLImpl.java @@ -0,0 +1,108 @@ +package com.ctrls.auto_enter_view.repository; + +import static com.ctrls.auto_enter_view.entity.QJobPostingEntity.jobPostingEntity; +import static com.ctrls.auto_enter_view.entity.QJobPostingTechStackEntity.jobPostingTechStackEntity; + +import com.ctrls.auto_enter_view.entity.JobPostingEntity; +import com.ctrls.auto_enter_view.enums.Education; +import com.ctrls.auto_enter_view.enums.JobCategory; +import com.ctrls.auto_enter_view.enums.TechStack; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; +import org.springframework.stereotype.Repository; + +@Repository +public class JobPostingRepositoryDSLImpl extends QuerydslRepositorySupport implements + JobPostingRepositoryDSL { + + private final JPAQueryFactory jpaQueryFactory; + + public JobPostingRepositoryDSLImpl(JPAQueryFactory jpaQueryFactory) { + + super(JobPostingEntity.class); + this.jpaQueryFactory = jpaQueryFactory; + } + + public Page searchJobPosting( + Pageable pageable, + JobCategory jobCategory, + List techStacks, + String employmentType, + Integer minCareer, + Integer maxCareer, + Education education + ) { + + JPQLQuery query = jpaQueryFactory.selectFrom(jobPostingEntity) + .leftJoin(jobPostingTechStackEntity) + .on(jobPostingEntity.jobPostingKey.eq(jobPostingTechStackEntity.jobPostingKey)) + .where( + eqJobCategory(jobCategory), + eqTechStack(techStacks), + eqEmploymentType(employmentType), + eqCareer(minCareer, maxCareer), + eqEducation(education) + ) + .distinct(); + + List entities = this.getQuerydsl().applyPagination(pageable, query).fetch(); + return new PageImpl<>(entities, pageable, query.fetch().size()); + } + + private BooleanExpression eqJobCategory(JobCategory jobCategory) { + + if (jobCategory == null) { + return null; + } + return jobPostingEntity.jobCategory.eq(jobCategory); + } + + private BooleanBuilder eqTechStack(List techStacks) { + + if (techStacks == null || techStacks.isEmpty()) { + return null; + } + + BooleanBuilder booleanBuilder = new BooleanBuilder(); + for (TechStack techStack : techStacks) { + booleanBuilder.and(jobPostingTechStackEntity.techName.eq(techStack)); + } + return booleanBuilder; + } + + private BooleanExpression eqEmploymentType(String employmentType) { + + if (employmentType == null) { + return null; + } + return jobPostingEntity.employmentType.eq(employmentType); + } + + private BooleanExpression eqCareer(Integer minCareer, Integer maxCareer) { + + if (minCareer == null && maxCareer == null) { + return null; + } else if (minCareer != null && maxCareer == null) { + return jobPostingEntity.career.goe(minCareer); + } else if (minCareer == null && maxCareer != null) { + return jobPostingEntity.career.loe(maxCareer); + } else { + return jobPostingEntity.career.between(minCareer, maxCareer); + } + } + + private BooleanExpression eqEducation(Education education) { + + if (education == null) { + return null; + } + return jobPostingEntity.education.eq(education); + } +} \ No newline at end of file diff --git a/src/main/java/com/ctrls/auto_enter_view/service/JobPostingService.java b/src/main/java/com/ctrls/auto_enter_view/service/JobPostingService.java index 65b85a81..58f5103e 100644 --- a/src/main/java/com/ctrls/auto_enter_view/service/JobPostingService.java +++ b/src/main/java/com/ctrls/auto_enter_view/service/JobPostingService.java @@ -22,7 +22,9 @@ import com.ctrls.auto_enter_view.entity.JobPostingImageEntity; import com.ctrls.auto_enter_view.entity.JobPostingStepEntity; import com.ctrls.auto_enter_view.entity.JobPostingTechStackEntity; +import com.ctrls.auto_enter_view.enums.Education; import com.ctrls.auto_enter_view.enums.ErrorCode; +import com.ctrls.auto_enter_view.enums.JobCategory; import com.ctrls.auto_enter_view.enums.TechStack; import com.ctrls.auto_enter_view.exception.CustomException; import com.ctrls.auto_enter_view.repository.ApplicantRepository; @@ -32,6 +34,7 @@ import com.ctrls.auto_enter_view.repository.CompanyRepository; import com.ctrls.auto_enter_view.repository.JobPostingImageRepository; import com.ctrls.auto_enter_view.repository.JobPostingRepository; +import com.ctrls.auto_enter_view.repository.JobPostingRepositoryDSL; import com.ctrls.auto_enter_view.repository.JobPostingStepRepository; import com.ctrls.auto_enter_view.repository.JobPostingTechStackRepository; import java.time.LocalDate; @@ -66,6 +69,7 @@ public class JobPostingService { private final JobPostingStepRepository jobPostingStepRepository; private final AppliedJobPostingRepository appliedJobPostingRepository; private final JobPostingImageRepository jobPostingImageRepository; + private final JobPostingRepositoryDSL jobPostingRepositoryDSL; private final FilteringService filteringService; private final MailComponent mailComponent; private final KeyGenerator keyGenerator; @@ -113,7 +117,6 @@ public JobPostingEntity createJobPosting(UserDetails userDetails, String company } return jobPostingEntity; - } /** @@ -128,6 +131,7 @@ public JobPostingEntity createJobPosting(UserDetails userDetails, String company */ @Transactional public void editJobPosting(UserDetails userDetails, String jobPostingKey, Request request) { + JobPostingEntity jobPostingEntity = jobPostingRepository.findByJobPostingKey(jobPostingKey) .orElseThrow(() -> new CustomException(JOB_POSTING_NOT_FOUND)); @@ -189,6 +193,7 @@ public void editJobPosting(UserDetails userDetails, String jobPostingKey, Reques */ @Transactional public void deleteJobPosting(UserDetails userDetails, String jobPostingKey) { + log.info("채용 공고 삭제하기"); if (verifyExistsByJobPostingKey(jobPostingKey)) { @@ -227,6 +232,7 @@ public void deleteJobPosting(UserDetails userDetails, String jobPostingKey) { @Transactional(readOnly = true) public List getJobPostingsByCompanyKey(UserDetails userDetails, String companyKey) { + log.info("회사 본인이 등록한 채용공고 목록 조회"); CompanyEntity company = findCompanyByPrincipal(userDetails); @@ -251,10 +257,12 @@ public List getJobPostingsByCompanyKey(UserDetails userDetail // TODO : 회사가 탈퇴했을 때, 발생하는 문제점 해결하기 - 탈퇴한 회사 이름을 가져오지 못해 에러 발생 상황이 있었음 @Transactional(readOnly = true) public MainJobPostingDto.Response getAllJobPosting(int page, int size) { + String cacheKey = "mainJobPostings:" + page + "-" + size; // Redis : 캐시된 데이터 확인 - MainJobPostingDto.Response cachedResponse = (MainJobPostingDto.Response) redisObjectTemplate.opsForValue().get(cacheKey); + MainJobPostingDto.Response cachedResponse = (MainJobPostingDto.Response) redisObjectTemplate.opsForValue() + .get(cacheKey); if (cachedResponse != null) { log.info("Redis에서 캐시된 데이터 조회"); return cachedResponse; @@ -297,6 +305,7 @@ public MainJobPostingDto.Response getAllJobPosting(int page, int size) { */ @Transactional(readOnly = true) public JobPostingDetailDto.Response getJobPostingDetail(String jobPostingKey) { + log.info("채용 공고 상세 보기"); LocalDate currentDate = LocalDate.now(); @@ -327,6 +336,7 @@ public JobPostingDetailDto.Response getJobPostingDetail(String jobPostingKey) { */ @Transactional public void applyJobPosting(String jobPostingKey, String candidateKey) { + JobPostingEntity jobPostingEntity = jobPostingRepository.findByJobPostingKey(jobPostingKey) .orElseThrow(() -> new CustomException( JOB_POSTING_NOT_FOUND)); @@ -363,6 +373,44 @@ public void applyJobPosting(String jobPostingKey, String candidateKey) { log.info("AppliedJobPosting 추가 완료"); } + /** + * 검색 기능 + * + * @param page + * @param size + * @param jobCategory + * @param techStacks + * @param employmentType + * @param minCareer + * @param maxCareer + * @param education + * @return + */ + public MainJobPostingDto.Response searchJobPosting(int page, int size, JobCategory jobCategory, + List techStacks, String employmentType, Integer minCareer, Integer maxCareer, + Education education) { + + Pageable pageable = PageRequest.of(page - 1, size, Sort.by("endDate").ascending()); + Page jobPostingPage = jobPostingRepositoryDSL.searchJobPosting(pageable, + jobCategory, techStacks, employmentType, minCareer, maxCareer, education); + + int totalPages = jobPostingPage.getTotalPages(); + long totalElements = jobPostingPage.getTotalElements(); + + List jobPostingMainInfoList = jobPostingPage.getContent() + .stream() + .map(this::createJobPostingMainInfo) + .collect(Collectors.toList()); + + MainJobPostingDto.Response response = MainJobPostingDto.Response.builder() + .jobPostingsList(jobPostingMainInfoList) + .totalPages(totalPages) + .totalElements(totalElements) + .build(); + + return response; + } + /** * 이미지 URL 가져오기 * @@ -377,7 +425,6 @@ private String getImageUrl(String jobPostingKey) { return imageEntityOpt.map(JobPostingImageEntity::getCompanyImageUrl).orElse(null); } - /** * 채용 공고 단계 중 맨 처음 단계 가져오기 * @@ -409,6 +456,7 @@ private CompanyEntity findCompanyByPrincipal(UserDetails userDetails) { * @param companyKey 회사 KEY */ private void verifyCompanyOwnership(CompanyEntity company, String companyKey) { + log.info("회사 본인 확인"); if (!company.getCompanyKey().equals(companyKey)) { throw new CustomException(NO_AUTHORITY); @@ -422,6 +470,7 @@ private void verifyCompanyOwnership(CompanyEntity company, String companyKey) { * @return 지원자 존재시 TRUE, 없을 시 FALSE */ private boolean verifyExistsByJobPostingKey(String jobPostingKey) { + log.info("채용 공고에 지원한 지원자가 존재하는지 확인"); Long firstStep = getJobPostingStepEntity(jobPostingKey).getId(); @@ -506,6 +555,7 @@ private List getStep(String jobPostingKey) { */ private void notifyCandidates(List candidates, JobPostingEntity jobPostingEntity) { + log.info("지원자들에게 채용 공고 수정 알림 메일 전송"); for (CandidateListEntity candidate : candidates) { From 5040f33c253c714102a4e11f6c2d7657571ec77f Mon Sep 17 00:00:00 2001 From: Goldbar97 Date: Fri, 30 Aug 2024 18:32:44 +0900 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20QueryDSL=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EB=AC=B4=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5a943ee6..4b5ae2d6 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ logs docker-compose.yml *.http +/src/main/generated