Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3a3e07e
[EA3-200] refactor: 메서드명 변경
pia01190 Jul 29, 2025
14245f9
[EA3-200] feature: 모집글 신청 목록 조회 userId 필드 추가
pia01190 Jul 29, 2025
5360b45
[EA3-200] refactor: 응답값 ResponseEntity로 변경
pia01190 Jul 29, 2025
b177136
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
pia01190 Jul 29, 2025
60af8bd
[EA3-200] refactor: 신청 승인 시 currentCount 증가 로직 수정
pia01190 Jul 29, 2025
d7b21b5
[EA3-200] chore: import문 정리
pia01190 Jul 29, 2025
c157a5d
[EA3-200] refactor: response 불변 필드로 변경
pia01190 Jul 29, 2025
c41a969
[EA3-138] feature: 모집글 조회 시큐리티 설정 추가
Tokwasp Jul 29, 2025
85ed215
[EA3-156] feature: 주관식 리뷰 작성시 빈값 허용 방지 추가
Tokwasp Jul 29, 2025
bd2f795
[EA3-183] chore : data.sql 스터디 멤버 번호 수정(test 계정)
endorsement0912 Jul 29, 2025
6fd062c
[EA3-200] refactor: 스터디 참여 제한 로직 수정
pia01190 Jul 29, 2025
06c00c5
[EA3-205] refactor: response 불변 필드로 변경
pia01190 Jul 29, 2025
e6c5fed
[EA3-205] refactor: 메서드 추상화
pia01190 Jul 29, 2025
835f3c1
[EA3-205] refactor: 응답값 ResponseEntity로 변경
pia01190 Jul 29, 2025
6b13598
Merge pull request #275 from prgrms-web-devcourse-final-project/featu…
hyeunS-P Jul 29, 2025
0fbfe5c
Merge pull request #277 from prgrms-web-devcourse-final-project/refac…
hyeunS-P Jul 29, 2025
c032ecc
Merge pull request #276 from prgrms-web-devcourse-final-project/featu…
endorsement0912 Jul 29, 2025
e1254ac
Merge pull request #280 from prgrms-web-devcourse-final-project/refac…
endorsement0912 Jul 29, 2025
995442f
Merge pull request #282 from prgrms-web-devcourse-final-project/refac…
hyeunS-P 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 @@ -5,6 +5,7 @@
import grep.neogulcoder.global.auth.Principal;
import grep.neogulcoder.global.response.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

Expand All @@ -16,17 +17,17 @@ public class AttendanceController implements AttendanceSpecification {
private final AttendanceService attendanceService;

@GetMapping
public ApiResponse<AttendanceInfoResponse> getAttendances(@PathVariable("studyId") Long studyId,
@AuthenticationPrincipal Principal userDetails) {
public ResponseEntity<ApiResponse<AttendanceInfoResponse>> getAttendances(@PathVariable("studyId") Long studyId,
@AuthenticationPrincipal Principal userDetails) {
AttendanceInfoResponse attendances = attendanceService.getAttendances(studyId, userDetails.getUserId());
return ApiResponse.success(attendances);
return ResponseEntity.ok(ApiResponse.success(attendances));
}

@PostMapping
public ApiResponse<Long> createAttendance(@PathVariable("studyId") Long studyId,
public ResponseEntity<ApiResponse<Long>> createAttendance(@PathVariable("studyId") Long studyId,
@AuthenticationPrincipal Principal userDetails) {
Long userId = userDetails.getUserId();
Long id = attendanceService.createAttendance(studyId, userId);
return ApiResponse.success(id);
return ResponseEntity.ok(ApiResponse.success(id));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
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;

@Tag(name = "Attendance", description = "출석 API")
public interface AttendanceSpecification {

@Operation(summary = "출석 조회", description = "일주일 단위로 출석을 조회합니다.")
ApiResponse<AttendanceInfoResponse> getAttendances(Long studyId, Principal userDetails);
ResponseEntity<ApiResponse<AttendanceInfoResponse>> getAttendances(Long studyId, Principal userDetails);

@Operation(summary = "출석 체크", description = "스터디에 출석을 합니다.")
ApiResponse<Long> createAttendance(Long studyId, Principal userDetails);
ResponseEntity<ApiResponse<Long>> createAttendance(Long studyId, Principal userDetails);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
public class AttendanceInfoResponse {

@Schema(description = "출석일 목록")
private List<AttendanceResponse> attendances;
private final List<AttendanceResponse> attendances;

@Schema(description = "출석률", example = "50")
private int attendanceRate;
private final int attendanceRate;

@Builder
private AttendanceInfoResponse(List<AttendanceResponse> attendances, int attendanceRate) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
public class AttendanceResponse {

@Schema(description = "스터디 Id", example = "1")
private Long studyId;
private final Long studyId;

@Schema(description = "유저 Id", example = "2")
private Long userId;
private final Long userId;

@Schema(description = "출석일", example = "2025-07-10")
private LocalDate attendanceDate;
private final LocalDate attendanceDate;

@Builder
private AttendanceResponse(Long studyId, Long userId, LocalDate attendanceDate) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ public class AttendanceService {
private final StudyMemberRepository studyMemberRepository;

public AttendanceInfoResponse getAttendances(Long studyId, Long userId) {
Study study = studyRepository.findByIdAndActivatedTrue(studyId)
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
Study study = getStudyById(studyId);

List<Attendance> attendances = attendanceRepository.findByStudyIdAndUserId(studyId, userId);
List<AttendanceResponse> responses = attendances.stream()
Expand All @@ -47,23 +46,26 @@ public AttendanceInfoResponse getAttendances(Long studyId, Long userId) {

@Transactional
public Long createAttendance(Long studyId, Long userId) {
Study study = studyRepository.findByIdAndActivatedTrue(studyId)
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));

getStudyById(studyId);
validateNotAlreadyChecked(studyId, userId);

Attendance attendance = Attendance.create(studyId, userId);
attendanceRepository.save(attendance);
return attendance.getId();
}

private Study getStudyById(Long studyId) {
return studyRepository.findById(studyId)
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
}

private int getAttendanceRate(Long studyId, Long userId, Study study, List<AttendanceResponse> responses) {
LocalDate start = study.getStartDate().toLocalDate();
LocalDate participated = studyMemberRepository.findCreatedDateByStudyIdAndUserId(studyId, userId).toLocalDate();
LocalDate attendanceStart = start.isAfter(participated) ? start : participated;
LocalDate end = study.getEndDate().toLocalDate();
LocalDate startDay = study.getStartDate().toLocalDate();
LocalDate participatedDay = studyMemberRepository.findCreatedDateByStudyIdAndUserId(studyId, userId).toLocalDate();
LocalDate attendanceStartDay = startDay.isAfter(participatedDay) ? startDay : participatedDay;
LocalDate endDay = study.getEndDate().toLocalDate();

int totalDays = (int) ChronoUnit.DAYS.between(attendanceStart, end) + 1;
int totalDays = (int) ChronoUnit.DAYS.between(attendanceStartDay, endDay) + 1;
int attendDays = responses.size();
int attendanceRate = totalDays == 0 ? 0 : Math.round(((float) attendDays / totalDays) * 100);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import grep.neogulcoder.domain.review.ReviewType;
import grep.neogulcoder.domain.review.service.request.ReviewSaveServiceRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
Expand All @@ -29,6 +30,7 @@ public class ReviewSaveRequest {
private List<String> reviewTag;

@Schema(example = "너무 친절 하세요!", description = "주관 리뷰")
@NotBlank
private String content;

private ReviewSaveRequest() {}
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/grep/neogulcoder/domain/study/Study.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public class Study extends BaseEntity {
@Version
private Long version;

private static final int MAX_JOINED_STUDY_COUNT = 10;

protected Study() {
}

Expand Down Expand Up @@ -122,4 +124,8 @@ public boolean isReviewableAt(LocalDateTime currentDateTime) {
return (currentDateTime.isEqual(this.endDate) || currentDateTime.isAfter(this.endDate)) &&
(currentDateTime.isEqual(reviewableDateTime) || currentDateTime.isBefore(reviewableDateTime));
}

public static boolean isOverJoinLimit(int joinedStudyCount) {
return joinedStudyCount >= MAX_JOINED_STUDY_COUNT;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@
import java.util.List;
import java.util.Optional;

import static grep.neogulcoder.domain.study.enums.StudyMemberRole.LEADER;
import static grep.neogulcoder.domain.study.enums.StudyMemberRole.*;
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.*;
import static grep.neogulcoder.domain.users.exception.code.UserErrorCode.USER_NOT_FOUND;
import static grep.neogulcoder.domain.users.exception.code.UserErrorCode.*;

@Transactional(readOnly = true)
@RequiredArgsConstructor
Expand Down Expand Up @@ -184,8 +184,8 @@ private StudyMember getStudyMemberById(Long studyId, Long userId) {
}

private void validateStudyCreateLimit(Long userId) {
int count = studyRepository.countByUserIdAndActivatedTrueAndFinishedFalse(userId);
if (count >= 10) {
int joinedStudyCount = studyRepository.countByUserIdAndActivatedTrueAndFinishedFalse(userId);
if (Study.isOverJoinLimit(joinedStudyCount)) {
throw new BusinessException(STUDY_CREATE_LIMIT_EXCEEDED);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

Expand All @@ -22,38 +23,38 @@ public class ApplicationController implements ApplicationSpecification {
private final ApplicationService applicationService;

@GetMapping("/{recruitment-post-id}/applications")
public ApiResponse<ReceivedApplicationPagingResponse> getReceivedApplications(@PathVariable("recruitment-post-id") Long recruitmentPostId,
@PageableDefault(size = 5) Pageable pageable,
@AuthenticationPrincipal Principal userDetails) {
return ApiResponse.success(applicationService.getReceivedApplicationsPaging(recruitmentPostId, pageable, userDetails.getUserId()));
public ResponseEntity<ApiResponse<ReceivedApplicationPagingResponse>> getReceivedApplications(@PathVariable("recruitment-post-id") Long recruitmentPostId,
@PageableDefault(size = 5) Pageable pageable,
@AuthenticationPrincipal Principal userDetails) {
return ResponseEntity.ok(ApiResponse.success(applicationService.getReceivedApplicationsPaging(recruitmentPostId, pageable, userDetails.getUserId())));
}

@GetMapping("/applications")
public ApiResponse<MyApplicationPagingResponse> getMyStudyApplications(@PageableDefault(size = 12) Pageable pageable,
public ResponseEntity<ApiResponse<MyApplicationPagingResponse>> getMyStudyApplications(@PageableDefault(size = 12) Pageable pageable,
@RequestParam(required = false) ApplicationStatus status,
@AuthenticationPrincipal Principal userDetails) {
return ApiResponse.success(applicationService.getMyStudyApplicationsPaging(pageable, userDetails.getUserId(), status));
return ResponseEntity.ok(ApiResponse.success(applicationService.getMyStudyApplicationsPaging(pageable, userDetails.getUserId(), status)));
}

@PostMapping("/{recruitment-post-id}/applications")
public ApiResponse<Long> createApplication(@PathVariable("recruitment-post-id") Long recruitmentPostId,
public ResponseEntity<ApiResponse<Long>> createApplication(@PathVariable("recruitment-post-id") Long recruitmentPostId,
@RequestBody @Valid ApplicationCreateRequest request,
@AuthenticationPrincipal Principal userDetails) {
Long id = applicationService.createApplication(recruitmentPostId, request, userDetails.getUserId());
return ApiResponse.success(id);
return ResponseEntity.ok(ApiResponse.success(id));
}

@PostMapping("/applications/{applicationId}/approve")
public ApiResponse<Void> approveApplication(@PathVariable("applicationId") Long applicationId,
public ResponseEntity<ApiResponse<Void>> approveApplication(@PathVariable("applicationId") Long applicationId,
@AuthenticationPrincipal Principal userDetails) {
applicationService.approveApplication(applicationId, userDetails.getUserId());
return ApiResponse.noContent();
return ResponseEntity.ok(ApiResponse.noContent());
}

@PostMapping("/applications/{applicationId}/reject")
public ApiResponse<Void> rejectApplication(@PathVariable("applicationId") Long applicationId,
public ResponseEntity<ApiResponse<Void>> rejectApplication(@PathVariable("applicationId") Long applicationId,
@AuthenticationPrincipal Principal userDetails) {
applicationService.rejectApplication(applicationId, userDetails.getUserId());
return ApiResponse.noContent();
return ResponseEntity.ok(ApiResponse.noContent());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,23 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;

@Tag(name = "StudyApplication", description = "스터디 신청 API")
public interface ApplicationSpecification {

@Operation(summary = "모집글 신청 목록 조회", description = "스터디장(모집글 작성자)이 신청 목록을 조회합니다.")
public ApiResponse<ReceivedApplicationPagingResponse> getReceivedApplications(Long recruitmentPostId, Pageable pageable, Principal userDetails);
ResponseEntity<ApiResponse<ReceivedApplicationPagingResponse>> getReceivedApplications(Long recruitmentPostId, Pageable pageable, Principal userDetails);

@Operation(summary = "내 스터디 신청 목록 조회", description = "내가 신청한 스터디의 목록을 조회합니다.")
ApiResponse<MyApplicationPagingResponse> getMyStudyApplications(Pageable pageable, ApplicationStatus status, Principal userDetails);
ResponseEntity<ApiResponse<MyApplicationPagingResponse>> getMyStudyApplications(Pageable pageable, ApplicationStatus status, Principal userDetails);

@Operation(summary = "스터디 신청 생성", description = "스터디를 신청합니다.")
ApiResponse<Long> createApplication(Long recruitmentPostId, ApplicationCreateRequest request, Principal userDetails);
ResponseEntity<ApiResponse<Long>> createApplication(Long recruitmentPostId, ApplicationCreateRequest request, Principal userDetails);

@Operation(summary = "스터디 신청 승인", description = "스터디장이 스터디 신청을 승인합니다.")
ApiResponse<Void> approveApplication(Long applicationId, Principal userDetails);
ResponseEntity<ApiResponse<Void>> approveApplication(Long applicationId, Principal userDetails);

@Operation(summary = "스터디 신청 거절", description = "스터디장이 스터디 신청을 거절합니다.")
ApiResponse<Void> rejectApplication(Long applicationId, Principal userDetails);
ResponseEntity<ApiResponse<Void>> rejectApplication(Long applicationId, Principal userDetails);
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ public class MyApplicationPagingResponse {
"\"status\": \"PENDING\"" +
"}]"
)
private List<MyApplicationResponse> applications;
private final List<MyApplicationResponse> applications;

@Schema(description = "총 페이지 수", example = "2")
private int totalPage;
private final int totalPage;

@Schema(description = "총 요소 개수", example = "10")
private int totalElementCount;
private final int totalElementCount;

@Schema(example = "false", description = "다음 페이지 여부")
private boolean hasNext;
private final boolean hasNext;

@Builder
private MyApplicationPagingResponse(Page<MyApplicationResponse> page) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,40 @@
public class MyApplicationResponse {

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

@Schema(description = "스터디 이름", example = "자바 스터디")
private String name;
private final String name;

@Schema(description = "스터디장 닉네임", example = "너굴")
private String leaderNickname;
private final String leaderNickname;

@Schema(description = "정원", example = "4")
private int capacity;
private final int capacity;

@Schema(description = "인원수", example = "3")
private int currentCount;
private final int currentCount;

@Schema(description = "시작일", example = "2025-07-15")
private LocalDateTime startDate;
private final LocalDateTime startDate;

@Schema(description = "대표 이미지", example = "http://localhost:8083/image.jpg")
private String imageUrl;
private final String imageUrl;

@Schema(description = "스터디 소개", example = "자바 스터디입니다.")
private String introduction;
private final String introduction;

@Schema(description = "카테고리", example = "IT")
private Category category;
private final Category category;

@Schema(description = "타입", example = "ONLINE")
private StudyType studyType;
final StudyType studyType;

@Schema(description = "열람 여부", example = "true")
private boolean isRead;
private final boolean isRead;

@Schema(description = "신청 상태", example = "PENDING")
private ApplicationStatus status;
private final ApplicationStatus status;

@QueryProjection
public MyApplicationResponse(Long recruitmentPostId, String name, String leaderNickname, int capacity, int currentCount, LocalDateTime startDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,24 @@ public class ReceivedApplicationPagingResponse {
description = "내 모집글에 지원한 신청 목록",
example = "[{" +
"\"applicationId\": 1," +
"\"userId\": 1," +
"\"nickname\": \"너굴\"," +
"\"profileImageUrl\": \"http://localhost:8083/image.jpg\"," +
"\"buddyEnergy\": 30," +
"\"createdDate\": \"2025-07-10T15:30:00\"," +
"\"applicationReason\": \"자바를 더 공부하고 싶어 지원합니다.\"" +
"}]"
)
private List<ReceivedApplicationResponse> receivedApplications;
private final List<ReceivedApplicationResponse> receivedApplications;

@Schema(description = "총 페이지 수", example = "2")
private int totalPage;
private final int totalPage;

@Schema(description = "총 요소 개수", example = "10")
private int totalElementCount;
private final int totalElementCount;

@Schema(description = "다음 페이지 여부", example = "false")
private boolean hasNext;
private final boolean hasNext;

@Builder
private ReceivedApplicationPagingResponse(Page<ReceivedApplicationResponse> page) {
Expand Down
Loading