Skip to content

Commit f8b70b0

Browse files
authored
feat : 프로젝트 목록 요약 API 추가 (#363)
* feat: 전체 프로젝트 요약 조회 기능 추가 * feat: 전체 프로젝트 요약 조회 기능 추가
1 parent 958e939 commit f8b70b0

7 files changed

Lines changed: 151 additions & 36 deletions

File tree

src/main/java/org/ject/support/domain/project/controller/ProjectApiSpec.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.swagger.v3.oas.annotations.tags.Tag;
55
import org.ject.support.domain.project.dto.ProjectDetailResponse;
66
import org.ject.support.domain.project.dto.ProjectResponse;
7+
import org.ject.support.domain.project.dto.ProjectSummaryResponse;
78
import org.ject.support.domain.project.entity.Project;
89
import org.springframework.data.domain.Page;
910
import org.springframework.data.domain.Pageable;
@@ -24,4 +25,9 @@ Page<ProjectResponse> findProjects(@RequestParam final Project.Category category
2425
summary = "프로젝트 상세 조회",
2526
description = "전달한 ID에 해당하는 프로젝트의 상세 내용을 조회합니다.")
2627
ProjectDetailResponse findProjectDetails(@PathVariable final Long projectId);
28+
29+
@Operation(
30+
summary = "프로젝트 현황 조회",
31+
description = "전체 프로젝트의 요약된 현황을 조회합니다.")
32+
ProjectSummaryResponse findProjectSummary();
2733
}

src/main/java/org/ject/support/domain/project/controller/ProjectController.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import lombok.RequiredArgsConstructor;
44
import org.ject.support.domain.project.dto.ProjectDetailResponse;
55
import org.ject.support.domain.project.dto.ProjectResponse;
6+
import org.ject.support.domain.project.dto.ProjectSummaryResponse;
67
import org.ject.support.domain.project.entity.Project;
78
import org.ject.support.domain.project.service.ProjectService;
89
import org.springframework.data.domain.Page;
@@ -33,4 +34,10 @@ public Page<ProjectResponse> findProjects(@RequestParam final Project.Category c
3334
public ProjectDetailResponse findProjectDetails(@PathVariable final Long projectId) {
3435
return projectService.findProjectDetails(projectId);
3536
}
37+
38+
@Override
39+
@GetMapping("/summary")
40+
public ProjectSummaryResponse findProjectSummary() {
41+
return projectService.findProjectSummary();
42+
}
3643
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.ject.support.domain.project.dto;
2+
3+
import org.ject.support.domain.project.entity.Project;
4+
5+
import java.util.ArrayList;
6+
import java.util.EnumMap;
7+
import java.util.List;
8+
import java.util.Map;
9+
10+
public record ProjectSummaryResponse(
11+
List<CategorySummary> categorySummaries
12+
) {
13+
14+
public static ProjectSummaryResponse of(List<Project> projects) {
15+
16+
Map<Project.Category, Long> categoryCountMap = new EnumMap<>(Project.Category.class);
17+
18+
for (Project.Category category : Project.Category.values()) {
19+
categoryCountMap.put(category, 0L);
20+
}
21+
22+
for (Project project : projects) {
23+
Project.Category category = project.getCategory();
24+
categoryCountMap.put(
25+
category,
26+
categoryCountMap.get(category) + 1
27+
);
28+
}
29+
30+
List<CategorySummary> summaries = new ArrayList<>();
31+
32+
summaries.add(new CategorySummary(
33+
"ALL",
34+
(long) projects.size()
35+
));
36+
37+
for (Project.Category category : Project.Category.values()) {
38+
summaries.add(new CategorySummary(
39+
category.name(),
40+
categoryCountMap.get(category)
41+
));
42+
}
43+
44+
return new ProjectSummaryResponse(summaries);
45+
}
46+
47+
public record CategorySummary(
48+
String categoryName,
49+
Long count
50+
) {
51+
}
52+
}

src/main/java/org/ject/support/domain/project/entity/Project.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
package org.ject.support.domain.project.entity;
22

3-
import jakarta.persistence.*;
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Convert;
5+
import jakarta.persistence.Entity;
6+
import jakarta.persistence.EnumType;
7+
import jakarta.persistence.Enumerated;
8+
import jakarta.persistence.FetchType;
9+
import jakarta.persistence.GeneratedValue;
10+
import jakarta.persistence.GenerationType;
11+
import jakarta.persistence.Id;
12+
import jakarta.persistence.JoinColumn;
13+
import jakarta.persistence.ManyToOne;
14+
import jakarta.persistence.OneToMany;
15+
import jakarta.persistence.OrderBy;
416
import lombok.AccessLevel;
517
import lombok.AllArgsConstructor;
618
import lombok.Builder;
@@ -30,7 +42,7 @@ public class Project extends BaseTimeEntity {
3042

3143
@Enumerated(EnumType.STRING)
3244
@Column(columnDefinition = "varchar(30)", nullable = false)
33-
private Project.Category category;
45+
private Category category;
3446

3547
@Column(length = 100, nullable = false)
3648
private String summary;
@@ -69,7 +81,15 @@ public class Project extends BaseTimeEntity {
6981
@Builder.Default
7082
private List<ProjectIntro> projectIntros = new ArrayList<>();
7183

84+
@Getter
85+
@AllArgsConstructor
7286
public enum Category {
73-
MAIN, HACKATHON
87+
88+
SEMESTER_1(1, "1기"),
89+
SEMESTER_2(2, "2기"),
90+
SEMESTER_3(3, "3기");
91+
92+
private final int order;
93+
private final String displayName;
7494
}
7595
}

src/main/java/org/ject/support/domain/project/service/ProjectService.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
package org.ject.support.domain.project.service;
22

3-
import static org.ject.support.domain.project.entity.ProjectIntro.Category.DEV;
4-
import static org.ject.support.domain.project.entity.ProjectIntro.Category.SERVICE;
5-
6-
import java.util.List;
73
import lombok.RequiredArgsConstructor;
84
import org.ject.support.domain.member.dto.TeamMemberNames;
95
import org.ject.support.domain.member.repository.MemberRepository;
106
import org.ject.support.domain.project.dto.ProjectDetailResponse;
117
import org.ject.support.domain.project.dto.ProjectIntroResponse;
128
import org.ject.support.domain.project.dto.ProjectResponse;
9+
import org.ject.support.domain.project.dto.ProjectSummaryResponse;
1310
import org.ject.support.domain.project.entity.Project;
1411
import org.ject.support.domain.project.entity.ProjectIntro;
1512
import org.ject.support.domain.project.exception.ProjectErrorCode;
@@ -21,6 +18,11 @@
2118
import org.springframework.stereotype.Service;
2219
import org.springframework.transaction.annotation.Transactional;
2320

21+
import java.util.List;
22+
23+
import static org.ject.support.domain.project.entity.ProjectIntro.Category.DEV;
24+
import static org.ject.support.domain.project.entity.ProjectIntro.Category.SERVICE;
25+
2426
@Service
2527
@RequiredArgsConstructor
2628
public class ProjectService {
@@ -57,6 +59,16 @@ public ProjectDetailResponse findProjectDetails(final Long projectId) {
5759
return ProjectDetailResponse.toResponse(project, teamMemberNames, serviceIntros, devIntros);
5860
}
5961

62+
/**
63+
* 전체 프로젝트의 요약된 현황을 조회합니다.
64+
*/
65+
@Cacheable(value = "project", key = "'summary'")
66+
@Transactional(readOnly = true)
67+
public ProjectSummaryResponse findProjectSummary() {
68+
List<Project> projects = projectRepository.findAll();
69+
return ProjectSummaryResponse.of(projects);
70+
}
71+
6072
private List<ProjectIntroResponse> mapToResponsesByCategory(List<ProjectIntro> projectIntros,
6173
final ProjectIntro.Category category) {
6274
return projectIntros.stream()

src/test/java/org/ject/support/domain/project/repository/ProjectQueryRepositoryTest.java

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import org.ject.support.domain.project.entity.Project;
77
import org.ject.support.testconfig.QueryDslTestConfig;
88
import org.junit.jupiter.api.BeforeEach;
9-
import org.junit.jupiter.api.DisplayName;
109
import org.junit.jupiter.api.Test;
1110
import org.springframework.beans.factory.annotation.Autowired;
1211
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
@@ -18,8 +17,6 @@
1817
import java.util.List;
1918

2019
import static org.assertj.core.api.Assertions.assertThat;
21-
import static org.ject.support.domain.project.entity.Project.Category.HACKATHON;
22-
import static org.ject.support.domain.project.entity.Project.Category.MAIN;
2320

2421
@Import(QueryDslTestConfig.class)
2522
@DataJpaTest
@@ -42,17 +39,16 @@ void setUp() {
4239
}
4340

4441
@Test
45-
@DisplayName("기수별 프로젝트 목록 조회")
46-
void find_projects_by_semester() {
42+
void 기수별_프로젝트_목록_조회() {
4743
// given
48-
Project project1 = createProject(MAIN, team1);
49-
Project project2 = createProject(MAIN, team2);
50-
Project project3 = createProject(MAIN, team3);
44+
Project project1 = createProject(Project.Category.SEMESTER_1, team1);
45+
Project project2 = createProject(Project.Category.SEMESTER_1, team2);
46+
Project project3 = createProject(Project.Category.SEMESTER_1, team3);
5147
projectRepository.saveAll(List.of(project1, project2, project3));
5248

5349
// when
5450
Page<ProjectResponse> result =
55-
projectRepository.findProjectsByCategoryAndSemester(MAIN, 1L, PageRequest.of(0, 30));
51+
projectRepository.findProjectsByCategoryAndSemester(Project.Category.SEMESTER_1, 1L, PageRequest.of(0, 30));
5652

5753
// then
5854
assertThat(result).isNotNull();
@@ -69,32 +65,30 @@ void find_projects_by_semester() {
6965
}
7066

7167
@Test
72-
@DisplayName("특정 년월에 진행한 해커톤 프로젝트 목록 조회")
73-
void find_hackathon_projects() {
68+
void 프로젝트_카테고리중_2기가진행한_프로젝트_조회() {
7469
// given
75-
Project project1 = createProject(MAIN, team1);
76-
Project project2 = createProject(HACKATHON, team2);
77-
Project project3 = createProject(HACKATHON, team3);
78-
Project project4 = createProject(HACKATHON, team1);
70+
Project project1 = createProject(Project.Category.SEMESTER_1, team1);
71+
Project project2 = createProject(Project.Category.SEMESTER_1, team2);
72+
Project project3 = createProject(Project.Category.SEMESTER_2, team3);
73+
Project project4 = createProject(Project.Category.SEMESTER_2, team1);
7974
projectRepository.saveAll(List.of(project1, project2, project3, project4));
8075

8176
// when
8277
Page<ProjectResponse> result =
83-
projectRepository.findProjectsByCategoryAndSemester(HACKATHON, 1L, PageRequest.of(0, 30));
78+
projectRepository.findProjectsByCategoryAndSemester(Project.Category.SEMESTER_1, 1L, PageRequest.of(0, 30));
8479

8580
// then
8681
assertThat(result.getContent()).hasSize(2);
8782
}
8883

8984
@Test
90-
@DisplayName("techStack이 List<String>와 JSON 문자열 간 정상 변환됨")
91-
void convert_tech_stack() {
85+
void techStack이_ListString와_JSON_문자열_간_정상_변환됨() {
9286
// given
9387
List<String> techStack = List.of("Java", "Spring Boot", "MySQL", "JPA");
9488

9589
Project project = Project.builder()
9690
.name("name")
97-
.category(Project.Category.MAIN)
91+
.category(Project.Category.SEMESTER_1)
9892
.summary("summary")
9993
.techStack(techStack)
10094
.startDate(LocalDate.of(2025, 3, 1))
@@ -111,18 +105,17 @@ void convert_tech_stack() {
111105
}
112106

113107
@Test
114-
@DisplayName("semesterId가 null이면 전체 조회")
115-
void find_all_projects_by_semester_id_null() {
108+
void semesterId가_NULL이면_전체_조회() {
116109
// given
117-
Project project1 = createProject(MAIN, team1);
118-
Project project2 = createProject(MAIN, team2);
119-
Project project3 = createProject(MAIN, team3);
120-
Project project4 = createProject(MAIN, team3);
110+
Project project1 = createProject(Project.Category.SEMESTER_1, team1);
111+
Project project2 = createProject(Project.Category.SEMESTER_1, team2);
112+
Project project3 = createProject(Project.Category.SEMESTER_1, team3);
113+
Project project4 = createProject(Project.Category.SEMESTER_1, team3);
121114
projectRepository.saveAll(List.of(project1, project2, project3, project4));
122115

123116
// when
124117
Page<ProjectResponse> result =
125-
projectRepository.findProjectsByCategoryAndSemester(MAIN, null, PageRequest.of(0, 30));
118+
projectRepository.findProjectsByCategoryAndSemester(Project.Category.SEMESTER_1, null, PageRequest.of(0, 30));
126119

127120
// then
128121
assertThat(result).isNotNull();
@@ -143,5 +136,4 @@ private Project createProject(Project.Category category, Team team) {
143136
.team(team)
144137
.build();
145138
}
146-
147139
}

src/test/java/org/ject/support/domain/project/service/ProjectServiceTest.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.ject.support.domain.member.repository.MemberRepository;
77
import org.ject.support.domain.project.dto.ProjectDetailResponse;
88
import org.ject.support.domain.project.dto.ProjectIntroResponse;
9+
import org.ject.support.domain.project.dto.ProjectSummaryResponse;
910
import org.ject.support.domain.project.entity.Project;
1011
import org.ject.support.domain.project.entity.ProjectIntro;
1112
import org.ject.support.domain.project.exception.ProjectException;
@@ -23,7 +24,6 @@
2324
import static org.assertj.core.api.Assertions.assertThat;
2425
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2526
import static org.ject.support.domain.project.entity.ProjectIntro.Category;
26-
import static org.ject.support.domain.project.entity.ProjectIntro.builder;
2727
import static org.mockito.Mockito.when;
2828

2929
class ProjectServiceTest extends UnitTestSupport {
@@ -132,8 +132,34 @@ void setUp() {
132132
assertThat(result.techStack()).containsExactly("java", "Spring", "JPA", "QueryDSL", "MySQL", "AWS");
133133
}
134134

135+
@Test
136+
void 전체_프로젝트_요약_조회() {
137+
Project p1 = Project.builder().id(10L).category(Project.Category.SEMESTER_1).build();
138+
Project p2 = Project.builder().id(11L).category(Project.Category.SEMESTER_2).build();
139+
Project p3 = Project.builder().id(12L).category(Project.Category.SEMESTER_2).build();
140+
141+
when(projectRepository.findAll()).thenReturn(List.of(p1, p2, p3));
142+
143+
// when
144+
ProjectSummaryResponse summary = projectService.findProjectSummary();
145+
146+
// then
147+
assertThat(summary.categorySummaries()).hasSize(1 + Project.Category.values().length);
148+
assertThat(summary.categorySummaries().get(0).categoryName()).isEqualTo("ALL");
149+
assertThat(summary.categorySummaries().get(0).count()).isEqualTo(3L);
150+
151+
assertThat(summary.categorySummaries().get(1).categoryName()).isEqualTo(Project.Category.SEMESTER_1.name());
152+
assertThat(summary.categorySummaries().get(1).count()).isEqualTo(1L);
153+
154+
assertThat(summary.categorySummaries().get(2).categoryName()).isEqualTo(Project.Category.SEMESTER_2.name());
155+
assertThat(summary.categorySummaries().get(2).count()).isEqualTo(2L);
156+
157+
assertThat(summary.categorySummaries().get(3).categoryName()).isEqualTo(Project.Category.SEMESTER_3.name());
158+
assertThat(summary.categorySummaries().get(3).count()).isEqualTo(0L);
159+
}
160+
135161
private ProjectIntro createProjectIntro(Long id, String imageUrl, Category category, int sequence) {
136-
return builder()
162+
return ProjectIntro.builder()
137163
.id(id)
138164
.imageUrl(imageUrl)
139165
.category(category)

0 commit comments

Comments
 (0)