Skip to content

Commit

Permalink
Merge pull request #18 from DDD-Community/develop
Browse files Browse the repository at this point in the history
3차 배포
  • Loading branch information
kikingki authored Aug 25, 2024
2 parents 60fc3cc + 7b90d56 commit 8a47588
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.dissonance.itit.controller;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -15,6 +17,7 @@
import com.dissonance.itit.dto.request.InfoPostReq;
import com.dissonance.itit.dto.response.InfoPostCreateRes;
import com.dissonance.itit.dto.response.InfoPostDetailRes;
import com.dissonance.itit.dto.response.InfoPostRes;
import com.dissonance.itit.service.InfoPostService;
import com.dissonance.itit.service.UserService;

Expand Down Expand Up @@ -51,4 +54,13 @@ public ResponseEntity<String> reportedInfoPost(@PathVariable Long infoPostId) {
Long resultId = infoPostService.reportedInfoPost(infoPostId);
return ResponseEntity.ok(resultId + "번 게시글의 신고가 성공적으로 접수되었습니다.");
}

@GetMapping("/categories/{categoryId}/posts")
@Operation(summary = "공고 게시글 목록 조회", description = "카테고리별 공고 게시글 목록을 조회합니다. (정렬: 최신순 - latest, 마감일순 - deadline)")
public ResponseEntity<Page<InfoPostRes>> getInfoPostsByCategory(@PathVariable Integer categoryId,
Pageable pageable) {
Page<InfoPostRes> infoPostRes = infoPostService.getInfoPostsByCategoryId(categoryId, pageable);

return ResponseEntity.ok(infoPostRes);
}
}
48 changes: 48 additions & 0 deletions src/main/java/com/dissonance/itit/dto/response/InfoPostRes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.dissonance.itit.dto.response;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class InfoPostRes {
private final Long id;
private final String imgUrl;
private final String title;
private final String remainingDays;

public static List<InfoPostRes> of(List<InfoPostInfo> postInfos) {
return postInfos.stream()
.map(postInfo -> InfoPostRes.builder()
.id(postInfo.id())
.imgUrl(postInfo.imgUrl())
.title(postInfo.title())
.remainingDays(calculateRemainingDays(postInfo.deadline()))
.build())
.toList();
}

private static String calculateRemainingDays(LocalDate deadline) {
LocalDate today = LocalDate.now();
long daysLeft = ChronoUnit.DAYS.between(today, deadline);

if (daysLeft > 0) {
return "D-" + daysLeft;
} else if (daysLeft == 0) {
return "D-Day";
} else {
return "마감";
}
}

public record InfoPostInfo(
Long id,
String imgUrl,
String title,
LocalDate deadline) {
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
package com.dissonance.itit.repository;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.stereotype.Repository;

import com.dissonance.itit.domain.entity.QInfoPost;
import com.dissonance.itit.dto.response.InfoPostDetailRes.InfoPostInfo;
import com.dissonance.itit.dto.response.InfoPostRes;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;

import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -42,4 +56,86 @@ private void incrementViewCount(Long infoPostId) {
.where(infoPost.id.eq(infoPostId))
.execute();
}

public Page<InfoPostRes> findInfoPostsByCategoryId(Integer categoryId, Pageable pageable) {
List<InfoPostRes.InfoPostInfo> postInfos = jpaQueryFactory.select(
Projections.constructor(InfoPostRes.InfoPostInfo.class,
infoPost.id,
infoPost.image.imageUrl,
infoPost.title,
infoPost.recruitmentEndDate))
.from(infoPost)
.where(createCondition(categoryId))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(getOrderSpecifiers(pageable.getSort()))
.fetch();

return paginationInfoPosts(categoryId, InfoPostRes.of(postInfos), pageable);
}

public OrderSpecifier<?>[] getOrderSpecifiers(Sort sort) {
List<OrderSpecifier<?>> orderSpecifiers = new ArrayList<>();

for (Sort.Order order : sort) {
OrderSpecifier<?> orderSpecifier = null;

switch (order.getProperty()) {
case "latest":
orderSpecifier = new OrderSpecifier<>(Order.DESC, infoPost.id);
break;
case "deadline":
// recruitmentEndDate가 현재 날짜보다 크거나 같은 게시글을 먼저 정렬
LocalDate currentDate = LocalDate.now();

// 정렬 기준 추가: 현재 날짜보다 크거나 같은 날짜는 우선순위가 높음
OrderSpecifier<?> prioritySort = new OrderSpecifier<>(
Order.DESC,
Expressions.booleanTemplate("CASE WHEN {0} >= {1} THEN 1 ELSE 0 END",
infoPost.recruitmentEndDate, currentDate)
);

// 기본 정렬: recruitmentEndDate 내림차순
OrderSpecifier<?> dateSort = new OrderSpecifier<>(Order.DESC, infoPost.recruitmentEndDate);

// 두 개의 정렬 기준을 조합
orderSpecifiers.add(prioritySort);
orderSpecifiers.add(dateSort);
break;
}

if (orderSpecifier != null) {
orderSpecifiers.add(orderSpecifier);
}
}

if (!orderSpecifiers.isEmpty()) {
return orderSpecifiers.toArray(new OrderSpecifier<?>[0]);
} else {
return new OrderSpecifier<?>[0];
}
}

private Page<InfoPostRes> paginationInfoPosts(Integer categoryId, List<InfoPostRes> infoPostRes,
Pageable pageable) {
JPAQuery<Long> countQuery = getCountQuery(categoryId);

return PageableExecutionUtils.getPage(infoPostRes, pageable, countQuery::fetchOne);
}

private JPAQuery<Long> getCountQuery(Integer categoryId) {
return jpaQueryFactory.select(infoPost.id.count())
.from(infoPost)
.where(createCondition(categoryId));
}

private BooleanExpression createCondition(Integer categoryId) {
BooleanExpression condition = infoPost.category.id.eq(categoryId);

if (categoryId == 1) {
condition = infoPost.category.parent.id.eq(categoryId);
}

return condition;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -18,6 +20,7 @@
import com.dissonance.itit.dto.response.InfoPostCreateRes;
import com.dissonance.itit.dto.response.InfoPostDetailRes;
import com.dissonance.itit.dto.response.InfoPostDetailRes.InfoPostInfo;
import com.dissonance.itit.dto.response.InfoPostRes;
import com.dissonance.itit.repository.InfoPostRepository;
import com.dissonance.itit.repository.InfoPostRepositorySupport;

Expand Down Expand Up @@ -70,4 +73,9 @@ public Long reportedInfoPost(Long infoPostId) {
infoPost.updateReported();
return infoPost.getId();
}

@Transactional(readOnly = true)
public Page<InfoPostRes> getInfoPostsByCategoryId(Integer categoryId, Pageable pageable) {
return infoPostRepositorySupport.findInfoPostsByCategoryId(categoryId, pageable);
}
}
64 changes: 22 additions & 42 deletions src/test/java/com/dissonance/itit/fixture/TestFixture.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.dissonance.itit.domain.enums.Role;
import com.dissonance.itit.dto.common.PositionInfo;
import com.dissonance.itit.dto.request.InfoPostReq;
import com.dissonance.itit.dto.response.InfoPostRes;

public class TestFixture {
public static MockMultipartFile getMockMultipartFile() {
Expand Down Expand Up @@ -79,47 +80,26 @@ public static List<PositionInfo> createMultiplePositionInfos() {
);
}

public static List<InfoPost> createMultipleInfoPosts(User author, Image image, Category category) {
InfoPostReq infoPostReq1 = InfoPostReq.builder()
.title("공고 1")
.content("내용 1")
.organization("조직 1")
.categoryId(4)
.activityStartDate("2000년 1월 1일")
.activityEndDate("2000년 1월 7일")
.recruitmentStartDate("2000년 6월 1일")
.recruitmentEndDate("2000년 6월 30일")
.detailUrl("https://example.com/1")
.build();

InfoPostReq infoPostReq2 = InfoPostReq.builder()
.title("공고 2")
.content("내용 2")
.organization("조직 2")
.categoryId(5)
.activityStartDate("2000년 2월 1일")
.activityEndDate("2000년 2월 7일")
.recruitmentStartDate("2000년 7월 1일")
.recruitmentEndDate("2000년 7월 31일")
.detailUrl("https://example.com/2")
.build();

InfoPostReq infoPostReq3 = InfoPostReq.builder()
.title("공고 3")
.content("내용 3")
.organization("조직 3")
.categoryId(6)
.activityStartDate("2000년 3월 1일")
.activityEndDate("2000년 3월 7일")
.recruitmentStartDate("2000년 8월 1일")
.recruitmentEndDate("2000년 8월 31일")
.detailUrl("https://example.com/3")
.build();

InfoPost infoPost1 = createInfoPost(infoPostReq1, author, image, category);
InfoPost infoPost2 = createInfoPost(infoPostReq2, author, image, category);
InfoPost infoPost3 = createInfoPost(infoPostReq3, author, image, category);

return List.of(infoPost1, infoPost2, infoPost3);
public static List<InfoPostRes> createMultipleInfoPostRes() {
return List.of(
InfoPostRes.builder()
.id(1L)
.imgUrl("http://example.com/img1.jpg")
.title("Post 1")
.remainingDays("D-5")
.build(),
InfoPostRes.builder()
.id(2L)
.imgUrl("http://example.com/img2.jpg")
.title("Post 2")
.remainingDays("D-Day")
.build(),
InfoPostRes.builder()
.id(3L)
.imgUrl("http://example.com/img3.jpg")
.title("Post 3")
.remainingDays("D+3")
.build()
);
}
}
32 changes: 32 additions & 0 deletions src/test/java/com/dissonance/itit/service/InfoPostServiceTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dissonance.itit.service;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.BDDMockito.*;

import java.time.LocalDate;
Expand All @@ -13,6 +14,10 @@
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.mock.web.MockMultipartFile;

import com.dissonance.itit.common.exception.CustomException;
Expand All @@ -26,6 +31,7 @@
import com.dissonance.itit.dto.request.InfoPostReq;
import com.dissonance.itit.dto.response.InfoPostCreateRes;
import com.dissonance.itit.dto.response.InfoPostDetailRes;
import com.dissonance.itit.dto.response.InfoPostRes;
import com.dissonance.itit.fixture.TestFixture;
import com.dissonance.itit.repository.InfoPostRepository;
import com.dissonance.itit.repository.InfoPostRepositorySupport;
Expand Down Expand Up @@ -163,4 +169,30 @@ void reportedInfoPost_throwCustomException_givenNonExistentId() {
.isInstanceOf(CustomException.class)
.hasMessage(ErrorCode.NON_EXISTENT_INFO_POST_ID.getMessage());
}

@Test
@DisplayName("공고 게시글 목록 page 조회")
void getInfoPostsByCategoryId_returnInfoPostResPage() {
// Given
Integer categoryId = 1;
Pageable pageable = PageRequest.of(0, 10);
List<InfoPostRes> postResList = TestFixture.createMultipleInfoPostRes();
Page<InfoPostRes> expectedPage = new PageImpl<>(postResList, pageable, postResList.size());

given(infoPostRepositorySupport.findInfoPostsByCategoryId(categoryId, pageable)).willReturn(expectedPage);

// When
Page<InfoPostRes> result = infoPostService.getInfoPostsByCategoryId(categoryId, pageable);

// Then
assertThat(result).isNotNull();
assertThat(result.getContent()).hasSize(3);

List<InfoPostRes> content = result.getContent();
assertAll(
() -> assertThat(content.get(0).getRemainingDays()).isEqualTo("D-5"),
() -> assertThat(content.get(1).getRemainingDays()).isEqualTo("D-Day"),
() -> assertThat(content.get(2).getRemainingDays()).isEqualTo("D+3")
);
}
}

0 comments on commit 8a47588

Please sign in to comment.