From 631b2f82b5246005d3e0f596cbbd3329c69c9ce4 Mon Sep 17 00:00:00 2001 From: pia01190 Date: Sat, 26 Jul 2025 15:26:23 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[EA3-182]=20feature:=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EC=8B=A0=EC=B2=AD=20=EC=8B=9C=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=EC=9E=A5=EC=97=90=EA=B2=8C=20=EC=95=8C=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/alram/service/AlarmService.java | 18 +++++++++ .../domain/alram/type/AlarmType.java | 6 ++- .../domain/alram/type/DomainType.java | 4 +- .../event/StudyApplicationEvent.java | 4 ++ .../StudyApplicationMessageProvider.java | 37 +++++++++++++++++++ .../service/ApplicationService.java | 5 +++ .../repository/TimeVoteQueryRepository.java | 2 +- 7 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 src/main/java/grep/neogulcoder/domain/studyapplication/event/StudyApplicationEvent.java create mode 100644 src/main/java/grep/neogulcoder/domain/studyapplication/provider/StudyApplicationMessageProvider.java diff --git a/src/main/java/grep/neogulcoder/domain/alram/service/AlarmService.java b/src/main/java/grep/neogulcoder/domain/alram/service/AlarmService.java index 50d770df..86ac55ba 100644 --- a/src/main/java/grep/neogulcoder/domain/alram/service/AlarmService.java +++ b/src/main/java/grep/neogulcoder/domain/alram/service/AlarmService.java @@ -6,6 +6,8 @@ import grep.neogulcoder.domain.alram.repository.AlarmRepository; import grep.neogulcoder.domain.alram.type.AlarmType; import grep.neogulcoder.domain.alram.type.DomainType; +import grep.neogulcoder.domain.recruitment.post.RecruitmentPost; +import grep.neogulcoder.domain.recruitment.post.repository.RecruitmentPostRepository; import grep.neogulcoder.domain.study.Study; import grep.neogulcoder.domain.study.StudyMember; import grep.neogulcoder.domain.study.enums.StudyMemberRole; @@ -15,6 +17,7 @@ import grep.neogulcoder.domain.study.repository.StudyMemberQueryRepository; import grep.neogulcoder.domain.study.repository.StudyMemberRepository; import grep.neogulcoder.domain.study.repository.StudyRepository; +import grep.neogulcoder.domain.studyapplication.event.StudyApplicationEvent; import grep.neogulcoder.domain.timevote.event.TimeVotePeriodCreatedEvent; import grep.neogulcoder.global.exception.business.BusinessException; import grep.neogulcoder.global.exception.business.NotFoundException; @@ -25,6 +28,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import static grep.neogulcoder.domain.recruitment.RecruitmentErrorCode.NOT_FOUND; import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.*; import static grep.neogulcoder.domain.studyapplication.exception.code.ApplicationErrorCode.*; @@ -38,6 +42,7 @@ public class AlarmService { private final StudyRepository studyRepository; private final StudyMemberQueryRepository studyMemberQueryRepository; private final StudyMemberRepository studyMemberRepository; + private final RecruitmentPostRepository recruitmentPostRepository; @Transactional public void saveAlarm(Long receiverId, AlarmType alarmType, DomainType domainType, Long domainId) { @@ -138,6 +143,19 @@ public void handleTimeVotePeriodCreatedEvent(TimeVotePeriodCreatedEvent event) { } } + @EventListener + public void handleStudyApplicationEvent(StudyApplicationEvent event) { + RecruitmentPost recruitmentPost = recruitmentPostRepository.findByIdAndActivatedTrue(event.recruitmentPostId()) + .orElseThrow(() -> new BusinessException(NOT_FOUND)); + + saveAlarm( + recruitmentPost.getUserId(), + AlarmType.STUDY_APPLICATION, + DomainType.RECRUITMENT_POST, + event.recruitmentPostId() + ); + } + private Alarm findValidAlarm(Long alarmId) { return alarmRepository.findById(alarmId).orElseThrow(() -> new NotFoundException(AlarmErrorCode.ALARM_NOT_FOUND)); } diff --git a/src/main/java/grep/neogulcoder/domain/alram/type/AlarmType.java b/src/main/java/grep/neogulcoder/domain/alram/type/AlarmType.java index 0133176b..00d652d2 100644 --- a/src/main/java/grep/neogulcoder/domain/alram/type/AlarmType.java +++ b/src/main/java/grep/neogulcoder/domain/alram/type/AlarmType.java @@ -1,5 +1,9 @@ package grep.neogulcoder.domain.alram.type; public enum AlarmType { - INVITE, STUDY_EXTEND, STUDY_EXTENSION_REMINDER, TIME_VOTE_REQUEST + INVITE, + STUDY_EXTEND, + STUDY_EXTENSION_REMINDER, + TIME_VOTE_REQUEST, + STUDY_APPLICATION } diff --git a/src/main/java/grep/neogulcoder/domain/alram/type/DomainType.java b/src/main/java/grep/neogulcoder/domain/alram/type/DomainType.java index 1e57844b..0720e9a3 100644 --- a/src/main/java/grep/neogulcoder/domain/alram/type/DomainType.java +++ b/src/main/java/grep/neogulcoder/domain/alram/type/DomainType.java @@ -1,5 +1,7 @@ package grep.neogulcoder.domain.alram.type; public enum DomainType { - STUDY, TIME_VOTE + STUDY, + TIME_VOTE, + RECRUITMENT_POST } diff --git a/src/main/java/grep/neogulcoder/domain/studyapplication/event/StudyApplicationEvent.java b/src/main/java/grep/neogulcoder/domain/studyapplication/event/StudyApplicationEvent.java new file mode 100644 index 00000000..311a0021 --- /dev/null +++ b/src/main/java/grep/neogulcoder/domain/studyapplication/event/StudyApplicationEvent.java @@ -0,0 +1,4 @@ +package grep.neogulcoder.domain.studyapplication.event; + +public record StudyApplicationEvent(Long recruitmentPostId, Long applicationId) { +} diff --git a/src/main/java/grep/neogulcoder/domain/studyapplication/provider/StudyApplicationMessageProvider.java b/src/main/java/grep/neogulcoder/domain/studyapplication/provider/StudyApplicationMessageProvider.java new file mode 100644 index 00000000..75a8bd81 --- /dev/null +++ b/src/main/java/grep/neogulcoder/domain/studyapplication/provider/StudyApplicationMessageProvider.java @@ -0,0 +1,37 @@ +package grep.neogulcoder.domain.studyapplication.provider; + +import grep.neogulcoder.domain.alram.type.AlarmType; +import grep.neogulcoder.domain.alram.type.DomainType; +import grep.neogulcoder.domain.recruitment.post.RecruitmentPost; +import grep.neogulcoder.domain.recruitment.post.repository.RecruitmentPostRepository; +import grep.neogulcoder.global.exception.business.NotFoundException; +import grep.neogulcoder.global.provider.MessageProvidable; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import static grep.neogulcoder.domain.recruitment.RecruitmentErrorCode.*; + +@Component +@RequiredArgsConstructor +public class StudyApplicationMessageProvider implements MessageProvidable { + + private final RecruitmentPostRepository recruitmentPostRepository; + + @Override + public boolean isSupport(AlarmType alarmType) { + return alarmType == AlarmType.STUDY_APPLICATION; + } + + @Override + public String provideMessage(DomainType domainType, Long domainId) { + if (domainType != DomainType.RECRUITMENT_POST) { + throw new IllegalArgumentException("스터디 신청 알림은 RECRUITMENT_POST 도메인에만 해당됩니다."); + } + + String recruitmentPostSubject = recruitmentPostRepository.findByIdAndActivatedTrue(domainId) + .map(RecruitmentPost::getSubject) + .orElseThrow(() -> new NotFoundException(NOT_FOUND)); + + return String.format("모집글 '%s'에 새로운 신청이 들어왔습니다.", recruitmentPostSubject); + } +} diff --git a/src/main/java/grep/neogulcoder/domain/studyapplication/service/ApplicationService.java b/src/main/java/grep/neogulcoder/domain/studyapplication/service/ApplicationService.java index 8c0a232b..87bfb1af 100644 --- a/src/main/java/grep/neogulcoder/domain/studyapplication/service/ApplicationService.java +++ b/src/main/java/grep/neogulcoder/domain/studyapplication/service/ApplicationService.java @@ -15,11 +15,13 @@ import grep.neogulcoder.domain.studyapplication.controller.dto.response.MyApplicationResponse; import grep.neogulcoder.domain.studyapplication.controller.dto.response.ReceivedApplicationPagingResponse; import grep.neogulcoder.domain.studyapplication.controller.dto.response.ReceivedApplicationResponse; +import grep.neogulcoder.domain.studyapplication.event.StudyApplicationEvent; import grep.neogulcoder.domain.studyapplication.repository.ApplicationQueryRepository; import grep.neogulcoder.domain.studyapplication.repository.ApplicationRepository; import grep.neogulcoder.global.exception.business.BusinessException; import grep.neogulcoder.global.exception.business.NotFoundException; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -40,6 +42,7 @@ public class ApplicationService { private final StudyMemberRepository studyMemberRepository; private final StudyRepository studyRepository; private final StudyMemberQueryRepository studyMemberQueryRepository; + private final ApplicationEventPublisher eventPublisher; @Transactional public ReceivedApplicationPagingResponse getReceivedApplicationsPaging(Long recruitmentPostId, Pageable pageable, Long userId) { @@ -68,6 +71,8 @@ public Long createApplication(Long recruitmentPostId, ApplicationCreateRequest r StudyApplication application = request.toEntity(recruitmentPostId, userId); applicationRepository.save(application); + eventPublisher.publishEvent(new StudyApplicationEvent(recruitmentPostId, application.getId())); + return application.getId(); } diff --git a/src/main/java/grep/neogulcoder/domain/timevote/repository/TimeVoteQueryRepository.java b/src/main/java/grep/neogulcoder/domain/timevote/repository/TimeVoteQueryRepository.java index a45234f7..8fe1e292 100644 --- a/src/main/java/grep/neogulcoder/domain/timevote/repository/TimeVoteQueryRepository.java +++ b/src/main/java/grep/neogulcoder/domain/timevote/repository/TimeVoteQueryRepository.java @@ -7,7 +7,7 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import grep.neogulcoder.domain.study.QStudyMember; import grep.neogulcoder.domain.timevote.dto.response.TimeVoteSubmissionStatusResponse; -import grep.neogulcoder.domain.timevote.entity.QTimeVote; +import grep.neogulcoder.domain.timevote.QTimeVote; import grep.neogulcoder.domain.users.entity.QUser; import jakarta.persistence.EntityManager; import com.querydsl.core.Tuple; From de8d5f45c680d965298ab852fda2c9aefb332acc Mon Sep 17 00:00:00 2001 From: pia01190 Date: Sat, 26 Jul 2025 16:25:26 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[EA3-182]=20feature:=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EC=8B=A0=EC=B2=AD=20=EC=83=81=ED=83=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EC=8B=9C=20=EC=95=8C=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/alram/service/AlarmService.java | 21 ++++++++- .../domain/alram/type/AlarmType.java | 4 +- .../domain/alram/type/DomainType.java | 3 +- .../event/ApplicationEvent.java | 4 ++ .../event/ApplicationStatusChangedEvent.java | 6 +++ .../event/StudyApplicationEvent.java | 4 -- .../ApplicationApprovedMessageProvider.java | 43 ++++++++++++++++++ ...r.java => ApplicationMessageProvider.java} | 2 +- .../ApplicationRejectedMessageProvider.java | 44 +++++++++++++++++++ .../service/ApplicationService.java | 10 ++++- 10 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 src/main/java/grep/neogulcoder/domain/studyapplication/event/ApplicationEvent.java create mode 100644 src/main/java/grep/neogulcoder/domain/studyapplication/event/ApplicationStatusChangedEvent.java delete mode 100644 src/main/java/grep/neogulcoder/domain/studyapplication/event/StudyApplicationEvent.java create mode 100644 src/main/java/grep/neogulcoder/domain/studyapplication/provider/ApplicationApprovedMessageProvider.java rename src/main/java/grep/neogulcoder/domain/studyapplication/provider/{StudyApplicationMessageProvider.java => ApplicationMessageProvider.java} (95%) create mode 100644 src/main/java/grep/neogulcoder/domain/studyapplication/provider/ApplicationRejectedMessageProvider.java diff --git a/src/main/java/grep/neogulcoder/domain/alram/service/AlarmService.java b/src/main/java/grep/neogulcoder/domain/alram/service/AlarmService.java index 86ac55ba..35e5c4d9 100644 --- a/src/main/java/grep/neogulcoder/domain/alram/service/AlarmService.java +++ b/src/main/java/grep/neogulcoder/domain/alram/service/AlarmService.java @@ -17,7 +17,10 @@ import grep.neogulcoder.domain.study.repository.StudyMemberQueryRepository; import grep.neogulcoder.domain.study.repository.StudyMemberRepository; import grep.neogulcoder.domain.study.repository.StudyRepository; -import grep.neogulcoder.domain.studyapplication.event.StudyApplicationEvent; +import grep.neogulcoder.domain.studyapplication.StudyApplication; +import grep.neogulcoder.domain.studyapplication.event.ApplicationEvent; +import grep.neogulcoder.domain.studyapplication.event.ApplicationStatusChangedEvent; +import grep.neogulcoder.domain.studyapplication.repository.ApplicationRepository; import grep.neogulcoder.domain.timevote.event.TimeVotePeriodCreatedEvent; import grep.neogulcoder.global.exception.business.BusinessException; import grep.neogulcoder.global.exception.business.NotFoundException; @@ -43,6 +46,7 @@ public class AlarmService { private final StudyMemberQueryRepository studyMemberQueryRepository; private final StudyMemberRepository studyMemberRepository; private final RecruitmentPostRepository recruitmentPostRepository; + private final ApplicationRepository applicationRepository; @Transactional public void saveAlarm(Long receiverId, AlarmType alarmType, DomainType domainType, Long domainId) { @@ -144,7 +148,7 @@ public void handleTimeVotePeriodCreatedEvent(TimeVotePeriodCreatedEvent event) { } @EventListener - public void handleStudyApplicationEvent(StudyApplicationEvent event) { + public void handleApplicationEvent(ApplicationEvent event) { RecruitmentPost recruitmentPost = recruitmentPostRepository.findByIdAndActivatedTrue(event.recruitmentPostId()) .orElseThrow(() -> new BusinessException(NOT_FOUND)); @@ -156,6 +160,19 @@ public void handleStudyApplicationEvent(StudyApplicationEvent event) { ); } + @EventListener + public void handleApplicationStatusChangedEvent(ApplicationStatusChangedEvent event) { + StudyApplication application = applicationRepository.findByIdAndActivatedTrue(event.applicationId()) + .orElseThrow(() -> new BusinessException(APPLICATION_NOT_FOUND)); + + saveAlarm( + application.getUserId(), + event.alarmType(), + DomainType.STUDY_APPLICATION, + application.getId() + ); + } + private Alarm findValidAlarm(Long alarmId) { return alarmRepository.findById(alarmId).orElseThrow(() -> new NotFoundException(AlarmErrorCode.ALARM_NOT_FOUND)); } diff --git a/src/main/java/grep/neogulcoder/domain/alram/type/AlarmType.java b/src/main/java/grep/neogulcoder/domain/alram/type/AlarmType.java index 00d652d2..19d2866d 100644 --- a/src/main/java/grep/neogulcoder/domain/alram/type/AlarmType.java +++ b/src/main/java/grep/neogulcoder/domain/alram/type/AlarmType.java @@ -5,5 +5,7 @@ public enum AlarmType { STUDY_EXTEND, STUDY_EXTENSION_REMINDER, TIME_VOTE_REQUEST, - STUDY_APPLICATION + STUDY_APPLICATION, + STUDY_APPLICATION_APPROVED, + STUDY_APPLICATION_REJECTED } diff --git a/src/main/java/grep/neogulcoder/domain/alram/type/DomainType.java b/src/main/java/grep/neogulcoder/domain/alram/type/DomainType.java index 0720e9a3..1f4d76a3 100644 --- a/src/main/java/grep/neogulcoder/domain/alram/type/DomainType.java +++ b/src/main/java/grep/neogulcoder/domain/alram/type/DomainType.java @@ -3,5 +3,6 @@ public enum DomainType { STUDY, TIME_VOTE, - RECRUITMENT_POST + RECRUITMENT_POST, + STUDY_APPLICATION } diff --git a/src/main/java/grep/neogulcoder/domain/studyapplication/event/ApplicationEvent.java b/src/main/java/grep/neogulcoder/domain/studyapplication/event/ApplicationEvent.java new file mode 100644 index 00000000..70081d49 --- /dev/null +++ b/src/main/java/grep/neogulcoder/domain/studyapplication/event/ApplicationEvent.java @@ -0,0 +1,4 @@ +package grep.neogulcoder.domain.studyapplication.event; + +public record ApplicationEvent(Long recruitmentPostId, Long applicationId) { +} diff --git a/src/main/java/grep/neogulcoder/domain/studyapplication/event/ApplicationStatusChangedEvent.java b/src/main/java/grep/neogulcoder/domain/studyapplication/event/ApplicationStatusChangedEvent.java new file mode 100644 index 00000000..e36f1a82 --- /dev/null +++ b/src/main/java/grep/neogulcoder/domain/studyapplication/event/ApplicationStatusChangedEvent.java @@ -0,0 +1,6 @@ +package grep.neogulcoder.domain.studyapplication.event; + +import grep.neogulcoder.domain.alram.type.AlarmType; + +public record ApplicationStatusChangedEvent(Long applicationId, AlarmType alarmType) { +} diff --git a/src/main/java/grep/neogulcoder/domain/studyapplication/event/StudyApplicationEvent.java b/src/main/java/grep/neogulcoder/domain/studyapplication/event/StudyApplicationEvent.java deleted file mode 100644 index 311a0021..00000000 --- a/src/main/java/grep/neogulcoder/domain/studyapplication/event/StudyApplicationEvent.java +++ /dev/null @@ -1,4 +0,0 @@ -package grep.neogulcoder.domain.studyapplication.event; - -public record StudyApplicationEvent(Long recruitmentPostId, Long applicationId) { -} diff --git a/src/main/java/grep/neogulcoder/domain/studyapplication/provider/ApplicationApprovedMessageProvider.java b/src/main/java/grep/neogulcoder/domain/studyapplication/provider/ApplicationApprovedMessageProvider.java new file mode 100644 index 00000000..e3559192 --- /dev/null +++ b/src/main/java/grep/neogulcoder/domain/studyapplication/provider/ApplicationApprovedMessageProvider.java @@ -0,0 +1,43 @@ +package grep.neogulcoder.domain.studyapplication.provider; + +import grep.neogulcoder.domain.alram.type.*; +import grep.neogulcoder.domain.recruitment.post.RecruitmentPost; +import grep.neogulcoder.domain.recruitment.post.repository.RecruitmentPostRepository; +import grep.neogulcoder.domain.studyapplication.StudyApplication; +import grep.neogulcoder.domain.studyapplication.repository.ApplicationRepository; +import grep.neogulcoder.global.exception.business.NotFoundException; +import grep.neogulcoder.global.provider.MessageProvidable; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import static grep.neogulcoder.domain.recruitment.RecruitmentErrorCode.*; +import static grep.neogulcoder.domain.studyapplication.exception.code.ApplicationErrorCode.*; + +@Component +@RequiredArgsConstructor +public class ApplicationApprovedMessageProvider implements MessageProvidable { + + private final ApplicationRepository applicationRepository; + private final RecruitmentPostRepository recruitmentPostRepository; + + @Override + public boolean isSupport(AlarmType alarmType) { + return alarmType == AlarmType.STUDY_APPLICATION_APPROVED; + } + + @Override + public String provideMessage(DomainType domainType, Long domainId) { + if (domainType != DomainType.STUDY_APPLICATION) { + throw new IllegalArgumentException("스터디 신청 승인 알림은 STUDY_APPLICATION 도메인에만 해당됩니다."); + } + + StudyApplication application = applicationRepository.findByIdAndActivatedTrue(domainId) + .orElseThrow(() -> new NotFoundException(APPLICATION_NOT_FOUND)); + + String subject = recruitmentPostRepository.findById(application.getRecruitmentPostId()) + .map(RecruitmentPost::getSubject) + .orElseThrow(() -> new NotFoundException(NOT_FOUND)); + + return String.format("당신이 지원한 '%s' 스터디가 승인되었습니다.", subject); + } +} diff --git a/src/main/java/grep/neogulcoder/domain/studyapplication/provider/StudyApplicationMessageProvider.java b/src/main/java/grep/neogulcoder/domain/studyapplication/provider/ApplicationMessageProvider.java similarity index 95% rename from src/main/java/grep/neogulcoder/domain/studyapplication/provider/StudyApplicationMessageProvider.java rename to src/main/java/grep/neogulcoder/domain/studyapplication/provider/ApplicationMessageProvider.java index 75a8bd81..55e85b52 100644 --- a/src/main/java/grep/neogulcoder/domain/studyapplication/provider/StudyApplicationMessageProvider.java +++ b/src/main/java/grep/neogulcoder/domain/studyapplication/provider/ApplicationMessageProvider.java @@ -13,7 +13,7 @@ @Component @RequiredArgsConstructor -public class StudyApplicationMessageProvider implements MessageProvidable { +public class ApplicationMessageProvider implements MessageProvidable { private final RecruitmentPostRepository recruitmentPostRepository; diff --git a/src/main/java/grep/neogulcoder/domain/studyapplication/provider/ApplicationRejectedMessageProvider.java b/src/main/java/grep/neogulcoder/domain/studyapplication/provider/ApplicationRejectedMessageProvider.java new file mode 100644 index 00000000..f13f564d --- /dev/null +++ b/src/main/java/grep/neogulcoder/domain/studyapplication/provider/ApplicationRejectedMessageProvider.java @@ -0,0 +1,44 @@ +package grep.neogulcoder.domain.studyapplication.provider; + +import grep.neogulcoder.domain.alram.type.AlarmType; +import grep.neogulcoder.domain.alram.type.DomainType; +import grep.neogulcoder.domain.recruitment.post.RecruitmentPost; +import grep.neogulcoder.domain.recruitment.post.repository.RecruitmentPostRepository; +import grep.neogulcoder.domain.studyapplication.StudyApplication; +import grep.neogulcoder.domain.studyapplication.repository.ApplicationRepository; +import grep.neogulcoder.global.exception.business.NotFoundException; +import grep.neogulcoder.global.provider.MessageProvidable; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import static grep.neogulcoder.domain.recruitment.RecruitmentErrorCode.*; +import static grep.neogulcoder.domain.studyapplication.exception.code.ApplicationErrorCode.*; + +@Component +@RequiredArgsConstructor +public class ApplicationRejectedMessageProvider implements MessageProvidable { + + private final ApplicationRepository applicationRepository; + private final RecruitmentPostRepository recruitmentPostRepository; + + @Override + public boolean isSupport(AlarmType alarmType) { + return alarmType == AlarmType.STUDY_APPLICATION_REJECTED; + } + + @Override + public String provideMessage(DomainType domainType, Long domainId) { + if (domainType != DomainType.STUDY_APPLICATION) { + throw new IllegalArgumentException("스터디 신청 거절 알림은 STUDY_APPLICATION 도메인에만 해당됩니다."); + } + + StudyApplication application = applicationRepository.findByIdAndActivatedTrue(domainId) + .orElseThrow(() -> new NotFoundException(APPLICATION_NOT_FOUND)); + + String subject = recruitmentPostRepository.findById(application.getRecruitmentPostId()) + .map(RecruitmentPost::getSubject) + .orElseThrow(() -> new NotFoundException(NOT_FOUND)); + + return String.format("당신이 지원한 '%s' 스터디가 거절되었습니다.", subject); + } +} diff --git a/src/main/java/grep/neogulcoder/domain/studyapplication/service/ApplicationService.java b/src/main/java/grep/neogulcoder/domain/studyapplication/service/ApplicationService.java index 87bfb1af..5327d5d0 100644 --- a/src/main/java/grep/neogulcoder/domain/studyapplication/service/ApplicationService.java +++ b/src/main/java/grep/neogulcoder/domain/studyapplication/service/ApplicationService.java @@ -1,5 +1,6 @@ package grep.neogulcoder.domain.studyapplication.service; +import grep.neogulcoder.domain.alram.type.AlarmType; import grep.neogulcoder.domain.recruitment.post.RecruitmentPost; import grep.neogulcoder.domain.recruitment.post.repository.RecruitmentPostRepository; import grep.neogulcoder.domain.study.Study; @@ -15,7 +16,8 @@ import grep.neogulcoder.domain.studyapplication.controller.dto.response.MyApplicationResponse; import grep.neogulcoder.domain.studyapplication.controller.dto.response.ReceivedApplicationPagingResponse; import grep.neogulcoder.domain.studyapplication.controller.dto.response.ReceivedApplicationResponse; -import grep.neogulcoder.domain.studyapplication.event.StudyApplicationEvent; +import grep.neogulcoder.domain.studyapplication.event.ApplicationEvent; +import grep.neogulcoder.domain.studyapplication.event.ApplicationStatusChangedEvent; import grep.neogulcoder.domain.studyapplication.repository.ApplicationQueryRepository; import grep.neogulcoder.domain.studyapplication.repository.ApplicationRepository; import grep.neogulcoder.global.exception.business.BusinessException; @@ -71,7 +73,7 @@ public Long createApplication(Long recruitmentPostId, ApplicationCreateRequest r StudyApplication application = request.toEntity(recruitmentPostId, userId); applicationRepository.save(application); - eventPublisher.publishEvent(new StudyApplicationEvent(recruitmentPostId, application.getId())); + eventPublisher.publishEvent(new ApplicationEvent(recruitmentPostId, application.getId())); return application.getId(); } @@ -91,6 +93,8 @@ public void approveApplication(Long applicationId, Long userId) { StudyMember studyMember = StudyMember.createMember(study, application.getUserId()); studyMemberRepository.save(studyMember); study.increaseMemberCount(); + + eventPublisher.publishEvent(new ApplicationStatusChangedEvent(applicationId, AlarmType.STUDY_APPLICATION_APPROVED)); } @Transactional @@ -103,6 +107,8 @@ public void rejectApplication(Long applicationId, Long userId) { validateStatusIsApplying(application); application.reject(); + + eventPublisher.publishEvent(new ApplicationStatusChangedEvent(applicationId, AlarmType.STUDY_APPLICATION_REJECTED)); } private Study findValidStudy(RecruitmentPost post) { From 3ea915ba13170d791f967ac02d34947ca74c3c4e Mon Sep 17 00:00:00 2001 From: hyeunS-P Date: Sun, 27 Jul 2025 02:12:38 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feature:=20=EC=95=8C=EB=9E=8C=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=95=8C=EB=9E=8C?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=EA=B0=84=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/AlarmResponse.java | 28 +++--- .../domain/alram/service/AlarmService.java | 92 ++++++++++--------- 2 files changed, 66 insertions(+), 54 deletions(-) diff --git a/src/main/java/grep/neogulcoder/domain/alram/controller/dto/response/AlarmResponse.java b/src/main/java/grep/neogulcoder/domain/alram/controller/dto/response/AlarmResponse.java index df200ed9..8e35f770 100644 --- a/src/main/java/grep/neogulcoder/domain/alram/controller/dto/response/AlarmResponse.java +++ b/src/main/java/grep/neogulcoder/domain/alram/controller/dto/response/AlarmResponse.java @@ -2,6 +2,7 @@ import grep.neogulcoder.domain.alram.type.AlarmType; import grep.neogulcoder.domain.alram.type.DomainType; +import java.time.LocalDateTime; import lombok.Builder; import lombok.Data; @@ -22,22 +23,26 @@ public class AlarmResponse { private boolean checked; - public static AlarmResponse toResponse(Long id, Long receiverUserId, AlarmType alarmType, DomainType domainType, - Long domainId, String message, boolean checked) { + private LocalDateTime createdDate; + + public static AlarmResponse toResponse(Long id, Long receiverUserId, AlarmType alarmType, + DomainType domainType, + Long domainId, String message, boolean checked, LocalDateTime createdDate) { return AlarmResponse.builder() - .id(id) - .receiverUserId(receiverUserId) - .alarmType(alarmType) - .domainType(domainType) - .domainId(domainId) - .message(message) - .checked(checked) - .build(); + .id(id) + .receiverUserId(receiverUserId) + .alarmType(alarmType) + .domainType(domainType) + .domainId(domainId) + .message(message) + .checked(checked) + .createdDate(createdDate) + .build(); } @Builder private AlarmResponse(Long id, Long receiverUserId, AlarmType alarmType, DomainType domainType, - Long domainId, String message, boolean checked) { + Long domainId, String message, boolean checked, LocalDateTime createdDate) { this.id = id; this.receiverUserId = receiverUserId; this.alarmType = alarmType; @@ -45,5 +50,6 @@ private AlarmResponse(Long id, Long receiverUserId, AlarmType alarmType, DomainT this.domainId = domainId; this.message = message; this.checked = checked; + this.createdDate = createdDate; } } diff --git a/src/main/java/grep/neogulcoder/domain/alram/service/AlarmService.java b/src/main/java/grep/neogulcoder/domain/alram/service/AlarmService.java index 0d39dcd7..b232fa61 100644 --- a/src/main/java/grep/neogulcoder/domain/alram/service/AlarmService.java +++ b/src/main/java/grep/neogulcoder/domain/alram/service/AlarmService.java @@ -19,16 +19,15 @@ import grep.neogulcoder.global.exception.business.BusinessException; import grep.neogulcoder.global.exception.business.NotFoundException; import grep.neogulcoder.global.provider.finder.MessageFinder; - import java.util.List; - import lombok.RequiredArgsConstructor; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.*; -import static grep.neogulcoder.domain.studyapplication.exception.code.ApplicationErrorCode.*; +import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.STUDY_LEADER_NOT_FOUND; +import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.STUDY_NOT_FOUND; +import static grep.neogulcoder.domain.studyapplication.exception.code.ApplicationErrorCode.APPLICATION_PARTICIPANT_LIMIT_EXCEEDED; @Service @RequiredArgsConstructor @@ -42,52 +41,55 @@ public class AlarmService { private final StudyMemberRepository studyMemberRepository; @Transactional - public void saveAlarm(Long receiverId, AlarmType alarmType, DomainType domainType, Long domainId) { + public void saveAlarm(Long receiverId, AlarmType alarmType, DomainType domainType, + Long domainId) { String message = messageFinder.findMessage(alarmType, domainType, domainId); alarmRepository.save(Alarm.init(alarmType, receiverId, domainType, domainId, message)); } public List getAllUncheckedAlarms(Long receiverUserId) { return alarmRepository.findAllByReceiverUserIdAndCheckedFalse(receiverUserId).stream() - .map(alarm -> AlarmResponse.toResponse( - alarm.getId(), - alarm.getReceiverUserId(), - alarm.getAlarmType(), - alarm.getDomainType(), - alarm.getDomainId(), - alarm.getMessage(), - alarm.isChecked())) - .toList(); + .map(alarm -> AlarmResponse.toResponse( + alarm.getId(), + alarm.getReceiverUserId(), + alarm.getAlarmType(), + alarm.getDomainType(), + alarm.getDomainId(), + alarm.getMessage(), + alarm.isChecked(), + alarm.getCreatedDate())) + .toList(); } public List getAllAlarms(Long receiverUserId) { return alarmRepository.findAllByReceiverUserId(receiverUserId).stream() - .map(alarm -> AlarmResponse.toResponse( - alarm.getId(), - alarm.getReceiverUserId(), - alarm.getAlarmType(), - alarm.getDomainType(), - alarm.getDomainId(), - alarm.getMessage(), - alarm.isChecked())) - .toList(); + .map(alarm -> AlarmResponse.toResponse( + alarm.getId(), + alarm.getReceiverUserId(), + alarm.getAlarmType(), + alarm.getDomainType(), + alarm.getDomainId(), + alarm.getMessage(), + alarm.isChecked(), + alarm.getCreatedDate())) + .toList(); } @Transactional public void checkAllAlarmWithoutInvite(Long receiverUserId) { List alarms = alarmRepository.findAllByReceiverUserIdAndCheckedFalse(receiverUserId); alarms.stream() - .filter(alarm -> alarm.getAlarmType() != AlarmType.INVITE) - .forEach(Alarm::checkAlarm); + .filter(alarm -> alarm.getAlarmType() != AlarmType.INVITE) + .forEach(Alarm::checkAlarm); } @EventListener public void handleStudyInviteEvent(StudyInviteEvent event) { saveAlarm( - event.targetUserId(), - AlarmType.INVITE, - DomainType.STUDY, - event.studyId() + event.targetUserId(), + AlarmType.INVITE, + DomainType.STUDY, + event.studyId() ); } @@ -111,15 +113,16 @@ public void rejectInvite(Long alarmId) { @EventListener public void handleStudyExtendEvent(StudyExtendEvent event) { - List members = studyMemberRepository.findAllByStudyIdAndActivatedTrue(event.studyId()); + List members = studyMemberRepository.findAllByStudyIdAndActivatedTrue( + event.studyId()); for (StudyMember member : members) { if (!member.isLeader()) { saveAlarm( - member.getUserId(), - AlarmType.STUDY_EXTEND, - DomainType.STUDY, - event.studyId() + member.getUserId(), + AlarmType.STUDY_EXTEND, + DomainType.STUDY, + event.studyId() ); } } @@ -127,20 +130,22 @@ public void handleStudyExtendEvent(StudyExtendEvent event) { @EventListener public void handleStudyExtensionReminderEvent(StudyExtensionReminderEvent event) { - StudyMember leader = studyMemberRepository.findByStudyIdAndRoleAndActivatedTrue(event.studyId(), StudyMemberRole.LEADER) - .orElseThrow(() -> new BusinessException(STUDY_LEADER_NOT_FOUND)); + StudyMember leader = studyMemberRepository.findByStudyIdAndRoleAndActivatedTrue( + event.studyId(), StudyMemberRole.LEADER) + .orElseThrow(() -> new BusinessException(STUDY_LEADER_NOT_FOUND)); saveAlarm( - leader.getUserId(), - AlarmType.STUDY_EXTENSION_REMINDER, - DomainType.STUDY, - event.studyId() + leader.getUserId(), + AlarmType.STUDY_EXTENSION_REMINDER, + DomainType.STUDY, + event.studyId() ); } @EventListener public void handleTimeVotePeriodCreatedEvent(TimeVotePeriodCreatedEvent event) { - List members = studyMemberRepository.findAllByStudyIdAndActivatedTrue(event.studyId()); + List members = studyMemberRepository.findAllByStudyIdAndActivatedTrue( + event.studyId()); for (StudyMember member : members) { if (!member.getUserId().equals(event.excludedUserId())) { @@ -155,12 +160,13 @@ public void handleTimeVotePeriodCreatedEvent(TimeVotePeriodCreatedEvent event) { } private Alarm findValidAlarm(Long alarmId) { - return alarmRepository.findById(alarmId).orElseThrow(() -> new NotFoundException(AlarmErrorCode.ALARM_NOT_FOUND)); + return alarmRepository.findById(alarmId) + .orElseThrow(() -> new NotFoundException(AlarmErrorCode.ALARM_NOT_FOUND)); } private Study findValidStudy(Long studyId) { return studyRepository.findById(studyId) - .orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND)); + .orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND)); } private void validateParticipantStudyLimit(Long userId) { From 8746562455b7f152cc0cca67ae14413490744461 Mon Sep 17 00:00:00 2001 From: dbrkdgus00 Date: Sun, 27 Jul 2025 12:17:16 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[EA3-111]=20feature=20:=20=EA=B3=BC?= =?UTF-8?q?=EA=B1=B0=20=EC=B1=84=ED=8C=85=20=EC=9D=91=EB=8B=B5=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20dto=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GroupChatRestController.java | 9 ++-- .../GroupChatRestSpecification.java | 3 +- .../response/ChatMessagePagingResponse.java | 44 +++++++++++++++++++ .../GroupChatMessageRepository.java | 3 +- .../groupchat/service/GroupChatService.java | 11 ++--- 5 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 src/main/java/grep/neogulcoder/domain/groupchat/controller/dto/response/ChatMessagePagingResponse.java diff --git a/src/main/java/grep/neogulcoder/domain/groupchat/controller/GroupChatRestController.java b/src/main/java/grep/neogulcoder/domain/groupchat/controller/GroupChatRestController.java index 99b02939..41865c02 100644 --- a/src/main/java/grep/neogulcoder/domain/groupchat/controller/GroupChatRestController.java +++ b/src/main/java/grep/neogulcoder/domain/groupchat/controller/GroupChatRestController.java @@ -1,5 +1,6 @@ package grep.neogulcoder.domain.groupchat.controller; +import grep.neogulcoder.domain.groupchat.controller.dto.response.ChatMessagePagingResponse; import grep.neogulcoder.domain.groupchat.controller.dto.response.GroupChatMessageResponseDto; import grep.neogulcoder.domain.groupchat.service.GroupChatService; import grep.neogulcoder.global.response.ApiResponse; @@ -17,15 +18,13 @@ public class GroupChatRestController implements GroupChatRestSpecification { // 과거 채팅 메시지 페이징 조회 (무한 스크롤용) @Override @GetMapping("/study/{studyId}/messages") - public ApiResponse> getMessages( + public ApiResponse getMessages( @PathVariable("studyId") Long studyId, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size ) { // 서비스에서 페이징된 메시지 조회 - PageResponse pageResponse = - groupChatService.getMessages(studyId, page, size); - - return ApiResponse.success(pageResponse); + ChatMessagePagingResponse response = groupChatService.getMessages(studyId, page, size); + return ApiResponse.success(response); } } diff --git a/src/main/java/grep/neogulcoder/domain/groupchat/controller/GroupChatRestSpecification.java b/src/main/java/grep/neogulcoder/domain/groupchat/controller/GroupChatRestSpecification.java index eddcb745..2a39b015 100644 --- a/src/main/java/grep/neogulcoder/domain/groupchat/controller/GroupChatRestSpecification.java +++ b/src/main/java/grep/neogulcoder/domain/groupchat/controller/GroupChatRestSpecification.java @@ -1,5 +1,6 @@ package grep.neogulcoder.domain.groupchat.controller; +import grep.neogulcoder.domain.groupchat.controller.dto.response.ChatMessagePagingResponse; import grep.neogulcoder.domain.groupchat.controller.dto.response.GroupChatMessageResponseDto; import grep.neogulcoder.global.response.ApiResponse; import grep.neogulcoder.global.response.PageResponse; @@ -73,7 +74,7 @@ public interface GroupChatRestSpecification { """ ) - ApiResponse> getMessages( + ApiResponse getMessages( @Parameter(description = "스터디 ID", example = "1") @PathVariable("studyId") Long studyId, diff --git a/src/main/java/grep/neogulcoder/domain/groupchat/controller/dto/response/ChatMessagePagingResponse.java b/src/main/java/grep/neogulcoder/domain/groupchat/controller/dto/response/ChatMessagePagingResponse.java new file mode 100644 index 00000000..d10a0f2c --- /dev/null +++ b/src/main/java/grep/neogulcoder/domain/groupchat/controller/dto/response/ChatMessagePagingResponse.java @@ -0,0 +1,44 @@ +package grep.neogulcoder.domain.groupchat.controller.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.domain.Page; + +import java.util.List; + +@Getter +public class ChatMessagePagingResponse { + + @Schema(description = "채팅 메시지 목록") + private final List content; + + @Schema(description = "현재 페이지 번호", example = "0") + private final int currentPage; + + @Schema(description = "페이지 크기", example = "20") + private final int size; + + @Schema(description = "전체 페이지 수", example = "5") + private final int totalPages; + + @Schema(description = "전체 메시지 수", example = "100") + private final long totalElements; + + @Schema(description = "다음 페이지 존재 여부", example = "true") + private final boolean hasNext; + + @Builder + private ChatMessagePagingResponse(Page page) { + this.content = page.getContent(); + this.currentPage = page.getNumber(); + this.size = page.getSize(); + this.totalPages = page.getTotalPages(); + this.totalElements = page.getTotalElements(); + this.hasNext = page.hasNext(); + } + + public static ChatMessagePagingResponse of(Page page) { + return new ChatMessagePagingResponse(page); + } +} diff --git a/src/main/java/grep/neogulcoder/domain/groupchat/repository/GroupChatMessageRepository.java b/src/main/java/grep/neogulcoder/domain/groupchat/repository/GroupChatMessageRepository.java index b9117608..d18e8ae7 100644 --- a/src/main/java/grep/neogulcoder/domain/groupchat/repository/GroupChatMessageRepository.java +++ b/src/main/java/grep/neogulcoder/domain/groupchat/repository/GroupChatMessageRepository.java @@ -11,7 +11,8 @@ public interface GroupChatMessageRepository extends JpaRepository findMessagesByRoomIdAsc(@Param("roomId") Long roomId, Pageable pageable); diff --git a/src/main/java/grep/neogulcoder/domain/groupchat/service/GroupChatService.java b/src/main/java/grep/neogulcoder/domain/groupchat/service/GroupChatService.java index 2093b905..2a013089 100644 --- a/src/main/java/grep/neogulcoder/domain/groupchat/service/GroupChatService.java +++ b/src/main/java/grep/neogulcoder/domain/groupchat/service/GroupChatService.java @@ -1,5 +1,6 @@ package grep.neogulcoder.domain.groupchat.service; +import grep.neogulcoder.domain.groupchat.controller.dto.response.ChatMessagePagingResponse; import grep.neogulcoder.domain.groupchat.entity.GroupChatMessage; import grep.neogulcoder.domain.groupchat.entity.GroupChatRoom; import grep.neogulcoder.domain.groupchat.controller.dto.requset.GroupChatMessageRequestDto; @@ -63,7 +64,8 @@ public GroupChatMessageResponseDto saveMessage(GroupChatMessageRequestDto reques } // 과거 채팅 메시지 페이징 조회 (무한 스크롤용) - public PageResponse getMessages(Long studyId, int page, int size) { + @Transactional(readOnly = true) + public ChatMessagePagingResponse getMessages(Long studyId, int page, int size) { GroupChatRoom room = roomRepository.findByStudyId(studyId) .orElseThrow(() -> new IllegalArgumentException("채팅방이 존재하지 않습니다.")); @@ -81,12 +83,7 @@ public PageResponse getMessages(Long studyId, int p return GroupChatMessageResponseDto.from(message, sender); }); - // PageResponse로 감싸서 반환 - return new PageResponse<>( - "/api/chat/study/" + studyId + "/messages", - messagePage, - 5 // 페이지 버튼 개수 - ); + return ChatMessagePagingResponse.of(messagePage); } From f1241d7113c2cb086ce8da6854327058250ca3a3 Mon Sep 17 00:00:00 2001 From: Tokwasp Date: Sun, 27 Jul 2025 14:43:25 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[EA3-190]=20refactor:=20=EB=AA=A8=EC=A7=91?= =?UTF-8?q?=EA=B8=80=20=EC=88=98=EC=A0=95=20=EB=AA=A8=EC=A7=91=20=EA=B8=B0?= =?UTF-8?q?=EA=B0=84=20=ED=95=84=EB=93=9C=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/RecruitmentPostUpdateRequest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/grep/neogulcoder/domain/recruitment/post/controller/dto/request/RecruitmentPostUpdateRequest.java b/src/main/java/grep/neogulcoder/domain/recruitment/post/controller/dto/request/RecruitmentPostUpdateRequest.java index c8313544..983d78d3 100644 --- a/src/main/java/grep/neogulcoder/domain/recruitment/post/controller/dto/request/RecruitmentPostUpdateRequest.java +++ b/src/main/java/grep/neogulcoder/domain/recruitment/post/controller/dto/request/RecruitmentPostUpdateRequest.java @@ -27,14 +27,14 @@ public class RecruitmentPostUpdateRequest { @Future @Schema(example = "2025-07-21T23:59:59", description = "모집글 마감 기간") - private LocalDateTime expiredDateTime; + private LocalDateTime expiredDate; @Builder - private RecruitmentPostUpdateRequest(String subject, String content, int recruitmentCount, LocalDateTime expiredDateTime) { + private RecruitmentPostUpdateRequest(String subject, String content, int recruitmentCount, LocalDateTime expiredDate) { this.subject = subject; this.content = content; this.recruitmentCount = recruitmentCount; - this.expiredDateTime = expiredDateTime; + this.expiredDate = expiredDate; } public RecruitmentPostUpdateServiceRequest toServiceRequest() { @@ -42,7 +42,7 @@ public RecruitmentPostUpdateServiceRequest toServiceRequest() { .subject(this.subject) .content(this.content) .recruitmentCount(this.recruitmentCount) - .expiredDateTime(this.expiredDateTime) + .expiredDateTime(this.expiredDate) .build(); }