Skip to content
Closed
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
24ca061
chore: 컨벤션에 위배된 메서드 네임 수정
7zrv Jan 14, 2025
3d3bb57
depend: queryDsl의 SQLExpression 사용을 위한 의존성 추가
7zrv Jan 16, 2025
627720f
chore: 테스트 클래스명 수정
7zrv Jan 16, 2025
9a0702f
feat: 전체, 월별, 주간 봉사 시간 랭킹 쿼리 구현
7zrv Jan 16, 2025
902d44c
feat: 랭킹 계산 유스케이스 구현
7zrv Jan 16, 2025
a754527
feat: 랭킹 캐싱을 위한 redis 레포지토리와 유스케이스 구현
7zrv Jan 16, 2025
92a6ce4
feat: 랭킹 계산, 캐싱을 위한 스케줄러 구현
7zrv Jan 16, 2025
0be813a
feat: 봉사 랭킹 조회 유스케이스 구현
7zrv Jan 16, 2025
e65c87d
feat: 테스트용 DB 설정 추가
7zrv Jan 16, 2025
c2c8e0d
feat: 누락된 테스트 클래스 추가
7zrv Jan 16, 2025
33c7151
feat: test container 추가
7zrv Jan 17, 2025
756d27e
chore: application-test.yml 수정
7zrv Jan 17, 2025
26da3b0
refactor: 랭킹 산출 쿼리 리팩토링
7zrv Jan 17, 2025
366523b
feat: 봉사 랭킹 엔드 포인트 구현
7zrv Jan 17, 2025
4abb947
feat: 봉사 시간 랭킹 스케줄러 로깅 추가
7zrv Jan 17, 2025
482ddfa
refactor: 봉사 시간 랭킹 반환 예외 메세지 수정
7zrv Jan 17, 2025
b753096
feat: 봉사자 닉네임 리스트 반환 쿼리 메서드 생성
7zrv Jan 18, 2025
b627ba1
feat: 봉사자 닉네임 리스트 반환 유스케이스 구현
7zrv Jan 18, 2025
d32c9e0
chore: 봉사자 닉네임 반환 관련 클래스의 명시적인 이름 수정
7zrv Jan 18, 2025
81a1e1c
feat: 봉사 시간 랭킹 Dto nickname 필드 추가
7zrv Jan 18, 2025
98fa3af
refactor: 봉사자 닉네임 반환 유스케이스 리팩토링
7zrv Jan 18, 2025
e2ec50d
refactor: 봉사자 닉네임 반환 유스케이스 리팩토링
7zrv Jan 18, 2025
529ec4f
refactor: 봉사 랭킹의 닉네임 필드 추가에 따른 테스트 수정
7zrv Jan 18, 2025
eeaf438
refactor: 봉사 랭킹 객체 맵퍼 리팩토링
7zrv Jan 18, 2025
5587c96
refactor: 봉사 시간 랭킹 유스케이스 리팩토링
7zrv Jan 18, 2025
c27cfbc
chore: 불필요한 클래스 삭제및 build.gradle 개행 수정
7zrv Jan 19, 2025
9f4953d
feat: test container 설정 추가
7zrv Jan 19, 2025
b6d7ac5
fix: 에러 발생 test 수정
7zrv Jan 19, 2025
78e3cf1
chore: cicd workflow 수정
7zrv Jan 19, 2025
c91552e
fix: ci 파이프 라인 수정
7zrv Jan 19, 2025
f2ff999
fix: ci 파이프 라인 수정
7zrv Jan 19, 2025
cb62123
fix: ci 파이프 라인 수정
7zrv Jan 19, 2025
450d909
fix: 봉사시간 계산 테스트 날짜 수정
7zrv Jan 19, 2025
0914343
fix: ci 테스트 레포트 분석을 위한 설정 추가
7zrv Jan 19, 2025
03126bd
fix: 동시성 테스트 스레드 획득 에러 수정
7zrv Jan 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions .github/workflows/CD.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ jobs:
build-and-push:
runs-on: ubuntu-latest

services:
redis:
image: redis
ports:
- 6379:6379

env:
DB_URL: ${{ secrets.DB_URL }}
DB_USERNAME: ${{ secrets.DB_USERNAME }}
Expand Down Expand Up @@ -43,6 +37,12 @@ jobs:
ELASTIC_PASSWORD: ${{ secrets.ELASTIC_PASSWORD }}
LOKI_URL: ${{ secrets.LOKI_URL }}

services:
redis:
image: redis
ports:
- 6379:6379

steps:
- name: Github Repository 파일 불러오기
uses: actions/checkout@v4
Expand All @@ -65,6 +65,10 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-

- name: Docker 권한 설정
run: |
sudo chmod 666 /var/run/docker.sock

- name: 빌드 권한 부여
run: chmod +x ./gradlew
shell: bash
Expand Down
34 changes: 21 additions & 13 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
name: CI Pipeline

on:
push:
branches:
[ main ]
pull_request:
branches:
[ main ]
Expand All @@ -14,14 +11,6 @@ jobs:
Continuous-Integration:
runs-on: ubuntu-latest

#Action 환경에서 Redis 실행을 위한 설정 추후 서버 배포시 제거 예정
services:
redis:
image: redis
ports:
- 6379:6379


env:
DB_URL: ${{ secrets.DB_URL }}
DB_USERNAME: ${{ secrets.DB_USERNAME }}
Expand Down Expand Up @@ -51,13 +40,25 @@ jobs:
ELASTIC_PASSWORD: ${{ secrets.ELASTIC_PASSWORD }}
LOKI_URL: ${{ secrets.LOKI_URL }}

TESTCONTAINERS_REUSE_ENABLE: true
TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE: /var/run/docker.sock
DOCKER_HOST: unix:///var/run/docker.sock

services:
redis:
image: redis
ports:
- 6379:6379

steps:
- name: Github Repository 파일 불러오기
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Docker
uses: docker/setup-buildx-action@v3

- name: JDK 21 버전 설치
uses: actions/setup-java@v4
with:
Expand Down Expand Up @@ -86,7 +87,14 @@ jobs:
shell: bash

- name: 빌드 및 테스트
run: ./gradlew build
run: ./gradlew build --info

- name: Save test reports
if: ${{ always() }}
uses: actions/upload-artifact@v3
with:
name: test-reports
path: build/reports/tests/test

- name: Close PR, if build fail
if: ${{ failure() }}
Expand All @@ -113,6 +121,6 @@ jobs:

- name: SonarCloud 빌드및 분석
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: ./gradlew build sonar --info
14 changes: 12 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies {

// Querydsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
implementation 'com.querydsl:querydsl-sql:5.0.0'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
Expand Down Expand Up @@ -79,6 +80,10 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation group: 'org.testcontainers', name: 'testcontainers', version: '1.20.4'
testImplementation group: 'org.testcontainers', name: 'junit-jupiter', version: '1.20.4'
testImplementation group: 'org.testcontainers', name: 'mysql', version: '1.20.4'

}

// Querydsl 설정
Expand All @@ -103,6 +108,7 @@ tasks.named('test') {
useJUnitPlatform()
jvmArgs("-XX:+EnableDynamicAgentLoading")
if (gradle.startParameter.taskNames.contains('build')) {
exclude '**/*ControllerTest.class'
finalizedBy 'jacocoTestReport'
}
}
Expand Down Expand Up @@ -131,7 +137,9 @@ def jacocoExcludePatterns = [
'**/fixture/*',
'**/*Factory*',
'**/event/**',
'**/*Aspect*'
'**/*Aspect*',
'**/Aspect/*',
'**/utils/*'
]

def jacocoExcludePatternsForVerify = [
Expand All @@ -151,7 +159,9 @@ def jacocoExcludePatternsForVerify = [
'*.fixture.*',
'*.*Factory*',
'*.*event*.*',
'*.*Aspect*'
'*.*Aspect*',
'*.Aspect.*',
'*.utils.*'
]

jacocoTestReport {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ private Optional<Volunteer> findOne(BooleanExpression condition) {
condition,
isNotDeleted()
)
.fetchOne()
.fetchFirst()
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.somemore.domains.volunteerrecord.controller;

import com.somemore.domains.volunteerrecord.dto.response.VolunteerRankingResponseDto;
import com.somemore.domains.volunteerrecord.usecase.GetVolunteerRankingUseCase;
import com.somemore.global.common.response.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@Tag(name = "Volunteer Ranking API", description = "봉사 시간 랭킹 API")
@RequiredArgsConstructor
@RequestMapping("/api/volunteerrecord")
@RestController
public class VolunteerRankingApiController {

private final GetVolunteerRankingUseCase getVolunteerRankingUseCase;

@Operation(summary = "봉사 시간 랭킹", description = "봉사 시간 랭킹을 반환합니다.")
@GetMapping("/ranking")
public ApiResponse<VolunteerRankingResponseDto> updateCenterProfile() {

VolunteerRankingResponseDto volunteerRankings = getVolunteerRankingUseCase.getVolunteerRanking();

return ApiResponse.ok(volunteerRankings,"봉사 시간 랭킹 반환 성공");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.somemore.domains.volunteerrecord.dto.response;

import java.util.UUID;

public record VolunteerMonthlyRankingResponseDto(
UUID volunteerId,
int totalHours,
long ranking,
String nickname
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.somemore.domains.volunteerrecord.dto.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.somemore.domains.center.dto.response.PreferItemResponseDto;
import lombok.Builder;

import java.util.List;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@Builder
public record VolunteerRankingResponseDto(
List<VolunteerTotalRankingResponseDto> volunteerTotalRankingResponse,
List<VolunteerMonthlyRankingResponseDto> volunteerMonthlyResponse,
List<VolunteerWeeklyRankingResponseDto> volunteerWeeklyRankingResponse
) {
public static VolunteerRankingResponseDto of(
List<VolunteerTotalRankingResponseDto> totalRanking,
List<VolunteerMonthlyRankingResponseDto> monthlyRanking,
List<VolunteerWeeklyRankingResponseDto> weeklyRanking){

return VolunteerRankingResponseDto.builder()
.volunteerTotalRankingResponse(totalRanking)
.volunteerMonthlyResponse(monthlyRanking)
.volunteerWeeklyRankingResponse(weeklyRanking)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.somemore.domains.volunteerrecord.dto.response;

import java.util.UUID;

public record VolunteerTotalRankingResponseDto(
UUID volunteerId,
int totalHours,
long ranking,
String nickname
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.somemore.domains.volunteerrecord.dto.response;

import java.util.UUID;

public record VolunteerWeeklyRankingResponseDto(
UUID volunteerId,
int totalHours,
long ranking,
String nickname
) {
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.somemore.domains.volunteerrecord.event.handler;

import com.somemore.domains.volunteerrecord.domain.VolunteerRecord;
import com.somemore.domains.volunteerrecord.usecase.VolunteerRecordCreateUseCase;
import com.somemore.domains.volunteerrecord.usecase.CreateVolunteerRecordUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -11,11 +11,11 @@
@Transactional
public class SettleVolunteerHoursHandlerImpl implements SettleVolunteerHoursHandler {

private final VolunteerRecordCreateUseCase volunteerRecordCreateUseCase;
private final CreateVolunteerRecordUseCase createvolunteerRecordUseCase;

@Override
public void handle(VolunteerRecord volunteerRecord) {

volunteerRecordCreateUseCase.create(volunteerRecord);
createvolunteerRecordUseCase.create(volunteerRecord);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.somemore.domains.volunteerrecord.repository;

import com.somemore.domains.volunteerrecord.dto.response.VolunteerMonthlyRankingResponseDto;
import com.somemore.domains.volunteerrecord.dto.response.VolunteerRankingResponseDto;
import com.somemore.domains.volunteerrecord.dto.response.VolunteerTotalRankingResponseDto;
import com.somemore.domains.volunteerrecord.dto.response.VolunteerWeeklyRankingResponseDto;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

import java.time.Duration;
import java.util.List;
import java.util.Optional;

@RequiredArgsConstructor
@Repository
public class VolunteerRankingRedisRepository {

private final RedisTemplate<String, Object> redisTemplate;

private static final Duration CACHE_TTL = Duration.ofMinutes(60);
private static final String TOTAL_RANKING_KEY = "volunteer:total_ranking";
private static final String MONTHLY_RANKING_KEY = "volunteer:monthly_ranking";
private static final String WEEKLY_RANKING_KEY = "volunteer:weekly_ranking";

public void saveRanking(VolunteerRankingResponseDto rankings) {
redisTemplate.opsForValue().set(TOTAL_RANKING_KEY, rankings.volunteerTotalRankingResponse(), CACHE_TTL);
redisTemplate.opsForValue().set(MONTHLY_RANKING_KEY, rankings.volunteerMonthlyResponse(), CACHE_TTL);
redisTemplate.opsForValue().set(WEEKLY_RANKING_KEY, rankings.volunteerWeeklyRankingResponse(), CACHE_TTL);
}

@SuppressWarnings("unchecked")
public Optional<VolunteerRankingResponseDto> getRankings() {

List<VolunteerTotalRankingResponseDto> totalRanking =
(List<VolunteerTotalRankingResponseDto>) redisTemplate.opsForValue().get(TOTAL_RANKING_KEY);
List<VolunteerMonthlyRankingResponseDto> monthlyRanking =
(List<VolunteerMonthlyRankingResponseDto>) redisTemplate.opsForValue().get(MONTHLY_RANKING_KEY);
List<VolunteerWeeklyRankingResponseDto> weeklyRanking =
(List<VolunteerWeeklyRankingResponseDto>) redisTemplate.opsForValue().get(WEEKLY_RANKING_KEY);

if (totalRanking == null || monthlyRanking == null || weeklyRanking == null) {
return Optional.empty();
}

return Optional.of(VolunteerRankingResponseDto.of(
totalRanking,
monthlyRanking,
weeklyRanking
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,54 @@

import com.somemore.domains.volunteerrecord.domain.VolunteerRecord;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface VolunteerRecordJpaRepository extends JpaRepository<VolunteerRecord, Long> {
}

@Query(value = """
SELECT volunteerId, totalHours, ranking
FROM (
SELECT
vr.volunteer_id AS volunteerId,
SUM(vr.volunteer_hours) AS totalHours,
DENSE_RANK() OVER (ORDER BY SUM(vr.volunteer_hours) DESC) AS ranking
FROM volunteer_record vr
GROUP BY vr.volunteer_id
) ranked
WHERE ranking <= 4
""", nativeQuery = true)
List<Object[]> findTotalTopRankingWithTies();

@Query(value = """
SELECT volunteerId, totalHours, ranking
FROM (
SELECT
vr.volunteer_id AS volunteerId,
SUM(vr.volunteer_hours) AS totalHours,
DENSE_RANK() OVER (ORDER BY SUM(vr.volunteer_hours) DESC) AS ranking
FROM volunteer_record vr
WHERE YEARWEEK(vr.volunteer_date, 1) = YEARWEEK(CURDATE(), 1)
GROUP BY vr.volunteer_id
) ranked
WHERE ranking <= 4
""", nativeQuery = true)
List<Object[]> findWeeklyTopRankingWithTies();

@Query(value = """
SELECT volunteerId, totalHours, ranking
FROM (
SELECT
vr.volunteer_id AS volunteerId,
SUM(vr.volunteer_hours) AS totalHours,
DENSE_RANK() OVER (ORDER BY SUM(vr.volunteer_hours) DESC) AS ranking
FROM volunteer_record vr
WHERE MONTH(vr.volunteer_date) = MONTH(CURDATE())
AND YEAR(vr.volunteer_date) = YEAR(CURDATE())
GROUP BY vr.volunteer_id
) ranked
WHERE ranking <= 4
""", nativeQuery = true)
List<Object[]> findMonthlyTopRankingWithTies();
}
Loading
Loading