Skip to content

Commit 09961aa

Browse files
Merge pull request #214 from prgrms-web-devcourse-final-project/feature/EA3-178-study-invite
[EA3-178]Feature:스터디 초대 및 수락/거절 구현
2 parents 25f59af + 9f47294 commit 09961aa

File tree

14 files changed

+221
-50
lines changed

14 files changed

+221
-50
lines changed

src/main/java/grep/neogulcoder/domain/alram/controller/AlarmController.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package grep.neogulcoder.domain.alram.controller;
22

3-
import grep.neogulcoder.domain.alram.entity.Alarm;
3+
import grep.neogulcoder.domain.alram.controller.dto.response.AlarmResponse;
44
import grep.neogulcoder.domain.alram.service.AlarmService;
55
import grep.neogulcoder.global.auth.Principal;
66
import grep.neogulcoder.global.response.ApiResponse;
77
import java.util.List;
88
import lombok.RequiredArgsConstructor;
99
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1010
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.PathVariable;
1112
import org.springframework.web.bind.annotation.PostMapping;
1213
import org.springframework.web.bind.annotation.RequestMapping;
1314
import org.springframework.web.bind.annotation.RestController;
@@ -20,7 +21,8 @@ public class AlarmController implements AlarmSpecification {
2021
private final AlarmService alarmService;
2122

2223
@GetMapping("/my")
23-
public ApiResponse<List<Alarm>> getAlarm(@AuthenticationPrincipal Principal userDetails) {
24+
public ApiResponse<List<AlarmResponse>> getAllAlarm(
25+
@AuthenticationPrincipal Principal userDetails) {
2426
return ApiResponse.success(alarmService.getAllAlarms(userDetails.getUserId()));
2527
}
2628

@@ -30,4 +32,17 @@ public ApiResponse<Void> checkAlarm(@AuthenticationPrincipal Principal userDetai
3032
return ApiResponse.noContent();
3133
}
3234

35+
@PostMapping("/choose/{alarmId}/response")
36+
public ApiResponse<Void> respondToInvite(@AuthenticationPrincipal Principal principal,
37+
@PathVariable Long alarmId,
38+
boolean accepted) {
39+
if (accepted) {
40+
alarmService.acceptInvite(principal.getUserId(), alarmId);
41+
} else {
42+
alarmService.rejectInvite(principal.getUserId());
43+
}
44+
45+
return ApiResponse.success(accepted ? "스터디 초대를 수락했습니다." : "스터디 초대를 거절했습니다.");
46+
}
47+
3348
}

src/main/java/grep/neogulcoder/domain/alram/controller/AlarmSpecification.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
package grep.neogulcoder.domain.alram.controller;
22

3-
4-
import grep.neogulcoder.domain.alram.entity.Alarm;
3+
import grep.neogulcoder.domain.alram.controller.dto.response.AlarmResponse;
54
import grep.neogulcoder.global.auth.Principal;
65
import grep.neogulcoder.global.response.ApiResponse;
76
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.tags.Tag;
88
import java.util.List;
99
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1010

11+
@Tag(name = "Alarm", description = "알림 관련 API 명세")
1112
public interface AlarmSpecification {
1213

1314
@Operation(summary = "내 알림 목록 조회", description = "로그인한 사용자의 알림 목록을 조회합니다.")
14-
ApiResponse<List<Alarm>> getAlarm(@AuthenticationPrincipal Principal userDetails);
15+
ApiResponse<List<AlarmResponse>> getAllAlarm(@AuthenticationPrincipal Principal userDetails);
1516

1617
@Operation(summary = "내 알림 전체 읽음 처리", description = "로그인한 사용자의 모든 알림을 읽음 처리합니다.")
1718
ApiResponse<Void> checkAlarm(@AuthenticationPrincipal Principal userDetails);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package grep.neogulcoder.domain.alram.controller.dto.response;
2+
3+
import grep.neogulcoder.domain.alram.entity.Alarm;
4+
import grep.neogulcoder.domain.alram.type.AlarmType;
5+
import grep.neogulcoder.domain.alram.type.DomainType;
6+
import lombok.Builder;
7+
import lombok.Data;
8+
9+
@Data
10+
public class AlarmResponse {
11+
12+
private Long id;
13+
14+
private Long receiverUserId;
15+
16+
private AlarmType alarmType;
17+
18+
private DomainType domainType;
19+
20+
private Long domainId;
21+
22+
private String message;
23+
24+
public static AlarmResponse toResponse(Long id, Long receiverUserId, AlarmType alarmType, DomainType domainType,
25+
Long domainId, String message) {
26+
return AlarmResponse.builder()
27+
.id(id)
28+
.receiverUserId(receiverUserId)
29+
.alarmType(alarmType)
30+
.domainType(domainType)
31+
.domainId(domainId)
32+
.message(message)
33+
.build();
34+
}
35+
36+
@Builder
37+
private AlarmResponse(Long id, Long receiverUserId, AlarmType alarmType, DomainType domainType,
38+
Long domainId, String message) {
39+
this.id = id;
40+
this.receiverUserId = receiverUserId;
41+
this.alarmType = alarmType;
42+
this.domainType = domainType;
43+
this.domainId = domainId;
44+
this.message = message;
45+
}
46+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package grep.neogulcoder.domain.alram.exception.code;
2+
3+
import grep.neogulcoder.global.response.code.ErrorCode;
4+
import lombok.Getter;
5+
import org.springframework.http.HttpStatus;
6+
7+
@Getter
8+
public enum AlarmErrorCode implements ErrorCode {
9+
10+
ALARM_NOT_FOUND("A001",HttpStatus.NOT_FOUND,"알람을 찾을 수 없습니다.");
11+
12+
13+
private final String code;
14+
private final HttpStatus status;
15+
private final String message;
16+
17+
AlarmErrorCode(String code, HttpStatus status, String message) {
18+
this.code = code;
19+
this.status = status;
20+
this.message = message;
21+
}
22+
}
Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,105 @@
11
package grep.neogulcoder.domain.alram.service;
22

3+
import grep.neogulcoder.domain.alram.controller.dto.response.AlarmResponse;
34
import grep.neogulcoder.domain.alram.entity.Alarm;
5+
import grep.neogulcoder.domain.alram.exception.code.AlarmErrorCode;
46
import grep.neogulcoder.domain.alram.repository.AlarmRepository;
57
import grep.neogulcoder.domain.alram.type.AlarmType;
68
import grep.neogulcoder.domain.alram.type.DomainType;
9+
import grep.neogulcoder.domain.study.Study;
10+
import grep.neogulcoder.domain.study.StudyMember;
11+
import grep.neogulcoder.domain.study.event.StudyInviteEvent;
12+
import grep.neogulcoder.domain.study.repository.StudyMemberQueryRepository;
13+
import grep.neogulcoder.domain.study.repository.StudyRepository;
14+
import grep.neogulcoder.global.exception.business.BusinessException;
15+
import grep.neogulcoder.global.exception.business.NotFoundException;
716
import grep.neogulcoder.global.provider.finder.MessageFinder;
817
import java.util.List;
918
import lombok.RequiredArgsConstructor;
19+
import org.springframework.context.event.EventListener;
1020
import org.springframework.stereotype.Service;
1121
import org.springframework.transaction.annotation.Transactional;
1222

23+
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.STUDY_NOT_FOUND;
24+
import static grep.neogulcoder.domain.studyapplication.exception.code.ApplicationErrorCode.APPLICATION_PARTICIPANT_LIMIT_EXCEEDED;
25+
1326
@Service
1427
@RequiredArgsConstructor
1528
@Transactional(readOnly = true)
1629
public class AlarmService {
1730

1831
private final AlarmRepository alarmRepository;
1932
private final MessageFinder messageFinder;
33+
private final StudyRepository studyRepository;
34+
private final StudyMemberQueryRepository studyMemberQueryRepository;
2035

2136
@Transactional
2237
public void saveAlarm(Long receiverId, AlarmType alarmType, DomainType domainType, Long domainId) {
23-
String message = messageFinder.findMessage(alarmType);
38+
String message = messageFinder.findMessage(alarmType, domainType, domainId);
2439
alarmRepository.save(Alarm.init(alarmType, receiverId, domainType, domainId, message));
2540
}
2641

27-
public List<Alarm> getAllAlarms(Long receiverUserId) {
28-
return alarmRepository.findAllByReceiverUserIdAndCheckedFalse(receiverUserId);
42+
public List<AlarmResponse> getAllAlarms(Long receiverUserId) {
43+
return alarmRepository.findAllByReceiverUserIdAndCheckedFalse(receiverUserId).stream()
44+
.map(alarm -> AlarmResponse.toResponse(
45+
alarm.getId(),
46+
alarm.getReceiverUserId(),
47+
alarm.getAlarmType(),
48+
alarm.getDomainType(),
49+
alarm.getDomainId(),
50+
alarm.getMessage()))
51+
.toList();
2952
}
3053

3154
@Transactional
3255
public void checkAllAlarm(Long receiverUserId) {
3356
List<Alarm> alarms = alarmRepository.findAllByReceiverUserIdAndCheckedFalse(receiverUserId);
34-
alarms.forEach(Alarm::checkAlarm);
57+
alarms.stream()
58+
.filter(alarm -> alarm.getAlarmType() != AlarmType.INVITE)
59+
.forEach(Alarm::checkAlarm);
60+
}
61+
62+
@EventListener
63+
public void handleStudyInviteEvent(StudyInviteEvent event) {
64+
saveAlarm(
65+
event.targetUserId(),
66+
AlarmType.INVITE,
67+
DomainType.STUDY,
68+
event.studyId()
69+
);
70+
}
71+
72+
@Transactional
73+
public void acceptInvite(Long targetUserId, Long alarmId) {
74+
75+
validateParticipantStudyLimit(targetUserId);
76+
77+
Alarm alarm = findValidAlarm(alarmId);
78+
Long studyId = alarm.getDomainId();
79+
Study study = findValidStudy(studyId);
80+
StudyMember.createMember(study,targetUserId);
81+
alarm.checkAlarm();
3582
}
3683

84+
@Transactional
85+
public void rejectInvite(Long alarmId) {
86+
Alarm alarm = findValidAlarm(alarmId);
87+
alarm.checkAlarm();
88+
}
89+
90+
private Alarm findValidAlarm(Long alarmId) {
91+
return alarmRepository.findById(alarmId).orElseThrow(() -> new NotFoundException(AlarmErrorCode.ALARM_NOT_FOUND));
92+
}
93+
94+
private Study findValidStudy(Long studyId) {
95+
return studyRepository.findById(studyId)
96+
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
97+
}
98+
99+
private void validateParticipantStudyLimit(Long userId) {
100+
int count = studyMemberQueryRepository.countActiveUnfinishedStudies(userId);
101+
if (count >= 10) {
102+
throw new BusinessException(APPLICATION_PARTICIPANT_LIMIT_EXCEEDED);
103+
}
104+
}
37105
}

src/main/java/grep/neogulcoder/domain/study/controller/StudyManagementController.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,4 @@ public ApiResponse<Void> inviteUser(@PathVariable("studyId") Long studyId, @Auth
6969
return ApiResponse.noContent();
7070
}
7171

72-
@PostMapping("/accept/invite")
73-
public ApiResponse<Void> acceptInvite(@PathVariable("studyId") Long studyId,@AuthenticationPrincipal Principal principal) {
74-
studyManagementService.acceptInvite(studyId, principal.getUserId());
75-
return ApiResponse.success("스터디 초대를 수락했습니다.");
76-
}
7772
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package grep.neogulcoder.domain.study.event;
2+
3+
public record StudyInviteEvent(Long studyId, Long inviterId, Long targetUserId) {
4+
5+
}

src/main/java/grep/neogulcoder/domain/study/service/StudyManagementService.java

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package grep.neogulcoder.domain.study.service;
22

3-
import grep.neogulcoder.domain.alram.service.AlarmService;
4-
import grep.neogulcoder.domain.alram.type.AlarmType;
5-
import grep.neogulcoder.domain.alram.type.DomainType;
63
import grep.neogulcoder.domain.study.Study;
74
import grep.neogulcoder.domain.study.StudyMember;
85
import grep.neogulcoder.domain.study.controller.dto.request.ExtendStudyRequest;
96
import grep.neogulcoder.domain.study.controller.dto.response.ExtendParticipationResponse;
107
import grep.neogulcoder.domain.study.controller.dto.response.StudyExtensionResponse;
8+
import grep.neogulcoder.domain.study.event.StudyInviteEvent;
119
import grep.neogulcoder.domain.study.repository.StudyMemberQueryRepository;
1210
import grep.neogulcoder.domain.study.repository.StudyMemberRepository;
1311
import grep.neogulcoder.domain.study.repository.StudyRepository;
@@ -16,18 +14,27 @@
1614
import grep.neogulcoder.domain.users.repository.UserRepository;
1715
import grep.neogulcoder.global.exception.business.BusinessException;
1816
import grep.neogulcoder.global.exception.business.NotFoundException;
19-
import lombok.RequiredArgsConstructor;
20-
import org.springframework.stereotype.Service;
21-
import org.springframework.transaction.annotation.Transactional;
22-
2317
import java.time.LocalDate;
2418
import java.time.LocalDateTime;
2519
import java.util.List;
2620
import java.util.Random;
21+
import lombok.RequiredArgsConstructor;
22+
import org.springframework.context.ApplicationEventPublisher;
23+
import org.springframework.stereotype.Service;
24+
import org.springframework.transaction.annotation.Transactional;
2725

2826
import static grep.neogulcoder.domain.study.enums.StudyMemberRole.LEADER;
2927
import static grep.neogulcoder.domain.study.enums.StudyMemberRole.MEMBER;
30-
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.*;
28+
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.ALREADY_EXTENDED_STUDY;
29+
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.ALREADY_REGISTERED_PARTICIPATION;
30+
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.END_DATE_BEFORE_ORIGIN_STUDY;
31+
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.EXTENDED_STUDY_NOT_FOUND;
32+
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.LEADER_CANNOT_DELEGATE_TO_SELF;
33+
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.LEADER_CANNOT_LEAVE_STUDY;
34+
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.NOT_STUDY_LEADER;
35+
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.STUDY_EXTENSION_NOT_AVAILABLE;
36+
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.STUDY_MEMBER_NOT_FOUND;
37+
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.STUDY_NOT_FOUND;
3138

3239
@Transactional(readOnly = true)
3340
@RequiredArgsConstructor
@@ -38,12 +45,13 @@ public class StudyManagementService {
3845
private final StudyMemberRepository studyMemberRepository;
3946
private final StudyMemberQueryRepository studyMemberQueryRepository;
4047
private final UserRepository userRepository;
41-
private final AlarmService alarmService;
48+
private final ApplicationEventPublisher eventPublisher;
4249

4350
public StudyExtensionResponse getStudyExtension(Long studyId) {
4451
Study study = findValidStudy(studyId);
4552

46-
List<ExtendParticipationResponse> members = studyMemberQueryRepository.findExtendParticipation(studyId);
53+
List<ExtendParticipationResponse> members = studyMemberQueryRepository.findExtendParticipation(
54+
studyId);
4755
return StudyExtensionResponse.from(study, members);
4856
}
4957

@@ -159,16 +167,11 @@ public void inviteTargetUser(Long studyId, Long userId, String targetUserNicknam
159167
StudyMember studyMember = findValidStudyMember(studyId, userId);
160168
studyMember.isLeader();
161169

162-
User targetUser = userRepository.findByNickname(targetUserNickname).orElseThrow(() -> new NotFoundException(
163-
UserErrorCode.USER_NOT_FOUND));
164-
165-
alarmService.saveAlarm(targetUser.getId(), AlarmType.INVITE, DomainType.STUDY, studyId);
166-
}
170+
User targetUser = userRepository.findByNickname(targetUserNickname)
171+
.orElseThrow(() -> new NotFoundException(
172+
UserErrorCode.USER_NOT_FOUND));
167173

168-
@Transactional
169-
public void acceptInvite(Long studyId, Long targetUserId) {
170-
Study study = findValidStudy(studyId);
171-
StudyMember.createMember(study,targetUserId);
174+
eventPublisher.publishEvent(new StudyInviteEvent(studyId, userId, targetUser.getId()));
172175
}
173176

174177
private Study findValidStudy(Long studyId) {
@@ -182,7 +185,8 @@ private StudyMember findValidStudyMember(Long studyId, Long userId) {
182185
}
183186

184187
private boolean isLastMember(Study study) {
185-
int activatedMemberCount = studyMemberRepository.countByStudyIdAndActivatedTrue(study.getId());
188+
int activatedMemberCount = studyMemberRepository.countByStudyIdAndActivatedTrue(
189+
study.getId());
186190
return activatedMemberCount == 1;
187191
}
188192

@@ -221,4 +225,5 @@ private void validateStudyExtendable(Study study, LocalDateTime endDate) {
221225
throw new BusinessException(END_DATE_BEFORE_ORIGIN_STUDY);
222226
}
223227
}
228+
224229
}

src/main/java/grep/neogulcoder/domain/study/service/StudyService.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,6 @@ public void deleteStudy(Long studyId, Long userId) {
174174
recruitmentPostRepository.deactivateByStudyId(studyId);
175175
}
176176

177-
@Transactional
178-
public void deleteStudyByAdmin(Long studyId) {
179-
Study study = findValidStudy(studyId);
180-
181-
study.delete();
182-
}
183-
184177
private Study findValidStudy(Long studyId) {
185178
return studyRepository.findById(studyId)
186179
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));

src/main/java/grep/neogulcoder/domain/users/controller/UserController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ public ApiResponse<List<AllUserResponse>> getAll() {
5959
@PutMapping(value = "/update/profile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
6060
public ApiResponse<Void> updateProfile(
6161
@AuthenticationPrincipal Principal principal,
62-
@RequestPart(value = "nickname", required = false) String nickname,
63-
@RequestPart(value = "profileImage", required = false) MultipartFile profileImage
62+
@RequestParam(value = "nickname", required = false) String nickname,
63+
@RequestParam(value = "profileImage", required = false) MultipartFile profileImage
6464
) throws IOException {
6565
usersService.updateProfile(principal.getUserId(), nickname, profileImage);
6666
return ApiResponse.noContent();

0 commit comments

Comments
 (0)