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 18 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,30 @@
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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -30,6 +40,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 +102,39 @@ 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);
studyDetailValidator.validateUpdateStudyDetail(studyDetails, request.studySessions());

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.

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


Map<Long, StudySessionCreateRequest> requestMap = request.studySessions().stream()
Copy link
Member

Choose a reason for hiding this comment

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

굳이 맵으로 만들 필요 없이 리스트에서 바로 찾아서 반환하면 될듯 합니다.

.collect(Collectors.toMap(StudySessionCreateRequest::studyDetailId, Function.identity()));

List<StudyDetail> updatedStudyDetails = new ArrayList<>();
Copy link
Member

Choose a reason for hiding this comment

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

굳이 updatedStudyDetails를 한번 더 담는 이유가 뭔가요?

for (StudyDetail studyDetail : studyDetails) {
Long id = studyDetail.getId();
StudySessionCreateRequest matchingSession = requestMap.get(id);

studyDetail.updateSession(
studyDetail.getStudy().getPeriod().getStartDate(),
Copy link
Member

Choose a reason for hiding this comment

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

세션 시작시간이 왜 항상 스터디 시작일이어야 하는 건가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

피그마상에서 세션 시작시간에 대해 요청을 받고 있지 않더라고요
그래서 startAt이 LocalDateTime으로 되어있길래 시작 날짜라고 생각햇습니다
이 부분은 기획분들한테 정확히 물어보겠습니다

matchingSession.title(),
matchingSession.description(),
matchingSession.difficulty(),
matchingSession.status());

updatedStudyDetails.add(studyDetail);
}
studyDetailRepository.saveAll(updatedStudyDetails);
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 intro) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public void update(String link, String intro) {
public void update(String link, String introduction) {

Copy link
Member Author

Choose a reason for hiding this comment

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

위에 필드랑 겹쳐서 오류가 나서 intro로 만들었습니다

Copy link
Member

Choose a reason for hiding this comment

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

그러면 this로 명시해서 넣으면 되지 않나요?

Copy link
Member Author

@AlmondBreez3 AlmondBreez3 Aug 18, 2024

Choose a reason for hiding this comment

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

아아 이전에 History쪽에서는 repository를 repo로 하신거 생각하고 이런식으로 처리를 하는건가보다~해서 intro라고 했습니다 다시 보니까 다 this로 바꿔져있군요!
this 쓰겠습니다

notionLink = link;
introduction = intro;
}
Copy link

Choose a reason for hiding this comment

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

필드 업데이트에 대한 유효성 검토 필요

update 메서드는 notionLinkintroduction을 직접 업데이트합니다. 입력 값에 대한 유효성 검사를 추가하여 잘못된 데이터가 저장되는 것을 방지하는 것이 좋습니다.

public void update(String link, String intro) {
    if (link == null || link.isEmpty()) {
        throw new IllegalArgumentException("Notion link cannot be null or empty");
    }
    if (intro == null || intro.isEmpty()) {
        throw new IllegalArgumentException("Introduction cannot be null or empty");
    }
    notionLink = link;
    introduction = intro;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ public LocalDate getAttendanceDay() {
- study.getStartDate().getDayOfWeek().getValue());
}

public void updateSession(
LocalDateTime 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 @@ -4,9 +4,13 @@

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.study.dto.request.AssignmentCreateUpdateRequest;
import com.gdschongik.gdsc.domain.study.dto.request.StudySessionCreateRequest;
import com.gdschongik.gdsc.global.annotation.DomainService;
import com.gdschongik.gdsc.global.exception.CustomException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

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

public void validateUpdateStudyDetail(List<StudyDetail> studyDetails, List<StudySessionCreateRequest> requests) {
Copy link
Member

Choose a reason for hiding this comment

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

validator는 도메인 계층에 속한 도메인서비스이기 때문에, 외부에 대한 의존성을 가져서는 안됩니다.
표현 계층에서 사용하는 Request를 직접 인자로 받는 것은 적절하지 않습니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

현재validator에서 만든 Set들을 서비스 레이어에서 만들어서넘기겠습니다

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

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

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

// 두 ID 집합이 동일한지 비교하여 ID 불일치 시 예외를 던짐
if (!studyDetailIds.equals(requestIds)) {
throw new CustomException(STUDY_DETAIL_ID_INVALID);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,15 @@ private Session(
public static Session createEmptySession() {
return Session.builder().status(StudyStatus.NONE).build();
}

public static Session generateSession(
LocalDateTime 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,79 @@
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 = LocalDateTime.now();
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());
assertThat(savedStudy.getIntroduction()).isEqualTo(request.introduction());

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);
assertThat(studyDetail.getSession().getDifficulty()).isEqualTo(Difficulty.HIGH);
Copy link
Member

Choose a reason for hiding this comment

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

불필요하게 모든 필드를 전부 검증하지 않아도 됩니다.

assertThat(studyDetail.getSession().getStatus()).isEqualTo(StudyStatus.OPEN);
}
}
}
}
Loading