diff --git a/backend/src/main/java/io/f1/backend/domain/stat/dao/StatJpaRepository.java b/backend/src/main/java/io/f1/backend/domain/stat/dao/StatJpaRepository.java index 5034bac5..2d65fee5 100644 --- a/backend/src/main/java/io/f1/backend/domain/stat/dao/StatJpaRepository.java +++ b/backend/src/main/java/io/f1/backend/domain/stat/dao/StatJpaRepository.java @@ -1,7 +1,7 @@ package io.f1.backend.domain.stat.dao; import io.f1.backend.domain.stat.dto.StatWithNickname; -import io.f1.backend.domain.stat.dto.StatWithNicknameAndUserId; +import io.f1.backend.domain.stat.dto.StatWithUserSummary; import io.f1.backend.domain.stat.entity.Stat; import org.springframework.data.domain.Page; @@ -33,12 +33,12 @@ public interface StatJpaRepository extends JpaRepository { @Query( """ SELECT - new io.f1.backend.domain.stat.dto.StatWithNicknameAndUserId + new io.f1.backend.domain.stat.dto.StatWithUserSummary (u.id, u.nickname, s.totalGames, s.winningGames, s.score) FROM Stat s JOIN s.user u """) - List findAllStatWithNicknameAndUserId(); + List findAllStatWithUserSummary(); @Modifying @Query( @@ -63,4 +63,14 @@ public interface StatJpaRepository extends JpaRepository { s.user.id = :userId """) void updateStatByUserIdCaseLose(long deltaScore, long userId); + + @Query( + """ + SELECT new io.f1.backend.domain.stat.dto.StatWithUserSummary( + u.id, u.nickname, s.totalGames, s.winningGames, s.score + ) + FROM Stat s JOIN s.user u + WHERE u.id = :userId + """) + Optional findStatWithUserSummary(long userId); } diff --git a/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRedisRepository.java b/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRedisRepository.java index 54c41ecb..827bdb94 100644 --- a/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRedisRepository.java +++ b/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRedisRepository.java @@ -4,7 +4,8 @@ import io.f1.backend.domain.stat.dto.StatPageResponse; import io.f1.backend.domain.stat.dto.StatResponse; -import io.f1.backend.domain.stat.dto.StatWithNicknameAndUserId; +import io.f1.backend.domain.stat.dto.StatWithUserSummary; +import io.f1.backend.domain.user.dto.MyPageInfo; import lombok.RequiredArgsConstructor; @@ -27,6 +28,7 @@ @Repository @RequiredArgsConstructor public class StatRedisRepository { + private static final String STAT_RANK = "stat:rank"; private static final String STAT_USER = "stat:user:%d"; private static final String STAT_NICKNAME = "stat:%s"; @@ -42,7 +44,7 @@ public void setup() { valueOps = redisTemplate.opsForValue(); } - public void initialize(StatWithNicknameAndUserId stat) { + public void initialize(StatWithUserSummary stat) { String statUserKey = getStatUserKey(stat.userId()); String statNicknameKey = getStatNickname(stat.nickname()); @@ -149,4 +151,23 @@ private static String getStatNickname(String nickname) { private long getUserIdFromNickname(String nickname) { return ((Number) requireNonNull(valueOps.get(getStatNickname(nickname)))).longValue(); } + + public MyPageInfo getStatByUserId(long userId) { + String statUserKey = getStatUserKey(userId); + + Long rank = zSetOps.reverseRank(STAT_RANK, userId); + Double score = zSetOps.score(STAT_RANK, userId); + Map statMap = hashOps.entries(statUserKey); + + if (rank == null || score == null || statMap.isEmpty()) { + throw new IllegalStateException("User not found in Redis: " + userId); + } + + return new MyPageInfo( + (String) statMap.get("nickname"), + rank + 1, + (long) statMap.get("totalGames"), + (long) statMap.get("winningGames"), + score.longValue()); + } } diff --git a/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepository.java b/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepository.java index 1326786e..d614ba07 100644 --- a/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepository.java +++ b/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepository.java @@ -1,6 +1,7 @@ package io.f1.backend.domain.stat.dao; import io.f1.backend.domain.stat.dto.StatPageResponse; +import io.f1.backend.domain.user.dto.MyPageInfo; import org.springframework.data.domain.Pageable; @@ -17,4 +18,6 @@ public interface StatRepository { void updateNickname(long userId, String nickname); void removeUser(long userId); + + MyPageInfo getMyPageByUserId(long userId); } diff --git a/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepositoryAdapter.java b/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepositoryAdapter.java index 06885eb7..d95aace5 100644 --- a/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepositoryAdapter.java +++ b/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepositoryAdapter.java @@ -4,9 +4,11 @@ import io.f1.backend.domain.stat.dto.StatPageResponse; import io.f1.backend.domain.stat.dto.StatWithNickname; -import io.f1.backend.domain.stat.dto.StatWithNicknameAndUserId; +import io.f1.backend.domain.stat.dto.StatWithUserSummary; +import io.f1.backend.domain.user.dto.MyPageInfo; import io.f1.backend.global.exception.CustomException; import io.f1.backend.global.exception.errorcode.RoomErrorCode; +import io.f1.backend.global.exception.errorcode.UserErrorCode; import jakarta.annotation.PostConstruct; @@ -54,7 +56,7 @@ public StatPageResponse getRanksByNickname(String nickname, int pageSize) { @Override public void addUser(long userId, String nickname) { - redisRepository.initialize(new StatWithNicknameAndUserId(userId, nickname, 0, 0, 0)); + redisRepository.initialize(new StatWithUserSummary(userId, nickname, 0, 0, 0)); } @Override @@ -78,7 +80,7 @@ public void removeUser(long userId) { } private void warmingRedis() { - jpaRepository.findAllStatWithNicknameAndUserId().forEach(redisRepository::initialize); + jpaRepository.findAllStatWithUserSummary().forEach(redisRepository::initialize); } private Pageable getPageableFromNickname(String nickname, int pageSize) { @@ -97,4 +99,25 @@ private Pageable getPageableFromNickname(String nickname, int pageSize) { int pageNumber = rowNum > 0 ? (int) (rowNum / pageSize) : 0; return PageRequest.of(pageNumber, pageSize, Sort.by(Direction.DESC, "score")); } + + @Override + public MyPageInfo getMyPageByUserId(long userId) { + try { + return redisRepository.getStatByUserId(userId); + } catch (Exception e) { + log.error("Redis miss, fallback to MySQL for userId={}", userId, e); + } + + StatWithUserSummary stat = findStatByUserId(userId); + long rank = jpaRepository.countByScoreGreaterThan(stat.score()) + 1; + + return new MyPageInfo( + stat.nickname(), rank, stat.totalGames(), stat.winningGames(), stat.score()); + } + + private StatWithUserSummary findStatByUserId(long userId) { + return jpaRepository + .findStatWithUserSummary(userId) + .orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND)); + } } diff --git a/backend/src/main/java/io/f1/backend/domain/stat/dto/StatWithNicknameAndUserId.java b/backend/src/main/java/io/f1/backend/domain/stat/dto/StatWithUserSummary.java similarity index 75% rename from backend/src/main/java/io/f1/backend/domain/stat/dto/StatWithNicknameAndUserId.java rename to backend/src/main/java/io/f1/backend/domain/stat/dto/StatWithUserSummary.java index 609313c1..52454e91 100644 --- a/backend/src/main/java/io/f1/backend/domain/stat/dto/StatWithNicknameAndUserId.java +++ b/backend/src/main/java/io/f1/backend/domain/stat/dto/StatWithUserSummary.java @@ -1,4 +1,4 @@ package io.f1.backend.domain.stat.dto; -public record StatWithNicknameAndUserId( +public record StatWithUserSummary( long userId, String nickname, long totalGames, long winningGames, long score) {} diff --git a/backend/src/main/java/io/f1/backend/domain/user/api/UserController.java b/backend/src/main/java/io/f1/backend/domain/user/api/UserController.java index 4a56d6e7..747137bf 100644 --- a/backend/src/main/java/io/f1/backend/domain/user/api/UserController.java +++ b/backend/src/main/java/io/f1/backend/domain/user/api/UserController.java @@ -3,6 +3,7 @@ import static io.f1.backend.global.util.SecurityUtils.logout; import io.f1.backend.domain.user.app.UserService; +import io.f1.backend.domain.user.dto.MyPageInfo; import io.f1.backend.domain.user.dto.SignupRequest; import io.f1.backend.domain.user.dto.UserPrincipal; @@ -13,6 +14,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -42,4 +44,11 @@ public ResponseEntity updateNickname( userPrincipal.getUserId(), signupRequest.nickname(), httpSession); return ResponseEntity.noContent().build(); } + + @GetMapping + public ResponseEntity getMyPage( + @AuthenticationPrincipal UserPrincipal userPrincipal) { + MyPageInfo response = userService.getMyPage(userPrincipal); + return ResponseEntity.ok(response); + } } diff --git a/backend/src/main/java/io/f1/backend/domain/user/app/UserService.java b/backend/src/main/java/io/f1/backend/domain/user/app/UserService.java index 3003d6a0..9ab5e21a 100644 --- a/backend/src/main/java/io/f1/backend/domain/user/app/UserService.java +++ b/backend/src/main/java/io/f1/backend/domain/user/app/UserService.java @@ -7,8 +7,10 @@ import static io.f1.backend.global.util.RedisPublisher.USER_UPDATE; import io.f1.backend.domain.auth.dto.CurrentUserAndAdminResponse; +import io.f1.backend.domain.stat.dao.StatRepository; import io.f1.backend.domain.user.dao.UserRepository; import io.f1.backend.domain.user.dto.AuthenticationUser; +import io.f1.backend.domain.user.dto.MyPageInfo; import io.f1.backend.domain.user.dto.SignupRequest; import io.f1.backend.domain.user.dto.UserPrincipal; import io.f1.backend.domain.user.dto.UserSummary; @@ -32,6 +34,7 @@ public class UserService { private final UserRepository userRepository; private final RedisPublisher redisPublisher; + private final StatRepository statRepository; @Transactional public CurrentUserAndAdminResponse signup(HttpSession session, SignupRequest signupRequest) { @@ -122,4 +125,10 @@ public void checkNickname(String nickname) { validateNicknameFormat(nickname); validateNicknameDuplicate(nickname); } + + @Transactional(readOnly = true) + public MyPageInfo getMyPage(UserPrincipal userPrincipal) { + Long userId = userPrincipal.getUserId(); + return statRepository.getMyPageByUserId(userId); + } } diff --git a/backend/src/main/java/io/f1/backend/domain/user/dto/MyPageInfo.java b/backend/src/main/java/io/f1/backend/domain/user/dto/MyPageInfo.java new file mode 100644 index 00000000..a66bbd1a --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/user/dto/MyPageInfo.java @@ -0,0 +1,4 @@ +package io.f1.backend.domain.user.dto; + +public record MyPageInfo( + String nickname, long rank, long score, long totalGames, long winningGames) {} diff --git a/backend/src/test/java/io/f1/backend/domain/stat/RedisStatBrowserTest.java b/backend/src/test/java/io/f1/backend/domain/stat/RedisStatBrowserTest.java index 8ced8afd..4cb06b2a 100644 --- a/backend/src/test/java/io/f1/backend/domain/stat/RedisStatBrowserTest.java +++ b/backend/src/test/java/io/f1/backend/domain/stat/RedisStatBrowserTest.java @@ -26,7 +26,7 @@ import io.f1.backend.domain.stat.dao.StatJpaRepository; import io.f1.backend.domain.stat.dao.StatRepositoryAdapter; import io.f1.backend.domain.stat.dto.StatWithNickname; -import io.f1.backend.domain.stat.dto.StatWithNicknameAndUserId; +import io.f1.backend.domain.stat.dto.StatWithUserSummary; import io.f1.backend.domain.user.dao.UserRepository; import io.f1.backend.domain.user.dto.AuthenticationUser; import io.f1.backend.domain.user.dto.SignupRequest; @@ -186,9 +186,9 @@ private MockHttpSession getMockSession(User user, boolean signup) { } private void warmingRedisOneUser(User user) { - StatWithNicknameAndUserId mockStat = - new StatWithNicknameAndUserId(user.getId(), user.getNickname(), 10, 10, 100); - given(statJpaRepository.findAllStatWithNicknameAndUserId()).willReturn(List.of(mockStat)); + StatWithUserSummary mockStat = + new StatWithUserSummary(user.getId(), user.getNickname(), 10, 10, 100); + given(statJpaRepository.findAllStatWithUserSummary()).willReturn(List.of(mockStat)); statRepositoryAdapter.setup(); } }