Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

compileOnly 'org.projectlombok:lombok'
// runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/example/moim/club/entity/Club.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.example.moim.global.exception.ResponseCode;
import com.example.moim.global.util.file.model.FileInfo;
import com.example.moim.match.entity.Match;
import com.example.moim.statistic.entity.Statistic;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand Down Expand Up @@ -88,6 +89,10 @@ public static Club createClub(ClubInput clubInput, FileInfo fileInfo) {
club.mainUniformColor = clubInput.getMainUniformColor();
club.subUniformColor = clubInput.getSubUniformColor();
club.memberCount = 1;

Statistic.createStatistic(club, SportsType.OVERALL);
Statistic.createStatistic(club, SportsType.FUTSAL);
Statistic.createStatistic(club, SportsType.SOCCER);
return club;
}

Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/example/moim/club/entity/UserClub.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public class UserClub extends BaseEntity {
private Integer scheduleCount;
private Integer matchCount;

private int score;

public static UserClub createLeaderUserClub(User user, Club club) {
UserClub userClub = new UserClub();
userClub.user = user;
Expand Down Expand Up @@ -59,4 +61,8 @@ public static UserClub createUserClub(User user, Club club) {
public void changeUserClub(ClubRole clubRole) {
this.clubRole = clubRole;
}

public void updateScore(int score) {
this.score += score;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import java.util.Optional;

public enum SportsType {
SOCCER("축구"), FUTSAL("풋살");
SOCCER("축구"), FUTSAL("풋살"), OVERALL("전체");
private final String koreanName;

SportsType(String koreanName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,15 @@ public enum ResponseCode {
MATCH_APPLICATION_NOT_FOUND(HttpStatus.BAD_REQUEST, "MATCH4006", "올바른 매치가 아닙니다."),
MATCH_TIME_OUT(HttpStatus.BAD_REQUEST, "MATCH4007", "매치가 종료된 후 48시간이 지났습니다."),
MATCH_NOT_CONFIRMED(HttpStatus.BAD_REQUEST, "MATCH4008", "확정된 매치가 아닙니다."),
MATCH_DUPLICATED(HttpStatus.BAD_REQUEST, "MATCH4009", "해당 시간에 다른 매치가 존재합니다."),

// MatchUser Error
MATCH_USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MATCH4005", "가입된 모임이 없습니다."),
MATCH_CANNOT_CANCEL(HttpStatus.BAD_REQUEST, "MATCH4006", "매치를 취소할 수 없습니다."),
MATCH_USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MATCHUSER4001", "가입된 모임이 없습니다."),
MATCH_CANNOT_CANCEL(HttpStatus.BAD_REQUEST, "MATCHUSER4002", "매치를 취소할 수 없습니다."),
MATCH_USER_NOT_ATTENDANCE(HttpStatus.BAD_REQUEST, "MATCHUSER4003", "매치에 참여한 클럽 소속이 아닙니다"),

// Statistic Error
STATISTIC_NOT_FOUND(HttpStatus.NOT_FOUND, "STATISTIC4001", "전적을 찾을 수 없습니다."),

// Token Error
ACCESS_TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "TOKEN4001", "헤더에 토큰 값이 없습니다"),
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/example/moim/match/entity/Match.java
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ public void setMatchScore(int homeScore, int awayScore) {
public void timeDuplicationCheck(LocalDateTime startTime, LocalDateTime endTime) {
if ((startTime.isBefore(this.startTime) && endTime.isBefore(this.startTime)) ||
(startTime.isAfter(this.endTime) && endTime.isAfter(this.endTime))) {
throw new MatchRecordExpireException("해당 시간대에 다른 매치 일정이 있습니다");
// throw new MatchRecordExpireException("해당 시간대에 다른 매치 일정이 있습니다");
throw new MatchControllerAdvice(ResponseCode.MATCH_DUPLICATED);
}
}
}
13 changes: 11 additions & 2 deletions src/main/java/com/example/moim/match/entity/MatchUser.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.example.moim.match.entity;

import com.example.moim.club.entity.Club;
import com.example.moim.global.exception.ResponseCode;
import com.example.moim.match.exception.advice.MatchControllerAdvice;
import com.example.moim.schedule.entity.ScheduleVote;
import com.example.moim.club.entity.UserClub;
import com.example.moim.match.dto.MatchRecordInput;
import com.example.moim.statistic.entity.Statistic;
import com.example.moim.user.entity.User;
import jakarta.persistence.*;
import lombok.Getter;
Expand All @@ -25,13 +28,16 @@ public class MatchUser {
private Club club;

private int score;
private String season;

public static MatchUser createMatchUser(Match match, ScheduleVote scheduleVote) {
MatchUser matchUser = new MatchUser();
matchUser.match = match;
matchUser.user = scheduleVote.getUser();
matchUser.club = findUserClubInMatch(match, scheduleVote.getUser());
matchUser.club = scheduleVote.getSchedule().getClub();
// matchUser.club = findUserClubInMatch(match, scheduleVote.getUser());
matchUser.score = 0;
matchUser.season = Statistic.getCurrentSeason();

return matchUser;
}
Expand All @@ -40,14 +46,17 @@ public void recordScore(MatchRecordInput matchRecordInput) {
this.score = matchRecordInput.getScore();
}

// 이거 뭐지
private static Club findUserClubInMatch(Match match, User user) {
for (UserClub userClub : user.getUserClub()) {
Club myClub = userClub.getClub();

if (myClub.equals(match.getHomeClub()) || myClub.equals(match.getAwayClub())) {
return myClub;
}
}

throw new RuntimeException("해당 유저는 매치에 참여한 클럽 소속이 아닙니다.");
// throw new RuntimeException("해당 유저는 매치에 참여한 클럽 소속이 아닙니다.");
throw new MatchControllerAdvice(ResponseCode.MATCH_USER_NOT_ATTENDANCE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.example.moim.match.entity.MatchUser;
import com.example.moim.match.repository.MatchRepository;
import com.example.moim.match.repository.MatchUserRepository;
import com.example.moim.statistic.service.StatisticService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
Expand All @@ -19,6 +20,7 @@
public class MatchAggregateService {
private final MatchRepository matchRepository;
private final MatchUserRepository matchUserRepository;
private final StatisticService statisticService;

//한시간마다 지금시간-48< 매치 끝나는시간 <지금시간이면 집계
@Scheduled(fixedRate = 1, timeUnit = TimeUnit.HOURS)
Expand All @@ -43,6 +45,8 @@ public void aggregateMatchScore() {
}
log.info("Match ID: {}, homeScore: {}, awayScore: {}", match.getId(), homeScore, awayScore);
match.setMatchScore(homeScore, awayScore);

statisticService.updateStatistic(match);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.moim.statistic.controller;

import com.example.moim.global.exception.BaseResponse;
import com.example.moim.global.exception.ResponseCode;
import com.example.moim.statistic.dto.StatisticDTO;
import com.example.moim.statistic.service.StatisticService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
public class StatisticController {
private StatisticService statisticService;

// 전적 조회(전적 메인)
@GetMapping("/statistic/{clubId}")
public BaseResponse<StatisticDTO.StatisticResponse> getStatistic(@PathVariable Long clubId,
@RequestBody StatisticDTO.StatisticRequest request) {
return BaseResponse.onSuccess(statisticService.getStatistic(clubId, request.getTargetSeason(), request.getTargetType()), ResponseCode.OK);
}
}
47 changes: 47 additions & 0 deletions src/main/java/com/example/moim/statistic/dto/StatisticDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.example.moim.statistic.dto;

import com.example.moim.statistic.entity.Statistic;
import lombok.AllArgsConstructor;
import lombok.Data;

public class StatisticDTO {
Copy link
Collaborator

Choose a reason for hiding this comment

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

StatisticDTO에 Request, Response 모아서 구현하신 이유가 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

DTO들이 많아지면 관리하기가 힘들어진다고 생각이 들어서 이너 클래스로 관련된 DTO들을 모아서 구현했습니다!

@Data
@AllArgsConstructor
public static class StatisticRequest {
Copy link
Collaborator

Choose a reason for hiding this comment

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

기존 코드 규칙에 따라 RequestBody, ResponseBody가 되는 DTO를 모두 XxxInput, XxxOutput으로 통일하는 게 좋을 것 같아요!

private String targetSeason;
private String targetType;
}

@Data
@AllArgsConstructor
public static class StatisticResponse {
private String season;
private String rank;
private int point;
private float winRate;
private int winCount;
private int defeatCount;
private int drawCount;
private String mvpName;
private int mvpGoalCount;

public StatisticResponse(Statistic statistic) {
this.season = statistic.getSeason();
this.point = statistic.getPoint();
this.winRate = statistic.getWinRate();
this.winCount = statistic.getWinCount();
this.defeatCount = statistic.getDefeatCount();
this.drawCount = statistic.getDrawCount();
this.mvpName = statistic.getMvpName();
this.mvpGoalCount = statistic.getMvpScore();
this.rank = statistic.getTier().toString();
}
}

@Data
@AllArgsConstructor
public static class mvpDTO {
private String name;
private Long goalCount;
}
}
156 changes: 156 additions & 0 deletions src/main/java/com/example/moim/statistic/entity/Statistic.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package com.example.moim.statistic.entity;

import com.example.moim.club.entity.Club;
import com.example.moim.club.entity.UserClub;
import com.example.moim.global.entity.BaseEntity;
import com.example.moim.global.enums.SportsType;
import jakarta.persistence.*;
import lombok.Getter;

import java.time.LocalDate;

import static com.example.moim.statistic.entity.Tier.ROOKIE;

@Entity
@Getter
public class Statistic extends BaseEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "club_id")
private Club club;

private SportsType sportsType;
private String season; // -년 전반기 / 후반기
private Tier tier;
private int point;

private float winRate; // 소수점 한 자리 수까지 표기
private int winCount;
private int drawCount;
private int defeatCount;

private int winStreak;
private int defeatStreak;

private int mvpScore;
private String mvpName;

public static String getCurrentSeason() {
LocalDate now = LocalDate.now();
int year = now.getYear();
int month = now.getMonthValue();
String half = (month <= 6) ? "상반기" : "하반기";

return year + " " + half + "전적";
}

// 모임 만들때 기본적으로 하나 생성, 반기별로 하나씩 생성
public static Statistic createStatistic(Club club, SportsType sportsType) {
Statistic statistic = new Statistic();

statistic.club = club;
statistic.sportsType = sportsType;
statistic.season = Statistic.getCurrentSeason();
statistic.tier = ROOKIE;
statistic.point = 305;
statistic.winRate = 0f;
statistic.winCount = 0;
statistic.drawCount = 0;
statistic.defeatCount = 0;
statistic.winStreak = 0;
statistic.defeatStreak = 0;
statistic.mvpScore = 0;
statistic.mvpName = null;

return statistic;
}

public void updateStatistic(int homeScore, int awayScore, int opponentRank, String mvpName, int mvpScore) {
int currentMatches = this.winCount + this.drawCount + this.defeatCount; // 배치고사 단계: 3경기 미만, 이후 경기부터는 정식 랭크 적용
int pointsChange = 0;

int ourRankLevel = this.tier.getLevel();
int diff = opponentRank - ourRankLevel;

if (homeScore > awayScore) {
this.winCount++;
this.winStreak += this.winStreak + 1;
this.defeatStreak = 0;

if (this.tier == ROOKIE) {
pointsChange += 125;
} else {
pointsChange += 128;
// 3연승 이상이면 연승 보너스: 현재 연승 수 × 10점을 추가
if (this.winStreak >= 3) {
pointsChange += this.winStreak * 10;
}
// 랭크 차이 보너스: 우리 팀의 랭크와 상대팀의 랭크 차이에 따라 추가 점수를 부여
if (diff == 2) {
pointsChange += 100;
} else if (diff >= 3) {
pointsChange += 150;
}
}
} else if (homeScore < awayScore) {
this.defeatCount++;
this.defeatStreak += this.defeatStreak + 1;
this.winStreak = 0;

if (this.tier == ROOKIE) {
pointsChange -= 75;
} else {
// 기본 패배 페널티는 -72점
// 단, 연패 완화 로직: 3연패 시에는 -30점, 4연패 이상이면 페널티가 0점
if (this.defeatStreak == 3) {
pointsChange += -30;
} else if (this.defeatStreak >= 4) {
pointsChange += 0;
} else {
pointsChange += -72;
}
}
} else {
this.drawCount++;
this.winStreak = 0;
this.defeatStreak = 0;

if (this.tier == ROOKIE) {
pointsChange += 50;
} else {
pointsChange += 47;
}
}

this.point += pointsChange;

boolean isRookie = (currentMatches < 3);
if (!isRookie) {
if (this.point >= 1600) {
this.tier = Tier.M1;
} else if (this.point >= 1300) {
this.tier = Tier.M2;
} else if (this.point >= 875) {
this.tier = Tier.M3;
} else if (this.point >= 500) {
this.tier = Tier.M4;
} else {
this.tier = Tier.M5;
}
}

this.mvpName = mvpName;
this.mvpScore = mvpScore;

// 총 경기수 계산 후 승률 업데이트 (승률 = (승리수/총경기수)*100)
int totalMatches = this.winCount + this.drawCount + this.defeatCount;
this.winRate = totalMatches > 0 ? ((float) this.winCount / totalMatches) * 100 : 0;
}

public void updateMvp(UserClub userClub) {
this.mvpName = userClub.getUser().getName();
this.mvpScore = userClub.getScore();
}
}
Loading