Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.gamzabat.algohub.feature.notification.exception.CannotFoundNotificationException;
import com.gamzabat.algohub.feature.notification.exception.CannotFoundNotificationSettingException;
import com.gamzabat.algohub.feature.notification.exception.NotificationValidationException;
import com.gamzabat.algohub.feature.problem.exception.InvalidRequestException;
import com.gamzabat.algohub.feature.problem.exception.NotBojLinkException;
import com.gamzabat.algohub.feature.problem.exception.SolvedAcApiErrorException;
import com.gamzabat.algohub.feature.solution.exception.CannotFoundSolutionException;
Expand Down Expand Up @@ -231,4 +232,12 @@ protected ResponseEntity<ErrorResponse> handleCannotFoundVerificationCodeExcepti
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(HttpStatus.BAD_REQUEST.value(), e.getMessage(), null));
}

@ExceptionHandler(InvalidRequestException.class)
protected ResponseEntity<ErrorResponse> handleInvalidRequestException(
InvalidRequestException e) {
return ResponseEntity
.status(e.getStatus())
.body(new ErrorResponse(e.getStatus().value(), e.getError(), null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.parameters.P;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -23,6 +26,8 @@
import com.gamzabat.algohub.feature.problem.dto.CreateProblemRequest;
import com.gamzabat.algohub.feature.problem.dto.EditProblemRequest;
import com.gamzabat.algohub.feature.problem.dto.GetProblemResponse;
import com.gamzabat.algohub.feature.problem.enums.ProblemListStatus;
import com.gamzabat.algohub.feature.problem.exception.InvalidRequestException;
import com.gamzabat.algohub.feature.problem.service.ProblemService;
import com.gamzabat.algohub.feature.user.domain.User;

Expand Down Expand Up @@ -59,26 +64,22 @@ public ResponseEntity<Void> editProblemDeadline(@AuthedUser User user,
return ResponseEntity.ok().build();
}

@GetMapping(value = "/groups/{groupId}/problems/in-progress")
@Operation(summary = "์ง„ํ–‰ ์ค‘์ธ ๋ฌธ์ œ ๋ชฉ๋ก ์กฐํšŒ API", description = "ํŠน์ • ๊ทธ๋ฃน์— ๋Œ€ํ•œ ๋ฌธ์ œ๋ฅผ ๋ชจ๋‘ ์กฐํšŒํ•˜๋Š” API")
public ResponseEntity<Page<GetProblemResponse>> getInProgressProblemList(@AuthedUser User user,
@GetMapping(value = "/groups/{groupId}/problems")
@Operation(summary = "๋ฌธ์ œ ๋ชฉ๋ก ์กฐํšŒ API", description = "ํŠน์ • ๊ทธ๋ฃน์— ๋Œ€ํ•œ ๋ฌธ์ œ๋ฅผ ๋ถ€๋ถ„ ์กฐํšŒํ•˜๋Š” API")
public ResponseEntity<Page<GetProblemResponse>> getProblems(@AuthedUser User user,
@PathVariable Long groupId,
@RequestParam(name = "unsolved-only") Boolean unsolvedOnly,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by(PROBLEM_SORT_BY).descending());
Page<GetProblemResponse> response = problemService.getInProgressProblems(user, groupId, unsolvedOnly, pageable);
return ResponseEntity.ok().body(response);
}
@RequestParam ProblemListStatus status,
@RequestParam(name = "unsolved-only", required = false) Boolean unsolvedOnly,
@PageableDefault(page = 0, size = 20, sort = "endDate", direction = Sort.Direction.DESC)
Pageable pageable) {
if (status == ProblemListStatus.IN_PROGRESS) {
if (unsolvedOnly == null) {
throw new InvalidRequestException(HttpStatus.BAD_REQUEST, "IN_PROGRESS ์ƒํƒœ์—์„œ๋Š” unsolvedOnly ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.");
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

๊ฐœ์ธ์ ์œผ๋กœ๋Š” IN_PROGRESS ์ƒํƒœ์— ๋Œ€ํ•ด์„œ unsolvedOnly ๊ฐ’์ด null๋กœ ๋“ค์–ด์˜ค๋ฉด unsolvedOnly=false๋ผ๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•œ ์˜๋ฏธ๋ผ๊ณ  ์ƒ๊ฐ์ด ๋“œ๋Š”๋ฐ์š”! ๊ทธ๋ž˜์„œ ์œ„ ๋ถ€๋ถ„์— ๋Œ€ํ•œ ๊ฒ€์ฆ์ด ์—†์–ด๋„ ๋˜์ง€ ์•Š์„๊นŒ ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค. ์–ด๋–ป๊ฒŒ ์ƒ๊ฐํ•˜์‹œ๋‚˜์š”?!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

์Œ ์ด unsolvedOnly๊ฐ€ ์ดํ›„์—์„œ findAllInProgressProblem ์ด ์ฟผ๋ฆฌ๋ถ€๋ถ„์—์„œ ์‚ฌ์šฉ๋˜์–ด์„œ์š”! null๋กœ ๋“ค์–ด์˜ค๋ฉด NPE๊ฐ€ ๋ฐœ์ƒํ•ด์„œ ๊ฒ€์ฆ์„ ๊ฑธ์–ด๋‘”๊ฑฐ๊ธดํ•ด์š”.
์‚ฌ์‹ค ๋งํ•œ๊ฑฐ์— ๋™์˜ํ•˜๊ธฐ๋Š” ํ•ด์„œ ์„œ๋น„์Šค ๋กœ์ง๋ถ€๋ถ„์—์„œ null ์ธ๊ฒฝ์šฐ false๋กœ ๋ฐ”๊พธ๋„๋ก ํ• ๊นŒ์š”?

Copy link
Contributor

Choose a reason for hiding this comment

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

์ €๋Š” ์„œ๋น„์Šค๊ณ„์ธต์—์„œ false๋กœ ๋ณ€๊ฒฝํ•ด๋„ ๋  ๊ฒƒ ๊ฐ™์•„๋ณด์ž…๋‹ˆ๋‹ค!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

๋„ต ๊ทธ๋ ‡๊ฒŒ ํ• ๊ฒŒ์š”~!


Page<GetProblemResponse> response = problemService.getProblems(user, groupId, status, unsolvedOnly, pageable);

@GetMapping(value = "/groups/{groupId}/problems/expired")
@Operation(summary = "๋งˆ๊ฐ ๋œ ๋ฌธ์ œ ๋ชฉ๋ก ์กฐํšŒ API", description = "ํŠน์ • ๊ทธ๋ฃน์— ๋Œ€ํ•œ ๋ฌธ์ œ๋ฅผ ๋ชจ๋‘ ์กฐํšŒํ•˜๋Š” API")
public ResponseEntity<Page<GetProblemResponse>> getExpiredProblemList(@AuthedUser User user,
@PathVariable Long groupId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by(PROBLEM_SORT_BY).descending());
Page<GetProblemResponse> response = problemService.getExpiredProblems(user, groupId, pageable);
return ResponseEntity.ok().body(response);
}

Expand All @@ -89,23 +90,6 @@ public ResponseEntity<GetProblemResponse> getProblem(@AuthedUser User user, @Pat
return ResponseEntity.ok().body(response);
}

@GetMapping("/groups/{groupId}/problems/deadline-reached")
@Operation(summary = "๋งˆ๊ฐ ๊ธฐํ•œ์ด ๋‚ด์ผ๊นŒ์ง€์ธ ๋ฌธ์ œ๋“ค ์กฐํšŒ API")
public ResponseEntity<List<GetProblemResponse>> getDeadlineReachedProblemList(@AuthedUser User user,
@PathVariable Long groupId) {
return ResponseEntity.ok().body(problemService.getDeadlineReachedProblemList(user, groupId));
}

@GetMapping("/groups/{groupId}/problems/queued")
@Operation(summary = "์‹œ์ž‘ ์˜ˆ์ •์ธ ๋ฌธ์ œ๋“ค ์กฐํšŒ API")
public ResponseEntity<Page<GetProblemResponse>> getQueuedProblemList(@AuthedUser User user,
@PathVariable Long groupId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by(PROBLEM_SORT_BY).descending());
return ResponseEntity.ok().body(problemService.getQueuedProblems(user, groupId, pageable));
}

@DeleteMapping(value = "/problems/{problemId}")
@Operation(summary = "๋ฌธ์ œ ์‚ญ์ œ API")
public ResponseEntity<Void> deleteProblem(@AuthedUser User user, @PathVariable Long problemId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.gamzabat.algohub.feature.problem.enums;

public enum ProblemListStatus {
IN_PROGRESS,
EXPIRED,
QUEUED

}
Copy link
Contributor

Choose a reason for hiding this comment

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

์˜ˆ์™ธ ๊ฐ์ฒด record๋กœ ๋งŒ๋“ค๋ฉด ํŽธํ•˜๊ฒŒ ์“ธ ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์•„์š”!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

record๋ฅผ ์‚ฌ์šฉํ•œ ๊ฒƒ์— ์ด์ ์ด ๋ญ˜๊นŒ์š”?! ์–ด์ฒ˜ํ”ผ ์ง€๊ธˆ ๊ตฌํ˜„ํ•˜๋Š” ์˜ˆ์™ธ ๊ฐ์ฒด๋“ค์€ ๋Œ€๋ถ€๋ถ„ status, error๋กœ ๋‹จ์ˆœํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋‹ˆ๊นŒ record๋กœ ๊ด€๋ฆฌํ•˜๋Š”๊ฒŒ ํŽธํ•œ๊ฑด๊ฐ€? record์‚ฌ์šฉ์‹œ ํ™•์žฅ์„ฑ์€ ์žƒ๋Š”๋‹ค๊ณ ํ•ด์„œ์š”..!!

Copy link
Contributor

Choose a reason for hiding this comment

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

์ €๋Š” ๊ทธ๋ƒฅ ์ƒ์„ฑ์ž, getter ๋ช…์‹œ๊ฐ€ ์—†์–ด๋„ ๋˜์–ด์ง€๋‹ˆ, ์ด๋ ‡๊ฒŒ ๋ถˆ๋ณ€์ ์ธ ํด๋ž˜์Šค๋Š” ์›ฌ๋งŒํ•˜๋ฉด record๋กœ ๋งŒ๋“ค์–ด์„œ ๊ฐ„๊ฒฐํ•จ์„ ์œ ์ง€ํ•˜๋ ค๊ณ  ํ•˜๋Š” ํŽธ์ž…๋‹ˆ๋‹ค!
ํ™•์žฅ์„ฑ์€ ์˜ˆ์™ธ ๊ฐ์ฒด์—์„œ๋Š” ๊ณ ๋ คํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ•ด์š”. BaseException ๊ฐ™์€ ๊ฐ์ฒด๋ฉด ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ, ์ด๋Ÿฐ ์ƒ์„ธ ๊ฒฝ์šฐ์— ๋Œ€ํ•œ Exception ๊ฐ์ฒด๋Š” ํ™•์žฅํ•  ๊ฒฝ์šฐ๊ฐ€ ์—†๋‹ค๊ณ  ์ƒ๊ฐํ•ด์„œ์š”!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

๋„ต ๋‹ค์Œ ์˜ˆ์™ธ ํด๋ž˜์Šค ๋งŒ๋“ค๋•Œ๋งˆ๋‹ค record๋กœ ๋งŒ๋“ค๊ฒŒ์š”! ์ด๊ฑฐ ๋งŒ๋“ ๊ฒŒ ํ•„์š”์—†์–ด์ ธ์„œ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.gamzabat.algohub.feature.problem.exception;

import org.springframework.http.HttpStatus;

import lombok.Getter;

@Getter
public class InvalidRequestException extends RuntimeException {
private final HttpStatus status;
private final String error;

public InvalidRequestException(HttpStatus status, String error) {
this.status = status;
this.error = error;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

๋งˆ์ง€๋ง‰ ์ค„ ๊ฐœํ–‰ ์ถ”๊ฐ€ ๋ถ€ํƒํ•ฉ๋‹ˆ๋‹ค!

Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import com.gamzabat.algohub.feature.problem.dto.CreateProblemRequest;
import com.gamzabat.algohub.feature.problem.dto.EditProblemRequest;
import com.gamzabat.algohub.feature.problem.dto.GetProblemResponse;
import com.gamzabat.algohub.feature.problem.enums.ProblemListStatus;
import com.gamzabat.algohub.feature.problem.exception.NotBojLinkException;
import com.gamzabat.algohub.feature.problem.exception.SolvedAcApiErrorException;
import com.gamzabat.algohub.feature.problem.repository.ProblemRepository;
Expand Down Expand Up @@ -148,6 +149,19 @@ private void checkProblemStartDate(EditProblemRequest request, Problem problem)
"๋ฌธ์ œ ์‹œ์ž‘ ๋‚ ์งœ๋Š” ๋งˆ๊ฐ ๋‚ ์งœ ์ดํ›„๋กœ ์ˆ˜์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
}

@Transactional(readOnly = true)
public Page<GetProblemResponse> getProblems(User user, Long groupId, ProblemListStatus status, Boolean unsolvedOnly, Pageable pageable) {
Page<GetProblemResponse> response;
if (status == ProblemListStatus.IN_PROGRESS) {
response = getInProgressProblems(user, groupId, unsolvedOnly, pageable);
} else if (status == ProblemListStatus.EXPIRED) {
response = getExpiredProblems(user, groupId, pageable);
} else {
response = getQueuedProblems(user, groupId, pageable);
}
return response;
}

@Transactional(readOnly = true)
public Page<GetProblemResponse> getInProgressProblems(User user, Long groupId, Boolean unsolvedOnly,
Pageable pageable) {
Expand Down Expand Up @@ -213,37 +227,6 @@ public void deleteProblem(User user, Long problemId) {
log.info("success to delete problem user_id={} , problem_id = {}", user.getId(), problemId);
}

@Transactional(readOnly = true)
public List<GetProblemResponse> getDeadlineReachedProblemList(User user, Long groupId) {
StudyGroup group = getGroup(groupId);
if (!groupMemberRepository.existsByUserAndStudyGroup(user, group))
throw new ProblemValidationException(HttpStatus.FORBIDDEN.value(), "๋ฌธ์ œ๋ฅผ ์กฐํšŒํ•  ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.");

List<Problem> problems = problemRepository.findAllByStudyGroupAndEndDateBetween(group, LocalDate.now(),
LocalDate.now().plusDays(1));
problems.sort(Comparator.comparing(Problem::getEndDate));

return problems.stream().map(problem -> {
Integer correctCount = solutionRepository.countDistinctUsersWithCorrectSolutionsByProblemId(problem.getId(),
BOJResultConstants.CORRECT);
Integer submitMemberCount = solutionRepository.countDistinctUsersByProblem(problem);
Integer groupMemberCount = groupMemberRepository.countMembersByStudyGroup(group);
Integer accuracy = calculateAccuracy(submitMemberCount, correctCount);

return new GetProblemResponse(
problem.getTitle(),
problem.getId(),
problem.getLink(),
problem.getStartDate(),
problem.getEndDate(),
problem.getLevel(),
solutionRepository.existsByUserAndProblemAndResult(user, problem, BOJResultConstants.CORRECT),
submitMemberCount,
groupMemberCount,
accuracy);
}).toList();
}

@Transactional(readOnly = true)
public Page<GetProblemResponse> getQueuedProblems(User user, Long groupId, Pageable pageable) {
StudyGroup group = getGroup(groupId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.internal.matchers.Null;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
Expand All @@ -41,6 +42,8 @@
import com.gamzabat.algohub.feature.problem.dto.CreateProblemRequest;
import com.gamzabat.algohub.feature.problem.dto.EditProblemRequest;
import com.gamzabat.algohub.feature.problem.dto.GetProblemResponse;
import com.gamzabat.algohub.feature.problem.enums.ProblemListStatus;
import com.gamzabat.algohub.feature.problem.exception.InvalidRequestException;
import com.gamzabat.algohub.feature.problem.exception.SolvedAcApiErrorException;
import com.gamzabat.algohub.feature.problem.repository.ProblemRepository;
import com.gamzabat.algohub.feature.problem.service.ProblemService;
Expand Down Expand Up @@ -300,38 +303,39 @@ void editProblemDeadlineFailed_6() throws Exception {
}

@Test
@DisplayName("์ง„ํ–‰ ์ค‘์ธ ๋ฌธ์ œ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต")
@DisplayName("๋ฌธ์ œ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต")
void getProblemList() throws Exception {
// given
Pageable pageable = PageRequest.of(0, 20, Sort.by("endDate").descending());
Page<GetProblemResponse> response = new PageImpl<>(new ArrayList<>());
when(problemService.getInProgressProblems(any(User.class), anyLong(), eq(false),
when(problemService.getProblems(any(User.class), anyLong(), eq(ProblemListStatus.IN_PROGRESS) ,eq(false),
any(Pageable.class))).thenReturn(
response);
// when, then
mockMvc.perform(get("/api/groups/{groupId}/problems/in-progress", groupId)
mockMvc.perform(get("/api/groups/{groupId}/problems", groupId)
.header("Authorization", token)
.param("unsolved-only", String.valueOf(false)))
.param("page", "0")
.param("size", "20")
.param("unsolved-only", String.valueOf(false))
.param("status", String.valueOf(ProblemListStatus.IN_PROGRESS)))
.andExpect(status().isOk())
.andExpect(content().string(objectMapper.writeValueAsString(response)));
verify(problemService, times(1)).getInProgressProblems(user, groupId, false, pageable);
verify(problemService, times(1)).getProblems(user, groupId,ProblemListStatus.IN_PROGRESS,false, pageable);
}

@Test
@DisplayName("๋ฌธ์ œ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ : ๊ถŒํ•œ ์—†์Œ")
@DisplayName("๋ฌธ์ œ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ : ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์š”์ฒญ")
void getProblemListFailed_1() throws Exception {
// given
Pageable pageable = PageRequest.of(0, 20, Sort.by("endDate").descending());
when(
problemService.getInProgressProblems(any(User.class), anyLong(), eq(false), any(Pageable.class))).thenThrow(
new ProblemValidationException(HttpStatus.FORBIDDEN.value(), "๋ฌธ์ œ๋ฅผ ์กฐํšŒํ•  ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."));

// when, then
mockMvc.perform(get("/api/groups/{groupId}/problems/in-progress", groupId)
mockMvc.perform(get("/api/groups/{groupId}/problems", groupId)
.header("Authorization", token)
.param("unsolved-only", String.valueOf(false)))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.error").value("๋ฌธ์ œ๋ฅผ ์กฐํšŒํ•  ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."));
verify(problemService, times(1)).getInProgressProblems(user, groupId, false, pageable);
.param("status", String.valueOf(ProblemListStatus.IN_PROGRESS)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error").value("IN_PROGRESS ์ƒํƒœ์—์„œ๋Š” unsolvedOnly ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."));
verifyNoInteractions(problemService);
}

@Test
Expand All @@ -343,6 +347,7 @@ void deleteProblem() throws Exception {
mockMvc.perform(delete("/api/problems/{problemId}", problemId)
.header("Authorization", token)
.param("problemId", String.valueOf(problemId)))

.andExpect(status().isOk());
verify(problemService, times(1)).deleteProblem(user, problemId);
}
Expand Down
Loading