diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/adapter/in/web/JobPostingDetailsRetrievalController.java b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/in/web/JobPostingDetailsRetrievalController.java new file mode 100644 index 000000000..281c5eb97 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/in/web/JobPostingDetailsRetrievalController.java @@ -0,0 +1,32 @@ +package page.clab.api.domain.community.jobPosting.adapter.in.web; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import page.clab.api.domain.community.jobPosting.application.dto.response.JobPostingDetailsResponseDto; +import page.clab.api.domain.community.jobPosting.application.port.in.RetrieveJobPostingDetailsUseCase; +import page.clab.api.global.common.dto.ApiResponse; + +@RestController +@RequestMapping("/api/v1/job-postings") +@RequiredArgsConstructor +@Tag(name = "Community - Job Posting", description = "커뮤니티 채용 공고") +public class JobPostingDetailsRetrievalController { + + private final RetrieveJobPostingDetailsUseCase retrieveJobPostingDetailsUseCase; + + @Operation(summary = "[U] 채용 공고 상세 조회", description = "ROLE_USER 이상의 권한이 필요함") + @Secured({ "ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER" }) + @GetMapping("/{jobPostingId}") + public ApiResponse retrieveJobPostingDetails( + @PathVariable(name = "jobPostingId") Long jobPostingId + ) { + JobPostingDetailsResponseDto jobPosting = retrieveJobPostingDetailsUseCase.retrieveJobPostingDetails(jobPostingId); + return ApiResponse.success(jobPosting); + } +} diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/adapter/in/web/JobPostingsByConditionsRetrievalController.java b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/in/web/JobPostingsByConditionsRetrievalController.java new file mode 100644 index 000000000..abe4c7fb0 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/in/web/JobPostingsByConditionsRetrievalController.java @@ -0,0 +1,53 @@ +package page.clab.api.domain.community.jobPosting.adapter.in.web; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import page.clab.api.domain.community.jobPosting.application.dto.response.JobPostingResponseDto; +import page.clab.api.domain.community.jobPosting.application.port.in.RetrieveJobPostingsByConditionsUseCase; +import page.clab.api.domain.community.jobPosting.domain.CareerLevel; +import page.clab.api.domain.community.jobPosting.domain.EmploymentType; +import page.clab.api.global.common.dto.ApiResponse; +import page.clab.api.global.common.dto.PagedResponseDto; +import page.clab.api.global.exception.InvalidColumnException; +import page.clab.api.global.exception.SortingArgumentException; +import page.clab.api.global.util.PageableUtils; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/job-postings") +@RequiredArgsConstructor +@Tag(name = "Community - Job Posting", description = "커뮤니티 채용 공고") +public class JobPostingsByConditionsRetrievalController { + + private final RetrieveJobPostingsByConditionsUseCase retrieveJobPostingsByConditionsUseCase; + private final PageableUtils pageableUtils; + + @Operation(summary = "[U] 채용 공고 목록 조회(공고명, 기업명, 경력, 근로 조건 기준)", description = "ROLE_USER 이상의 권한이 필요함
" + + "4개의 파라미터를 자유롭게 조합하여 필터링 가능
" + + "공고명, 기업명, 경력, 근로 조건 중 하나라도 입력하지 않으면 전체 조회됨
" + + "DTO의 필드명을 기준으로 정렬 가능하며, 정렬 방향은 오름차순(asc)과 내림차순(desc)이 가능함") + @Secured({ "ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER" }) + @GetMapping("") + public ApiResponse> retrieveJobPostingsByConditions( + @RequestParam(name = "title", required = false) String title, + @RequestParam(name = "companyName", required = false) String companyName, + @RequestParam(name = "careerLevel", required = false) CareerLevel careerLevel, + @RequestParam(name = "employmentType", required = false) EmploymentType employmentType, + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size, + @RequestParam(name = "sortBy", defaultValue = "createdAt") List sortBy, + @RequestParam(name = "sortDirection", defaultValue = "desc") List sortDirection + ) throws SortingArgumentException, InvalidColumnException { + Pageable pageable = pageableUtils.createPageable(page, size, sortBy, sortDirection, JobPostingResponseDto.class); + PagedResponseDto jobPostings = retrieveJobPostingsByConditionsUseCase.retrieveJobPostings(title, companyName, careerLevel, employmentType, pageable); + return ApiResponse.success(jobPostings); + } +} diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingMapper.java b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingMapper.java new file mode 100644 index 000000000..b051ec488 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingMapper.java @@ -0,0 +1,12 @@ +package page.clab.api.domain.community.jobPosting.adapter.out.persistence; + +import org.mapstruct.Mapper; +import page.clab.api.domain.community.jobPosting.domain.JobPosting; + +@Mapper(componentModel = "spring") +public interface JobPostingMapper { + + JobPostingJpaEntity toJpaEntity(JobPosting domain); + + JobPosting toDomain(JobPostingJpaEntity entity); +} diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingPersistenceAdapter.java b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingPersistenceAdapter.java new file mode 100644 index 000000000..3050c7176 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingPersistenceAdapter.java @@ -0,0 +1,32 @@ +package page.clab.api.domain.community.jobPosting.adapter.out.persistence; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; +import page.clab.api.domain.community.jobPosting.application.port.out.RetrieveJobPostingPort; +import page.clab.api.domain.community.jobPosting.domain.CareerLevel; +import page.clab.api.domain.community.jobPosting.domain.EmploymentType; +import page.clab.api.domain.community.jobPosting.domain.JobPosting; +import page.clab.api.global.exception.NotFoundException; + +@Component +@RequiredArgsConstructor +public class JobPostingPersistenceAdapter implements + RetrieveJobPostingPort { + + private final JobPostingRepository repository; + private final JobPostingMapper jobPostingMapper; + + @Override + public JobPosting findByIdOrThrow(Long jobPostingId) { + return repository.findById(jobPostingId) + .map(jobPostingMapper::toDomain) + .orElseThrow(() -> new NotFoundException("[JobPosting] id: " + jobPostingId + "에 해당하는 채용 공고가 존재하지 않습니다.")); + } + + @Override + public Page findByConditions(String title, String companyName, CareerLevel careerLevel, EmploymentType employmentType, Pageable pageable) { + return repository.findByConditions(title, companyName, careerLevel, employmentType, pageable).map(jobPostingMapper::toDomain); + } +} diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingRepository.java b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingRepository.java new file mode 100644 index 000000000..fe8b52dab --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingRepository.java @@ -0,0 +1,9 @@ +package page.clab.api.domain.community.jobPosting.adapter.out.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +@Repository +public interface JobPostingRepository extends JpaRepository, JobPostingRepositoryCustom, QuerydslPredicateExecutor { +} diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingRepositoryCustom.java b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingRepositoryCustom.java new file mode 100644 index 000000000..31c234e32 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingRepositoryCustom.java @@ -0,0 +1,10 @@ +package page.clab.api.domain.community.jobPosting.adapter.out.persistence; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import page.clab.api.domain.community.jobPosting.domain.CareerLevel; +import page.clab.api.domain.community.jobPosting.domain.EmploymentType; + +public interface JobPostingRepositoryCustom { + Page findByConditions(String title, String companyName, CareerLevel careerLevel, EmploymentType employmentType, Pageable pageable); +} diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingRepositoryImpl.java b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingRepositoryImpl.java new file mode 100644 index 000000000..a6871bb1a --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingRepositoryImpl.java @@ -0,0 +1,46 @@ +package page.clab.api.domain.community.jobPosting.adapter.out.persistence; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; +import page.clab.api.domain.community.jobPosting.domain.CareerLevel; +import page.clab.api.domain.community.jobPosting.domain.EmploymentType; +import page.clab.api.global.util.OrderSpecifierUtil; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class JobPostingRepositoryImpl implements JobPostingRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Page findByConditions(String title, String companyName, CareerLevel careerLevel, EmploymentType employmentType, Pageable pageable) { + QJobPostingJpaEntity jobPosting = QJobPostingJpaEntity.jobPostingJpaEntity; + BooleanBuilder builder = new BooleanBuilder(); + + if (title != null && !title.isEmpty()) builder.and(jobPosting.title.containsIgnoreCase(title)); + if (companyName != null && !companyName.isEmpty()) + builder.and(jobPosting.companyName.containsIgnoreCase(companyName)); + if (careerLevel != null) builder.and(jobPosting.careerLevel.eq(careerLevel)); + if (employmentType != null) builder.and(jobPosting.employmentType.eq(employmentType)); + + List jobPostings = queryFactory.selectFrom(jobPosting) + .where(builder) + .orderBy(OrderSpecifierUtil.getOrderSpecifiers(pageable, jobPosting)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + long total = queryFactory.query().from(jobPosting) + .where(builder) + .fetchCount(); + + return new PageImpl<>(jobPostings, pageable, total); + } +} diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/response/JobPostingDetailsResponseDto.java b/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/response/JobPostingDetailsResponseDto.java new file mode 100644 index 000000000..d33104028 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/response/JobPostingDetailsResponseDto.java @@ -0,0 +1,36 @@ +package page.clab.api.domain.community.jobPosting.application.dto.response; + +import lombok.Builder; +import lombok.Getter; +import page.clab.api.domain.community.jobPosting.domain.CareerLevel; +import page.clab.api.domain.community.jobPosting.domain.EmploymentType; +import page.clab.api.domain.community.jobPosting.domain.JobPosting; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class JobPostingDetailsResponseDto { + + private Long id; + private String title; + private CareerLevel careerLevel; + private EmploymentType employmentType; + private String companyName; + private String recruitmentPeriod; + private String jobPostingUrl; + private LocalDateTime createdAt; + + public static JobPostingDetailsResponseDto toDto(JobPosting jobPosting) { + return JobPostingDetailsResponseDto.builder() + .id(jobPosting.getId()) + .title(jobPosting.getTitle()) + .careerLevel(jobPosting.getCareerLevel()) + .employmentType(jobPosting.getEmploymentType()) + .companyName(jobPosting.getCompanyName()) + .recruitmentPeriod(jobPosting.getRecruitmentPeriod()) + .jobPostingUrl(jobPosting.getJobPostingUrl()) + .createdAt(jobPosting.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/response/JobPostingResponseDto.java b/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/response/JobPostingResponseDto.java new file mode 100644 index 000000000..c76db0ddd --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/response/JobPostingResponseDto.java @@ -0,0 +1,35 @@ +package page.clab.api.domain.community.jobPosting.application.dto.response; + +import lombok.Builder; +import lombok.Getter; +import page.clab.api.domain.community.jobPosting.domain.JobPosting; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Builder +public class JobPostingResponseDto { + + private Long id; + private String title; + private String recruitmentPeriod; + private String jobPostingUrl; + private LocalDateTime createdAt; + + public static JobPostingResponseDto toDto(JobPosting jobPosting) { + return JobPostingResponseDto.builder() + .id(jobPosting.getId()) + .title(jobPosting.getTitle()) + .recruitmentPeriod(jobPosting.getRecruitmentPeriod()) + .jobPostingUrl(jobPosting.getJobPostingUrl()) + .createdAt(jobPosting.getCreatedAt()) + .build(); + } + + public static List toDto(List jobPostingList) { + return jobPostingList.stream() + .map(JobPostingResponseDto::toDto) + .toList(); + } +} diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/application/port/in/RetrieveJobPostingDetailsUseCase.java b/src/main/java/page/clab/api/domain/community/jobPosting/application/port/in/RetrieveJobPostingDetailsUseCase.java new file mode 100644 index 000000000..27164ba0d --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/jobPosting/application/port/in/RetrieveJobPostingDetailsUseCase.java @@ -0,0 +1,7 @@ +package page.clab.api.domain.community.jobPosting.application.port.in; + +import page.clab.api.domain.community.jobPosting.application.dto.response.JobPostingDetailsResponseDto; + +public interface RetrieveJobPostingDetailsUseCase { + JobPostingDetailsResponseDto retrieveJobPostingDetails(Long jobPostingId); +} diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/application/port/in/RetrieveJobPostingsByConditionsUseCase.java b/src/main/java/page/clab/api/domain/community/jobPosting/application/port/in/RetrieveJobPostingsByConditionsUseCase.java new file mode 100644 index 000000000..386b452c0 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/jobPosting/application/port/in/RetrieveJobPostingsByConditionsUseCase.java @@ -0,0 +1,11 @@ +package page.clab.api.domain.community.jobPosting.application.port.in; + +import org.springframework.data.domain.Pageable; +import page.clab.api.domain.community.jobPosting.application.dto.response.JobPostingResponseDto; +import page.clab.api.domain.community.jobPosting.domain.CareerLevel; +import page.clab.api.domain.community.jobPosting.domain.EmploymentType; +import page.clab.api.global.common.dto.PagedResponseDto; + +public interface RetrieveJobPostingsByConditionsUseCase { + PagedResponseDto retrieveJobPostings(String title, String companyName, CareerLevel careerLevel, EmploymentType employmentType, Pageable pageable); +} diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/application/port/out/RetrieveJobPostingPort.java b/src/main/java/page/clab/api/domain/community/jobPosting/application/port/out/RetrieveJobPostingPort.java new file mode 100644 index 000000000..27081e0b7 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/jobPosting/application/port/out/RetrieveJobPostingPort.java @@ -0,0 +1,14 @@ +package page.clab.api.domain.community.jobPosting.application.port.out; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import page.clab.api.domain.community.jobPosting.domain.CareerLevel; +import page.clab.api.domain.community.jobPosting.domain.EmploymentType; +import page.clab.api.domain.community.jobPosting.domain.JobPosting; + +public interface RetrieveJobPostingPort { + + JobPosting findByIdOrThrow(Long jobPostingId); + + Page findByConditions(String title, String companyName, CareerLevel careerLevel, EmploymentType employmentType, Pageable pageable); +} diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/application/service/JobPostingDetailsRetrievalService.java b/src/main/java/page/clab/api/domain/community/jobPosting/application/service/JobPostingDetailsRetrievalService.java new file mode 100644 index 000000000..15b038ccb --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/jobPosting/application/service/JobPostingDetailsRetrievalService.java @@ -0,0 +1,23 @@ +package page.clab.api.domain.community.jobPosting.application.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.jobPosting.application.dto.response.JobPostingDetailsResponseDto; +import page.clab.api.domain.community.jobPosting.application.port.in.RetrieveJobPostingDetailsUseCase; +import page.clab.api.domain.community.jobPosting.application.port.out.RetrieveJobPostingPort; +import page.clab.api.domain.community.jobPosting.domain.JobPosting; + +@Service +@RequiredArgsConstructor +public class JobPostingDetailsRetrievalService implements RetrieveJobPostingDetailsUseCase { + + private final RetrieveJobPostingPort retrieveJobPostingPort; + + @Transactional(readOnly = true) + @Override + public JobPostingDetailsResponseDto retrieveJobPostingDetails(Long jobPostingId) { + JobPosting jobPosting = retrieveJobPostingPort.findByIdOrThrow(jobPostingId); + return JobPostingDetailsResponseDto.toDto(jobPosting); + } +} diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/application/service/JobPostingsByConditionsRetrievalService.java b/src/main/java/page/clab/api/domain/community/jobPosting/application/service/JobPostingsByConditionsRetrievalService.java new file mode 100644 index 000000000..6158611e8 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/jobPosting/application/service/JobPostingsByConditionsRetrievalService.java @@ -0,0 +1,28 @@ +package page.clab.api.domain.community.jobPosting.application.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.jobPosting.application.dto.response.JobPostingResponseDto; +import page.clab.api.domain.community.jobPosting.application.port.in.RetrieveJobPostingsByConditionsUseCase; +import page.clab.api.domain.community.jobPosting.application.port.out.RetrieveJobPostingPort; +import page.clab.api.domain.community.jobPosting.domain.CareerLevel; +import page.clab.api.domain.community.jobPosting.domain.EmploymentType; +import page.clab.api.domain.community.jobPosting.domain.JobPosting; +import page.clab.api.global.common.dto.PagedResponseDto; + +@Service +@RequiredArgsConstructor +public class JobPostingsByConditionsRetrievalService implements RetrieveJobPostingsByConditionsUseCase { + + private final RetrieveJobPostingPort retrieveJobPostingPort; + + @Transactional(readOnly = true) + @Override + public PagedResponseDto retrieveJobPostings(String title, String companyName, CareerLevel careerLevel, EmploymentType employmentType, Pageable pageable) { + Page jobPostings = retrieveJobPostingPort.findByConditions(title, companyName, careerLevel, employmentType, pageable); + return new PagedResponseDto<>(jobPostings.map(JobPostingResponseDto::toDto)); + } +} diff --git a/src/main/java/page/clab/api/domain/community/news/adapter/in/web/NewsByConditionsRetrievalController.java b/src/main/java/page/clab/api/domain/community/news/adapter/in/web/NewsByConditionsRetrievalController.java new file mode 100644 index 000000000..e482ffa0f --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/news/adapter/in/web/NewsByConditionsRetrievalController.java @@ -0,0 +1,49 @@ +package page.clab.api.domain.community.news.adapter.in.web; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import page.clab.api.domain.community.news.application.dto.response.NewsResponseDto; +import page.clab.api.domain.community.news.application.port.in.RetrieveNewsByConditionsUseCase; +import page.clab.api.global.common.dto.ApiResponse; +import page.clab.api.global.common.dto.PagedResponseDto; +import page.clab.api.global.exception.InvalidColumnException; +import page.clab.api.global.exception.SortingArgumentException; +import page.clab.api.global.util.PageableUtils; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/news") +@RequiredArgsConstructor +@Tag(name = "Community - News", description = "커뮤니티 뉴스") +public class NewsByConditionsRetrievalController { + + private final RetrieveNewsByConditionsUseCase retrieveNewsByConditionsUseCase; + private final PageableUtils pageableUtils; + + @Operation(summary = "[U] 뉴스 목록 조회(제목, 카테고리 기준)", description = "ROLE_USER 이상의 권한이 필요함
" + + "2개의 파라미터를 자유롭게 조합하여 필터링 가능
" + + "제목, 카테고리 중 하나라도 입력하지 않으면 전체 조회됨
" + + "DTO의 필드명을 기준으로 정렬 가능하며, 정렬 방향은 오름차순(asc)과 내림차순(desc)이 가능함") + @Secured({ "ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER" }) + @GetMapping("") + public ApiResponse> retrieveNewsByConditions( + @RequestParam(name = "title", required = false) String title, + @RequestParam(name = "category", required = false) String category, + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size, + @RequestParam(name = "sortBy", defaultValue = "createdAt") List sortBy, + @RequestParam(name = "sortDirection", defaultValue = "desc") List sortDirection + ) throws SortingArgumentException, InvalidColumnException { + Pageable pageable = pageableUtils.createPageable(page, size, sortBy, sortDirection, NewsResponseDto.class); + PagedResponseDto news = retrieveNewsByConditionsUseCase.retrieveNews(title, category, pageable); + return ApiResponse.success(news); + } +} diff --git a/src/main/java/page/clab/api/domain/community/news/adapter/in/web/NewsDetailsRetrievalController.java b/src/main/java/page/clab/api/domain/community/news/adapter/in/web/NewsDetailsRetrievalController.java new file mode 100644 index 000000000..06f5e44ba --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/news/adapter/in/web/NewsDetailsRetrievalController.java @@ -0,0 +1,33 @@ +package page.clab.api.domain.community.news.adapter.in.web; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import page.clab.api.domain.community.news.application.dto.response.NewsDetailsResponseDto; +import page.clab.api.domain.community.news.application.port.in.RetrieveNewsDetailsUseCase; +import page.clab.api.global.common.dto.ApiResponse; + +@RestController +@RequestMapping("/api/v1/news") +@RequiredArgsConstructor +@Tag(name = "Community - News", description = "커뮤니티 뉴스") +public class NewsDetailsRetrievalController { + + private final RetrieveNewsDetailsUseCase retrieveNewsDetailsUseCase; + + @Operation(summary = "[U] 뉴스 상세 조회", description = "ROLE_USER 이상의 권한이 필요함") + @Secured({ "ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER" }) + @GetMapping("/{newsId}") + public ApiResponse retrieveNewsDetails( + @PathVariable(name = "newsId") Long newsId + ) { + NewsDetailsResponseDto news = retrieveNewsDetailsUseCase.retrieveNewsDetails(newsId); + return ApiResponse.success(news); + } +} + diff --git a/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsMapper.java b/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsMapper.java new file mode 100644 index 000000000..82aefb6c4 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsMapper.java @@ -0,0 +1,12 @@ +package page.clab.api.domain.community.news.adapter.out.persistence; + +import org.mapstruct.Mapper; +import page.clab.api.domain.community.news.domain.News; + +@Mapper(componentModel = "spring") +public interface NewsMapper { + + NewsJpaEntity toJpaEntity(News news); + + News toDomainEntity(NewsJpaEntity jpaEntity); +} diff --git a/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsPersistenceAdapter.java b/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsPersistenceAdapter.java new file mode 100644 index 000000000..7c1467774 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsPersistenceAdapter.java @@ -0,0 +1,31 @@ +package page.clab.api.domain.community.news.adapter.out.persistence; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; +import page.clab.api.domain.community.news.application.port.out.RetrieveNewsPort; +import page.clab.api.domain.community.news.domain.News; +import page.clab.api.global.exception.NotFoundException; + +@Component +@RequiredArgsConstructor +public class NewsPersistenceAdapter implements + RetrieveNewsPort { + + private final NewsRepository repository; + private final NewsMapper mapper; + + @Override + public News findByIdOrThrow(Long id) { + return repository.findById(id) + .map(mapper::toDomainEntity) + .orElseThrow(() -> new NotFoundException("[News] id: " + id + "에 해당하는 뉴스가 존재하지 않습니다.")); + } + + @Override + public Page findByConditions(String title, String category, Pageable pageable) { + return repository.findByConditions(title, category, pageable) + .map(mapper::toDomainEntity); + } +} diff --git a/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsRepository.java b/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsRepository.java new file mode 100644 index 000000000..5c6983c75 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsRepository.java @@ -0,0 +1,9 @@ +package page.clab.api.domain.community.news.adapter.out.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.stereotype.Repository; + +@Repository +public interface NewsRepository extends JpaRepository, NewsRepositoryCustom, QuerydslPredicateExecutor { +} diff --git a/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsRepositoryCustom.java b/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsRepositoryCustom.java new file mode 100644 index 000000000..c75aab4c3 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsRepositoryCustom.java @@ -0,0 +1,8 @@ +package page.clab.api.domain.community.news.adapter.out.persistence; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface NewsRepositoryCustom { + Page findByConditions(String title, String category, Pageable pageable); +} diff --git a/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsRepositoryImpl.java b/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsRepositoryImpl.java new file mode 100644 index 000000000..10d6e98ca --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsRepositoryImpl.java @@ -0,0 +1,41 @@ +package page.clab.api.domain.community.news.adapter.out.persistence; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; +import page.clab.api.global.util.OrderSpecifierUtil; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class NewsRepositoryImpl implements NewsRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Page findByConditions(String title, String category, Pageable pageable) { + QNewsJpaEntity news = QNewsJpaEntity.newsJpaEntity; + BooleanBuilder builder = new BooleanBuilder(); + + if (title != null && !title.isEmpty()) builder.and(news.title.containsIgnoreCase(title)); + if (category != null && !category.isEmpty()) builder.and(news.category.eq(category)); + + List newsList = queryFactory.selectFrom(news) + .where(builder) + .orderBy(OrderSpecifierUtil.getOrderSpecifiers(pageable, news)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + long count = queryFactory.selectFrom(news) + .where(builder) + .fetchCount(); + + return new PageImpl<>(newsList, pageable, count); + } +} diff --git a/src/main/java/page/clab/api/domain/community/news/application/dto/response/NewsDetailsResponseDto.java b/src/main/java/page/clab/api/domain/community/news/application/dto/response/NewsDetailsResponseDto.java new file mode 100644 index 000000000..61fb337f6 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/news/application/dto/response/NewsDetailsResponseDto.java @@ -0,0 +1,38 @@ +package page.clab.api.domain.community.news.application.dto.response; + +import lombok.Builder; +import lombok.Getter; +import page.clab.api.domain.community.news.domain.News; +import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Builder +public class NewsDetailsResponseDto { + + private Long id; + private String title; + private String category; + private String content; + private String articleUrl; + private String source; + private List files; + private LocalDate date; + private LocalDateTime createdAt; + + public static NewsDetailsResponseDto toDto(News news) { + return NewsDetailsResponseDto.builder() + .id(news.getId()) + .title(news.getTitle()) + .category(news.getCategory()) + .content(news.getContent()) + .articleUrl(news.getArticleUrl()) + .source(news.getSource()) + .date(news.getDate()) + .createdAt(news.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/community/news/application/dto/response/NewsResponseDto.java b/src/main/java/page/clab/api/domain/community/news/application/dto/response/NewsResponseDto.java new file mode 100644 index 000000000..2657c2cc2 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/news/application/dto/response/NewsResponseDto.java @@ -0,0 +1,31 @@ +package page.clab.api.domain.community.news.application.dto.response; + +import lombok.Builder; +import lombok.Getter; +import page.clab.api.domain.community.news.domain.News; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Getter +@Builder +public class NewsResponseDto { + + private Long id; + private String title; + private String category; + private String articleUrl; + private LocalDate date; + private LocalDateTime createdAt; + + public static NewsResponseDto toDto(News news) { + return NewsResponseDto.builder() + .id(news.getId()) + .title(news.getTitle()) + .category(news.getCategory()) + .articleUrl(news.getArticleUrl()) + .date(news.getDate()) + .createdAt(news.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/community/news/application/port/in/RetrieveNewsByConditionsUseCase.java b/src/main/java/page/clab/api/domain/community/news/application/port/in/RetrieveNewsByConditionsUseCase.java new file mode 100644 index 000000000..9dd8c8a2a --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/news/application/port/in/RetrieveNewsByConditionsUseCase.java @@ -0,0 +1,9 @@ +package page.clab.api.domain.community.news.application.port.in; + +import org.springframework.data.domain.Pageable; +import page.clab.api.domain.community.news.application.dto.response.NewsResponseDto; +import page.clab.api.global.common.dto.PagedResponseDto; + +public interface RetrieveNewsByConditionsUseCase { + PagedResponseDto retrieveNews(String title, String category, Pageable pageable); +} diff --git a/src/main/java/page/clab/api/domain/community/news/application/port/in/RetrieveNewsDetailsUseCase.java b/src/main/java/page/clab/api/domain/community/news/application/port/in/RetrieveNewsDetailsUseCase.java new file mode 100644 index 000000000..ec23ec150 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/news/application/port/in/RetrieveNewsDetailsUseCase.java @@ -0,0 +1,7 @@ +package page.clab.api.domain.community.news.application.port.in; + +import page.clab.api.domain.community.news.application.dto.response.NewsDetailsResponseDto; + +public interface RetrieveNewsDetailsUseCase { + NewsDetailsResponseDto retrieveNewsDetails(Long newsId); +} diff --git a/src/main/java/page/clab/api/domain/community/news/application/port/out/RetrieveNewsPort.java b/src/main/java/page/clab/api/domain/community/news/application/port/out/RetrieveNewsPort.java new file mode 100644 index 000000000..5144473c4 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/news/application/port/out/RetrieveNewsPort.java @@ -0,0 +1,12 @@ +package page.clab.api.domain.community.news.application.port.out; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import page.clab.api.domain.community.news.domain.News; + +public interface RetrieveNewsPort { + + News findByIdOrThrow(Long id); + + Page findByConditions(String title, String category, Pageable pageable); +} diff --git a/src/main/java/page/clab/api/domain/community/news/application/service/NewsByConditionsRetrievalService.java b/src/main/java/page/clab/api/domain/community/news/application/service/NewsByConditionsRetrievalService.java new file mode 100644 index 000000000..f36c5f288 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/news/application/service/NewsByConditionsRetrievalService.java @@ -0,0 +1,26 @@ +package page.clab.api.domain.community.news.application.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.news.application.dto.response.NewsResponseDto; +import page.clab.api.domain.community.news.application.port.in.RetrieveNewsByConditionsUseCase; +import page.clab.api.domain.community.news.application.port.out.RetrieveNewsPort; +import page.clab.api.domain.community.news.domain.News; +import page.clab.api.global.common.dto.PagedResponseDto; + +@Service +@RequiredArgsConstructor +public class NewsByConditionsRetrievalService implements RetrieveNewsByConditionsUseCase { + + private final RetrieveNewsPort retrieveNewsPort; + + @Transactional(readOnly = true) + @Override + public PagedResponseDto retrieveNews(String title, String category, Pageable pageable) { + Page newsPage = retrieveNewsPort.findByConditions(title, category, pageable); + return new PagedResponseDto<>(newsPage.map(NewsResponseDto::toDto)); + } +} diff --git a/src/main/java/page/clab/api/domain/community/news/application/service/NewsDetailsRetrievalService.java b/src/main/java/page/clab/api/domain/community/news/application/service/NewsDetailsRetrievalService.java new file mode 100644 index 000000000..c98ea2140 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/news/application/service/NewsDetailsRetrievalService.java @@ -0,0 +1,23 @@ +package page.clab.api.domain.community.news.application.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.news.application.dto.response.NewsDetailsResponseDto; +import page.clab.api.domain.community.news.application.port.in.RetrieveNewsDetailsUseCase; +import page.clab.api.domain.community.news.application.port.out.RetrieveNewsPort; +import page.clab.api.domain.community.news.domain.News; + +@Service +@RequiredArgsConstructor +public class NewsDetailsRetrievalService implements RetrieveNewsDetailsUseCase { + + private final RetrieveNewsPort retrieveNewsPort; + + @Transactional(readOnly = true) + @Override + public NewsDetailsResponseDto retrieveNewsDetails(Long newsId) { + News news = retrieveNewsPort.findByIdOrThrow(newsId); + return NewsDetailsResponseDto.toDto(news); + } +}