From 8c5b73033df41f5ca59f7d93198f0950f2cba5e4 Mon Sep 17 00:00:00 2001 From: parkmineum Date: Sat, 15 Feb 2025 15:29:33 +0900 Subject: [PATCH 01/17] =?UTF-8?q?[feat]=20QueryDSL=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 39 +++++++++++++++++-- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 3 +- .../global/config/QueryDslConfig.java | 19 +++++++++ 4 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/umc/yeogi_gal_lae/global/config/QueryDslConfig.java diff --git a/build.gradle b/build.gradle index 88bd9a49..23369e3f 100644 --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,6 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' //security - implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'io.jsonwebtoken:jjwt-api:0.11.2' @@ -60,7 +59,12 @@ dependencies { implementation 'com.h2database:h2:2.2.220' implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-jdbc' - + + // QueryDSL + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" } @@ -71,4 +75,33 @@ tasks.named('test') { test { // 모든 테스트를 스킵하도록 설정 (임시) enabled = false -} \ No newline at end of file +} +// +//// Q타입 클래스 생성 경로 +// +//configurations.all { +// exclude group: "javax.persistence", module: "javax.persistence-api" +//} +// +//tasks.withType(JavaCompile).configureEach { +// options.compilerArgs << "-Aquerydsl.jpa.packageSuffix=.jpa" +// options.compilerArgs << "-Aquerydsl.jpa.imports=jakarta.persistence.*" +//} +// +//// Q타입 클래스 생성 경로 +//def generated = "$buildDir/generated/sources/annotationProcessor/java/main" +// +//// QueryDSL QClass 파일 생성 위치 설정 +//tasks.withType(JavaCompile).configureEach { +// options.getGeneratedSourceOutputDirectory().set(file(generated)) +//} +// +//// java source set에 QueryDSL QClass 위치 추가 +//sourceSets { +// main.java.srcDirs += [generated] +//} +// +//// gradle clean 시 QClass 디렉토리 삭제 +//clean { +// delete file(generated) +//} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e2847c82..b82aa23a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6d..f3b75f3b 100755 --- a/gradlew +++ b/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/src/main/java/com/umc/yeogi_gal_lae/global/config/QueryDslConfig.java b/src/main/java/com/umc/yeogi_gal_lae/global/config/QueryDslConfig.java new file mode 100644 index 00000000..20f6bb0e --- /dev/null +++ b/src/main/java/com/umc/yeogi_gal_lae/global/config/QueryDslConfig.java @@ -0,0 +1,19 @@ +package com.umc.yeogi_gal_lae.global.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +@Configuration +@RequiredArgsConstructor +public class QueryDslConfig { + + private final EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() {return new JPAQueryFactory(entityManager);} +} + From 0d0ef8cc81edc740b2f07b9ea6c4efb5b15c579b Mon Sep 17 00:00:00 2001 From: parkmineum Date: Sat, 15 Feb 2025 15:30:45 +0900 Subject: [PATCH 02/17] =?UTF-8?q?[feat]=20QClass=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/build.gradle b/build.gradle index 23369e3f..1a1f6be0 100644 --- a/build.gradle +++ b/build.gradle @@ -75,33 +75,4 @@ tasks.named('test') { test { // 모든 테스트를 스킵하도록 설정 (임시) enabled = false -} -// -//// Q타입 클래스 생성 경로 -// -//configurations.all { -// exclude group: "javax.persistence", module: "javax.persistence-api" -//} -// -//tasks.withType(JavaCompile).configureEach { -// options.compilerArgs << "-Aquerydsl.jpa.packageSuffix=.jpa" -// options.compilerArgs << "-Aquerydsl.jpa.imports=jakarta.persistence.*" -//} -// -//// Q타입 클래스 생성 경로 -//def generated = "$buildDir/generated/sources/annotationProcessor/java/main" -// -//// QueryDSL QClass 파일 생성 위치 설정 -//tasks.withType(JavaCompile).configureEach { -// options.getGeneratedSourceOutputDirectory().set(file(generated)) -//} -// -//// java source set에 QueryDSL QClass 위치 추가 -//sourceSets { -// main.java.srcDirs += [generated] -//} -// -//// gradle clean 시 QClass 디렉토리 삭제 -//clean { -// delete file(generated) -//} \ No newline at end of file +} \ No newline at end of file From d80d9a3b449c61294a0cf9ad280a70c22f53487a Mon Sep 17 00:00:00 2001 From: parkmineum Date: Tue, 18 Feb 2025 03:38:07 +0900 Subject: [PATCH 03/17] =?UTF-8?q?[chore]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-test.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/main/resources/application-test.yml diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 00000000..5757a24b --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,18 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb # 메모리 DB 사용 (테스트 환경에서 적합) + driver-class-name: org.h2.Driver + username: sa + password: + h2: + console: + enabled: true + path: /h2-console + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: create-drop # 테스트 시 자동으로 테이블 생성 후 삭제 (create-drop) + properties: + hibernate: + format_sql: true + show_sql: true From e7b43234695fcc26039842546f5e3aa01b54344f Mon Sep 17 00:00:00 2001 From: parkmineum Date: Tue, 18 Feb 2025 04:36:28 +0900 Subject: [PATCH 04/17] =?UTF-8?q?[chore]=20=EB=A0=88=EB=94=94=EC=8A=A4=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EC=84=B8=ED=8C=85=20=EB=B0=8F=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../global/config/RedisConfig.java | 84 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 src/main/java/com/umc/yeogi_gal_lae/global/config/RedisConfig.java diff --git a/build.gradle b/build.gradle index 1a1f6be0..a805fdd7 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,7 @@ dependencies { implementation 'mysql:mysql-connector-java:8.0.30' implementation 'com.h2database:h2:2.2.220' implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.redisson:redisson-spring-boot-starter:3.22.0' implementation 'org.springframework.boot:spring-boot-starter-jdbc' // QueryDSL diff --git a/src/main/java/com/umc/yeogi_gal_lae/global/config/RedisConfig.java b/src/main/java/com/umc/yeogi_gal_lae/global/config/RedisConfig.java new file mode 100644 index 00000000..860eaa9c --- /dev/null +++ b/src/main/java/com/umc/yeogi_gal_lae/global/config/RedisConfig.java @@ -0,0 +1,84 @@ +package com.umc.yeogi_gal_lae.global.config; + + +import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.SimpleKeyGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.cache.annotation.EnableCaching; + + + +@Configuration +@EnableCaching +public class RedisConfig { + + // Redisson 설정 (분산 락용) + @Bean + public RedissonClient redissonClient() { + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://redis1:6379") // Docker 컨테이너 이름 사용 + .setConnectionPoolSize(10) + .setRetryAttempts(3) + .setRetryInterval(2000); + + return Redisson.create(config); + } + + // Redis 연결 팩토리 (Spring, Redis 연결) + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory("redis1", 6379); // Docker 컨테이너 이름 사용 + } + + // RedisTemplate 설정 (Redis 캐싱 및 데이터 저장용) + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + + // Key, Value 직렬화 설정 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + + return redisTemplate; + } + + // Spring CacheManager 설정 (Redis 캐싱) + @Bean + public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { + RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(10)) // 캐시 TTL 10분 설정 + .disableCachingNullValues() + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); + + return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)) + .cacheDefaults(cacheConfig) + .build(); + } + + // 캐시 Key 생성 전략 (기본 설정) + @Bean + public SimpleKeyGenerator keyGenerator() { + return new SimpleKeyGenerator(); + } +} From 8d10274bb067da59257c64281333014245a2c403 Mon Sep 17 00:00:00 2001 From: parkmineum Date: Wed, 19 Feb 2025 03:24:12 +0900 Subject: [PATCH 05/17] =?UTF-8?q?[chore]=20Redis=20ObjectMapper=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +- .../global/config/ModuleConfig.java | 17 +++++++ .../global/config/RedisConfig.java | 45 ++++++++++++------- 3 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/umc/yeogi_gal_lae/global/config/ModuleConfig.java diff --git a/build.gradle b/build.gradle index a805fdd7..fd30be42 100644 --- a/build.gradle +++ b/build.gradle @@ -48,8 +48,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'io.github.cdimascio:dotenv-java:3.0.0' - - // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' @@ -66,6 +64,8 @@ dependencies { annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' } diff --git a/src/main/java/com/umc/yeogi_gal_lae/global/config/ModuleConfig.java b/src/main/java/com/umc/yeogi_gal_lae/global/config/ModuleConfig.java new file mode 100644 index 00000000..1dda43c3 --- /dev/null +++ b/src/main/java/com/umc/yeogi_gal_lae/global/config/ModuleConfig.java @@ -0,0 +1,17 @@ +package com.umc.yeogi_gal_lae.global.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ModuleConfig { + + @Bean + public ObjectMapper objectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + return objectMapper; + } +} \ No newline at end of file diff --git a/src/main/java/com/umc/yeogi_gal_lae/global/config/RedisConfig.java b/src/main/java/com/umc/yeogi_gal_lae/global/config/RedisConfig.java index 860eaa9c..2db8b753 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/global/config/RedisConfig.java +++ b/src/main/java/com/umc/yeogi_gal_lae/global/config/RedisConfig.java @@ -1,6 +1,10 @@ package com.umc.yeogi_gal_lae.global.config; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; import org.springframework.cache.interceptor.SimpleKeyGenerator; import org.springframework.context.annotation.Bean; @@ -28,55 +32,66 @@ @EnableCaching public class RedisConfig { - // Redisson 설정 (분산 락용) + @Value("${spring.data.redis.host}") + private String redisHost; + + @Value("${spring.data.redis.port}") + private int redisPort; + @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer() - .setAddress("redis://redis1:6379") // Docker 컨테이너 이름 사용 - .setConnectionPoolSize(10) + .setAddress("redis://" + redisHost + ":" + redisPort) + .setConnectionPoolSize(35) + .setConnectionMinimumIdleSize(35) .setRetryAttempts(3) .setRetryInterval(2000); return Redisson.create(config); } - // Redis 연결 팩토리 (Spring, Redis 연결) @Bean public RedisConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory("redis1", 6379); // Docker 컨테이너 이름 사용 + return new LettuceConnectionFactory(redisHost, redisPort); } - // RedisTemplate 설정 (Redis 캐싱 및 데이터 저장용) @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory, ObjectMapper objectMapper) { RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); - // Key, Value 직렬화 설정 + // Redis에서 LocalDateTime 지원하는 ObjectMapper 사용 + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper); + redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.setValueSerializer(serializer); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); - redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.setHashValueSerializer(serializer); return redisTemplate; } - // Spring CacheManager 설정 (Redis 캐싱) + // CacheManager에서도 ObjectMapper 적용 @Bean - public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { + public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, ObjectMapper objectMapper) { + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(Duration.ofMinutes(10)) // 캐시 TTL 10분 설정 + .entryTtl(Duration.ofMinutes(10)) .disableCachingNullValues() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper))); return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)) .cacheDefaults(cacheConfig) .build(); } - // 캐시 Key 생성 전략 (기본 설정) @Bean public SimpleKeyGenerator keyGenerator() { return new SimpleKeyGenerator(); From 8c6019cb0acbaa1a2813e735efdd5e04f3bbe6ee Mon Sep 17 00:00:00 2001 From: parkmineum Date: Wed, 19 Feb 2025 03:27:15 +0900 Subject: [PATCH 06/17] =?UTF-8?q?[chore]=20.yml=20Redis=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-prod.yml | 2 +- src/main/resources/application.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 590ee8c2..86d81de8 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -21,7 +21,7 @@ spring: enabled: false data: redis: - host: redis + host: redis1 port: 6379 mvc: static-path-pattern: /static/** diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0fda36e0..8efbddb1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -18,6 +18,11 @@ spring: show_sql: true flyway: enabled: false + data: + redis: + host: localhost + port: 6379 + timeout: 5000ms springdoc: api-docs: enabled: true From f7a6a04d410965a67547151c1bb99a47ef60572e Mon Sep 17 00:00:00 2001 From: parkmineum Date: Wed, 19 Feb 2025 04:56:15 +0900 Subject: [PATCH 07/17] =?UTF-8?q?[feat]=20Vote=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20JSON=20=EB=B3=80=ED=99=98=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc/yeogi_gal_lae/api/vote/domain/Vote.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/vote/domain/Vote.java b/src/main/java/com/umc/yeogi_gal_lae/api/vote/domain/Vote.java index a6ca68d0..fdf5f501 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/vote/domain/Vote.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/vote/domain/Vote.java @@ -1,11 +1,15 @@ package com.umc.yeogi_gal_lae.api.vote.domain; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.umc.yeogi_gal_lae.api.tripPlan.domain.TripPlan; import com.umc.yeogi_gal_lae.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; +import java.time.LocalDateTime; + @Builder @Getter @Setter @NoArgsConstructor @@ -19,17 +23,19 @@ public class Vote extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "trip_plan_id", nullable = false) + @JsonIgnore private TripPlan tripPlan; - // 특정 사용자의 투표 결과 조회 빈번하게 일어날 것으로 예상되기에, VoteRoom 을 통하지 않고 바로 Use 와 매핑 - // VoteRoom 은 투표 방 자체를 관리하고, Vote 는 사용자들의 실제 투표 데이터를 관리하므로 역할 분리 - // 투표 데이터를 추가하거나 변경할 때 VoteRoom 에 불필요한 데이터가 섞이지 않기 위함 - @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "vote_room_id", nullable = false) + @JsonIgnore private VoteRoom voteRoom; @Enumerated(EnumType.STRING) @Column(nullable = false) private VoteType type; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + @Override + public LocalDateTime getCreatedAt() { return super.getCreatedAt(); } } From b4f688149268c0210249afee296136d4165decc2 Mon Sep 17 00:00:00 2001 From: parkmineum Date: Wed, 19 Feb 2025 19:17:03 +0900 Subject: [PATCH 08/17] =?UTF-8?q?[refactor]=20=EB=B6=84=EC=82=B0=20?= =?UTF-8?q?=EB=9D=BD=20=ED=95=A0=EB=8B=B9=EC=9D=84=20=ED=86=B5=ED=95=9C=20?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=EC=84=B1=20=EC=A0=9C=EC=96=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/vote/service/VoteService.java | 108 ++++++++++++------ .../global/config/RedisConfig.java | 3 +- .../yeogi_gal_lae/global/error/ErrorCode.java | 2 + 3 files changed, 80 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/VoteService.java b/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/VoteService.java index 65082ae8..74100a2f 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/VoteService.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/VoteService.java @@ -13,6 +13,7 @@ import com.umc.yeogi_gal_lae.api.vote.domain.VoteRoom; import com.umc.yeogi_gal_lae.api.vote.domain.VoteType; import com.umc.yeogi_gal_lae.api.notification.service.NotificationService; +import org.redisson.api.RedissonClient; import com.umc.yeogi_gal_lae.api.vote.dto.request.VoteRequest; @@ -21,13 +22,17 @@ import com.umc.yeogi_gal_lae.api.vote.repository.VoteRoomRepository; import com.umc.yeogi_gal_lae.global.error.BusinessException; import com.umc.yeogi_gal_lae.global.error.ErrorCode; -import jakarta.persistence.EntityNotFoundException; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.client.RedisException; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static com.umc.yeogi_gal_lae.global.error.ErrorCode.*; @@ -45,6 +50,9 @@ public class VoteService { private final NotificationService notificationService; private final RoomMemberRepository roomMemberRepository; + private RedissonClient redissonClient; + private CacheManager cacheManager; + @Transactional(readOnly = true) public VoteResponse.VoteInfoDTO getTripPlanInfoForVote(Long tripId, Long roomId , String userEmail){ @@ -61,47 +69,76 @@ public VoteResponse.VoteInfoDTO getTripPlanInfoForVote(Long tripId, Long roomId @Transactional public void createVote(VoteRequest.createVoteReq request, String userEmail){ - // 유저 이메일로 검증 User user = userRepository.findByEmail(userEmail).orElseThrow(()-> new BusinessException(ErrorCode.USER_NOT_FOUND)); TripPlan tripPlan = tripPlanRepository.findById(request.getTripId()).orElseThrow(()-> new BusinessException(ErrorCode.TRIP_PLAN_NOT_FOUND)); VoteRoom voteRoom = voteRoomRepository.findByTripPlanId(tripPlan.getId()).orElseThrow(() -> new BusinessException(VOTE_ROOM_NOT_FOUND)); - - - Vote vote = voteRepository.findByTripPlanId(tripPlan.getId()) // DB 에 Vote 객체가 있다면, - .orElseGet(() -> voteRepository.save(Vote.builder() // 없다면, Vote 객체 생성 + VoteType voteType = VoteType.valueOf(request.getType().trim().toUpperCase()); + + // Key 기반의 분산 락을 적용하므로써, 사용자의 동시 투표 방지 + RLock lock = redissonClient.getLock("voteLock:" + userEmail); + boolean isLocked = false; + try{ + isLocked = lock.tryLock(5, 10, TimeUnit.SECONDS); + log.info("락 획득 여부: {}, 현재 스레드가 락을 보유하고 있는가? {}", isLocked, lock.isHeldByCurrentThread()); + if (!isLocked) { throw new BusinessException(ErrorCode.VOTE_CONCURRENT_UPDATE);} + + // 락 획득 성공 시, 레디스에 저장된 투표 데이터를 확인하여 캐싱된 데이터가 있을 시, DB 조회 x + Vote vote = getCachedVoteByTripPlan(tripPlan.getId()); + if (vote == null) { + vote = voteRepository.save(Vote.builder() .tripPlan(tripPlan) .voteRoom(voteRoom) - .type(VoteType.valueOf(request.getType().trim().toUpperCase())) // 초기 타입 설정 - .build())); - // 투표 시작 알림 생성 - notificationService.createStartNotification(tripPlan.getRoom().getName(), user.getUsername(), user.getEmail(),NotificationType.VOTE_START, tripPlan.getId(), tripPlan.getTripPlanType()); - - // 기존 투표 이력 확인 - Vote currentVote = user.getVote(); - - if (currentVote == null) { user.setVote(vote); } - else if (currentVote.getTripPlan().getId().equals(tripPlan.getId())) { - VoteType requestedType = VoteType.valueOf(request.getType().trim().toUpperCase()); - - if (currentVote.getType().equals(requestedType)) { throw new IllegalArgumentException("같은 타입으로 중복 투표는 불가능합니다.");} - - currentVote.setType(requestedType); - voteRepository.save(currentVote); - } else { - user.setVote(vote); + .type(voteType) + .build()); + + // 새로 생성된 경우 캐싱 + Vote finalVote = vote; + Optional.ofNullable(cacheManager.getCache("votes")) + .ifPresent(cache -> cache.put(tripPlan.getId(), finalVote)); + } + + // 투표 시작 알림 생성 + notificationService.createStartNotification(tripPlan.getRoom().getName(), user.getUsername(), user.getEmail(),NotificationType.VOTE_START, tripPlan.getId(), tripPlan.getTripPlanType()); + + // 기존 투표 이력 확인 + Vote currentVote = user.getVote(); + if (currentVote == null) { + user.setVote(vote); + } + else if (currentVote.getTripPlan().getId().equals(tripPlan.getId())) { + if (Thread.currentThread().isInterrupted()) { + log.warn("BusinessException 발생 후 스레드가 인터럽트된 상태 - 사용자: {}", userEmail); + Thread.interrupted(); // 인터럽트 상태 초기화 + } + if (currentVote.getType().equals(voteType)) { throw new BusinessException(ErrorCode.DUPLICATE_VOTE_NOT_ALLOWED);} + + currentVote.setType(voteType); + voteRepository.save(currentVote); + + // 사용자가 이미 투표한 내역을 변경할 경우, 레디스에 저장된 데이터를 삭제하여 최신화 + Optional.ofNullable(cacheManager.getCache("votes")) + .ifPresent(cache -> cache.evict(tripPlan.getId())); + } else { + user.setVote(vote); + } + userRepository.save(user); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new BusinessException(ErrorCode.VOTE_CONCURRENT_UPDATE); + } catch (Exception e) { + log.error("예외 발생 함. 사용자: {} - 예외 타입: {}", userEmail, e.getClass().getName(), e); + throw e; + }finally { + log.info("현재 스레드 사용자 {} 에 대한 락을 해제합니다.", userEmail); + if (isLocked && lock.isHeldByCurrentThread()) { lock.unlock(); } } - - userRepository.save(user); } public List getVoteResults(String userEmail, Long tripId){ - Long userId = userRepository.findByEmail(userEmail) - .orElseThrow(()-> new BusinessException.UserNotFoundException("요청하신 이메일과 일치하는 유저가 존재하지 않습니다.")) - .getId(); - - if (!tripPlanRepository.existsById(tripId)) { throw new EntityNotFoundException("여행 계획을 찾을 수 없습니다.");} - + Long userId = userRepository.findByEmail(userEmail).orElseThrow(()-> new BusinessException(ErrorCode.USER_NOT_FOUND)).getId(); + if (!tripPlanRepository.existsById(tripId)) { throw new BusinessException(TRIP_PLAN_NOT_FOUND);} List users = userRepository.findUsersByVoteTripPlanId(tripId); @@ -124,4 +161,11 @@ public List getVoteResults(String userEmail, Long tripId return List.of(goodResponse, badResponse); } + + // 캐싱 공간 및 키 할당 + @Cacheable(value = "votes", key = "#tripPlanId") + public Vote getCachedVoteByTripPlan(Long tripPlanId) { + return voteRepository.findByTripPlanId(tripPlanId).orElse(null); + } + } \ No newline at end of file diff --git a/src/main/java/com/umc/yeogi_gal_lae/global/config/RedisConfig.java b/src/main/java/com/umc/yeogi_gal_lae/global/config/RedisConfig.java index 2db8b753..cfd3f82e 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/global/config/RedisConfig.java +++ b/src/main/java/com/umc/yeogi_gal_lae/global/config/RedisConfig.java @@ -46,7 +46,8 @@ public RedissonClient redissonClient() { .setConnectionPoolSize(35) .setConnectionMinimumIdleSize(35) .setRetryAttempts(3) - .setRetryInterval(2000); + .setRetryInterval(2000) + .setConnectTimeout(30000); // 락 자동 연장 시간 return Redisson.create(config); } diff --git a/src/main/java/com/umc/yeogi_gal_lae/global/error/ErrorCode.java b/src/main/java/com/umc/yeogi_gal_lae/global/error/ErrorCode.java index 737cbb28..a715193b 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/global/error/ErrorCode.java +++ b/src/main/java/com/umc/yeogi_gal_lae/global/error/ErrorCode.java @@ -42,6 +42,8 @@ public enum ErrorCode implements BaseStatus { VOTE_NOT_COMPLETED_YET(HttpStatus.BAD_REQUEST, "VOTE_400", "아직 투표가 종료되지 않았습니다."), VOTE_ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "VOTE_401", "요청 하신 투표 방을 찾을 수 없습니다."), VOTE_RESULT_FAILED(HttpStatus.BAD_REQUEST, "VOTE_403", "여행 확정에 실패하셨습니다. 이 방은 사라집니다."), + DUPLICATE_VOTE_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "VOTE_404", "중복 투표는 불가능합니다."), + VOTE_CONCURRENT_UPDATE(HttpStatus.BAD_REQUEST, "VOTE_404", "동시 투표는 이용이 제한 됩니다."), // Room Member Error ROOM_MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "ROOM_MEMBER_404", "방에 멤버가 존재하지 않습니다."), From 9d78c5c5069f19e16533cc2faff4c7a7c3fed737 Mon Sep 17 00:00:00 2001 From: parkmineum Date: Thu, 20 Feb 2025 04:53:31 +0900 Subject: [PATCH 09/17] =?UTF-8?q?[chore]=20QueryDSL=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 +++++- .../com/umc/yeogi_gal_lae/global/config/QueryDslConfig.java | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index fd30be42..9da18342 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ dependencies { // QueryDSL implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' - annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" @@ -76,4 +76,8 @@ tasks.named('test') { test { // 모든 테스트를 스킵하도록 설정 (임시) enabled = false +} + +clean { + delete file('src/main/generated') } \ No newline at end of file diff --git a/src/main/java/com/umc/yeogi_gal_lae/global/config/QueryDslConfig.java b/src/main/java/com/umc/yeogi_gal_lae/global/config/QueryDslConfig.java index 20f6bb0e..273fab99 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/global/config/QueryDslConfig.java +++ b/src/main/java/com/umc/yeogi_gal_lae/global/config/QueryDslConfig.java @@ -14,6 +14,8 @@ public class QueryDslConfig { private final EntityManager entityManager; @Bean - public JPAQueryFactory jpaQueryFactory() {return new JPAQueryFactory(entityManager);} + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } } From 889cea063f672f00c22cca2bc376862ae221f161 Mon Sep 17 00:00:00 2001 From: parkmineum Date: Thu, 20 Feb 2025 04:53:54 +0900 Subject: [PATCH 10/17] =?UTF-8?q?[chore]=20=ED=88=AC=ED=91=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/vote/service/VoteService.java | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/VoteService.java b/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/VoteService.java index 74100a2f..2442c593 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/VoteService.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/VoteService.java @@ -1,5 +1,7 @@ package com.umc.yeogi_gal_lae.api.vote.service; +import com.querydsl.core.Tuple; +import com.querydsl.jpa.impl.JPAQueryFactory; import com.umc.yeogi_gal_lae.api.notification.domain.NotificationType; import com.umc.yeogi_gal_lae.api.room.domain.Room; import com.umc.yeogi_gal_lae.api.room.repository.RoomMemberRepository; @@ -9,12 +11,14 @@ import com.umc.yeogi_gal_lae.api.user.domain.User; import com.umc.yeogi_gal_lae.api.user.repository.UserRepository; import com.umc.yeogi_gal_lae.api.vote.converter.VoteConverter; +import com.umc.yeogi_gal_lae.api.vote.domain.QVote; import com.umc.yeogi_gal_lae.api.vote.domain.Vote; import com.umc.yeogi_gal_lae.api.vote.domain.VoteRoom; import com.umc.yeogi_gal_lae.api.vote.domain.VoteType; import com.umc.yeogi_gal_lae.api.notification.service.NotificationService; import org.redisson.api.RedissonClient; +import com.umc.yeogi_gal_lae.api.user.domain.QUser; import com.umc.yeogi_gal_lae.api.vote.dto.request.VoteRequest; import com.umc.yeogi_gal_lae.api.vote.dto.VoteResponse; @@ -25,7 +29,6 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RLock; -import org.redisson.client.RedisException; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @@ -52,6 +55,7 @@ public class VoteService { private RedissonClient redissonClient; private CacheManager cacheManager; + private final JPAQueryFactory queryFactory; @Transactional(readOnly = true) public VoteResponse.VoteInfoDTO getTripPlanInfoForVote(Long tripId, Long roomId , String userEmail){ @@ -140,24 +144,36 @@ public List getVoteResults(String userEmail, Long tripId Long userId = userRepository.findByEmail(userEmail).orElseThrow(()-> new BusinessException(ErrorCode.USER_NOT_FOUND)).getId(); if (!tripPlanRepository.existsById(tripId)) { throw new BusinessException(TRIP_PLAN_NOT_FOUND);} - List users = userRepository.findUsersByVoteTripPlanId(tripId); - - // 현재 접속한 사용자에 대한 투표 데이터 - Optional userVote = users.stream() - .filter(user -> user.getId().equals(userId)) + QUser qUser = QUser.user; + QVote qVote = QVote.vote; + List voteData = queryFactory + .select(qUser.id, qVote.type) + .from(qUser) + .leftJoin(qVote).on(qUser.vote.eq(qVote)) + .where(qVote.tripPlan.id.eq(tripId)) + .fetch(); + + // 사용자 투표 확인 + Optional userVote = voteData.stream() + .filter(tuple -> tuple.get(qUser.id).equals(userId)) .findFirst(); - // 투표 데이터를 type 이름 기준('GOOD ','BAD')으로 그룹화, 타입 당 투표 수 계산 // {"GOOD": 3, "BAD": 2} - Map groupedVotes = users.stream() - .filter(user -> user.getVote() != null && user.getVote().getType() != null) // 투표한 데이터만 카운팅 - .map(user -> user.getVote().getType().name()) + Map groupedVotes = voteData.stream() + .map(tuple -> tuple.get(qVote.type)) // type 추출 + .filter(Objects::nonNull) + .map(Enum::name) // Enum -> String 변환 .collect(Collectors.groupingBy( typeName -> typeName, Collectors.counting()) ); - VoteResponse.ResultDTO goodResponse = VoteConverter.convert("GOOD", userVote.orElse(null), groupedVotes); - VoteResponse.ResultDTO badResponse = VoteConverter.convert("BAD", userVote.orElse(null), groupedVotes); + // 사용자 투표 확인 (Tuple -> User) + User userVoteData = userVote + .map(tuple -> userRepository.findById(tuple.get(qUser.id)).orElse(null)) + .orElse(null); + VoteResponse.ResultDTO goodResponse = VoteConverter.convert("GOOD", userVoteData, groupedVotes); + VoteResponse.ResultDTO badResponse = VoteConverter.convert("BAD", userVoteData, groupedVotes); + return List.of(goodResponse, badResponse); } From 6d84ca2d7228a179da353cf493c0294fefa94580 Mon Sep 17 00:00:00 2001 From: Gwanghyeon-k Date: Thu, 20 Feb 2025 15:02:02 +0900 Subject: [PATCH 11/17] =?UTF-8?q?[feat]=20budget=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc/yeogi_gal_lae/api/budget/dto/BudgetDetailResponse.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/budget/dto/BudgetDetailResponse.java b/src/main/java/com/umc/yeogi_gal_lae/api/budget/dto/BudgetDetailResponse.java index 80c26105..d1e2a058 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/budget/dto/BudgetDetailResponse.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/budget/dto/BudgetDetailResponse.java @@ -12,6 +12,7 @@ @AllArgsConstructor @Builder public class BudgetDetailResponse { + private String location; private String imageUrl; private LocalDate startDate; private LocalDate endDate; From 68e8c93d50516115573a9aae4915061e97379a6a Mon Sep 17 00:00:00 2001 From: Gwanghyeon-k Date: Thu, 20 Feb 2025 15:02:19 +0900 Subject: [PATCH 12/17] =?UTF-8?q?[feat]=20dto=20=EB=B3=80=ED=99=98=20conve?= =?UTF-8?q?rter=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/budget/converter/BudgetConverter.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/budget/converter/BudgetConverter.java b/src/main/java/com/umc/yeogi_gal_lae/api/budget/converter/BudgetConverter.java index 7cf78d84..afe492b8 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/budget/converter/BudgetConverter.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/budget/converter/BudgetConverter.java @@ -2,6 +2,7 @@ import com.umc.yeogi_gal_lae.api.budget.domain.Budget; import com.umc.yeogi_gal_lae.api.budget.dto.BudgetAssignment; +import com.umc.yeogi_gal_lae.api.budget.dto.BudgetDetailResponse; import com.umc.yeogi_gal_lae.api.budget.dto.BudgetResponse; import com.umc.yeogi_gal_lae.api.budget.dto.DailyBudgetAssignmentResponse; import java.util.List; @@ -33,4 +34,17 @@ public static List toDailyBudgetAssignmentRespons .build()) .collect(Collectors.toList()); } + + public static BudgetDetailResponse toBudgetDetailResponse(Budget budget, + Map> budgetMap) { + List dailyAssignments = toDailyBudgetAssignmentResponseList(budgetMap); + return BudgetDetailResponse.builder() + .dailyAssignments(dailyAssignments) + .location(budget.getAiCourse().getTripPlan().getLocation()) + .imageUrl(budget.getAiCourse().getTripPlan().getImageUrl()) + .startDate(budget.getAiCourse().getTripPlan().getStartDate()) + .endDate(budget.getAiCourse().getTripPlan().getEndDate()) + .build(); + } + } From 831a27e81ade93aaeaa201750e459c478fc544ef Mon Sep 17 00:00:00 2001 From: Gwanghyeon-k Date: Thu, 20 Feb 2025 15:02:36 +0900 Subject: [PATCH 13/17] =?UTF-8?q?[feat]=20=ED=94=84=EB=A1=AC=ED=94=84?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95,=20controller=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../budget/controller/BudgetController.java | 18 +----------------- .../api/budget/service/BudgetService.java | 7 +++---- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/budget/controller/BudgetController.java b/src/main/java/com/umc/yeogi_gal_lae/api/budget/controller/BudgetController.java index 4bb8c844..d83f089c 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/budget/controller/BudgetController.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/budget/controller/BudgetController.java @@ -6,13 +6,11 @@ import com.umc.yeogi_gal_lae.api.budget.dto.BudgetAssignment; import com.umc.yeogi_gal_lae.api.budget.dto.BudgetDetailResponse; import com.umc.yeogi_gal_lae.api.budget.dto.BudgetResponse; -import com.umc.yeogi_gal_lae.api.budget.dto.DailyBudgetAssignmentResponse; import com.umc.yeogi_gal_lae.api.budget.repository.BudgetRepository; import com.umc.yeogi_gal_lae.api.budget.service.BudgetService; import com.umc.yeogi_gal_lae.global.common.response.Response; import com.umc.yeogi_gal_lae.global.error.ErrorCode; import com.umc.yeogi_gal_lae.global.success.SuccessCode; -import java.time.LocalDate; import java.util.Collections; import java.util.List; import java.util.Map; @@ -56,21 +54,7 @@ public Response getBudget(@PathVariable Long budgetId) { } Budget budget = budgetOpt.get(); Map> budgetMap = budgetService.getBudgetMapById(budgetId); - List dailyAssignments = BudgetConverter.toDailyBudgetAssignmentResponseList( - budgetMap); - - // AICourse를 통해 TripPlan 정보를 조회 (TripPlan 클래스가 startDate와 endDate를 LocalDate 타입으로 제공한다고 가정) - String imageUrl = budget.getAiCourse().getTripPlan().getImageUrl(); - LocalDate startDate = budget.getAiCourse().getTripPlan().getStartDate(); - LocalDate endDate = budget.getAiCourse().getTripPlan().getEndDate(); - - BudgetDetailResponse detailResponse = BudgetDetailResponse.builder() - .dailyAssignments(dailyAssignments) - .imageUrl(imageUrl) - .startDate(startDate) - .endDate(endDate) - .build(); - + BudgetDetailResponse detailResponse = BudgetConverter.toBudgetDetailResponse(budget, budgetMap); return Response.of(SuccessCode.OK, detailResponse); } diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/budget/service/BudgetService.java b/src/main/java/com/umc/yeogi_gal_lae/api/budget/service/BudgetService.java index 5d0a519c..0835f57e 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/budget/service/BudgetService.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/budget/service/BudgetService.java @@ -80,6 +80,7 @@ private String buildBudgetPrompt(String scheduleJson) { prompt.append("Given the following travel schedule in JSON format: ") .append(scheduleJson) .append(", generate budget recommendations for each day. "); + prompt.append("단, 'placeName' 필드의 값은 반드시 한국어로만 작성되어야 하며, 영어 표현은 사용하지 마세요. "); prompt.append( "For each place, assign exactly one budget type (one of MEAL, ACTIVITY, SHOPPING, TRANSPORT) and a recommended amount. "); prompt.append( @@ -125,7 +126,7 @@ private Map> parseBudgetGptResponse(String gptRes if (jsonStart != -1) { content = content.substring(jsonStart); } - return objectMapper.readValue(content, new TypeReference>>() { + return objectMapper.readValue(content, new TypeReference<>() { }); } catch (Exception e) { e.printStackTrace(); @@ -144,13 +145,11 @@ public Map> getBudgetMapById(Long id) { } try { String budgetJson = budgetOpt.get().getBudgetJson(); - return objectMapper.readValue(budgetJson, new TypeReference>>() { + return objectMapper.readValue(budgetJson, new TypeReference<>() { }); } catch (Exception e) { e.printStackTrace(); return Collections.emptyMap(); } } - - } From 9b7df09002ae03278386915d35ec5d8d25bfa9bd Mon Sep 17 00:00:00 2001 From: GithubKangMin <158579562+GithubKangMin@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:30:46 +0900 Subject: [PATCH 14/17] =?UTF-8?q?[refactor]=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=97=86=EC=9C=BC=EB=A9=B4=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/notification/controller/NotificationController.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/notification/controller/NotificationController.java b/src/main/java/com/umc/yeogi_gal_lae/api/notification/controller/NotificationController.java index 351741e6..8606f450 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/notification/controller/NotificationController.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/notification/controller/NotificationController.java @@ -4,6 +4,7 @@ import com.umc.yeogi_gal_lae.api.notification.service.NotificationService; import com.umc.yeogi_gal_lae.api.notification.domain.NotificationType; import com.umc.yeogi_gal_lae.api.tripPlan.types.TripPlanType; +import com.umc.yeogi_gal_lae.api.user.domain.User; import com.umc.yeogi_gal_lae.api.vote.AuthenticatedUserUtils; import com.umc.yeogi_gal_lae.global.common.response.Response; import com.umc.yeogi_gal_lae.global.success.SuccessCode; @@ -59,6 +60,10 @@ public ResponseEntity> createEndNotification( */ @GetMapping public ResponseEntity>> getAllNotifications() { + + //로그인 없을시 에러냄 + String userEmail = AuthenticatedUserUtils.getAuthenticatedUserEmail(); + List notifications = notificationService.getAllNotifications(); return ResponseEntity.ok(Response.of(SuccessCode.NOTIFICATION_FETCH_OK, notifications)); } From 1ae551d3f25bea06d241d5bac0bdc3437088f53d Mon Sep 17 00:00:00 2001 From: GithubKangMin <158579562+GithubKangMin@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:44:13 +0900 Subject: [PATCH 15/17] =?UTF-8?q?[refactor]=20=EB=B0=A9=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=EB=B3=B8=EC=9D=B8=EB=A7=8C=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=EB=B0=A9=EC=9D=98=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=95=88=EB=90=98=EB=8A=94=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/umc/yeogi_gal_lae/api/room/service/RoomService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/room/service/RoomService.java b/src/main/java/com/umc/yeogi_gal_lae/api/room/service/RoomService.java index 62c78e89..48397927 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/room/service/RoomService.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/room/service/RoomService.java @@ -167,10 +167,9 @@ public RoomListResponse getRoomsByUserId(Long userId) { .roomId(room.getId()) .roomName(room.getName()) .members(room.getRoomMembers().stream() - .filter(member -> !member.getUser().getId().equals(userId)) // 본인 제외 .map(member -> new SimpleRoomMemberResponse( member.getUser().getId(), - member.getUser().getProfileImage() // 프로필 이미지 추가 + member.getUser().getId().equals(userId) ? null : member.getUser().getProfileImage() // 본인 프로필만 null 처리 )) .collect(Collectors.toList())) .build()) From 17b2dac1dd361882488e232cc54d25efad2330d9 Mon Sep 17 00:00:00 2001 From: parkmineum Date: Thu, 20 Feb 2025 16:54:24 +0900 Subject: [PATCH 16/17] =?UTF-8?q?[fix]=20=EC=97=AC=ED=96=89=20=ED=99=95?= =?UTF-8?q?=EC=A0=95=20=ED=8C=90=EB=8B=A8=20=EC=97=AC=EB=B6=80=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/vote/dto/request/VoteRequest.java | 1 + .../api/vote/dto/request/VoteRoomRequest.java | 5 +- .../vote/service/ValidVoteResultService.java | 89 +++++++++---------- 3 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/vote/dto/request/VoteRequest.java b/src/main/java/com/umc/yeogi_gal_lae/api/vote/dto/request/VoteRequest.java index 4b73da5d..120f73f6 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/vote/dto/request/VoteRequest.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/vote/dto/request/VoteRequest.java @@ -6,6 +6,7 @@ import net.minidev.json.annotate.JsonIgnore; +@NoArgsConstructor public class VoteRequest { diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/vote/dto/request/VoteRoomRequest.java b/src/main/java/com/umc/yeogi_gal_lae/api/vote/dto/request/VoteRoomRequest.java index c1ec268a..00136b56 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/vote/dto/request/VoteRoomRequest.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/vote/dto/request/VoteRoomRequest.java @@ -1,12 +1,12 @@ package com.umc.yeogi_gal_lae.api.vote.dto.request; import jakarta.validation.constraints.NotNull; -import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; -@Builder @Getter +@NoArgsConstructor public class VoteRoomRequest { @NotNull @@ -17,5 +17,4 @@ public class VoteRoomRequest { @NotNull private Long voteRoomId; - } \ No newline at end of file diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/ValidVoteResultService.java b/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/ValidVoteResultService.java index 7e44ad7e..b0e2fb12 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/ValidVoteResultService.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/vote/service/ValidVoteResultService.java @@ -14,7 +14,6 @@ import com.umc.yeogi_gal_lae.api.vote.repository.VoteRoomRepository; import com.umc.yeogi_gal_lae.global.error.BusinessException; import com.umc.yeogi_gal_lae.global.error.ErrorCode; -import jakarta.persistence.EntityNotFoundException; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -41,64 +40,52 @@ public class ValidVoteResultService { public boolean validResult(VoteRoomRequest voteRoomRequest) { // 투표 완료 여부 확인 - if (!checkVoteCompleted(voteRoomRequest)) { throw new BusinessException(ErrorCode.VOTE_NOT_COMPLETED_YET); } + if (!checkVoteCompleted(voteRoomRequest)) { throw new BusinessException(ErrorCode.VOTE_NOT_COMPLETED_YET);} VoteRoom voteRoom = findVoteRoomById(voteRoomRequest.getVoteRoomId()); TripPlan tripPlan = voteRoom.getTripPlan(); + VoteCounts voteCounts = countVotes(tripPlan.getId()); - if (isVoteTimeExpired(voteRoom, tripPlan)) { - voteRoomRepository.delete(voteRoom); - return true; // 재투표 - } - - // 찬성/반대 투표 집계 - List votes = voteRepository.findAllVotesByTripPlanId(tripPlan.getId()); - long goodVotes = votes.stream().filter(v -> v.getType() == VoteType.GOOD).count(); - long badVotes = votes.stream().filter(v -> v.getType() == VoteType.BAD).count(); - - - // 반대표가 더 많을 시, 재투표를 위해 투표 방 삭제 - if (goodVotes < badVotes) { + if (voteCounts.goodVotes > voteCounts.badVotes) { + tripPlan.setStatus(Status.COMPLETED); + tripPlanRepository.save(tripPlan); + } else { voteRoomRepository.delete(voteRoom); - - // roomId를 통해 roomName 가져오기 - Room room = findRoomById(voteRoomRequest.getRoomId()); - String roomName = room.getName(); - - // 투표 완료 알림 생성 - notificationService.createEndNotification(roomName, tripPlan.getUser().getEmail(), NotificationType.VOTE_COMPLETE, tripPlan.getId(), tripPlan.getTripPlanType()); - return true; } - else{ - tripPlan.setStatus(Status.COMPLETED); // 여행 계획 '완료'로 상태 변경 - tripPlanRepository.save(tripPlan); - // roomId를 통해 roomName 가져오기 - Room room = findRoomById(voteRoomRequest.getRoomId()); - String roomName = room.getName(); - - // 투표 완료 알림 생성 - notificationService.createEndNotification(roomName, tripPlan.getUser().getEmail(), NotificationType.VOTE_COMPLETE, tripPlan.getId(), tripPlan.getTripPlanType()); - return false; - } + Room room = findRoomById(voteRoomRequest.getRoomId()); + notificationService.createEndNotification( + room.getName(), tripPlan.getUser().getEmail(), NotificationType.VOTE_COMPLETE, tripPlan.getId(), tripPlan.getTripPlanType() + ); + return voteCounts.goodVotes <= voteCounts.badVotes; } @Transactional(readOnly = true) public boolean checkVoteCompleted(VoteRoomRequest voteRoomRequest) { + try { + VoteRoom voteRoom = findVoteRoomById(voteRoomRequest.getVoteRoomId()); + TripPlan tripPlan = findTripPlanById(voteRoomRequest.getTripId()); + VoteCounts voteCounts = countVotes(tripPlan.getId()); - // 반복되는 로직 헬퍼 클래스로 분리 - VoteRoom voteRoom = findVoteRoomById(voteRoomRequest.getVoteRoomId()); - TripPlan tripPlan = findTripPlanById(voteRoomRequest.getTripId()); + // 조건 1. 모든 멤버가 투표했는지 확인 + boolean allMembersVoted = isAllMembersVoted(voteRoomRequest.getRoomId(), voteCounts.totalVotes); - // 조건 1. 모든 멤버가 투표 했는지 - List votes = voteRepository.findAllVotesByTripPlanId(tripPlan.getId()); // 여행에 해당하는 모든 투표 리스트 - boolean allMembersVoted = isAllMembersVoted(voteRoomRequest.getRoomId(), votes); + // 조건 2. 투표 제한 시간 초과 확인 + boolean isTimeExpired = isVoteTimeExpired(voteRoom, tripPlan); - // 조건 2. 투표 제한 시간 초과 - boolean isTimeExpired = isVoteTimeExpired(voteRoom, tripPlan); + return (isTimeExpired || allMembersVoted) && (voteCounts.goodVotes != voteCounts.badVotes); + } catch (Exception e) { + throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } - return isTimeExpired || allMembersVoted; + private VoteCounts countVotes(Long tripPlanId) { + List votes = voteRepository.findAllVotesByTripPlanId(tripPlanId); + long goodVotes = votes.stream().filter(v -> v.getType() == VoteType.GOOD).count(); + long badVotes = votes.stream().filter(v -> v.getType() == VoteType.BAD).count(); + + return new VoteCounts(goodVotes, badVotes, votes.size()); } private TripPlan findTripPlanById(Long tripId) { @@ -116,9 +103,9 @@ private VoteRoom findVoteRoomById(Long voteRoomId) { .orElseThrow(() -> new BusinessException(ErrorCode.VOTE_ROOM_NOT_FOUND)); } - private boolean isAllMembersVoted(Long roomId, List votes) { + private boolean isAllMembersVoted(Long roomId, long totalVotes) { Room room = findRoomById(roomId); - return room.getRoomMembers().size() == votes.size(); + return room.getRoomMembers().size() == totalVotes; } private boolean isVoteTimeExpired(VoteRoom voteRoom, TripPlan tripPlan) { @@ -127,4 +114,16 @@ private boolean isVoteTimeExpired(VoteRoom voteRoom, TripPlan tripPlan) { voteRoom.getCreatedAt().plusSeconds(tripPlan.getVoteLimitTime().getSeconds()) ); } + + private static class VoteCounts { + final long goodVotes; + final long badVotes; + final long totalVotes; + + public VoteCounts(long goodVotes, long badVotes, long totalVotes) { + this.goodVotes = goodVotes; + this.badVotes = badVotes; + this.totalVotes = totalVotes; + } + } } From c2c323614b9c2b1e27b85a11bb36b4575cfad393 Mon Sep 17 00:00:00 2001 From: parkmineum Date: Thu, 20 Feb 2025 16:59:31 +0900 Subject: [PATCH 17/17] =?UTF-8?q?[fix]=20DTO=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yeogi_gal_lae/api/vote/dto/request/VoteRoomRequest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/umc/yeogi_gal_lae/api/vote/dto/request/VoteRoomRequest.java b/src/main/java/com/umc/yeogi_gal_lae/api/vote/dto/request/VoteRoomRequest.java index 00136b56..b215e86b 100644 --- a/src/main/java/com/umc/yeogi_gal_lae/api/vote/dto/request/VoteRoomRequest.java +++ b/src/main/java/com/umc/yeogi_gal_lae/api/vote/dto/request/VoteRoomRequest.java @@ -1,12 +1,16 @@ package com.umc.yeogi_gal_lae.api.vote.dto.request; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @NoArgsConstructor +@Builder // 추가 +@AllArgsConstructor // 필요 시 추가 public class VoteRoomRequest { @NotNull