Skip to content

Commit 4ffa70b

Browse files
authored
feat: Jectalk 엔티티 스펙 변경 (#412)
* feat: ContentType enum 추가하여 콘텐츠 유형 정의 * feat: Jectalk 엔티티 및 DTO 구조 변경 * feat: JectalkQueryRepository 반환 필드 수정 * test: JectalkQueryRepositoryTest 수정으로 반환 필드 검증 업데이트 * feat: Jectalk 테이블 구조 변경 및 데이터 마이그레이션으로 인한 Flyway migration 추 * feat: Jectalk 테이블 content_type 컬럼 기본값 추가 * feat: Category 필드 및 필터링 추가
1 parent 4f50d17 commit 4ffa70b

10 files changed

Lines changed: 181 additions & 34 deletions

File tree

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package org.ject.support.domain.jectalk.controller;
22

33
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.Parameter;
45
import io.swagger.v3.oas.annotations.tags.Tag;
56
import org.ject.support.domain.jectalk.dto.JectalkResponse;
7+
import org.ject.support.domain.project.entity.Project;
68
import org.springframework.data.domain.Page;
79
import org.springframework.data.domain.Pageable;
810
import org.springframework.data.web.PageableDefault;
11+
import org.springframework.web.bind.annotation.RequestParam;
912

1013
@Tag(name = "Jectalk", description = "젝톡 API")
1114
public interface JectalkApiSpec {
@@ -14,5 +17,8 @@ public interface JectalkApiSpec {
1417
summary = "젝톡 목록 조회",
1518
description = "젝톡 목록을 조회합니다."
1619
)
17-
Page<JectalkResponse> findJectalks(@PageableDefault(size = 12) Pageable pageable);
20+
Page<JectalkResponse> findJectalks(
21+
@PageableDefault(size = 12) Pageable pageable,
22+
@Parameter(description = "기수 (SEMESTER_1, SEMESTER_2, SEMESTER_3)", example = "SEMESTER_1")
23+
@RequestParam(required = false) Project.Category category);
1824
}

src/main/java/org/ject/support/domain/jectalk/controller/JectalkController.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
import lombok.RequiredArgsConstructor;
44
import org.ject.support.domain.jectalk.dto.JectalkResponse;
55
import org.ject.support.domain.jectalk.service.JectalkService;
6+
import org.ject.support.domain.project.entity.Project;
67
import org.springframework.data.domain.Page;
78
import org.springframework.data.domain.Pageable;
89
import org.springframework.data.web.PageableDefault;
910
import org.springframework.web.bind.annotation.GetMapping;
1011
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RequestParam;
1113
import org.springframework.web.bind.annotation.RestController;
1214

1315
@RestController
@@ -19,7 +21,9 @@ public class JectalkController implements JectalkApiSpec {
1921

2022
@Override
2123
@GetMapping
22-
public Page<JectalkResponse> findJectalks(@PageableDefault(size = 12) Pageable pageable) {
23-
return jectalkService.findJectalks(pageable);
24+
public Page<JectalkResponse> findJectalks(
25+
@PageableDefault(size = 12) Pageable pageable,
26+
@RequestParam(required = false) Project.Category category) {
27+
return jectalkService.findJectalks(pageable, category);
2428
}
2529
}

src/main/java/org/ject/support/domain/jectalk/dto/JectalkResponse.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
22

33
import com.querydsl.core.annotations.QueryProjection;
44
import lombok.Builder;
5+
import org.ject.support.domain.jectalk.enums.ContentType;
56

67
@Builder
7-
public record JectalkResponse(Long id, String name, String youtubeUrl, String imageUrl, String summary) {
8+
public record JectalkResponse(
9+
Long id,
10+
String title,
11+
String description,
12+
String contentUrl,
13+
ContentType contentType,
14+
String thumbnailUrl,
15+
String summary) {
816

917
@QueryProjection
1018
public JectalkResponse {

src/main/java/org/ject/support/domain/jectalk/entity/Jectalk.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44
import lombok.AccessLevel;
55
import lombok.AllArgsConstructor;
66
import lombok.Builder;
7+
import lombok.Getter;
78
import lombok.NoArgsConstructor;
89
import org.ject.support.domain.base.BaseTimeEntity;
10+
import org.ject.support.domain.jectalk.enums.ContentType;
11+
import org.ject.support.domain.project.entity.Project.Category;
912

1013
@Entity
14+
@Getter
1115
@Builder
1216
@AllArgsConstructor(access = AccessLevel.PRIVATE)
1317
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@@ -18,14 +22,25 @@ public class Jectalk extends BaseTimeEntity {
1822
private Long id;
1923

2024
@Column(length = 50, nullable = false)
21-
private String name;
25+
private String title;
2226

2327
@Column(nullable = false)
24-
private String summary;
28+
private String description;
29+
30+
@Column(nullable = false)
31+
@Enumerated(EnumType.STRING)
32+
private ContentType contentType;
2533

2634
@Column(length = 2083)
27-
private String youtubeUrl;
35+
private String contentUrl;
2836

2937
@Column(length = 2083)
30-
private String imageUrl;
38+
private String thumbnailUrl;
39+
40+
@Column(nullable = false)
41+
private String author;
42+
43+
@Enumerated(EnumType.STRING)
44+
@Column(columnDefinition = "varchar(30)", nullable = false)
45+
private Category category;
3146
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.ject.support.domain.jectalk.enums;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@AllArgsConstructor
8+
public enum ContentType {
9+
YOUTUBE("YouTube"),
10+
;
11+
12+
private final String name;
13+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package org.ject.support.domain.jectalk.repository;
22

33
import org.ject.support.domain.jectalk.dto.JectalkResponse;
4+
import org.ject.support.domain.project.entity.Project;
45
import org.springframework.data.domain.Page;
56
import org.springframework.data.domain.Pageable;
67

78
public interface JectalkQueryRepository {
8-
Page<JectalkResponse> findJectalks(Pageable pageable);
9+
Page<JectalkResponse> findJectalks(Pageable pageable, Project.Category category);
910
}
Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,57 @@
11
package org.ject.support.domain.jectalk.repository;
22

3+
import static org.ject.support.domain.jectalk.entity.QJectalk.jectalk;
4+
5+
import com.querydsl.core.types.dsl.BooleanExpression;
36
import com.querydsl.jpa.impl.JPAQuery;
47
import com.querydsl.jpa.impl.JPAQueryFactory;
58
import java.util.List;
9+
import java.util.Optional;
610
import lombok.RequiredArgsConstructor;
711
import org.ject.support.common.data.PageResponse;
812
import org.ject.support.domain.jectalk.dto.JectalkResponse;
913
import org.ject.support.domain.jectalk.dto.QJectalkResponse;
14+
import org.ject.support.domain.project.entity.Project;
1015
import org.springframework.data.domain.Page;
1116
import org.springframework.data.domain.Pageable;
1217
import org.springframework.stereotype.Repository;
1318

14-
import static org.ject.support.domain.jectalk.entity.QJectalk.jectalk;
15-
1619
@Repository
1720
@RequiredArgsConstructor
1821
public class JectalkQueryRepositoryImpl implements JectalkQueryRepository {
1922

2023
private final JPAQueryFactory queryFactory;
2124

2225
@Override
23-
public Page<JectalkResponse> findJectalks(Pageable pageable) {
26+
public Page<JectalkResponse> findJectalks(Pageable pageable, Project.Category category) {
2427
List<JectalkResponse> content = queryFactory
2528
.select(new QJectalkResponse(
2629
jectalk.id,
27-
jectalk.name,
28-
jectalk.youtubeUrl,
29-
jectalk.imageUrl,
30-
jectalk.summary
30+
jectalk.title,
31+
jectalk.description,
32+
jectalk.contentUrl,
33+
jectalk.contentType,
34+
jectalk.thumbnailUrl,
35+
jectalk.author
3136
))
3237
.from(jectalk)
38+
.where(categoryEq(category))
3339
.orderBy(jectalk.id.desc())
3440
.offset(pageable.getOffset())
3541
.limit(pageable.getPageSize())
3642
.fetch();
3743

3844
JPAQuery<Long> countQuery = queryFactory
3945
.select(jectalk.count())
40-
.from(jectalk);
46+
.from(jectalk)
47+
.where(categoryEq(category));
4148

4249
return PageResponse.from(content, pageable, countQuery.fetchFirst());
4350
}
51+
52+
private BooleanExpression categoryEq(Project.Category category) {
53+
return Optional.ofNullable(category)
54+
.map(jectalk.category::eq)
55+
.orElse(null);
56+
}
4457
}

src/main/java/org/ject/support/domain/jectalk/service/JectalkService.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import lombok.RequiredArgsConstructor;
44
import org.ject.support.domain.jectalk.dto.JectalkResponse;
55
import org.ject.support.domain.jectalk.repository.JectalkRepository;
6+
import org.ject.support.domain.project.entity.Project;
67
import org.springframework.cache.annotation.Cacheable;
78
import org.springframework.data.domain.Page;
89
import org.springframework.data.domain.Pageable;
@@ -15,9 +16,9 @@ public class JectalkService {
1516

1617
private final JectalkRepository jectalkRepository;
1718

18-
@Cacheable(value = "jectalk", key = "#pageable.pageNumber + ':' + #pageable.pageSize")
19+
@Cacheable(value = "jectalk", key = "#pageable.pageNumber + ':' + #pageable.pageSize + ':' + (#category != null ? #category.name() : 'ALL')")
1920
@Transactional(readOnly = true)
20-
public Page<JectalkResponse> findJectalks(Pageable pageable) {
21-
return jectalkRepository.findJectalks(pageable);
21+
public Page<JectalkResponse> findJectalks(Pageable pageable, Project.Category category) {
22+
return jectalkRepository.findJectalks(pageable, category);
2223
}
2324
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-- Jectalk 테이블 구조 변경
2+
-- 기존: name, summary, youtube_url, image_url
3+
-- 변경: title, description, content_type, content_url, thumbnail_url, author, category
4+
5+
-- author 컬럼 추가
6+
ALTER TABLE jectalk ADD COLUMN author VARCHAR(255) NOT NULL DEFAULT '';
7+
8+
-- 기존 summary 데이터를 author로 마이그레이션
9+
UPDATE jectalk SET author = summary;
10+
11+
-- content_type 컬럼 추가
12+
ALTER TABLE jectalk ADD COLUMN content_type VARCHAR(50) NOT NULL DEFAULT 'YOUTUBE';
13+
14+
-- category 컬럼 추가
15+
ALTER TABLE jectalk ADD COLUMN category VARCHAR(30) NOT NULL DEFAULT 'SEMESTER_1';
16+
17+
-- 컬럼명 변경
18+
ALTER TABLE jectalk CHANGE COLUMN name title VARCHAR(50) NOT NULL;
19+
ALTER TABLE jectalk CHANGE COLUMN summary description VARCHAR(255) NOT NULL;
20+
ALTER TABLE jectalk CHANGE COLUMN youtube_url content_url VARCHAR(2083) NULL;
21+
ALTER TABLE jectalk CHANGE COLUMN image_url thumbnail_url VARCHAR(2083) NULL;
22+
23+
-- DEFAULT 제거
24+
ALTER TABLE jectalk MODIFY COLUMN author VARCHAR(255) NOT NULL;
25+
ALTER TABLE jectalk MODIFY COLUMN content_type VARCHAR(50) NOT NULL;
26+
ALTER TABLE jectalk MODIFY COLUMN category VARCHAR(30) NOT NULL;
27+

src/test/java/org/ject/support/domain/jectalk/repository/JectalkQueryRepositoryTest.java

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.List;
44
import org.ject.support.domain.jectalk.dto.JectalkResponse;
55
import org.ject.support.domain.jectalk.entity.Jectalk;
6+
import org.ject.support.domain.project.entity.Project;
67
import org.ject.support.testconfig.IntegrationTest;
78
import org.junit.jupiter.api.DisplayName;
89
import org.junit.jupiter.api.Test;
@@ -30,7 +31,7 @@ void findJectalks() {
3031
jectalkRepository.saveAll(List.of(jectalk1, jectalk2, jectalk3));
3132

3233
// when
33-
Page<JectalkResponse> result = jectalkRepository.findJectalks(PageRequest.of(0, 2));
34+
Page<JectalkResponse> result = jectalkRepository.findJectalks (PageRequest.of(0, 2), null);
3435

3536
// then
3637
assertThat(result).isNotNull();
@@ -43,30 +44,88 @@ void findJectalks() {
4344
assertThat(responses).hasSize(2); // 현재 페이지의 데이터 개수
4445
responses.forEach(jectalkResponse -> {
4546
assertThat(jectalkResponse.id()).isNotNull();
46-
assertThat(jectalkResponse.name()).isNotNull();
47-
assertThat(jectalkResponse.imageUrl()).isNotNull();
48-
assertThat(jectalkResponse.youtubeUrl()).isNotNull();
47+
assertThat(jectalkResponse.title()).isNotNull();
48+
assertThat(jectalkResponse.thumbnailUrl()).isNotNull();
49+
assertThat(jectalkResponse.contentUrl()).isNotNull();
50+
assertThat(jectalkResponse.description()).isNotNull();
51+
assertThat(jectalkResponse.contentType()).isNotNull();
4952
assertThat(jectalkResponse.summary()).isNotNull();
5053
});
51-
5254
JectalkResponse firstResponse = responses.get(0);
53-
assertThat(firstResponse.name()).isEqualTo("젝톡 3"); // ID 내림차순이므로 마지막에 생성된 데이터가 첫 번째
54-
assertThat(firstResponse.summary()).isEqualTo("summary");
55-
assertThat(firstResponse.youtubeUrl()).isEqualTo("https://youtube.com/jectalk3");
56-
assertThat(firstResponse.imageUrl()).isEqualTo("https://image.com/jectalk3.png");
55+
assertThat(firstResponse.title()).isEqualTo("젝톡 3"); // ID 내림차순이므로 마지막에 생성된 데이터가 첫 번째
56+
assertThat(firstResponse.description()).isEqualTo("description");
57+
assertThat(firstResponse.contentUrl()).isEqualTo("https://youtube.com/jectalk3");
58+
assertThat(firstResponse.thumbnailUrl()).isEqualTo("https://image.com/jectalk3.png");
59+
assertThat(firstResponse.summary()).isEqualTo("author");
60+
5761

5862
// 두 번째 페이지 조회
59-
Page<JectalkResponse> secondPage = jectalkRepository.findJectalks(PageRequest.of(1, 2));
63+
Page<JectalkResponse> secondPage = jectalkRepository.findJectalks(PageRequest.of(1, 2), null);
6064
assertThat(secondPage.getContent()).hasSize(1); // 마지막 페이지는 1개의 데이터만 존재
6165
}
6266

67+
@Test
68+
@DisplayName("젝톡 목록 조회 - 카테고리 필터링")
69+
void findJectalksByCategory() {
70+
// given
71+
Jectalk jectalk1 = createJectalkWithCategory("젝톡 1기-1", Project.Category.SEMESTER_1);
72+
Jectalk jectalk2 = createJectalkWithCategory("젝톡 1기-2", Project.Category.SEMESTER_1);
73+
Jectalk jectalk3 = createJectalkWithCategory("젝톡 2기-1", Project.Category.SEMESTER_2);
74+
Jectalk jectalk4 = createJectalkWithCategory("젝톡 2기-2", Project.Category.SEMESTER_2);
75+
jectalkRepository.saveAll(List.of(jectalk1, jectalk2, jectalk3, jectalk4));
76+
77+
// when - 1기 조회
78+
Page<JectalkResponse> result1 = jectalkRepository.findJectalks(PageRequest.of(0, 10), Project.Category.SEMESTER_1);
79+
80+
// then
81+
assertThat(result1.getTotalElements()).isEqualTo(2);
82+
83+
// when - 2기 조회
84+
Page<JectalkResponse> result2 = jectalkRepository.findJectalks(PageRequest.of(0, 10), Project.Category.SEMESTER_2);
85+
86+
// then
87+
assertThat(result2.getTotalElements()).isEqualTo(2);
88+
}
89+
90+
@Test
91+
@DisplayName("젝톡 목록 조회 - 카테고리 미지정 시 전체 조회")
92+
void findJectalksWithoutCategory() {
93+
// given
94+
Jectalk jectalk1 = createJectalkWithCategory("젝톡 1기", Project.Category.SEMESTER_1);
95+
Jectalk jectalk2 = createJectalkWithCategory("젝톡 2기", Project.Category.SEMESTER_2);
96+
Jectalk jectalk3 = createJectalkWithCategory("젝톡 3기", Project.Category.SEMESTER_3);
97+
jectalkRepository.saveAll(List.of(jectalk1, jectalk2, jectalk3));
98+
99+
// when - category=null로 전체 조회
100+
Page<JectalkResponse> result = jectalkRepository.findJectalks(PageRequest.of(0, 10), null);
101+
102+
// then - 모든 기수의 젝톡이 조회됨
103+
assertThat(result.getTotalElements()).isEqualTo(3);
104+
}
105+
63106
private Jectalk createJectalk(String name) {
64107
String urlSafeName = "jectalk" + name.replaceAll("[젝톡 ]", "");
65108
return Jectalk.builder()
66-
.name(name)
67-
.summary("summary")
68-
.youtubeUrl("https://youtube.com/" + urlSafeName)
69-
.imageUrl("https://image.com/" + urlSafeName + ".png")
109+
.title(name)
110+
.description("description")
111+
.contentType(org.ject.support.domain.jectalk.enums.ContentType.YOUTUBE)
112+
.contentUrl("https://youtube.com/" + urlSafeName)
113+
.thumbnailUrl("https://image.com/" + urlSafeName + ".png")
114+
.author("author")
115+
.category(Project.Category.SEMESTER_1)
116+
.build();
117+
}
118+
119+
private Jectalk createJectalkWithCategory(String name, Project.Category category) {
120+
String urlSafeName = "jectalk" + name.replaceAll("[젝톡 기\\-]", "");
121+
return Jectalk.builder()
122+
.title(name)
123+
.description("description")
124+
.contentType(org.ject.support.domain.jectalk.enums.ContentType.YOUTUBE)
125+
.contentUrl("https://youtube.com/" + urlSafeName)
126+
.thumbnailUrl("https://image.com/" + urlSafeName + ".png")
127+
.author("author")
128+
.category(category)
70129
.build();
71130
}
72131
}

0 commit comments

Comments
 (0)