Skip to content

Commit 1844783

Browse files
authoredFeb 24, 2025
Merge pull request #55 from CommitField/feat/#43
Feat/#43
2 parents 8ddb9d3 + 93f3763 commit 1844783

13 files changed

+417
-0
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package cmf.commitField.domain.season.controller;
2+
3+
import cmf.commitField.domain.season.entity.Season;
4+
import cmf.commitField.domain.season.service.SeasonService;
5+
import cmf.commitField.global.scheduler.SeasonScheduler;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.web.bind.annotation.*;
8+
9+
import javax.swing.*;
10+
import java.time.LocalDateTime;
11+
12+
@RestController
13+
@RequestMapping("/api/seasons")
14+
@RequiredArgsConstructor
15+
public class ApiV1SeasonController {
16+
private final SeasonService seasonService;
17+
private final SeasonScheduler seasonScheduler;
18+
19+
// ์˜ˆ์‹œ๋กœ Season ๋ฐ์ดํ„ฐ ์ž…๋ ฅ ํ•ด๋ณด๊ธฐ
20+
@PostMapping
21+
public Season createSeason() {
22+
String name = "2025 1๋ถ„๊ธฐ";
23+
LocalDateTime start = LocalDateTime.of(2025, 1, 1, 0, 0, 0);
24+
LocalDateTime end = LocalDateTime.of(2025, 2, 28, 23, 59, 59);
25+
26+
return seasonService.createNewSeason(name, start, end);
27+
}
28+
29+
@GetMapping("/active")
30+
public Season getActiveSeason() {
31+
return seasonService.getActiveSeason();
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package cmf.commitField.domain.season.controller;
2+
3+
import cmf.commitField.domain.season.dto.UserSeasonDto;
4+
import cmf.commitField.domain.season.entity.UserSeason;
5+
import cmf.commitField.domain.season.service.UserSeasonService;
6+
import cmf.commitField.global.globalDto.GlobalResponse;
7+
import cmf.commitField.global.globalDto.GlobalResponseCode;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.PathVariable;
12+
import org.springframework.web.bind.annotation.RequestMapping;
13+
import org.springframework.web.bind.annotation.RestController;
14+
15+
import java.util.List;
16+
import java.util.stream.Collectors;
17+
18+
@RestController
19+
@RequestMapping("/user-season")
20+
@RequiredArgsConstructor
21+
public class ApiV1UserSeasonController {
22+
private final UserSeasonService userSeasonService;
23+
24+
// ํ˜„์žฌ ์‹œ์ฆŒ์— ์œ ์ € ๋žญํฌ ์ถ”๊ฐ€ํ•˜๊ธฐ (SEED ๋“ฑ๊ธ‰)
25+
@GetMapping("/{userId}")
26+
public GlobalResponse<UserSeasonDto> addUserRank(@PathVariable Long userId) {
27+
UserSeason userSeason = userSeasonService.addUserRank(userId);
28+
UserSeasonDto userSeasonDto = new UserSeasonDto(userSeason);
29+
return GlobalResponse.success(userSeasonDto);
30+
}
31+
32+
// ์œ ์ €์˜ ๋ชจ๋“  ์‹œ์ฆŒ ๋žญํฌ ์กฐํšŒ
33+
@GetMapping("/{userId}/ranks")
34+
public GlobalResponse<List<UserSeasonDto>> getUserRanks(@PathVariable Long userId) {
35+
List<UserSeason> userSeasons = userSeasonService.getUserRanks(userId);
36+
37+
// UserSeason -> UserSeasonDto ๋ณ€ํ™˜
38+
List<UserSeasonDto> userSeasonDtos = userSeasons.stream()
39+
.map(UserSeasonDto::new)
40+
.collect(Collectors.toList());
41+
42+
return GlobalResponse.success(userSeasonDtos);
43+
}
44+
45+
// ํŠน์ • ์‹œ์ฆŒ์˜ ์œ ์ € ๋žญํฌ ์กฐํšŒ
46+
@GetMapping("/{userId}/season/{seasonId}")
47+
public GlobalResponse<UserSeasonDto> getUserRankBySeason(
48+
@PathVariable Long userId,
49+
@PathVariable Long seasonId
50+
) {
51+
UserSeason userSeason = userSeasonService.getUserRankBySeason(userId, seasonId);
52+
UserSeasonDto userSeasonDto = new UserSeasonDto(userSeason);
53+
return GlobalResponse.success(userSeasonDto);
54+
}
55+
}
56+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package cmf.commitField.domain.season.dto;
2+
3+
import cmf.commitField.domain.season.entity.Rank;
4+
import cmf.commitField.domain.season.entity.UserSeason;
5+
import lombok.Getter;
6+
7+
@Getter
8+
public class UserSeasonDto {
9+
private Long userId;
10+
private String username;
11+
private String seasonName;
12+
private Rank rank;
13+
14+
public UserSeasonDto(UserSeason userSeason) {
15+
this.userId = userSeason.getUser().getId();
16+
this.username = userSeason.getUser().getUsername();
17+
this.seasonName = userSeason.getSeason().getName();
18+
this.rank = userSeason.getRank();
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package cmf.commitField.domain.season.entity;
2+
3+
public enum Rank {
4+
SEED, // ์”จ์•—
5+
SPROUT, // ์ƒˆ์‹น
6+
STEM, // ์ค„๊ธฐ
7+
FLOWER, // ๊ฝƒ
8+
FRUIT, // ์—ด๋งค
9+
TREE // ๋‚˜๋ฌด
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package cmf.commitField.domain.season.entity;
2+
3+
import cmf.commitField.global.jpa.BaseEntity;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.EnumType;
6+
import jakarta.persistence.Enumerated;
7+
import jakarta.persistence.Table;
8+
import lombok.*;
9+
import lombok.experimental.SuperBuilder;
10+
11+
import java.time.LocalDateTime;
12+
13+
@Entity
14+
@Getter
15+
@Setter
16+
@NoArgsConstructor
17+
@AllArgsConstructor
18+
@SuperBuilder
19+
@Table(name = "season")
20+
public class Season extends BaseEntity {
21+
private String name;
22+
23+
private LocalDateTime startDate;
24+
private LocalDateTime endDate;
25+
26+
@Enumerated(EnumType.STRING)
27+
private SeasonStatus status; // ACTIVE, INACTIVE
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package cmf.commitField.domain.season.entity;
2+
3+
public enum SeasonStatus {
4+
ACTIVE, INACTIVE
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package cmf.commitField.domain.season.entity;
2+
3+
import cmf.commitField.domain.user.entity.User;
4+
import cmf.commitField.global.jpa.BaseEntity;
5+
import jakarta.persistence.*;
6+
import lombok.*;
7+
import lombok.experimental.SuperBuilder;
8+
9+
import java.time.LocalDateTime;
10+
11+
@Entity
12+
@Getter
13+
@Setter
14+
@NoArgsConstructor
15+
@AllArgsConstructor
16+
@SuperBuilder
17+
@Table(name = "user_season")
18+
public class UserSeason extends BaseEntity {
19+
20+
@ManyToOne
21+
@JoinColumn(name = "user_id", nullable = false)
22+
private User user;
23+
24+
@ManyToOne
25+
@JoinColumn(name = "season_id", nullable = false)
26+
private Season season;
27+
28+
@Enumerated(EnumType.STRING)
29+
@Column(name = "`rank`", nullable = false) // ๋ฐฑํ‹ฑ(`)์„ ์‚ฌ์šฉํ•˜์—ฌ ์˜ˆ์•ฝ์–ด ๋ฌธ์ œ ํ•ด๊ฒฐ
30+
private Rank rank; // ์˜ˆ: SEED, TREE ๋“ฑ๋“ฑ
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package cmf.commitField.domain.season.repository;
2+
3+
import cmf.commitField.domain.season.entity.Season;
4+
import cmf.commitField.domain.season.entity.SeasonStatus;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.stereotype.Repository;
7+
8+
@Repository
9+
public interface SeasonRepository extends JpaRepository<Season, Long> {
10+
Season findByStatus(SeasonStatus status);
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package cmf.commitField.domain.season.repository;
2+
3+
import cmf.commitField.domain.season.entity.Season;
4+
import cmf.commitField.domain.season.entity.UserSeason;
5+
import cmf.commitField.domain.user.entity.User;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.stereotype.Repository;
8+
9+
import java.util.List;
10+
import java.util.Optional;
11+
12+
@Repository
13+
public interface UserSeasonRepository extends JpaRepository<UserSeason, Long> {
14+
List<UserSeason> findByUser(User user);
15+
Optional<UserSeason> findByUserAndSeason(User user, Season season);
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package cmf.commitField.domain.season.service;
2+
3+
import cmf.commitField.domain.season.entity.Season;
4+
import cmf.commitField.domain.season.entity.SeasonStatus;
5+
import cmf.commitField.domain.season.repository.SeasonRepository;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.stereotype.Service;
8+
9+
import java.time.LocalDateTime;
10+
11+
@Service
12+
@RequiredArgsConstructor
13+
public class SeasonService {
14+
private final SeasonRepository seasonRepository;
15+
16+
public Season createNewSeason(String name, LocalDateTime start, LocalDateTime end) {
17+
Season season = Season.builder()
18+
.name(name)
19+
.startDate(start)
20+
.endDate(end)
21+
.status(SeasonStatus.ACTIVE)
22+
.build();
23+
return seasonRepository.save(season);
24+
}
25+
26+
public Season getActiveSeason() {
27+
return seasonRepository.findByStatus(SeasonStatus.ACTIVE);
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cmf.commitField.domain.season.service;
2+
3+
import cmf.commitField.domain.season.entity.Rank;
4+
import cmf.commitField.domain.season.entity.Season;
5+
import cmf.commitField.domain.season.entity.UserSeason;
6+
import cmf.commitField.domain.season.repository.SeasonRepository;
7+
import cmf.commitField.domain.season.repository.UserSeasonRepository;
8+
import cmf.commitField.domain.user.entity.User;
9+
import cmf.commitField.domain.user.repository.UserRepository;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.stereotype.Service;
12+
13+
import java.util.List;
14+
15+
@Service
16+
@RequiredArgsConstructor
17+
public class UserSeasonService {
18+
private final UserSeasonRepository userSeasonRepository;
19+
private final UserRepository userRepository;
20+
private final SeasonRepository seasonRepository;
21+
private final SeasonService seasonService;
22+
23+
// ํ˜„์žฌ ์‹œ์ฆŒ์— ์œ ์ € ๋žญํฌ ์ถ”๊ฐ€ํ•˜๊ธฐ (SEED ๋“ฑ๊ธ‰)
24+
public UserSeason addUserRank(Long userId) {
25+
User user = userRepository.findById(userId)
26+
.orElseThrow(() -> new RuntimeException("User not found"));
27+
28+
Season season = seasonService.getActiveSeason();
29+
30+
UserSeason userSeason = new UserSeason();
31+
userSeason.setUser(user);
32+
userSeason.setSeason(season);
33+
userSeason.setRank(Rank.SEED);
34+
35+
return userSeasonRepository.save(userSeason);
36+
}
37+
38+
// ์œ ์ €์˜ ๋ชจ๋“  ์‹œ์ฆŒ ๋žญํฌ ์กฐํšŒ
39+
public List<UserSeason> getUserRanks(Long userId) {
40+
User user = userRepository.findById(userId)
41+
.orElseThrow(() -> new RuntimeException("User not found"));
42+
return userSeasonRepository.findByUser(user);
43+
}
44+
45+
// ํŠน์ • ์‹œ์ฆŒ์˜ ์œ ์ € ๋žญํฌ ์กฐํšŒ
46+
public UserSeason getUserRankBySeason(Long userId, Long seasonId) {
47+
User user = userRepository.findById(userId)
48+
.orElseThrow(() -> new RuntimeException("User not found"));
49+
Season season = seasonRepository.findById(seasonId)
50+
.orElseThrow(() -> new RuntimeException("Season not found"));
51+
return userSeasonRepository.findByUserAndSeason(user, season)
52+
.orElseThrow(() -> new RuntimeException("Rank not found for this season"));
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package cmf.commitField.global.jpa;
2+
3+
import cmf.commitField.global.scheduler.SeasonScheduler;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.boot.ApplicationRunner;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.stereotype.Component;
8+
9+
@RequiredArgsConstructor
10+
@Component
11+
public class DataLoader {
12+
13+
private final SeasonScheduler seasonScheduler;
14+
15+
// ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ ์‹œ ์ž๋™ ์‹คํ–‰๋˜๋Š” ๋ฉ”์„œ๋“œ
16+
@Bean
17+
public ApplicationRunner loadInitialData() {
18+
return args -> {
19+
// DataLoader์—์„œ ๋ฐ”๋กœ SeasonScheduler์˜ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœ
20+
seasonScheduler.checkAndCreateNewSeason();
21+
};
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package cmf.commitField.global.scheduler;
2+
3+
import cmf.commitField.domain.season.entity.Rank;
4+
import cmf.commitField.domain.season.entity.Season;
5+
import cmf.commitField.domain.season.entity.SeasonStatus;
6+
import cmf.commitField.domain.season.entity.UserSeason;
7+
import cmf.commitField.domain.season.repository.SeasonRepository;
8+
import cmf.commitField.domain.season.repository.UserSeasonRepository;
9+
import cmf.commitField.domain.season.service.SeasonService;
10+
import cmf.commitField.domain.user.entity.User;
11+
import cmf.commitField.domain.user.repository.UserRepository;
12+
import lombok.RequiredArgsConstructor;
13+
import lombok.extern.slf4j.Slf4j;
14+
import org.springframework.scheduling.annotation.Scheduled;
15+
import org.springframework.stereotype.Component;
16+
17+
import java.time.LocalDate;
18+
import java.time.LocalDateTime;
19+
import java.time.Month;
20+
import java.util.List;
21+
22+
@Component
23+
@RequiredArgsConstructor
24+
@Slf4j
25+
public class SeasonScheduler {
26+
27+
private final SeasonRepository seasonRepository;
28+
private final UserSeasonRepository userSeasonRepository;
29+
private final UserRepository userRepository;
30+
private final SeasonService seasonService;
31+
32+
// ๋งค์ผ ์ž์ •๋งˆ๋‹ค ์‹œ์ฆŒ ํ™•์ธ ๋ฐ ์ƒ์„ฑ
33+
@Scheduled(cron = "0 0 0 * * *")
34+
public void checkAndCreateNewSeason() {
35+
LocalDate today = LocalDate.now();
36+
String seasonName = today.getYear() + " " + getSeasonName(today.getMonthValue());
37+
38+
// ํ˜„์žฌ ํ™œ์„ฑ ์‹œ์ฆŒ ์กฐํšŒ
39+
Season activeSeason = seasonService.getActiveSeason();
40+
41+
// ํ˜„์žฌ ํ™œ์„ฑ ์‹œ์ฆŒ์ด ์—†๊ฑฐ๋‚˜, ํ˜„์žฌ ์‹œ์ฆŒ๊ณผ ๋‹ค๋ฅด๋ฉด ์ƒˆ๋กœ์šด ์‹œ์ฆŒ ์ƒ์„ฑ
42+
if (activeSeason == null || !activeSeason.getName().equals(seasonName)) {
43+
LocalDateTime startDate = getSeasonStartDate(today.getYear(), today.getMonth());
44+
LocalDateTime endDate = startDate.plusMonths(3).minusSeconds(1);
45+
46+
//ํ˜„์žฌ ํ™œ์„ฑ ์‹œ์ฆŒ ์ข…๋ฃŒ
47+
if (activeSeason != null) {
48+
activeSeason.setStatus(SeasonStatus.INACTIVE);
49+
seasonRepository.save(activeSeason);
50+
log.info("โœ… ๊ธฐ์กด ์‹œ์ฆŒ '{}' ์ข…๋ฃŒ๋จ", activeSeason.getName());
51+
}
52+
53+
Season newSeason = seasonService.createNewSeason(seasonName, startDate, endDate);
54+
55+
// ๋ชจ๋“  ์œ ์ €์˜ ๋žญํฌ ์ดˆ๊ธฐํ™”
56+
resetUserRanks(newSeason);
57+
58+
System.out.println("์ƒˆ ์‹œ์ฆŒ ์ƒ์„ฑ: " + newSeason.getName());
59+
} else {
60+
System.out.println("์ด๋ฏธ ํ™œ์„ฑํ™”๋œ ์‹œ์ฆŒ: " + activeSeason.getName());
61+
}
62+
}
63+
64+
private void resetUserRanks(Season newSeason) {
65+
List<User> users = userRepository.findAll();
66+
for (User user : users) {
67+
UserSeason userSeason = UserSeason.builder()
68+
.user(user)
69+
.season(newSeason)
70+
.rank(Rank.SEED) // ์ดˆ๊ธฐ ๋žญํฌ ์„ค์ • (์˜ˆ: SEED)
71+
.build();
72+
73+
userSeasonRepository.save(userSeason);
74+
log.info("๐Ÿ”„ ์œ ์ € '{}'์˜ ์‹œ์ฆŒ ๋žญํฌ ์ดˆ๊ธฐํ™” ({} -> ์”จ์•—)", user.getNickname(), newSeason.getName());
75+
}
76+
}
77+
78+
// ์›”์„ ๊ธฐ์ค€์œผ๋กœ ์‹œ์ฆŒ ์ด๋ฆ„ ์ •ํ•˜๊ธฐ (์˜ˆ: 2025 Spring, 2025 Summer)
79+
private String getSeasonName(int month) {
80+
if (month >= 3 && month <= 5) {
81+
return "Spring"; // ๋ด„
82+
} else if (month >= 6 && month <= 8) {
83+
return "Summer"; // ์—ฌ๋ฆ„
84+
} else if (month >= 9 && month <= 11) {
85+
return "Fall"; // ๊ฐ€์„
86+
} else {
87+
return "Winter"; // ๊ฒจ์šธ
88+
}
89+
}
90+
91+
// ํ•ด๋‹น ์‹œ์ฆŒ์˜ ์‹œ์ž‘ ๋‚ ์งœ ๋ฐ˜ํ™˜
92+
private LocalDateTime getSeasonStartDate(int year, Month month) {
93+
int startMonth = switch (month) {
94+
case MARCH, APRIL, MAY -> 3; // ๋ด„
95+
case JUNE, JULY, AUGUST -> 6; // ์—ฌ๋ฆ„
96+
case SEPTEMBER, OCTOBER, NOVEMBER -> 9; // ๊ฐ€์„
97+
default -> 12; // ๊ฒจ์šธ (12์›”๋ถ€ํ„ฐ ์‹œ์ž‘)
98+
};
99+
return LocalDateTime.of(year, startMonth, 1, 0, 0);
100+
}
101+
}

0 commit comments

Comments
 (0)
Please sign in to comment.