Skip to content

Commit 2b02301

Browse files
authored
[Feat] 진행 중인 투표 목록 조회 API 개발 (#50)
* feat: 진행 중인 카드 목록 조회 API 응답 DTO 생성 (#36) * refactor: 투표 응답 DTO를 기능별로 list/와 result/ 하위 패키지로 분리 (#36) * feat: 커스텀 에러 코드 추가 (#36) * feat: 진행 중 투표 목록용 커서 파서 추가(#36) * feat: QueryDSL 환경 설정 및 Q 클래스 생성 설정 추가 (#36) - querydsl-core, querydsl-jpa(jakarta) 의존성 추가 - annotationProcessor 설정을 통한 Q 클래스 자동 생성 설정 - generated/querydsl 디렉토리 설정 및 sourceSets 등록 - JPAQueryFactory 빈 등록을 위한 QuerydslConfig 생성 - QueryDSL 관련 캐시 및 빌드 이슈 해결 * refactor: config 패키지 구조를 기능별로 분리 (security, querydsl, jpa) (#36) * feat: 진행 중인 투표 목록 조회용 QueryDSL 리포지토리 구현 (#36) - 사용자가 접근 가능한 그룹(공개 그룹 + 참여 그룹)의 진행 중인 투표만 조회 - 커서 기반 페이지네이션 조건 (closedAt, createdAt) - 사용자 참여 여부 필터링 (응답하지 않은 투표만) * chore: Hibernate SQL 디버깅을 위한 trace 로그 레벨 설정 (#36) - org.hibernate.type.descriptor.sql 로그 레벨을 trace로 설정하여 바인딩 값 확인 가능 * feat: 진행 중인 투표 목록 조회 서비스 로직 구현 (#36) - getActiveVotes(): 비로그인/로그인 분기 처리 및 커서 기반 페이지네이션 지원 - getAccessibleGroups(): 공개 그룹 + 사용자 소속 그룹 권한 기반 접근 목록 반환 - VoteListResponse 구성 및 hasNext, nextCursor 처리 포함 * feat: 진행 중인 투표 목록 조회 API 컨트롤러 구현 및 비로그인 접근 허용 설정 (#36) * feat: 미래 시간 커서 사용 시 성공 응답 처리 및 INVALID_CURSOR 예외 삭제(#36)
1 parent c586342 commit 2b02301

21 files changed

+341
-22
lines changed

build.gradle

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,39 @@ dependencies {
3838
testImplementation 'org.springframework.boot:spring-boot-starter-test'
3939
testImplementation 'org.springframework.security:spring-security-test'
4040
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
41+
42+
// querydsl 의존성
43+
implementation "com.querydsl:querydsl-core:5.1.0"
44+
implementation "com.querydsl:querydsl-jpa:5.1.0:jakarta"
45+
46+
annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta"
47+
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
48+
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
49+
50+
// testImplementation 'org.springframework.boot:spring-boot-starter-test'
51+
// testImplementation 'jakarta.persistence:jakarta.persistence-api'
52+
// testImplementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
4153
}
4254

4355
tasks.named('test') {
4456
useJUnitPlatform()
4557
}
58+
59+
// QueryDSL Q파일 생성용
60+
def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile
61+
62+
sourceSets {
63+
main {
64+
java {
65+
srcDir querydslDir
66+
}
67+
}
68+
}
69+
70+
tasks.withType(JavaCompile).configureEach {
71+
options.generatedSourceOutputDirectory.set(querydslDir)
72+
}
73+
74+
compileJava {
75+
options.annotationProcessorPath = configurations.annotationProcessor
76+
}

src/main/java/com/moa/moa_server/config/JpaAuditingConfig.java renamed to src/main/java/com/moa/moa_server/config/jpa/JpaAuditingConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.moa.moa_server.config;
1+
package com.moa.moa_server.config.jpa;
22

33
import org.springframework.context.annotation.Configuration;
44
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.moa.moa_server.config.querydsl;
2+
3+
import com.querydsl.jpa.impl.JPAQueryFactory;
4+
import jakarta.persistence.EntityManager;
5+
import jakarta.persistence.PersistenceContext;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
9+
@Configuration
10+
public class QuerydslConfig {
11+
12+
@PersistenceContext
13+
private EntityManager em;
14+
15+
@Bean
16+
public JPAQueryFactory jpaQueryFactory() {
17+
return new JPAQueryFactory(em);
18+
}
19+
}

src/main/java/com/moa/moa_server/config/DevSecurityConfig.java renamed to src/main/java/com/moa/moa_server/config/security/DevSecurityConfig.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
package com.moa.moa_server.config;
1+
package com.moa.moa_server.config.security;
22

3-
import com.moa.moa_server.config.security.CustomAuthenticationEntryPoint;
4-
import com.moa.moa_server.config.security.JwtAuthenticationFilter;
53
import lombok.RequiredArgsConstructor;
64
import org.springframework.beans.factory.annotation.Value;
75
import org.springframework.context.annotation.Bean;
86
import org.springframework.context.annotation.Configuration;
97
import org.springframework.context.annotation.Profile;
8+
import org.springframework.http.HttpMethod;
109
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
1110
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
1211
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@@ -21,7 +20,7 @@
2120

2221
import java.util.List;
2322

24-
import static com.moa.moa_server.config.SecurityConstants.ALLOWED_URLS;
23+
import static com.moa.moa_server.config.security.SecurityConstants.ALLOWED_URLS;
2524

2625
@RequiredArgsConstructor
2726
@EnableWebSecurity
@@ -44,6 +43,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, CorsConfigurat
4443
.formLogin(AbstractHttpConfigurer::disable)
4544
.exceptionHandling(ex -> ex.authenticationEntryPoint(customAuthenticationEntryPoint))
4645
.authorizeHttpRequests(auth -> auth
46+
.requestMatchers(HttpMethod.GET, "/api/v1/votes").permitAll()
4747
.requestMatchers(ALLOWED_URLS).permitAll()
4848
.anyRequest().authenticated()
4949
)

src/main/java/com/moa/moa_server/config/LocalSecurityConfig.java renamed to src/main/java/com/moa/moa_server/config/security/LocalSecurityConfig.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
package com.moa.moa_server.config;
1+
package com.moa.moa_server.config.security;
22

3-
import com.moa.moa_server.config.security.CustomAuthenticationEntryPoint;
4-
import com.moa.moa_server.config.security.JwtAuthenticationFilter;
53
import lombok.RequiredArgsConstructor;
64
import org.springframework.context.annotation.Bean;
75
import org.springframework.context.annotation.Configuration;
86
import org.springframework.context.annotation.Profile;
7+
import org.springframework.http.HttpMethod;
98
import org.springframework.security.config.Customizer;
109
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
1110
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@@ -15,7 +14,7 @@
1514
import org.springframework.security.web.SecurityFilterChain;
1615
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
1716

18-
import static com.moa.moa_server.config.SecurityConstants.ALLOWED_URLS;
17+
import static com.moa.moa_server.config.security.SecurityConstants.ALLOWED_URLS;
1918

2019
@RequiredArgsConstructor
2120
@Configuration
@@ -34,6 +33,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
3433
.formLogin(AbstractHttpConfigurer::disable)
3534
.exceptionHandling(ex -> ex.authenticationEntryPoint(customAuthenticationEntryPoint))
3635
.authorizeHttpRequests(auth -> auth
36+
.requestMatchers(HttpMethod.GET, "/api/v1/votes").permitAll()
3737
.requestMatchers(ALLOWED_URLS).permitAll()
3838
.anyRequest().authenticated()
3939
)

src/main/java/com/moa/moa_server/config/ProdSecurityConfig.java renamed to src/main/java/com/moa/moa_server/config/security/ProdSecurityConfig.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
package com.moa.moa_server.config;
1+
package com.moa.moa_server.config.security;
22

33
import org.springframework.context.annotation.Bean;
44
import org.springframework.context.annotation.Configuration;
55
import org.springframework.context.annotation.Profile;
66
import org.springframework.security.config.Customizer;
77
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
8-
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
98
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
109
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
1110
import org.springframework.security.crypto.password.PasswordEncoder;
1211
import org.springframework.security.web.SecurityFilterChain;
1312

14-
import static com.moa.moa_server.config.SecurityConstants.ALLOWED_URLS;
13+
import static com.moa.moa_server.config.security.SecurityConstants.ALLOWED_URLS;
1514

1615
@Configuration
1716
@Profile("prod")

src/main/java/com/moa/moa_server/config/SecurityConstants.java renamed to src/main/java/com/moa/moa_server/config/security/SecurityConstants.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.moa.moa_server.config;
1+
package com.moa.moa_server.config.security;
22

33
public class SecurityConstants {
44
public static final String[] ALLOWED_URLS = {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.moa.moa_server.domain.global.cursor;
2+
3+
import com.moa.moa_server.domain.vote.handler.VoteErrorCode;
4+
import com.moa.moa_server.domain.vote.handler.VoteException;
5+
import lombok.extern.slf4j.Slf4j;
6+
7+
import java.time.LocalDateTime;
8+
import java.time.format.DateTimeFormatter;
9+
10+
@Slf4j
11+
public record VoteClosedCursor(LocalDateTime closedAt, LocalDateTime createdAt) {
12+
13+
/**
14+
* "closedAt_createdAt" 형식의 커서를 LocalDateTime 쌍으로 파싱
15+
*/
16+
public static VoteClosedCursor parse(String cursor) {
17+
try {
18+
String[] parts = cursor.split("_");
19+
if (parts.length != 2) {
20+
log.warn("[VoteClosedCursor#parse] Cursor must contain exactly two parts.");
21+
throw new VoteException(VoteErrorCode.INVALID_CURSOR_FORMAT);
22+
}
23+
return new VoteClosedCursor(
24+
LocalDateTime.parse(parts[0]),
25+
LocalDateTime.parse(parts[1])
26+
);
27+
} catch (Exception e) {
28+
log.warn("[VoteClosedCursor#parse] Failed to parse cursor '{}': {}", cursor, e.toString());
29+
throw new VoteException(VoteErrorCode.INVALID_CURSOR_FORMAT);
30+
}
31+
}
32+
33+
public String encode() {
34+
DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
35+
return closedAt.format(formatter) + "_" + createdAt.format(formatter);
36+
}
37+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.moa.moa_server.domain.group.handler;
2+
3+
import com.moa.moa_server.domain.global.exception.BaseErrorCode;
4+
import org.springframework.http.HttpStatus;
5+
6+
public enum GroupErrorCode implements BaseErrorCode {
7+
GROUP_NOT_FOUND(HttpStatus.NOT_FOUND),;
8+
9+
private final HttpStatus status;
10+
11+
GroupErrorCode(HttpStatus status) { this.status = status; }
12+
13+
public HttpStatus getStatus() { return status; }
14+
}

src/main/java/com/moa/moa_server/domain/group/repository/GroupMemberRepository.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@
55
import com.moa.moa_server.domain.user.entity.User;
66
import org.springframework.data.jpa.repository.JpaRepository;
77
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
89

10+
import java.util.List;
911
import java.util.Optional;
1012

1113
public interface GroupMemberRepository extends JpaRepository<GroupMember, Long> {
1214
@Query("SELECT gm FROM GroupMember gm WHERE gm.group = :group AND gm.user = :user")
1315
Optional<GroupMember> findByGroupAndUserIncludingDeleted(Group group, User user);
16+
17+
@Query("SELECT gm.group FROM GroupMember gm WHERE gm.user = :user AND gm.deletedAt IS NULL")
18+
List<Group> findAllActiveGroupsByUser(@Param("user") User user);
1419
}

0 commit comments

Comments
 (0)