Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.threestar.trainus.domain.profile.service.ProfileFacadeService;
import com.threestar.trainus.global.unit.BaseResponse;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
Expand All @@ -29,6 +30,7 @@ public class ProfileController {
private final ProfileFacadeService facadeService;

@GetMapping("{userId}")
@Operation(summary = "μœ μ € ν”„λ‘œν•„ 상세 쑰회 api")
public ResponseEntity<BaseResponse<ProfileDetailResponseDto>> getProfileDetail(
@PathVariable Long userId
) {
Expand All @@ -37,6 +39,7 @@ public ResponseEntity<BaseResponse<ProfileDetailResponseDto>> getProfileDetail(
}

@PatchMapping
@Operation(summary = "μœ μ € ν”„λ‘œν•„ μˆ˜μ • api")
public ResponseEntity<BaseResponse<ProfileResponseDto>> updateProfile(
@Valid @RequestBody ProfileUpdateRequestDto requestDto,
HttpSession session
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.threestar.trainus.domain.ranking.dto.RankingResponseDto;
import com.threestar.trainus.domain.ranking.service.RankingService;
import com.threestar.trainus.global.unit.BaseResponse;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

Expand All @@ -24,8 +26,19 @@ public class RankingController {
private final RankingService rankingService;

@GetMapping
@Operation(summary = "전체 λž­ν‚Ή 쑰회 api", description = "μΉ΄ν…Œκ³ λ¦¬μ™€ 관계없이 전체 λž­ν‚Ή Top10을 쑰회")
public ResponseEntity<BaseResponse<List<RankingResponseDto>>> getRankings() {
List<RankingResponseDto> rankings = rankingService.getTopRankings();
List<RankingResponseDto> rankings = rankingService.getTopRankings("ALL");
return BaseResponse.ok("전체 λž­ν‚Ή 쑰회 성곡", rankings, HttpStatus.OK);
}

@GetMapping("/{category}")
@Operation(summary = "νŠΉμ • μΉ΄ν…Œκ³ λ¦¬ λž­ν‚Ή 쑰회 api", description = "μ§€μ •λœ μΉ΄ν…Œκ³ λ¦¬μ˜ λž­ν‚Ήμ„ 쑰회")
public ResponseEntity<BaseResponse<List<RankingResponseDto>>> getRankingsByCategory(
@PathVariable String category
) {
List<RankingResponseDto> rankings = rankingService.getTopRankings(category);
return BaseResponse.ok(category + "μΉ΄ν…Œκ³ λ¦¬λ³„ λž­ν‚Ή 쑰회 성곡", rankings, HttpStatus.OK);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public interface RankingData {

Integer getReviewCount();

Float getRating();
Double getRating();

String getProfileImage();
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class RankingResponseDto {

private Long userId;
private String userNickname;
private Float rating;
private Double rating;
private Integer reviewCount;
private Category category;
private Integer rank;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,51 @@

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.threestar.trainus.domain.lesson.admin.entity.Category;
import com.threestar.trainus.domain.metadata.entity.ProfileMetadata;
import com.threestar.trainus.domain.ranking.dto.RankingData;

public interface RankingRepository extends JpaRepository<ProfileMetadata, Long> {

@Query("""
SELECT pm.user.id as userId,
pm.user.nickname as userNickname,
pm.reviewCount as reviewCount,
pm.rating as rating,
p.profileImage as profileImage
FROM ProfileMetadata pm
JOIN pm.user u
LEFT JOIN Profile p ON p.user = u
WHERE pm.reviewCount >= 20
ORDER BY (
(pm.rating / 5.0) * 0.5 +
(LEAST(pm.reviewCount, 100) / 100.0) * 0.5
) DESC
LIMIT 10
""")
SELECT pm.user.id as userId,
pm.user.nickname as userNickname,
pm.reviewCount as reviewCount,
pm.rating as rating,
p.profileImage as profileImage
FROM ProfileMetadata pm
JOIN pm.user u
LEFT JOIN Profile p ON p.user = u
WHERE pm.reviewCount >= 20
ORDER BY (
(pm.rating / 5.0) * 0.5 +
(LEAST(pm.reviewCount, 100) / 100.0) * 0.5
) DESC
LIMIT 10
""")
List<RankingData> findTopRankings();

}
@Query("""
SELECT r.reviewee.id as userId,
r.reviewee.nickname as userNickname,
CAST(COUNT(r.reviewId) AS INTEGER) as reviewCount,
CAST(AVG(r.rating) AS DOUBLE) as rating,
p.profileImage as profileImage
FROM Review r
JOIN r.lesson l
JOIN r.reviewee u
LEFT JOIN Profile p ON p.user = u
WHERE l.category = :category
AND r.deletedAt IS NULL
GROUP BY r.reviewee.id, r.reviewee.nickname, p.profileImage
HAVING COUNT(r.reviewId) >= 20
ORDER BY (
(AVG(r.rating) / 5.0) * 0.5 +
(LEAST(COUNT(r.reviewId), 100) / 100.0) * 0.5
) DESC
LIMIT 10
""")
List<RankingData> findTopRankingsByCategory(@Param("category") Category category);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@

import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.threestar.trainus.domain.lesson.admin.entity.Category;
import com.threestar.trainus.domain.ranking.dto.RankingData;
import com.threestar.trainus.domain.ranking.dto.RankingResponseDto;
import com.threestar.trainus.domain.ranking.repository.RankingRepository;
import com.threestar.trainus.global.exception.domain.ErrorCode;
import com.threestar.trainus.global.exception.handler.BusinessException;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -27,10 +32,25 @@ public class RankingService {
private final ObjectMapper objectMapper;

private static final String RANKING_KEY = "ranking:all:top10";
private static final String CATEGORY_RANKING_KEY_PREFIX = "ranking:";
private static final String CATEGORY_RANKING_KEY_SUFFIX = ":top10";

public List<RankingResponseDto> getTopRankings(String categoryStr) {

Category category = null;
String cacheKey = RANKING_KEY;

if (!"ALL".equalsIgnoreCase(categoryStr)) {
try {
category = Category.valueOf(categoryStr.toUpperCase());
cacheKey = CATEGORY_RANKING_KEY_PREFIX + categoryStr.toLowerCase() + CATEGORY_RANKING_KEY_SUFFIX;
} catch (IllegalArgumentException e) {
throw new BusinessException(ErrorCode.INVALID_CATEGORY);
}
}

public List<RankingResponseDto> getTopRankings() {
try {
String cachedData = redisTemplate.opsForValue().get(RANKING_KEY);
String cachedData = redisTemplate.opsForValue().get(cacheKey);
if (cachedData != null) {
return objectMapper.readValue(cachedData, new TypeReference<List<RankingResponseDto>>() {
});
Expand All @@ -39,14 +59,15 @@ public List<RankingResponseDto> getTopRankings() {
log.warn("λ ˆλ””μŠ€ 쑰회 μ‹€νŒ¨: {}", e.getMessage());
}

List<RankingResponseDto> rankings = calculateRankings();
saveToRedis(rankings);
List<RankingResponseDto> rankings = calculateRankings(category);
saveToRedis(rankings, cacheKey);

return rankings;
}

private List<RankingResponseDto> calculateRankings() {
List<RankingData> data = rankingRepository.findTopRankings();
private List<RankingResponseDto> calculateRankings(Category category) {
List<RankingData> data = (category == null) ? rankingRepository.findTopRankings() :
rankingRepository.findTopRankingsByCategory(category);

List<RankingResponseDto> rankings = new ArrayList<>();

Expand All @@ -55,7 +76,7 @@ private List<RankingResponseDto> calculateRankings() {
rankings.add(RankingResponseDto.builder()
.userId(item.getUserId())
.userNickname(item.getUserNickname())
.category(null) //μΉ΄ν…Œκ³ λ¦¬λ³„ λΆ„λ₯˜λŠ” μΆ”ν›„ λ„μž…
.category(category) //null이면 전체 쑰회
.rating(item.getRating())
.reviewCount(item.getReviewCount())
.rank(i + 1)
Expand All @@ -66,11 +87,10 @@ private List<RankingResponseDto> calculateRankings() {
return rankings;
}

private void saveToRedis(List<RankingResponseDto> rankings) {
private void saveToRedis(List<RankingResponseDto> rankings, String cacheKey) {
try {
String json = objectMapper.writeValueAsString(rankings);
redisTemplate.opsForValue().set(RANKING_KEY, json, Duration.ofHours(24));
log.info("Redis에 λž­ν‚Ή 데이터 μ €μž₯ μ™„λ£Œ");
redisTemplate.opsForValue().set(cacheKey, json, Duration.ofHours(24));
} catch (Exception e) {
log.warn("Redis μ €μž₯ μ‹€νŒ¨: {}", e.getMessage());
}
Expand All @@ -80,10 +100,22 @@ private void saveToRedis(List<RankingResponseDto> rankings) {
@Scheduled(cron = "0 0 0 * * *")
public void updateRankings() {
log.info("λž­ν‚Ή μ—…λ°μ΄νŠΈ μ‹œμž‘");

Map<String, Integer> categoryCounts = new HashMap<>();

try {
List<RankingResponseDto> rankings = calculateRankings();
saveToRedis(rankings);
log.info("λž­ν‚Ή μ—…λ°μ΄νŠΈ μ™„λ£Œ");
//전체 λž­ν‚Ή μ—…λ°μ΄νŠΈ
List<RankingResponseDto> allRankings = calculateRankings(null);
saveToRedis(allRankings, RANKING_KEY);
categoryCounts.put("ALL", allRankings.size());

//μΉ΄ν…Œκ³ λ¦¬λ³„ λž­ν‚Ή μ—…λ°μ΄νŠΈ
for (Category category : Category.values()) {
List<RankingResponseDto> categoryRankings = calculateRankings(category);
String cacheKey = CATEGORY_RANKING_KEY_PREFIX + category.name().toLowerCase() + CATEGORY_RANKING_KEY_SUFFIX;
saveToRedis(categoryRankings, cacheKey);
categoryCounts.put(category.name(), categoryRankings.size());
}
} catch (Exception e) {
log.error("λž­ν‚Ή μ—…λ°μ΄νŠΈ μ‹€νŒ¨: {}", e.getMessage(), e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.threestar.trainus.domain.user.service.UserService;
import com.threestar.trainus.global.unit.BaseResponse;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
Expand All @@ -34,6 +35,7 @@ public class UserController {
private final EmailVerificationService emailVerificationService;

@PostMapping("/signup")
@Operation(summary = "νšŒμ›κ°€μž… api")
public ResponseEntity<BaseResponse<SignupResponseDto>> signup(
@Valid @RequestBody SignupRequestDto request
) {
Expand All @@ -43,6 +45,7 @@ public ResponseEntity<BaseResponse<SignupResponseDto>> signup(
}

@PostMapping("/login")
@Operation(summary = "둜그인 api")
public ResponseEntity<BaseResponse<LoginResponseDto>> login(
@Valid @RequestBody LoginRequestDto request,
HttpSession session
Expand All @@ -52,12 +55,14 @@ public ResponseEntity<BaseResponse<LoginResponseDto>> login(
}

@PostMapping("/logout")
@Operation(summary = "λ‘œκ·Έμ•„μ›ƒ api")
public ResponseEntity<BaseResponse<Void>> logout(HttpSession session) {
userService.logout(session);
return BaseResponse.ok("λ‘œκ·Έμ•„μ›ƒμ΄ μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.", null, HttpStatus.OK);
}

@PostMapping("/verify/check-nickname")
@Operation(summary = "λ‹‰λ„€μž„ 쀑볡 체크 api")
public ResponseEntity<BaseResponse<Void>> checkNickname(
@Valid @RequestBody NicknameCheckRequestDto request
) {
Expand All @@ -66,6 +71,7 @@ public ResponseEntity<BaseResponse<Void>> checkNickname(
}

@PostMapping("/verify/email-send")
@Operation(summary = "이메일 μΈμ¦μ½”λ“œ λ°œμ†‘ api", description = "νšŒμ›κ°€μž… 쀑 이메일 인증 μ½”λ“œλ₯Ό λ°œμ†‘")
public ResponseEntity<BaseResponse<EmailSendResponseDto>> sendVerificationCode(
@Valid @RequestBody EmailSendRequestDto request
) {
Expand All @@ -74,6 +80,7 @@ public ResponseEntity<BaseResponse<EmailSendResponseDto>> sendVerificationCode(
}

@PostMapping("/verify/email-check")
@Operation(summary = "이메일 μΈμ¦μ½”λ“œ 인증 api", description = "이메일 μΈμ¦μ½”λ“œ(6자리) μž…λ ₯ μ‹œ 인증 κ°€λŠ₯ν•˜κ³  λ‚˜λ¨Έμ§€ νšŒμ›κ°€μž… μ§„ν–‰")
public ResponseEntity<BaseResponse<Void>> confirmVerificationCode(
@Valid @RequestBody EmailVerificationDto request
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ private void createReviews(List<User> instructors, List<User> students, List<Les
// 평점은 metadata의 rating μ£Όλ³€μœΌλ‘œ 생성
double baseRating = metadata.getRating();
double reviewRating = Math.max(1.0d, Math.min(5.0d,
baseRating + (random.nextFloat() - 0.5d) * 2)); // Β±1점 λ²”μœ„
baseRating + (random.nextDouble() - 0.5d) * 2)); // Β±1점 λ²”μœ„

Review review = Review.builder()
.reviewer(randomStudent)
Expand Down Expand Up @@ -228,7 +228,7 @@ private int generateReviewCount() {

private double generateRating() {
// 3.0 ~ 5.0 μ‚¬μ΄μ˜ 평점 생성 (μ†Œμˆ˜μ  1자리)
double rating = 3.0d + random.nextFloat() * 2.0d;
double rating = 3.0d + random.nextDouble() * 2.0d;
return Math.round(rating * 10) / 10.0d;
}
}