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
87a6adc
[EA3-168] feature: 모집글 신청 목록 조회 기능
pia01190 Jul 23, 2025
b9f642b
[EA3-168] refactor: 페이지네이션 개수 변경
pia01190 Jul 23, 2025
ab2971b
[EA3-168] refactor: getMyStudyApplications 경로 수정
pia01190 Jul 23, 2025
5007f21
[EA3-168] feature: 신청서 조회 시 isRead 변경 로직 추가
pia01190 Jul 23, 2025
5af3398
[EA3-161] refactor: 스터디 생성, 수정 request imageUrl 삭제
pia01190 Jul 23, 2025
4f0cdb8
[EA3-161] refactor: 스터디 수정 mediatype 지정
pia01190 Jul 23, 2025
64cbb0a
[EA3-167] feature: 스터디 조회 기능
pia01190 Jul 23, 2025
af2e868
[EA3-134] chore: time_vote_stat 테이블에 version 컬럼 추가
endorsement0912 Jul 24, 2025
7c19d26
[EA3-134] chore : 통계 API 컨트롤러 메서드 연결
endorsement0912 Jul 24, 2025
6918408
[EA3-134] refactor : 에러 코드 접두어 정리 및 투표 관련 에러 추가
endorsement0912 Jul 24, 2025
8842bdc
[EA3-134] refactor : equals 및 hashCode 메서드 추가
endorsement0912 Jul 24, 2025
1a436e2
[EA3-134] refactor : 시작일이 과거거나 종료일보다 늦은 경우 예외 처리 추가
endorsement0912 Jul 24, 2025
9a206a5
[EA3-134] refactor : 중복 투표, 만료된 투표, 빈 투표시간 검증 및 통계 서비스 연결
endorsement0912 Jul 24, 2025
6178c42
[EA3-134] chore : Swagger 예시값 수정 및 통계 응답 DTO 변경 반영
endorsement0912 Jul 24, 2025
a0a94a3
[EA3-134] chore : 통계 응답 통합으로 불필요 클래스 제거
endorsement0912 Jul 24, 2025
37d8733
[EA3-134] feat : 통계 계산 및 증가 반영 쿼리 메서드 추가
endorsement0912 Jul 24, 2025
210196d
[EA3-134] refactor : 기간 기반 조회 및 삭제 쿼리 메서드 추가
endorsement0912 Jul 24, 2025
eaed49c
[EA3-134] refactor : TimeVoteStatListResponse 통합 및 from 메서드 개선
endorsement0912 Jul 24, 2025
52c2a88
[EA3-134] refactor : 통계 계산/재계산 기능 추가 및 낙관적 락 충돌 처리, 유효성 검증 로직 추가
endorsement0912 Jul 24, 2025
e4dd29d
[EA3-134] refactor : version 필드 추가 및 of 생성 메서드, addVotes 유틸 메서드 추가
endorsement0912 Jul 24, 2025
99b7b15
Merge branch 'main' of https://github.com/prgrms-web-devcourse-final-…
endorsement0912 Jul 24, 2025
e8e97b6
[EA3-134] refactor : id명 statId로 변경
endorsement0912 Jul 24, 2025
07a6197
Merge pull request #195 from prgrms-web-devcourse-final-project/featu…
Tokwasp Jul 24, 2025
9729075
Merge pull request #198 from prgrms-web-devcourse-final-project/featu…
Tokwasp Jul 24, 2025
f28a12d
Merge pull request #199 from prgrms-web-devcourse-final-project/featu…
Tokwasp Jul 24, 2025
eef7014
Merge pull request #200 from prgrms-web-devcourse-final-project/featu…
Tokwasp Jul 24, 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 @@ -2,7 +2,6 @@

import grep.neogulcoder.domain.recruitment.post.controller.dto.request.RecruitmentPostStatusUpdateRequest;
import grep.neogulcoder.domain.recruitment.post.controller.dto.request.RecruitmentPostUpdateRequest;
import grep.neogulcoder.domain.recruitment.post.controller.dto.response.RecruitmentApplicationPagingInfo;
import grep.neogulcoder.domain.recruitment.post.controller.dto.response.RecruitmentPostInfo;
import grep.neogulcoder.domain.recruitment.post.controller.dto.response.RecruitmentPostPagingInfo;
import grep.neogulcoder.domain.recruitment.post.service.RecruitmentPostService;
Expand Down Expand Up @@ -76,11 +75,4 @@ public ApiResponse<Long> changeStatus(@PathVariable("recruitment-post-id") long
long postId = recruitmentPostService.updateStatus(request.toServiceRequest(), recruitmentPostId, userDetails.getUserId());
return ApiResponse.success(postId);
}

@GetMapping("{recruitment-post-id}/applications")
public ApiResponse<RecruitmentApplicationPagingInfo> getApplications(@PageableDefault(size = 5) Pageable pageable,
@PathVariable("recruitment-post-id") long recruitmentPostId) {
return ApiResponse.success(new RecruitmentApplicationPagingInfo());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import grep.neogulcoder.domain.recruitment.post.controller.dto.request.RecruitmentPostStatusUpdateRequest;
import grep.neogulcoder.domain.recruitment.post.controller.dto.request.RecruitmentPostUpdateRequest;
import grep.neogulcoder.domain.recruitment.post.controller.dto.response.RecruitmentApplicationPagingInfo;
import grep.neogulcoder.domain.recruitment.post.controller.dto.response.RecruitmentPostInfo;
import grep.neogulcoder.domain.recruitment.post.controller.dto.response.RecruitmentPostPagingInfo;
import grep.neogulcoder.domain.study.enums.Category;
Expand Down Expand Up @@ -144,32 +143,4 @@ ApiResponse<RecruitmentPostPagingInfo> getMyPostPagingInfo(Pageable pageable, Ca
"""
)
ApiResponse<RecruitmentPostPagingInfo> getPagingInfo(Pageable pageable, Category category, StudyType studyType, String keyword);

@Operation(
summary = "스터디 신청한 회원 목록 페이징 조회",
description = """
특정 모집글에 신청한 회원 목록을 페이징하여 조회합니다.

✅ 요청 형식:
`GET /recruitment-posts/{id}/applications?page=0&size=5`

✅ 응답 예시:
```json
{
"applicationInfos": [
{
"nickname": "테스터",
"buddyEnergy": 30,
"createdDate": "2025-07-13T15:30:00",
"applicationReason": "자바를 더 공부 하고싶어요!"
}
],
"totalPage": 3,
"totalElementCount": 20
}
```
"""
)
ApiResponse<RecruitmentApplicationPagingInfo> getApplications(Pageable pageable, long recruitmentPostId);

}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public ApiResponse<StudyHeaderResponse> getStudyHeader(@PathVariable("studyId")

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

@GetMapping("/me/images")
Expand Down Expand Up @@ -77,7 +77,7 @@ public ApiResponse<Long> createStudy(@RequestPart("request") @Valid StudyCreateR
return ApiResponse.success(id);
}

@PutMapping("/{studyId}")
@PutMapping(value = "/{studyId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponse<Void> updateStudy(@PathVariable("studyId") Long studyId,
@RequestPart @Valid StudyUpdateRequest request,
@RequestPart(value = "image", required = false) MultipartFile image,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class StudyCreateRequest {
private String location;

@NotNull
@Schema(description = "시작일", example = "2025-07-15")
@Schema(description = "시작일", example = "2025-07-25T10:00:00")
private LocalDateTime startDate;

@NotNull
Expand All @@ -45,14 +45,11 @@ public class StudyCreateRequest {
@Schema(description = "스터디 소개", example = "자바 스터디입니다.")
private String introduction;

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

private StudyCreateRequest() {}

@Builder
private StudyCreateRequest(String name, Category category, int capacity, StudyType studyType, String location,
LocalDateTime startDate, LocalDateTime endDate, String introduction, String imageUrl) {
LocalDateTime startDate, LocalDateTime endDate, String introduction) {
this.name = name;
this.category = category;
this.capacity = capacity;
Expand All @@ -61,20 +58,19 @@ private StudyCreateRequest(String name, Category category, int capacity, StudyTy
this.startDate = startDate;
this.endDate = endDate;
this.introduction = introduction;
this.imageUrl = imageUrl;
}

public Study toEntity(String imageUrl) {
return Study.builder()
.name(this.name)
.category(this.category)
.capacity(this.capacity)
.studyType(this.studyType)
.location(this.location)
.startDate(this.startDate)
.endDate(this.endDate)
.introduction(this.introduction)
.imageUrl(imageUrl)
.build();
.name(this.name)
.category(this.category)
.capacity(this.capacity)
.studyType(this.studyType)
.location(this.location)
.startDate(this.startDate)
.endDate(this.endDate)
.introduction(this.introduction)
.imageUrl(imageUrl)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,18 @@ public class StudyUpdateRequest {
@Schema(description = "스터디 소개", example = "자바 스터디입니다.")
private String introduction;

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

private StudyUpdateRequest() {}

@Builder
private StudyUpdateRequest(String name, Category category, int capacity, StudyType studyType, String location,
LocalDateTime startDate, String introduction, String imageUrl) {
private StudyUpdateRequest(String name, Category category, int capacity, StudyType studyType,
String location, LocalDateTime startDate, String introduction) {
this.name = name;
this.category = category;
this.capacity = capacity;
this.studyType = studyType;
this.location = location;
this.startDate = startDate;
this.introduction = introduction;
this.imageUrl = imageUrl;
}

public Study toEntity(String imageUrl) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package grep.neogulcoder.domain.study.controller.dto.response;

import grep.neogulcoder.domain.attendance.controller.dto.response.AttendanceResponse;
import grep.neogulcoder.domain.calender.controller.dto.response.TeamCalendarResponse;
import grep.neogulcoder.domain.studypost.controller.dto.StudyPostListResponse;
import grep.neogulcoder.domain.study.Study;
import grep.neogulcoder.domain.studypost.controller.dto.response.FreePostInfo;
import grep.neogulcoder.domain.studypost.controller.dto.response.NoticePostInfo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

import java.util.List;
Expand All @@ -26,15 +28,39 @@ public class StudyResponse {
@Schema(description = "스터디 총 게시물 수", example = "10")
private int totalPostCount;

@Schema(description = "출석 정보")
private List<AttendanceResponse> attendances;
@Schema(description = "팀 달력")
private List<TeamCalendarResponse> teamCalendars;

@Schema(description = "출석률", example = "60")
private int attendanceRate;
@Schema(description = "스터디 공지글 2개")
private List<NoticePostInfo> noticePosts;

@Schema(description = "팀 달력")
private List<TeamCalendarResponse> teamCalenders;
@Schema(description = "스터디 자유글 3개")
private List<FreePostInfo> freePosts;

@Builder
private StudyResponse(int progressDays, int totalDays, int capacity, int currentCount, int totalPostCount,
List<TeamCalendarResponse> teamCalendars, List<NoticePostInfo> noticePosts, List<FreePostInfo> freePosts) {
this.progressDays = progressDays;
this.totalDays = totalDays;
this.capacity = capacity;
this.currentCount = currentCount;
this.totalPostCount = totalPostCount;
this.teamCalendars = teamCalendars;
this.noticePosts = noticePosts;
this.freePosts = freePosts;
}

@Schema(description = "게시글 리스트")
private List<StudyPostListResponse> studyPosts;
public static StudyResponse from(Study study, int progressDays, int totalDays, int totalPostCount,
List<TeamCalendarResponse> teamCalendars, List<NoticePostInfo> noticePosts, List<FreePostInfo> freePosts) {
return StudyResponse.builder()
.progressDays(progressDays)
.totalDays(totalDays)
.capacity(study.getCapacity())
.currentCount(study.getCurrentCount())
.totalPostCount(totalPostCount)
.teamCalendars(teamCalendars)
.noticePosts(noticePosts)
.freePosts(freePosts)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package grep.neogulcoder.domain.study.service;

import grep.neogulcoder.domain.calender.controller.dto.response.TeamCalendarResponse;
import grep.neogulcoder.domain.calender.service.TeamCalendarService;
import grep.neogulcoder.domain.recruitment.post.repository.RecruitmentPostRepository;
import grep.neogulcoder.domain.study.Study;
import grep.neogulcoder.domain.study.StudyMember;
Expand All @@ -12,6 +14,10 @@
import grep.neogulcoder.domain.study.repository.StudyMemberRepository;
import grep.neogulcoder.domain.study.repository.StudyQueryRepository;
import grep.neogulcoder.domain.study.repository.StudyRepository;
import grep.neogulcoder.domain.studypost.controller.dto.response.FreePostInfo;
import grep.neogulcoder.domain.studypost.controller.dto.response.NoticePostInfo;
import grep.neogulcoder.domain.studypost.repository.StudyPostQueryRepository;
import grep.neogulcoder.domain.studypost.repository.StudyPostRepository;
import grep.neogulcoder.domain.users.entity.User;
import grep.neogulcoder.domain.users.repository.UserRepository;
import grep.neogulcoder.global.exception.business.BusinessException;
Expand All @@ -27,6 +33,8 @@
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;

Expand All @@ -46,6 +54,9 @@ public class StudyService {
private final RecruitmentPostRepository recruitmentPostRepository;
private final StudyMemberQueryRepository studyMemberQueryRepository;
private final UserRepository userRepository;
private final StudyPostRepository studyPostRepository;
private final StudyPostQueryRepository studyPostQueryRepository;
private final TeamCalendarService teamCalendarService;

public StudyItemPagingResponse getMyStudiesPaging(Pageable pageable, Long userId, Boolean finished) {
Page<StudyItemResponse> page = studyQueryRepository.findMyStudiesPaging(pageable, userId, finished);
Expand All @@ -62,12 +73,30 @@ public List<StudyItemResponse> getMyStudies(Long userId) {
}

public StudyHeaderResponse getStudyHeader(Long studyId) {
Study study = studyRepository.findByIdAndActivatedTrue(studyId)
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
Study study = findValidStudy(studyId);

return StudyHeaderResponse.from(study);
}

public StudyResponse getStudy(Long studyId) {
Study study = findValidStudy(studyId);

int progressDays = (int) ChronoUnit.DAYS.between(study.getStartDate().toLocalDate(), LocalDate.now()) + 1;
int totalDays = (int) ChronoUnit.DAYS.between(study.getStartDate().toLocalDate(), study.getEndDate().toLocalDate()) + 1;
progressDays = Math.max(0, Math.min(progressDays, totalDays));
int totalPostCount = studyPostRepository.countByStudyIdAndActivatedTrue(studyId);

LocalDate now = LocalDate.now();
int currentYear = now.getYear();
int currentMonth = now.getMonthValue();
List<TeamCalendarResponse> teamCalendars = teamCalendarService.findByMonth(studyId, currentYear, currentMonth);

List<NoticePostInfo> noticePosts = studyPostQueryRepository.findLatestNoticeInfoBy(studyId);
List<FreePostInfo> freePosts = studyPostQueryRepository.findLatestFreeInfoBy(studyId);

return StudyResponse.from(study, progressDays, totalDays, totalPostCount, teamCalendars, noticePosts, freePosts);
}

public List<StudyImageResponse> getStudyImages(Long userId) {
List<Study> myStudiesImage = studyMemberRepository.findStudiesByUserId(userId);

Expand All @@ -77,8 +106,7 @@ public List<StudyImageResponse> getStudyImages(Long userId) {
}

public StudyInfoResponse getMyStudyContent(Long studyId, Long userId) {
Study study = studyRepository.findByIdAndActivatedTrue(studyId)
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
Study study = findValidStudy(studyId);

validateStudyMember(studyId, userId);
validateStudyLeader(studyId, userId);
Expand Down Expand Up @@ -118,8 +146,7 @@ public Long createStudy(StudyCreateRequest request, Long userId, MultipartFile i

@Transactional
public void updateStudy(Long studyId, StudyUpdateRequest request, Long userId, MultipartFile image) throws IOException {
Study study = studyRepository.findById(studyId)
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
Study study = findValidStudy(studyId);

validateLocation(request.getStudyType(), request.getLocation());
validateStudyMember(studyId, userId);
Expand All @@ -142,8 +169,7 @@ public void updateStudy(Long studyId, StudyUpdateRequest request, Long userId, M

@Transactional
public void deleteStudy(Long studyId, Long userId) {
Study study = studyRepository.findById(studyId)
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
Study study = findValidStudy(studyId);

validateStudyMember(studyId, userId);
validateStudyLeader(studyId, userId);
Expand All @@ -156,12 +182,16 @@ public void deleteStudy(Long studyId, Long userId) {

@Transactional
public void deleteStudyByAdmin(Long studyId) {
Study study = studyRepository.findById(studyId)
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND));
Study study = findValidStudy(studyId);

study.delete();
}

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

private static void validateLocation(StudyType studyType, String location) {
if ((studyType == StudyType.OFFLINE || studyType == StudyType.HYBRID) && (location == null || location.isBlank())) {
throw new BusinessException(STUDY_LOCATION_REQUIRED);
Expand Down
Loading