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 추가 #594

Merged
merged 16 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
@@ -1,16 +1,20 @@
package com.gdschongik.gdsc.domain.study.api;

import com.gdschongik.gdsc.domain.study.application.StudyService;
import com.gdschongik.gdsc.domain.study.dto.request.RepositoryUpdateRequest;
import com.gdschongik.gdsc.domain.study.dto.response.StudyResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
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.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand Down Expand Up @@ -42,4 +46,12 @@ public ResponseEntity<Void> cancelStudyApply(@PathVariable Long studyId) {
studyService.cancelStudyApply(studyId);
return ResponseEntity.noContent().build();
}

@Operation(summary = "레포지토리 입력", description = "레포지토리를 입력합니다. 이미 제출한 과제가 있다면 수정할 수 없습니다.")
@PatchMapping("/{studyId}/repository")
Copy link
Contributor

Choose a reason for hiding this comment

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

이건 궁금증인데 혹시 이 플젝에서 Patch Put을 쓰는 기준이 있나요? 어떤건 PUT인거같고 어떤건 PATCH인거같아서요!
저는 이전 플젝들 할때는 PATCH로 고정해서 사용했었거든요!

Copy link
Member Author

Choose a reason for hiding this comment

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

테이블에 있는 필드들을 전반적으로 수정할 때는 put을 사용하고 있고요. 특정 필드만 수정할 때는 patch 사용하고 있습니다!

Copy link
Member

Choose a reason for hiding this comment

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

저는 이것도 put이 맞는 것 같네요
patch -> 해당 endpoint가 나타내는 리소스의 일부분만 수정
put -> 해당 endpoint가 나타내는 리소스의 전체를 수정 + 덮어쓰기

그리고 레포지터리는 수강이력에서 관리되는 리소스이기 때문에 endpoint도 수정되어야 합니다

public ResponseEntity<Void> updateRepository(
@PathVariable Long studyId, @Valid @RequestBody RepositoryUpdateRequest request) {
studyService.updateRepository(studyId, request);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package com.gdschongik.gdsc.domain.study.application;

import static com.gdschongik.gdsc.global.common.constant.GithubConstant.*;
import static com.gdschongik.gdsc.global.exception.ErrorCode.*;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.study.dao.AssignmentHistoryRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyHistoryRepository;
import com.gdschongik.gdsc.domain.study.dao.StudyRepository;
import com.gdschongik.gdsc.domain.study.domain.Study;
import com.gdschongik.gdsc.domain.study.domain.StudyHistory;
import com.gdschongik.gdsc.domain.study.domain.StudyHistoryValidator;
import com.gdschongik.gdsc.domain.study.dto.request.RepositoryUpdateRequest;
import com.gdschongik.gdsc.domain.study.dto.response.StudyResponse;
import com.gdschongik.gdsc.global.exception.CustomException;
import com.gdschongik.gdsc.global.util.MemberUtil;
import com.gdschongik.gdsc.infra.client.github.GithubClient;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -24,8 +28,10 @@
public class StudyService {

private final MemberUtil memberUtil;
private final GithubClient githubClient;
private final StudyRepository studyRepository;
private final StudyHistoryRepository studyHistoryRepository;
private final AssignmentHistoryRepository assignmentHistoryRepository;
private final StudyHistoryValidator studyHistoryValidator;

public List<StudyResponse> getAllApplicableStudies() {
Expand Down Expand Up @@ -64,4 +70,34 @@ public void cancelStudyApply(Long studyId) {

log.info("[StudyService] 스터디 수강신청 취소: studyId={}, memberId={}", study.getId(), currentMember.getId());
}

@Transactional
public void updateRepository(Long studyId, RepositoryUpdateRequest request) {
Member currentMember = memberUtil.getCurrentMember();
Study study = studyRepository.findById(studyId).orElseThrow(() -> new CustomException(STUDY_NOT_FOUND));

StudyHistory studyHistory = studyHistoryRepository
.findByMenteeAndStudy(currentMember, study)
.orElseThrow(() -> new CustomException(STUDY_HISTORY_NOT_FOUND));

boolean isAnyAssignmentSubmitted =
assignmentHistoryRepository.existsSubmittedAssignmentByMemberAndStudy(currentMember, study);
studyHistoryValidator.validateUpdateRepository(isAnyAssignmentSubmitted);
validateRepositoryLink(request.repositoryLink());

studyHistory.updateRepositoryLink(request.repositoryLink());
studyHistoryRepository.save(studyHistory);

log.info("[StudentStudyService] 레포지토리 입력: studyHistoryId={}", studyHistory.getId());
}

private void validateRepositoryLink(String repositoryLink) {
String ownerRepo = getOwnerRepo(repositoryLink);
githubClient.getRepository(ownerRepo);
}

private String getOwnerRepo(String repositoryLink) {
Copy link
Member

Choose a reason for hiding this comment

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

REpository라고 끝까지 치는게 더 좋지 않을까용

int startIndex = repositoryLink.indexOf(GITHUB_DOMAIN) + GITHUB_DOMAIN.length();
return repositoryLink.substring(startIndex);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.gdschongik.gdsc.domain.study.dao;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.study.domain.Study;

public interface AssignmentHistoryCustomRepository {

boolean existsSubmittedAssignmentByMemberAndStudy(Member member, Study study);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.gdschongik.gdsc.domain.study.dao;

import static com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus.*;
import static com.gdschongik.gdsc.domain.study.domain.QAssignmentHistory.*;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.study.domain.Study;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class AssignmentHistoryCustomRepositoryImpl implements AssignmentHistoryCustomRepository {

private final JPAQueryFactory queryFactory;

@Override
public boolean existsSubmittedAssignmentByMemberAndStudy(Member member, Study study) {
Integer fetchOne = queryFactory
.selectOne()
.from(assignmentHistory)
.where(eqMember(member), eqStudy(study), isSubmitted())
.fetchFirst();

return fetchOne != null;
}

private BooleanExpression eqMember(Member member) {
return member == null ? null : assignmentHistory.member.eq(member);
}

private BooleanExpression eqStudy(Study study) {
return study == null ? null : assignmentHistory.studyDetail.study.eq(study);
}

private BooleanExpression isSubmitted() {
return assignmentHistory.submissionStatus.in(FAILURE, SUCCESS);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.gdschongik.gdsc.domain.study.dao;

import com.gdschongik.gdsc.domain.study.domain.AssignmentHistory;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AssignmentHistoryRepository
extends JpaRepository<AssignmentHistory, Long>, AssignmentHistoryCustomRepository {}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public class StudyHistory extends BaseEntity {
@JoinColumn(name = "study_id")
private Study study;

private String repositoryLink;

@Builder(access = AccessLevel.PRIVATE)
private StudyHistory(Member mentee, Study study) {
this.mentee = mentee;
Expand All @@ -43,6 +45,13 @@ public static StudyHistory create(Member mentee, Study study) {
return StudyHistory.builder().mentee(mentee).study(study).build();
}

/**
* 레포지토리 링크를 업데이트합니다.
*/
public void updateRepositoryLink(String repositoryLink) {
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.

이 부분은 피그마에 '과제 최초 제출 시'까지 변경 가능하다고 써있더라고요.
validator에서 과제 제출 이력이 있는지 검증하는 것도 같은 이유입니다!

this.repositoryLink = repositoryLink;
}

// 데이터 전달 로직
public boolean isStudyOngoing() {
Copy link
Member

Choose a reason for hiding this comment

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

이건 해당 PR건은 아니지만 올바른 책임의 메서드가 아닌 것 같네요
현재 스터디가 진행중인지의 여부를 반환하는 것에 대한 책임은 스터디 클래스가 가져야 합니다
스터디 히스토리가 아니라요

Copy link
Member Author

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.

++ todo 달아두겠습니다~

return study.isStudyOngoing();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,11 @@ public void validateCancelStudyApply(Study study) {
throw new CustomException(STUDY_NOT_CANCELABLE_APPLICATION_PERIOD);
}
}

public void validateUpdateRepository(boolean isAnyAssignmentSubmitted) {
// 이미 제출한 과제가 있는 경우
if (isAnyAssignmentSubmitted) {
throw new CustomException(STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.gdschongik.gdsc.domain.study.dto.request;

import jakarta.validation.constraints.NotBlank;

public record RepositoryUpdateRequest(@NotBlank String repositoryLink) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.gdschongik.gdsc.global.common.constant;

public class GithubConstant {

public static final String GITHUB_DOMAIN = "github.com/";

private GithubConstant() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public enum ErrorCode {
STUDY_HISTORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 수강 기록입니다."),
STUDY_HISTORY_DUPLICATE(HttpStatus.CONFLICT, "이미 해당 스터디를 신청했습니다."),
STUDY_HISTORY_ONGOING_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 진행중인 스터디가 있습니다."),
STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE(HttpStatus.CONFLICT, "이미 제출한 과제가 있으므로 레포지토리를 수정할 수 없습니다."),

// Order
ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "주문이 존재하지 않습니다."),
Expand All @@ -142,7 +143,10 @@ public enum ErrorCode {

// Assignment
ASSIGNMENT_CAN_NOT_BE_UPDATED(HttpStatus.CONFLICT, "휴강인 과제는 수정할 수 없습니다."),
ASSIGNMENT_DEADLINE_INVALID(HttpStatus.CONFLICT, "과제 마감 기한이 현재보다 빠릅니다.");
ASSIGNMENT_DEADLINE_INVALID(HttpStatus.CONFLICT, "과제 마감 기한이 현재보다 빠릅니다."),

// Github
GITHUB_REPOSITORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 레포지토리입니다.");

private final HttpStatus status;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.gdschongik.gdsc.infra.client.github;

import com.gdschongik.gdsc.global.exception.CustomException;
import com.gdschongik.gdsc.global.exception.ErrorCode;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;
import org.springframework.stereotype.Component;

Expand All @@ -9,4 +13,12 @@
public class GithubClient {

private final GitHub github;

public GHRepository getRepository(String ownerRepo) {
try {
return github.getRepository(ownerRepo);
} catch (IOException e) {
throw new CustomException(ErrorCode.GITHUB_REPOSITORY_NOT_FOUND);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,19 @@ class 스터디_수강신청_취소시 {
.hasMessage(STUDY_NOT_CANCELABLE_APPLICATION_PERIOD.getMessage());
}
}

@Nested
class 레포지토리_입력시 {

@Test
void 이미_제출한_과제가_있다면_실패한다() {
// given
boolean isAnyAssignmentSubmitted = true;

// when & then
assertThatThrownBy(() -> studyHistoryValidator.validateUpdateRepository(isAnyAssignmentSubmitted))
.isInstanceOf(CustomException.class)
.hasMessage(STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE.getMessage());
}
}
}