Skip to content

feat(game-rythm8beat): 8박자 게임 랭킹 라우트 구현#308

Merged
smpringles24 merged 1 commit into
developfrom
feature/rythm8beat
Mar 9, 2026
Merged

feat(game-rythm8beat): 8박자 게임 랭킹 라우트 구현#308
smpringles24 merged 1 commit into
developfrom
feature/rythm8beat

Conversation

@smpringles24
Copy link
Copy Markdown
Contributor

@smpringles24 smpringles24 commented Mar 9, 2026

✨ 작업 내용

8박자 게임 점수 저장을 위한 BE라우트등 구현
FE 배포가 flutter로 이루어져서 임시로 CORS설정에 개인 레포 url들어가있음 (이후 FE 마이그레이션, CORS목록 정리 필요)

Summary by CodeRabbit

릴리스 노트

  • 새 기능
    • 리듬 게임 점수 제출 기능 추가
    • 상위 3개 랭킹 및 개인 순위 조회 기능 추가
    • 전체 점수 초기화 기능 추가
    • 전화번호별 최고 점수 자동 업데이트 지원
    • 외부 클라이언트 CORS 접근 허용 확대

@smpringles24 smpringles24 requested a review from Copilot March 9, 2026 08:28
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 9, 2026

Walkthrough

리듬 게임 점수 관리를 위한 REST API 엔드포인트, 서비스 로직, JPA 엔티티, DTO, 리포지토리를 추가하고, 데이터베이스 스키마를 생성하며, CORS 원점을 확장했습니다.

Changes

코호트 / 파일(들) 요약
컨트롤러 & 메시지
src/main/java/inha/gdgoc/domain/game/controller/Rythm8beatScoreController.java, src/main/java/inha/gdgoc/domain/game/controller/message/Rythm8beatScoreMessage.java
점수 제출, 랭킹 조회, 전체 점수 초기화를 위한 3가지 POST/GET/DELETE 엔드포인트를 정의하는 새로운 컨트롤러와 관련 메시지 상수를 추가했습니다.
데이터 전송 객체
src/main/java/inha/gdgoc/domain/game/dto/request/Rythm8beatScoreRequest.java, src/main/java/inha/gdgoc/domain/game/dto/response/Rythm8beatRankItemResponse.java, src/main/java/inha/gdgoc/domain/game/dto/response/Rythm8beatRankingResponse.java
점수 요청, 랭킹 항목, 랭킹 응답 데이터를 위한 DTO를 추가했으며, 요청 검증 제약 조건을 포함합니다.
엔티티 & 리포지토리
src/main/java/inha/gdgoc/domain/game/entity/Rythm8beatScore.java, src/main/java/inha/gdgoc/domain/game/repository/Rythm8beatScoreRepository.java
점수를 저장하는 JPA 엔티티와 점수별 조회, 상위 3개 조회, 비교 계산을 지원하는 리포지토리를 추가했습니다.
서비스
src/main/java/inha/gdgoc/domain/game/service/Rythm8beatScoreService.java
점수 제출(업저트), 랭킹 조회, 전체 초기화 로직을 포함하는 비즈니스 로직 서비스를 추가했습니다.
보안 설정
src/main/java/inha/gdgoc/global/security/SecurityConfig.java
CORS 허용 원점에 새로운 외부 도메인을 추가했습니다.
데이터베이스 마이그레이션
src/main/resources/db/migration/V20260309__create_game_scores.sql
점수 정보를 저장하기 위한 game_scores 테이블과 점수 기준 인덱스를 생성했습니다.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Controller as Rythm8beatScoreController
    participant Service as Rythm8beatScoreService
    participant Repository as Rythm8beatScoreRepository
    participant DB as Database

    Client->>Controller: POST /api/v1/game/rythm8beat/scores<br/>(phoneNumber, nickname, score, stageReached)
    Controller->>Service: submitScore(request)
    Service->>Repository: findByPhoneNumber(phoneNumber)
    Repository->>DB: SELECT * FROM game_scores<br/>WHERE phone_number = ?
    DB-->>Repository: Rythm8beatScore or null
    Repository-->>Service: Optional<Rythm8beatScore>
    
    alt Score exists and newScore > currentScore
        Service->>Service: updateIfHigherScore(nickname, newScore, stageReached)
        Service->>Repository: save(entity)
        Repository->>DB: UPDATE game_scores SET ...
    else New score or lower
        Service->>Repository: save(newEntity)
        Repository->>DB: INSERT INTO game_scores ...
    end
    
    DB-->>Repository: Success
    Repository-->>Service: Rythm8beatScore
    Service-->>Controller: void
    Controller-->>Client: 200 OK<br/>ApiResponse(SCORE_SUBMITTED)
Loading
sequenceDiagram
    participant Client
    participant Controller as Rythm8beatScoreController
    participant Service as Rythm8beatScoreService
    participant Repository as Rythm8beatScoreRepository
    participant DB as Database

    Client->>Controller: GET /api/v1/game/rythm8beat/ranking<br/>?phoneNumber=010XXXXXXXX
    Controller->>Service: getRanking(phoneNumber)
    Service->>Repository: findTop3ByOrderByScoreDescUpdatedAtAsc()
    Repository->>DB: SELECT * FROM game_scores<br/>ORDER BY score DESC LIMIT 3
    DB-->>Repository: List<Rythm8beatScore>
    
    Service->>Repository: findByPhoneNumber(phoneNumber)
    Repository->>DB: SELECT * FROM game_scores<br/>WHERE phone_number = ?
    DB-->>Repository: Rythm8beatScore
    
    Service->>Repository: countByScoreGreaterThan(userScore)
    Repository->>DB: SELECT COUNT(*) FROM game_scores<br/>WHERE score > ?
    DB-->>Repository: count
    
    Repository-->>Service: results
    Service->>Service: Map to RankItemResponse + compute userRank
    Service-->>Controller: Rythm8beatRankingResponse(top3, userRank)
    Controller-->>Client: 200 OK<br/>ApiResponse(RANKING_RETRIEVED, payload)
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

Poem

🐰 새로운 점수 시스템이 점프하듯 뛰어올랐네요,
리듬에 맞춰 데이터는 춤을 추고,
랭킹은 빠르게 순위를 나열하며,
데이터베이스는 기쁨으로 가득 차 있습니다! 🎵✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 변경 사항의 주요 내용인 8박자 게임 랭킹 라우트 구현을 정확하게 설명하고 있으며, 제공된 변경 사항 요약과 완벽하게 일치합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/rythm8beat

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 9, 2026

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #309

coderabbitai Bot added a commit that referenced this pull request Mar 9, 2026
Docstrings generation was requested by @smpringles24.

* #308 (comment)

The following files were modified:

* `src/main/java/inha/gdgoc/domain/game/controller/Rythm8beatScoreController.java`
* `src/main/java/inha/gdgoc/domain/game/entity/Rythm8beatScore.java`
* `src/main/java/inha/gdgoc/domain/game/repository/Rythm8beatScoreRepository.java`
* `src/main/java/inha/gdgoc/domain/game/service/Rythm8beatScoreService.java`
* `src/main/java/inha/gdgoc/global/security/SecurityConfig.java`
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 9, 2026

CI status
✅ Assemble 성공
✅ Test 성공

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
src/main/java/inha/gdgoc/domain/game/entity/Rythm8beatScore.java (1)

41-47: 점수가 동일할 때 닉네임 업데이트 여부를 확인하세요.

현재 updateIfHigherScore 메서드는 newScore가 기존 점수보다 클 때만 업데이트합니다. 동일한 점수로 닉네임을 변경하려는 사용자의 요청은 무시됩니다.

의도된 동작이라면 괜찮지만, 사용자가 닉네임 변경을 기대할 수 있으므로 비즈니스 요구사항을 확인해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/inha/gdgoc/domain/game/entity/Rythm8beatScore.java` around
lines 41 - 47, The method updateIfHigherScore currently only updates
nickname/score/stageReached when newScore is strictly greater than the existing
score, so equal scores will not update the nickname; decide the intended
behavior and either change the comparison in updateIfHigherScore from this.score
< newScore to this.score <= newScore to allow updates when scores are equal, or
leave the strict check but document/guard it with business logic (e.g., add a
flag or a separate method updateIfGreaterOrEqualScore) and ensure callers use
the correct method; refer to updateIfHigherScore and the fields nickname, score,
stageReached when making the change.
src/main/java/inha/gdgoc/domain/game/service/Rythm8beatScoreService.java (3)

59-61: 삭제 작업에 대한 감사 로깅 권장

모든 점수 데이터를 삭제하는 관리자 작업입니다. 운영 추적을 위해 삭제 전 레코드 수와 작업 수행자를 로깅하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/inha/gdgoc/domain/game/service/Rythm8beatScoreService.java`
around lines 59 - 61, The resetAll method performs a destructive admin action;
update Rythm8beatScoreService.resetAll to log the number of records and the
acting user before deletion: obtain the record count via
rythm8beatScoreRepository.count(), determine the operator (either add a String
operator parameter to resetAll or fetch the current principal via
SecurityContextHolder.getContext().getAuthentication().getName()), call
logger.info with a clear message including the count and operator, then call
rythm8beatScoreRepository.deleteAll(); ensure any exceptions are logged so the
audit trail captures success/failure.

21-36: stageReached null 처리 로직 중복

라인 27과 33에서 stageReached != null ? request.getStageReached() : 1 로직이 동일하게 반복됩니다. 메서드 상단에서 한 번만 계산하여 중복을 제거하세요.

♻️ 리팩토링 제안
 public void submitScore(Rythm8beatScoreRequest request) {
+    int stageReached = request.getStageReached() != null ? request.getStageReached() : 1;
     rythm8beatScoreRepository.findByPhoneNumber(request.getPhoneNumber())
             .ifPresentOrElse(
                     entity -> entity.updateIfHigherScore(
                             request.getNickname(),
                             request.getScore(),
-                            request.getStageReached() != null ? request.getStageReached() : 1
+                            stageReached
                     ),
                     () -> rythm8beatScoreRepository.save(Rythm8beatScore.builder()
                             .phoneNumber(request.getPhoneNumber())
                             .nickname(request.getNickname())
                             .score(request.getScore())
-                            .stageReached(request.getStageReached() != null ? request.getStageReached() : 1)
+                            .stageReached(stageReached)
                             .build())
             );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/inha/gdgoc/domain/game/service/Rythm8beatScoreService.java`
around lines 21 - 36, The submitScore method in Rythm8beatScoreService repeats
the null-check logic for stageReached; compute a local variable (e.g., int stage
= request.getStageReached() != null ? request.getStageReached() : 1) at the top
of submitScore and use that variable in both the entity.updateIfHigherScore(...)
call and the Rythm8beatScore.builder().stageReached(...) call to remove
duplication and make the logic clearer.

48-53: 동점자 순위 계산 불일치 가능성

findTop3ByOrderByScoreDescUpdatedAtAsc는 점수 동점 시 updatedAt을 보조 정렬 기준으로 사용하지만, countByScoreGreaterThan은 점수만 고려합니다. 동점자가 있을 경우 top3에서의 순서와 userRank 값이 불일치할 수 있습니다.

예: top3에서 2위인 사용자가 userRank로 조회하면 동점자와 같은 순위로 표시될 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/inha/gdgoc/domain/game/service/Rythm8beatScoreService.java`
around lines 48 - 53, The rank calculation is inconsistent because
findTop3ByOrderByScoreDescUpdatedAtAsc uses score then updatedAt as tie-breaker
but the service uses countByScoreGreaterThan which ignores ties; update the
logic in Rythm8beatScoreService where
rythm8beatScoreRepository.findByPhoneNumber(...) is mapped to compute rank by
counting records that are "better" according to the same sort: add/use a
repository method that counts rows with score > gs.getScore() OR (score ==
gs.getScore() AND updatedAt < gs.getUpdatedAt()), call that (e.g.,
countByScoreGreaterThanOrScoreEqualsAndUpdatedAtBefore) to compute rank = count
+ 1, and return the same Rythm8beatRankItemResponse((int) rank,
gs.getNickname(), gs.getScore()) so top3 ordering and userRank remain
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/inha/gdgoc/domain/game/controller/Rythm8beatScoreController.java`:
- Around line 36-41: The getRanking endpoint in Rythm8beatScoreController
currently accepts a sensitive phoneNumber via `@RequestParam`; change it to accept
a POST with a request body DTO (e.g., GetRankingRequest with an optional
phoneNumber or better a non-PII userId/UUID) and update the mapping from
`@GetMapping`("/ranking") to `@PostMapping`("/ranking"); adjust the controller
method signature and call to rythm8beatScoreService.getRanking(...) to accept
the DTO (or UUID) and update Rythm8beatRankingResponse usage accordingly so
phoneNumber is no longer exposed in query strings or logs.

In
`@src/main/java/inha/gdgoc/domain/game/dto/request/Rythm8beatScoreRequest.java`:
- Line 29: The field stageReached in Rythm8beatScoreRequest lacks a
minimum-value validation, so negative or zero values can be accepted; add a
constraint to enforce stageReached >= 1 (e.g., annotate the Integer stageReached
field with `@Min`(1) from javax.validation.constraints) and import the annotation,
and ensure request validation is triggered in the controller (e.g., `@Valid` on
the request parameter) so invalid values are rejected before persisting.

In
`@src/main/java/inha/gdgoc/domain/game/repository/Rythm8beatScoreRepository.java`:
- Around line 14-17: Top3 ranking uses updatedAt to break ties but
countByScoreGreaterThan only checks score, causing inconsistent ranks; update
countByScoreGreaterThan to accept an additional updatedAt parameter and change
its query to "SELECT COUNT(r) FROM Rythm8beatScore r WHERE r.score > :score OR
(r.score = :score AND r.updatedAt < :updatedAt)" (use `@Param`("score") and
`@Param`("updatedAt")) so ranking uses the same tie-breaker as
findTop3ByOrderByScoreDescUpdatedAtAsc(), and adjust any callers to pass the
contestant's updatedAt (LocalDateTime/Timestamp) when computing rank.

In `@src/main/resources/db/migration/V20260309__create_game_scores.sql`:
- Around line 1-10: The migration stores PII in plain text: update the CREATE
TABLE for game_scores to avoid storing raw phone_number by either replacing
phone_number VARCHAR(20) with a non-reversible hash column (e.g., phone_hash
VARCHAR(64) NOT NULL UNIQUE) or an encrypted column and add metadata for
retention (e.g., retention_policy or deleted_at) plus auditing fields; also
rename the table from game_scores to rythm8beat_scores to match the entity
Rythm8beatScore (update any references to game_scores in your
codebase/migrations) and ensure any application code that inserts/queries uses
the chosen hashing/encryption method and logs access for audit.

---

Nitpick comments:
In `@src/main/java/inha/gdgoc/domain/game/entity/Rythm8beatScore.java`:
- Around line 41-47: The method updateIfHigherScore currently only updates
nickname/score/stageReached when newScore is strictly greater than the existing
score, so equal scores will not update the nickname; decide the intended
behavior and either change the comparison in updateIfHigherScore from this.score
< newScore to this.score <= newScore to allow updates when scores are equal, or
leave the strict check but document/guard it with business logic (e.g., add a
flag or a separate method updateIfGreaterOrEqualScore) and ensure callers use
the correct method; refer to updateIfHigherScore and the fields nickname, score,
stageReached when making the change.

In `@src/main/java/inha/gdgoc/domain/game/service/Rythm8beatScoreService.java`:
- Around line 59-61: The resetAll method performs a destructive admin action;
update Rythm8beatScoreService.resetAll to log the number of records and the
acting user before deletion: obtain the record count via
rythm8beatScoreRepository.count(), determine the operator (either add a String
operator parameter to resetAll or fetch the current principal via
SecurityContextHolder.getContext().getAuthentication().getName()), call
logger.info with a clear message including the count and operator, then call
rythm8beatScoreRepository.deleteAll(); ensure any exceptions are logged so the
audit trail captures success/failure.
- Around line 21-36: The submitScore method in Rythm8beatScoreService repeats
the null-check logic for stageReached; compute a local variable (e.g., int stage
= request.getStageReached() != null ? request.getStageReached() : 1) at the top
of submitScore and use that variable in both the entity.updateIfHigherScore(...)
call and the Rythm8beatScore.builder().stageReached(...) call to remove
duplication and make the logic clearer.
- Around line 48-53: The rank calculation is inconsistent because
findTop3ByOrderByScoreDescUpdatedAtAsc uses score then updatedAt as tie-breaker
but the service uses countByScoreGreaterThan which ignores ties; update the
logic in Rythm8beatScoreService where
rythm8beatScoreRepository.findByPhoneNumber(...) is mapped to compute rank by
counting records that are "better" according to the same sort: add/use a
repository method that counts rows with score > gs.getScore() OR (score ==
gs.getScore() AND updatedAt < gs.getUpdatedAt()), call that (e.g.,
countByScoreGreaterThanOrScoreEqualsAndUpdatedAtBefore) to compute rank = count
+ 1, and return the same Rythm8beatRankItemResponse((int) rank,
gs.getNickname(), gs.getScore()) so top3 ordering and userRank remain
consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 74be5994-00d1-4026-adcc-d77ef8c4c210

📥 Commits

Reviewing files that changed from the base of the PR and between 938474f and 7ce0365.

📒 Files selected for processing (10)
  • src/main/java/inha/gdgoc/domain/game/controller/Rythm8beatScoreController.java
  • src/main/java/inha/gdgoc/domain/game/controller/message/Rythm8beatScoreMessage.java
  • src/main/java/inha/gdgoc/domain/game/dto/request/Rythm8beatScoreRequest.java
  • src/main/java/inha/gdgoc/domain/game/dto/response/Rythm8beatRankItemResponse.java
  • src/main/java/inha/gdgoc/domain/game/dto/response/Rythm8beatRankingResponse.java
  • src/main/java/inha/gdgoc/domain/game/entity/Rythm8beatScore.java
  • src/main/java/inha/gdgoc/domain/game/repository/Rythm8beatScoreRepository.java
  • src/main/java/inha/gdgoc/domain/game/service/Rythm8beatScoreService.java
  • src/main/java/inha/gdgoc/global/security/SecurityConfig.java
  • src/main/resources/db/migration/V20260309__create_game_scores.sql

Comment on lines +36 to +41
@GetMapping("/ranking")
public ResponseEntity<ApiResponse<Rythm8beatRankingResponse, Void>> getRanking(
@RequestParam(required = false) String phoneNumber) {
Rythm8beatRankingResponse response = rythm8beatScoreService.getRanking(phoneNumber);
return ResponseEntity.ok(ApiResponse.ok(RANKING_RETRIEVED, response));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

개인정보(전화번호)가 쿼리 파라미터로 노출됨

phoneNumber는 개인식별정보(PII)입니다. 쿼리 파라미터는 서버 액세스 로그, 브라우저 히스토리, 중간 프록시 등에 기록될 수 있어 개인정보 노출 위험이 있습니다.

보안을 강화하려면 POST 요청의 body로 전달하거나, 별도의 사용자 식별자(UUID 등)를 사용하는 것을 고려하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/inha/gdgoc/domain/game/controller/Rythm8beatScoreController.java`
around lines 36 - 41, The getRanking endpoint in Rythm8beatScoreController
currently accepts a sensitive phoneNumber via `@RequestParam`; change it to accept
a POST with a request body DTO (e.g., GetRankingRequest with an optional
phoneNumber or better a non-PII userId/UUID) and update the mapping from
`@GetMapping`("/ranking") to `@PostMapping`("/ranking"); adjust the controller
method signature and call to rythm8beatScoreService.getRanking(...) to accept
the DTO (or UUID) and update Rythm8beatRankingResponse usage accordingly so
phoneNumber is no longer exposed in query strings or logs.

@Max(10000)
private Integer score;

private Integer stageReached;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

stageReached 필드에 최소값 검증이 없습니다.

stageReachednull이 아닌 경우 음수나 0이 입력될 수 있습니다. DB 스키마에서 기본값이 1이고, 서비스 레이어에서 null일 때 1로 처리하지만, 명시적으로 0이나 음수가 전달되면 그대로 저장됩니다.

제안된 수정
+    `@Min`(1)
     private Integer stageReached;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private Integer stageReached;
`@Min`(1)
private Integer stageReached;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/inha/gdgoc/domain/game/dto/request/Rythm8beatScoreRequest.java`
at line 29, The field stageReached in Rythm8beatScoreRequest lacks a
minimum-value validation, so negative or zero values can be accepted; add a
constraint to enforce stageReached >= 1 (e.g., annotate the Integer stageReached
field with `@Min`(1) from javax.validation.constraints) and import the annotation,
and ensure request validation is triggered in the controller (e.g., `@Valid` on
the request parameter) so invalid values are rejected before persisting.

Comment on lines +14 to +17
List<Rythm8beatScore> findTop3ByOrderByScoreDescUpdatedAtAsc();

@Query("SELECT COUNT(r) FROM Rythm8beatScore r WHERE r.score > :score")
long countByScoreGreaterThan(@Param("score") int score);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Top3 랭킹과 사용자 랭킹 계산 로직 간 불일치가 있습니다.

findTop3ByOrderByScoreDescUpdatedAtAsc()는 동점 시 updatedAt 오름차순으로 순위를 결정하지만, countByScoreGreaterThan()은 점수만 비교합니다.

예시: 사용자 A(100점, 먼저 제출)와 B(100점, 나중 제출)가 있을 때:

  • Top3에서는 A가 1위, B가 2위
  • 하지만 B의 countByScoreGreaterThan(100) + 1 = 1이므로 B도 1위로 표시됨

동점자 처리를 일관되게 하려면 countByScoreGreaterThan 쿼리에 updatedAt 조건을 추가하거나, 별도의 순위 계산 로직이 필요합니다.

제안된 수정
-    `@Query`("SELECT COUNT(r) FROM Rythm8beatScore r WHERE r.score > :score")
-    long countByScoreGreaterThan(`@Param`("score") int score);
+    `@Query`("SELECT COUNT(r) FROM Rythm8beatScore r WHERE r.score > :score OR (r.score = :score AND r.updatedAt < :updatedAt)")
+    long countByScoreGreaterThanOrEqualWithEarlierUpdate(`@Param`("score") int score, `@Param`("updatedAt") java.time.Instant updatedAt);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/inha/gdgoc/domain/game/repository/Rythm8beatScoreRepository.java`
around lines 14 - 17, Top3 ranking uses updatedAt to break ties but
countByScoreGreaterThan only checks score, causing inconsistent ranks; update
countByScoreGreaterThan to accept an additional updatedAt parameter and change
its query to "SELECT COUNT(r) FROM Rythm8beatScore r WHERE r.score > :score OR
(r.score = :score AND r.updatedAt < :updatedAt)" (use `@Param`("score") and
`@Param`("updatedAt")) so ranking uses the same tie-breaker as
findTop3ByOrderByScoreDescUpdatedAtAsc(), and adjust any callers to pass the
contestant's updatedAt (LocalDateTime/Timestamp) when computing rank.

Comment on lines +1 to +10
CREATE TABLE IF NOT EXISTS game_scores (
id BIGSERIAL PRIMARY KEY,
phone_number VARCHAR(20) NOT NULL UNIQUE,
nickname VARCHAR(20) NOT NULL,
score INT NOT NULL DEFAULT 0,
stage_reached INT NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_game_scores_score ON game_scores(score DESC);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

전화번호(PII) 저장에 대한 개인정보 보호 고려가 필요합니다.

phone_number 컬럼에 개인 식별 정보(PII)가 평문으로 저장됩니다. GDPR/개인정보보호법 준수를 위해 다음 사항을 검토하세요:

  1. 데이터 보존 정책 수립 필요
  2. 필요시 해시 처리 또는 암호화 고려
  3. 접근 로깅 및 감사 추적

또한 테이블명이 game_scores로 범용적인데, 엔티티명은 Rythm8beatScore입니다. 향후 다른 게임 추가 시 혼란을 피하려면 테이블명을 rythm8beat_scores로 변경하는 것을 고려해 보세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/db/migration/V20260309__create_game_scores.sql` around
lines 1 - 10, The migration stores PII in plain text: update the CREATE TABLE
for game_scores to avoid storing raw phone_number by either replacing
phone_number VARCHAR(20) with a non-reversible hash column (e.g., phone_hash
VARCHAR(64) NOT NULL UNIQUE) or an encrypted column and add metadata for
retention (e.g., retention_policy or deleted_at) plus auditing fields; also
rename the table from game_scores to rythm8beat_scores to match the entity
Rythm8beatScore (update any references to game_scores in your
codebase/migrations) and ensure any application code that inserts/queries uses
the chosen hashing/encryption method and logs access for audit.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

8박자(rythm8beat) 게임의 점수 저장 및 랭킹 조회를 위한 백엔드 라우트/도메인 구성을 추가하고, 배포된 FE 접근을 위해 CORS 허용 Origin을 확장합니다.

Changes:

  • 8박자 게임 점수 테이블(game_scores) Flyway 마이그레이션 추가
  • 점수 제출/랭킹 조회/전체 초기화 API 및 관련 Service/Repository/Entity/DTO 추가
  • CORS 허용 Origin에 GitHub Pages 도메인 추가

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/main/resources/db/migration/V20260309__create_game_scores.sql 8박자 게임 점수 저장용 테이블/인덱스 생성
src/main/java/inha/gdgoc/global/security/SecurityConfig.java CORS 허용 Origin 목록 확장
src/main/java/inha/gdgoc/domain/game/service/Rythm8beatScoreService.java 점수 upsert 및 랭킹 계산 로직 추가
src/main/java/inha/gdgoc/domain/game/repository/Rythm8beatScoreRepository.java 점수 조회/랭킹 조회용 JPA 쿼리 추가
src/main/java/inha/gdgoc/domain/game/entity/Rythm8beatScore.java 점수 엔티티 추가 (BaseEntity 상속)
src/main/java/inha/gdgoc/domain/game/dto/response/Rythm8beatRankingResponse.java 랭킹 응답 DTO 추가
src/main/java/inha/gdgoc/domain/game/dto/response/Rythm8beatRankItemResponse.java 랭킹 항목 DTO 추가
src/main/java/inha/gdgoc/domain/game/dto/request/Rythm8beatScoreRequest.java 점수 제출 요청 DTO 및 validation 추가
src/main/java/inha/gdgoc/domain/game/controller/message/Rythm8beatScoreMessage.java 8박자 점수 API 메시지 상수 추가
src/main/java/inha/gdgoc/domain/game/controller/Rythm8beatScoreController.java 점수 제출/랭킹 조회/전체 초기화 API 추가

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_game_scores_score ON game_scores(score DESC);
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

랭킹 조회가 ORDER BY score DESC, updated_at ASC 기준을 사용하므로(Repository 메서드 기준) score DESC 단일 인덱스만으로는 정렬을 충분히 지원하지 못해 테이블이 커지면 정렬 비용이 커질 수 있습니다. (score DESC, updated_at ASC) 복합 인덱스로 바꾸거나 추가하는 것을 고려해주세요.

Suggested change
CREATE INDEX IF NOT EXISTS idx_game_scores_score ON game_scores(score DESC);
CREATE INDEX IF NOT EXISTS idx_game_scores_score ON game_scores(score DESC, updated_at ASC);

Copilot uses AI. Check for mistakes.
@Min(0)
@Max(10000)
private Integer score;

Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stageReached는 null일 때 1로 보정하고 DB 기본값도 1로 두는 것으로 보아 1 이상의 값만 의미가 있는 필드로 보입니다. 그런데 현재는 값이 들어오는 경우에 대한 검증이 없어(음수/0 등) 잘못된 데이터가 저장될 수 있으니 @Min(1) 같은 validation을 추가해주세요(필요하면 상한도 함께).

Suggested change
@Min(1)

Copilot uses AI. Check for mistakes.
Comment on lines +103 to 105
"https://*.gdgocinha.com",
"https://smpringles24.github.io"
));
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CorsConfiguration#setAllowedOrigins does not support wildcard subdomains (e.g., https://*.gdgocinha.com). With the current setup, requests from subdomains may still fail CORS validation. Use setAllowedOriginPatterns for wildcard patterns (and keep setAllowedOrigins for exact matches), or list the exact origins you intend to allow.

Suggested change
"https://*.gdgocinha.com",
"https://smpringles24.github.io"
));
"https://smpringles24.github.io"
));
config.setAllowedOriginPatterns(List.of(
"https://*.gdgocinha.com"
));

Copilot uses AI. Check for mistakes.
if (phoneNumber != null && !phoneNumber.isBlank()) {
userRank = rythm8beatScoreRepository.findByPhoneNumber(phoneNumber)
.map(gs -> {
long rank = rythm8beatScoreRepository.countByScoreGreaterThan(gs.getScore()) + 1;
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userRank 계산이 top3 정렬 기준(score DESC, updatedAt ASC)과 일치하지 않습니다. 동점(같은 score)일 때는 countByScoreGreaterThan(score) + 1이 항상 1로 계산되어, top3에서 2~3등인 유저도 1등으로 표시될 수 있습니다. 동점 처리까지 포함해 동일한 정렬 기준으로 rank를 계산하도록(예: score가 더 크거나, score가 같고 updatedAt이 더 빠른 레코드 수를 카운트) 쿼리/로직을 맞춰주세요.

Suggested change
long rank = rythm8beatScoreRepository.countByScoreGreaterThan(gs.getScore()) + 1;
long rank = rythm8beatScoreRepository
.countByScoreGreaterThanOrScoreEqualsAndUpdatedAtBefore(
gs.getScore(),
gs.getUpdatedAt()
) + 1;

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +46
@DeleteMapping("/scores/all")
public ResponseEntity<ApiResponse<Void, Void>> resetAll() {
rythm8beatScoreService.resetAll();
return ResponseEntity.ok(ApiResponse.ok(SCORES_RESET));
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/scores/all은 인증/권한 체크 없이 전체 점수를 삭제할 수 있어 누구나 랭킹 데이터를 초기화할 수 있습니다(현재 SecurityConfig에서 /api/v1/game/**가 permitAll). 운영 환경에서 치명적이므로 이 엔드포인트를 제거하거나, 관리자 전용으로 보호(@PreAuthorize 등)하고 별도의 보호수단(예: 내부망/환경별 비활성화)을 추가해주세요.

Copilot uses AI. Check for mistakes.
@smpringles24 smpringles24 merged commit 5ad3016 into develop Mar 9, 2026
6 checks passed
smpringles24 added a commit that referenced this pull request Mar 9, 2026
Co-authored-by: sm_mac_mini <smpringles24@sm-mac-miniui-Macmini.local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants