Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
bae97c7
[EA3-134] fix : 누락된 알림 타입 추가
endorsement0912 Jul 25, 2025
0b1ce38
Merge pull request #226 from prgrms-web-devcourse-final-project/featu…
endorsement0912 Jul 25, 2025
eb44c4f
[EA3-134] chore : Time Vote 관련 엔티티 위치 변경
endorsement0912 Jul 25, 2025
be3dd48
[EA3-134] refactor : 알림이 전송된 팀원 멤버 ID 목록 삭제
endorsement0912 Jul 25, 2025
9ce5df2
[EA3-134] refactor : 기존의 스터디 모임 일정 조율 알림 서비스를 Event 객체로 분리
endorsement0912 Jul 25, 2025
233a26b
[EA3-163] fix: 스터디 게시글 수정 문제 해결
Tokwasp Jul 25, 2025
3b8acc9
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
Tokwasp Jul 25, 2025
ea8390b
[EA3-138] refactor: 모집글 댓글 작성 URL 변경
Tokwasp Jul 25, 2025
894a82d
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
Tokwasp Jul 25, 2025
87c7e40
Merge branch 'feature/EA3-163-study-post-implement' of https://github…
Tokwasp Jul 25, 2025
602c6cb
[EA3-163] refactor: 게시글 조회시 댓글 ID 필드명 변경
Tokwasp Jul 25, 2025
75ec874
[EA3-188] feature: 내 스터디 신청 목록 조회 recruitmentPostId 필드 추가
pia01190 Jul 26, 2025
e64cd95
Merge pull request #227 from prgrms-web-devcourse-final-project/featu…
pia01190 Jul 26, 2025
5c6dea5
Merge pull request #230 from prgrms-web-devcourse-final-project/featu…
pia01190 Jul 26, 2025
793161d
feature: 알림 전체 조회 엔드포인트 추가 및 읽음 여부 반환 추기
hyeunS-P Jul 26, 2025
db6b06f
Merge pull request #231 from prgrms-web-devcourse-final-project/featu…
hyeunS-P Jul 26, 2025
d98d5b4
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
pia01190 Jul 26, 2025
b189682
[EA3-188] fix: TimeVoteQueryRepository import문 오류 수정
pia01190 Jul 26, 2025
086ba29
Merge pull request #235 from prgrms-web-devcourse-final-project/featu…
pia01190 Jul 26, 2025
0cc34db
Merge pull request #234 from prgrms-web-devcourse-final-project/featu…
pia01190 Jul 26, 2025
0bceb5f
[EA3-190] feature: 모집글 더미데이터 생성일 추가
Tokwasp Jul 26, 2025
24490e3
[EA3-190] feature: 모집글 페이징 조회 요청 전체 권한 변경
Tokwasp Jul 26, 2025
641d94f
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
Tokwasp Jul 26, 2025
2185f35
[EA3-191] refactor: 스터디 연장 반환값 studyId로 변경
pia01190 Jul 26, 2025
76798b0
Merge pull request #237 from prgrms-web-devcourse-final-project/featu…
endorsement0912 Jul 26, 2025
14a6443
Merge pull request #239 from prgrms-web-devcourse-final-project/refac…
endorsement0912 Jul 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@ public class AlarmController implements AlarmSpecification {
private final AlarmService alarmService;

@GetMapping("/my")
public ApiResponse<List<AlarmResponse>> getAllAlarm(
@AuthenticationPrincipal Principal userDetails) {
public ApiResponse<List<AlarmResponse>> getAllAlarms(@AuthenticationPrincipal Principal userDetails) {
return ApiResponse.success(alarmService.getAllAlarms(userDetails.getUserId()));
}

@GetMapping("/unchecked/my")
public ApiResponse<List<AlarmResponse>> getAllUncheckedAlarm(
@AuthenticationPrincipal Principal userDetails) {
return ApiResponse.success(alarmService.getAllUncheckedAlarms(userDetails.getUserId()));
}

@PostMapping("/my/check/all")
public ApiResponse<Void> checkAlarm(@AuthenticationPrincipal Principal userDetails) {
alarmService.checkAllAlarm(userDetails.getUserId());
alarmService.checkAllAlarmWithoutInvite(userDetails.getUserId());
return ApiResponse.noContent();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
@Tag(name = "Alarm", description = "알림 관련 API 명세")
public interface AlarmSpecification {

@Operation(summary = "내 알림 목록 조회", description = "로그인한 사용자의 알림 목록을 조회합니다.")
ApiResponse<List<AlarmResponse>> getAllAlarm(@AuthenticationPrincipal Principal userDetails);
@Operation(summary = "읽음 여부 관련 없이 내 전체 알림 목록 조회", description = "로그인한 사용자의 전체 알림 목록을 조회합니다.")
ApiResponse<List<AlarmResponse>> getAllAlarms(@AuthenticationPrincipal Principal userDetails);

@Operation(summary = "내 읽지 않은 알림 목록 조회", description = "로그인한 사용자의 읽지 않은 알림 목록을 조회합니다.")
ApiResponse<List<AlarmResponse>> getAllUncheckedAlarm(@AuthenticationPrincipal Principal userDetails);

@Operation(summary = "내 알림 전체 읽음 처리", description = "로그인한 사용자의 모든 알림을 읽음 처리합니다.")
ApiResponse<Void> checkAlarm(@AuthenticationPrincipal Principal userDetails);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package grep.neogulcoder.domain.alram.controller.dto.response;

import grep.neogulcoder.domain.alram.entity.Alarm;
import grep.neogulcoder.domain.alram.type.AlarmType;
import grep.neogulcoder.domain.alram.type.DomainType;
import lombok.Builder;
Expand All @@ -21,26 +20,30 @@ public class AlarmResponse {

private String message;

private boolean checked;

public static AlarmResponse toResponse(Long id, Long receiverUserId, AlarmType alarmType, DomainType domainType,
Long domainId, String message) {
Long domainId, String message, boolean checked) {
return AlarmResponse.builder()
.id(id)
.receiverUserId(receiverUserId)
.alarmType(alarmType)
.domainType(domainType)
.domainId(domainId)
.message(message)
.build();
.id(id)
.receiverUserId(receiverUserId)
.alarmType(alarmType)
.domainType(domainType)
.domainId(domainId)
.message(message)
.checked(checked)
.build();
}

@Builder
private AlarmResponse(Long id, Long receiverUserId, AlarmType alarmType, DomainType domainType,
Long domainId, String message) {
Long domainId, String message, boolean checked) {
this.id = id;
this.receiverUserId = receiverUserId;
this.alarmType = alarmType;
this.domainType = domainType;
this.domainId = domainId;
this.message = message;
this.checked = checked;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@

public interface AlarmRepository extends JpaRepository<Alarm, Long> {
List<Alarm> findAllByReceiverUserIdAndCheckedFalse(Long receiverUserId);
List<Alarm> findAllByReceiverUserId(Long receiverUserId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
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.timevote.event.TimeVotePeriodCreatedEvent;
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;
Expand All @@ -44,33 +47,47 @@ public void saveAlarm(Long receiverId, AlarmType alarmType, DomainType domainTyp
alarmRepository.save(Alarm.init(alarmType, receiverId, domainType, domainId, message));
}

public List<AlarmResponse> getAllAlarms(Long receiverUserId) {
public List<AlarmResponse> getAllUncheckedAlarms(Long receiverUserId) {
return alarmRepository.findAllByReceiverUserIdAndCheckedFalse(receiverUserId).stream()
.map(alarm -> AlarmResponse.toResponse(
alarm.getId(),
alarm.getReceiverUserId(),
alarm.getAlarmType(),
alarm.getDomainType(),
alarm.getDomainId(),
alarm.getMessage()))
.toList();
.map(alarm -> AlarmResponse.toResponse(
alarm.getId(),
alarm.getReceiverUserId(),
alarm.getAlarmType(),
alarm.getDomainType(),
alarm.getDomainId(),
alarm.getMessage(),
alarm.isChecked()))
.toList();
}

public List<AlarmResponse> 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();
}

@Transactional
public void checkAllAlarm(Long receiverUserId) {
public void checkAllAlarmWithoutInvite(Long receiverUserId) {
List<Alarm> 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()
);
}

Expand All @@ -82,7 +99,7 @@ public void acceptInvite(Long targetUserId, Long alarmId) {
Alarm alarm = findValidAlarm(alarmId);
Long studyId = alarm.getDomainId();
Study study = findValidStudy(studyId);
StudyMember.createMember(study,targetUserId);
StudyMember.createMember(study, targetUserId);
alarm.checkAlarm();
}

Expand All @@ -99,10 +116,10 @@ public void handleStudyExtendEvent(StudyExtendEvent event) {
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()
);
}
}
Expand All @@ -111,23 +128,39 @@ 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));
.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<StudyMember> members = studyMemberRepository.findAllByStudyIdAndActivatedTrue(event.studyId());

for (StudyMember member : members) {
if (!member.getUserId().equals(event.excludedUserId())) {
saveAlarm(
member.getUserId(),
AlarmType.TIME_VOTE_REQUEST,
DomainType.TIME_VOTE,
event.studyId()
);
}
}
}

private Alarm findValidAlarm(Long alarmId) {
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package grep.neogulcoder.domain.alram.type;

public enum AlarmType {
INVITE, STUDY_EXTEND, STUDY_EXTENSION_REMINDER
INVITE, STUDY_EXTEND, STUDY_EXTENSION_REMINDER, TIME_VOTE_REQUEST
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,30 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RequestMapping("/recruitment-posts/comments")
@RequestMapping("/recruitment-posts")
@RequiredArgsConstructor
@RestController
public class RecruitmentPostCommentController implements RecruitmentPostCommentSpecification {

private final RecruitmentPostCommentService commentService;

@PostMapping
public ApiResponse<Long> save(@RequestBody @Valid RecruitmentCommentSaveRequest request,
@PostMapping("/{post-id}/comments")
public ApiResponse<Long> save(@PathVariable("post-id") long postId,
@RequestBody @Valid RecruitmentCommentSaveRequest request,
@AuthenticationPrincipal Principal userDetails) {
long commentId = commentService.save(request, userDetails.getUserId());
long commentId = commentService.save(postId, request, userDetails.getUserId());
return ApiResponse.success(commentId);
}

@PutMapping("/{comment-id}")
@PutMapping("/comments/{comment-id}")
public ApiResponse<Void> update(@PathVariable("comment-id") long commentId,
@RequestBody RecruitmentCommentUpdateRequest request,
@AuthenticationPrincipal Principal userDetails) {
commentService.update(request, commentId, userDetails.getUserId());
return ApiResponse.noContent();
}

@DeleteMapping("/{comment-id}")
@DeleteMapping("/comments/{comment-id}")
public ApiResponse<Void> delete(@PathVariable("comment-id") long commentId,
@AuthenticationPrincipal Principal userDetails) {
commentService.delete(commentId, userDetails.getUserId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
public interface RecruitmentPostCommentSpecification {

@Operation(summary = "모집글 댓글 작성", description = "모집글에 대한 댓글을 작성 합니다.")
ApiResponse<Long> save(RecruitmentCommentSaveRequest request, Principal userDetails);
ApiResponse<Long> save(long postId, RecruitmentCommentSaveRequest request, Principal userDetails);

@Operation(summary = "모집글 댓글 수정", description = "모집글에 대한 댓글을 수정 합니다.")
ApiResponse<Void> update(long commentId, RecruitmentCommentUpdateRequest request, Principal userDetails);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,13 @@
@Getter
public class RecruitmentCommentSaveRequest {

@Schema(example = "2", description = "모집글 ID")
private long postId;

@Schema(example = "저도 참여 할래요!", description = "모집글 내용")
private String content;

private RecruitmentCommentSaveRequest() {
}

public RecruitmentPostComment toEntity(RecruitmentPost post, long userId){
public RecruitmentPostComment toEntity(RecruitmentPost post, long userId) {
return RecruitmentPostComment.builder()
.recruitmentPost(post)
.userId(userId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ public class RecruitmentPostCommentService {
private final RecruitmentPostCommentQueryRepository commentQueryRepository;

@Transactional
public long save(RecruitmentCommentSaveRequest request, long userId) {
RecruitmentPost post = postRepository.findPostBy(request.getPostId())
public long save(long postId, RecruitmentCommentSaveRequest request, long userId) {
RecruitmentPost post = postRepository.findPostBy(postId)
.orElseThrow(() -> new NotFoundException(NOT_FOUND));

return commentRepository.save(request.toEntity(post, userId)).getId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ public ApiResponse<Void> delegateLeader(@PathVariable("studyId") Long studyId,
}

@PostMapping("/extension")
public ApiResponse<Void> extendStudy(@PathVariable("studyId") Long studyId,
public ApiResponse<Long> extendStudy(@PathVariable("studyId") Long studyId,
@RequestBody @Valid ExtendStudyRequest request,
@AuthenticationPrincipal Principal userDetails) {
studyManagementService.extendStudy(studyId, request, userDetails.getUserId());
return ApiResponse.noContent();
Long extendStudyId = studyManagementService.extendStudy(studyId, request, userDetails.getUserId());
return ApiResponse.success(extendStudyId);
}

@PostMapping("/extension/participations")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public interface StudyManagementSpecification {
ApiResponse<Void> delegateLeader(Long studyId, DelegateLeaderRequest request, Principal userDetails);

@Operation(summary = "스터디 연장", description = "스터디장이 스터디를 연장합니다.")
ApiResponse<Void> extendStudy(Long studyId, ExtendStudyRequest request, Principal userDetails);
ApiResponse<Long> extendStudy(Long studyId, ExtendStudyRequest request, Principal userDetails);

@Operation(summary = "연장 스터디 참여", description = "스터디원이 연장된 스터디에 참여합니다.")
ApiResponse<Void> registerExtensionParticipation(Long studyId, Principal userDetails);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public void deleteUserFromStudies(Long userId) {
}

@Transactional
public void extendStudy(Long studyId, ExtendStudyRequest request, Long userId) {
public Long extendStudy(Long studyId, ExtendStudyRequest request, Long userId) {
Study originStudy = findValidStudy(studyId);

StudyMember leader = findValidStudyMember(studyId, userId);
Expand All @@ -141,6 +141,8 @@ public void extendStudy(Long studyId, ExtendStudyRequest request, Long userId) {
studyMemberRepository.save(extendedLeader);

eventPublisher.publishEvent(new StudyExtendEvent(originStudy.getId()));

return extendedStudy.getId();
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class MyApplicationPagingResponse {
@Schema(
description = "내가 신청한 스터디 목록",
example = "[{" +
"\"applicationId\": 1," +
"\"recruitmentPostId\": 1," +
"\"name\": \"자바 스터디\"," +
"\"leaderNickname\": \"너굴\"," +
"\"capacity\": 4," +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
@Getter
public class MyApplicationResponse {

@Schema(description = "신청 번호", example = "1")
private Long applicationId;
@Schema(description = "모집글 번호", example = "1")
private Long recruitmentPostId;

@Schema(description = "스터디 이름", example = "자바 스터디")
private String name;
Expand Down Expand Up @@ -49,9 +49,9 @@ public class MyApplicationResponse {
private ApplicationStatus status;

@QueryProjection
public MyApplicationResponse(Long applicationId, String name, String leaderNickname, int capacity, int currentCount, LocalDateTime startDate,
public MyApplicationResponse(Long recruitmentPostId, String name, String leaderNickname, int capacity, int currentCount, LocalDateTime startDate,
String imageUrl,String introduction, Category category, StudyType studyType, boolean isRead, ApplicationStatus status) {
this.applicationId = applicationId;
this.recruitmentPostId = recruitmentPostId;
this.name = name;
this.leaderNickname = leaderNickname;
this.capacity = capacity;
Expand Down
Loading