Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -140,6 +140,7 @@ public void updatePlayer(FplElement element,
String picUri = "https://resources.premierleague.com/premierleague25/photos/players/110x140/"
+ element.getCode() + ".png";
this.pic = picUri;
this.code = element.getCode();
this.status = element.getStatus();
this.fplId = element.getFplId();
this.news = element.getNews();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ public class PlayerFixtureStat {
private Integer redCards;
private Integer saves;
private Integer bonus;
private Boolean inDreamteam;

@Builder.Default
@Column(name = "in_dreamteam", nullable = false)
private boolean inDreamteam = false; // 기본 false, primitive라 null 불가

private Integer totalPoints;

@ManyToOne(fetch = FetchType.LAZY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,24 @@
public interface PlayerRepository extends JpaRepository<Player, UUID> {


List<Player> findAllByCodeIn(List<Integer> codes);
List<Player> findAllByFplIdIn(List<Integer> codes);
@Query("""
SELECT p
FROM Player p
JOIN FETCH p.team t
WHERE p.code = :fplId
WHERE p.fplId = :fplId
""")
Optional<Player> findByFplId(@Param("fplId") Integer fplId);

@Modifying
@Transactional
@Query("UPDATE Player p SET p.status = 'x' WHERE p.code NOT IN :codes")
void markDeletedByCodeNotIn(@Param("codes") List<Integer> codes);
@Query("UPDATE Player p SET p.status = 'x' WHERE p.fplId NOT IN :fplIds")
void markDeletedByFplIdNotIn(@Param("fplIds") List<Integer> fplIds);
Comment on lines +36 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

빈 컬렉션 처리 없는 NOT IN 업데이트 쿼리 — 안전장치 필요

p.fplId NOT IN :fplIds에서 :fplIds가 빈 경우 방언에 따라 전행 업데이트 위험 또는 쿼리 오류가 발생합니다. 서비스 레벨에서 빈 리스트 가드를 강제하세요. (DataUpdaterService에 가드 추가 제안 드렸습니다)

레거시 코드가 남아 있을 수 있으니, 오래된 메서드 호출이 없는지 확인해 주세요:


🏁 Script executed:

#!/bin/bash
# 이전 코드 기반 메서드 호출 잔존 여부 점검
rg -n -C2 -g '!**/build/**' -g '!**/out/**' -S -tjava $'findAllByCodeIn|markDeletedByCodeNotIn'

Length of output: 96


기존 스크립트가 잘못된 메서드명을 검색해 빈 결과를 반환했습니다. 실제 메서드명(markDeletedByFplIdNotIn, findAllByFplIdIn)에 대해 호출 잔존 여부를 재확인합니다.


🏁 Script executed:

#!/bin/bash
# fplId 기반 메서드 호출 잔존 여부 점검
rg -n -C2 -g '!**/build/**' -g '!**/out/**' -S -tjava 'markDeletedByFplIdNotIn|findAllByFplIdIn'

Length of output: 9751


빈 리스트에 의한 NOT IN 업데이트 쿼리 안전장치 추가 필요

PlayerRepository.markDeletedByFplIdNotIn(List<Integer> fplIds)는 DataUpdaterService에서 단 한 곳(…/DataUpdaterService.java:113)에서만 호출되고 있습니다.
:fplIds가 빈 컬렉션일 때 방언에 따라 전체 행이 삭제되거나 쿼리 오류가 발생할 수 있으므로, 호출 직전에 빈 리스트인 경우 조기 종료 로직을 추가해주세요.

  • 파일:
    ‑ backendProject/src/main/java/likelion/mlb/backendProject/global/scheduler/service/DataUpdaterService.java
    ‑ 라인: 113 (playerRepository.markDeletedByFplIdNotIn(allFplIds);)
  • 제안 코드 스니펫:
    if (allFplIds.isEmpty()) {
        // 삭제 대상 없음
        return;
    }
    playerRepository.markDeletedByFplIdNotIn(allFplIds);
🤖 Prompt for AI Agents
In
backendProject/src/main/java/likelion/mlb/backendProject/global/scheduler/service/DataUpdaterService.java
around line 113, the call to playerRepository.markDeletedByFplIdNotIn(allFplIds)
must be guarded against an empty collection because some SQL dialects treat NOT
IN with an empty list as dangerous (affecting all rows or throwing); add an
early return when allFplIds.isEmpty() (i.e., if empty, log or comment “삭제 대상 없음”
and return) so the repository method is only invoked with a non-empty list.




@Query("SELECT p FROM Player p WHERE p.fplId IN :ids")
List<Player> findAllByFplIdIn(@Param("ids") List<Integer> ids);
// @Query("SELECT p FROM Player p WHERE p.fplId IN :ids")
// List<Player> findAllByFplIdIn(@Param("ids") List<Integer> ids);

default Map<Integer, Player> findAllByFplIdInAsMap(List<Integer> ids) {
return findAllByFplIdIn(ids).stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ public void updateFixture(FplFixture fplFixture,
/**
* 실시간으로 진행 중인 경기 정보 업데이트
*/
public void updateLiveFixture(LiveFixtureDto dto) {
this.started = dto.getStarted();
this.finished = dto.getFinished();
this.homeTeamScore = dto.getHomeScore();
this.awayTeamScore = dto.getAwayScore();
public void updateLiveFixture(FplFixture dto) {
this.started = dto.isStarted();
this.finished = dto.isFinished();
this.homeTeamScore = dto.getHomeTeamScore();
this.awayTeamScore = dto.getAwayTeamScore();
this.minutes = dto.getMinutes();
}
Comment on lines +117 to 123

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

minutes 컬럼(nullable=false)에 null 대입 가능성 — 방어 로직 필요

외부 응답에서 minutes가 null일 수 있습니다. 현재 그대로 대입하면 JPA flush 시 제약 위반 위험이 있습니다. 이전 값을 유지하거나 0으로 폴백하세요. 이전 값 유지가 안전합니다.

     public void updateLiveFixture(FplFixture dto) {
         this.started = dto.isStarted();
         this.finished = dto.isFinished();
         this.homeTeamScore = dto.getHomeTeamScore();
         this.awayTeamScore = dto.getAwayTeamScore();
-        this.minutes = dto.getMinutes();
+        this.minutes = dto.getMinutes() != null ? dto.getMinutes() : this.minutes;
     }
🤖 Prompt for AI Agents
In
backendProject/src/main/java/likelion/mlb/backendProject/domain/round/entity/Fixture.java
around lines 117 to 123, the updateLiveFixture method assigns dto.getMinutes()
directly but the minutes column is non-nullable and external responses may
return null; change the assignment to guard against null by only overwriting
this.minutes when dto.getMinutes() != null (otherwise preserve the existing
value), so the entity never gets a null minutes value (alternatively fall back
to 0 if you prefer explicit default).

}
Original file line number Diff line number Diff line change
Expand Up @@ -100,24 +100,24 @@ private void updatePlayer(List<FplElement> elements,
Map<Integer, Team> teamMap) {

// 1) DTO 에서 모든 코드 수집
List<Integer> allCodes = elements.stream()
.map(FplElement::getCode)
List<Integer> allFplIds = elements.stream()
.map(FplElement::getFplId)
.toList();

// 2) 한 번에 DB 조회: 기존 선수들만
List<Player> existingPlayers = playerRepository.findAllByCodeIn(allCodes);
List<Player> existingPlayers = playerRepository.findAllByFplIdIn(allFplIds);
Map<Integer, Player> existingMap = existingPlayers.stream()
.collect(Collectors.toMap(Player::getCode, Function.identity()));
.collect(Collectors.toMap(Player::getFplId, Function.identity()));

// 3) 삭제 처리 (DB에 남아있으나 allCodes 에 없는 선수들)
playerRepository.markDeletedByCodeNotIn(allCodes);
playerRepository.markDeletedByFplIdNotIn(allFplIds);

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

빈 목록 전달 시 대량 업데이트 위험 (NOT IN 빈 컬렉션)

markDeletedByFplIdNotIn(allFplIds) 호출 시, allFplIds가 빈 리스트면 JPA 구현체/DB 방언에 따라 전행 업데이트가 발생하거나 쿼리 오류가 날 수 있습니다. 외부 API 장애 등으로 elements가 비어올 경우를 방어해야 합니다.

서비스 레벨에서 빈 리스트 가드 추가를 권장합니다.

-        playerRepository.markDeletedByFplIdNotIn(allFplIds);
+        if (!allFplIds.isEmpty()) {
+            playerRepository.markDeletedByFplIdNotIn(allFplIds);
+        } else {
+            log.warn("elements가 비어있어 삭제 마킹을 건너뜁니다. (외부 API 점검/오류 가능성)");
+        }

또는 레포지토리 측에 별도 안전 쿼리 메서드 추가가 가능합니다. 필요 시 패치 제안 드리겠습니다.

📝 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
playerRepository.markDeletedByFplIdNotIn(allFplIds);
// 빈 목록 전달 시 전행 업데이트/오류 방어
if (!allFplIds.isEmpty()) {
playerRepository.markDeletedByFplIdNotIn(allFplIds);
} else {
log.warn("elements가 비어있어 삭제 마킹을 건너뜁니다. (외부 API 점검/오류 가능성)");
}
🤖 Prompt for AI Agents
In
backendProject/src/main/java/likelion/mlb/backendProject/global/scheduler/service/DataUpdaterService.java
around line 113, calling playerRepository.markDeletedByFplIdNotIn(allFplIds)
without checking for an empty collection can trigger a full-table update or SQL
error when allFplIds is empty; add a guard that checks if allFplIds is null or
empty and skip the repository call (optionally log a warning) or implement/use a
repository method that safely ignores empty input (e.g.,
markDeletedByFplIdNotInIfNotEmpty) so we never pass an empty collection to a NOT
IN query.


// 4) 새로 추가할 선수들 모아두기
List<Player> toInsert = new ArrayList<>();

// 5) DTO 순회하면서 in-memory upsert
for (FplElement dto : elements) {
Player existing = existingMap.get(dto.getCode());
Player existing = existingMap.get(dto.getFplId());
if (existing != null) {
// 기존 선수면 필드만 업데이트 (dirty-checking 으로 커밋 시점에 UPDATE)
existing.updatePlayer(dto, typeMap, teamMap);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import likelion.mlb.backendProject.domain.round.repository.FixtureRepository;
import likelion.mlb.backendProject.domain.round.repository.RoundRepository;
import likelion.mlb.backendProject.global.configuration.FplClient;
import likelion.mlb.backendProject.global.staticdata.dto.fixture.FplFixture;
import likelion.mlb.backendProject.global.staticdata.dto.live.LiveElementDto;
import likelion.mlb.backendProject.global.staticdata.dto.live.LiveEventDto;
import likelion.mlb.backendProject.global.staticdata.dto.live.LiveFixtureDto;
Expand All @@ -24,6 +25,7 @@

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

@Service
Expand All @@ -48,19 +50,19 @@ public void pollLiveFixtures() {

// 해당 라운드의 경기 정보를 API를 통해 받아옴
LiveEventDto liveData = fetchLiveData(currentRound);

List<FplFixture> fixtures = fpl.getFixtures(currentRound.getRound());
// 진행중인 경기가 없으면 종료
if (hasNoActiveFixtures(liveData)) {
if (hasNoActiveFixtures(fixtures)) {
log.info("진행 중인 경기가 없어 스케줄링을 종료합니다.");
return;
}

// 경기 정보 업데이트
int updatedFixtures = updateLiveFixtures(liveData.getFixtures());
int updatedFixtures = updateLiveFixtures(fixtures);
log.info("업데이트된 경기 수: {}", updatedFixtures);

// 선수 실시간 데이터 정보 업데이트
int updatedPlayers = processPlayerEvents(liveData);
int updatedPlayers = processPlayerEvents(liveData, fixtures);
log.info("업데이트된 선수 수: {}", updatedPlayers);

} catch (Exception e) {
Expand All @@ -78,33 +80,29 @@ private LiveEventDto fetchLiveData(Round currentRound) {
return fpl.getLive(currentRound.getRound());
}

private boolean hasNoActiveFixtures(LiveEventDto liveData) {
if (liveData.getFixtures() == null || liveData.getFixtures().isEmpty()) {
return true;
}

// 시작되었거나 진행중인 경기가 있는지 확인
return liveData.getFixtures().stream()
.noneMatch(fixture -> fixture.getStarted() || fixture.getMinutes() > 0);
private boolean hasNoActiveFixtures(List<FplFixture> fixtures) {
if (fixtures == null || fixtures.isEmpty()) return true;
return fixtures.stream().noneMatch(f ->
Boolean.TRUE.equals(f.isStarted()) || (f.getMinutes() != null && f.getMinutes() > 0));
}
Comment on lines +83 to 87

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

종료된 경기까지 ‘활성’으로 오판 가능 — 활성 판단 로직 불일치

hasNoActiveFixturesstarted == true만으로 활성로 간주합니다. 종료(finished == true)인 경기도 started는 true라 계속 활성로 인식됩니다. 아래와 같이 finished 배제를 포함해 getActiveFixtureIds와 기준을 일치시키세요.

-    private boolean hasNoActiveFixtures(List<FplFixture> fixtures) {
-        if (fixtures == null || fixtures.isEmpty()) return true;
-        return fixtures.stream().noneMatch(f ->
-                Boolean.TRUE.equals(f.isStarted()) || (f.getMinutes() != null && f.getMinutes() > 0));
-    }
+    private boolean hasNoActiveFixtures(List<FplFixture> fixtures) {
+        if (fixtures == null || fixtures.isEmpty()) return true;
+        return fixtures.stream().noneMatch(f ->
+            (f.isStarted() || (f.getMinutes() != null && f.getMinutes() > 0))
+            && !f.isFinished()
+        );
+    }

또는 더 단순하게 return getActiveFixtureIds(fixtures).isEmpty();로 통일해도 됩니다.

📝 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 boolean hasNoActiveFixtures(List<FplFixture> fixtures) {
if (fixtures == null || fixtures.isEmpty()) return true;
return fixtures.stream().noneMatch(f ->
Boolean.TRUE.equals(f.isStarted()) || (f.getMinutes() != null && f.getMinutes() > 0));
}
private boolean hasNoActiveFixtures(List<FplFixture> fixtures) {
if (fixtures == null || fixtures.isEmpty()) return true;
return fixtures.stream().noneMatch(f ->
(f.isStarted() || (f.getMinutes() != null && f.getMinutes() > 0))
&& !f.isFinished()
);
}
🤖 Prompt for AI Agents
In
backendProject/src/main/java/likelion/mlb/backendProject/global/scheduler/service/LiveDataService.java
around lines 83 to 87, the hasNoActiveFixtures method treats fixtures with
started == true as active even if finished == true, causing inconsistency with
getActiveFixtureIds; update the logic to exclude fixtures where finished == true
(mirror the active criteria used in getActiveFixtureIds) or replace the method
body with a call to getActiveFixtureIds(fixtures).isEmpty() to ensure both use
the same active-definition.


private int updateLiveFixtures(List<LiveFixtureDto> fixtureList) {
private int updateLiveFixtures(List<FplFixture> fixtureList) {
if (fixtureList == null || fixtureList.isEmpty()) {
return 0;
}

// N+1 문제 해결을 위한 배치 조회
List<Integer> fplIds = fixtureList.stream()
.map(LiveFixtureDto::getFixtureId)
.map(FplFixture::getFplId)
.toList();

Map<Integer, Fixture> fixtureMap = fixtureRepository.findAllByFplIdInAsMap(fplIds);
int updatedCount = 0;

for (LiveFixtureDto dto : fixtureList) {
Fixture fixture = fixtureMap.get(dto.getFixtureId());
for (FplFixture dto : fixtureList) {
Fixture fixture = fixtureMap.get(dto.getFplId());
if (fixture == null) {
log.warn("매핑된 Fixture가 없습니다. fplId={}", dto.getFixtureId());
log.warn("매핑된 Fixture가 없습니다. fplId={}", dto.getFplId());
continue;
}

Expand All @@ -114,29 +112,44 @@ private int updateLiveFixtures(List<LiveFixtureDto> fixtureList) {
updatedCount++;
log.debug("Fixture 업데이트 완료 {}: started={}, finished={}, minutes={}, homeScore={}, awayScore={}",
fixture.getFplId(), fixture.isStarted(), fixture.isFinished(),
fixture.getMinutes(), dto.getHomeScore(), dto.getAwayScore());
fixture.getMinutes(), dto.getHomeTeamScore(), dto.getAwayTeamScore());
}
}

return updatedCount;
}

private boolean hasFixtureChanged(Fixture fixture, LiveFixtureDto dto) {
return fixture.isStarted() != dto.getStarted() ||
fixture.isFinished() != dto.getFinished() ||
fixture.getMinutes() != dto.getMinutes() ||
!fixture.getHomeTeamScore().equals(dto.getHomeScore()) ||
!fixture.getAwayTeamScore().equals(dto.getAwayScore());
}
// private boolean hasFixtureChanged(Fixture fixture, FplFixture dto) {
// return fixture.isStarted() != dto.isStarted() ||
// fixture.isFinished() != dto.isFinished() ||
// fixture.getMinutes() != dto.getMinutes() ||
// !fixture.getHomeTeamScore().equals(dto.getHomeTeamScore()) ||
// !fixture.getAwayTeamScore().equals(dto.getAwayTeamScore());
// }
private boolean hasFixtureChanged(Fixture fixture, FplFixture dto) {
// minutes가 Integer라면 언박싱 전에 기본값 처리
int fMin = fixture.getMinutes() == null ? 0 : fixture.getMinutes();
int dMin = dto.getMinutes() == null ? 0 : dto.getMinutes();

// started/finished가 Boolean일 수도 있으니 null-safe 비교
boolean startedChanged = !Objects.equals(fixture.isStarted(), dto.isStarted());
boolean finishedChanged = !Objects.equals(fixture.isFinished(), dto.isFinished());

boolean homeScoreChanged = !Objects.equals(fixture.getHomeTeamScore(), dto.getHomeTeamScore());
boolean awayScoreChanged = !Objects.equals(fixture.getAwayTeamScore(), dto.getAwayTeamScore());
boolean minutesChanged = (fMin != dMin);

return startedChanged || finishedChanged || homeScoreChanged || awayScoreChanged || minutesChanged;
}

private int processPlayerEvents(LiveEventDto liveData) {
private int processPlayerEvents(LiveEventDto liveData, List<FplFixture> fixtures) {
if (liveData.getElements() == null || liveData.getElements().isEmpty()) {
log.info("처리할 선수 데이터가 없습니다.");
return 0;
}

// 진행중인 경기 필터링
List<Integer> activeFixtureIds = getActiveFixtureIds(liveData);
List<Integer> activeFixtureIds = getActiveFixtureIds(fixtures);
if (activeFixtureIds.isEmpty()) {
log.info("진행중인 경기가 없습니다.");
return 0;
Expand Down Expand Up @@ -192,10 +205,10 @@ private int processPlayerEvents(LiveEventDto liveData) {
return updatedCount;
}

private List<Integer> getActiveFixtureIds(LiveEventDto liveData) {
return liveData.getFixtures().stream()
.filter(fixture -> fixture.getStarted() && !fixture.getFinished())
.map(LiveFixtureDto::getFixtureId)
private List<Integer> getActiveFixtureIds(List<FplFixture> liveData) {
return liveData.stream()
.filter(fixture -> fixture.isStarted() && !fixture.isFinished())
.map(FplFixture::getFplId)
.toList();
}

Expand Down Expand Up @@ -310,7 +323,7 @@ private void createMatchEvent(PlayerFixtureStat stat, String eventType, int minu

MatchEvent saved = matchEventRepository.save(matchEvent);

notificationService.sendMatchAlert(saved);
//notificationService.sendMatchAlert(saved);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
@Getter
public class LiveElementDto {
// FPL player ID
@JsonProperty("element")
@JsonProperty("id")
private Integer playerId;

// 실시간 집계 스탯
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package likelion.mlb.backendProject.global.staticdata.dto.live;

import likelion.mlb.backendProject.global.staticdata.dto.fixture.FplFixture;
import lombok.Getter;

import java.util.List;
Expand All @@ -11,5 +12,5 @@ public class LiveEventDto {
private List<LiveElementDto> elements;

// 경기별 진행 상태
private List<LiveFixtureDto> fixtures;
private List<FplFixture> fixtures;
}