-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] 경기 날짜 기준 매칭 완료,실패 스케쥴러 생성 #250
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
base: dev
Are you sure you want to change the base?
Changes from all commits
caec1ae
5f2c6c5
25f8bbb
dd318e3
bd64e70
59aeee8
1d5ded4
07de09d
69892fa
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 |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| package at.mateball.domain.group.core; | ||
|
|
||
| import at.mateball.domain.chatting.core.Chatting; | ||
| import at.mateball.domain.chatting.core.service.ChattingV2Service; | ||
| import at.mateball.domain.gameinformation.core.GameInformation; | ||
| import at.mateball.domain.group.scheduler.MatchSchedule; | ||
| import at.mateball.domain.group.scheduler.ScheduleType; | ||
| import at.mateball.domain.group.scheduler.repository.SchedulerRepository; | ||
| import at.mateball.domain.groupmember.GroupMemberStatus; | ||
| import at.mateball.domain.groupmember.core.GroupMember; | ||
| import at.mateball.domain.user.core.User; | ||
| import at.mateball.exception.BusinessException; | ||
| import at.mateball.exception.code.BusinessErrorCode; | ||
| import jakarta.persistence.EntityManager; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Component | ||
| public class GroupExecutorV3 { | ||
|
|
||
| private final EntityManager entityManager; | ||
| private final ChattingV2Service chattingV2Service; | ||
| private final SchedulerRepository schedulerRepository; | ||
|
|
||
| public GroupExecutorV3(EntityManager entityManager, ChattingV2Service chattingV2Service, SchedulerRepository schedulerRepository) { | ||
| this.entityManager = entityManager; | ||
| this.chattingV2Service = chattingV2Service; | ||
| this.schedulerRepository = schedulerRepository; | ||
| } | ||
|
|
||
| @Transactional | ||
| public Long createGroup(Long userId, Long gameId, boolean isGroup) { | ||
| User user = entityManager.find(User.class, userId); | ||
| if (user == null) { | ||
| throw new BusinessException(BusinessErrorCode.USER_NOT_FOUND); | ||
| } | ||
|
|
||
| GameInformation game = entityManager.find(GameInformation.class, gameId); | ||
| LocalDateTime gameDateTime = LocalDateTime.of(game.getGameDate(), game.getGameTime()); | ||
|
|
||
| Chatting chatting = chattingV2Service.assignChatting(); | ||
| if (chatting == null) { | ||
| throw new BusinessException(BusinessErrorCode.CHATTING_NOT_FOUND); | ||
| } | ||
|
|
||
| Group group = Group.create(user, game, isGroup); | ||
| group.assignChatting(chatting); | ||
|
|
||
| entityManager.persist(group); | ||
| entityManager.flush(); | ||
|
|
||
| GroupMember leader = GroupMember.leader(user, group, GroupMemberStatus.PENDING_REQUEST.getValue()); | ||
| entityManager.persist(leader); | ||
|
|
||
| schedulerRepository.save(new MatchSchedule(group, gameDateTime.minusMinutes(1), ScheduleType.FAIL)); | ||
| schedulerRepository.save(new MatchSchedule(group, gameDateTime, ScheduleType.COMPLETE)); | ||
|
|
||
| return group.getId(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package at.mateball.domain.group.scheduler; | ||
|
|
||
| import at.mateball.domain.group.core.Group; | ||
| import at.mateball.domain.group.core.repository.GroupRepository; | ||
| import at.mateball.domain.groupmember.core.repository.GroupMemberRepository; | ||
| import at.mateball.exception.BusinessException; | ||
| import at.mateball.exception.code.BusinessErrorCode; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class GroupMatchCompleteService { | ||
| private static final int MIN_PARTICIPANT_FOR_COMPLETE = 2; | ||
|
|
||
| private final GroupRepository groupRepository; | ||
| private final GroupMemberRepository groupMemberRepository; | ||
|
|
||
| @Transactional | ||
| public void validateCompleteStatus(Long groupId) { | ||
| Group group = groupRepository.findById(groupId) | ||
| .orElseThrow(() -> new BusinessException(BusinessErrorCode.GROUP_NOT_FOUND)); | ||
|
|
||
| int memberCount = groupMemberRepository.countGroupMember(groupId).count(); | ||
|
|
||
| if ((group.isGroup() && memberCount >= MIN_PARTICIPANT_FOR_COMPLETE)) { | ||
| groupMemberRepository.updateAllStatusComplete(groupId); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package at.mateball.domain.group.scheduler; | ||
|
|
||
| import at.mateball.domain.group.core.repository.GroupRepository; | ||
| import at.mateball.domain.groupmember.core.repository.GroupMemberRepository; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
@@ -12,10 +13,14 @@ | |
| @Service | ||
| @Slf4j | ||
| public class GroupMatchFailService { | ||
| private static final int MIN_PARTICIPANT_FOR_COMPLETE = 2; | ||
|
|
||
| private final GroupRepository groupRepository; | ||
| private final GroupMemberRepository groupMemberRepository; | ||
|
|
||
| public GroupMatchFailService(GroupRepository groupRepository) { | ||
| public GroupMatchFailService(GroupRepository groupRepository, GroupMemberRepository groupMemberRepository) { | ||
| this.groupRepository = groupRepository; | ||
| this.groupMemberRepository = groupMemberRepository; | ||
| } | ||
|
|
||
| @Transactional | ||
|
|
@@ -44,4 +49,12 @@ private LocalDate calculateTargetDate(LocalDate today) { | |
|
|
||
| return today.plusDays(2); | ||
| } | ||
|
|
||
| public void validateFailStatus(Long groupId) { | ||
| int participantCount = groupMemberRepository.countGroupMember(groupId).count(); | ||
|
|
||
| if (participantCount < MIN_PARTICIPANT_FOR_COMPLETE) { | ||
| groupMemberRepository.updateAllStatusFail(groupId); | ||
| } | ||
| } | ||
|
Comment on lines
+53
to
+59
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.
두 가지 문제가 있습니다:
🐛 수정 제안+@Transactional
public void validateFailStatus(Long groupId) {
+ Group group = groupRepository.findById(groupId)
+ .orElseThrow(() -> new BusinessException(BusinessErrorCode.GROUP_NOT_FOUND));
+
int participantCount = groupMemberRepository.countGroupMember(groupId).count();
- if (participantCount < MIN_PARTICIPANT_FOR_COMPLETE) {
+ if (group.isGroup() && participantCount < MIN_PARTICIPANT_FOR_COMPLETE) {
groupMemberRepository.updateAllStatusFail(groupId);
}
}🤖 Prompt for AI Agents
Collaborator
Author
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. 맞습니다, 일대일과 그룹매칭 모두 MIN_PARTICIPANT_FOR_COMPLETE 이하일때 fail 로직이 실행되어야 하므로, 그룹여부는 검증하지 않았습니다. 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. The PR description image shows test execution results for the
다만, `@Transactional`
public void validateFailStatus(Long groupId) {
int participantCount = groupMemberRepository.countGroupMember(groupId).count();
if (participantCount < MIN_PARTICIPANT_FOR_COMPLETE) {
groupMemberRepository.updateAllStatusFail(groupId);
}
}✏️ Learnings added
🧠 Learnings used |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| package at.mateball.domain.group.scheduler; | ||
|
|
||
| import at.mateball.domain.group.core.Group; | ||
| import jakarta.persistence.*; | ||
| import lombok.Getter; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @Table(name = "match_schedule") | ||
| public class MatchSchedule { | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY, optional = false) | ||
| @JoinColumn(name = "group_id", nullable = false) | ||
| private Group group; | ||
|
Comment on lines
+17
to
+19
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. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Group 삭제 로직 확인
rg -n "deleteGroup|delete.*Group" --type=java -C3Repository: MATEBALL/MATEBALL-SERVER Length of output: 745 🏁 Script executed: # Find and examine the Group entity definition
fd -t f "Group.java" --type=file | grep -E "domain/group|entity" | head -5Repository: MATEBALL/MATEBALL-SERVER Length of output: 122 🏁 Script executed: # Search for Group entity class definition
rg -n "class Group" --type=java -A 30 | head -100Repository: MATEBALL/MATEBALL-SERVER Length of output: 11902 🏁 Script executed: # Check for any cascade or orphan delete configurations related to MatchSchedule
rg -n "MatchSchedule" --type=java -B 2 -A 2Repository: MATEBALL/MATEBALL-SERVER Length of output: 13209 🏁 Script executed: # Look for deletion logic related to MatchSchedule
rg -n "delete.*MatchSchedule|MatchSchedule.*delete" --type=java -C 3Repository: MATEBALL/MATEBALL-SERVER Length of output: 50 🏁 Script executed: cat -n src/main/java/at/mateball/domain/group/core/Group.javaRepository: MATEBALL/MATEBALL-SERVER Length of output: 1821 🏁 Script executed: # Check if there's any explicit cleanup logic for MatchSchedule deletion in services
rg -n "delete|remove" src/main/java/at/mateball/domain/group/scheduler/ --type=java -B 2 -A 2Repository: MATEBALL/MATEBALL-SERVER Length of output: 50 🏁 Script executed: # Check MatchScheduler implementation to understand how it handles missing groups
cat -n src/main/java/at/mateball/domain/group/scheduler/MatchScheduler.javaRepository: MATEBALL/MATEBALL-SERVER Length of output: 1798 🏁 Script executed: # Check how lazy loading behaves when Group is deleted - look for FK constraint details
rg -n "foreign key|FOREIGN KEY|constraint" --type=sql --type=ddl 2>/dev/null || echo "No SQL files found"Repository: MATEBALL/MATEBALL-SERVER Length of output: 86 🏁 Script executed: # Look for any test that validates Group deletion behavior
rg -n "deleteAllByLeaderId|testDelete|Group.*delete" src/test --type=java -B 2 -A 5Repository: MATEBALL/MATEBALL-SERVER Length of output: 50 🏁 Script executed: # Check if there are any migration files or schema definitions that show FK constraints
fd -type f -name "*.sql" -o -name "*schema*" -o -name "*migration*" | head -20Repository: MATEBALL/MATEBALL-SERVER Length of output: 238 🏁 Script executed: # Look for any repository delete methods related to Group
rg -n "delete" src/main/java/at/mateball/domain/group/core/repository --type=java -B 1 -A 3Repository: MATEBALL/MATEBALL-SERVER Length of output: 872 🏁 Script executed: # Check GroupMatchFailService to see how it handles the Group
cat -n src/main/java/at/mateball/domain/group/scheduler/GroupMatchFailService.java | head -50Repository: MATEBALL/MATEBALL-SERVER Length of output: 2216 🏁 Script executed: # Check if there are any database migration files or schema files
find . -name "*.sql" -o -name "*.ddl" -o -name "*schema*" 2>/dev/null | grep -v node_modules | head -20Repository: MATEBALL/MATEBALL-SERVER Length of output: 50 🏁 Script executed: # Look at the test file to see what happens with orphaned schedules
cat -n src/test/java/at/mateball/scheduler/MatchSchedulerIntegrationTest.java | head -100Repository: MATEBALL/MATEBALL-SERVER Length of output: 3981 🏁 Script executed: # Check how the scheduler actually uses the group object
rg -n "getGroup\(\)" src/main/java/at/mateball/domain/group/scheduler/ --type=java -B 1 -A 1Repository: MATEBALL/MATEBALL-SERVER Length of output: 1025 🏁 Script executed: # Check if there's any validation or FK constraint setup in persistence configuration
rg -n "hibernate|JPA|constraint|foreign" src/main/resources --type=properties -iRepository: MATEBALL/MATEBALL-SERVER Length of output: 102 🏁 Script executed: # Look for any additional methods in GroupMatchFailService that might handle cleanup
cat -n src/main/java/at/mateball/domain/group/scheduler/GroupMatchFailService.javaRepository: MATEBALL/MATEBALL-SERVER Length of output: 2578 🏁 Script executed: # Look for any usage of deleteAllByLeaderId to understand when/if groups are actually deleted
rg -n "deleteAllByLeaderId" --type=java -B 2 -A 2Repository: MATEBALL/MATEBALL-SERVER Length of output: 1029 🏁 Script executed: # Check application.yml or application.properties for JPA/Hibernate settings
find . -name "application*.yml" -o -name "application*.properties" 2>/dev/null | xargs cat 2>/dev/null | grep -i "hibernate\|jpa\|constraint" | head -20Repository: MATEBALL/MATEBALL-SERVER Length of output: 140 🏁 Script executed: # Check if there's any explicit handling in MatchScheduler for deleted groups
cat -n src/main/java/at/mateball/domain/group/scheduler/MatchScheduler.javaRepository: MATEBALL/MATEBALL-SERVER Length of output: 1798 그룹 삭제 시 MatchSchedule 레코드가 orphan으로 남을 수 있는 설계 이슈
🤖 Prompt for AI Agents |
||
|
|
||
| @Column(nullable = false) | ||
| private LocalDateTime executeTime; | ||
|
|
||
| @Column(nullable = false) | ||
| @Enumerated(EnumType.STRING) | ||
| private ScheduleType type; | ||
|
|
||
| @Column(nullable = false) | ||
| private boolean executed = false; | ||
|
|
||
| protected MatchSchedule() { | ||
| } | ||
|
|
||
| public MatchSchedule(Group group, LocalDateTime executeTime, ScheduleType type) { | ||
| this.group = group; | ||
| this.executeTime = executeTime; | ||
| this.type = type; | ||
| this.executed = false; | ||
| } | ||
|
|
||
| public void markExecuted() { | ||
| this.executed = true; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package at.mateball.domain.group.scheduler; | ||
|
|
||
| import at.mateball.domain.group.scheduler.repository.SchedulerRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.scheduling.annotation.Scheduled; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| @Slf4j | ||
| public class MatchScheduler { | ||
|
|
||
| private final SchedulerRepository repository; | ||
| private final GroupMatchFailService matchFailService; | ||
| private final GroupMatchCompleteService matchCompleteService; | ||
|
|
||
| @Scheduled(cron = "0 */1 * * * *") | ||
| @Transactional | ||
| public void run() { | ||
| log.info("매칭 상태 업데이트 스케줄러 호출 완료"); | ||
|
|
||
| LocalDateTime now = LocalDateTime.now(); | ||
| List<MatchSchedule> targets = repository.findAllDueSchedules(now); | ||
|
|
||
| for (MatchSchedule s : targets) { | ||
| try { | ||
| if (s.getType().isFail()) { | ||
| matchFailService.validateFailStatus(s.getGroup().getId()); | ||
| } else { | ||
| matchCompleteService.validateCompleteStatus(s.getGroup().getId()); | ||
| } | ||
|
|
||
| s.markExecuted(); | ||
|
|
||
| } catch (Exception e) { | ||
| log.error("스케줄 처리 실패 groupId={}", s.getGroup().getId(), e); | ||
| } | ||
|
Comment on lines
+30
to
+42
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. 예외 발생 시 스케줄 재처리 동작 명확화 필요 현재 구조에서 예외 발생 시 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package at.mateball.domain.group.scheduler; | ||
|
|
||
| public enum ScheduleType { | ||
|
|
||
| FAIL, | ||
| COMPLETE; | ||
|
|
||
| public boolean isFail() { | ||
| return this == FAIL; | ||
| } | ||
|
|
||
| public boolean isComplete() { | ||
| return this == COMPLETE; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package at.mateball.domain.group.scheduler.repository; | ||
|
|
||
| import at.mateball.domain.group.scheduler.MatchSchedule; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
|
|
||
| public interface SchedulerCustom { | ||
| void save(MatchSchedule schedule); | ||
|
|
||
| List<MatchSchedule> findAllDueSchedules(LocalDateTime now); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package at.mateball.domain.group.scheduler.repository; | ||
|
|
||
| import at.mateball.domain.group.core.Group; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| @Repository | ||
| public interface SchedulerRepository extends JpaRepository<Group, Long>, SchedulerCustom { | ||
|
Comment on lines
+7
to
+8
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. 치명적 버그:
🐛 수정 제안-import at.mateball.domain.group.core.Group;
+import at.mateball.domain.group.scheduler.MatchSchedule;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
`@Repository`
-public interface SchedulerRepository extends JpaRepository<Group, Long>, SchedulerCustom {
+public interface SchedulerRepository extends JpaRepository<MatchSchedule, Long>, SchedulerCustom {
}🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package at.mateball.domain.group.scheduler.repository; | ||
|
|
||
| import at.mateball.domain.group.scheduler.MatchSchedule; | ||
| import at.mateball.domain.group.scheduler.QMatchSchedule; | ||
| import com.querydsl.jpa.impl.JPAQueryFactory; | ||
| import jakarta.persistence.EntityManager; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
|
|
||
| public class SchedulerRepositoryImpl implements SchedulerCustom { | ||
| private final JPAQueryFactory queryFactory; | ||
| private final EntityManager em; | ||
|
|
||
| public SchedulerRepositoryImpl(JPAQueryFactory queryFactory, EntityManager em) { | ||
| this.queryFactory = queryFactory; | ||
| this.em = em; | ||
| } | ||
|
|
||
| @Override | ||
| public void save(MatchSchedule schedule) { | ||
| em.persist(schedule); | ||
| } | ||
|
|
||
| @Override | ||
| public List<MatchSchedule> findAllDueSchedules(LocalDateTime now) { | ||
| QMatchSchedule matchSchedule = QMatchSchedule.matchSchedule; | ||
|
|
||
| return queryFactory | ||
| .selectFrom(matchSchedule) | ||
| .where( | ||
| matchSchedule.executed.isFalse(), | ||
| matchSchedule.executeTime.loe(now) | ||
| ) | ||
| .fetch(); | ||
| } | ||
| } |
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.
FAIL과 COMPLETE 스케줄 동시 실행 가능성
FAIL 스케줄은
gameDateTime - 1분, COMPLETE 스케줄은gameDateTime에 등록됩니다. 스케줄러가 1분 간격으로 실행되므로, 스케줄러 실행 시점에 따라 두 스케줄이 동시에 due 상태가 될 수 있습니다.SchedulerRepositoryImpl.findAllDueSchedules()에 ORDER BY가 없으면 처리 순서가 보장되지 않아 의도치 않은 결과가 발생할 수 있습니다.🤖 Prompt for AI Agents