diff --git a/build.gradle b/build.gradle index ba5dfc5..7dfae56 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,8 @@ dependencies { //redis implementation ("org.springframework.boot:spring-boot-starter-data-redis") + implementation ("org.springframework.boot:spring-boot-starter-data-redis:2.3.1.RELEASE") + // aws implementation("org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE") diff --git a/src/main/java/com/example/domaserver/domain/rank/entity/Rank.java b/src/main/java/com/example/domaserver/domain/rank/entity/Rank.java new file mode 100644 index 0000000..96b574b --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/rank/entity/Rank.java @@ -0,0 +1,33 @@ +package com.example.domaserver.domain.rank.entity; + +import com.example.domaserver.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "rank_table") +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Rank { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long rankId; + + private int penaltyPoints; + private double rankScore; + + @ManyToOne + @JoinColumn(name = "user_id", columnDefinition = "CHAR(36)") + private User user; + + public Rank(Long rankId, User user, Double rankScore) { + this.rankId = rankId; + this.user = user; + this.rankScore = rankScore; + } +} diff --git a/src/main/java/com/example/domaserver/domain/rank/presentation/RankController.java b/src/main/java/com/example/domaserver/domain/rank/presentation/RankController.java new file mode 100644 index 0000000..460bdc2 --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/rank/presentation/RankController.java @@ -0,0 +1,58 @@ +package com.example.domaserver.domain.rank.presentation; + +import com.example.domaserver.domain.rank.entity.Rank; +import com.example.domaserver.domain.rank.presentation.dto.response.RankResponse; +import com.example.domaserver.domain.rank.service.GetRankingService; +import com.example.domaserver.domain.rank.service.GetTopRankingService; +import com.example.domaserver.domain.rank.service.UpdateRankingService; +import com.example.domaserver.domain.user.entity.User; +import com.example.domaserver.domain.user.service.UserService; +import com.example.domaserver.global.security.jwt.JwtService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/home") +public class RankController { + private final GetRankingService getRankingService; + private final GetTopRankingService getTopRankingService; + private final UpdateRankingService updateRankingService; + private final UserService userService; + private final JwtService jwtService; + + @GetMapping("/rank") + public ResponseEntity> getRanks() { + List topRanks = getTopRankingService.getTopRanking(25); + return ResponseEntity.ok(toRankResponses(topRanks)); + } + + @GetMapping("/my-rank") + public ResponseEntity getMyRank(@RequestHeader("Authorization") String token) { + User user = jwtService.getUserFromToken(token.substring(7)); + Long rank = getRankingService.getRanking(user); + return ResponseEntity.ok(rank); + } + + private List toRankResponses(List ranks) { + return ranks.stream() + .map(this::toRankResponse) + .collect(Collectors.toList()); + } + + private RankResponse toRankResponse(Rank rank) { + return RankResponse.builder() + .Id(rank.getUser().getId()) + .name(rank.getUser().getUsername()) + .penaltyPoints(rank.getPenaltyPoints()) + .RankScore(rank.getRankScore()) + .build(); + } +} diff --git a/src/main/java/com/example/domaserver/domain/rank/presentation/dto/response/RankResponse.java b/src/main/java/com/example/domaserver/domain/rank/presentation/dto/response/RankResponse.java new file mode 100644 index 0000000..6296953 --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/rank/presentation/dto/response/RankResponse.java @@ -0,0 +1,16 @@ +package com.example.domaserver.domain.rank.presentation.dto.response; + +import lombok.Builder; +import lombok.Getter; + +import java.util.UUID; + +@Builder +@Getter +public class RankResponse { + private UUID Id; + private String name; + private String profileImageUrl; + private int penaltyPoints; + private double RankScore; +} diff --git a/src/main/java/com/example/domaserver/domain/rank/service/GetRankingService.java b/src/main/java/com/example/domaserver/domain/rank/service/GetRankingService.java new file mode 100644 index 0000000..f5fa15b --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/rank/service/GetRankingService.java @@ -0,0 +1,7 @@ +package com.example.domaserver.domain.rank.service; + +import com.example.domaserver.domain.user.entity.User; + +public interface GetRankingService { + Long getRanking(User user); +} diff --git a/src/main/java/com/example/domaserver/domain/rank/service/GetTopRankingService.java b/src/main/java/com/example/domaserver/domain/rank/service/GetTopRankingService.java new file mode 100644 index 0000000..f6fdd84 --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/rank/service/GetTopRankingService.java @@ -0,0 +1,9 @@ +package com.example.domaserver.domain.rank.service; + +import com.example.domaserver.domain.rank.entity.Rank; + +import java.util.List; + +public interface GetTopRankingService { + List getTopRanking(int topN); +} diff --git a/src/main/java/com/example/domaserver/domain/rank/service/UpdateRankingService.java b/src/main/java/com/example/domaserver/domain/rank/service/UpdateRankingService.java new file mode 100644 index 0000000..0f29221 --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/rank/service/UpdateRankingService.java @@ -0,0 +1,7 @@ +package com.example.domaserver.domain.rank.service; + +import com.example.domaserver.domain.rank.entity.Rank; + +public interface UpdateRankingService { + void updateRanking(Rank rank); +} diff --git a/src/main/java/com/example/domaserver/domain/rank/service/impl/GetRankingServiceImpl.java b/src/main/java/com/example/domaserver/domain/rank/service/impl/GetRankingServiceImpl.java new file mode 100644 index 0000000..c444a81 --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/rank/service/impl/GetRankingServiceImpl.java @@ -0,0 +1,20 @@ +package com.example.domaserver.domain.rank.service.impl; + +import com.example.domaserver.domain.rank.service.GetRankingService; +import com.example.domaserver.domain.user.entity.User; +import com.example.domaserver.global.annotation.ServiceWithReadOnlyTransactional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; + +@ServiceWithReadOnlyTransactional +@RequiredArgsConstructor +public class GetRankingServiceImpl implements GetRankingService { + private static final String GET_RANKING = "getRank"; + + private final RedisTemplate redisTemplate; + + public Long getRanking(User user) { + Long rank = redisTemplate.opsForZSet().reverseRank(GET_RANKING, user.getId().toString()); + return (rank != null) ? rank + 1 : null; + } +} diff --git a/src/main/java/com/example/domaserver/domain/rank/service/impl/GetTopRankingServiceImpl.java b/src/main/java/com/example/domaserver/domain/rank/service/impl/GetTopRankingServiceImpl.java new file mode 100644 index 0000000..7af3efe --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/rank/service/impl/GetTopRankingServiceImpl.java @@ -0,0 +1,51 @@ +package com.example.domaserver.domain.rank.service.impl; + +import com.example.domaserver.domain.rank.entity.Rank; +import com.example.domaserver.domain.rank.service.GetTopRankingService; +import com.example.domaserver.domain.user.entity.User; +import com.example.domaserver.domain.user.repository.UserRepository; +import com.example.domaserver.global.annotation.ServiceWithReadOnlyTransactional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@ServiceWithReadOnlyTransactional +@RequiredArgsConstructor +public class GetTopRankingServiceImpl implements GetTopRankingService { + private static final String GET_TOP_RANKING = "getTopRanking"; + + private final RedisTemplate redisTemplate; + private final UserRepository userRepository; + + public List getTopRanking(int topN) { + Set> rankedUsers = + redisTemplate.opsForZSet().reverseRangeWithScores(GET_TOP_RANKING, 0, topN - 1); + + List userIds = rankedUsers.stream() + .map(entry -> UUID.fromString(entry.getValue())) + .collect(Collectors.toList()); + + Map userMap = userRepository.findAllById(userIds).stream() + .collect(Collectors.toMap(User::getId, user -> user)); + + List userRanks = new ArrayList<>(); + for (ZSetOperations.TypedTuple entry : rankedUsers) { + UUID userId = UUID.fromString(entry.getValue()); + Double rankScore = entry.getScore(); + User user = userMap.get(userId); + + if (user != null) { + userRanks.add(new Rank(null, user, rankScore)); + } + } + + return userRanks; + } +} diff --git a/src/main/java/com/example/domaserver/domain/rank/service/impl/UpdateRankingServiceImpl.java b/src/main/java/com/example/domaserver/domain/rank/service/impl/UpdateRankingServiceImpl.java new file mode 100644 index 0000000..19cf47a --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/rank/service/impl/UpdateRankingServiceImpl.java @@ -0,0 +1,20 @@ +package com.example.domaserver.domain.rank.service.impl; + +import com.example.domaserver.domain.rank.entity.Rank; +import com.example.domaserver.domain.rank.service.UpdateRankingService; +import com.example.domaserver.global.annotation.ServiceWithTransaction; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; + + +@ServiceWithTransaction +@RequiredArgsConstructor +public class UpdateRankingServiceImpl implements UpdateRankingService { + private static final String UPDATE_RANKING = "update ranking set rank=rank+1 where rank=?"; + + private final RedisTemplate redisTemplate; + + public void updateRanking(Rank rank) { + redisTemplate.opsForZSet().add(UPDATE_RANKING, rank.getUser().getId().toString(), rank.getPenaltyPoints()); + } +} diff --git a/src/main/java/com/example/domaserver/domain/user/entity/User.java b/src/main/java/com/example/domaserver/domain/user/entity/User.java index 8503f5b..071706d 100644 --- a/src/main/java/com/example/domaserver/domain/user/entity/User.java +++ b/src/main/java/com/example/domaserver/domain/user/entity/User.java @@ -13,17 +13,16 @@ public class User { @Id @GeneratedValue(generator = "UUID4") + @Column(length = 36) private UUID id; - private String name; - + private String username; private String email; + private String password; @Embedded private StudentNum studentNum; @Enumerated(EnumType.STRING) private Authority authority; - - } diff --git a/src/main/java/com/example/domaserver/domain/user/exception/AlreadExistsException.java b/src/main/java/com/example/domaserver/domain/user/exception/AlreadExistsException.java new file mode 100644 index 0000000..d5cca8d --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/user/exception/AlreadExistsException.java @@ -0,0 +1,7 @@ +package com.example.domaserver.domain.user.exception; + +public class AlreadExistsException extends RuntimeException { + public AlreadExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/domaserver/domain/user/exception/InvalidUserExeption.java b/src/main/java/com/example/domaserver/domain/user/exception/InvalidUserExeption.java new file mode 100644 index 0000000..3502aa9 --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/user/exception/InvalidUserExeption.java @@ -0,0 +1,7 @@ +package com.example.domaserver.domain.user.exception; + +public class InvalidUserExeption extends RuntimeException { + public InvalidUserExeption(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/domaserver/domain/user/exception/NotFoundException.java b/src/main/java/com/example/domaserver/domain/user/exception/NotFoundException.java new file mode 100644 index 0000000..a1fdcfb --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/user/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package com.example.domaserver.domain.user.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/domaserver/domain/user/exception/UserNotFoundException.java b/src/main/java/com/example/domaserver/domain/user/exception/UserNotFoundException.java new file mode 100644 index 0000000..aecd425 --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/user/exception/UserNotFoundException.java @@ -0,0 +1,10 @@ +package com.example.domaserver.domain.user.exception; + +import com.example.domaserver.global.exception.CustomException; +import com.example.domaserver.global.exception.ErrorCode; + +public class UserNotFoundException extends CustomException { + public UserNotFoundException() { + super(ErrorCode.MEMBER_NOT_FOUND); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/domaserver/domain/user/exception/UsernameNotFoundException.java b/src/main/java/com/example/domaserver/domain/user/exception/UsernameNotFoundException.java new file mode 100644 index 0000000..7a28c90 --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/user/exception/UsernameNotFoundException.java @@ -0,0 +1,10 @@ +package com.example.domaserver.domain.user.exception; + +import com.example.domaserver.global.exception.CustomException; +import com.example.domaserver.global.exception.ErrorCode; + +public class UsernameNotFoundException extends CustomException { + public UsernameNotFoundException() { + super(ErrorCode.MEMBER_NOT_FOUND_BY_USERNAME); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/domaserver/domain/user/repository/UserRepository.java b/src/main/java/com/example/domaserver/domain/user/repository/UserRepository.java index 01f917c..27b2d67 100644 --- a/src/main/java/com/example/domaserver/domain/user/repository/UserRepository.java +++ b/src/main/java/com/example/domaserver/domain/user/repository/UserRepository.java @@ -2,10 +2,13 @@ import com.example.domaserver.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import java.util.Optional; import java.util.UUID; +@Repository public interface UserRepository extends JpaRepository { Optional findByEmail(String email); + Optional findByUsername(String username); } diff --git a/src/main/java/com/example/domaserver/domain/user/service/UserService.java b/src/main/java/com/example/domaserver/domain/user/service/UserService.java new file mode 100644 index 0000000..ceaabac --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/user/service/UserService.java @@ -0,0 +1,10 @@ +package com.example.domaserver.domain.user.service; + +import com.example.domaserver.domain.user.entity.User; + +import java.util.UUID; + +public interface UserService { + User findByUsername(String name); + User findById(UUID id); +} diff --git a/src/main/java/com/example/domaserver/domain/user/service/impl/UserServiceImpl.java b/src/main/java/com/example/domaserver/domain/user/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..549fce2 --- /dev/null +++ b/src/main/java/com/example/domaserver/domain/user/service/impl/UserServiceImpl.java @@ -0,0 +1,33 @@ +package com.example.domaserver.domain.user.service.impl; + +import com.example.domaserver.domain.user.entity.User; +import com.example.domaserver.domain.user.exception.NotFoundException; +import com.example.domaserver.domain.user.exception.UserNotFoundException; +import com.example.domaserver.domain.user.exception.UsernameNotFoundException; +import com.example.domaserver.domain.user.exception.UserNotFoundException; +import com.example.domaserver.domain.user.exception.UsernameNotFoundException; +import com.example.domaserver.domain.user.repository.UserRepository; +import com.example.domaserver.domain.user.service.UserService; +import com.example.domaserver.global.annotation.ServiceWithTransaction; +import lombok.RequiredArgsConstructor; + +import java.util.UUID; + +@RequiredArgsConstructor +@ServiceWithTransaction +public class UserServiceImpl implements UserService { + + private final UserRepository userRepository; + + @Override + public User findByUsername(String name) { + return userRepository.findByUsername(name) + .orElseThrow(() -> new NotFoundException("User not found with username " + name)); + } + + @Override + public User findById(UUID id) { + return userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("User not found with id " + id)); + } +} diff --git a/src/main/java/com/example/domaserver/global/exception/ErrorCode.java b/src/main/java/com/example/domaserver/global/exception/ErrorCode.java index 5ad8568..50bb817 100644 --- a/src/main/java/com/example/domaserver/global/exception/ErrorCode.java +++ b/src/main/java/com/example/domaserver/global/exception/ErrorCode.java @@ -7,13 +7,17 @@ @Getter public enum ErrorCode { - MEMBER_NOT_FOUND(404, "유저를 찾을 수 없습니다"), + MEMBER_NOT_FOUND(404, "해당 ID의 유저를 찾을 수 없습니다."), + MEMBER_NOT_FOUND_BY_USERNAME(404, "해당 이름의 유저를 찾을 수 없습니다."), EXPIRED_TOKEN(401, "토큰이 만료되었습니다."), INVALID_TOKEN_TYPE(401, "유효하지 않은 토큰 타입입니다."), INVALID_TOKEN(401, "유효하지 않은 토큰입니다."), EXPIRED_REFRESH_TOKEN(401, "만료된 리프레쉬 토큰입니다."), + EMAIL_ALREADY_EXISTS(409, "이미 존재하는 이메일입니다."), + PASSWORD_MISMATCH(401, "비밀번호가 일치하지 않습니다."), + INTERNAL_SERVER_ERROR(500, "예기치 못한 서버 에러"); private final int httpStatus; diff --git a/src/main/java/com/example/domaserver/global/security/jwt/JwtService.java b/src/main/java/com/example/domaserver/global/security/jwt/JwtService.java new file mode 100644 index 0000000..d05e71c --- /dev/null +++ b/src/main/java/com/example/domaserver/global/security/jwt/JwtService.java @@ -0,0 +1,62 @@ +package com.example.domaserver.global.security.jwt; + + +import com.example.domaserver.domain.user.entity.User; +import com.example.domaserver.domain.user.service.UserService; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Value; + +import java.util.Date; +import java.util.function.Function; + +@Service +@Slf4j +public class JwtService { + + private final UserService userService; + + @Value("${jwt.secret}") + private String secretKey; + + public JwtService(UserService userService) { + this.userService = userService; + } + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody(); + } + + public boolean isTokenValid(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + private boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + public User getUserFromToken(String token) { + String username = extractUsername(token); + return userService.findByUsername(username); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 89f9ffc..06d7c20 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,5 +1,9 @@ server: - port: 8090 + redis: + port: 6739 + host: localhost + + port: 8080 servlet: context-path: / encoding: @@ -9,7 +13,7 @@ server: spring: datasource: - driver-class-name: com.mysql.cj.jdbc.Driver + driver-class-name: org.mariadb.jdbc.Driver url: ${DB_URL} username: ${DB_USERNAME} password: ${DB_PASSWORD} @@ -33,9 +37,10 @@ spring: port: ${REDIS_PORT} jwt: - secret: ${JWT_SECRET} + secret: "your_secret_key_here" gauth: clientId: ${GAUTH_CLIENT} clientSecret: ${GAUTH_SECRET} - redirectUri: ${GAUTH_REDIRECT_URI} \ No newline at end of file + redirectUri: ${GAUTH_REDIRECT_URI} +