Skip to content

Commit

Permalink
feat: 스터디 과제 제출하기 V2 API 구현 (#938)
Browse files Browse the repository at this point in the history
* feat: 스터치 회차 에러코드 추가

* feat: 스터디 회차 관련 조회 메서드 추가

* feat: 회차 ID로 스터디 조회하는 메서드 추가

* feat: 과제 제출 가능 검증 로직 추가

* feat: 과제 히스토리 레포지터리 추가

* feat: 과제 채점 도메인 서비스 V2 추가

* feat: 과제 제출하기 V2 API 구현

* feat: 누락된 리턴문 추가

* fix: 오타 수정

* refactor: 메서드 이름 수정
  • Loading branch information
uwoobeat authored Feb 27, 2025
1 parent c45e9b2 commit 3c5c595
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.gdschongik.gdsc.domain.studyv2.api;

import com.gdschongik.gdsc.domain.studyv2.application.StudentAssignmentHistoryServiceV2;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Student Assignment History V2", description = "학생 과제 제출이력 API입니다.")
@RestController
@RequestMapping("/v2/assignment-histories")
@RequiredArgsConstructor
public class StudentAssignmentHistoryControllerV2 {

private final StudentAssignmentHistoryServiceV2 studentAssignmentHistoryServiceV2;

@Operation(summary = "내 과제 제출하기", description = "나의 과제를 제출합니다. 제출된 과제는 채점되어 제출내역에 반영됩니다.")
@PostMapping("/submit")
public ResponseEntity<Void> submitMyAssignment(@RequestParam(name = "studySessionId") Long studySessionId) {
studentAssignmentHistoryServiceV2.submitMyAssignment(studySessionId);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.gdschongik.gdsc.domain.studyv2.application;

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

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionFetcher;
import com.gdschongik.gdsc.domain.studyv2.dao.AssignmentHistoryV2Repository;
import com.gdschongik.gdsc.domain.studyv2.dao.StudyHistoryV2Repository;
import com.gdschongik.gdsc.domain.studyv2.dao.StudyV2Repository;
import com.gdschongik.gdsc.domain.studyv2.domain.AssignmentHistoryGraderV2;
import com.gdschongik.gdsc.domain.studyv2.domain.AssignmentHistoryV2;
import com.gdschongik.gdsc.domain.studyv2.domain.AssignmentHistoryValidatorV2;
import com.gdschongik.gdsc.domain.studyv2.domain.StudyHistoryV2;
import com.gdschongik.gdsc.domain.studyv2.domain.StudySessionV2;
import com.gdschongik.gdsc.domain.studyv2.domain.StudyV2;
import com.gdschongik.gdsc.global.exception.CustomException;
import com.gdschongik.gdsc.global.util.MemberUtil;
import com.gdschongik.gdsc.infra.github.client.GithubClient;
import jakarta.transaction.Transactional;
import java.time.LocalDateTime;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class StudentAssignmentHistoryServiceV2 {

private final MemberUtil memberUtil;
private final GithubClient githubClient;
private final StudyV2Repository studyV2Repository;
private final StudyHistoryV2Repository studyHistoryV2Repository;
private final AssignmentHistoryV2Repository assignmentHistoryV2Repository;
private final AssignmentHistoryValidatorV2 assignmentHistoryValidatorV2;
private final AssignmentHistoryGraderV2 assignmentHistoryGraderV2;

@Transactional
public void submitMyAssignment(Long studySessionId) {
Member currentMember = memberUtil.getCurrentMember();
StudyV2 study = studyV2Repository
.findFetchBySessionId(studySessionId)
.orElseThrow(() -> new CustomException(STUDY_NOT_FOUND));
Optional<StudyHistoryV2> optionalStudyHistory =
studyHistoryV2Repository.findByStudentAndStudy(currentMember, study);

boolean isAppliedToStudy = optionalStudyHistory.isPresent();
StudySessionV2 studySession = study.getStudySession(studySessionId);
LocalDateTime now = LocalDateTime.now();

assignmentHistoryValidatorV2.validateSubmitAvailable(isAppliedToStudy, studySession, now);

String repositoryLink =
optionalStudyHistory.map(StudyHistoryV2::getRepositoryLink).orElse(null);
AssignmentSubmissionFetcher fetcher =
githubClient.getLatestAssignmentSubmissionFetcher(repositoryLink, studySession.getPosition());
AssignmentHistoryV2 assignmentHistory = findOrCreate(currentMember, studySession);

assignmentHistoryGraderV2.judge(fetcher, assignmentHistory);

assignmentHistoryV2Repository.save(assignmentHistory);

log.info(
"[StudentAssignmentHistoryServiceV2] 과제 제출: studySessionId={}, studentId={}, submissionStatus={}, submissionFailureType={}",
studySessionId,
currentMember.getId(),
assignmentHistory.getSubmissionStatus(),
assignmentHistory.getSubmissionFailureType());
}

private AssignmentHistoryV2 findOrCreate(Member student, StudySessionV2 studySession) {
return assignmentHistoryV2Repository
.findByMemberAndStudySession(student, studySession)
.orElseGet(() -> AssignmentHistoryV2.create(studySession, student));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.gdschongik.gdsc.domain.studyv2.dao;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.studyv2.domain.AssignmentHistoryV2;
import com.gdschongik.gdsc.domain.studyv2.domain.StudySessionV2;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AssignmentHistoryV2Repository extends JpaRepository<AssignmentHistoryV2, Long> {
Optional<AssignmentHistoryV2> findByMemberAndStudySession(Member member, StudySessionV2 studySession);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
public interface StudyV2CustomRepository {
Optional<StudyV2> findFetchById(Long id);

Optional<StudyV2> findFetchBySessionId(Long sessionId);

List<StudyV2> findFetchAll();
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ public Optional<StudyV2> findFetchById(Long id) {
.fetchOne());
}

@Override
public Optional<StudyV2> findFetchBySessionId(Long sessionId) {
return Optional.ofNullable(queryFactory
.selectFrom(studyV2)
.join(studyV2.studySessions)
.fetchJoin()
.where(studyV2.studySessions.any().id.eq(sessionId))
.fetchOne());
}

@Override
public List<StudyV2> findFetchAll() {
return queryFactory
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.gdschongik.gdsc.domain.studyv2.domain;

import static com.gdschongik.gdsc.domain.study.domain.SubmissionFailureType.*;
import static com.gdschongik.gdsc.global.exception.ErrorCode.*;

import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmission;
import com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionFetcher;
import com.gdschongik.gdsc.domain.study.domain.SubmissionFailureType;
import com.gdschongik.gdsc.global.annotation.DomainService;
import com.gdschongik.gdsc.global.exception.CustomException;
import com.gdschongik.gdsc.global.exception.ErrorCode;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@DomainService
public class AssignmentHistoryGraderV2 {

private static final int MINIMUM_ASSIGNMENT_CONTENT_LENGTH = 300;

public void judge(AssignmentSubmissionFetcher assignmentSubmissionFetcher, AssignmentHistoryV2 assignmentHistory) {
try {
AssignmentSubmission assignmentSubmission = assignmentSubmissionFetcher.fetch();
judgeAssignmentSubmission(assignmentSubmission, assignmentHistory);
} catch (CustomException e) {
SubmissionFailureType failureType = translateException(e);
assignmentHistory.fail(failureType);
}
}

private void judgeAssignmentSubmission(
AssignmentSubmission assignmentSubmission, AssignmentHistoryV2 assignmentHistory) {
if (assignmentSubmission.contentLength() < MINIMUM_ASSIGNMENT_CONTENT_LENGTH) {
assignmentHistory.fail(WORD_COUNT_INSUFFICIENT);
return;
}

assignmentHistory.success(
assignmentSubmission.url(),
assignmentSubmission.commitHash(),
assignmentSubmission.contentLength(),
assignmentSubmission.committedAt());
}

private SubmissionFailureType translateException(CustomException e) {
ErrorCode errorCode = e.getErrorCode();

if (errorCode == GITHUB_CONTENT_NOT_FOUND) {
return LOCATION_UNIDENTIFIABLE;
}

log.warn("[AssignmentHistoryGrader] 과제 제출정보 조회 중 알 수 없는 오류 발생: {}", e.getMessage());

return UNKNOWN;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.gdschongik.gdsc.domain.studyv2.domain;

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

import com.gdschongik.gdsc.global.annotation.DomainService;
import com.gdschongik.gdsc.global.exception.CustomException;
import java.time.LocalDateTime;

@DomainService
public class AssignmentHistoryValidatorV2 {

/**
* 채점을 수행하기 전, 과제 제출이 가능한지 검증합니다.
*/
public void validateSubmitAvailable(boolean isAppliedToStudy, StudySessionV2 studySession, LocalDateTime now) {
if (!isAppliedToStudy) {
throw new CustomException(ASSIGNMENT_STUDY_NOT_APPLIED);
}

studySession.validateAssignmentSubmittable(now);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -107,6 +108,22 @@ public static void createEmptyForAssignment(Integer position, StudyV2 study) {
StudySessionV2.builder().position(position).study(study).build();
}

// 데이터 전달 로직

public void validateAssignmentSubmittable(LocalDateTime now) {
if (assignmentPeriod == null) {
throw new CustomException(ASSIGNMENT_SUBMIT_NOT_PUBLISHED);
}

if (now.isBefore(assignmentPeriod.getStartDate())) {
throw new CustomException(ASSIGNMENT_SUBMIT_NOT_STARTED);
}

if (now.isAfter(assignmentPeriod.getEndDate())) {
throw new CustomException(ASSIGNMENT_SUBMIT_DEADLINE_PASSED);
}
}

// 데이터 변경 로직

public void update(StudyUpdateCommand.Session command) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -197,6 +198,18 @@ public static StudyV2 createAssignment(
.build();
}

// 데이터 조회 로직

public Optional<StudySessionV2> getOptionalStudySession(Long studySessionId) {
return studySessions.stream()
.filter(session -> session.getId().equals(studySessionId))
.findFirst();
}

public StudySessionV2 getStudySession(Long studySessionId) {
return getOptionalStudySession(studySessionId).orElseThrow(() -> new CustomException(STUDY_SESSION_NOT_FOUND));
}

// 데이터 변경 로직

public void update(StudyUpdateCommand command) {
Expand All @@ -208,16 +221,14 @@ public void update(StudyUpdateCommand command) {
this.endTime = command.endTime();

command.studySessions().forEach(sessionCommand -> {
getStudySession(sessionCommand.studySessionId()).update(sessionCommand);
getStudySessionForUpdate(sessionCommand.studySessionId()).update(sessionCommand);
});

validateLessonTimeOrderMatchesPosition();
}

private StudySessionV2 getStudySession(Long studySessionId) {
return studySessions.stream()
.filter(session -> session.getId().equals(studySessionId))
.findFirst()
private StudySessionV2 getStudySessionForUpdate(Long studySessionId) {
return getOptionalStudySession(studySessionId)
.orElseThrow(() -> new CustomException(STUDY_NOT_UPDATABLE_SESSION_NOT_FOUND));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ public enum ErrorCode {
STUDY_TIME_INVALID(HttpStatus.CONFLICT, "스터디종료 시각이 스터디시작 시각보다 빠릅니다."),
ASSIGNMENT_STUDY_CAN_NOT_INPUT_STUDY_TIME(HttpStatus.CONFLICT, "과제 스터디는 스터디 시간을 입력할 수 없습니다."),
STUDY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디입니다."),
STUDY_SESSION_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디 회차입니다."),
STUDY_NOT_APPLICABLE(HttpStatus.CONFLICT, "스터디 신청기간이 아닙니다."),
STUDY_NOT_CANCELABLE_APPLICATION_PERIOD(HttpStatus.CONFLICT, "스터디 신청기간이 아니라면 취소할 수 없습니다."),
STUDY_NOT_CREATABLE_NOT_LIVE(HttpStatus.INTERNAL_SERVER_ERROR, "온라인 및 오프라인 타입만 라이브 스터디로 생성할 수 있습니다."),
Expand Down

0 comments on commit 3c5c595

Please sign in to comment.