Skip to content

Commit

Permalink
시험 제출자는 시험에 대한 제출들을 모아볼 수 있다. (#4)
Browse files Browse the repository at this point in the history
* refactor: issue, pr template 수정

* fix: redissonClient를 SpyBean으로 변경

* feat: exam에 isSingleAttempt 추가

* feat: 시험 응시 횟수 제한 수정 기능

* feat: 시험 응시 횟수 제한에 따라 분산락 적용과 미적용 서비스 사용

* fix: RedissonClient spybean으로 할 시 나머지 테스트 실패

* feat: 대시보드에 제출한 시험 탭 생성

* refactor: 헤더 드랍다운 한글로 변경

* refactor: card description line clamp 3

* refactor: date format 변경

* refactor: 시간 포맷 중 초단위는 제거

* feat: 제출한 시험들 목록을 조회하고, 최근에 제출한 순서로 정렬하는 API 구현

* feat: 제출한 시험 요약 목록 조회 구현

* feat: exam submission sidebar response

* feat: 내가 제출한 시험 목록 세부 사항 확인 페이지 구현

* refactor: web code format

* refactor: server code format

* refactor: 제출 확인 페이지 overflow y scroll

* fix: 시험 제작자, 시험 응시자가 아니면 답을 볼 수 없다.
  • Loading branch information
alstn113 authored Dec 31, 2024
1 parent 5076bb2 commit 89107e2
Show file tree
Hide file tree
Showing 61 changed files with 3,608 additions and 1,141 deletions.
15 changes: 10 additions & 5 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,34 @@ body:
- type: markdown
attributes:
value: |
버그 신고를 작성해주셔서 감사합니다! 🙏
버그 신고를 작성해주셔서 감사합니다! 🙏 여러분의 소중한 피드백이 시스템 개선에 큰 도움이 됩니다.
- type: textarea
id: what-happened
attributes:
label: 어떤 일이 발생했나요? 🤔
description: 또한, 어떤 결과를 기대했었는지 알려주세요.
description: 문제가 발생한 상황을 자세히 설명해주세요. 어떤 결과를 기대하셨는지도 함께 적어주세요.
placeholder: 예상치 못한 버그가 발생했습니다...
validations:
required: true
- type: textarea
id: what-caused
attributes:
label: 왜 발생했을까요?
description: 무슨 생각이 드는지 알려주세요.
description: 문제의 원인에 대한 개인적인 생각이나 추측을 적어주세요.
placeholder: 제 생각에는...
validations:
required: false
- type: textarea
id: logs
attributes:
label: 관련 로그 출력
description: 관련 로그 출력을 복사하여 붙여넣어주세요. 자동으로 코드 형식으로 서식이 지정됩니다.
label: 관련 로그 또는 오류 메시지
description: 문제와 관련된 로그나 오류 메시지를 복사하여 붙여넣어주세요. 코드 형식으로 자동 서식이 지정됩니다.
render: shell
- type: file
id: screenshot
attributes:
label: 스크린샷 첨부
description: 문제가 발생한 화면의 스크린샷을 첨부해주세요. (선택 사항)
- type: checkboxes
id: terms
attributes:
Expand Down
30 changes: 20 additions & 10 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
name: 기능 제안
description: 새로운 기능을 제안해주세요.
descrition: 추가하고 싶은 기능이나 개선 사항을 제안해주세요.
labels: ['Feature']
body:
- type: markdown
attributes:
value: |
새로운 기능을 제안해주셔서 감사합니다! 🙏
기능 구현을 제안해주셔서 감사합니다! 🙏 여러분의 소중한 의견이 시스템 개선에 큰 도움이 됩니다.
- type: textarea
id: new-feature
id: what-feature
attributes:
label: 어떤 기능을 제안하시나요? 🤔
description: 이 새로운 기능으로 어떤 효과를 기대하시나요? 🚀
placeholder: 이 기능이 있으면 좋겠다면, 이유를 적어주세요...
label: 어떤 기능을 추가하거나 개선하고 싶으신가요? 🤔
description: 제안하고자 하는 기능에 대해 자세히 설명해주세요.
placeholder: 새로운 기능을 추가하면 사용자들이...
validations:
required: true
- type: textarea
id: how
id: why-feature
attributes:
label: 이 기능은 어떻게 구현할 수 있을까요? 🛠️
description: 필요한 기술 스택이나 구현 방법 등을 알려주세요. 💻
placeholder: 이러한 방법으로 구현할 수 있습니다...
label: 왜 이 기능이 필요한가요?
description: 제안하고자 하는 기능이 시스템에 어떤 도움을 줄 수 있는지 설명해주세요.
placeholder: 이 기능을 추가하면 사용자들이...
validations:
required: true
- type: textarea
id: how-feature
attributes:
label: 어떻게 구현할 수 있을까요?
description: 제안하고자 하는 기능을 구현하는 방법에 대해 자유롭게 적어주세요.
placeholder: 이 기능을 추가하려면...
validations:
required: false
- type: checkboxes
id: terms
attributes:
Expand Down
18 changes: 4 additions & 14 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
## 구현 요약

이 부분을 제거하고 작업한 내용에 대해서 자유롭게 작성해주세요.

## 연관 이슈
## 연관된 이슈

이 부분을 제거하고 연관된 이슈를 아래와 같이 명시해 닫아주세요.

> ex) * close #12
## 참고
> ex) \* close #12
코드 리뷰에 `RCA 룰`을 적용할 시 참고해주세요.
## 작업 내용

| 헤더 | 설명 |
|---------------------|--------------------------------|
| R (Request Changes) | 적극적으로 반영을 고려해주세요 |
| C (Comment) | 웬만하면 반영해주세요 |
| A (Approve) | 반영해도 좋고, 넘어가도 좋습니다. 사소한 의견입니다. |
작업 내용을 간략하게 적어주세요.
13 changes: 13 additions & 0 deletions server/src/main/java/com/fluffy/exam/api/ExamController.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.fluffy.exam.application.response.ExamWithAnswersResponse;
import com.fluffy.exam.domain.ExamStatus;
import com.fluffy.exam.domain.dto.ExamSummaryDto;
import com.fluffy.exam.domain.dto.SubmittedExamSummaryDto;
import com.fluffy.global.response.PageResponse;
import com.fluffy.global.web.Accessor;
import com.fluffy.global.web.Auth;
Expand Down Expand Up @@ -79,6 +80,18 @@ public ResponseEntity<ExamWithAnswersResponse> getExamWithAnswers(
return ResponseEntity.ok(response);
}

@GetMapping("/api/v1/exams/submitted")
public ResponseEntity<PageResponse<SubmittedExamSummaryDto>> getSubmittedExamSummaries(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@Auth Accessor accessor
) {
Pageable pageable = PageRequest.of(page, size);
PageResponse<SubmittedExamSummaryDto> response = examQueryService.getSubmittedExamSummaries(pageable, accessor);

return ResponseEntity.ok(response);
}

@PostMapping("/api/v1/exams")
public ResponseEntity<CreateExamResponse> create(
@RequestBody @Valid CreateExamWebRequest request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.fluffy.exam.domain.ExamRepository;
import com.fluffy.exam.domain.ExamStatus;
import com.fluffy.exam.domain.dto.ExamSummaryDto;
import com.fluffy.exam.domain.dto.SubmittedExamSummaryDto;
import com.fluffy.global.exception.ForbiddenException;
import com.fluffy.global.response.PageResponse;
import com.fluffy.global.web.Accessor;
Expand All @@ -24,16 +25,16 @@ public class ExamQueryService {

@Transactional(readOnly = true)
public PageResponse<ExamSummaryDto> getPublishedExamSummaries(Pageable pageable) {
Page<ExamSummaryDto> examSummaries = examRepository.findPublishedExamSummaries(pageable);
Page<ExamSummaryDto> summaries = examRepository.findPublishedExamSummaries(pageable);

return PageResponse.of(examSummaries);
return PageResponse.of(summaries);
}

@Transactional(readOnly = true)
public PageResponse<ExamSummaryDto> getMyExamSummaries(Pageable pageable, ExamStatus status, Accessor accessor) {
Page<ExamSummaryDto> examSummaries = examRepository.findMyExamSummaries(pageable, status, accessor.id());
Page<ExamSummaryDto> summaries = examRepository.findMyExamSummaries(pageable, status, accessor.id());

return PageResponse.of(examSummaries);
return PageResponse.of(summaries);
}

@Transactional(readOnly = true)
Expand All @@ -53,4 +54,11 @@ public ExamWithAnswersResponse getExamWithAnswers(Long examId, Accessor accessor

return examMapper.toWithAnswersResponse(exam);
}

@Transactional(readOnly = true)
public PageResponse<SubmittedExamSummaryDto> getSubmittedExamSummaries(Pageable pageable, Accessor accessor) {
Page<SubmittedExamSummaryDto> summaries = examRepository.findSubmittedExamSummaries(pageable, accessor.id());

return PageResponse.of(summaries);
}
}
15 changes: 13 additions & 2 deletions server/src/main/java/com/fluffy/exam/domain/Exam.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public class Exam extends AuditableEntity {
@Embedded
private ExamPeriod examPeriod;

@Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT FALSE")
private boolean isSingleAttempt = false;

public static Exam create(String title, Long memberId) {
Exam exam = new Exam();
exam.title = new ExamTitle(title);
Expand Down Expand Up @@ -92,20 +95,28 @@ private void addQuestions(List<Question> questions) {

public void updateTitle(String title) {
if (status.isPublished()) {
throw new BadRequestException("시험이 출시된 후에는 정보를 수정할 수 없습니다.");
throw new BadRequestException("시험이 출시된 후에는 제목를 수정할 수 없습니다.");
}

this.title = new ExamTitle(title);
}

public void updateDescription(String description) {
if (status.isPublished()) {
throw new BadRequestException("시험이 출시된 후에는 정보를 수정할 수 없습니다.");
throw new BadRequestException("시험이 출시된 후에는 설명를 수정할 수 없습니다.");
}

this.description = new ExamDescription(description);
}

public void updateIsSingleAttempt(boolean isSingleAttempt) {
if (status.isPublished()) {
throw new BadRequestException("시험이 출시된 후에는 응시 횟수 제한을 수정할 수 없습니다.");
}

this.isSingleAttempt = isSingleAttempt;
}

public String getTitle() {
return title.getValue();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.fluffy.exam.domain;

import com.fluffy.exam.domain.dto.ExamSummaryDto;
import com.fluffy.exam.domain.dto.SubmittedExamSummaryDto;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

Expand All @@ -9,4 +10,6 @@ public interface ExamRepositoryCustom {
Page<ExamSummaryDto> findPublishedExamSummaries(Pageable pageable);

Page<ExamSummaryDto> findMyExamSummaries(Pageable pageable, ExamStatus status, Long memberId);

Page<SubmittedExamSummaryDto> findSubmittedExamSummaries(Pageable pageable, Long memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.fluffy.exam.domain.dto;

import com.querydsl.core.annotations.QueryProjection;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SubmittedExamSummaryDto {

private Long examId;
private String title;
private String description;
private AuthorDto author;
private Long submissionCount;
private LocalDateTime lastSubmissionDate;

@QueryProjection
public SubmittedExamSummaryDto(
Long examId,
String title,
String description,
AuthorDto author,
Long submissionCount,
LocalDateTime lastSubmissionDate
) {
this.examId = examId;
this.title = title;
this.description = description;
this.author = author;
this.submissionCount = submissionCount;
this.lastSubmissionDate = lastSubmissionDate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import static com.fluffy.auth.domain.QMember.member;
import static com.fluffy.exam.domain.QExam.exam;
import static com.fluffy.exam.domain.QQuestion.question;
import static com.fluffy.submission.domain.QSubmission.submission;

import com.fluffy.exam.domain.ExamRepositoryCustom;
import com.fluffy.exam.domain.ExamStatus;
import com.fluffy.exam.domain.dto.AuthorDto;
import com.fluffy.exam.domain.dto.ExamSummaryDto;
import com.fluffy.exam.domain.dto.QAuthorDto;
import com.fluffy.exam.domain.dto.QExamSummaryDto;
import com.fluffy.exam.domain.dto.QSubmittedExamSummaryDto;
import com.fluffy.exam.domain.dto.SubmittedExamSummaryDto;
import com.querydsl.core.types.ConstructorExpression;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
Expand Down Expand Up @@ -111,4 +114,40 @@ public Page<ExamSummaryDto> findMyExamSummaries(Pageable pageable, ExamStatus st

return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
}

@Override
public Page<SubmittedExamSummaryDto> findSubmittedExamSummaries(Pageable pageable, Long memberId) {
List<SubmittedExamSummaryDto> content = queryFactory
.select(new QSubmittedExamSummaryDto(
exam.id,
exam.title.value,
exam.description.value,
AUTHOR_PROJECTION,
submission.count(),
submission.createdAt.max()
))
.from(exam)
.join(submission).on(exam.id.eq(submission.examId))
.join(member).on(exam.memberId.eq(member.id))
.where(submission.memberId.eq(memberId))
.groupBy(
exam.id,
exam.title,
exam.description,
member.id,
member.name,
member.avatarUrl
)
.orderBy(submission.createdAt.max().desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

JPAQuery<Long> countQuery = queryFactory.select(submission.count())
.from(exam)
.join(submission).on(exam.id.eq(submission.examId))
.where(submission.memberId.eq(memberId));

return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.fluffy.submission.application.SubmissionQueryService;
import com.fluffy.submission.application.SubmissionService;
import com.fluffy.submission.application.response.SubmissionDetailResponse;
import com.fluffy.submission.domain.dto.MySubmissionSummaryDto;
import com.fluffy.submission.domain.dto.SubmissionSummaryDto;
import jakarta.validation.Valid;
import java.util.List;
Expand Down Expand Up @@ -45,14 +46,24 @@ public ResponseEntity<SubmissionDetailResponse> getDetail(
return ResponseEntity.ok(response);
}

@GetMapping("/api/v1/exams/{examId}/submissions/me")
public ResponseEntity<List<MySubmissionSummaryDto>> getMySubmissionSummaries(
@PathVariable Long examId,
@Auth Accessor accessor
) {
List<MySubmissionSummaryDto> response = submissionQueryService.getMySubmissionSummaries(examId, accessor);

return ResponseEntity.ok(response);
}


@PostMapping("/api/v1/exams/{examId}/submissions")
public ResponseEntity<Void> submit(
@PathVariable Long examId,
@RequestBody @Valid SubmissionWebRequest request,
@Auth Accessor accessor
) {
String lockName = "submit:%d:%d".formatted(examId, accessor.id());
submissionService.submit(request.toAppRequest(examId, accessor), lockName);
submissionService.submit(request.toAppRequest(examId, accessor));

return ResponseEntity.ok().build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.fluffy.submission.application;

import com.fluffy.auth.domain.Member;
import com.fluffy.exam.domain.Exam;
import com.fluffy.global.exception.BadRequestException;
import com.fluffy.global.redis.DistributedLock;
import com.fluffy.submission.application.request.SubmissionAppRequest;
import com.fluffy.submission.domain.Submission;
import com.fluffy.submission.domain.SubmissionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class SubmissionLockService {

private final SubmissionRepository submissionRepository;
private final SubmissionMapper submissionMapper;

@DistributedLock(key = "#lockName")
public void submitWithLock(SubmissionAppRequest request, Exam exam, Member member, String lockName) {
if (submissionRepository.existsByExamIdAndMemberId(exam.getId(), member.getId())) {
throw new BadRequestException("한 번만 제출 가능합니다.");
}

Submission submission = submissionMapper.toSubmission(exam, member.getId(), request);
submissionRepository.save(submission);
}
}
Loading

0 comments on commit 89107e2

Please sign in to comment.