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 all 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
@@ -0,0 +1,32 @@
package com.gdschongik.gdsc.domain.study.api;

import com.gdschongik.gdsc.domain.study.application.StudentStudyHistoryService;
import com.gdschongik.gdsc.domain.study.dto.request.RepositoryUpdateRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
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;

@Tag(name = "Student Study History", description = "사용자 스터디 수강 이력 API입니다.")
@RestController
@RequestMapping("/study-history")
@RequiredArgsConstructor
public class StudentStudyHistoryController {

private final StudentStudyHistoryService studentStudyHistoryService;

@Operation(summary = "레포지토리 입력", description = "레포지토리를 입력합니다. 이미 제출한 과제가 있다면 수정할 수 없습니다.")
@PutMapping("/{studyHistoryId}/repository")
public ResponseEntity<Void> updateRepository(
@PathVariable Long studyHistoryId, @Valid @RequestBody RepositoryUpdateRequest request) throws IOException {
studentStudyHistoryService.updateRepository(studyHistoryId, request);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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.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.global.exception.CustomException;
import com.gdschongik.gdsc.global.util.MemberUtil;
import com.gdschongik.gdsc.infra.client.github.GithubClient;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.kohsuke.github.GHRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class StudentStudyHistoryService {

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

@Transactional
public void updateRepository(Long studyHistoryId, RepositoryUpdateRequest request) throws IOException {
Member currentMember = memberUtil.getCurrentMember();
StudyHistory studyHistory = studyHistoryRepository
.findById(studyHistoryId)
.orElseThrow(() -> new CustomException(STUDY_HISTORY_NOT_FOUND));
Study study = studyHistory.getStudy();

boolean isAnyAssignmentSubmitted =
assignmentHistoryRepository.existsSubmittedAssignmentByMemberAndStudy(currentMember, study);
String ownerRepo = getOwnerRepo(request.repositoryLink());
GHRepository repository = githubClient.getRepository(ownerRepo);
studyHistoryValidator.validateUpdateRepository(
isAnyAssignmentSubmitted, String.valueOf(repository.getOwner().getId()), currentMember.getOauthId());

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

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

private String getOwnerRepo(String repositoryLink) {
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 @@ -24,6 +24,7 @@ public void validateApplyStudy(Study study, List<StudyHistory> currentMemberStud
}

// 이미 듣고 있는 스터디가 있는 경우
// todo: StudyHistory가 아닌 Study의 isOngoning 호출하도록 수정
boolean isInOngoingStudy = currentMemberStudyHistories.stream().anyMatch(StudyHistory::isStudyOngoing);

if (isInOngoingStudy) {
Expand All @@ -37,4 +38,17 @@ public void validateCancelStudyApply(Study study) {
throw new CustomException(STUDY_NOT_CANCELABLE_APPLICATION_PERIOD);
}
}

public void validateUpdateRepository(
boolean isAnyAssignmentSubmitted, String repositoryOwnerOauthId, String currentMemberOauthId) {
// 이미 제출한 과제가 있는 경우
if (isAnyAssignmentSubmitted) {
throw new CustomException(STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_ASSIGNMENT_ALREADY_SUBMITTED);
}

// 레포지토리 소유자가 현 멤버가 아닌 경우
if (!repositoryOwnerOauthId.equals(currentMemberOauthId)) {
throw new CustomException(STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_OWNER_MISMATCH);
}
}
}
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,9 @@ 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_ASSIGNMENT_ALREADY_SUBMITTED(
HttpStatus.CONFLICT, "이미 제출한 과제가 있으므로 레포지토리를 수정할 수 없습니다."),
STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_OWNER_MISMATCH(HttpStatus.CONFLICT, "레포지토리 소유자가 현재 멤버와 다릅니다."),

// Order
ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "주문이 존재하지 않습니다."),
Expand All @@ -142,7 +145,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
@@ -1,5 +1,6 @@
package com.gdschongik.gdsc.domain.study.domain;

import static com.gdschongik.gdsc.global.common.constant.MemberConstant.*;
import static com.gdschongik.gdsc.global.exception.ErrorCode.*;
import static org.assertj.core.api.Assertions.*;

Expand Down Expand Up @@ -96,4 +97,31 @@ class 스터디_수강신청_취소시 {
.hasMessage(STUDY_NOT_CANCELABLE_APPLICATION_PERIOD.getMessage());
}
}

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

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

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

@Test
void 레포지토리의_소유자와_현재_멤버가_일치하지_않는다면_실패한다() {
// given
String wrongOauthId = "1234567";

// when & then
assertThatThrownBy(() -> studyHistoryValidator.validateUpdateRepository(false, wrongOauthId, OAUTH_ID))
.isInstanceOf(CustomException.class)
.hasMessage(STUDY_HISTORY_REPOSITORY_NOT_UPDATABLE_OWNER_MISMATCH.getMessage());
}
}
}