Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4eb07c2
[EA3-195] chore: 필요없는 메서드 삭제 및 불변 객체 변경
pia01190 Jul 28, 2025
c5cb73c
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
pia01190 Jul 28, 2025
4f93681
[EA3-195] chore: import문 제거
pia01190 Jul 28, 2025
4351dd3
[EA3-195] refactor: 확장 스터디 toEntity userId 추가
pia01190 Jul 28, 2025
fa65d8b
[EA3-195] refactor: StudyMember 생성 로직 리팩터링
pia01190 Jul 28, 2025
9b47419
[EA3-195] refactor: study, studymember validate 메서드명 변경
pia01190 Jul 28, 2025
d3b1f56
[EA3-195] feature: 스터디 탈퇴 시 모집글 상태 변경
pia01190 Jul 28, 2025
5ae1dc0
[EA3-195] refactor: 유저 탈퇴 시 스터디 삭제 코드 리팩터링
pia01190 Jul 28, 2025
bf1c87c
[EA3-195] refactor: currentCount 감소 로직 리팩터링
pia01190 Jul 28, 2025
cedabdc
[EA3-195] refactor: study currentCount 증가 로직 추가
pia01190 Jul 28, 2025
e088ef5
[EA3-195] refactor: study 생성, 수정 dto 검증 로직 추가
pia01190 Jul 28, 2025
8234729
[EA3-195] refactor: 응답값 ResponseEntity로 변경
pia01190 Jul 28, 2025
48799df
[EA3-195] feature: 스터디 초대 수락 시 currentCount 증가
pia01190 Jul 28, 2025
9917527
[EA3-183] refactor: Version 제거
endorsement0912 Jul 28, 2025
a05b442
[EA3-183] feature : 통계 집계용 인덱스 추가
endorsement0912 Jul 28, 2025
4604182
[EA3-183] feature : Soft deleted 데이터 정리 스케줄러 추가
endorsement0912 Jul 28, 2025
63fe117
[EA3-183] refactor : TimeVotePeriod soft delete 적용
endorsement0912 Jul 28, 2025
0aa2a2b
[EA3-183] refactor : 통계 처리 로직 리팩토링 및 soft delete 대응
endorsement0912 Jul 28, 2025
139b909
[EA3-183] refactor : TimeVote soft delete 적용 및 성능 개선
endorsement0912 Jul 28, 2025
23d8f82
[EA3-183] refactor : Validator 메서드 명 수정
endorsement0912 Jul 28, 2025
6582f59
[EA3-183] refactor : 에러 코드 추가
endorsement0912 Jul 29, 2025
1dc9df6
[EA3-183] refactor : 스케줄러 실패시를 고려한 에러 로그 추가
endorsement0912 Jul 29, 2025
e09e510
[EA3-183] refactor : 기존 투표 삭제 실패시를 고려해 예외및 로그 추가
endorsement0912 Jul 29, 2025
c0ba7ef
[EA3-195] refactor: Study isStarted 메서드 현재 시간 주입 변경
pia01190 Jul 29, 2025
5061f15
[EA3-195] refactor: 메서드명 변경
pia01190 Jul 29, 2025
e2a351b
fix: 초대 수락/거절 시 받은 사람 ID와 알림 ID를 통해 찾도록 변경
hyeunS-P Jul 29, 2025
119bc65
feature: 이미 읽은 초대에 대한 수락/거절 validation
hyeunS-P Jul 29, 2025
0c86db9
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
pia01190 Jul 29, 2025
81512e7
[EA3-183] refactor : timeSlots 기반 toEntities 메서드 추가
endorsement0912 Jul 29, 2025
bdc759c
[EA3-183] refactor : incrementStats 메서드 제거 (recalculateStats 방식으로 통일)
endorsement0912 Jul 29, 2025
9e45ed0
[EA3-183] chore : 디버깅을 위한 로그 추가
endorsement0912 Jul 29, 2025
2c21b47
[EA3-183] refactor : incrementStats 메서드 제거 (recalculateStats 방식으로 통일)
endorsement0912 Jul 29, 2025
fc03c82
Merge pull request #271 from prgrms-web-devcourse-final-project/fix/a…
endorsement0912 Jul 29, 2025
fe736b1
[EA3-195] refactor: 필요없는 메서드 삭제
pia01190 Jul 29, 2025
8709d0a
[EA3-195] chore: import문 정리
pia01190 Jul 29, 2025
6905f63
Merge pull request #273 from prgrms-web-devcourse-final-project/refac…
endorsement0912 Jul 29, 2025
570ab0a
Merge pull request #269 from prgrms-web-devcourse-final-project/refac…
endorsement0912 Jul 29, 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 @@ -41,7 +41,7 @@ public ApiResponse<Void> respondToInvite(@AuthenticationPrincipal Principal prin
if (accepted) {
alarmService.acceptInvite(principal.getUserId(), alarmId);
} else {
alarmService.rejectInvite(principal.getUserId());
alarmService.rejectInvite(principal.getUserId(), alarmId);
}

return ApiResponse.success(accepted ? "스터디 초대를 수락했습니다." : "스터디 초대를 거절했습니다.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
@Getter
public enum AlarmErrorCode implements ErrorCode {

ALARM_NOT_FOUND("A001",HttpStatus.NOT_FOUND,"알람을 찾을 수 없습니다.");
ALARM_NOT_FOUND("A001",HttpStatus.NOT_FOUND,"알람을 찾을 수 없습니다."),
ALREADY_CHECKED("A002",HttpStatus.BAD_REQUEST,"이미 읽은 알림입니다.");

private final String code;
private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import grep.neogulcoder.domain.alram.entity.Alarm;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

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

Optional<Alarm> findByReceiverUserIdAndId(Long targetUserId, Long alarmId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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.study.service.StudyManagementServiceFacade;
import grep.neogulcoder.domain.studyapplication.StudyApplication;
import grep.neogulcoder.domain.studyapplication.event.ApplicationEvent;
import grep.neogulcoder.domain.studyapplication.event.ApplicationStatusChangedEvent;
Expand All @@ -37,6 +38,7 @@

import java.util.List;

import static grep.neogulcoder.domain.alram.exception.code.AlarmErrorCode.ALREADY_CHECKED;
import static grep.neogulcoder.domain.recruitment.RecruitmentErrorCode.NOT_FOUND;
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;
Expand All @@ -56,6 +58,7 @@ public class AlarmService {
private final StudyMemberRepository studyMemberRepository;
private final RecruitmentPostRepository recruitmentPostRepository;
private final ApplicationRepository applicationRepository;
private final StudyManagementServiceFacade studyManagementServiceFacade;

@Transactional
public void saveAlarm(Long receiverId, AlarmType alarmType, DomainType domainType,
Expand Down Expand Up @@ -113,18 +116,28 @@ public void handleStudyInviteEvent(StudyInviteEvent event) {
@Transactional
public void acceptInvite(Long targetUserId, Long alarmId) {

if(isChecked(targetUserId, alarmId)){
throw new BusinessException(ALREADY_CHECKED);
}

validateParticipantStudyLimit(targetUserId);

Alarm alarm = findValidAlarm(alarmId);
Alarm alarm = findValidAlarm(targetUserId, alarmId);
Long studyId = alarm.getDomainId();
Study study = findValidStudy(studyId);
studyMemberRepository.save(StudyMember.createMember(study, targetUserId));
studyManagementServiceFacade.increaseMemberCount(study, targetUserId);
alarm.checkAlarm();
}

@Transactional
public void rejectInvite(Long alarmId) {
Alarm alarm = findValidAlarm(alarmId);
public void rejectInvite(Long targetUserId,Long alarmId) {

if(isChecked(targetUserId, alarmId)){
throw new BusinessException(ALREADY_CHECKED);
}

Alarm alarm = findValidAlarm(targetUserId, alarmId);
alarm.checkAlarm();
}

Expand Down Expand Up @@ -248,9 +261,8 @@ public void handleStudyPostCommentEvent(StudyPostCommentEvent event) {
);
}

private Alarm findValidAlarm(Long alarmId) {
return alarmRepository.findById(alarmId)
.orElseThrow(() -> new NotFoundException(AlarmErrorCode.ALARM_NOT_FOUND));
private Alarm findValidAlarm(Long targetUserId, Long alarmId) {
return alarmRepository.findByReceiverUserIdAndId(targetUserId, alarmId).orElseThrow(() -> new NotFoundException(AlarmErrorCode.ALARM_NOT_FOUND));
}

private Study findValidStudy(Long studyId) {
Expand All @@ -264,4 +276,8 @@ private void validateParticipantStudyLimit(Long userId) {
throw new BusinessException(APPLICATION_PARTICIPANT_LIMIT_EXCEEDED);
}
}

private boolean isChecked(Long targetUserId, Long alarmId) {
return findValidAlarm(targetUserId, alarmId).isChecked();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ public interface RecruitmentPostRepository extends JpaRepository<RecruitmentPost
void deactivateByStudyId(@Param("studyId") Long studyId);

Page<RecruitmentPost> findBySubjectContainingIgnoreCase(String subject, Pageable pageable);

Optional<RecruitmentPost> findByStudyIdAndActivatedTrue(Long studyId);
}
4 changes: 2 additions & 2 deletions src/main/java/grep/neogulcoder/domain/study/Study.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ public void update(String name, Category category, int capacity, StudyType study
this.imageUrl = imageUrl;
}

public boolean isStarted() {
return this.startDate.isBefore(LocalDateTime.now());
public boolean isStarted(LocalDateTime currentDateTime) {
return this.startDate.isBefore(currentDateTime);
}

public void delete() {
Expand Down
4 changes: 0 additions & 4 deletions src/main/java/grep/neogulcoder/domain/study/StudyMember.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,6 @@ public void changeRoleMember() {
this.role = StudyMemberRole.MEMBER;
}

public boolean isParticipated() {
return this.participated;
}

public void participate() {
this.participated = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -26,70 +27,70 @@ public class StudyController implements StudySpecification {
private final StudyService studyService;

@GetMapping
public ApiResponse<StudyItemPagingResponse> getMyStudies(@PageableDefault(size = 12) Pageable pageable,
@RequestParam(required = false) Boolean finished,
@AuthenticationPrincipal Principal userDetails) {
public ResponseEntity<ApiResponse<StudyItemPagingResponse>> getMyStudies(@PageableDefault(size = 12) Pageable pageable,
@RequestParam(required = false) Boolean finished,
@AuthenticationPrincipal Principal userDetails) {
StudyItemPagingResponse myStudies = studyService.getMyStudiesPaging(pageable, userDetails.getUserId(), finished);
return ApiResponse.success(myStudies);
return ResponseEntity.ok(ApiResponse.success(myStudies));
}

@GetMapping("/main")
public ApiResponse<List<StudyItemResponse>> getMyUnfinishedStudies(@AuthenticationPrincipal Principal userDetails) {
public ResponseEntity<ApiResponse<List<StudyItemResponse>>> getMyUnfinishedStudies(@AuthenticationPrincipal Principal userDetails) {
List<StudyItemResponse> unfinishedStudies = studyService.getMyUnfinishedStudies(userDetails.getUserId());
return ApiResponse.success(unfinishedStudies);
return ResponseEntity.ok(ApiResponse.success(unfinishedStudies));
}

@GetMapping("/{studyId}/header")
public ApiResponse<StudyHeaderResponse> getStudyHeader(@PathVariable("studyId") Long studyId) {
return ApiResponse.success(studyService.getStudyHeader(studyId));
public ResponseEntity<ApiResponse<StudyHeaderResponse>> getStudyHeader(@PathVariable("studyId") Long studyId) {
return ResponseEntity.ok(ApiResponse.success(studyService.getStudyHeader(studyId)));
}

@GetMapping("/{studyId}")
public ApiResponse<StudyResponse> getStudy(@PathVariable("studyId") Long studyId) {
return ApiResponse.success(studyService.getStudy(studyId));
public ResponseEntity<ApiResponse<StudyResponse>> getStudy(@PathVariable("studyId") Long studyId) {
return ResponseEntity.ok(ApiResponse.success(studyService.getStudy(studyId)));
}

@GetMapping("/me/images")
public ApiResponse<List<StudyImageResponse>> getStudyImages(@AuthenticationPrincipal Principal userDetails) {
public ResponseEntity<ApiResponse<List<StudyImageResponse>>> getStudyImages(@AuthenticationPrincipal Principal userDetails) {
Long userId = userDetails.getUserId();
return ApiResponse.success(studyService.getStudyImages(userId));
return ResponseEntity.ok(ApiResponse.success(studyService.getStudyImages(userId)));
}

@GetMapping("/{studyId}/info")
public ApiResponse<StudyInfoResponse> getStudyInfo(@PathVariable("studyId") Long studyId,
public ResponseEntity<ApiResponse<StudyInfoResponse>> getStudyInfo(@PathVariable("studyId") Long studyId,
@AuthenticationPrincipal Principal userDetails) {
Long userId = userDetails.getUserId();
return ApiResponse.success(studyService.getMyStudyContent(studyId, userId));
return ResponseEntity.ok(ApiResponse.success(studyService.getMyStudyContent(studyId, userId)));
}

@GetMapping("/{studyId}/me")
public ApiResponse<StudyMemberInfoResponse> getMyStudyMemberInfo(@PathVariable("studyId") Long studyId,
public ResponseEntity<ApiResponse<StudyMemberInfoResponse>> getMyStudyMemberInfo(@PathVariable("studyId") Long studyId,
@AuthenticationPrincipal Principal userDetails) {
Long userId = userDetails.getUserId();
return ApiResponse.success(studyService.getMyStudyMemberInfo(studyId, userId));
return ResponseEntity.ok(ApiResponse.success(studyService.getMyStudyMemberInfo(studyId, userId)));
}

@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponse<Long> createStudy(@RequestPart("request") @Valid StudyCreateRequest request,
public ResponseEntity<ApiResponse<Long>> createStudy(@RequestPart("request") @Valid StudyCreateRequest request,
@RequestPart(value = "image", required = false) MultipartFile image,
@AuthenticationPrincipal Principal userDetails) throws IOException {
Long id = studyService.createStudy(request, userDetails.getUserId(), image);
return ApiResponse.success(id);
return ResponseEntity.ok(ApiResponse.success(id));
}

@PutMapping(value = "/{studyId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponse<Void> updateStudy(@PathVariable("studyId") Long studyId,
public ResponseEntity<ApiResponse<Void>> updateStudy(@PathVariable("studyId") Long studyId,
@RequestPart @Valid StudyUpdateRequest request,
@RequestPart(value = "image", required = false) MultipartFile image,
@AuthenticationPrincipal Principal userDetails) throws IOException {
studyService.updateStudy(studyId, request, userDetails.getUserId(), image);
return ApiResponse.noContent();
return ResponseEntity.ok(ApiResponse.noContent());
}

@DeleteMapping("/{studyId}")
public ApiResponse<Void> deleteStudy(@PathVariable("studyId") Long studyId,
public ResponseEntity<ApiResponse<Void>> deleteStudy(@PathVariable("studyId") Long studyId,
@AuthenticationPrincipal Principal userDetails) {
studyService.deleteStudy(studyId, userDetails.getUserId());
return ApiResponse.noContent();
return ResponseEntity.ok(ApiResponse.noContent());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import grep.neogulcoder.global.response.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

Expand All @@ -22,51 +23,50 @@ public class StudyManagementController implements StudyManagementSpecification {
private final StudyManagementService studyManagementService;

@GetMapping("/extension")
public ApiResponse<StudyExtensionResponse> getStudyExtension(@PathVariable("studyId") Long studyId) {
public ResponseEntity<ApiResponse<StudyExtensionResponse>> getStudyExtension(@PathVariable("studyId") Long studyId) {
StudyExtensionResponse studyExtension = studyManagementService.getStudyExtension(studyId);
return ApiResponse.success(studyExtension);
return ResponseEntity.ok(ApiResponse.success(studyExtension));
}

@GetMapping("/extension/participations")
public ApiResponse<List<ExtendParticipationResponse>> getExtendParticipations(@PathVariable("studyId") Long studyId) {
public ResponseEntity<ApiResponse<List<ExtendParticipationResponse>>> getExtendParticipations(@PathVariable("studyId") Long studyId) {
List<ExtendParticipationResponse> extendParticipations = studyManagementService.getExtendParticipations(studyId);
return ApiResponse.success(extendParticipations);
return ResponseEntity.ok(ApiResponse.success(extendParticipations));
}

@DeleteMapping("/me")
public ApiResponse<Void> leaveStudy(@PathVariable("studyId") Long studyId,
public ResponseEntity<ApiResponse<Void>> leaveStudy(@PathVariable("studyId") Long studyId,
@AuthenticationPrincipal Principal userDetails) {
studyManagementService.leaveStudy(studyId, userDetails.getUserId());
return ApiResponse.noContent();
return ResponseEntity.ok(ApiResponse.noContent());
}

@PostMapping("/delegate")
public ApiResponse<Void> delegateLeader(@PathVariable("studyId") Long studyId,
public ResponseEntity<ApiResponse<Void>> delegateLeader(@PathVariable("studyId") Long studyId,
@RequestBody DelegateLeaderRequest request,
@AuthenticationPrincipal Principal userDetails) {
studyManagementService.delegateLeader(studyId, userDetails.getUserId(), request.getNewLeaderId());
return ApiResponse.noContent();
return ResponseEntity.ok(ApiResponse.noContent());
}

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

@PostMapping("/extension/participations")
public ApiResponse<Void> registerExtensionParticipation(@PathVariable("studyId") Long studyId,
public ResponseEntity<ApiResponse<Void>> registerExtensionParticipation(@PathVariable("studyId") Long studyId,
@AuthenticationPrincipal Principal userDetails) {
studyManagementService.registerExtensionParticipation(studyId, userDetails.getUserId());
return ApiResponse.noContent();
return ResponseEntity.ok(ApiResponse.noContent());
}

@PostMapping("/invite/user")
public ApiResponse<Void> inviteUser(@PathVariable("studyId") Long studyId, @AuthenticationPrincipal Principal userDetails, String targetUserNickname) {
public ResponseEntity<ApiResponse<Void>> inviteUser(@PathVariable("studyId") Long studyId, @AuthenticationPrincipal Principal userDetails, String targetUserNickname) {
studyManagementService.inviteTargetUser(studyId, userDetails.getUserId(), targetUserNickname);
return ApiResponse.noContent();
return ResponseEntity.ok(ApiResponse.noContent());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,31 @@
import grep.neogulcoder.global.response.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;

import java.util.List;

@Tag(name = "StudyManagement", description = "스터디 관리 API")
public interface StudyManagementSpecification {

@Operation(summary = "스터디 연장 여부 조회", description = "스터디 연장 여부를 조회합니다.")
ApiResponse<StudyExtensionResponse> getStudyExtension(Long studyId);
ResponseEntity<ApiResponse<StudyExtensionResponse>> getStudyExtension(Long studyId);

@Operation(summary = "연장 스터디 참여 멤버 목록 조회", description = "연장 스터디에 참여하는 멤버 목록을 조회합니다.")
ApiResponse<List<ExtendParticipationResponse>> getExtendParticipations(Long studyId);
ResponseEntity<ApiResponse<List<ExtendParticipationResponse>>> getExtendParticipations(Long studyId);

@Operation(summary = "스터디 탈퇴", description = "스터디를 탈퇴합니다.")
ApiResponse<Void> leaveStudy(Long studyId, Principal userDetails);
ResponseEntity<ApiResponse<Void>> leaveStudy(Long studyId, Principal userDetails);

@Operation(summary = "스터디장 위임", description = "스터디원에게 스터디장을 위임합니다.")
ApiResponse<Void> delegateLeader(Long studyId, DelegateLeaderRequest request, Principal userDetails);
ResponseEntity<ApiResponse<Void>> delegateLeader(Long studyId, DelegateLeaderRequest request, Principal userDetails);

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

@Operation(summary = "연장 스터디 참여", description = "스터디원이 연장된 스터디에 참여합니다.")
ApiResponse<Void> registerExtensionParticipation(Long studyId, Principal userDetails);
ResponseEntity<ApiResponse<Void>> registerExtensionParticipation(Long studyId, Principal userDetails);

@Operation(summary = "스터디 초대", description = "스터디장이 스터디에 초대합니다.")
ResponseEntity<ApiResponse<Void>> inviteUser(Long studyId, Principal userDetails, String targetUserNickname);
}
Loading