From a3ea845ea97db382d42547d5d8a2dcb0d5025976 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Thu, 21 Nov 2024 23:40:55 +0900 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20dev=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20(#494)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/makers/crew/main/global/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/config/SecurityConfig.java b/main/src/main/java/org/sopt/makers/crew/main/global/config/SecurityConfig.java index 637ac9ab..1e6bf5f9 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/global/config/SecurityConfig.java +++ b/main/src/main/java/org/sopt/makers/crew/main/global/config/SecurityConfig.java @@ -92,7 +92,7 @@ CorsConfigurationSource corsConfigurationSource() { "https://playground.sopt.org/", "http://localhost:3000/", "https://sopt-internal-dev.pages.dev/", - "https://crew.api.develop.sopt.org", + "https://crew.api.dev.sopt.org", "https://crew.api.prod.sopt.org" )); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE", "PUT", "OPTIONS")); From 2398c9ba918b0372a585237f1953c7b54fbf3bae Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Sat, 23 Nov 2024 17:25:17 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=9D=BC=EB=B6=80=20=EC=88=98=EC=A0=95=20(#496)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/main/meeting/v2/service/MeetingV2ServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java index 244790cc..76a6ee56 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java @@ -543,7 +543,7 @@ private void validateApplyPeriod(Meeting meeting) { } private void validateUserActivities(User user) { - if (user.getActivities().isEmpty()) { + if (user.getActivities() == null || user.getActivities().isEmpty()) { throw new BadRequestException(MISSING_GENERATION_PART.getErrorCode()); } } From 2e4f7f37a76cda8b1dc31206714d6ed22a6ee35c Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Sun, 15 Dec 2024 22:58:29 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20redis=EB=A5=BC=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20=EC=A1=B0=ED=9A=8C=20=EC=84=B1=EB=8A=A5=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94=20(#480)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: redis 설정 추가 * chore: redis 관련 의존성 추가 * feat(CoLeaderReader): 캐싱을 위한 CoLeaderReader 레이어 구현 * refactor(CoLeaderRepository): CoLeader user 관련 데이터 페치조인 구현 * refactor(ControllerExceptionAdvice): 서버 에러 트레이스를 위해 트레이스 스택 출력하도록 개선 * chore(ExecutionLoggingAop): redis 관련 세팅 로깅 aop 에서 제외 * chore(UserActivityVO): redis에 저장되어 있는 객체 역직렬화 하기 위한 코드 수정 * chore(ImageUrlVO): redis에 저장되어 있는 객체 역직렬화 하기 위한 코드 수정 * chore(Meeting): 1. mStartDate, mEndDate 역직렬화 문제를 위해 getter 선언 2. 기존에 사용되었던 getter 수정 * chore(BaseTimeEntity): LocalDateTime 직렬화 및 역직렬화 문제로 인해 코드 추가 * chore(Meeting): LocalDateTime 직렬화 및 역직렬화 문제로 인해 코드 추가 * feat(UserReader): 캐싱을 위한 UserReader 레이어 구현 * feat(MeetingReader): 캐싱을 위한 MeetingReader 레이어 구현 * chore: LazyLoading 객체 저장 오류 해결 * add(RedisConfig): redis 관련 설정 추가 1. 직렬화, 역직렬화 관련된 설정 추가 2. 캐시 TTL 5시간 설정 * refactor(MeetingV2ServiceImpl): 캐시를 활용환 성능 최적회 1. 모임 조회 : 모임, 모임장, 공동모임장 관련 데이터 캐싱 2. 모임 수정 : 1번에서 캐싱한 데이터 초기화 * test(yml): redis 설정 추가 * chore(User, Meeting): lazyLoading 객체 redis에 저장하지 않도록 구현 * chore(RedisConfig): host와 port 명시적으로 설정 * feat(AbstractContainerBaseTest): redis 테스트 컨테이너 추가 * fix(AbstractContainerBaseTest): Property 수정 * chore(yml): dev redis 경로 변경 * chore(cd-dev): 배포 테스트 * chore(cd-dev): 배포 테스트 * chore(cd-dev): 배포 테스트 * chore(cd-dev): 배포 테스트 * chore(UserReader): 레디스에 캐싱하는 데이터를 User 에서 MeetingCreatorDto 로 변경 * rename(redisContainerBaseTest): redisContainerBaseTest 으로 파일 이름 변경 * chore(redisContainerBaseTest): 싱글톤으로 수정 * chore: redis 추가 * chore: redis localhost로 변경 * chore: readonly 옵션 추가 * del(User): 주석삭제 * chore(UserReader): readonly 옵션 추가 * chore(RedisConfig): 디폴트 ttl 설정, 이후에 다른 ttl 주기의 캐시가 필요하면 추가할 수 있도록 구현 * chore(ImageUrlVO): 필드 final 로 변경 및 직렬화 설정 * chore(Meeting): 기존 상태로 원상복구 * feat(MeetingReader): Meeting 반환이 아닌 MeetingRedisDto 를 사용으로 수정 * 🚨feat(CoLeaderReader): CoLeaders 가 아닌 CoLeadersRedisDto 반환으로 수정 1. User 생성자에 불가피하게 id 추가 2. CoLeader 에 Meeting 필드를 불가피하게 null 로 저장 * chore(RedisConfig): objectMapper 커스텀화하여 공통적으로 사용할 수 있게 구현 * chore(MeetingV2ServiceImpl): 로직 변경으로 인한 수정 * chore(MeetingV2GetMeetingByIdResponseDto): Meeting 생성자 변경없이 하기 위해 모임 id 반환 방식을 변경 * chore: redis 커넥션 풀 설정 * chore: redis 관련 기능들만 커스텀 objectMapper 사용하도록 수정 * chore(UserActivityVO): UserActivityVO final 추가 * chore(RedisProperties): RedisProperties 를 따로 정의하여 사용 * fix: 잘못된 코드 원상복구 * chore(CoLeaderReader): readonly 추가 * chore(ImageUrlVO): static -> 인스턴스 메서드로 수정 * chore(cd): cd 트리거 수정 * chore(UserActivityVO): 검증 로직 추가 * chore(CoLeadersRedisDto): 기본 생성자 추가 * fix: 기존 코드 복구 * feat(AuthV2ServiceImpl): 회원 정보 수정된 경우에 캐시 초기화 * feat(User): updateIfChanged 메서드 구현, withUserIdForRedis 메서드 구현 * chore(yml): 커넥션 풀 설정 제거 * chore(CoLeaderRedisDto): User 생성 로직 수정 * chore(CoLeadersRedisDto): 기본 해시맵 생성으로 변경 * test: 파트 및 기수가 잘못됐을 경우 예외 발생 * fix: NPE 문제 해결 * chore: 검증로직 private 메서드화 --- docker-compose.yml | 78 ++++++- main/build.gradle | 7 + .../auth/v2/service/AuthV2ServiceImpl.java | 82 ++++--- .../makers/crew/main/entity/apply/Apply.java | 31 ++- .../main/entity/common/BaseTimeEntity.java | 9 + .../crew/main/entity/meeting/CoLeader.java | 2 +- .../main/entity/meeting/CoLeaderReader.java | 24 ++ .../entity/meeting/CoLeaderRepository.java | 6 +- .../crew/main/entity/meeting/CoLeaders.java | 4 + .../crew/main/entity/meeting/Meeting.java | 14 +- .../main/entity/meeting/MeetingReader.java | 22 ++ .../main/entity/meeting/vo/ImageUrlVO.java | 33 ++- .../makers/crew/main/entity/user/User.java | 213 ++++++++++++------ .../crew/main/entity/user/UserReader.java | 20 ++ .../main/entity/user/vo/UserActivityVO.java | 47 +++- .../crew/main/external/redis/RedisConfig.java | 77 +++++++ .../main/external/redis/RedisProperties.java | 14 ++ .../advice/ControllerExceptionAdvice.java | 2 +- .../main/global/aop/ExecutionLoggingAop.java | 3 +- .../constant/PropertiesConfiguration.java | 11 + .../main/global/dto/MeetingCreatorDto.java | 21 +- .../main/global/dto/MeetingResponseDto.java | 2 +- .../main/global/exception/ErrorStatus.java | 2 +- .../v2/dto/redis/CoLeaderRedisDto.java | 45 ++++ .../v2/dto/redis/CoLeadersRedisDto.java | 50 ++++ .../meeting/v2/dto/redis/MeetingRedisDto.java | 120 ++++++++++ .../MeetingV2GetMeetingBannerResponseDto.java | 2 +- .../MeetingV2GetMeetingByIdResponseDto.java | 17 +- .../v2/service/MeetingV2ServiceImpl.java | 58 +++-- ...gV2GetCreatedMeetingByUserResponseDto.java | 2 +- main/src/main/resources/application-dev.yml | 4 + main/src/main/resources/application-local.yml | 4 + main/src/main/resources/application-prod.yml | 4 + main/src/main/resources/application-test.yml | 4 + .../main/entity/user/UserActivityVOTest.java | 16 ++ .../crew/main/entity/user/UserEntityTest.java | 23 +- .../main/external/redisContainerBaseTest.java | 25 ++ .../v2/service/MeetingV2ServiceTest.java | 11 +- .../crew/main/user/v2/UserServiceTest.java | 32 --- 39 files changed, 899 insertions(+), 242 deletions(-) create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderReader.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/meeting/MeetingReader.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/user/UserReader.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/external/redis/RedisConfig.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/external/redis/RedisProperties.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/global/constant/PropertiesConfiguration.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/redis/CoLeaderRedisDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/redis/CoLeadersRedisDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/redis/MeetingRedisDto.java create mode 100644 main/src/test/java/org/sopt/makers/crew/main/entity/user/UserActivityVOTest.java create mode 100644 main/src/test/java/org/sopt/makers/crew/main/external/redisContainerBaseTest.java diff --git a/docker-compose.yml b/docker-compose.yml index 30df9779..372ca83d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,11 @@ services: caddy.log.output: stdout caddy.log.format: json caddy.log.include: http.log.access.localhost + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "10" swagger: image: swaggerapi/swagger-ui @@ -42,9 +47,23 @@ services: labels: caddy.route: /docs* caddy.route.reverse_proxy: "{{ upstreams 8080 }}" + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "10" + + redis: + image: redis:alpine + container_name: redis + hostname: redis + ports: + - 6379:6379 + networks: + - caddy nestjs-green: - image: makerscrew/server:latest + image: makerscrew/server:develop container_name: nestjs-green ports: - 3001:3000 @@ -107,17 +126,28 @@ services: caddy.route_14.reverse_proxy: "{{ upstreams 3000 }}" spring-green: - image: makerscrew/main:latest + image: makerscrew/main:develop environment: - - TZ=Asia/Seoul + TZ: Asia/Seoul container_name: spring-green ports: - 4001:4000 + - 5556:5555 restart: unless-stopped depends_on: - nestjs-green + - pinpoint-agent + - redis + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "100" networks: - caddy + volumes: + - data-volume:/pinpoint-agent + labels: caddy.log: "localhost" # for Swagger spec @@ -152,9 +182,17 @@ services: caddy.route_13.reverse_proxy: "{{ upstreams 4000 }}" caddy.route_14: /auth/v2/* caddy.route_14.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_15: /8da2d7e6-72aa-4120-9e84-8f459a2584a1/* + caddy.route_15.reverse_proxy: "{{ upstreams 5555 }}" + caddy.route_16: /internal/* + caddy.route_16.reverse_proxy: "{{ upstreams 4000 }}" nestjs-blue: - image: makerscrew/server:latest + image: makerscrew/server:develop + container_name: nestjs-blue + ports: + - 3002:3000 + restart: unless-stopped env_file: - ./.env environment: @@ -170,10 +208,11 @@ services: - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - AWS_REGION=${AWS_REGION} - JWT_SECRET=${JWT_SECRET} - container_name: nestjs-blue - ports: - - 3002:3000 - restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "10" networks: - caddy labels: @@ -212,17 +251,27 @@ services: caddy.route_14.reverse_proxy: "{{ upstreams 3000 }}" spring-blue: - image: makerscrew/main:latest + image: makerscrew/main:develop environment: - - TZ=Asia/Seoul + TZ: Asia/Seoul container_name: spring-blue ports: - 4002:4000 + - 5557:5555 restart: unless-stopped depends_on: - nestjs-blue + - pinpoint-agent + - redis + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "100" networks: - caddy + volumes: + - data-volume:/pinpoint-agent labels: caddy.log: "localhost" # for Swagger spec @@ -257,7 +306,14 @@ services: caddy.route_13.reverse_proxy: "{{ upstreams 4000 }}" caddy.route_14: /auth/v2/* caddy.route_14.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_15: /8da2d7e6-72aa-4120-9e84-8f459a2584a1/* + caddy.route_15.reverse_proxy: "{{ upstreams 5555 }}" + caddy.route_16: /internal/* + caddy.route_16.reverse_proxy: "{{ upstreams 4000 }}" + +volumes: + data-volume: networks: caddy: - external: true + external: true \ No newline at end of file diff --git a/main/build.gradle b/main/build.gradle index 1faa3948..698a4bfe 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -99,6 +99,13 @@ dependencies { // Slack Webhook implementation 'com.github.maricn:logback-slack-appender:1.4.0' + + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-cache' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate6' + } tasks.named('test') { diff --git a/main/src/main/java/org/sopt/makers/crew/main/auth/v2/service/AuthV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/auth/v2/service/AuthV2ServiceImpl.java index e3c27d8a..4245437d 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/auth/v2/service/AuthV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/auth/v2/service/AuthV2ServiceImpl.java @@ -1,15 +1,16 @@ package org.sopt.makers.crew.main.auth.v2.service; -import java.util.Optional; - import org.sopt.makers.crew.main.auth.v2.dto.request.AuthV2RequestDto; import org.sopt.makers.crew.main.auth.v2.dto.response.AuthV2ResponseDto; +import org.sopt.makers.crew.main.entity.meeting.CoLeaderRepository; import org.sopt.makers.crew.main.global.jwt.JwtTokenProvider; import org.sopt.makers.crew.main.entity.user.User; import org.sopt.makers.crew.main.entity.user.UserRepository; import org.sopt.makers.crew.main.external.playground.PlaygroundService; import org.sopt.makers.crew.main.external.playground.dto.request.PlaygroundUserRequestDto; import org.sopt.makers.crew.main.external.playground.dto.response.PlaygroundUserResponseDto; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Caching; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,6 +24,7 @@ public class AuthV2ServiceImpl implements AuthV2Service { private final UserRepository userRepository; + private final CoLeaderRepository coLeaderRepository; private final PlaygroundService playgroundService; private final JwtTokenProvider jwtTokenProvider; @@ -30,35 +32,61 @@ public class AuthV2ServiceImpl implements AuthV2Service { @Override @Transactional public AuthV2ResponseDto loginUser(AuthV2RequestDto requestDto) { + PlaygroundUserResponseDto responseDto = fetchPlaygroundUser(requestDto); + User curUser = userRepository.findByOrgId(responseDto.getId()) + .orElseGet(() -> signUpNewUser(responseDto)); - // 플그 서버로의 요청 - PlaygroundUserResponseDto responseDto = playgroundService.getUser(PlaygroundUserRequestDto.of(requestDto.authToken())); - Optional user = userRepository.findByOrgId(responseDto.getId()); - - /** - * @note: 회원가입 경우 - * - * */ - if (user.isEmpty()) { - User newUser = responseDto.toEntity(); - userRepository.save(newUser); - - log.info("new user signup : {} {}", newUser.getId(), newUser.getName()); - String accessToken = jwtTokenProvider.generateAccessToken(newUser.getId(), newUser.getName()); - return AuthV2ResponseDto.of(accessToken); + if (updateUserIfChanged(curUser, responseDto)) { + clearCacheForUser(curUser.getId()); } - /** - * @note: 로그인 경우 : 기존 정보에서 변화있는 부분은 업데이트 한다. - * - * */ - User curUser = user.get(); - curUser.updateUser(responseDto.getName(), responseDto.getId(), responseDto.getUserActivities(), - responseDto.getProfileImage(), responseDto.getPhone()); - String accessToken = jwtTokenProvider.generateAccessToken(curUser.getId(), curUser.getName()); - log.info("accessToken : {}", accessToken); - + log.info("Access token generated for user {}: {}", curUser.getId(), accessToken); return AuthV2ResponseDto.of(accessToken); } + + private PlaygroundUserResponseDto fetchPlaygroundUser(AuthV2RequestDto requestDto) { + return playgroundService.getUser(PlaygroundUserRequestDto.of(requestDto.authToken())); + } + + private User signUpNewUser(PlaygroundUserResponseDto responseDto) { + User newUser = responseDto.toEntity(); + User savedUser = userRepository.save(newUser); + log.info("New user signup: {} {}", savedUser.getId(), savedUser.getName()); + return savedUser; + } + + private boolean updateUserIfChanged(User curUser, PlaygroundUserResponseDto responseDto) { + User playgroundUser = responseDto.toEntity(); + boolean isUpdated = curUser.updateIfChanged(playgroundUser); + + if (isUpdated) { + log.info("User updated: {}", curUser.getId()); + } + + return isUpdated; + } + + private void clearCacheForUser(Integer userId) { + clearCacheForLeader(userId); + + coLeaderRepository.findAllByUserIdWithMeeting(userId).forEach( + coLeader -> clearCacheForCoLeader(coLeader.getMeeting().getId()) + ); + log.info("Cache cleared for user: {}", userId); + } + + @Caching(evict = { + @CacheEvict(value = "meetingLeaderCache", key = "#userId") + }) + public void clearCacheForLeader(Integer userId) { + + } + + @Caching(evict = { + @CacheEvict(value = "coLeadersCache", key = "#meetingId") + }) + public void clearCacheForCoLeader(Integer meetingId) { + + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/Apply.java b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/Apply.java index d0467851..83ee4651 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/Apply.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/Apply.java @@ -1,41 +1,38 @@ package org.sopt.makers.crew.main.entity.apply; -import static jakarta.persistence.GenerationType.IDENTITY; +import static jakarta.persistence.GenerationType.*; import static org.sopt.makers.crew.main.global.exception.ErrorStatus.*; +import java.time.LocalDateTime; + +import org.sopt.makers.crew.main.entity.apply.enums.ApplyStatusConverter; +import org.sopt.makers.crew.main.entity.apply.enums.ApplyTypeConverter; +import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; +import org.sopt.makers.crew.main.entity.apply.enums.EnApplyType; +import org.sopt.makers.crew.main.entity.common.BaseTimeEntity; +import org.sopt.makers.crew.main.entity.meeting.Meeting; +import org.sopt.makers.crew.main.entity.user.User; +import org.sopt.makers.crew.main.global.exception.BadRequestException; +import org.springframework.data.annotation.CreatedDate; + import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; - -import java.time.LocalDateTime; - import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.sopt.makers.crew.main.entity.common.BaseTimeEntity; -import org.sopt.makers.crew.main.global.exception.BadRequestException; -import org.sopt.makers.crew.main.entity.apply.enums.ApplyStatusConverter; -import org.sopt.makers.crew.main.entity.apply.enums.ApplyTypeConverter; -import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; -import org.sopt.makers.crew.main.entity.apply.enums.EnApplyType; -import org.sopt.makers.crew.main.entity.meeting.Meeting; -import org.sopt.makers.crew.main.entity.user.User; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - @Entity @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "apply") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Apply extends BaseTimeEntity { /** diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/common/BaseTimeEntity.java b/main/src/main/java/org/sopt/makers/crew/main/entity/common/BaseTimeEntity.java index 5e1d34db..24ef2afa 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/common/BaseTimeEntity.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/common/BaseTimeEntity.java @@ -6,6 +6,11 @@ import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; + import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; @@ -16,10 +21,14 @@ public abstract class BaseTimeEntity { @CreatedDate @Column(name = "createdTimestamp") + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) public LocalDateTime createdTimestamp; @LastModifiedDate @Column(name = "modifiedTimestamp") + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) public LocalDateTime modifiedTimestamp; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java index 0dee399b..0c1bfeea 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java @@ -43,7 +43,7 @@ public class CoLeader extends BaseTimeEntity { @Builder private CoLeader(Meeting meeting, User user) { - if (Objects.equals(meeting.getUserId(), user.getId())) { + if (meeting != null && Objects.equals(meeting.getUserId(), user.getId())) { throw new BadRequestException(LEADER_CANNOT_BE_CO_LEADER_APPLY.getErrorCode()); } this.meeting = meeting; diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderReader.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderReader.java new file mode 100644 index 00000000..d3d920f7 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderReader.java @@ -0,0 +1,24 @@ +package org.sopt.makers.crew.main.entity.meeting; + +import java.util.List; + +import org.sopt.makers.crew.main.meeting.v2.dto.redis.CoLeadersRedisDto; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CoLeaderReader { + private final CoLeaderRepository coLeaderRepository; + + @Cacheable(value = "coLeadersCache", key = "#meetingId") + public CoLeadersRedisDto getCoLeaders(Integer meetingId) { + List coLeaders = coLeaderRepository.findAllByMeetingId(meetingId); + + return new CoLeadersRedisDto(coLeaders); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderRepository.java index 734591b0..5b2b764c 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderRepository.java @@ -2,17 +2,21 @@ import java.util.List; -import org.sopt.makers.crew.main.entity.user.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface CoLeaderRepository extends JpaRepository { void deleteAllByMeetingId(Integer meetingId); + @Query("SELECT c FROM CoLeader c JOIN FETCH c.user WHERE c.meeting.id =:meetingId") List findAllByMeetingId(Integer meetingId); List findAllByMeetingIdIn(List meetingId); List findAllByUserId(Integer userId); + @Query("SELECT c FROM CoLeader c JOIN FETCH c.meeting WHERE c.user.id =:userId") + List findAllByUserIdWithMeeting(Integer userId); + } \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaders.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaders.java index ee77312a..23345831 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaders.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaders.java @@ -25,6 +25,10 @@ public CoLeaders(List coLeaders) { .collect(Collectors.groupingBy(coLeader -> coLeader.getMeeting().getId())); } + public CoLeaders(Map> coLeadersMap) { + this.coLeadersMap = coLeadersMap; + } + public void validateCoLeader(Integer meetingId, Integer requestUserId) { if (isCoLeader(meetingId, requestUserId)) { throw new BadRequestException(CO_LEADER_CANNOT_APPLY.getErrorCode()); diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java index 2120c58f..98e3cf81 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java @@ -8,6 +8,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -245,8 +246,8 @@ public void updateMeeting(Meeting updateMeeting) { this.capacity = updateMeeting.getCapacity(); this.desc = updateMeeting.getDesc(); this.processDesc = updateMeeting.getProcessDesc(); - this.mStartDate = updateMeeting.mStartDate; - this.mEndDate = updateMeeting.getMEndDate(); + this.mStartDate = updateMeeting.getmStartDate(); + this.mEndDate = updateMeeting.getmEndDate(); this.leaderDesc = updateMeeting.getLeaderDesc(); this.note = updateMeeting.getNote(); this.isMentorNeeded = updateMeeting.getIsMentorNeeded(); @@ -255,4 +256,13 @@ public void updateMeeting(Meeting updateMeeting) { this.joinableParts = updateMeeting.getJoinableParts(); } + public LocalDateTime getmStartDate() { + return mStartDate; + } + + public LocalDateTime getmEndDate() { + return mEndDate; + } + + } \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/MeetingReader.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/MeetingReader.java new file mode 100644 index 00000000..0a25a7d9 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/MeetingReader.java @@ -0,0 +1,22 @@ +package org.sopt.makers.crew.main.entity.meeting; + +import org.sopt.makers.crew.main.meeting.v2.dto.redis.MeetingRedisDto; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MeetingReader { + + private final MeetingRepository meetingRepository; + + @Cacheable(value = "meetingCache", key = "#meetingId") + public MeetingRedisDto getMeetingById(Integer meetingId) { + Meeting meeting = meetingRepository.findByIdOrThrow(meetingId); + return MeetingRedisDto.of(meeting); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/vo/ImageUrlVO.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/vo/ImageUrlVO.java index c4d3bcf3..7cc9f9c2 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/vo/ImageUrlVO.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/vo/ImageUrlVO.java @@ -1,12 +1,35 @@ package org.sopt.makers.crew.main.entity.meeting.vo; +import static org.sopt.makers.crew.main.global.exception.ErrorStatus.*; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.RequiredArgsConstructor; @Getter -@RequiredArgsConstructor +@EqualsAndHashCode public class ImageUrlVO { - - private final Integer id; - private final String url; + private final Integer id; + private final String url; + + @JsonCreator + public ImageUrlVO( + @JsonProperty("id") Integer id, + @JsonProperty("url") String url) { + + validateImageUrlVO(id, url); + this.id = id; + this.url = url; + } + + private void validateImageUrlVO(Integer id, String url) { + if (id == null || id < 0) { + throw new IllegalArgumentException(INVALID_INPUT_VALUE.getErrorCode()); // 커스텀 에러로 처리 + } + if (url == null || url.isBlank()) { + throw new IllegalArgumentException(INVALID_INPUT_VALUE.getErrorCode()); // 커스텀 에러로 처리 + } + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java b/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java index 216c391b..c0677c4f 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java @@ -9,90 +9,165 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; + import java.util.Comparator; import java.util.List; +import java.util.Objects; + import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; + import org.hibernate.annotations.Type; import org.sopt.makers.crew.main.entity.common.BaseTimeEntity; import org.sopt.makers.crew.main.global.exception.ServerException; import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; + @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "user") public class User extends BaseTimeEntity { - /** - * Primary Key - */ - @Id - @Column(name = "id") - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Integer id; - - /** - * 사용자 이름 - */ - @Column(name = "name", nullable = false) - private String name; - - /** - * sopt org unique id - */ - @Column(name = "orgId", nullable = false) - private Integer orgId; - - /** - * 활동 목록 - */ - @Column(name = "activities", columnDefinition = "jsonb") - @Type(JsonBinaryType.class) - private List activities; - - /** - * 프로필 이미지 - */ - @Column(name = "profileImage") - private String profileImage; - - /** - * 핸드폰 번호 - */ - @Column(name = "phone") - private String phone; - - @Builder - public User(String name, Integer orgId, List activities, String profileImage, - String phone) { - this.name = name; - this.orgId = orgId; - this.activities = activities; - this.profileImage = profileImage; - this.phone = phone; - } - - public void setUserIdForTest(Integer userId) { - this.id = userId; - } - - public UserActivityVO getRecentActivityVO(){ - return activities.stream() - .filter(userActivityVO -> userActivityVO.getPart() != null) - .max(Comparator.comparingInt(UserActivityVO::getGeneration)) - .orElseThrow(() -> new ServerException(INTERNAL_SERVER_ERROR.getErrorCode())); - } - - public void updateUser(String name, Integer orgId, List activities, String profileImage, - String phone){ - - this.name = name; - this.orgId = orgId; - this.activities = activities; - this.profileImage = profileImage; - this.phone = phone; - } + /** + * Primary Key + */ + @Id + @Column(name = "id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** + * 사용자 이름 + */ + @Column(name = "name", nullable = false) + private String name; + + /** + * sopt org unique id + */ + @Column(name = "orgId", nullable = false) + private Integer orgId; + + /** + * 활동 목록 + */ + @Column(name = "activities", columnDefinition = "jsonb") + @Type(JsonBinaryType.class) + private List activities; + + /** + * 프로필 이미지 + */ + @Column(name = "profileImage") + private String profileImage; + + /** + * 핸드폰 번호 + */ + @Column(name = "phone") + private String phone; + + @Builder + public User(String name, Integer orgId, List activities, String profileImage, + String phone) { + this.name = name; + this.orgId = orgId; + this.activities = activities; + this.profileImage = profileImage; + this.phone = phone; + } + + public void setUserIdForTest(Integer userId) { + this.id = userId; + } + + /** + * @implSpec : redis 에서 조회한 데이터를 엔티티로 변환할 때 사용하는 메서드 + * + * **/ + public User withUserIdForRedis(Integer id) { + this.id = id; + return this; + } + + public UserActivityVO getRecentActivityVO() { + return activities.stream() + .filter(userActivityVO -> userActivityVO.getPart() != null) + .max(Comparator.comparingInt(UserActivityVO::getGeneration)) + .orElseThrow(() -> new ServerException(INTERNAL_SERVER_ERROR.getErrorCode())); + } + + public boolean updateIfChanged(User playgroundUser) { + boolean isUpdated = false; + + if (validateAndUpdateName(playgroundUser.getName())) { + isUpdated = true; + } + + if (validateAndUpdateOrgId(playgroundUser.getId())) { + isUpdated = true; + } + + if (validateAndUpdateActivities(playgroundUser.getActivities())) { + isUpdated = true; + } + + if (validateAndUpdateProfileImage(playgroundUser.getProfileImage())) { + isUpdated = true; + } + + if (validateAndUpdatePhone(playgroundUser.getPhone())) { + isUpdated = true; + } + + return isUpdated; + } + + private boolean validateAndUpdateName(String newName) { + if (!Objects.equals(this.name, newName)) { + this.name = newName; + return true; + } + return false; + } + + private boolean validateAndUpdateOrgId(Integer newOrgId) { + if (!Objects.equals(this.orgId, newOrgId)) { + this.orgId = newOrgId; + return true; + } + return false; + } + + private boolean validateAndUpdateActivities(List newActivities) { + if (!Objects.equals(this.activities, newActivities)) { + this.activities = newActivities; + return true; + } + return false; + } + + private boolean validateAndUpdateProfileImage(String newProfileImage) { + if (!Objects.equals(this.profileImage, newProfileImage)) { + this.profileImage = newProfileImage; + return true; + } + return false; + } + + private boolean validateAndUpdatePhone(String newPhone) { + if (!Objects.equals(this.phone, newPhone)) { + this.phone = newPhone; + return true; + } + return false; + } + + + public List getActivities() { + return activities; + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserReader.java b/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserReader.java new file mode 100644 index 00000000..25df2a47 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserReader.java @@ -0,0 +1,20 @@ +package org.sopt.makers.crew.main.entity.user; + +import org.sopt.makers.crew.main.global.dto.MeetingCreatorDto; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserReader { + private final UserRepository userRepository; + + @Cacheable(value = "meetingLeaderCache", key = "#userId") + public MeetingCreatorDto getMeetingLeader(Integer userId) { + return MeetingCreatorDto.of(userRepository.findByIdOrThrow(userId)); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/user/vo/UserActivityVO.java b/main/src/main/java/org/sopt/makers/crew/main/entity/user/vo/UserActivityVO.java index 1d78ce5a..6dff34dc 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/user/vo/UserActivityVO.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/user/vo/UserActivityVO.java @@ -1,24 +1,55 @@ package org.sopt.makers.crew.main.entity.user.vo; +import static org.sopt.makers.crew.main.global.exception.ErrorStatus.*; + +import org.sopt.makers.crew.main.global.exception.ServerException; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.RequiredArgsConstructor; import lombok.ToString; @Getter -@RequiredArgsConstructor @ToString @EqualsAndHashCode public class UserActivityVO { - @Schema(description = "파트", example = "서버") - @NotNull - private final String part; + @Schema(description = "파트", example = "서버") + @NotNull + private final String part; + + @Schema(description = "기수", example = "36") + @NotNull + private final int generation; + + @JsonCreator + public UserActivityVO( + @JsonProperty("part") String part, + @JsonProperty("generation") int generation) { + + validateUserActivityVO(part, generation); + this.part = part; + this.generation = generation; + } + + private void validateUserActivityVO(String part, int generation) { + // Part validation: Ensure it's not null, empty, or contain invalid characters. + if (part == null || part.trim().isEmpty()) { + throw new ServerException(INTERNAL_SERVER_ERROR.getErrorCode() + part); + } - @Schema(description = "기수", example = "36") - @NotNull - private final int generation; + // Validate that part only contains letters (and optionally spaces) + if (!part.matches("^[a-zA-Z가-힣\s]+$")) { + throw new IllegalArgumentException(INTERNAL_SERVER_ERROR.getErrorCode() + part); + } + // Generation validation: Must be a positive integer + if (generation <= 0) { + throw new IllegalArgumentException(INTERNAL_SERVER_ERROR.getErrorCode() + generation); + } + } } \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/external/redis/RedisConfig.java b/main/src/main/java/org/sopt/makers/crew/main/external/redis/RedisConfig.java new file mode 100644 index 00000000..391bf389 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/external/redis/RedisConfig.java @@ -0,0 +1,77 @@ +package org.sopt.makers.crew.main.external.redis; + +import java.time.Duration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.EnableCaching; +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.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +@Configuration +@EnableCaching +public class RedisConfig { + + @Bean + public RedisConnectionFactory redisConnectionFactory(RedisProperties redisProperties) { + return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // ISO-8601 형식 + objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), + ObjectMapper.DefaultTyping.NON_FINAL); // 타입 정보 추가 + + StringRedisSerializer stringSerializer = new StringRedisSerializer(); + GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(objectMapper); + + // Key serializer 설정 + template.setKeySerializer(stringSerializer); + template.setHashKeySerializer(stringSerializer); + + // Value serializer 설정 + template.setValueSerializer(jsonSerializer); + template.setHashValueSerializer(jsonSerializer); + + template.afterPropertiesSet(); + return template; + } + + @Bean + public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { + // 동일한 직렬화 설정 공유 + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // ISO-8601 형식 + objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), + ObjectMapper.DefaultTyping.NON_FINAL); // 타입 정보 추가 + + GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(objectMapper); + + RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofHours(24)) + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer)); + + return RedisCacheManager.builder(redisConnectionFactory) + .cacheDefaults(defaultCacheConfig) + .build(); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/external/redis/RedisProperties.java b/main/src/main/java/org/sopt/makers/crew/main/external/redis/RedisProperties.java new file mode 100644 index 00000000..b31023e4 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/external/redis/RedisProperties.java @@ -0,0 +1,14 @@ +package org.sopt.makers.crew.main.external.redis; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@ConfigurationProperties(prefix = "spring.data.redis") +@RequiredArgsConstructor +public class RedisProperties { + private final String host; + private final int port; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/advice/ControllerExceptionAdvice.java b/main/src/main/java/org/sopt/makers/crew/main/global/advice/ControllerExceptionAdvice.java index 77bed3c7..01c707b5 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/global/advice/ControllerExceptionAdvice.java +++ b/main/src/main/java/org/sopt/makers/crew/main/global/advice/ControllerExceptionAdvice.java @@ -120,7 +120,7 @@ public ResponseEntity handleIOException(IOException e) { @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception e) { - log.error("{}", e.getMessage()); + log.error("", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ExceptionResponse.fail( ErrorStatus.INTERNAL_SERVER_ERROR.getErrorCode())); diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/aop/ExecutionLoggingAop.java b/main/src/main/java/org/sopt/makers/crew/main/global/aop/ExecutionLoggingAop.java index ad8d6de0..6af23c7a 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/global/aop/ExecutionLoggingAop.java +++ b/main/src/main/java/org/sopt/makers/crew/main/global/aop/ExecutionLoggingAop.java @@ -26,11 +26,12 @@ public class ExecutionLoggingAop { /** * @implNote : 일부 클래스를 제외하고, 모든 클래스의 메서드의 시작과 끝을 로깅한다. - * @implNote : 제외 클래스 - global 패키지, config 관련 패키지, Test 클래스 + * @implNote : 제외 클래스 - global 패키지, config 관련 패키지, Test 클래스, redis 클래스 * */ @Around("execution(* org.sopt.makers.crew.main..*(..)) " + "&& !within(org.sopt.makers.crew.main.global..*) " + "&& !within(org.sopt.makers.crew.main.external.s3.config..*)" + + "&& !within(org.sopt.makers.crew.main.external.redis..*) " ) public Object logExecutionTrace(ProceedingJoinPoint pjp) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest(); diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/constant/PropertiesConfiguration.java b/main/src/main/java/org/sopt/makers/crew/main/global/constant/PropertiesConfiguration.java new file mode 100644 index 00000000..a6a12d1a --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/global/constant/PropertiesConfiguration.java @@ -0,0 +1,11 @@ +package org.sopt.makers.crew.main.global.constant; + +import org.sopt.makers.crew.main.external.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(RedisProperties.class) +public class PropertiesConfiguration { + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/dto/MeetingCreatorDto.java b/main/src/main/java/org/sopt/makers/crew/main/global/dto/MeetingCreatorDto.java index 6c93efbf..20ee37c4 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/global/dto/MeetingCreatorDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/global/dto/MeetingCreatorDto.java @@ -5,13 +5,14 @@ import org.sopt.makers.crew.main.entity.user.User; import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Getter; -import lombok.RequiredArgsConstructor; @Getter -@RequiredArgsConstructor @Schema(name = "MeetingCreatorDto", description = "모임 개설자 Dto") public class MeetingCreatorDto { @Schema(description = "모임장 id, 크루에서 사용하는 userId", example = "1") @@ -42,4 +43,20 @@ public static MeetingCreatorDto of(User user) { user.getActivities(), user.getPhone()); } + @JsonCreator // JSON 역직렬화를 위한 생성자 + public MeetingCreatorDto( + @JsonProperty("id") Integer id, + @JsonProperty("name") String name, + @JsonProperty("orgId") Integer orgId, + @JsonProperty("profileImage") String profileImage, + @JsonProperty("activities") List activities, + @JsonProperty("phone") String phone + ) { + this.id = id; + this.name = name; + this.orgId = orgId; + this.profileImage = profileImage; + this.activities = activities; + this.phone = phone; + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/dto/MeetingResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/global/dto/MeetingResponseDto.java index 05125b0d..14933c98 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/global/dto/MeetingResponseDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/global/dto/MeetingResponseDto.java @@ -92,7 +92,7 @@ public static MeetingResponseDto of(Meeting meeting, User meetingCreator, int ap return new MeetingResponseDto(meeting.getId(), meeting.getTitle(), meeting.getTargetActiveGeneration(), meeting.getJoinableParts(), meeting.getCategory().getValue(), canJoinOnlyActiveGeneration, meeting.getMeetingStatusValue(now), meeting.getImageURL(), - meeting.getIsMentorNeeded(), meeting.getMStartDate(), meeting.getMEndDate(), meeting.getCapacity(), + meeting.getIsMentorNeeded(), meeting.getmStartDate(), meeting.getmEndDate(), meeting.getCapacity(), creatorDto, approvedCount, approvedCount); } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java b/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java index 90e5e61d..8b9b8399 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java +++ b/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java @@ -17,7 +17,7 @@ public enum ErrorStatus { */ VALIDATION_EXCEPTION("CF-001"), VALIDATION_REQUEST_MISSING_EXCEPTION("요청값이 입력되지 않았습니다."), - INVALID_INPUT_VALUE("요청값이 올바르지 않습니다."), + INVALID_INPUT_VALUE("요청값이 올바르지 않습니다. : "), INVALID_INPUT_VALUE_FILTER("요청값 또는 토큰이 올바르지 않습니다."), NOT_FOUND_MEETING("모임이 없습니다."), NOT_FOUND_POST("존재하지 않는 게시글입니다."), diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/redis/CoLeaderRedisDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/redis/CoLeaderRedisDto.java new file mode 100644 index 00000000..34d31358 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/redis/CoLeaderRedisDto.java @@ -0,0 +1,45 @@ +package org.sopt.makers.crew.main.meeting.v2.dto.redis; + +import org.sopt.makers.crew.main.entity.meeting.CoLeader; +import org.sopt.makers.crew.main.entity.user.User; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import lombok.Getter; + +@Getter +@JsonTypeName("CoLeaderRedisDto") +public class CoLeaderRedisDto { + private final Integer userId; + private final Integer orgId; + private final String userName; + private final String userProfileImage; + private final Integer meetingId; + + @JsonCreator + public CoLeaderRedisDto( + @JsonProperty("userId") Integer userId, + @JsonProperty("orgId") Integer orgId, + @JsonProperty("userName") String userName, + @JsonProperty("userProfileImage") String userProfileImage, + @JsonProperty("meetingId") Integer meetingId) { + + this.userId = userId; + this.orgId = orgId; + this.userName = userName; + this.userProfileImage = userProfileImage; + this.meetingId = meetingId; + } + + public CoLeader toEntity() { + User coLeader = new User(userName, orgId, null, userProfileImage, null).withUserIdForRedis(userId); + + return CoLeader.builder() + .meeting(null) + .user(coLeader) + .build(); + } +} + diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/redis/CoLeadersRedisDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/redis/CoLeadersRedisDto.java new file mode 100644 index 00000000..d2a89402 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/redis/CoLeadersRedisDto.java @@ -0,0 +1,50 @@ +package org.sopt.makers.crew.main.meeting.v2.dto.redis; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.sopt.makers.crew.main.entity.meeting.CoLeader; +import org.sopt.makers.crew.main.entity.meeting.CoLeaders; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CoLeadersRedisDto { + private final Map> coLeadersMap = new HashMap<>(); + + public CoLeadersRedisDto(List coLeaders) { + coLeaders + .forEach(coLeader -> { + CoLeaderRedisDto coLeaderRedisDto = new CoLeaderRedisDto(coLeader.getUser().getId(), + coLeader.getUser().getOrgId(), + coLeader.getUser().getName(), coLeader.getUser().getProfileImage(), coLeader.getMeeting().getId()); + List coLeaderRedisDtos = coLeadersMap.getOrDefault(coLeader.getMeeting().getId(), + new ArrayList<>()); + coLeaderRedisDtos.add(coLeaderRedisDto); + coLeadersMap.put(coLeader.getMeeting().getId(), coLeaderRedisDtos); + }); + } + + public CoLeaders toEntity() { + Map> coLeadersEntityMap = coLeadersMap.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().stream() + .map(CoLeaderRedisDto::toEntity) // CoLeaderRedisDto -> CoLeader 변환 + .toList() + )); + + return new CoLeaders(coLeadersEntityMap); + } + +} + diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/redis/MeetingRedisDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/redis/MeetingRedisDto.java new file mode 100644 index 00000000..c97e1e14 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/redis/MeetingRedisDto.java @@ -0,0 +1,120 @@ +package org.sopt.makers.crew.main.meeting.v2.dto.redis; + +import java.time.LocalDateTime; +import java.util.List; + +import org.sopt.makers.crew.main.entity.meeting.Meeting; +import org.sopt.makers.crew.main.entity.meeting.enums.MeetingCategory; +import org.sopt.makers.crew.main.entity.meeting.enums.MeetingJoinablePart; +import org.sopt.makers.crew.main.entity.meeting.vo.ImageUrlVO; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; + +import lombok.Getter; + +@Getter +public class MeetingRedisDto { + private final Integer id; + private final Integer userId; + private final String title; + private final MeetingCategory category; + private final List imageURL; + + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + private final LocalDateTime startDate; + + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + private final LocalDateTime endDate; + + private final Integer capacity; + private final String desc; + private final String processDesc; + + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + private final LocalDateTime mStartDate; + + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + private final LocalDateTime mEndDate; + + private final String leaderDesc; + private final String note; + private final Boolean isMentorNeeded; + private final Boolean canJoinOnlyActiveGeneration; + private final Integer createdGeneration; + private final Integer targetActiveGeneration; + private final MeetingJoinablePart[] joinableParts; + + public Meeting toEntity() { + return new Meeting(null, userId, title, category, imageURL, startDate, endDate, capacity, desc, processDesc, + mStartDate, mEndDate, leaderDesc, null, note, isMentorNeeded, canJoinOnlyActiveGeneration, + createdGeneration, targetActiveGeneration, joinableParts); + } + + public static MeetingRedisDto of(Meeting meeting) { + return new MeetingRedisDto(meeting.getId(), meeting.getUserId(), meeting.getTitle(), meeting.getCategory(), + meeting.getImageURL(), meeting.getStartDate(), meeting.getEndDate(), meeting.getCapacity(), + meeting.getDesc(), meeting.getProcessDesc(), meeting.getmStartDate(), meeting.getmEndDate(), + meeting.getLeaderDesc(), meeting.getNote(), meeting.getIsMentorNeeded(), + meeting.getCanJoinOnlyActiveGeneration(), meeting.getCreatedGeneration(), + meeting.getTargetActiveGeneration(), meeting.getJoinableParts()); + } + + @JsonCreator + public MeetingRedisDto( + @JsonProperty("id") Integer id, + @JsonProperty("userId") Integer userId, + @JsonProperty("title") String title, + @JsonProperty("category") MeetingCategory category, + @JsonProperty("imageURL") List imageURL, + @JsonProperty("startDate") LocalDateTime startDate, + @JsonProperty("endDate") LocalDateTime endDate, + @JsonProperty("capacity") Integer capacity, + @JsonProperty("desc") String desc, + @JsonProperty("processDesc") String processDesc, + @JsonProperty("mStartDate") LocalDateTime mStartDate, + @JsonProperty("mEndDate") LocalDateTime mEndDate, + @JsonProperty("leaderDesc") String leaderDesc, + @JsonProperty("note") String note, + @JsonProperty("isMentorNeeded") Boolean isMentorNeeded, + @JsonProperty("canJoinOnlyActiveGeneration") Boolean canJoinOnlyActiveGeneration, + @JsonProperty("createdGeneration") Integer createdGeneration, + @JsonProperty("targetActiveGeneration") Integer targetActiveGeneration, + @JsonProperty("joinableParts") MeetingJoinablePart[] joinableParts) { + this.id = id; + this.userId = userId; + this.title = title; + this.category = category; + this.imageURL = imageURL; + this.startDate = startDate; + this.endDate = endDate; + this.capacity = capacity; + this.desc = desc; + this.processDesc = processDesc; + this.mStartDate = mStartDate; + this.mEndDate = mEndDate; + this.leaderDesc = leaderDesc; + this.note = note; + this.isMentorNeeded = isMentorNeeded; + this.canJoinOnlyActiveGeneration = canJoinOnlyActiveGeneration; + this.createdGeneration = createdGeneration; + this.targetActiveGeneration = targetActiveGeneration; + this.joinableParts = joinableParts; + } + + public LocalDateTime getmStartDate() { + return mStartDate; + } + + public LocalDateTime getmEndDate() { + return mEndDate; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingBannerResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingBannerResponseDto.java index 76832234..e34da848 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingBannerResponseDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingBannerResponseDto.java @@ -115,7 +115,7 @@ public static MeetingV2GetMeetingBannerResponseDto of(Meeting meeting, LocalDate return new MeetingV2GetMeetingBannerResponseDto(meeting.getId(), meeting.getUserId(), meeting.getTitle(), meeting.getCategory(), - meeting.getImageURL(), meeting.getMStartDate(), meeting.getMEndDate(), meeting.getStartDate(), + meeting.getImageURL(), meeting.getmStartDate(), meeting.getmEndDate(), meeting.getStartDate(), meeting.getEndDate(), meeting.getCapacity(), recentActivityDate, meeting.getTargetActiveGeneration(), meeting.getJoinableParts(), applicantCount, approvedUserCount, meetingCreator, meeting.getMeetingStatusValue(now)); diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingByIdResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingByIdResponseDto.java index e3d274cc..17037d71 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingByIdResponseDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingByIdResponseDto.java @@ -8,7 +8,6 @@ import org.sopt.makers.crew.main.entity.meeting.Meeting; import org.sopt.makers.crew.main.entity.meeting.enums.MeetingJoinablePart; import org.sopt.makers.crew.main.entity.meeting.vo.ImageUrlVO; -import org.sopt.makers.crew.main.entity.user.User; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; @@ -140,23 +139,21 @@ public class MeetingV2GetMeetingByIdResponseDto { @NotNull private final List appliedInfo; - public static MeetingV2GetMeetingByIdResponseDto of(Meeting meeting, List coLeaders, + public static MeetingV2GetMeetingByIdResponseDto of(Integer meetingId, Meeting meeting, List coLeaders, boolean isCoLeader, long approvedCount, Boolean isHost, Boolean isApply, - Boolean isApproved, User meetingCreator, + Boolean isApproved, MeetingCreatorDto meetingCreatorDto, List appliedInfo, LocalDateTime now) { - MeetingCreatorDto meetingCreatorDto = MeetingCreatorDto.of(meetingCreator); - Integer meetingStatus = meeting.getMeetingStatusValue(now); List coLeaderResponseDtos = coLeaders.stream() .map(coLeader -> MeetingV2CoLeaderResponseDto.of(coLeader.getUser())) .toList(); - return new MeetingV2GetMeetingByIdResponseDto(meeting.getId(), meeting.getUserId(), meeting.getTitle(), + return new MeetingV2GetMeetingByIdResponseDto(meetingId, meeting.getUserId(), meeting.getTitle(), meeting.getCategory().getValue(), meeting.getImageURL(), meeting.getStartDate(), meeting.getEndDate(), - meeting.getCapacity(), meeting.getDesc(), meeting.getProcessDesc(), meeting.getMStartDate(), - meeting.getMEndDate(), meeting.getLeaderDesc(), meeting.getNote(), + meeting.getCapacity(), meeting.getDesc(), meeting.getProcessDesc(), meeting.getmStartDate(), + meeting.getmEndDate(), meeting.getLeaderDesc(), meeting.getNote(), meeting.getIsMentorNeeded(), meeting.getCanJoinOnlyActiveGeneration(), meeting.getCreatedGeneration(), meeting.getTargetActiveGeneration(), meeting.getJoinableParts(), coLeaderResponseDtos, isCoLeader, meetingStatus, @@ -170,8 +167,4 @@ public LocalDateTime getmStartDate() { public LocalDateTime getmEndDate() { return mEndDate; } - - public boolean getIsCoLeader() { - return this.isCoLeader; - } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java index 76a6ee56..d1997fa4 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java @@ -1,8 +1,8 @@ package org.sopt.makers.crew.main.meeting.v2.service; -import static org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus.*; import static org.sopt.makers.crew.main.global.constant.CrewConst.*; import static org.sopt.makers.crew.main.global.exception.ErrorStatus.*; +import static org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus.*; import java.io.FileOutputStream; import java.io.IOException; @@ -22,6 +22,24 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import lombok.RequiredArgsConstructor; + +import org.sopt.makers.crew.main.entity.meeting.CoLeader; +import org.sopt.makers.crew.main.entity.meeting.CoLeaderReader; +import org.sopt.makers.crew.main.entity.meeting.CoLeaderRepository; +import org.sopt.makers.crew.main.entity.meeting.CoLeaders; +import org.sopt.makers.crew.main.entity.meeting.MeetingReader; +import org.sopt.makers.crew.main.entity.meeting.enums.MeetingCategory; +import org.sopt.makers.crew.main.entity.user.UserReader; +import org.sopt.makers.crew.main.global.dto.MeetingCreatorDto; +import org.sopt.makers.crew.main.global.dto.MeetingResponseDto; +import org.sopt.makers.crew.main.global.exception.BadRequestException; +import org.sopt.makers.crew.main.global.exception.ServerException; +import org.sopt.makers.crew.main.global.pagination.dto.PageMetaDto; +import org.sopt.makers.crew.main.global.pagination.dto.PageOptionsDto; +import org.sopt.makers.crew.main.global.util.AdvertisementCustomPageable; +import org.sopt.makers.crew.main.global.util.Time; +import org.sopt.makers.crew.main.global.util.UserPartUtil; import org.sopt.makers.crew.main.entity.apply.Applies; import org.sopt.makers.crew.main.entity.apply.Apply; import org.sopt.makers.crew.main.entity.apply.ApplyRepository; @@ -30,12 +48,8 @@ import org.sopt.makers.crew.main.entity.comment.Comment; import org.sopt.makers.crew.main.entity.comment.CommentRepository; import org.sopt.makers.crew.main.entity.like.LikeRepository; -import org.sopt.makers.crew.main.entity.meeting.CoLeader; -import org.sopt.makers.crew.main.entity.meeting.CoLeaderRepository; -import org.sopt.makers.crew.main.entity.meeting.CoLeaders; import org.sopt.makers.crew.main.entity.meeting.Meeting; import org.sopt.makers.crew.main.entity.meeting.MeetingRepository; -import org.sopt.makers.crew.main.entity.meeting.enums.MeetingCategory; import org.sopt.makers.crew.main.entity.meeting.enums.MeetingJoinablePart; import org.sopt.makers.crew.main.entity.post.Post; import org.sopt.makers.crew.main.entity.post.PostRepository; @@ -44,14 +58,6 @@ import org.sopt.makers.crew.main.entity.user.enums.UserPart; import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; import org.sopt.makers.crew.main.external.s3.service.S3Service; -import org.sopt.makers.crew.main.global.dto.MeetingResponseDto; -import org.sopt.makers.crew.main.global.exception.BadRequestException; -import org.sopt.makers.crew.main.global.exception.ServerException; -import org.sopt.makers.crew.main.global.pagination.dto.PageMetaDto; -import org.sopt.makers.crew.main.global.pagination.dto.PageOptionsDto; -import org.sopt.makers.crew.main.global.util.AdvertisementCustomPageable; -import org.sopt.makers.crew.main.global.util.Time; -import org.sopt.makers.crew.main.global.util.UserPartUtil; import org.sopt.makers.crew.main.meeting.v2.dto.ApplyMapper; import org.sopt.makers.crew.main.meeting.v2.dto.MeetingMapper; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; @@ -72,6 +78,8 @@ import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetMeetingBannerResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetMeetingBannerResponseUserDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetMeetingByIdResponseDto; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Caching; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -80,8 +88,6 @@ import com.opencsv.CSVWriter; -import lombok.RequiredArgsConstructor; - @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -96,6 +102,9 @@ public class MeetingV2ServiceImpl implements MeetingV2Service { private final CommentRepository commentRepository; private final LikeRepository likeRepository; private final CoLeaderRepository coLeaderRepository; + private final MeetingReader meetingReader; + private final CoLeaderReader coLeaderReader; + private final UserReader userReader; private final S3Service s3Service; @@ -124,7 +133,7 @@ public MeetingV2GetAllMeetingByOrgUserDto getAllMeetingByOrgUser( .map(meeting -> MeetingV2GetAllMeetingByOrgUserMeetingDto.of(meeting.getId(), meeting.checkMeetingLeader(existUser.getId()), meeting.getTitle(), meeting.getImageURL().get(0).getUrl(), meeting.getCategory().getValue(), - meeting.getMStartDate(), meeting.getMEndDate(), checkActivityStatus(meeting))) + meeting.getmStartDate(), meeting.getmEndDate(), checkActivityStatus(meeting))) .sorted(Comparator.comparing(MeetingV2GetAllMeetingByOrgUserMeetingDto::getId).reversed()) .collect(Collectors.toList()); } @@ -340,6 +349,11 @@ public void deleteMeeting(Integer meetingId, Integer userId) { meetingRepository.delete(meeting); } + @Caching(evict = { + @CacheEvict(value = "meetingCache", key = "#meetingId"), + @CacheEvict(value = "meetingLeaderCache", key = "#userId"), + @CacheEvict(value = "coLeadersCache", key = "#meetingId") + }) @Override @Transactional public void updateMeeting(Integer meetingId, MeetingV2CreateMeetingBodyDto requestBody, Integer userId) { @@ -409,9 +423,9 @@ public AppliesCsvFileUrlResponseDto getAppliesCsvFileUrl(Integer meetingId, List public MeetingV2GetMeetingByIdResponseDto getMeetingById(Integer meetingId, Integer userId) { User user = userRepository.findByIdOrThrow(userId); - Meeting meeting = meetingRepository.findByIdOrThrow(meetingId); - User meetingLeader = userRepository.findByIdOrThrow(meeting.getUserId()); - CoLeaders coLeaders = new CoLeaders(coLeaderRepository.findAllByMeetingId(meetingId)); + Meeting meeting = meetingReader.getMeetingById(meetingId).toEntity(); + MeetingCreatorDto meetingLeader = userReader.getMeetingLeader(meeting.getUserId()); + CoLeaders coLeaders = coLeaderReader.getCoLeaders(meetingId).toEntity(); Applies applies = new Applies( applyRepository.findAllByMeetingIdWithUser(meetingId, List.of(WAITING, APPROVE, REJECT), ORDER_ASC)); @@ -429,7 +443,7 @@ public MeetingV2GetMeetingByIdResponseDto getMeetingById(Integer meetingId, Inte .toList(); } - return MeetingV2GetMeetingByIdResponseDto.of(meeting, coLeaders.getCoLeaders(meetingId), isCoLeader, + return MeetingV2GetMeetingByIdResponseDto.of(meetingId, meeting, coLeaders.getCoLeaders(meetingId), isCoLeader, approvedCount, isHost, isApply, isApproved, meetingLeader, applyWholeInfoDtos, time.now()); } @@ -480,8 +494,8 @@ private String createCsvFile(List applies) { private Boolean checkActivityStatus(Meeting meeting) { LocalDateTime now = time.now(); - LocalDateTime mStartDate = meeting.getMStartDate(); - LocalDateTime mEndDate = meeting.getMEndDate(); + LocalDateTime mStartDate = meeting.getmStartDate(); + LocalDateTime mEndDate = meeting.getmEndDate(); return now.isEqual(mStartDate) || (now.isAfter(mStartDate) && now.isBefore(mEndDate)); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java index 8a3c2485..e3036ddc 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java @@ -83,7 +83,7 @@ public static MeetingV2GetCreatedMeetingByUserResponseDto of(Meeting meeting, bo return new MeetingV2GetCreatedMeetingByUserResponseDto(meeting.getId(), meeting.getTitle(), meeting.getTargetActiveGeneration(), meeting.getJoinableParts(), meeting.getCategory().getValue(), canJoinOnlyActiveGeneration, meeting.getMeetingStatusValue(now), isCoLeader, meeting.getImageURL(), - meeting.getIsMentorNeeded(), meeting.getMStartDate(), meeting.getMEndDate(), meeting.getCapacity(), + meeting.getIsMentorNeeded(), meeting.getmStartDate(), meeting.getmEndDate(), meeting.getCapacity(), creatorDto, approvedCount, approvedCount); } diff --git a/main/src/main/resources/application-dev.yml b/main/src/main/resources/application-dev.yml index 9f73f98c..65b144bf 100644 --- a/main/src/main/resources/application-dev.yml +++ b/main/src/main/resources/application-dev.yml @@ -27,6 +27,10 @@ spring: format_sql: ture dialect: org.hibernate.dialect.PostgreSQLDialect storage_engine: innodb + data: + redis: + host: redis + port: 6379 jwt: header: Authorization diff --git a/main/src/main/resources/application-local.yml b/main/src/main/resources/application-local.yml index 7e76915c..c6f1b84d 100644 --- a/main/src/main/resources/application-local.yml +++ b/main/src/main/resources/application-local.yml @@ -27,6 +27,10 @@ spring: format_sql: true dialect: org.hibernate.dialect.PostgreSQLDialect storage_engine: innodb + data: + redis: + host: localhost + port: 6379 jwt: header: Authorization diff --git a/main/src/main/resources/application-prod.yml b/main/src/main/resources/application-prod.yml index eaabdca2..8b76da2b 100644 --- a/main/src/main/resources/application-prod.yml +++ b/main/src/main/resources/application-prod.yml @@ -26,6 +26,10 @@ spring: format_sql: ture dialect: org.hibernate.dialect.PostgreSQLDialect storage_engine: innodb + data: + redis: + host: redis + port: 6379 jwt: header: Authorization diff --git a/main/src/main/resources/application-test.yml b/main/src/main/resources/application-test.yml index 37ea31a7..08c2faae 100644 --- a/main/src/main/resources/application-test.yml +++ b/main/src/main/resources/application-test.yml @@ -25,6 +25,10 @@ spring: dialect: org.hibernate.dialect.PostgreSQLDialect storage_engine: innodb defer-datasource-initialization: true + data: + redis: + host: localhost + port: 6379 jwt: header: Authorization diff --git a/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserActivityVOTest.java b/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserActivityVOTest.java new file mode 100644 index 00000000..43167afd --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserActivityVOTest.java @@ -0,0 +1,16 @@ +package org.sopt.makers.crew.main.entity.user; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; +import org.sopt.makers.crew.main.global.exception.ServerException; + +class UserActivityVOTest { + @Test + void 최근_기수_조회_잘못된_데이터가_저장된_경우_예외가_발생한다(){ + + // given, when, then + Assertions.assertThatThrownBy(() -> new UserActivityVO(null, 34)) + .isInstanceOf(ServerException.class); + } +} diff --git a/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserEntityTest.java b/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserEntityTest.java index a88f3907..39134b39 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserEntityTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserEntityTest.java @@ -5,8 +5,7 @@ import org.junit.jupiter.api.Test; import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; -public class UserEntityTest { - +class UserEntityTest { @Test void 최근_기수_조회(){ @@ -28,24 +27,4 @@ public class UserEntityTest { Assertions.assertThat(recentActivityVO.getPart()).isEqualTo("iOS"); } - - @Test - void 최근_기수_조회_잘못된_데이터가_저장된_경우_해당_데이터는_무시한다(){ - // given - User user = User.builder() - .name("홍길동") - .orgId(1) - .activities(List.of(new UserActivityVO(null, 34), new UserActivityVO("서버", 33))) - .profileImage("image-url") - .phone("010-1234-5678") - .build(); - - // when - UserActivityVO recentActivityVO = user.getRecentActivityVO(); - - // then - - Assertions.assertThat(recentActivityVO.getGeneration()).isEqualTo(33); - Assertions.assertThat(recentActivityVO.getPart()).isEqualTo("서버"); - } } diff --git a/main/src/test/java/org/sopt/makers/crew/main/external/redisContainerBaseTest.java b/main/src/test/java/org/sopt/makers/crew/main/external/redisContainerBaseTest.java new file mode 100644 index 00000000..a69a1866 --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/external/redisContainerBaseTest.java @@ -0,0 +1,25 @@ +package org.sopt.makers.crew.main.external; + +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +public abstract class redisContainerBaseTest { + static final String REDIS_IMAGE = "redis:6-alpine"; + static final GenericContainer REDIS_CONTAINER; + + static { + REDIS_CONTAINER = new GenericContainer<>(REDIS_IMAGE) + .withExposedPorts(6379) + .withReuse(true) + .waitingFor(Wait.forListeningPort()); + REDIS_CONTAINER.start(); + } + + @DynamicPropertySource + public static void overrideProps(DynamicPropertyRegistry registry) { + registry.add("spring.data.redis.host", REDIS_CONTAINER::getHost); + registry.add("spring.data.redis.port", () -> String.valueOf(REDIS_CONTAINER.getMappedPort(6379))); + } +} \ No newline at end of file diff --git a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java index ce4aff6a..53a1eab8 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java @@ -22,21 +22,22 @@ import org.sopt.makers.crew.main.entity.apply.enums.EnApplyType; import org.sopt.makers.crew.main.entity.meeting.CoLeader; import org.sopt.makers.crew.main.entity.meeting.CoLeaderRepository; +import org.sopt.makers.crew.main.entity.meeting.enums.EnMeetingStatus; +import org.sopt.makers.crew.main.entity.meeting.vo.ImageUrlVO; +import org.sopt.makers.crew.main.external.redisContainerBaseTest; +import org.sopt.makers.crew.main.global.annotation.IntegratedTest; import org.sopt.makers.crew.main.entity.meeting.Meeting; import org.sopt.makers.crew.main.entity.meeting.MeetingRepository; -import org.sopt.makers.crew.main.entity.meeting.enums.EnMeetingStatus; import org.sopt.makers.crew.main.entity.meeting.enums.MeetingCategory; import org.sopt.makers.crew.main.entity.meeting.enums.MeetingJoinablePart; -import org.sopt.makers.crew.main.entity.meeting.vo.ImageUrlVO; import org.sopt.makers.crew.main.entity.user.User; import org.sopt.makers.crew.main.entity.user.UserRepository; import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; -import org.sopt.makers.crew.main.global.annotation.IntegratedTest; import org.sopt.makers.crew.main.global.dto.MeetingCreatorDto; import org.sopt.makers.crew.main.global.dto.MeetingResponseDto; -import org.sopt.makers.crew.main.global.exception.BadRequestException; import org.sopt.makers.crew.main.global.exception.ForbiddenException; import org.sopt.makers.crew.main.global.exception.NotFoundException; +import org.sopt.makers.crew.main.global.exception.BadRequestException; import org.sopt.makers.crew.main.meeting.v2.dto.ApplyMapper; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingV2GetAllMeetingQueryDto; @@ -56,7 +57,7 @@ import org.springframework.test.context.jdbc.SqlGroup; @IntegratedTest -public class MeetingV2ServiceTest { +public class MeetingV2ServiceTest extends redisContainerBaseTest { @Autowired private MeetingV2Service meetingV2Service; diff --git a/main/src/test/java/org/sopt/makers/crew/main/user/v2/UserServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/user/v2/UserServiceTest.java index c9a9596e..4bf2a27e 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/user/v2/UserServiceTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/user/v2/UserServiceTest.java @@ -98,38 +98,6 @@ class 전체_사용자_조회 { .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") .containsExactly("김철수", "안드로이드", 33, "image-url2"); } - - @Test - void 멘션_사용자_조회시_db에_올바르지_않은_데이터_저장된_경우() { - // given - User user1 = User.builder() - .name("홍길동") - .orgId(1) - .activities(List.of(new UserActivityVO("서버", 33), new UserActivityVO("iOS", 34))) - .profileImage("image-url1") - .phone("010-1234-5678") - .build(); - User user2 = User.builder() - .name("김철수") - .orgId(2) - .activities(List.of(new UserActivityVO(null, 30), new UserActivityVO("", 34))) - .profileImage("image-url2") - .phone("010-1111-2222") - .build(); - userRepository.saveAll(List.of(user1, user2)); - - // when - List allMentionUsers = userV2Service.getAllUser(); - - // then - assertThat(allMentionUsers).hasSize(2); - assertThat(allMentionUsers.get(0)) - .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") - .containsExactly("홍길동", "iOS", 34, "image-url1"); - assertThat(allMentionUsers.get(1)) - .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") - .containsExactly("김철수", "", 34, "image-url2"); - } } @Nested