Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ logs
docker-compose.yml
*.http

/src/main/generated
20 changes: 20 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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)
}
16 changes: 16 additions & 0 deletions src/main/java/com/ctrls/auto_enter_view/config/QueryDslConfig.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,4 +51,21 @@ public ResponseEntity<JobPostingDetailDto.Response> getDetailJobPosting(
JobPostingDetailDto.Response response = jobPostingService.getJobPostingDetail(jobPostingKey);
return ResponseEntity.ok(response);
}

@GetMapping("/search")
public ResponseEntity<MainJobPostingDto.Response> searchJobPosting(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "24") int size,
@RequestParam(required = false) JobCategory jobCategory,
@RequestParam(required = false) List<TechStack> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<JobPostingEntity> searchJobPosting(
Pageable pageable,
JobCategory jobCategory,
List<TechStack> techStacks,
String employmentType,
Integer minCareer,
Integer maxCareer,
Education education
);
}
Original file line number Diff line number Diff line change
@@ -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<JobPostingEntity> searchJobPosting(
Pageable pageable,
JobCategory jobCategory,
List<TechStack> techStacks,
String employmentType,
Integer minCareer,
Integer maxCareer,
Education education
) {

JPQLQuery<JobPostingEntity> 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<JobPostingEntity> 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<TechStack> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -113,7 +117,6 @@ public JobPostingEntity createJobPosting(UserDetails userDetails, String company
}

return jobPostingEntity;

}

/**
Expand All @@ -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));

Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -227,6 +232,7 @@ public void deleteJobPosting(UserDetails userDetails, String jobPostingKey) {
@Transactional(readOnly = true)
public List<JobPostingInfoDto> getJobPostingsByCompanyKey(UserDetails userDetails,
String companyKey) {

log.info("회사 본인이 등록한 채용공고 목록 조회");

CompanyEntity company = findCompanyByPrincipal(userDetails);
Expand All @@ -251,10 +257,12 @@ public List<JobPostingInfoDto> 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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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<TechStack> techStacks, String employmentType, Integer minCareer, Integer maxCareer,
Education education) {

Pageable pageable = PageRequest.of(page - 1, size, Sort.by("endDate").ascending());
Page<JobPostingEntity> jobPostingPage = jobPostingRepositoryDSL.searchJobPosting(pageable,
jobCategory, techStacks, employmentType, minCareer, maxCareer, education);

int totalPages = jobPostingPage.getTotalPages();
long totalElements = jobPostingPage.getTotalElements();

List<MainJobPostingDto.JobPostingMainInfo> 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 가져오기
*
Expand All @@ -377,7 +425,6 @@ private String getImageUrl(String jobPostingKey) {
return imageEntityOpt.map(JobPostingImageEntity::getCompanyImageUrl).orElse(null);
}


/**
* 채용 공고 단계 중 맨 처음 단계 가져오기
*
Expand Down Expand Up @@ -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);
Expand All @@ -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();

Expand Down Expand Up @@ -506,6 +555,7 @@ private List<String> getStep(String jobPostingKey) {
*/
private void notifyCandidates(List<CandidateListEntity> candidates,
JobPostingEntity jobPostingEntity) {

log.info("지원자들에게 채용 공고 수정 알림 메일 전송");

for (CandidateListEntity candidate : candidates) {
Expand Down
4 changes: 3 additions & 1 deletion src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
<!-- 콘솔에 남길 로그 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS}] %highlight(%-5level) $magenta(%-4relative) --- [ %thread{10} ] %cyan(%logger{20}) : %msg%n </pattern>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS}] %highlight(%-5level) %magenta(%-4relative) --- [
%thread{10} ] %cyan(%logger{20}) : %msg%n
</pattern>
</encoder>
</appender>

Expand Down