Skip to content

Commit

Permalink
Merge pull request #69 from 9oormDari/develop
Browse files Browse the repository at this point in the history
fix: history ๋งˆ๊ฐ์ผ ์ด์ „/์ดํ›„ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ
  • Loading branch information
T-ferret authored Sep 28, 2024
2 parents ae6945c + 6dab6a1 commit 9a039b5
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,28 @@
import com.goormdari.domain.history.domain.dto.response.HistoryResponse;
import com.goormdari.domain.history.domain.repository.HistoryRepository;
import com.goormdari.domain.history.exception.ResourceNotFoundException;
import com.goormdari.domain.history.exception.UnValidUpdateHistories;
import com.goormdari.domain.routine.domain.Routine;
import com.goormdari.domain.routine.domain.repository.RoutineRepository;
import com.goormdari.domain.team.domain.Team;
import com.goormdari.domain.team.domain.repository.TeamRepository;
import com.goormdari.domain.user.domain.User;
import com.goormdari.domain.user.domain.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
Expand All @@ -30,17 +37,17 @@ public class HistoryService {
private final UserRepository userRepository;

// ์ตœ์ดˆ ๊ธฐ๋ก ์ƒ์„ฑ
public void createHistory(Long userId, Long teamId, boolean isSuccess) {
public void createHistory(Long userId, Long teamId) {
// ํŒ€ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
Team team = teamRepository.findById(teamId)
.orElseThrow(() -> new ResourceNotFoundException("Team " + teamId + " not found"));

// ํŒ€์˜ ์‹œ์ž‘์ผ๊ณผ ๋ฐ๋“œ๋ผ์ธ
LocalDate startDate = team.getCreatedAt().toLocalDate();
LocalDate endDate = team.getDeadLine();
LocalDate deadline = team.getDeadLine();

// ์ค‘๋ณต ํ™•์ธ: ํ•ด๋‹น ์œ ์ €์™€ ๊ธฐ๊ฐ„(์‹œ์ž‘์ผ ~ ๋ฐ๋“œ๋ผ์ธ)์— ๋Œ€ํ•œ ๊ธฐ๋ก์ด ์ด๋ฏธ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
boolean historyExists = historyRepository.existsByUserIdAndDateRange(userId, startDate, endDate);
boolean historyExists = historyRepository.existsByUserIdAndDateRange(userId, startDate, deadline);

if (historyExists) {
// ์ด๋ฏธ ๊ธฐ๋ก์ด ์กด์žฌํ•˜๋ฉด ๋” ์ด์ƒ ์ƒ์„ฑํ•˜์ง€ ์•Š์Œ
Expand All @@ -51,6 +58,33 @@ public void createHistory(Long userId, Long teamId, boolean isSuccess) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User " + userId + " not found"));

// ํ˜„์žฌ ๋‚ ์งœ ํ™•์ธ
LocalDate today = LocalDate.now();

// ์ง„ํ–‰ ์ƒํƒœ
String statusLabel;
String dDayLabel;

// ๋ฐ๋“œ๋ผ์ธ ํ™•์ธ
if (today.isBefore(deadline)) {
// deadline ์ด์ „
statusLabel = "์ง„ํ–‰ ์ค‘";
long daysDifference = ChronoUnit.DAYS.between(today, deadline);
dDayLabel = daysDifference > 0 ? "D-" + daysDifference
: daysDifference < 0 ? "D+" + Math.abs(daysDifference)
: "D-Day";
} else if (today.isEqual(deadline)) {
// ๋งˆ๊ฐ์ผ ๋‹น์ผ: "์ง„ํ–‰ ์ค‘" ๋ฐ D-Day ํ‘œ์‹œ
statusLabel = "์ง„ํ–‰ ์ค‘";
dDayLabel = "D-Day";
} else {
// ๋ฐ๋“œ๋ผ์ธ ์ง€๋‚œ ํ›„: "์„ฑ๊ณต" ๋˜๋Š” "์‹คํŒจ" ๋ฐ D-Day ํ‘œ์‹œ
boolean isSuccess = calculateGoalAchievement(userId, teamId);
statusLabel = isSuccess ? "์„ฑ๊ณต" : "์‹คํŒจ";
long daysDifference = ChronoUnit.DAYS.between(today, deadline);
dDayLabel = "D+" + Math.abs(daysDifference);
}

// ํžˆ์Šคํ† ๋ฆฌ ์ƒ์„ฑ ๋ฐ ์ €์žฅ
History history = History.builder()
.goal(team.getGoal())
Expand All @@ -59,43 +93,118 @@ public void createHistory(Long userId, Long teamId, boolean isSuccess) {
.routine3(team.getRoutine3())
.routine4(team.getRoutine4())
.user(user) // ์œ ์ € ์ •๋ณด ํ• ๋‹น
.isSuccess(isSuccess) // 70% ์ด์ƒ ์„ฑ๊ณต ์—ฌ๋ถ€ ๋ฐ˜์˜
.dDayLabel(dDayLabel)
.statusLabel(statusLabel) // 70% ์ด์ƒ ์„ฑ๊ณต ์—ฌ๋ถ€ ๋ฐ˜์˜
.build();
historyRepository.save(history);
}


// ๋งˆ๊ฐ์ผ์ด ์ง€๋‚œ ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ๊ฐฑ์‹ ํ•˜์—ฌ ์ƒํƒœ์™€ D-Day๋ฅผ ์—…๋ฐ์ดํŠธ
@Scheduled(cron = "0 0 0 * * *") // ๋งค์ผ ์ž์ • ์‹คํ–‰
public void checkTeamGoalsAndCreateHistory() {
LocalDate today = LocalDate.now();
List<Team> expiredTeams = teamRepository.findAllByDeadLineBefore(today);
public void updateHistoriesAfterDeadline() {

updateHistoriesAfterDeadlineAsync();
}

for (Team team : expiredTeams) {
List<User> teamMembers = userRepository.findAllByTeamId(team.getId());
// ๋น„๋™๊ธฐ ๋ฉ”์†Œ๋“œ: ์ง„ํ–‰ ์ค‘ ์ƒํƒœ์˜ ๋ชจ๋“  ํžˆ์Šคํ† ๋ฆฌ ๊ฐฑ์‹ 
@Async
@Transactional
public CompletableFuture<Void> updateHistoriesAfterDeadlineAsync() {
return CompletableFuture.runAsync(() -> {
try {
// ๋งˆ๊ฐ์ผ์ด ์ง€๋‚œ ํŒ€๋“ค์˜ ๋ชจ๋“  ํžˆ์Šคํ† ๋ฆฌ ์กฐํšŒ
List<History> expiredHistories = historyRepository.findAllExpiredHistories(LocalDate.now());

for (User user : teamMembers) {
// ๋ชฉํ‘œ ๋‹ฌ์„ฑ ์—ฌ๋ถ€ ๊ณ„์‚ฐ
boolean isGoalAchieved = calculateGoalAchievement(user.getId(), team.getId());
if (expiredHistories.isEmpty()) {
throw new UnValidUpdateHistories("๋งŒ๋ฃŒ๋œ ๋ชฉํ‘œ ๊ธฐ๋ก๋“ค์ด ์—†์Šต๋‹ˆ๋‹ค.");
}

// ์ค‘๋ณต ๊ธฐ๋ก ์ƒ์„ฑ ๋ฐฉ์ง€ ์ฒ˜๋ฆฌ๋œ ํžˆ์Šคํ† ๋ฆฌ ์ƒ์„ฑ
createHistory(user.getId(), team.getId(), isGoalAchieved);
int batchSize = 10; // ๋ฐฐ์น˜ ํฌ๊ธฐ ์„ค์ •

for (int i = 0; i < expiredHistories.size(); i += batchSize) {
int end = Math.min(i + batchSize, expiredHistories.size());
List<History> batch = expiredHistories.subList(i, end);
processBatchExpiredHistories(batch);
}
log.info("updateExpiredHistoriesAsync completed at {}", LocalDateTime.now());
} catch (UnValidUpdateHistories e) {
throw e;
} catch (Exception e) {
throw new UnValidUpdateHistories("๋งŒ๋ฃŒ๋œ ๋ชฉํ‘œ ๊ธฐ๋ก๋“ค์ด ๊ฐฑ์‹ ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.");
}
log.info("updateHistoriesAfterDeadlineAsync completed at {}", LocalDateTime.now());
});
}

// In Progress ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ํ™•์ธํ•˜๊ณ  ์„ฑ๊ณต ๊ธฐ์ค€ ์ถฉ์กฑ ์‹œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
@Scheduled(cron = "0 58 23 * * *") // ๋งค์ผ 23์‹œ 58๋ถ„ ์‹คํ–‰
public void scheduleUpdateInProgressHistories() {
updateInProgressHistoriesAsync();
}

// ๋น„๋™๊ธฐ ๋ฉ”์†Œ๋“œ: ์ง„ํ–‰ ์ค‘ ์ƒํƒœ์˜ ๋ชจ๋“  ํžˆ์Šคํ† ๋ฆฌ ๊ฐฑ์‹ 
@Async
@Transactional
public CompletableFuture<Void> updateInProgressHistoriesAsync() {
return CompletableFuture.runAsync(() -> {
try {
List<History> inProgressHistories = historyRepository.findAllByStatusLabel("์ง„ํ–‰ ์ค‘");

if (inProgressHistories.isEmpty()) {
throw new UnValidUpdateHistories("'์ง„ํ–‰ ์ค‘' ์ƒํƒœ์˜ ๋ชฉํ‘œ ๊ธฐ๋ก๋“ค์ด ์—†์Šต๋‹ˆ๋‹ค.");
}

int batchSize = 10; // ๋ฐฐ์น˜ ํฌ๊ธฐ ์„ค์ •

for (int i = 0; i < inProgressHistories.size(); i += batchSize) {
int end = Math.min(i + batchSize, inProgressHistories.size());
List<History> batch = inProgressHistories.subList(i, end);
processBatchInProcess(batch);
}
log.info("updateInProgressHistoriesAsync completed at {}", LocalDateTime.now());
} catch (UnValidUpdateHistories e) {
throw e;
} catch (Exception e) {
throw new UnValidUpdateHistories("'์ง„ํ–‰ ์ค‘' ์ƒํƒœ์˜ ๋ชฉํ‘œ ๊ธฐ๋ก๋“ค์ด ๊ฐฑ์‹ ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.");
}
log.info("updateInProgressHistoriesAsync completed at {}", LocalDateTime.now());
});
}

private void processBatchInProcess(List<History> batch) {
for (History history : batch) {
boolean goalAchieved = calculateGoalAchievement(history.getUser().getId(), history.getUser().getTeam().getId());
if (goalAchieved) {
history.updateStatusLabel("์„ฑ๊ณต");

// D-Day ์—…๋ฐ์ดํŠธ
Team team = history.getUser().getTeam();
LocalDate deadline = team.getDeadLine();
String dDayLabel = calculateDDay(deadline);
history.updateDDayLabel(dDayLabel);
}
}
historyRepository.saveAll(batch);
}

// D-Day ์ž์ •๋งˆ๋‹ค ์ดˆ๊ธฐํ™”
// ๋งค์ผ ์ž์ •์— ์‹คํ–‰ (cron ํ‘œํ˜„์‹: "0 0 0 * * *")
@Scheduled(cron = "0 0 0 * * *")
public void updateDDayPlus() {
// ๋ชจ๋“  ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์กฐํšŒ
List<History> histories = historyRepository.findAll();
private void processBatchExpiredHistories(List<History> batch) {
for (History history : batch) {
// ์ด๋ฏธ ์„ฑ๊ณต/์‹คํŒจ ์ƒํƒœ์ธ ๊ฒฝ์šฐ ๊ฑด๋„ˆ๋œ€
if (history.getStatusLabel().equals("์„ฑ๊ณต") || history.getStatusLabel().equals("์‹คํŒจ")) {
continue;
}

// ๋ชฉํ‘œ ๋‹ฌ์„ฑ ์—ฌ๋ถ€ ๊ณ„์‚ฐ
boolean isSuccess = calculateGoalAchievement(history.getUser().getId(), history.getUser().getTeam().getId());

for (History history : histories) {
// dDayPlus ๊ฐ’์„ 1์”ฉ ์ฆ๊ฐ€
history.incrementDDayPlus();
// ์ƒํƒœ ์—…๋ฐ์ดํŠธ
history.updateStatusLabel(isSuccess ? "์„ฑ๊ณต" : "์‹คํŒจ");

// D-Day ์—…๋ฐ์ดํŠธ
String dDayLabel = calculateDDay(history.getUser().getTeam().getDeadLine());
history.updateDDayLabel(dDayLabel);
}
// ์—…๋ฐ์ดํŠธ๋œ ํžˆ์Šคํ† ๋ฆฌ ์ €์žฅ
historyRepository.saveAll(histories);
historyRepository.saveAll(batch);
}

// 70% ์ด์ƒ ๋ฃจํ‹ด ์™„์ˆ˜ ์‹œ์— ์„ฑ๊ณต์œผ๋กœ ๊ณ„์‚ฐ
Expand All @@ -106,7 +215,16 @@ public boolean calculateGoalAchievement(Long userId, Long teamId) {

// ํŒ€์˜ ์‹œ์ž‘์ผ๊ณผ ๋ฐ๋“œ๋ผ์ธ ๊ฐ€์ ธ์˜ค๊ธฐ
LocalDate startDate = team.getCreatedAt().toLocalDate();
LocalDate endDate = team.getDeadLine();

// ๋งˆ๊ฐ์ผ ์ด์ „์ด๋ฉด ํ˜„์žฌ๊นŒ์ง€ ์™„๋ฃŒํ•œ ๋ฃจํ‹ด ์ˆ˜ ๊ธฐ์ค€
// ๋งˆ๊ฐ์ผ ์ดํ›„๋ผ๋ฉด ๋งˆ๊ฐ์ผ๊นŒ์ง€ ์™„๋ฃŒํ•œ ๋ฃจํ‹ด ์ˆ˜ ๊ธฐ์ค€
LocalDate endDate = LocalDate.now().isBefore(team.getDeadLine()) ? LocalDate.now() : team.getDeadLine();

// ๋ชฉํ‘œ ์ƒ์„ฑ์ผ๋ถ€ํ„ฐ ๋งˆ๊ฐ์ผ๊นŒ์ง€์˜ ์ด ๋ฃจํ‹ด ์ˆ˜ (ํ•˜๋ฃจ 4๊ฐœ ๊ฐ€์ •)
long totalDays = ChronoUnit.DAYS.between(startDate, team.getDeadLine()) + 1; // ์‹œ์ž‘์ผ ํฌํ•จ

// ์ „์ฒด ๋ฃจํ‹ด ๊ฐœ์ˆ˜ ๊ณ„์‚ฐ
long totalRoutines = totalDays * 4;

// ํ•ด๋‹น ๊ธฐ๊ฐ„ ๋‚ด์— ์œ ์ €๊ฐ€ ์ˆ˜ํ–‰ํ•œ ๋ฃจํ‹ด ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
List<Routine> routines = routineRepository.findRoutinesByUserAndDateRange(userId, startDate, endDate);
Expand All @@ -116,9 +234,6 @@ public boolean calculateGoalAchievement(Long userId, Long teamId) {
.filter(routine -> routine.getRoutineImg() != null && !routine.getRoutineImg().isEmpty())
.count();

// ์ „์ฒด ๋ฃจํ‹ด ๊ฐœ์ˆ˜ ๊ณ„์‚ฐ
long totalRoutines = routines.size();

// ์„ฑ๊ณต๋ฅ  ๊ณ„์‚ฐ (70% ์ด์ƒ ์„ฑ๊ณต ์‹œ ๋ชฉํ‘œ ๋‹ฌ์„ฑ)
if (totalRoutines > 0) {
double successRate = (double) successfulRoutines / totalRoutines;
Expand All @@ -127,7 +242,22 @@ public boolean calculateGoalAchievement(Long userId, Long teamId) {
return false; // ๋ฃจํ‹ด์ด ์—†๋Š” ๊ฒฝ์šฐ ์‹คํŒจ๋กœ ๊ฐ„์ฃผ
}

// ํŠน์ • ์œ ์ €์˜ ํžˆ์Šคํ† ๋ฆฌ ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ
public String calculateDDay(LocalDate deadline) {
// D-Day ๊ณ„์‚ฐ
LocalDate today = LocalDate.now();
long daysDifference = ChronoUnit.DAYS.between(today, deadline);
String dDayLabel;
if (daysDifference > 0) {
dDayLabel = "D-" + daysDifference;
} else if (daysDifference < 0) {
dDayLabel = "D+" + Math.abs(daysDifference);
} else {
dDayLabel = "D-Day";
}
return dDayLabel;
}

// ํŠน์ • ์œ ์ €์˜ ํžˆ์Šคํ† ๋ฆฌ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ
public List<HistoryResponse> getAllHistoriesByUser(Long userId) {
List<History> histories = historyRepository.findAllByUserId(userId);

Expand All @@ -139,10 +269,7 @@ public List<HistoryResponse> getAllHistoriesByUser(Long userId) {
history.getRoutine4()
};

int dDay = history.getDDayPlus();
String result = history.getIsSuccess() ? "์„ฑ๊ณต" : "์‹คํŒจ";

return new HistoryResponse(dDay, history.getGoal(), routines, result);
return new HistoryResponse(history.getDDayLabel(), history.getGoal(), routines, history.getStatusLabel());
}).collect(Collectors.toList());
}
}
21 changes: 12 additions & 9 deletions src/main/java/com/goormdari/domain/history/domain/History.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,32 +34,35 @@ public class History extends BaseEntity {
@Column(name = "routine4")
private String routine4;

@Column(name = "is_success")
private Boolean isSuccess;
private String dDayLabel;

private String statusLabel;

@CreatedDate
private LocalDateTime createAt;

private int dDayPlus;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id") // user_id๋กœ User์™€ ์—ฐ๊ฒฐ
private User user;

@Builder
public History(String goal, String routine1, String routine2, String routine3, String routine4, User user, Boolean isSuccess, int dDayPlus) {
public History(String goal, String routine1, String routine2, String routine3, String routine4, User user, String statusLabel, String dDayLabel) {
this.goal = goal;
this.routine1 = routine1;
this.routine2 = routine2;
this.routine3 = routine3;
this.routine4 = routine4;
this.user = user;
this.isSuccess = isSuccess;
this.dDayPlus = dDayPlus;
this.statusLabel = statusLabel;
this.dDayLabel = dDayLabel;
}

public void updateDDayLabel(String dDayLabel) {
this.dDayLabel = dDayLabel;
}

public void incrementDDayPlus() {
this.dDayPlus++; // dDayPlus ์ฆ๊ฐ€
public void updateStatusLabel(String statusLabel) {
this.statusLabel = statusLabel;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
@NoArgsConstructor
@AllArgsConstructor
public class HistoryResponse {
private int dDay;
private String dDay; // "D-20", "D+5", "D-Day"
private String goal;
private String[] routineList;
private String result;
private String result; // "์„ฑ๊ณต", "์‹คํŒจ", "์ง„ํ–‰ ์ค‘"
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,11 @@ boolean existsByUserIdAndDateRange(@Param("userId") Long userId,
@Param("endDate") LocalDate endDate);

List<History> findAllByUserId(Long userId);

// ์ƒํƒœ๋ณ„ ํžˆ์Šคํ† ๋ฆฌ ์กฐํšŒ (String ์‚ฌ์šฉ)
List<History> findAllByStatusLabel(String statusLabel);

// ๋งˆ๊ฐ์ผ์ด ์ง€๋‚œ ํžˆ์Šคํ† ๋ฆฌ ์กฐํšŒ
@Query("SELECT h FROM History h WHERE h.user.team.deadLine < :today")
List<History> findAllExpiredHistories(@Param("today") LocalDate today);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.goormdari.domain.history.exception;

public class UnValidUpdateHistories extends RuntimeException {

public UnValidUpdateHistories(String message) {
super(message);
}
}

0 comments on commit 9a039b5

Please sign in to comment.