Skip to content

Commit

Permalink
DailyDefense 컨트롤러 작성 및 시험 타이머 기능 개발 (#36)
Browse files Browse the repository at this point in the history
* feat: solved problemnumber set 반환

* feat: 사용자 로그인 여부에 따라 오늘의 문제 정보를 반환하는 service 및 테스트 개발

* feat: 오늘의 문제 시험 정보, 랭킹 정보 페이지로 반환하는 서비스

* fix: RankUseCaseImpl 패키지 이동 및 AtomicLong long 캐스팅

* test: DailyDefenseInfoResponse Test 추가

* refactor: DTO 생성 로직 Mapper로 분리

* feat: 오늘의 문제 정보 반환, 오늘의 문제 랭킹 Controller 및 테스트 작성

* 🎨 Mapper 기본 생성자 private 변경

* 🎨 필요없는 중괄호 삭제

* 🔥 Remove unused import 'StartDailyDefenseResponse'

* 🎨 Dailydefense mapper 생성

* fix: @JsonInclude 어노테이션 필드로 이동

* fix: dailydefense 조회 시 problem fecth join

* ✨ DailyDefense 시작 시 문제 content 포함 dto 변경

* ✨ 시험 시작 시 문제 본문 가져오게 구현, 테스트코드 작성

* fix: 응용서비스에서 Port 이용하게 변경

* 🐛 WebClient 정상적으로 mocking되도록 변경

* 🔥 Remove unused import

* 🔥 사용하지 않는 throws 제거

* ✨ Webclient retryWhen을 통해 재처리 로직 추가

* ♻️ Problem Content 책임에 따라 problem_information하위로 이동

* ⚡ TempCode hashmap enummap으로 변경

* 🔥 Remove unused import

* 🎨 기본생성자 private으로 변경

* Google OAuth 적용 및 커스텀 예외 코드 추가 (#37)

* ✨ Google OAuth 적용 및 커스텀 예외 추가

* ✏️ 패키지 구조 일부 변경

* ✏️ 출력 관련 코드 제거

* ✏️ MemberAdapter 코드 수정

* ✏️ OAuth Login과 관련한 Cookie 발급과 관련한 작업을 Service 단에서 처리하도록 수정

* ✏️ OAuth AccessToken 발급과 관련한 메서드 분리

* ✏️ GoogleUserDto에 @Setter, @AllArgsConstructor 제거

* ✏️ SetDomain 관련 url, path를 yml에 저장하여 관리

* ✏️ ErrorCode 관련 코드 수정 및 RestControllerAdvice 코드 수정

* ✏️ OAuth domain 관련 패키지 구조 변경

* ✏️ LoginUseCase를 목적에 맞게 AuthenticationUseCase로 변경 (인증과 관련한 작업)

* ✏️ JwtToken 및 publicKey와 PrivateKey를 발급하는 코드 수정

* ✏️ LoginMember에서 Repository를 호출하는 형태 변경 및 패키지 구조 일부 변경

* ✏️ Filter를 통과하는 url 리스트를 정규 표현식으로 검사하도록 수정

* ✏️ Spring Security Filter와 관련한 코드 수정

* ✏️ CORS 설정을 시큐리티 기본값에서 직접 정의한 내용으로 수정

* ✨ RefreshToken 관리를 위한 Redis 환경 구성

* ✨ RefreshToken을 검증하여 accessToken을 재발급하도록 코드 추가

* ✏️ Redis에 저장하는 refreshToken에 대한 key 값을 명확하게 하기 위해 코드 수정

* ✏️ RefreshToken을 Redis에 저장하는 로직 수정

* ✏️ OAuth 정보와 관련된 DTO를 OAuthUserInfo, GoogleOAuthUserInfo 형태로 변경

* ✏️ RefreshToken을 구하는 로직 수정

* ✏️ JwtAuthenticationFilter 구조 변경 (jwtProvider, authenticationProvider, isIgnoredURIManager로 분리)

* ✏️ JwtAuthenticationFilter에서 else if를 if로 수정

* ✏️ AccessToken, RefreshToken을 검사하는 필터에서 주석 추가

* ✏️ Cookie 발급 로직을 CookieUtils에서 처리하도록 수정

* ✏️ Security, OAuth 부분 패키지 구조 변경

* ✏️ ErrorCode 및 일부 패키지 구조 수정

* ✏️ 사용하지 않는 ErrorCode 삭제

* ✏️ RestControllerAdvice에서 쿠키를 받는 로직을 cookieUtils 를 사용하도록 수정

* ✏️ OAuth 관련 패키지 구조 일부 수정

* ✏️ Jwt 검증과 관련한 메서드를 JwtValidator로 분리

* ✏️ Securty, Jwt, OAuth 관련 패키지 분리

* ✏️ Domain security 관련 패키지를 Application로 이동

* ✏️ 패키지 구조 일부 변경

* 🎨 RefreshToken 오류 수정 및 임시 coverage 하향

* 🎨 Google oauth 예외처리

* 🐛 Google oauth  예외처리

---------

Co-authored-by: miiiinju1 <[email protected]>

* ✨ 시험 시작 시 문제 본문 가져오게 구현, 테스트코드 작성

* ✅ Spring Security & WebMvcTest 충돌 해결

* 🔥 충돌 해결

* 🔥 Remove unused import

* ✨ HandlerMethodArgumentResolver 추가 및 controller 반영

* ✨ SetAuthentication 로직 변경

* 🎨 GetDailydefenseInfo 로직 변경

* 🎨 로그인 여부 관계없는 API로직 filter 반영

* ✨ 제한 시간 후 시험 자동 종료 로직 추가

* 🎨 도메인 서비스 구조 변경

* 🎨 ConcurrentHashMap 대신 구조적으로 타이머 등록 1회 보장

* 🎨 롤백시 타이머 정상 제거를 위해 Timer Event 발행 방식 선택

* ✅ 롤백 테스트 추가

* ✨ RestDocs 추가 및 docstest 작성

* 📝 오늘의 문제 랭킹 restdocs 추가

* 🔥 Remove unused import

* 🔥 Remove unused import

* ✅ 테스트 환경 통합

* 🎨 DailyDefenseProblemStrategy로 이름 변경

---------

Co-authored-by: Jeong Yong Choi <[email protected]>
  • Loading branch information
miiiinju1 and aj4941 authored May 5, 2024
1 parent 50ad3ac commit 9c31e95
Show file tree
Hide file tree
Showing 117 changed files with 3,700 additions and 411 deletions.
53 changes: 47 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id 'java'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.4'
id 'org.asciidoctor.jvm.convert' version '3.3.2'
}
plugins {
id "org.sonarqube" version "4.4.1.3373"
Expand Down Expand Up @@ -65,39 +66,55 @@ configurations {
compileOnly {
extendsFrom annotationProcessor
}

asciidoctorExt
}

repositories {
mavenCentral()
}

dependencies {

// Spring Data Jpa
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// Spring Data Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

// validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

implementation 'org.springframework.boot:spring-boot-starter-web'

// WebFlux (WebClient)
implementation 'org.springframework.boot:spring-boot-starter-webflux'

implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
// Spring Web
implementation 'org.springframework.boot:spring-boot-starter-web'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'

runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'

// MariaDB
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'

// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// Test Dependencies
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'com.h2database:h2'

// RestDocs
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}

tasks.named('sonarqube').configure {
Expand All @@ -109,7 +126,31 @@ tasks.named('test') {
finalizedBy 'jacocoTestReport'
}

ext { //전역 변수
snippetsDir = file("build/generated-snippets")
}

test {
outputs.dir snippetsDir
}

asciidoctor {
inputs.dir snippetsDir
configurations 'asciidoctorExt'

sources { // 특정 파일만 html로 만든다
include '**/index.adoc'
}
baseDirFollowsSourceFile() // 다른 adoc 파일을 참조할 때 경로를 baseDir 기준으로 찾음
dependsOn test
}

bootJar {
dependsOn asciidoctor
from("${asciidoctor.outputDir}") {
into 'static/docs'
}
}

jacocoTestReport {
dependsOn test
Expand Down
28 changes: 28 additions & 0 deletions src/docs/asciidoc/api/dailydefense/dailydefense.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[[Daily-Defense]]
== 오늘의 문제 정보 조회

=== Request
include::{snippets}/daily-defense-info/http-request.adoc[]

=== Response
include::{snippets}/daily-defense-info/http-response.adoc[]
include::{snippets}/daily-defense-info/response-fields.adoc[]

== 오늘의 문제 기록 조회

=== Request
include::{snippets}/daily-defense-ranking/http-request.adoc[]

=== Response
include::{snippets}/daily-defense-ranking/http-response.adoc[]
include::{snippets}/daily-defense-ranking/response-fields.adoc[]


== 오늘의 문제 시작

=== Request
include::{snippets}/daily-defense-start/http-request.adoc[]

=== Response
include::{snippets}/daily-defense-start/http-response.adoc[]
include::{snippets}/daily-defense-start/response-fields.adoc[]
16 changes: 16 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
ifndef::snippets[]
:snippets: ../../build/generated-snippets
endif::[]
= 모두의 랜덤 디펜스 REST API
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 1
:sectlinks:

include::api/dailydefense/dailydefense.adoc[]



[[Daily-Defense-List]]
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

@Configuration
public class WebClientConfig {

@Bean
public WebClient webClient() {
return WebClient.builder().build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package kr.co.morandi.backend.common.config;

import kr.co.morandi.backend.common.web.resolver.MemberHandlerMethodArgumentResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

@Bean
MemberHandlerMethodArgumentResolver memberHandlerMethodArgumentResolver() {
return new MemberHandlerMethodArgumentResolver();
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(memberHandlerMethodArgumentResolver());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ public class MorandiException extends RuntimeException {
private final ErrorCode errorCode;

public MorandiException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}

public MorandiException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,13 @@
import kr.co.morandi.backend.member_management.infrastructure.config.cookie.utils.CookieUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.net.URI;

import static kr.co.morandi.backend.common.exception.handler.exception.CommonErrorCode.INTERNAL_SERVER_ERROR;
import static kr.co.morandi.backend.member_management.infrastructure.config.jwt.constants.TokenType.REFRESH_TOKEN;

Expand All @@ -28,9 +24,6 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

private final CookieUtils cookieUtils;

@Value("${oauth2.signup-url}")
private String signupPath;

@ExceptionHandler(MorandiException.class)
public ResponseEntity<ErrorResponse> morandiExceptionHandler(MorandiException e) {
log.error(e.getErrorCode().name()+" : ", e.getErrorCode().getMessage() + " : ", e);
Expand All @@ -39,8 +32,7 @@ public ResponseEntity<ErrorResponse> morandiExceptionHandler(MorandiException e)
if (e.getErrorCode().getHttpStatus() == HttpStatus.UNAUTHORIZED) {
HttpHeaders headers = createUnauthorizedHeaders();

// 로그인 페이지로 리다이렉트
return new ResponseEntity<>(headers, HttpStatus.FOUND);
return new ResponseEntity<>(headers, HttpStatus.UNAUTHORIZED);
}

// 그 외의 에러가 발생한 경우
Expand All @@ -61,11 +53,9 @@ public ResponseEntity<String> handleAllException(Exception e) {
/**
* Unauthorized 에러가 발생한 경우
* Refresh Token 쿠키를 제거하고
* 로그인 페이지로 리다이렉트
*/
private HttpHeaders createUnauthorizedHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setLocation(URI.create(signupPath));
Cookie cookie = cookieUtils.removeCookie(REFRESH_TOKEN, null);
headers.add("Set-Cookie", cookie.toString());
return headers;
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/kr/co/morandi/backend/common/web/MemberId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kr.co.morandi.backend.common.web;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface MemberId {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package kr.co.morandi.backend.common.web.resolver;

import kr.co.morandi.backend.common.web.MemberId;
import kr.co.morandi.backend.member_management.infrastructure.config.security.utils.SecurityUtils;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class MemberHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(MemberId.class) != null
&& parameter.getParameterType().equals(Long.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return SecurityUtils.getCurrentMemberId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package kr.co.morandi.backend.defense_information.application.dto.response;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class DailyDefenseInfoResponse {

private String defenseName;
private Integer problemCount;
private Long attemptCount;
private List<DailyDefenseProblemInfoResponse> problems;


@Builder
private DailyDefenseInfoResponse(String defenseName, Integer problemCount, Long attemptCount, List<DailyDefenseProblemInfoResponse> problems) {
this.defenseName = defenseName;
this.problemCount = problemCount;
this.attemptCount = attemptCount;
this.problems = problems;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package kr.co.morandi.backend.defense_information.application.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import kr.co.morandi.backend.defense_information.domain.model.defense.ProblemTier;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class DailyDefenseProblemInfoResponse {

private Long problemNumber;
private Long problemId;
private Long baekjoonProblemId;
private ProblemTier difficulty;
private Long solvedCount;
private Long submitCount;

@JsonInclude(JsonInclude.Include.NON_NULL)
private Boolean isSolved;

@Builder
private DailyDefenseProblemInfoResponse(Long problemNumber, Long problemId, Long baekjoonProblemId, ProblemTier difficulty, Long solvedCount, Long submitCount, Boolean isSolved) {
this.problemNumber = problemNumber;
this.problemId = problemId;
this.baekjoonProblemId = baekjoonProblemId;
this.difficulty = difficulty;
this.solvedCount = solvedCount;
this.submitCount = submitCount;
this.isSolved = isSolved;
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package kr.co.morandi.backend.defense_information.application.mapper.dailydefense;

import kr.co.morandi.backend.defense_information.application.dto.response.DailyDefenseInfoResponse;
import kr.co.morandi.backend.defense_information.domain.model.dailydefense.DailyDefense;
import kr.co.morandi.backend.defense_record.domain.model.dailydefense_record.DailyRecord;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DailyDefenseInfoMapper {

public static DailyDefenseInfoResponse fromNonAttempted(DailyDefense dailyDefense) {
return DailyDefenseInfoResponse.builder()
.defenseName(dailyDefense.getContentName())
.problemCount(dailyDefense.getProblemCount())
.attemptCount(dailyDefense.getAttemptCount())
.problems(DailyDefenseProblemInfoMapper.ofNonAttempted(dailyDefense.getDailyDefenseProblems()))
.build();
}

public static DailyDefenseInfoResponse ofAttempted(DailyDefense dailyDefense, DailyRecord dailyRecord) {
return DailyDefenseInfoResponse.builder()
.defenseName(dailyDefense.getContentName())
.problemCount(dailyDefense.getProblemCount())
.attemptCount(dailyDefense.getAttemptCount())
.problems(DailyDefenseProblemInfoMapper.ofAttempted(
dailyDefense.getDailyDefenseProblems(),
dailyRecord.getSolvedProblemNumbers())
)
.build();
}
}
Loading

0 comments on commit 9c31e95

Please sign in to comment.