-
Notifications
You must be signed in to change notification settings - Fork 3
fix: 실시간 데이터 반영 완료 #68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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); | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 빈 목록 전달 시 대량 업데이트 위험 (NOT IN 빈 컬렉션)
서비스 레벨에서 빈 리스트 가드 추가를 권장합니다. - playerRepository.markDeletedByFplIdNotIn(allFplIds);
+ if (!allFplIds.isEmpty()) {
+ playerRepository.markDeletedByFplIdNotIn(allFplIds);
+ } else {
+ log.warn("elements가 비어있어 삭제 마킹을 건너뜁니다. (외부 API 점검/오류 가능성)");
+ }또는 레포지토리 측에 별도 안전 쿼리 메서드 추가가 가능합니다. 필요 시 패치 제안 드리겠습니다. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| // 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); | ||||||||||||||||
|
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||||||||||||||||
|
|
@@ -24,6 +25,7 @@ | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||
| import java.util.Map; | ||||||||||||||||||||||||||
| import java.util.Objects; | ||||||||||||||||||||||||||
| import java.util.stream.Collectors; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @Service | ||||||||||||||||||||||||||
|
|
@@ -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) { | ||||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 종료된 경기까지 ‘활성’으로 오판 가능 — 활성 판단 로직 불일치
- 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()
+ );
+ }또는 더 단순하게 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -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; | ||||||||||||||||||||||||||
|
|
@@ -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(); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -310,7 +323,7 @@ private void createMatchEvent(PlayerFixtureStat stat, String eventType, int minu | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| MatchEvent saved = matchEventRepository.save(matchEvent); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| notificationService.sendMatchAlert(saved); | ||||||||||||||||||||||||||
| //notificationService.sendMatchAlert(saved); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
There was a problem hiding this comment.
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:
Length of output: 96
기존 스크립트가 잘못된 메서드명을 검색해 빈 결과를 반환했습니다. 실제 메서드명(
markDeletedByFplIdNotIn,findAllByFplIdIn)에 대해 호출 잔존 여부를 재확인합니다.🏁 Script executed:
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);)🤖 Prompt for AI Agents