Skip to content

Commit 0f90502

Browse files
smpringles24sm_mac_mini
andauthored
feat(game-rythm8beat): 8박자 게임 랭킹 라우트 구현 (#308) (#310)
Co-authored-by: sm_mac_mini <smpringles24@sm-mac-miniui-Macmini.local>
1 parent 9695d1b commit 0f90502

10 files changed

Lines changed: 249 additions & 1 deletion

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package inha.gdgoc.domain.game.controller;
2+
3+
import static inha.gdgoc.domain.game.controller.message.Rythm8beatScoreMessage.RANKING_RETRIEVED;
4+
import static inha.gdgoc.domain.game.controller.message.Rythm8beatScoreMessage.SCORES_RESET;
5+
import static inha.gdgoc.domain.game.controller.message.Rythm8beatScoreMessage.SCORE_SUBMITTED;
6+
7+
import inha.gdgoc.domain.game.dto.request.Rythm8beatScoreRequest;
8+
import inha.gdgoc.domain.game.dto.response.Rythm8beatRankingResponse;
9+
import inha.gdgoc.domain.game.service.Rythm8beatScoreService;
10+
import inha.gdgoc.global.dto.response.ApiResponse;
11+
import jakarta.validation.Valid;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.http.ResponseEntity;
14+
import org.springframework.web.bind.annotation.DeleteMapping;
15+
import org.springframework.web.bind.annotation.GetMapping;
16+
import org.springframework.web.bind.annotation.PostMapping;
17+
import org.springframework.web.bind.annotation.RequestBody;
18+
import org.springframework.web.bind.annotation.RequestMapping;
19+
import org.springframework.web.bind.annotation.RequestParam;
20+
import org.springframework.web.bind.annotation.RestController;
21+
22+
@RequestMapping("/api/v1/game/rythm8beat")
23+
@RestController
24+
@RequiredArgsConstructor
25+
public class Rythm8beatScoreController {
26+
27+
private final Rythm8beatScoreService rythm8beatScoreService;
28+
29+
@PostMapping("/scores")
30+
public ResponseEntity<ApiResponse<Void, Void>> submitScore(
31+
@Valid @RequestBody Rythm8beatScoreRequest request) {
32+
rythm8beatScoreService.submitScore(request);
33+
return ResponseEntity.ok(ApiResponse.ok(SCORE_SUBMITTED));
34+
}
35+
36+
@GetMapping("/ranking")
37+
public ResponseEntity<ApiResponse<Rythm8beatRankingResponse, Void>> getRanking(
38+
@RequestParam(required = false) String phoneNumber) {
39+
Rythm8beatRankingResponse response = rythm8beatScoreService.getRanking(phoneNumber);
40+
return ResponseEntity.ok(ApiResponse.ok(RANKING_RETRIEVED, response));
41+
}
42+
43+
@DeleteMapping("/scores/all")
44+
public ResponseEntity<ApiResponse<Void, Void>> resetAll() {
45+
rythm8beatScoreService.resetAll();
46+
return ResponseEntity.ok(ApiResponse.ok(SCORES_RESET));
47+
}
48+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package inha.gdgoc.domain.game.controller.message;
2+
3+
public class Rythm8beatScoreMessage {
4+
public static final String SCORE_SUBMITTED = "점수가 등록되었습니다.";
5+
public static final String RANKING_RETRIEVED = "랭킹을 조회했습니다.";
6+
public static final String SCORES_RESET = "모든 점수가 초기화되었습니다.";
7+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package inha.gdgoc.domain.game.dto.request;
2+
3+
import jakarta.validation.constraints.Max;
4+
import jakarta.validation.constraints.Min;
5+
import jakarta.validation.constraints.NotBlank;
6+
import jakarta.validation.constraints.NotNull;
7+
import jakarta.validation.constraints.Pattern;
8+
import jakarta.validation.constraints.Size;
9+
import lombok.Getter;
10+
import lombok.NoArgsConstructor;
11+
12+
@Getter
13+
@NoArgsConstructor
14+
public class Rythm8beatScoreRequest {
15+
16+
@NotBlank
17+
@Pattern(regexp = "^01[0-9]{8,9}$", message = "올바른 휴대폰 번호 형식이 아닙니다.")
18+
private String phoneNumber;
19+
20+
@NotBlank
21+
@Size(max = 20)
22+
private String nickname;
23+
24+
@NotNull
25+
@Min(0)
26+
@Max(10000)
27+
private Integer score;
28+
29+
private Integer stageReached;
30+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package inha.gdgoc.domain.game.dto.response;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@AllArgsConstructor
8+
public class Rythm8beatRankItemResponse {
9+
private int rank;
10+
private String nickname;
11+
private int score;
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package inha.gdgoc.domain.game.dto.response;
2+
3+
import java.util.List;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@AllArgsConstructor
9+
public class Rythm8beatRankingResponse {
10+
private List<Rythm8beatRankItemResponse> top3;
11+
private Rythm8beatRankItemResponse userRank;
12+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package inha.gdgoc.domain.game.entity;
2+
3+
import inha.gdgoc.global.entity.BaseEntity;
4+
import jakarta.persistence.Column;
5+
import jakarta.persistence.Entity;
6+
import jakarta.persistence.GeneratedValue;
7+
import jakarta.persistence.GenerationType;
8+
import jakarta.persistence.Id;
9+
import jakarta.persistence.Table;
10+
import lombok.AccessLevel;
11+
import lombok.AllArgsConstructor;
12+
import lombok.Builder;
13+
import lombok.Getter;
14+
import lombok.NoArgsConstructor;
15+
16+
@Entity
17+
@Table(name = "game_scores")
18+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
19+
@AllArgsConstructor
20+
@Getter
21+
@Builder
22+
public class Rythm8beatScore extends BaseEntity {
23+
24+
@Id
25+
@GeneratedValue(strategy = GenerationType.IDENTITY)
26+
@Column(name = "id")
27+
private Long id;
28+
29+
@Column(name = "phone_number", nullable = false, unique = true, length = 20)
30+
private String phoneNumber;
31+
32+
@Column(name = "nickname", nullable = false, length = 20)
33+
private String nickname;
34+
35+
@Column(name = "score", nullable = false)
36+
private int score;
37+
38+
@Column(name = "stage_reached", nullable = false)
39+
private int stageReached;
40+
41+
public void updateIfHigherScore(String nickname, int newScore, int newStageReached) {
42+
if (this.score < newScore) {
43+
this.nickname = nickname;
44+
this.score = newScore;
45+
this.stageReached = newStageReached;
46+
}
47+
}
48+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package inha.gdgoc.domain.game.repository;
2+
3+
import inha.gdgoc.domain.game.entity.Rythm8beatScore;
4+
import java.util.List;
5+
import java.util.Optional;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
9+
10+
public interface Rythm8beatScoreRepository extends JpaRepository<Rythm8beatScore, Long> {
11+
12+
Optional<Rythm8beatScore> findByPhoneNumber(String phoneNumber);
13+
14+
List<Rythm8beatScore> findTop3ByOrderByScoreDescUpdatedAtAsc();
15+
16+
@Query("SELECT COUNT(r) FROM Rythm8beatScore r WHERE r.score > :score")
17+
long countByScoreGreaterThan(@Param("score") int score);
18+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package inha.gdgoc.domain.game.service;
2+
3+
import inha.gdgoc.domain.game.dto.request.Rythm8beatScoreRequest;
4+
import inha.gdgoc.domain.game.dto.response.Rythm8beatRankItemResponse;
5+
import inha.gdgoc.domain.game.dto.response.Rythm8beatRankingResponse;
6+
import inha.gdgoc.domain.game.entity.Rythm8beatScore;
7+
import inha.gdgoc.domain.game.repository.Rythm8beatScoreRepository;
8+
import java.util.List;
9+
import java.util.stream.IntStream;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.stereotype.Service;
12+
import org.springframework.transaction.annotation.Transactional;
13+
14+
@Service
15+
@RequiredArgsConstructor
16+
@Transactional
17+
public class Rythm8beatScoreService {
18+
19+
private final Rythm8beatScoreRepository rythm8beatScoreRepository;
20+
21+
public void submitScore(Rythm8beatScoreRequest request) {
22+
rythm8beatScoreRepository.findByPhoneNumber(request.getPhoneNumber())
23+
.ifPresentOrElse(
24+
entity -> entity.updateIfHigherScore(
25+
request.getNickname(),
26+
request.getScore(),
27+
request.getStageReached() != null ? request.getStageReached() : 1
28+
),
29+
() -> rythm8beatScoreRepository.save(Rythm8beatScore.builder()
30+
.phoneNumber(request.getPhoneNumber())
31+
.nickname(request.getNickname())
32+
.score(request.getScore())
33+
.stageReached(request.getStageReached() != null ? request.getStageReached() : 1)
34+
.build())
35+
);
36+
}
37+
38+
@Transactional(readOnly = true)
39+
public Rythm8beatRankingResponse getRanking(String phoneNumber) {
40+
List<Rythm8beatScore> top3 = rythm8beatScoreRepository.findTop3ByOrderByScoreDescUpdatedAtAsc();
41+
42+
List<Rythm8beatRankItemResponse> top3Response = IntStream.range(0, top3.size())
43+
.mapToObj(i -> new Rythm8beatRankItemResponse(i + 1, top3.get(i).getNickname(), top3.get(i).getScore()))
44+
.toList();
45+
46+
Rythm8beatRankItemResponse userRank = null;
47+
if (phoneNumber != null && !phoneNumber.isBlank()) {
48+
userRank = rythm8beatScoreRepository.findByPhoneNumber(phoneNumber)
49+
.map(gs -> {
50+
long rank = rythm8beatScoreRepository.countByScoreGreaterThan(gs.getScore()) + 1;
51+
return new Rythm8beatRankItemResponse((int) rank, gs.getNickname(), gs.getScore());
52+
})
53+
.orElse(null);
54+
}
55+
56+
return new Rythm8beatRankingResponse(top3Response, userRank);
57+
}
58+
59+
public void resetAll() {
60+
rythm8beatScoreRepository.deleteAll();
61+
}
62+
}

src/main/java/inha/gdgoc/global/security/SecurityConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ public CorsConfigurationSource corsConfigurationSource() {
100100
"https://www.gdgocinha.com",
101101
"https://typing-game-alpha-umber.vercel.app",
102102
"https://api.gdgocinha.com",
103-
"https://*.gdgocinha.com"
103+
"https://*.gdgocinha.com",
104+
"https://smpringles24.github.io"
104105
));
105106
config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS","PATCH"));
106107
config.setAllowedHeaders(List.of("Origin","X-Requested-With","Content-Type","Accept","Authorization"));
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CREATE TABLE IF NOT EXISTS game_scores (
2+
id BIGSERIAL PRIMARY KEY,
3+
phone_number VARCHAR(20) NOT NULL UNIQUE,
4+
nickname VARCHAR(20) NOT NULL,
5+
score INT NOT NULL DEFAULT 0,
6+
stage_reached INT NOT NULL DEFAULT 1,
7+
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
8+
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
9+
);
10+
CREATE INDEX IF NOT EXISTS idx_game_scores_score ON game_scores(score DESC);

0 commit comments

Comments
 (0)