Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 스터디 기본 정보와 상세 정보 작성 API #642

Merged
merged 23 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fc8157d
feat: 스터디 상세 작성 스펙 구현
AlmondBreez3 Aug 16, 2024
07c10f7
feat: 스터디 상세 정보 기본 로직 작성
AlmondBreez3 Aug 16, 2024
05422cc
feat: 스터디 상세 정보 작성 API
AlmondBreez3 Aug 16, 2024
0bbf3f1
feat: validate 개선
AlmondBreez3 Aug 17, 2024
dfcd0d1
feat: 테스트 케이스 추가
AlmondBreez3 Aug 17, 2024
616f2f5
fix: merge conflict 해결
AlmondBreez3 Aug 18, 2024
b10f888
feat: 전체 로직 구현
AlmondBreez3 Aug 18, 2024
15452f7
feat: 전체 로직, validaotr 테스트코드 작성
AlmondBreez3 Aug 18, 2024
16c9aac
feat: log 추가
AlmondBreez3 Aug 18, 2024
22d0afc
feat: test 가독성 개선
AlmondBreez3 Aug 18, 2024
846ffa5
fix: merge conflict 해결하기
AlmondBreez3 Aug 18, 2024
6593f4e
feat: 에러 코드 수정
AlmondBreez3 Aug 18, 2024
a6fb8db
feat: 수정 로직 불필요한 주석 제거
AlmondBreez3 Aug 18, 2024
5badce8
feat: 스터디 상세 정보 id 제약조건 추가
AlmondBreez3 Aug 18, 2024
a444b60
feat: 스터디 상세 정보 비즈니스 로직에서 스터디 비즈니스 로직으로 변경
AlmondBreez3 Aug 18, 2024
868f05e
feat: 스터디 요청 Dto 이름 변경
AlmondBreez3 Aug 18, 2024
37d650b
feat: 스터디 상세 정보 -> 스터디 상세 & 기본으로 변경
AlmondBreez3 Aug 18, 2024
fc65781
fix: develop과 머지 컨플릭트 해결
AlmondBreez3 Aug 18, 2024
d944516
fix: pr 수정 요청 부분 수정
AlmondBreez3 Aug 18, 2024
d878603
feat: 고정된 날짜 사용으로 대체하기
AlmondBreez3 Aug 18, 2024
cffe5bb
feat: this로 객체 메서드 오류 해결, 테스트 불필요한 fixture삭제
AlmondBreez3 Aug 18, 2024
55eea81
feat: 올바른 테스트 데이터 작성
AlmondBreez3 Aug 18, 2024
f20ef1a
feat: 스터디 시작 시간을 세션 시작시간으로 변경
AlmondBreez3 Aug 18, 2024
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,6 +2,7 @@

import com.gdschongik.gdsc.domain.study.application.MentorStudyService;
import com.gdschongik.gdsc.domain.study.dto.request.StudyAnnouncementCreateUpdateRequest;
import com.gdschongik.gdsc.domain.study.dto.request.StudyUpdateRequest;
import com.gdschongik.gdsc.domain.study.dto.response.MentorStudyResponse;
import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentResponse;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -10,14 +11,7 @@
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

@Tag(name = "Mentor Study", description = "멘토 스터디 API입니다.")
@RestController
Expand All @@ -27,6 +21,13 @@ public class MentorStudyController {

private final MentorStudyService mentorStudyService;

@Operation(summary = "스터디 정보 작성", description = "스터디 기본 정보와 상세 정보를 작성합니다.")
@PatchMapping("/{studyId}")
public ResponseEntity<Void> updateStudy(@PathVariable Long studyId, @RequestBody StudyUpdateRequest request) {
mentorStudyService.updateStudy(studyId, request);
return ResponseEntity.ok().build();
}

@Operation(summary = "내 스터디 조회", description = "내가 멘토로 있는 스터디를 조회합니다.")
@GetMapping("/me")
public ResponseEntity<List<MentorStudyResponse>> getStudiesInCharge() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
package com.gdschongik.gdsc.domain.study.application;

import static com.gdschongik.gdsc.global.exception.ErrorCode.STUDY_NOT_FOUND;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.study.dao.StudyAnnouncementRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyDetailRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyRepository;
import com.gdschongik.gdsc.domain.study.domain.*;
import com.gdschongik.gdsc.domain.study.domain.Study;
import com.gdschongik.gdsc.domain.study.domain.StudyAnnouncement;
import com.gdschongik.gdsc.domain.study.domain.StudyHistory;
import com.gdschongik.gdsc.domain.study.domain.StudyValidator;
import com.gdschongik.gdsc.domain.study.dto.request.StudyAnnouncementCreateUpdateRequest;
import com.gdschongik.gdsc.domain.study.dto.request.StudySessionCreateRequest;
import com.gdschongik.gdsc.domain.study.dto.request.StudyUpdateRequest;
import com.gdschongik.gdsc.domain.study.dto.response.MentorStudyResponse;
import com.gdschongik.gdsc.domain.study.dto.response.StudyStudentResponse;
import com.gdschongik.gdsc.global.exception.CustomException;
import com.gdschongik.gdsc.global.exception.ErrorCode;
import com.gdschongik.gdsc.global.util.MemberUtil;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -30,6 +38,8 @@ public class MentorStudyService {
private final StudyAnnouncementRepository studyAnnouncementRepository;
private final StudyHistoryRepository studyHistoryRepository;
private final StudyValidator studyValidator;
private final StudyDetailRepository studyDetailRepository;
private final StudyDetailValidator studyDetailValidator;

@Transactional(readOnly = true)
public List<MentorStudyResponse> getStudiesInCharge() {
Expand Down Expand Up @@ -90,4 +100,49 @@ public void deleteStudyAnnouncement(Long studyAnnouncementId) {

log.info("[MentorStudyService] 스터디 공지 삭제 완료: studyAnnouncementId={}", studyAnnouncement.getId());
}

// TODO session -> curriculum 변경
@Transactional
public void updateStudy(Long studyId, StudyUpdateRequest request) {
Member currentMember = memberUtil.getCurrentMember();
Study study = studyRepository.findById(studyId).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND));
studyValidator.validateStudyMentor(currentMember, study);

List<StudyDetail> studyDetails = studyDetailRepository.findAllByStudyId(studyId);
// StudyDetail ID를 추출하여 Set으로 저장
Set<Long> studyDetailIds = studyDetails.stream().map(StudyDetail::getId).collect(Collectors.toSet());

// 요청된 StudySessionCreateRequest의 StudyDetail ID를 추출하여 Set으로 저장
Set<Long> requestIds = request.studySessions().stream()
.map(StudySessionCreateRequest::studyDetailId)
.collect(Collectors.toSet());

studyDetailValidator.validateUpdateStudyDetail(studyDetailIds, requestIds);

study.update(request.notionLink(), request.introduction());
studyRepository.save(study);
log.info("[MentorStudyService] 스터디 기본 정보 수정 완료: studyId={}", studyId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

두 로직을 추출하여 적절히 분리하는게 좋을 것 같습니다.


updateAllStudyDetailSession(studyDetails, request.studySessions());
}

private void updateAllStudyDetailSession(
List<StudyDetail> studyDetails, List<StudySessionCreateRequest> studySessions) {
for (StudyDetail studyDetail : studyDetails) {
Long id = studyDetail.getId();
StudySessionCreateRequest matchingSession = studySessions.stream()
.filter(session -> session.studyDetailId().equals(id))
.findFirst()
.get();

studyDetail.updateSession(
studyDetail.getStudy().getStartTime(),
matchingSession.title(),
matchingSession.description(),
matchingSession.difficulty(),
matchingSession.status());
}
studyDetailRepository.saveAll(studyDetails);
log.info("[MentorStudyService] 스터디 상세정보 커리큘럼 작성 완료: studyDetailId={}", studyDetails);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,9 @@ public boolean isStudyOngoing() {
public LocalDate getStartDate() {
return period.getStartDate().toLocalDate();
}

public void update(String link, String introduction) {
notionLink = link;
this.introduction = introduction;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import jakarta.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -101,6 +102,11 @@ public LocalDate getAttendanceDay() {
- study.getStartDate().getDayOfWeek().getValue());
}

public void updateSession(
LocalTime startAt, String title, String description, Difficulty difficulty, StudyStatus status) {
session = Session.generateSession(startAt, title, description, difficulty, status);
}

public void validateAssignmentSubmittable(LocalDateTime now) {
if (now.isBefore(period.getStartDate())) {
throw new CustomException(ASSIGNMENT_SUBMIT_NOT_STARTED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.gdschongik.gdsc.global.annotation.DomainService;
import com.gdschongik.gdsc.global.exception.CustomException;
import java.time.LocalDateTime;
import java.util.Set;

@DomainService
public class StudyDetailValidator {
Expand Down Expand Up @@ -54,4 +55,16 @@ private void validateUpdateDeadline(
throw new CustomException(STUDY_DETAIL_ASSIGNMENT_INVALID_UPDATE_DEADLINE);
}
}

public void validateUpdateStudyDetail(Set<Long> studyDetails, Set<Long> requests) {
// StudyDetail 목록과 요청된 StudySessionCreateRequest 목록의 크기를 먼저 비교
if (studyDetails.size() != requests.size()) {
throw new CustomException(STUDY_DETAIL_SESSION_SIZE_MISMATCH);
}

// 두 ID 집합이 동일한지 비교하여 ID 불일치 시 예외를 던짐
if (!studyDetails.equals(requests)) {
throw new CustomException(STUDY_DETAIL_ID_INVALID);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import jakarta.persistence.Embeddable;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import java.time.LocalDateTime;
import java.time.LocalTime;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.EqualsAndHashCode;
Expand All @@ -19,7 +19,7 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Session {

private LocalDateTime startAt;
private LocalTime startAt;

private String title;

Expand All @@ -33,8 +33,7 @@ public class Session {
private StudyStatus status;

@Builder(access = AccessLevel.PRIVATE)
private Session(
LocalDateTime startAt, String title, String description, Difficulty difficulty, StudyStatus status) {
private Session(LocalTime startAt, String title, String description, Difficulty difficulty, StudyStatus status) {
this.startAt = startAt;
this.title = title;
this.description = description;
Expand All @@ -45,4 +44,15 @@ private Session(
public static Session createEmptySession() {
return Session.builder().status(StudyStatus.NONE).build();
}

public static Session generateSession(
LocalTime startAt, String title, String description, Difficulty difficulty, StudyStatus status) {
return Session.builder()
.startAt(startAt)
.title(title)
.description(description)
.difficulty(difficulty)
.status(status)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.gdschongik.gdsc.domain.study.dto.request;

import com.gdschongik.gdsc.domain.study.domain.Difficulty;
import com.gdschongik.gdsc.domain.study.domain.StudyStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

public record StudySessionCreateRequest(
@NotNull Long studyDetailId,
@Schema(description = "제목") String title,
@Schema(description = "설명") String description,
@Schema(description = "난이도") Difficulty difficulty,
@Schema(description = "휴강 여부") StudyStatus status) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.gdschongik.gdsc.domain.study.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;

public record StudyUpdateRequest(
@Schema(description = "스터디 소개 페이지 링크") String notionLink,
@Schema(description = "스터디 한 줄 소개") String introduction,
List<StudySessionCreateRequest> studySessions) {}
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ public enum ErrorCode {
STUDY_DETAIL_UPDATE_RESTRICTED_TO_MENTOR(HttpStatus.CONFLICT, "해당 스터디의 멘토만 수정할 수 있습니다."),
STUDY_DETAIL_ASSIGNMENT_INVALID_DEADLINE(HttpStatus.CONFLICT, "마감기한이 지난 과제의 마감기한을 수정할 수 없습니다"),
STUDY_DETAIL_ASSIGNMENT_INVALID_UPDATE_DEADLINE(HttpStatus.CONFLICT, "수정하려고 하는 과제의 마감기한은 기존의 마감기한보다 빠르면 안됩니다."),

STUDY_DETAIL_ID_INVALID(HttpStatus.CONFLICT, "수정하려는 스터디 상세정보가 서버에 존재하지 않거나 유효하지 않습니다."),
STUDY_DETAIL_SESSION_SIZE_MISMATCH(HttpStatus.BAD_REQUEST, "스터디 커리큘럼의 총 개수가 일치하지 않습니다."),
// StudyHistory
STUDY_HISTORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 수강 기록입니다."),
STUDY_HISTORY_DUPLICATE(HttpStatus.CONFLICT, "이미 해당 스터디를 신청했습니다."),
Expand Down Expand Up @@ -168,7 +169,6 @@ public enum ErrorCode {
GITHUB_COMMIT_DATE_FETCH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "깃허브 커밋 날짜 조회에 실패했습니다."),
GITHUB_ASSIGNMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 과제 파일입니다."),
;

private final HttpStatus status;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.gdschongik.gdsc.domain.study.application;

import static com.gdschongik.gdsc.global.common.constant.StudyConstant.*;
import static com.gdschongik.gdsc.global.common.constant.StudyConstant.SESSION_DESCRIPTION;
import static org.assertj.core.api.Assertions.assertThat;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.member.domain.MemberRole;
import com.gdschongik.gdsc.domain.recruitment.domain.vo.Period;
import com.gdschongik.gdsc.domain.study.domain.Difficulty;
import com.gdschongik.gdsc.domain.study.domain.Study;
import com.gdschongik.gdsc.domain.study.domain.StudyDetail;
import com.gdschongik.gdsc.domain.study.domain.StudyStatus;
import com.gdschongik.gdsc.domain.study.dto.request.StudySessionCreateRequest;
import com.gdschongik.gdsc.domain.study.dto.request.StudyUpdateRequest;
import com.gdschongik.gdsc.helper.IntegrationTest;
import java.time.LocalDateTime;
import java.util.*;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;

public class MentorStudyServiceTest extends IntegrationTest {

@Autowired
private MentorStudyService mentorStudyService;

@Nested
class 스터디_정보_작성시 {

@Test
void 성공한다() {
// given
LocalDateTime now = STUDY_START_DATETIME;
Member mentor = createMentor();
Study study = createNewStudy(
mentor,
4L,
Period.createPeriod(now.plusDays(5), now.plusDays(10)),
Period.createPeriod(now.minusDays(5), now));
for (int i = 1; i <= 4; i++) {
Long week = (long) i;
createNewStudyDetail(week, study, now, now.plusDays(7));
now = now.plusDays(8);
}
logoutAndReloginAs(study.getMentor().getId(), MemberRole.ASSOCIATE);

List<StudySessionCreateRequest> sessionCreateRequests = new ArrayList<>();
for (int i = 1; i <= study.getTotalWeek(); i++) {
Long id = (long) i;
StudySessionCreateRequest sessionCreateRequest = new StudySessionCreateRequest(
id, SESSION_TITLE + i, SESSION_DESCRIPTION + i, Difficulty.HIGH, StudyStatus.OPEN);
sessionCreateRequests.add(sessionCreateRequest);
}

StudyUpdateRequest request =
new StudyUpdateRequest(STUDY_NOTION_LINK, STUDY_INTRODUCTION, sessionCreateRequests);

// when
mentorStudyService.updateStudy(1L, request);

// then
Study savedStudy = studyRepository.findById(study.getId()).get();
assertThat(savedStudy.getNotionLink()).isEqualTo(request.notionLink());

List<StudyDetail> studyDetails = studyDetailRepository.findAllByStudyId(1L);
for (int i = 0; i < studyDetails.size(); i++) {
StudyDetail studyDetail = studyDetails.get(i);
Long expectedId = studyDetail.getId();

assertThat(studyDetail.getId()).isEqualTo(expectedId);
assertThat(studyDetail.getSession().getTitle()).isEqualTo(SESSION_TITLE + expectedId);
assertThat(studyDetail.getSession().getDescription()).isEqualTo(SESSION_DESCRIPTION + expectedId);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import com.gdschongik.gdsc.global.exception.CustomException;
import com.gdschongik.gdsc.helper.FixtureHelper;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.LongStream;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -157,4 +160,34 @@ class 과제_수정시 {
.hasMessage(STUDY_DETAIL_ASSIGNMENT_INVALID_DEADLINE.getMessage());
}
}

@Nested
class 스터디_상세정보_작성시 {

@Test
void 존재하는_스터디상세정보_총개수와_요청된_스터디상세정보_총개수가_다르면_실패한다() {
// given
Set<Long> studyDetailIds = LongStream.rangeClosed(1, 4).boxed().collect(Collectors.toSet());

Set<Long> requestIds = LongStream.rangeClosed(1, 5).boxed().collect(Collectors.toSet());

// when & then
assertThatThrownBy(() -> studyDetailValidator.validateUpdateStudyDetail(studyDetailIds, requestIds))
.isInstanceOf(CustomException.class)
.hasMessage(STUDY_DETAIL_SESSION_SIZE_MISMATCH.getMessage());
}

@Test
void 요청한_상세정보_id와_기존의_상세정보_id가_맞지_않으면_실패한다() {
// given
Set<Long> studyDetailIds = LongStream.rangeClosed(1, 4).boxed().collect(Collectors.toSet());

Set<Long> requestIds = LongStream.rangeClosed(2, 5).boxed().collect(Collectors.toSet());

// when & then
assertThatThrownBy(() -> studyDetailValidator.validateUpdateStudyDetail(studyDetailIds, requestIds))
.isInstanceOf(CustomException.class)
.hasMessage(STUDY_DETAIL_ID_INVALID.getMessage());
}
}
}
Loading