diff --git a/.gitignore b/.gitignore
index 4718b889..a80f0a69 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,4 +48,5 @@ backend/src/main/resources/static/api/docs/**
backend/src/main/resources/application-dev.yml
backend/src/main/resources/application-local.yml
backend/src/main/resources/application-prod.yml
-backend/src/apis/**
\ No newline at end of file
+backend/src/main/resources/aws-s3.yml
+backend/src/apis/**
diff --git a/README.md b/README.md
index 9f03330b..716f186c 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,50 @@
-## 2021-botobo
+
+
+
+
+
+
+
+
+
+
+
+ 보고 또 보고는 취준생, 학생을 위한 반복 학습 장려 서비스입니다.
+면접 준비는 보고 또 보고 😎
+
+
+
+
+## 🐸 보고 또 보고
+* [보고 또 보고 바로가기](https://botobo.kro.kr)
+* [보고 또 보고 이야기](https://github.com/woowacourse-teams/2021-botobo/wiki)
+
+
+## 구성원 👨👩👧👧
+
+| [카일](https://github.com/gwangyeol-im) | [디토](https://github.com/dudtjr913) | [중간곰](https://github.com/ggyool) | [오즈](https://github.com/ohjoohyung) | [피케이](https://github.com/pkeugine) | [조앤](https://github.com/seovalue) |
+| :----------: | :--------: | :---------: | :---------: | :---------: | :---------: |
+| | | | | | |
+|프론트엔드 담당✨|프론트엔드 담당✨| 백엔드 담당🎢 |백엔드 담당🎢|백엔드 담당🎢|백엔드 담당🎢|
+
+
+## 🏡 팀 문화
+
+
+
+
+
+
+## 💻 기술 스택
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/backend/build.gradle b/backend/build.gradle
index 6c92a8f3..46f09b16 100644
--- a/backend/build.gradle
+++ b/backend/build.gradle
@@ -1,7 +1,9 @@
plugins {
id "org.asciidoctor.convert" version "1.5.9.2"
+ id "org.sonarqube" version "3.3"
id 'org.springframework.boot' version '2.4.3'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
+ id 'jacoco'
id 'java'
}
@@ -47,13 +49,30 @@ dependencies {
// jwt
implementation 'io.jsonwebtoken:jjwt:0.9.1'
+ // bind
implementation 'javax.xml.bind:jaxb-api:2.1'
+
+ // aws s3
+ implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
+
+ //test containers
+ implementation("com.amazonaws:aws-java-sdk-s3")
+ testImplementation "org.testcontainers:localstack:1.15.3"
+ compile group: 'commons-fileupload', name: 'commons-fileupload', version: '1.4'
+
+ // FileNameUtils
+ implementation 'org.apache.tika:tika-core:2.0.0'
+
+ // Flyway
+ implementation('org.flywaydb:flyway-core:6.4.2')
}
ext {
snippetsDir = file('build/generated-snippets')
}
+processResources.dependsOn('copyDev')
+
task copyDev(type: Copy) {
from '../dev/application-prod.yml'
into './src/main/resources'
@@ -63,10 +82,59 @@ task copyDev(type: Copy) {
from '../dev/application-local.yml'
into './src/main/resources'
+
+ from '../dev/aws-s3.yml'
+ into './src/main/resources'
}
test {
+ jacoco {
+ destinationFile = file("$buildDir/jacoco/jacoco.exec")
+ }
useJUnitPlatform()
+ finalizedBy 'jacocoTestReport'
+}
+
+jacoco {
+ toolVersion = '0.8.5'
+}
+
+jacocoTestReport {
+ reports {
+ html.enabled true
+ xml.enabled true
+ csv.enabled false
+ }
+ finalizedBy 'jacocoTestCoverageVerification'
+}
+
+jacocoTestCoverageVerification {
+ violationRules {
+ rule {
+ enabled = true
+ element = 'CLASS'
+
+ limit {
+ counter = 'BRANCH'
+ value = 'COVEREDRATIO'
+ minimum = 0.80
+ }
+
+ limit {
+ counter = 'LINE'
+ value = 'COVEREDRATIO'
+ minimum = 0.80
+ }
+
+ excludes = [
+ 'botobo.core.infrastructure.**',
+ 'botobo.core.exception.**',
+ 'botobo.core.dto.**',
+ 'botobo.core.DataLoader',
+ 'botobo.core.BotoboApplication'
+ ]
+ }
+ }
}
asciidoctor.doFirst {
@@ -78,6 +146,25 @@ asciidoctor {
dependsOn test
}
+sonarqube {
+ properties {
+ property "sonar.projectKey", "botobo-develop"
+ property "sonar.host.url", System.getenv('SONAR_URL')
+ property "sonar.login", System.getenv('SONAR_LOGIN')
+ property "sonar.language", "java"
+ property "sonar.binaries", "$buildDir/classes"
+ property "sonar.sources", "src/main"
+ property "sonar.tests", "src/test/java"
+ property "sonar.junit.reportsPath", "$buildDir/test-reports"
+ property "sonar.java.coveragePlugin", "jacoco"
+ property "sonar.coverage.jacoco.reportPaths", "$buildDir/jacoco/jacoco.exec"
+ property "sonar.exclusions", "**/exception/**, " +
+ "**/dto/**, " +
+ "**/DataLoader.java, " +
+ "**/BotoboApplication.java"
+ }
+}
+
task createDocument(type: Copy) {
dependsOn asciidoctor
@@ -86,6 +173,5 @@ task createDocument(type: Copy) {
}
build {
- dependsOn copyDev
dependsOn createDocument
}
diff --git a/backend/lombok.config b/backend/lombok.config
new file mode 100644
index 00000000..7a21e880
--- /dev/null
+++ b/backend/lombok.config
@@ -0,0 +1 @@
+lombok.addLombokGeneratedAnnotation = true
diff --git a/backend/src/docs/asciidoc/categories.adoc b/backend/src/docs/asciidoc/categories.adoc
deleted file mode 100644
index a720b385..00000000
--- a/backend/src/docs/asciidoc/categories.adoc
+++ /dev/null
@@ -1,13 +0,0 @@
-== 카테고리 API
-
-=== 카테고리 전체 보기
-==== 요청
-include::{snippets}/categories-get-success/http-request.adoc[]
-==== 응답
-include::{snippets}/categories-get-success/http-response.adoc[]
-
-=== 카테고리별 카드 모아보기
-==== 요청
-include::{snippets}/categories-cards-get-success/http-request.adoc[]
-==== 응답
-include::{snippets}/categories-cards-get-success/http-response.adoc[]
\ No newline at end of file
diff --git a/backend/src/docs/asciidoc/errors.adoc b/backend/src/docs/asciidoc/errors.adoc
new file mode 100644
index 00000000..1faf99a8
--- /dev/null
+++ b/backend/src/docs/asciidoc/errors.adoc
@@ -0,0 +1,68 @@
+== 에러 상세
+
+
+[width="100%",cols="^10,^10,^40,^40",options="header"]
+|====
+|분류|코드|메시지|설명
+|A|001|토큰이 유효하지 않습니다.|이상한 토큰으로 접근한 경우
+||002|만료된 토큰입니다.|토큰의 형식은 맞으나 시간이 만료된 경우
+||003|작성자가 아니므로 권한이 없습니다.|유저 개인적인 동작을 요청하는 api에서 발생
+||004|AccessToken을 받아오는데 실패했습니다.|소셜에게 토큰을 받아오는데 실패
+||005|유저정보를 불러오는데 실패했습니다.|소셜에게 유저정보를 받아오는데 실패
+||006|Admin 권한이 아니기에 접근할 수 없습니다.|관리자 api 관련
+||007|존재하지 않는 소셜 로그인 방식입니다.|지원하지 않는 소셜로그인으로 요청
+|U|001|해당 유저를 찾을 수 없습니다.|토큰은 유효하나 유저가 없는 경우
+||002|프로필 이미지 수정은 불가합니다.|수정
+||003|이미 존재하는 회원 이름입니다.|수정, 닉네임 검사
+||004|회원명은 필수 입력값입니다.|수정, 닉네임 검사
+||005|이름은 최소 1자 이상, 최대 20자까지 입력 가능합니다.|수정, 닉네임 검사
+||006|회원명에 공백은 포함될 수 없습니다.|수정, 닉네임 검사
+||007|회원 정보를 수정하기 위해서는 프로필 사진이 필요합니다.|수정
+||008|회원 정보를 수정하기 위해서는 소개글은 최소 0자 이상이 필요합니다.|수정, 닉네임 검사
+||009|소개글은 최대 255자까지 가능합니다.|수정, 닉네임 검사
+||010|10MB 이하의 파일만 업로드할 수 있습니다.|프로필 이미지 수정
+||011|요청할 수 있는 최대 파일 크기는 100MB 입니다.|프로필 이미지 수정
+|W|001|문제집 이름은 30자 이하여야 합니다.|추가, 수정
+||002|문제집 이름은 필수 입력값입니다.|추가, 수정
+||003|태그 아이디는 필수 입력값입니다.|추가, 수정
+||004|태그 아이디는 0이상의 숫자입니다.|추가, 수정
+||005|태그 이름은 필수 입력값입니다.|추가, 수정
+||006|태그는 20자 이하여야 합니다.|추가, 수정
+||007|문제집이 가질 수 있는 태그수는 최대 3개 입니다.|추가, 수정
+||008|문제집을 수정하려면 태그가 필요합니다.|수정
+||009|문제집의 공개 여부는 필수 입력값입니다.|수정
+||010|카드 개수는 필수 입력값입니다.|수정
+||011|카드 개수는 0이상 입니다.|수정
+||012|좋아요 개수는 필수 입력값입니다.|수정
+||013|좋아요 개수는 0이상 입니다.|수정
+||014|해당 문제집을 찾을 수 없습니다.|문제집 id로 접근하는 api
+||015|공개 문제집이 아닙니다.|공개 문제집 상세 조회
+||016|카드를 내 문제집으로 옮기려면 카드 아이디가 필요합니다.|가져가기
+|C|001|해당 카드를 찾을 수 없습니다.|카드가 필요한 경우인데 찾지 못한 경우
+||002|질문은 필수 입력값입니다.|추가, 수정
+||003|질문은 최대 2000자까지 입력 가능합니다.|추가, 수정
+||004|답변은 필수 입력값입니다.|추가, 수정
+||005|답변은 최대 2000자까지 입력 가능합니다.|추가, 수정
+||006|문제집 아이디는 필수 입력값입니다.|추가, 수정
+||007|문제집 아이디는 0이상의 숫자입니다.|추가, 수정
+||008|마주친 횟수는 필수 입력값입니다.|수정
+||009|마주친 횟수는 0이상 입니다.|수정
+||010|카드를 수정하기 위해서는 북마크 정보가 필요합니다.|수정
+||011|카드를 수정 위해서는 또 보기 정보가 필요합니다.|수정
+||012|유효하지 않은 또 보기 카드 등록 요청입니다.|또 보기
+|Q|001|퀴즈를 진행하려면 문제집 아이디가 필요합니다.|퀴즈 생성
+||002|퀴즈의 개수는 10 ~ 30 사이의 수만 가능합니다.|퀴즈 생성
+||003|퀴즈에 문제가 존재하지 않습니다.|퀴즈 생성
+|S|001|페이지의 시작 값은 음수가 될 수 없습니다.|문제집 검색
+||002|유효하지 않은 페이지 크기입니다. 유효한 크기 : 1 ~ 100|문제집 검색
+||003|유효하지 않은 정렬 조건입니다. 유효한 정렬 조건 : date, name, count, heart|문제집 검색
+||004|유효하지 않은 정렬 방향입니다. 유효한 정렬 방식 : ASC, DESC|문제집 검색
+||005|유효하지 않은 검색 타입입니다. 유효한 검색 타임 : name, tag, user|문제집 검색
+||006|검색어는 null일 수 없습니다.|문제집, 태그, 유저 검색
+||007|검색어는 30자 이하여야 합니다.|문제집, 태그, 유저 검색
+||008|검색어는 1자 이상이어야 합니다.|문제집, 태그, 유저 검색
+||009|금지어를 입력했습니다.|문제집, 태그, 유저 검색
+|E|001|서버에러|
+||002|파라미터를 입력해야 합니다.|query parameter 관련 에러
+|X|001|정의되지 않은 에러|
+|====
diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc
index a854b387..9983bed0 100644
--- a/backend/src/docs/asciidoc/index.adoc
+++ b/backend/src/docs/asciidoc/index.adoc
@@ -11,8 +11,12 @@ include::cards.adoc[]
include::quizzes.adoc[]
+include::search.adoc[]
+
include::admin.adoc[]
include::login.adoc[]
-include::user.adoc[]
\ No newline at end of file
+include::user.adoc[]
+
+include::errors.adoc[]
\ No newline at end of file
diff --git a/backend/src/docs/asciidoc/login.adoc b/backend/src/docs/asciidoc/login.adoc
index 7d4223f8..5bf325a4 100644
--- a/backend/src/docs/asciidoc/login.adoc
+++ b/backend/src/docs/asciidoc/login.adoc
@@ -1,7 +1,14 @@
== 로그인 API
-=== Github 소셜 로그인
+=== 소셜 로그인
==== 요청
include::{snippets}/login-success/http-request.adoc[]
==== 응답
include::{snippets}/login-success/http-response.adoc[]
+|===
+| Parameter | 설명
+
+| SocialType
+| 소셜 로그인 종류 (github, google)
+
+|===
diff --git a/backend/src/docs/asciidoc/quizzes.adoc b/backend/src/docs/asciidoc/quizzes.adoc
index 2f783b40..3f04f5d8 100644
--- a/backend/src/docs/asciidoc/quizzes.adoc
+++ b/backend/src/docs/asciidoc/quizzes.adoc
@@ -8,9 +8,9 @@ include::{snippets}/quizzes-post-success/http-response.adoc[]
=== 퀴즈 생성 실패 - 해당 id의 문제집이 존재하지 않음.
==== 요청
-include::{snippets}/quizzes-post-fail-invalid-category-id/http-request.adoc[]
+include::{snippets}/quizzes-post-fail-invalid-workbook-id/http-request.adoc[]
==== 응답
-include::{snippets}/quizzes-post-fail-invalid-category-id/http-response.adoc[]
+include::{snippets}/quizzes-post-fail-invalid-workbook-id/http-response.adoc[]
=== 비회원용 퀴즈 생성하기
==== 요청
diff --git a/backend/src/docs/asciidoc/search.adoc b/backend/src/docs/asciidoc/search.adoc
new file mode 100644
index 00000000..b810bce4
--- /dev/null
+++ b/backend/src/docs/asciidoc/search.adoc
@@ -0,0 +1,99 @@
+== 검색 API
+
+=== 문제집 검색
+==== 요청
+include::{snippets}/search-workbooks-get-success/http-request.adoc[]
+==== 응답
+include::{snippets}/search-workbooks-get-success/http-response.adoc[]
+
+.URI 파라미터
+|===
+|파라미터 이름 |값 |필수 여부 |기본 값 |설명
+
+|type
+|name, tag, user
+|false
+|name
+|검색 타입
+
+|criteria
+|date, name, count, heart
+|false
+|date
+|정렬 기준
+
+|order
+|desc, asc
+|false
+|desc
+|정렬 방법
+
+|keyword
+|-
+|true
+|-
+|검색어
+
+|start
+|-
+|false
+|0
+|페이징 시작 인덱스
+
+|size
+|-
+|false
+|20
+|가져올 수
+|===
+
+.파라미터 값 설명
+|===
+|값 |설명
+
+|name
+|문제집 이름 (대소문자 구별하지 않음)
+
+|tag
+|태그 이름 (대소문자 구별하지 않음)
+
+|user
+|유저 이름 (대소문자 구별 함)
+
+|date
+|문제집 생성 날짜
+
+|count
+|문제집에 포함된 카드 수
+
+|heart
+|좋아요 수
+
+|asc
+|오름차순
+
+|desc
+|내림차순
+|===
+
+=== 태그 자동완성
+==== 요청
+include::{snippets}/search-tags-get-success/http-request.adoc[]
+==== 응답
+include::{snippets}/search-tags-get-success/http-response.adoc[]
+
+.URI 파라미터
+|===
+|keyword |태그에 포함될 키워드 (대소문자 구별하지 않음)
+|===
+
+=== 유저 자동완성
+==== 요청
+include::{snippets}/search-users-get-success/http-request.adoc[]
+==== 응답
+include::{snippets}/search-users-get-success/http-response.adoc[]
+
+.URI 파라미터
+|===
+|keyword |유저 이름에 포함될 키워드 (대소문자 구별함)
+|===
\ No newline at end of file
diff --git a/backend/src/docs/asciidoc/user.adoc b/backend/src/docs/asciidoc/user.adoc
index 0b612764..fc4b04f6 100644
--- a/backend/src/docs/asciidoc/user.adoc
+++ b/backend/src/docs/asciidoc/user.adoc
@@ -1,7 +1,32 @@
== 유저 API
-=== 로그인 한 유저 정보 조회 요청
+=== 회원 정보 조회
==== 요청
include::{snippets}/users-find-me-get-success/http-request.adoc[]
==== 응답
include::{snippets}/users-find-me-get-success/http-response.adoc[]
+
+=== 유저 프로필 이미지 수정 요청
+==== 요청
+include::{snippets}/users-update-profile-get-success/http-request.adoc[]
+==== 응답
+include::{snippets}/users-update-profile-get-success/http-response.adoc[]
+
+=== 회원 정보 수정
+==== 요청
+include::{snippets}/users-update-put-success/http-request.adoc[]
+==== 응답
+include::{snippets}/users-update-put-success/http-response.adoc[]
+
+=== 회원명 중복 조회
+==== 요청
+include::{snippets}/users-name-check-post-success/http-request.adoc[]
+==== 응답
+include::{snippets}/users-name-check-post-success/http-response.adoc[]
+
+===== 회원명 중복 발생 시
+|===
+|Http Status|메시지
+|409 Conflict
+|"[중복된 회원명](은)는 이미 존재합니다."
+|===
diff --git a/backend/src/docs/asciidoc/workbooks.adoc b/backend/src/docs/asciidoc/workbooks.adoc
index 3739a688..95905c6f 100644
--- a/backend/src/docs/asciidoc/workbooks.adoc
+++ b/backend/src/docs/asciidoc/workbooks.adoc
@@ -36,14 +36,18 @@ include::{snippets}/workbooks-delete-success/http-request.adoc[]
==== 응답
include::{snippets}/workbooks-delete-success/http-response.adoc[]
-=== 검색어를 이용하여 공유 문제집 검색
+=== 공유 문제집 상세보기
==== 요청
-include::{snippets}/workbooks-public-search-get-success/http-request.adoc[]
+include::{snippets}/workbooks-public-get-success/http-request.adoc[]
==== 응답
-include::{snippets}/workbooks-public-search-get-success/http-response.adoc[]
+include::{snippets}/workbooks-public-get-success/http-response.adoc[]
-=== 공유 문제집 상세보기
+=== 문제집으로 카드 가져오기
==== 요청
-include::{snippets}/workbooks-public-get-success/http-request.adoc[]
+include::{snippets}/workbooks-scrap-cards-success/http-request.adoc[]
+==== 응답
+include::{snippets}/workbooks-scrap-cards-success/http-response.adoc[]
+
+=== 하트 토글하기
==== 응답
-include::{snippets}/workbooks-public-get-success/http-response.adoc[]
\ No newline at end of file
+include::{snippets}/workbooks-toggle-hearts-success/http-response.adoc[]
\ No newline at end of file
diff --git a/backend/src/docs/dummydata/database.txt b/backend/src/docs/dummydata/database.txt
index 48841629..b02f93fc 100644
--- a/backend/src/docs/dummydata/database.txt
+++ b/backend/src/docs/dummydata/database.txt
@@ -1,4 +1,24 @@
-데이터베이스
+database
+인덱스(index)란 무엇인가요?
+인덱스는 데이터분야에 있어서 테이블에 대한 동작의 속도를 높여주는 자료 구조를 말한다. 인덱스는 테이블 내의 1개의 컬럼, 혹은 여러 개의 컬럼을 이용하여 생성될 수 있다. 고속의 검색 동작 뿐만 아니라 레코드 접근과 관련하여 효율적인 순서 매김 동작에 대한 기초를 제공한다.
+트랜잭션(Transaction)이란 무엇인가요?
+하나의 논리적인 기능을 수행하기 위한 작업 단위로 데이터베이스의 일관된 상태를 또 다른 일관된 상태로 변환시키는 기능을 수행한다.
+관계형 데이터베이스 관리 시스템(RDBMS)를 정의하세요.
+관계형 데이터베이스 관리 시스템 (RDBMS)은 데이터베이스에 별도의 테이블에 저장된 관계형 데이터 모델을 기반으로하며 공통 열의 사용과 관련이 있다. SQL (Structured Query Language)을 사용하여 관계형 데이터베이스에서 데이터에 쉽게 액세스 할 수 있다.
+데이터베이스 무결성이란 무엇인가요?
+데이터 베이스에 저장된 데이터 값과 그것이 표현하는 현실 세계의 실제값이 일치하는 정확성을 말합니다.
+데이터베이스 정규화란 무엇인가요?
+자료의 손실이나 불필요한 정보의 도입 없이 데이터의 일관성, 데이터 중복을 최소화하고 최대의 데이터 안정성 확보를 위한 안정적 자료 구조로 변환하기 위해서 하나의 테이블을 둘 이상을 분리하는 작업이다.
+트랜잭션의 4가지 성질에 대해 설명해주세요.
+Atomicity(원자성) 는 트랜잭션의 연산이 DB에 모두 반영되던지 전혀 반영이되지 않던지 둘중에 하나만 수행해야한다.\nConsistency(일관성) 는 트랜잭션이 성공적으로 완료된 후에는 언제나 일관성 있는 DB상태로 변환되어야한다.\nIsolation(독립성) 은 수행중인 트랜잭션이 완전히 완료되기 전에는 다른 트랙잭션에서 수행 결과를 참조할 수 없다.\nDurablility(지속성) 는 성공적으로 완료된 트랜잭션의 결과는 시스템이 고장나더라도 영구적으로 반영되어야 한다.
+데이터베이스에서 데드락(Dead Lock)이란 무엇인가요?
+2개 이상의 트랜잭션이 특정 자원(테이블 또는 행)의 잠금(Lock)을 획득한 채 다른 트랜잭션이 소유하고 있는 잠금을 요구하면 아무리 기다려도 상황이 바뀌지 않는 상태가 되는데 이를 교착상태 라고 한다.
+트랜잭션에서 격리 수준(Isolation Level)은 무엇이고 종류를 설명해주세요.
+격리 수준은 트랜잭션에서 일관성이 없는 데이터를 허용하도록 하는 수준을 말한다.\n\nRead Uncommitted(레벨 0): 한 트랜잭션에서 커밋하지 않은 데이터에 다른 트랜잭션의 접근이 가능하다. 즉 커밋하지 않은 데이터를 읽을 수 있다. 해당 격리 수준은 모든 문제에서 발생 가능성이 존재하지만 처리 성능은 가장 높다.\nRead Committed(레벨 1): Committed가 완료된 데이터만 읽을 수 있다. 이는 Dirty Read가 발생할 여지가 없으나, Read Uncommitted 수준보다 동시 처리 성능은 떨어진다. 다만 Non-Repeatable Read, Phantom Read가 발생 가능하다. 데이터베이스들은 보통 Read Committed를 디폴트 수준으로 지정한다.\nRepeatable Read(레벨 2): 트랜잭션 내에서 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회된다. 이는 개별 데이터 이슈인 Dirty Read나 Non-Repeatable Read는 발생하지 않지만, 결과 집합 자체가 달라지는 Phantom Read는 발생 가능 하다.\nSerializable(레벨 3): 가장 엄격한 격리 수준. 3가지 문제점을 모두 커버할 수 있다. 다만 동시 처리 성능은 급격히 떨어질 수 있다.
+데이터베이스 락(Lock)이란 무엇인가요?
+데이터베이스 연산(read/write)을 수행하기 전에 해당 데이터에 먼저 lock 연산을 실행하여 독점권을 획득하는 방식으로 트랜잭션의 직렬가능성을 보장하는 방식이다. 병행 수행되는 트랜잭션들이 동일한 데이터에 동시에 접근하지 못하도록 lock과 unlock이라는 2개의 연산을 이용해 제어한다. 기본 원리는 먼저 접근한 데이터에 대한 연산을 다 마칠 때까지, 해당 데이터에 다른 트랜잭션이 접근하지 못하도록 상호배제하여 직렬가능성을 보장하는 것이다.
+COMMIT과 ROLLBACK에 대해 설명해주세요.
+COMMIT은 해당 트랜잭션으로 반영된 데이터베이스 변경사항을 저장 하는 것이고 ROLLBACK은 해당 트랜잭션으로 반영된 데이터베이스 변경사항을 취소 하는 것이다.
인덱스(index)란 무엇인가요?
인덱스는 데이터분야에 있어서 테이블에 대한 동작의 속도를 높여주는 자료 구조를 말한다. 인덱스는 테이블 내의 1개의 컬럼, 혹은 여러 개의 컬럼을 이용하여 생성될 수 있다. 고속의 검색 동작 뿐만 아니라 레코드 접근과 관련하여 효율적인 순서 매김 동작에 대한 기초를 제공한다.
트랜잭션(Transaction)이란 무엇인가요?
diff --git a/backend/src/docs/dummydata/java.txt b/backend/src/docs/dummydata/java.txt
index 5c29a9d7..6474895e 100644
--- a/backend/src/docs/dummydata/java.txt
+++ b/backend/src/docs/dummydata/java.txt
@@ -18,4 +18,62 @@ public, protected, default, private 이 있습니다.
상속을 써서 얻을 수 있는 이점이 무엇인가요?
같은 기능을 하는 클래스들이 있을 경우, 그 기능을 구현한 코드를 한 부모 클래스에 넣어 중복되는 코드를 줄이며 재사용 가능한 구조를 만들 수 있다는 이점이 있습니다.
Stack과 Queue의 차이를 간단하게 설명해주세요.
-Stack과 Queue 둘 다 정보를 담는 자료구조임은 같습니다. 그러나 Stack은 마지막으로 넣은 자료를 가장 먼저 꺼낼 수 있는 Last In First Out (LIFO) 구조이며, Queue는 처음으로 넣은 자료를 먼저 꺼낼 수 있는 First In First Out (FIFO) 구조입니다.
\ No newline at end of file
+Stack과 Queue 둘 다 정보를 담는 자료구조임은 같습니다. 그러나 Stack은 마지막으로 넣은 자료를 가장 먼저 꺼낼 수 있는 Last In First Out (LIFO) 구조이며, Queue는 처음으로 넣은 자료를 먼저 꺼낼 수 있는 First In First Out (FIFO) 구조입니다.
+인덱스(index)란 무엇인가요?
+인덱스는 데이터분야에 있어서 테이블에 대한 동작의 속도를 높여주는 자료 구조를 말한다. 인덱스는 테이블 내의 1개의 컬럼, 혹은 여러 개의 컬럼을 이용하여 생성될 수 있다. 고속의 검색 동작 뿐만 아니라 레코드 접근과 관련하여 효율적인 순서 매김 동작에 대한 기초를 제공한다.
+트랜잭션(Transaction)이란 무엇인가요?
+하나의 논리적인 기능을 수행하기 위한 작업 단위로 데이터베이스의 일관된 상태를 또 다른 일관된 상태로 변환시키는 기능을 수행한다.
+관계형 데이터베이스 관리 시스템(RDBMS)를 정의하세요.
+관계형 데이터베이스 관리 시스템 (RDBMS)은 데이터베이스에 별도의 테이블에 저장된 관계형 데이터 모델을 기반으로하며 공통 열의 사용과 관련이 있다. SQL (Structured Query Language)을 사용하여 관계형 데이터베이스에서 데이터에 쉽게 액세스 할 수 있다.
+데이터베이스 무결성이란 무엇인가요?
+데이터 베이스에 저장된 데이터 값과 그것이 표현하는 현실 세계의 실제값이 일치하는 정확성을 말합니다.
+데이터베이스 정규화란 무엇인가요?
+자료의 손실이나 불필요한 정보의 도입 없이 데이터의 일관성, 데이터 중복을 최소화하고 최대의 데이터 안정성 확보를 위한 안정적 자료 구조로 변환하기 위해서 하나의 테이블을 둘 이상을 분리하는 작업이다.
+트랜잭션의 4가지 성질에 대해 설명해주세요.
+Atomicity(원자성) 는 트랜잭션의 연산이 DB에 모두 반영되던지 전혀 반영이되지 않던지 둘중에 하나만 수행해야한다.\nConsistency(일관성) 는 트랜잭션이 성공적으로 완료된 후에는 언제나 일관성 있는 DB상태로 변환되어야한다.\nIsolation(독립성) 은 수행중인 트랜잭션이 완전히 완료되기 전에는 다른 트랙잭션에서 수행 결과를 참조할 수 없다.\nDurablility(지속성) 는 성공적으로 완료된 트랜잭션의 결과는 시스템이 고장나더라도 영구적으로 반영되어야 한다.
+데이터베이스에서 데드락(Dead Lock)이란 무엇인가요?
+2개 이상의 트랜잭션이 특정 자원(테이블 또는 행)의 잠금(Lock)을 획득한 채 다른 트랜잭션이 소유하고 있는 잠금을 요구하면 아무리 기다려도 상황이 바뀌지 않는 상태가 되는데 이를 교착상태 라고 한다.
+트랜잭션에서 격리 수준(Isolation Level)은 무엇이고 종류를 설명해주세요.
+격리 수준은 트랜잭션에서 일관성이 없는 데이터를 허용하도록 하는 수준을 말한다.\n\nRead Uncommitted(레벨 0): 한 트랜잭션에서 커밋하지 않은 데이터에 다른 트랜잭션의 접근이 가능하다. 즉 커밋하지 않은 데이터를 읽을 수 있다. 해당 격리 수준은 모든 문제에서 발생 가능성이 존재하지만 처리 성능은 가장 높다.\nRead Committed(레벨 1): Committed가 완료된 데이터만 읽을 수 있다. 이는 Dirty Read가 발생할 여지가 없으나, Read Uncommitted 수준보다 동시 처리 성능은 떨어진다. 다만 Non-Repeatable Read, Phantom Read가 발생 가능하다. 데이터베이스들은 보통 Read Committed를 디폴트 수준으로 지정한다.\nRepeatable Read(레벨 2): 트랜잭션 내에서 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회된다. 이는 개별 데이터 이슈인 Dirty Read나 Non-Repeatable Read는 발생하지 않지만, 결과 집합 자체가 달라지는 Phantom Read는 발생 가능 하다.\nSerializable(레벨 3): 가장 엄격한 격리 수준. 3가지 문제점을 모두 커버할 수 있다. 다만 동시 처리 성능은 급격히 떨어질 수 있다.
+데이터베이스 락(Lock)이란 무엇인가요?
+데이터베이스 연산(read/write)을 수행하기 전에 해당 데이터에 먼저 lock 연산을 실행하여 독점권을 획득하는 방식으로 트랜잭션의 직렬가능성을 보장하는 방식이다. 병행 수행되는 트랜잭션들이 동일한 데이터에 동시에 접근하지 못하도록 lock과 unlock이라는 2개의 연산을 이용해 제어한다. 기본 원리는 먼저 접근한 데이터에 대한 연산을 다 마칠 때까지, 해당 데이터에 다른 트랜잭션이 접근하지 못하도록 상호배제하여 직렬가능성을 보장하는 것이다.
+COMMIT과 ROLLBACK에 대해 설명해주세요.
+COMMIT은 해당 트랜잭션으로 반영된 데이터베이스 변경사항을 저장 하는 것이고 ROLLBACK은 해당 트랜잭션으로 반영된 데이터베이스 변경사항을 취소 하는 것이다.
+인덱스(index)란 무엇인가요?
+인덱스는 데이터분야에 있어서 테이블에 대한 동작의 속도를 높여주는 자료 구조를 말한다. 인덱스는 테이블 내의 1개의 컬럼, 혹은 여러 개의 컬럼을 이용하여 생성될 수 있다. 고속의 검색 동작 뿐만 아니라 레코드 접근과 관련하여 효율적인 순서 매김 동작에 대한 기초를 제공한다.
+트랜잭션(Transaction)이란 무엇인가요?
+하나의 논리적인 기능을 수행하기 위한 작업 단위로 데이터베이스의 일관된 상태를 또 다른 일관된 상태로 변환시키는 기능을 수행한다.
+관계형 데이터베이스 관리 시스템(RDBMS)를 정의하세요.
+관계형 데이터베이스 관리 시스템 (RDBMS)은 데이터베이스에 별도의 테이블에 저장된 관계형 데이터 모델을 기반으로하며 공통 열의 사용과 관련이 있다. SQL (Structured Query Language)을 사용하여 관계형 데이터베이스에서 데이터에 쉽게 액세스 할 수 있다.
+데이터베이스 무결성이란 무엇인가요?
+데이터 베이스에 저장된 데이터 값과 그것이 표현하는 현실 세계의 실제값이 일치하는 정확성을 말합니다.
+데이터베이스 정규화란 무엇인가요?
+자료의 손실이나 불필요한 정보의 도입 없이 데이터의 일관성, 데이터 중복을 최소화하고 최대의 데이터 안정성 확보를 위한 안정적 자료 구조로 변환하기 위해서 하나의 테이블을 둘 이상을 분리하는 작업이다.
+트랜잭션의 4가지 성질에 대해 설명해주세요.
+Atomicity(원자성) 는 트랜잭션의 연산이 DB에 모두 반영되던지 전혀 반영이되지 않던지 둘중에 하나만 수행해야한다.\nConsistency(일관성) 는 트랜잭션이 성공적으로 완료된 후에는 언제나 일관성 있는 DB상태로 변환되어야한다.\nIsolation(독립성) 은 수행중인 트랜잭션이 완전히 완료되기 전에는 다른 트랙잭션에서 수행 결과를 참조할 수 없다.\nDurablility(지속성) 는 성공적으로 완료된 트랜잭션의 결과는 시스템이 고장나더라도 영구적으로 반영되어야 한다.
+데이터베이스에서 데드락(Dead Lock)이란 무엇인가요?
+2개 이상의 트랜잭션이 특정 자원(테이블 또는 행)의 잠금(Lock)을 획득한 채 다른 트랜잭션이 소유하고 있는 잠금을 요구하면 아무리 기다려도 상황이 바뀌지 않는 상태가 되는데 이를 교착상태 라고 한다.
+트랜잭션에서 격리 수준(Isolation Level)은 무엇이고 종류를 설명해주세요.
+격리 수준은 트랜잭션에서 일관성이 없는 데이터를 허용하도록 하는 수준을 말한다.\n\nRead Uncommitted(레벨 0): 한 트랜잭션에서 커밋하지 않은 데이터에 다른 트랜잭션의 접근이 가능하다. 즉 커밋하지 않은 데이터를 읽을 수 있다. 해당 격리 수준은 모든 문제에서 발생 가능성이 존재하지만 처리 성능은 가장 높다.\nRead Committed(레벨 1): Committed가 완료된 데이터만 읽을 수 있다. 이는 Dirty Read가 발생할 여지가 없으나, Read Uncommitted 수준보다 동시 처리 성능은 떨어진다. 다만 Non-Repeatable Read, Phantom Read가 발생 가능하다. 데이터베이스들은 보통 Read Committed를 디폴트 수준으로 지정한다.\nRepeatable Read(레벨 2): 트랜잭션 내에서 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회된다. 이는 개별 데이터 이슈인 Dirty Read나 Non-Repeatable Read는 발생하지 않지만, 결과 집합 자체가 달라지는 Phantom Read는 발생 가능 하다.\nSerializable(레벨 3): 가장 엄격한 격리 수준. 3가지 문제점을 모두 커버할 수 있다. 다만 동시 처리 성능은 급격히 떨어질 수 있다.
+데이터베이스 락(Lock)이란 무엇인가요?
+데이터베이스 연산(read/write)을 수행하기 전에 해당 데이터에 먼저 lock 연산을 실행하여 독점권을 획득하는 방식으로 트랜잭션의 직렬가능성을 보장하는 방식이다. 병행 수행되는 트랜잭션들이 동일한 데이터에 동시에 접근하지 못하도록 lock과 unlock이라는 2개의 연산을 이용해 제어한다. 기본 원리는 먼저 접근한 데이터에 대한 연산을 다 마칠 때까지, 해당 데이터에 다른 트랜잭션이 접근하지 못하도록 상호배제하여 직렬가능성을 보장하는 것이다.
+COMMIT과 ROLLBACK에 대해 설명해주세요.
+COMMIT은 해당 트랜잭션으로 반영된 데이터베이스 변경사항을 저장 하는 것이고 ROLLBACK은 해당 트랜잭션으로 반영된 데이터베이스 변경사항을 취소 하는 것이다.
+인덱스(index)란 무엇인가요?
+인덱스는 데이터분야에 있어서 테이블에 대한 동작의 속도를 높여주는 자료 구조를 말한다. 인덱스는 테이블 내의 1개의 컬럼, 혹은 여러 개의 컬럼을 이용하여 생성될 수 있다. 고속의 검색 동작 뿐만 아니라 레코드 접근과 관련하여 효율적인 순서 매김 동작에 대한 기초를 제공한다.
+트랜잭션(Transaction)이란 무엇인가요?
+하나의 논리적인 기능을 수행하기 위한 작업 단위로 데이터베이스의 일관된 상태를 또 다른 일관된 상태로 변환시키는 기능을 수행한다.
+관계형 데이터베이스 관리 시스템(RDBMS)를 정의하세요.
+관계형 데이터베이스 관리 시스템 (RDBMS)은 데이터베이스에 별도의 테이블에 저장된 관계형 데이터 모델을 기반으로하며 공통 열의 사용과 관련이 있다. SQL (Structured Query Language)을 사용하여 관계형 데이터베이스에서 데이터에 쉽게 액세스 할 수 있다.
+데이터베이스 무결성이란 무엇인가요?
+데이터 베이스에 저장된 데이터 값과 그것이 표현하는 현실 세계의 실제값이 일치하는 정확성을 말합니다.
+데이터베이스 정규화란 무엇인가요?
+자료의 손실이나 불필요한 정보의 도입 없이 데이터의 일관성, 데이터 중복을 최소화하고 최대의 데이터 안정성 확보를 위한 안정적 자료 구조로 변환하기 위해서 하나의 테이블을 둘 이상을 분리하는 작업이다.
+트랜잭션의 4가지 성질에 대해 설명해주세요.
+Atomicity(원자성) 는 트랜잭션의 연산이 DB에 모두 반영되던지 전혀 반영이되지 않던지 둘중에 하나만 수행해야한다.\nConsistency(일관성) 는 트랜잭션이 성공적으로 완료된 후에는 언제나 일관성 있는 DB상태로 변환되어야한다.\nIsolation(독립성) 은 수행중인 트랜잭션이 완전히 완료되기 전에는 다른 트랙잭션에서 수행 결과를 참조할 수 없다.\nDurablility(지속성) 는 성공적으로 완료된 트랜잭션의 결과는 시스템이 고장나더라도 영구적으로 반영되어야 한다.
+데이터베이스에서 데드락(Dead Lock)이란 무엇인가요?
+2개 이상의 트랜잭션이 특정 자원(테이블 또는 행)의 잠금(Lock)을 획득한 채 다른 트랜잭션이 소유하고 있는 잠금을 요구하면 아무리 기다려도 상황이 바뀌지 않는 상태가 되는데 이를 교착상태 라고 한다.
+트랜잭션에서 격리 수준(Isolation Level)은 무엇이고 종류를 설명해주세요.
+격리 수준은 트랜잭션에서 일관성이 없는 데이터를 허용하도록 하는 수준을 말한다.\n\nRead Uncommitted(레벨 0): 한 트랜잭션에서 커밋하지 않은 데이터에 다른 트랜잭션의 접근이 가능하다. 즉 커밋하지 않은 데이터를 읽을 수 있다. 해당 격리 수준은 모든 문제에서 발생 가능성이 존재하지만 처리 성능은 가장 높다.\nRead Committed(레벨 1): Committed가 완료된 데이터만 읽을 수 있다. 이는 Dirty Read가 발생할 여지가 없으나, Read Uncommitted 수준보다 동시 처리 성능은 떨어진다. 다만 Non-Repeatable Read, Phantom Read가 발생 가능하다. 데이터베이스들은 보통 Read Committed를 디폴트 수준으로 지정한다.\nRepeatable Read(레벨 2): 트랜잭션 내에서 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회된다. 이는 개별 데이터 이슈인 Dirty Read나 Non-Repeatable Read는 발생하지 않지만, 결과 집합 자체가 달라지는 Phantom Read는 발생 가능 하다.\nSerializable(레벨 3): 가장 엄격한 격리 수준. 3가지 문제점을 모두 커버할 수 있다. 다만 동시 처리 성능은 급격히 떨어질 수 있다.
+데이터베이스 락(Lock)이란 무엇인가요?
+데이터베이스 연산(read/write)을 수행하기 전에 해당 데이터에 먼저 lock 연산을 실행하여 독점권을 획득하는 방식으로 트랜잭션의 직렬가능성을 보장하는 방식이다. 병행 수행되는 트랜잭션들이 동일한 데이터에 동시에 접근하지 못하도록 lock과 unlock이라는 2개의 연산을 이용해 제어한다. 기본 원리는 먼저 접근한 데이터에 대한 연산을 다 마칠 때까지, 해당 데이터에 다른 트랜잭션이 접근하지 못하도록 상호배제하여 직렬가능성을 보장하는 것이다.
\ No newline at end of file
diff --git a/backend/src/docs/dummydata/network.txt b/backend/src/docs/dummydata/network.txt
index b01120ce..fab96cd5 100644
--- a/backend/src/docs/dummydata/network.txt
+++ b/backend/src/docs/dummydata/network.txt
@@ -18,4 +18,20 @@ Internet Protocol Address로서 컴퓨터 네트워크에서 기기들이 서로
패킷이란 무엇인가요?
네트워크 상에서 전송하는 데이터를 일정한 크기로 자른 작게 나뉘어진 데이터의 묶음을 패킷이라 한다. 누구에게, 어디로, 무엇을 보내야하는지에 대한 정보가 담겨있다.
로드 밸런싱이란 무엇인가요?
-분산식 웹 서비스로 여러 서버에 부하를 나누어주는 것을 말한다. Round Robin, Least Connection, Response Time, Hash등의 기법이 존재한다.
\ No newline at end of file
+분산식 웹 서비스로 여러 서버에 부하를 나누어주는 것을 말한다. Round Robin, Least Connection, Response Time, Hash등의 기법이 존재한다.
+인덱스(index)란 무엇인가요?
+인덱스는 데이터분야에 있어서 테이블에 대한 동작의 속도를 높여주는 자료 구조를 말한다. 인덱스는 테이블 내의 1개의 컬럼, 혹은 여러 개의 컬럼을 이용하여 생성될 수 있다. 고속의 검색 동작 뿐만 아니라 레코드 접근과 관련하여 효율적인 순서 매김 동작에 대한 기초를 제공한다.
+트랜잭션(Transaction)이란 무엇인가요?
+하나의 논리적인 기능을 수행하기 위한 작업 단위로 데이터베이스의 일관된 상태를 또 다른 일관된 상태로 변환시키는 기능을 수행한다.
+관계형 데이터베이스 관리 시스템(RDBMS)를 정의하세요.
+관계형 데이터베이스 관리 시스템 (RDBMS)은 데이터베이스에 별도의 테이블에 저장된 관계형 데이터 모델을 기반으로하며 공통 열의 사용과 관련이 있다. SQL (Structured Query Language)을 사용하여 관계형 데이터베이스에서 데이터에 쉽게 액세스 할 수 있다.
+데이터베이스 무결성이란 무엇인가요?
+데이터 베이스에 저장된 데이터 값과 그것이 표현하는 현실 세계의 실제값이 일치하는 정확성을 말합니다.
+데이터베이스 정규화란 무엇인가요?
+자료의 손실이나 불필요한 정보의 도입 없이 데이터의 일관성, 데이터 중복을 최소화하고 최대의 데이터 안정성 확보를 위한 안정적 자료 구조로 변환하기 위해서 하나의 테이블을 둘 이상을 분리하는 작업이다.
+트랜잭션의 4가지 성질에 대해 설명해주세요.
+Atomicity(원자성) 는 트랜잭션의 연산이 DB에 모두 반영되던지 전혀 반영이되지 않던지 둘중에 하나만 수행해야한다.\nConsistency(일관성) 는 트랜잭션이 성공적으로 완료된 후에는 언제나 일관성 있는 DB상태로 변환되어야한다.\nIsolation(독립성) 은 수행중인 트랜잭션이 완전히 완료되기 전에는 다른 트랙잭션에서 수행 결과를 참조할 수 없다.\nDurablility(지속성) 는 성공적으로 완료된 트랜잭션의 결과는 시스템이 고장나더라도 영구적으로 반영되어야 한다.
+데이터베이스에서 데드락(Dead Lock)이란 무엇인가요?
+2개 이상의 트랜잭션이 특정 자원(테이블 또는 행)의 잠금(Lock)을 획득한 채 다른 트랜잭션이 소유하고 있는 잠금을 요구하면 아무리 기다려도 상황이 바뀌지 않는 상태가 되는데 이를 교착상태 라고 한다.
+트랜잭션에서 격리 수준(Isolation Level)은 무엇이고 종류를 설명해주세요.
+격리 수준은 트랜잭션에서 일관성이 없는 데이터를 허용하도록 하는 수준을 말한다.\n\nRead Uncommitted(레벨 0): 한 트랜잭션에서 커밋하지 않은 데이터에 다른 트랜잭션의 접근이 가능하다. 즉 커밋하지 않은 데이터를 읽을 수 있다. 해당 격리 수준은 모든 문제에서 발생 가능성이 존재하지만 처리 성능은 가장 높다.\nRead Committed(레벨 1): Committed가 완료된 데이터만 읽을 수 있다. 이는 Dirty Read가 발생할 여지가 없으나, Read Uncommitted 수준보다 동시 처리 성능은 떨어진다. 다만 Non-Repeatable Read, Phantom Read가 발생 가능하다. 데이터베이스들은 보통 Read Committed를 디폴트 수준으로 지정한다.\nRepeatable Read(레벨 2): 트랜잭션 내에서 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회된다. 이는 개별 데이터 이슈인 Dirty Read나 Non-Repeatable Read는 발생하지 않지만, 결과 집합 자체가 달라지는 Phantom Read는 발생 가능 하다.\nSerializable(레벨 3): 가장 엄격한 격리 수준. 3가지 문제점을 모두 커버할 수 있다. 다만 동시 처리 성능은 급격히 떨어질 수 있다.
\ No newline at end of file
diff --git a/backend/src/docs/dummydata/react.txt b/backend/src/docs/dummydata/react.txt
index 121c0d28..9d79345e 100644
--- a/backend/src/docs/dummydata/react.txt
+++ b/backend/src/docs/dummydata/react.txt
@@ -19,3 +19,33 @@ state나 props의 변화를 객체를 모두 순회하면서 비교하는 것이
리액트에 값이 완전히 제어되는 컴포넌트로 input의 값을 관리하기 위해 많이 사용된다.
제어 컴포넌트를 사용해야 하는 이유는?
값이 항상 리액트 생명주기 내에서 관리되기 때문에 믿을 수 있는 값으로 관리할 수 있다. 즉, single source of truth를 만족하기 때문이다.
+인덱스(index)란 무엇인가요?
+인덱스는 데이터분야에 있어서 테이블에 대한 동작의 속도를 높여주는 자료 구조를 말한다. 인덱스는 테이블 내의 1개의 컬럼, 혹은 여러 개의 컬럼을 이용하여 생성될 수 있다. 고속의 검색 동작 뿐만 아니라 레코드 접근과 관련하여 효율적인 순서 매김 동작에 대한 기초를 제공한다.
+트랜잭션(Transaction)이란 무엇인가요?
+하나의 논리적인 기능을 수행하기 위한 작업 단위로 데이터베이스의 일관된 상태를 또 다른 일관된 상태로 변환시키는 기능을 수행한다.
+관계형 데이터베이스 관리 시스템(RDBMS)를 정의하세요.
+관계형 데이터베이스 관리 시스템 (RDBMS)은 데이터베이스에 별도의 테이블에 저장된 관계형 데이터 모델을 기반으로하며 공통 열의 사용과 관련이 있다. SQL (Structured Query Language)을 사용하여 관계형 데이터베이스에서 데이터에 쉽게 액세스 할 수 있다.
+데이터베이스 무결성이란 무엇인가요?
+데이터 베이스에 저장된 데이터 값과 그것이 표현하는 현실 세계의 실제값이 일치하는 정확성을 말합니다.
+데이터베이스 정규화란 무엇인가요?
+자료의 손실이나 불필요한 정보의 도입 없이 데이터의 일관성, 데이터 중복을 최소화하고 최대의 데이터 안정성 확보를 위한 안정적 자료 구조로 변환하기 위해서 하나의 테이블을 둘 이상을 분리하는 작업이다.
+트랜잭션의 4가지 성질에 대해 설명해주세요.
+Atomicity(원자성) 는 트랜잭션의 연산이 DB에 모두 반영되던지 전혀 반영이되지 않던지 둘중에 하나만 수행해야한다.\nConsistency(일관성) 는 트랜잭션이 성공적으로 완료된 후에는 언제나 일관성 있는 DB상태로 변환되어야한다.\nIsolation(독립성) 은 수행중인 트랜잭션이 완전히 완료되기 전에는 다른 트랙잭션에서 수행 결과를 참조할 수 없다.\nDurablility(지속성) 는 성공적으로 완료된 트랜잭션의 결과는 시스템이 고장나더라도 영구적으로 반영되어야 한다.
+데이터베이스에서 데드락(Dead Lock)이란 무엇인가요?
+2개 이상의 트랜잭션이 특정 자원(테이블 또는 행)의 잠금(Lock)을 획득한 채 다른 트랜잭션이 소유하고 있는 잠금을 요구하면 아무리 기다려도 상황이 바뀌지 않는 상태가 되는데 이를 교착상태 라고 한다.
+트랜잭션에서 격리 수준(Isolation Level)은 무엇이고 종류를 설명해주세요.
+격리 수준은 트랜잭션에서 일관성이 없는 데이터를 허용하도록 하는 수준을 말한다.\n\nRead Uncommitted(레벨 0): 한 트랜잭션에서 커밋하지 않은 데이터에 다른 트랜잭션의 접근이 가능하다. 즉 커밋하지 않은 데이터를 읽을 수 있다. 해당 격리 수준은 모든 문제에서 발생 가능성이 존재하지만 처리 성능은 가장 높다.\nRead Committed(레벨 1): Committed가 완료된 데이터만 읽을 수 있다. 이는 Dirty Read가 발생할 여지가 없으나, Read Uncommitted 수준보다 동시 처리 성능은 떨어진다. 다만 Non-Repeatable Read, Phantom Read가 발생 가능하다. 데이터베이스들은 보통 Read Committed를 디폴트 수준으로 지정한다.\nRepeatable Read(레벨 2): 트랜잭션 내에서 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회된다. 이는 개별 데이터 이슈인 Dirty Read나 Non-Repeatable Read는 발생하지 않지만, 결과 집합 자체가 달라지는 Phantom Read는 발생 가능 하다.\nSerializable(레벨 3): 가장 엄격한 격리 수준. 3가지 문제점을 모두 커버할 수 있다. 다만 동시 처리 성능은 급격히 떨어질 수 있다.
+데이터베이스 락(Lock)이란 무엇인가요?
+데이터베이스 연산(read/write)을 수행하기 전에 해당 데이터에 먼저 lock 연산을 실행하여 독점권을 획득하는 방식으로 트랜잭션의 직렬가능성을 보장하는 방식이다. 병행 수행되는 트랜잭션들이 동일한 데이터에 동시에 접근하지 못하도록 lock과 unlock이라는 2개의 연산을 이용해 제어한다. 기본 원리는 먼저 접근한 데이터에 대한 연산을 다 마칠 때까지, 해당 데이터에 다른 트랜잭션이 접근하지 못하도록 상호배제하여 직렬가능성을 보장하는 것이다.
+COMMIT과 ROLLBACK에 대해 설명해주세요.
+COMMIT은 해당 트랜잭션으로 반영된 데이터베이스 변경사항을 저장 하는 것이고 ROLLBACK은 해당 트랜잭션으로 반영된 데이터베이스 변경사항을 취소 하는 것이다.
+인덱스(index)란 무엇인가요?
+인덱스는 데이터분야에 있어서 테이블에 대한 동작의 속도를 높여주는 자료 구조를 말한다. 인덱스는 테이블 내의 1개의 컬럼, 혹은 여러 개의 컬럼을 이용하여 생성될 수 있다. 고속의 검색 동작 뿐만 아니라 레코드 접근과 관련하여 효율적인 순서 매김 동작에 대한 기초를 제공한다.
+트랜잭션(Transaction)이란 무엇인가요?
+하나의 논리적인 기능을 수행하기 위한 작업 단위로 데이터베이스의 일관된 상태를 또 다른 일관된 상태로 변환시키는 기능을 수행한다.
+관계형 데이터베이스 관리 시스템(RDBMS)를 정의하세요.
+관계형 데이터베이스 관리 시스템 (RDBMS)은 데이터베이스에 별도의 테이블에 저장된 관계형 데이터 모델을 기반으로하며 공통 열의 사용과 관련이 있다. SQL (Structured Query Language)을 사용하여 관계형 데이터베이스에서 데이터에 쉽게 액세스 할 수 있다.
+데이터베이스 무결성이란 무엇인가요?
+데이터 베이스에 저장된 데이터 값과 그것이 표현하는 현실 세계의 실제값이 일치하는 정확성을 말합니다.
+데이터베이스 정규화란 무엇인가요?
+자료의 손실이나 불필요한 정보의 도입 없이 데이터의 일관성, 데이터 중복을 최소화하고 최대의 데이터 안정성 확보를 위한 안정적 자료 구조로 변환하기 위해서 하나의 테이블을 둘 이상을 분리하는 작업이다.
\ No newline at end of file
diff --git a/backend/src/docs/dummydata/spring.txt b/backend/src/docs/dummydata/spring.txt
index fc9b734d..294c1f65 100644
--- a/backend/src/docs/dummydata/spring.txt
+++ b/backend/src/docs/dummydata/spring.txt
@@ -18,4 +18,12 @@ Spring Bean의 Scope에 대해 설명해주세요.
Spring에서 CORS 에러를 해결하기 위한 방법을 설명해주세요.
Servlet Filter를 사용하여 커스텀한 Cors 설정하거나, WebMvcConfiguer를 구현한 Configuration 클래스를 만들어서 addCorsMappings()를 재정의할 수도 있고, 마지막으로 Spring Security에서 CorsConfigurationSource를 Bean으로 등록하고 config에 추가해줌으로써 해결할 수 있습니다.
Spring Security란 무엇인가요?
-Spring Security는 Java 애플리케이션에서 인증 및 권한 부여 방법을 제공하는 데 초점을 맞춘 Spring 프레임 워크의 별도 모듈입니다. 또한 CSRF 공격과 같은 대부분의 일반적인 Security 취약점을 처리합니다.
\ No newline at end of file
+Spring Security는 Java 애플리케이션에서 인증 및 권한 부여 방법을 제공하는 데 초점을 맞춘 Spring 프레임 워크의 별도 모듈입니다. 또한 CSRF 공격과 같은 대부분의 일반적인 Security 취약점을 처리합니다.
+인덱스(index)란 무엇인가요?
+인덱스는 데이터분야에 있어서 테이블에 대한 동작의 속도를 높여주는 자료 구조를 말한다. 인덱스는 테이블 내의 1개의 컬럼, 혹은 여러 개의 컬럼을 이용하여 생성될 수 있다. 고속의 검색 동작 뿐만 아니라 레코드 접근과 관련하여 효율적인 순서 매김 동작에 대한 기초를 제공한다.
+트랜잭션(Transaction)이란 무엇인가요?
+하나의 논리적인 기능을 수행하기 위한 작업 단위로 데이터베이스의 일관된 상태를 또 다른 일관된 상태로 변환시키는 기능을 수행한다.
+관계형 데이터베이스 관리 시스템(RDBMS)를 정의하세요.
+관계형 데이터베이스 관리 시스템 (RDBMS)은 데이터베이스에 별도의 테이블에 저장된 관계형 데이터 모델을 기반으로하며 공통 열의 사용과 관련이 있다. SQL (Structured Query Language)을 사용하여 관계형 데이터베이스에서 데이터에 쉽게 액세스 할 수 있다.
+데이터베이스 무결성이란 무엇인가요?
+데이터 베이스에 저장된 데이터 값과 그것이 표현하는 현실 세계의 실제값이 일치하는 정확성을 말합니다.
\ No newline at end of file
diff --git a/backend/src/docs/request/quizzes.http b/backend/src/docs/request/quizzes.http
index accac9d6..5873c038 100644
--- a/backend/src/docs/request/quizzes.http
+++ b/backend/src/docs/request/quizzes.http
@@ -10,9 +10,10 @@ Host: localhost:8080
1,
2,
3
- ]
+ ],
+ "count": 10
}
### 비회원용 퀴즈 생성하기
GET /api/quizzes/guest HTTP/1.1
-Host: botobo.r-e.kr
\ No newline at end of file
+Host: localhost:8080
\ No newline at end of file
diff --git a/backend/src/docs/request/search.http b/backend/src/docs/request/search.http
new file mode 100644
index 00000000..b0421410
--- /dev/null
+++ b/backend/src/docs/request/search.http
@@ -0,0 +1,9 @@
+### 문제집 검색
+### 컴색 타입 : 문제집 이름
+### 정렬 기준 : 날짜
+### 정렬 방법 : 내림차순
+### 키워드 : java
+### 시작 인덱스 : 0
+### 결과 가져올 수 : 10
+GET /api/search/workbooks?type=name&criteria=date&order=desc&keyword=java&start=0&size=10 HTTP/1.1
+Host: botobo.kro.kr
\ No newline at end of file
diff --git a/backend/src/docs/request/users.http b/backend/src/docs/request/users.http
index b415dcaf..d7fd8b47 100644
--- a/backend/src/docs/request/users.http
+++ b/backend/src/docs/request/users.http
@@ -1,4 +1,34 @@
### 로그인 한 유저 정보 조회 요청
GET /api/users/me HTTP/1.1
Authorization: Bearer botobo.access.token
-Host: localhost:8080
\ No newline at end of file
+Host: localhost:8080
+
+### 회원 이미지 수정 요청
+POST /api/users/profile HTTP/1.1
+Authorization: Bearer botobo.access.token
+Host: localhost:8080
+Content-Type: multipart/form-data; boundary=WebAppBoundary
+
+--WebAppBoundary
+Content-Disposition: form-data; name='profile'; filename='botobo.png'
+
+#### 회원 정보 수정 요청
+PUT /api/users/me HTTP/1.1
+Authorization: Bearer botobo.access.token
+Host: localhost:8080
+
+{
+ "userName": "수정된_조앤",
+ "profileUrl": "유저의 기존 프로필 url",
+ "bio": "수정된 바이오"
+}
+
+#### 회원명 중복 조회
+POST /api/users/name-check HTTP/1.1
+Authorization: Bearer botobo.access.token
+Host: localhost:8080
+
+{
+ "userName": "중복_조회할_이름"
+}
+
diff --git a/backend/src/docs/sql/ddl.sql b/backend/src/docs/sql/ddl.sql
index 279f2eac..6d22a67d 100644
--- a/backend/src/docs/sql/ddl.sql
+++ b/backend/src/docs/sql/ddl.sql
@@ -2,10 +2,12 @@
CREATE TABLE user(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
- github_id BIGINT NOT NULL,
+ social_id VARCHAR(255) NOT NULL,
user_name VARCHAR(255) NOT NULL,
profile_url VARCHAR(255) NOT NULL,
role VARCHAR(255) NOT NULL,
+ social_type VARCHAR(255),
+ bio VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
@@ -51,4 +53,13 @@ CREATE TABLE workbook_tag(
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY(workbook_id) REFERENCES workbook(id) ON UPDATE CASCADE ON DELETE RESTRICT,
FOREIGN KEY(tag_id) REFERENCES tag(id) ON UPDATE CASCADE ON DELETE RESTRICT
+);
+
+CREATE TABLE heart(
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ workbook_id BIGINT not null,
+ user_id BIGINT not null,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY(workbook_id) REFERENCES workbook(id) ON UPDATE CASCADE ON DELETE RESTRICT
);
\ No newline at end of file
diff --git a/backend/src/docs/sql/dml.sql b/backend/src/docs/sql/dml.sql
index 60a787e8..e171ba2e 100644
--- a/backend/src/docs/sql/dml.sql
+++ b/backend/src/docs/sql/dml.sql
@@ -6,6 +6,8 @@ ALTER TABLE user AUTO_INCREMENT = 1;
-- 어드민 생성
INSERT INTO user (github_id, user_name, profile_url, role, created_at, updated_at) VALUES (88036280, "1번 어드민", 'botobo.profile.url', 'ADMIN', now(), now());
INSERT INTO user (github_id, user_name, profile_url, role, created_at, updated_at) VALUES (88143445, "일반 유저", 'botobo.profile.url', 'USER', now(), now());
+UPDATE user SET user_name = 'botobo-admin', profile_url = 'https://avatars.githubusercontent.com/u/88036280?v=4' where id = 1;
+UPDATE user SET user_name = 'botoboUser', profile_url = 'https://avatars.githubusercontent.com/u/88143445?v=4' where id = 2;
-- 문제집 생성
INSERT INTO workbook (name, user_id, opened, deleted, created_at, updated_at) VALUES ('데이터베이스', 1, true, false, now(), now());
diff --git a/backend/src/main/java/botobo/core/DataLoader.java b/backend/src/main/java/botobo/core/DataLoader.java
index 3d311590..bbf85412 100644
--- a/backend/src/main/java/botobo/core/DataLoader.java
+++ b/backend/src/main/java/botobo/core/DataLoader.java
@@ -2,6 +2,7 @@
import botobo.core.domain.card.Card;
import botobo.core.domain.card.CardRepository;
+import botobo.core.domain.heart.Heart;
import botobo.core.domain.tag.Tag;
import botobo.core.domain.tag.Tags;
import botobo.core.domain.user.Role;
@@ -14,6 +15,7 @@
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
import java.io.BufferedReader;
import java.io.File;
@@ -29,6 +31,7 @@
@Component
@Slf4j
@Profile("local")
+@Transactional
public class DataLoader implements CommandLineRunner {
@Value("${dummy.file-path}")
@@ -58,13 +61,15 @@ public DataLoader(WorkbookRepository workbookRepository, CardRepository cardRepo
public void run(String... args) {
this.adminUser = saveAdminUser();
this.normalUser = saveNormalUser();
- this.adminWorkbookCount = 3;
+ this.adminWorkbookCount = 11;
String targetPath = filePath;
if (isBootrun(bootrunFilePath)) {
targetPath = bootrunFilePath;
}
for (String workbook : workbooks) {
- readFile(targetPath + workbook);
+ for (int i = 0; i < 10; i++) {
+ readFile(targetPath + workbook, i);
+ }
}
}
@@ -72,11 +77,11 @@ private boolean isBootrun(String filePath) {
return new File(filePath).exists();
}
- public void readFile(String filePath) {
+ public void readFile(String filePath, int i) {
try {
File file = new File(filePath);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8));
- saveData(bufferedReader);
+ saveData(bufferedReader, i);
bufferedReader.close();
} catch (FileNotFoundException e) {
log.info("Dummy Data 파일 경로를 찾지 못했습니다.");
@@ -85,12 +90,12 @@ public void readFile(String filePath) {
}
}
- private void saveData(BufferedReader bufferedReader) throws IOException {
+ private void saveData(BufferedReader bufferedReader, int i) throws IOException {
String workbookName = bufferedReader.readLine();
if (Objects.isNull(workbookName)) {
return;
}
- Workbook workbook = saveWorkbook(workbookName);
+ Workbook workbook = saveWorkbook(workbookName + i);
String question = bufferedReader.readLine();
String answer = bufferedReader.readLine();
@@ -103,8 +108,8 @@ private void saveData(BufferedReader bufferedReader) throws IOException {
private User saveAdminUser() {
User user = User.builder()
- .userName("1번 어드민")
- .githubId(88036280L)
+ .userName("admin")
+ .socialId("88036280")
.profileUrl("https://avatars.githubusercontent.com/u/88036280?v=4")
.role(Role.ADMIN)
.build();
@@ -113,8 +118,8 @@ private User saveAdminUser() {
private User saveNormalUser() {
User user = User.builder()
- .userName("일반 유저")
- .githubId(88143445L)
+ .userName("user")
+ .socialId("88143445")
.profileUrl("botobo.profile.url")
.role(Role.USER)
.build();
@@ -122,9 +127,9 @@ private User saveNormalUser() {
}
private Workbook saveWorkbook(String workbookName) {
- User author = this.normalUser;
+ User author = normalUser;
if (adminWorkbookCount > 0) {
- author = this.adminUser;
+ author = adminUser;
adminWorkbookCount--;
}
Workbook workbook = Workbook.builder()
@@ -133,6 +138,22 @@ private Workbook saveWorkbook(String workbookName) {
.opened(true)
.tags(Tags.of(Collections.singletonList(Tag.of(workbookName))))
.build();
+ Heart heart1 = Heart.builder()
+ .workbook(workbook)
+ .userId(adminUser.getId())
+ .build();
+ Heart heart2 = Heart.builder()
+ .workbook(workbook)
+ .userId(normalUser.getId())
+ .build();
+ if (author.equals(adminUser)) {
+ workbook.toggleHeart(heart1);
+ }
+ if (author.equals(normalUser)) {
+
+ workbook.toggleHeart(heart1);
+ workbook.toggleHeart(heart2);
+ }
return workbookRepository.save(workbook);
}
diff --git a/backend/src/main/java/botobo/core/application/AbstractUserService.java b/backend/src/main/java/botobo/core/application/AbstractUserService.java
new file mode 100644
index 00000000..12564d71
--- /dev/null
+++ b/backend/src/main/java/botobo/core/application/AbstractUserService.java
@@ -0,0 +1,19 @@
+package botobo.core.application;
+
+import botobo.core.domain.user.AppUser;
+import botobo.core.domain.user.User;
+import botobo.core.domain.user.UserRepository;
+import botobo.core.exception.user.UserNotFoundException;
+
+public abstract class AbstractUserService {
+ protected final UserRepository userRepository;
+
+ public AbstractUserService(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ }
+
+ protected User findUser(AppUser appUser) {
+ return userRepository.findById(appUser.getId())
+ .orElseThrow(UserNotFoundException::new);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/application/AdminService.java b/backend/src/main/java/botobo/core/application/AdminService.java
index a85c8982..237df498 100644
--- a/backend/src/main/java/botobo/core/application/AdminService.java
+++ b/backend/src/main/java/botobo/core/application/AdminService.java
@@ -11,29 +11,26 @@
import botobo.core.dto.admin.AdminCardResponse;
import botobo.core.dto.admin.AdminWorkbookRequest;
import botobo.core.dto.admin.AdminWorkbookResponse;
-import botobo.core.exception.user.UserNotFoundException;
import botobo.core.exception.workbook.WorkbookNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly = true)
-public class AdminService {
+public class AdminService extends AbstractUserService {
private final WorkbookRepository workbookRepository;
private final CardRepository cardRepository;
- private final UserRepository userRepository;
public AdminService(WorkbookRepository workbookRepository, CardRepository cardRepository, UserRepository userRepository) {
+ super(userRepository);
this.workbookRepository = workbookRepository;
this.cardRepository = cardRepository;
- this.userRepository = userRepository;
}
@Transactional
public AdminWorkbookResponse createWorkbook(AdminWorkbookRequest adminWorkbookRequest, AppUser appUser) {
- User user = userRepository.findById(appUser.getId())
- .orElseThrow(UserNotFoundException::new);
+ User user = findUser(appUser);
Workbook workbook = adminWorkbookRequest.toWorkbook(user);
Workbook savedWorkbook = workbookRepository.save(workbook);
return AdminWorkbookResponse.of(savedWorkbook);
diff --git a/backend/src/main/java/botobo/core/application/AuthService.java b/backend/src/main/java/botobo/core/application/AuthService.java
index b65c4874..fd50006c 100644
--- a/backend/src/main/java/botobo/core/application/AuthService.java
+++ b/backend/src/main/java/botobo/core/application/AuthService.java
@@ -2,16 +2,17 @@
import botobo.core.domain.user.AppUser;
import botobo.core.domain.user.Role;
+import botobo.core.domain.user.SocialType;
import botobo.core.domain.user.User;
import botobo.core.domain.user.UserRepository;
-import botobo.core.dto.auth.GithubUserInfoResponse;
import botobo.core.dto.auth.LoginRequest;
import botobo.core.dto.auth.TokenResponse;
import botobo.core.exception.auth.NotAdminException;
import botobo.core.exception.auth.TokenNotValidException;
import botobo.core.exception.user.UserNotFoundException;
-import botobo.core.infrastructure.GithubOauthManager;
import botobo.core.infrastructure.JwtTokenProvider;
+import botobo.core.infrastructure.OauthManager;
+import botobo.core.infrastructure.OauthManagerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -21,24 +22,26 @@
@Transactional(readOnly = true)
public class AuthService {
- private final GithubOauthManager githubOauthManager;
private final JwtTokenProvider jwtTokenProvider;
private final UserRepository userRepository;
+ private final OauthManagerFactory oauthManagerFactory;
- public AuthService(GithubOauthManager githubOauthManager, JwtTokenProvider jwtTokenProvider, UserRepository userRepository) {
- this.githubOauthManager = githubOauthManager;
+ public AuthService(JwtTokenProvider jwtTokenProvider, UserRepository userRepository, OauthManagerFactory oauthManagerFactory) {
this.jwtTokenProvider = jwtTokenProvider;
this.userRepository = userRepository;
+ this.oauthManagerFactory = oauthManagerFactory;
}
@Transactional
- public TokenResponse createToken(LoginRequest loginRequest) {
- GithubUserInfoResponse userInfo = githubOauthManager.getUserInfoFromGithub(loginRequest);
- Optional user = userRepository.findByGithubId(userInfo.getGithubId());
+ public TokenResponse createToken(String socialType, LoginRequest loginRequest) {
+ SocialType socialLoginType = SocialType.of(socialType);
+ OauthManager oauthManager = oauthManagerFactory.findOauthMangerBySocialType(socialLoginType);
+ User userInfo = oauthManager.getUserInfo(loginRequest.getCode());
+ Optional user = userRepository.findBySocialIdAndSocialType(userInfo.getSocialId(), socialLoginType);
if (user.isPresent()) {
return TokenResponse.of(jwtTokenProvider.createToken(user.get().getId()));
}
- User savedUser = userRepository.save(userInfo.toUser());
+ User savedUser = userRepository.save(userInfo);
return TokenResponse.of(jwtTokenProvider.createToken(savedUser.getId()));
}
@@ -46,6 +49,7 @@ public AppUser findAppUserByToken(String credentials) {
if (credentials == null) {
return AppUser.anonymous();
}
+ validateToken(credentials);
Long userId = jwtTokenProvider.getIdFromPayLoad(credentials);
if (userRepository.existsByIdAndRole(userId, Role.ADMIN)) {
return AppUser.admin(userId);
diff --git a/backend/src/main/java/botobo/core/application/CardService.java b/backend/src/main/java/botobo/core/application/CardService.java
index 61d82125..f71cf7e7 100644
--- a/backend/src/main/java/botobo/core/application/CardService.java
+++ b/backend/src/main/java/botobo/core/application/CardService.java
@@ -12,9 +12,8 @@
import botobo.core.dto.card.CardUpdateRequest;
import botobo.core.dto.card.CardUpdateResponse;
import botobo.core.dto.card.NextQuizCardsRequest;
-import botobo.core.exception.NotAuthorException;
import botobo.core.exception.card.CardNotFoundException;
-import botobo.core.exception.user.UserNotFoundException;
+import botobo.core.exception.user.NotAuthorException;
import botobo.core.exception.workbook.WorkbookNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -23,16 +22,15 @@
@Service
@Transactional(readOnly = true)
-public class CardService {
+public class CardService extends AbstractUserService {
private final CardRepository cardRepository;
private final WorkbookRepository workbookRepository;
- private final UserRepository userRepository;
public CardService(CardRepository cardRepository, WorkbookRepository workbookRepository, UserRepository userRepository) {
+ super(userRepository);
this.cardRepository = cardRepository;
this.workbookRepository = workbookRepository;
- this.userRepository = userRepository;
}
@Transactional
@@ -43,10 +41,6 @@ public CardResponse createCard(CardRequest cardRequest, AppUser appUser) {
return CardResponse.of(card);
}
- private User findUser(AppUser appUser) {
- return userRepository.findById(appUser.getId()).orElseThrow(UserNotFoundException::new);
- }
-
private Card convertToCard(CardRequest cardRequest, User author) {
Workbook workbook = findWorkbook(cardRequest.getWorkbookId());
if (!workbook.isAuthorOf(author)) {
@@ -85,10 +79,17 @@ public void deleteCard(Long id, AppUser appUser) {
card.delete();
}
- // TODO nextQuizCard도 Interceptor 타도록 변경
@Transactional
- public void selectNextQuizCards(NextQuizCardsRequest nextQuizCardsRequest) {
+ public void selectNextQuizCards(NextQuizCardsRequest nextQuizCardsRequest, AppUser appUser) {
+ User user = findUser(appUser);
List cards = cardRepository.findByIdIn(nextQuizCardsRequest.getCardIds());
+ if (!isAuthorOfCards(user, cards)) {
+ throw new NotAuthorException();
+ }
cards.forEach(Card::makeNextQuiz);
}
+
+ private boolean isAuthorOfCards(User user, List cards) {
+ return cards.stream().allMatch(card -> card.isAuthorOf(user));
+ }
}
diff --git a/backend/src/main/java/botobo/core/application/QuizService.java b/backend/src/main/java/botobo/core/application/QuizService.java
index dc2eabfd..2106de84 100644
--- a/backend/src/main/java/botobo/core/application/QuizService.java
+++ b/backend/src/main/java/botobo/core/application/QuizService.java
@@ -5,8 +5,14 @@
import botobo.core.domain.card.Cards;
import botobo.core.domain.card.GuestCards;
import botobo.core.domain.card.Quiz;
+import botobo.core.domain.user.AppUser;
+import botobo.core.domain.user.User;
+import botobo.core.domain.user.UserRepository;
+import botobo.core.domain.workbook.Workbook;
import botobo.core.domain.workbook.WorkbookRepository;
+import botobo.core.dto.card.QuizRequest;
import botobo.core.dto.card.QuizResponse;
+import botobo.core.exception.user.NotAuthorException;
import botobo.core.exception.workbook.WorkbookNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -15,13 +21,14 @@
@Service
@Transactional(readOnly = true)
-public class QuizService {
+public class QuizService extends AbstractUserService {
private static final int DEFAULT_QUIZ_COUNT = 10;
private final WorkbookRepository workbookRepository;
private final CardRepository cardRepository;
- public QuizService(WorkbookRepository workbookRepository, CardRepository cardRepository) {
+ public QuizService(WorkbookRepository workbookRepository, CardRepository cardRepository, UserRepository userRepository) {
+ super(userRepository);
this.workbookRepository = workbookRepository;
this.cardRepository = cardRepository;
}
@@ -32,13 +39,34 @@ public List createQuizForGuest() {
}
@Transactional
- public List createQuiz(List workbookIds) {
- validateWorkbookIds(workbookIds);
- final Cards quiz = makeQuiz(cardRepository.findCardsByWorkbookIds(workbookIds), DEFAULT_QUIZ_COUNT)
+ public List createQuiz(QuizRequest quizRequest, AppUser appUser) {
+ User user = findUser(appUser);
+ final List workbookIds = quizRequest.getWorkbookIds();
+ final int count = quizRequest.getCount();
+
+ validateWorkbooks(workbookIds, user);
+
+ final Cards quiz = makeQuiz(cardRepository.findCardsByWorkbookIds(workbookIds), count)
.postProcess();
return QuizResponse.cardsOf(quiz);
}
+ private void validateWorkbooks(List workbookIds, User user) {
+ for (Long id : workbookIds) {
+ validateAuthor(findWorkbookById(id), user);
+ }
+ }
+
+ private Workbook findWorkbookById(Long workbookId) {
+ return workbookRepository.findById(workbookId).orElseThrow(WorkbookNotFoundException::new);
+ }
+
+ private void validateAuthor(Workbook workbook, User user) {
+ if (!workbook.isAuthorOf(user)) {
+ throw new NotAuthorException();
+ }
+ }
+
public List createQuizFromWorkbook(Long workbookId) {
validateWorkbook(workbookId);
final Cards quiz = makeQuiz(cardRepository.findCardsByWorkbookId(workbookId), DEFAULT_QUIZ_COUNT);
@@ -50,19 +78,6 @@ private Cards makeQuiz(List cards, int counts) {
return preparedQuizSet.makeQuiz();
}
- private void validateWorkbookIds(List workbookIds) {
- // Opinion: id를 예외에 넘겨서 로그에 남겨도 좋을 것 같음.
- for (Long id : workbookIds) {
- validateWorkbookId(id);
- }
- }
-
- private void validateWorkbookId(Long workbookId) {
- if (!workbookRepository.existsById(workbookId)) {
- throw new WorkbookNotFoundException();
- }
- }
-
private void validateWorkbook(Long workbookId) {
if (!workbookRepository.existsByIdAndOpenedTrue(workbookId)) {
throw new WorkbookNotFoundException();
diff --git a/backend/src/main/java/botobo/core/application/SearchService.java b/backend/src/main/java/botobo/core/application/SearchService.java
new file mode 100644
index 00000000..d3d9aa26
--- /dev/null
+++ b/backend/src/main/java/botobo/core/application/SearchService.java
@@ -0,0 +1,54 @@
+package botobo.core.application;
+
+import botobo.core.domain.tag.TagRepository;
+import botobo.core.domain.tag.Tags;
+import botobo.core.domain.user.User;
+import botobo.core.domain.user.UserRepository;
+import botobo.core.domain.workbook.Workbook;
+import botobo.core.domain.workbook.WorkbookRepository;
+import botobo.core.dto.tag.TagResponse;
+import botobo.core.dto.user.SimpleUserResponse;
+import botobo.core.dto.workbook.WorkbookResponse;
+import botobo.core.ui.search.SearchKeyword;
+import botobo.core.ui.search.WorkbookSearchParameter;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@Service
+@Transactional(readOnly = true)
+public class SearchService {
+
+ private final WorkbookRepository workbookRepository;
+ private final TagRepository tagRepository;
+ private final UserRepository userRepository;
+
+ public SearchService(WorkbookRepository workbookRepository, TagRepository tagRepository, UserRepository userRepository) {
+ this.workbookRepository = workbookRepository;
+ this.tagRepository = tagRepository;
+ this.userRepository = userRepository;
+ }
+
+ public List searchWorkbooks(WorkbookSearchParameter workbookSearchParameter) {
+ Specification specification = workbookSearchParameter.toSpecification();
+ PageRequest pageRequest = workbookSearchParameter.toPageRequest();
+
+ Page page = workbookRepository.findAll(specification, pageRequest);
+ List workbooks = page.toList();
+ return WorkbookResponse.openedListOf(workbooks);
+ }
+
+ public List searchTags(SearchKeyword keyword) {
+ Tags tags = Tags.of(tagRepository.findByKeyword(keyword.toLowercase()));
+ return TagResponse.listOf(tags);
+ }
+
+ public List searchUsers(SearchKeyword keyword) {
+ List users = userRepository.findByKeyword(keyword.getValue());
+ return SimpleUserResponse.listOf(users);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/application/UserService.java b/backend/src/main/java/botobo/core/application/UserService.java
index d92695c4..6beb3e83 100644
--- a/backend/src/main/java/botobo/core/application/UserService.java
+++ b/backend/src/main/java/botobo/core/application/UserService.java
@@ -1,29 +1,68 @@
package botobo.core.application;
+import botobo.core.domain.user.AppUser;
import botobo.core.domain.user.User;
import botobo.core.domain.user.UserRepository;
+import botobo.core.dto.user.ProfileResponse;
+import botobo.core.dto.user.UserNameRequest;
import botobo.core.dto.user.UserResponse;
-import botobo.core.exception.user.UserNotFoundException;
+import botobo.core.dto.user.UserUpdateRequest;
+import botobo.core.exception.user.UserNameDuplicatedException;
+import botobo.core.infrastructure.S3Uploader;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
@Service
@Transactional(readOnly = true)
-public class UserService {
+public class UserService extends AbstractUserService {
+
+ private final S3Uploader s3Uploader;
+
+ public UserService(UserRepository userRepository, S3Uploader s3Uploader) {
+ super(userRepository);
+ this.s3Uploader = s3Uploader;
+ }
- private final UserRepository userRepository;
+ public UserResponse findById(AppUser appUser) {
+ return UserResponse.of(findUser(appUser));
+ }
- public UserService(UserRepository userRepository) {
- this.userRepository = userRepository;
+ @Transactional
+ public UserResponse update(UserUpdateRequest userUpdateRequest, AppUser appUser) {
+ validateDuplicatedUserName(userUpdateRequest.getUserName(), appUser);
+ User user = findUser(appUser);
+ user.update(userUpdateRequest.toUser());
+ return UserResponse.of(user);
}
- public UserResponse findById(Long id) {
- User user = userRepository.findById(id)
- .orElseThrow(UserNotFoundException::new);
- return UserResponse.builder()
- .id(user.getId())
- .userName(user.getUserName())
- .profileUrl(user.getProfileUrl())
+ @Transactional
+ public ProfileResponse updateProfile(MultipartFile multipartFile, AppUser appUser) throws IOException {
+ User user = findUser(appUser);
+ String oldProfileUrl = user.getProfileUrl();
+ String newProfileUrl = s3Uploader.upload(multipartFile, user.getUserName());
+
+ user.updateProfileUrl(newProfileUrl);
+
+ s3Uploader.deleteFromS3(oldProfileUrl);
+ return ProfileResponse.builder()
+ .profileUrl(newProfileUrl)
.build();
}
+
+ public void checkDuplicatedUserName(UserNameRequest userNameRequest, AppUser appUser) {
+ validateDuplicatedUserName(userNameRequest.getUserName(), appUser);
+ }
+
+ private void validateDuplicatedUserName(String requestedName, AppUser me) {
+ userRepository.findByUserName(requestedName).ifPresent(
+ findUser -> {
+ if (!findUser.isSameId(me.getId())) {
+ throw new UserNameDuplicatedException();
+ }
+ }
+ );
+ }
}
diff --git a/backend/src/main/java/botobo/core/application/WorkbookService.java b/backend/src/main/java/botobo/core/application/WorkbookService.java
index 8802e03b..8ea4fe4d 100644
--- a/backend/src/main/java/botobo/core/application/WorkbookService.java
+++ b/backend/src/main/java/botobo/core/application/WorkbookService.java
@@ -3,23 +3,21 @@
import botobo.core.domain.card.Card;
import botobo.core.domain.card.CardRepository;
import botobo.core.domain.card.Cards;
+import botobo.core.domain.heart.Heart;
import botobo.core.domain.tag.Tags;
import botobo.core.domain.user.AppUser;
import botobo.core.domain.user.User;
import botobo.core.domain.user.UserRepository;
import botobo.core.domain.workbook.Workbook;
-import botobo.core.domain.workbook.WorkbookFinder;
import botobo.core.domain.workbook.WorkbookRepository;
-import botobo.core.domain.workbook.criteria.SearchKeyword;
-import botobo.core.domain.workbook.criteria.WorkbookCriteria;
import botobo.core.dto.card.ScrapCardRequest;
+import botobo.core.dto.heart.HeartResponse;
import botobo.core.dto.workbook.WorkbookCardResponse;
import botobo.core.dto.workbook.WorkbookRequest;
import botobo.core.dto.workbook.WorkbookResponse;
import botobo.core.dto.workbook.WorkbookUpdateRequest;
-import botobo.core.exception.NotAuthorException;
import botobo.core.exception.card.CardNotFoundException;
-import botobo.core.exception.user.UserNotFoundException;
+import botobo.core.exception.user.NotAuthorException;
import botobo.core.exception.workbook.WorkbookNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -30,17 +28,16 @@
@Service
@Transactional(readOnly = true)
-public class WorkbookService {
+public class WorkbookService extends AbstractUserService {
private final WorkbookRepository workbookRepository;
- private final UserRepository userRepository;
private final CardRepository cardRepository;
private final TagService tagService;
public WorkbookService(WorkbookRepository workbookRepository, UserRepository userRepository,
CardRepository cardRepository, TagService tagService) {
+ super(userRepository);
this.workbookRepository = workbookRepository;
- this.userRepository = userRepository;
this.cardRepository = cardRepository;
this.tagService = tagService;
}
@@ -88,25 +85,10 @@ public List findWorkbooksByUser(AppUser appUser) {
);
}
- public List findPublicWorkbooksBySearch(String search) {
- WorkbookCriteria workbookCriteria = WorkbookCriteria.builder()
- .searchKeyword(SearchKeyword.from(search))
- .build();
-
- return findWorkbookByCriteria(workbookCriteria);
- }
-
- private List findWorkbookByCriteria(WorkbookCriteria workbookCriteria) {
- List workbooks = WorkbookFinder.builder()
- .workbooks(workbookRepository.findAll())
- .build()
- .apply(workbookCriteria);
-
- return WorkbookResponse.openedListOf(workbooks);
- }
-
- public WorkbookCardResponse findWorkbookCardsById(Long id) {
+ public WorkbookCardResponse findWorkbookCardsById(Long id, AppUser appUser) {
+ User user = findUser(appUser);
Workbook workbook = findWorkbookByIdAndOrderCardByNew(id);
+ validateAuthor(user, workbook);
return WorkbookCardResponse.ofUserWorkbook(workbook);
}
@@ -115,12 +97,13 @@ private Workbook findWorkbookByIdAndOrderCardByNew(Long id) {
.orElseThrow(WorkbookNotFoundException::new);
}
- public WorkbookCardResponse findPublicWorkbookById(Long id) {
+ public WorkbookCardResponse findPublicWorkbookById(Long id, AppUser appUser) {
Workbook workbook = findWorkbookByIdAndOrderCardByNew(id);
if (!workbook.isOpened()) {
throw new NotAuthorException();
}
- return WorkbookCardResponse.ofOpenedWorkbook(workbook);
+ boolean heartExists = workbook.existsHeartByAppUser(appUser);
+ return WorkbookCardResponse.ofOpenedWorkbook(workbook, heartExists);
}
private void validateAuthor(User user, Workbook workbook) {
@@ -140,11 +123,6 @@ public void scrapSelectedCardsToWorkbook(Long workbookId, ScrapCardRequest scrap
addScrappedCardsToWorkbook(workbook, scrappedCards);
}
- private User findUser(AppUser appUser) {
- return userRepository.findById(appUser.getId())
- .orElseThrow(UserNotFoundException::new);
- }
-
private Workbook findWorkbook(Long workbookId) {
return workbookRepository.findById(workbookId)
.orElseThrow(WorkbookNotFoundException::new);
@@ -163,4 +141,17 @@ private List scrapCards(List cardIds) {
private void addScrappedCardsToWorkbook(Workbook workbook, Cards scrappedCards) {
workbook.addCards(scrappedCards);
}
+
+ @Transactional
+ public HeartResponse toggleHeart(Long workbookId, AppUser appUser) {
+ Long userId = appUser.getId();
+ Workbook workbook = findWorkbook(workbookId);
+ Heart heart = Heart.builder()
+ .workbook(workbook)
+ .userId(userId)
+ .build();
+ return HeartResponse.of(
+ workbook.toggleHeart(heart)
+ );
+ }
}
diff --git a/backend/src/main/java/botobo/core/config/ApplicationConfig.java b/backend/src/main/java/botobo/core/config/ApplicationConfig.java
new file mode 100644
index 00000000..5e214846
--- /dev/null
+++ b/backend/src/main/java/botobo/core/config/ApplicationConfig.java
@@ -0,0 +1,23 @@
+package botobo.core.config;
+
+import botobo.core.ui.search.SearchArgumentResolver;
+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 ApplicationConfig implements WebMvcConfigurer {
+
+ @Override
+ public void addArgumentResolvers(List resolvers) {
+ resolvers.add(searchArgumentResolver());
+ }
+
+ @Bean
+ public SearchArgumentResolver searchArgumentResolver() {
+ return new SearchArgumentResolver();
+ }
+}
diff --git a/backend/src/main/java/botobo/core/config/AuthenticationPrincipalConfig.java b/backend/src/main/java/botobo/core/config/AuthenticationPrincipalConfig.java
index a96921ec..6c3f6e3f 100644
--- a/backend/src/main/java/botobo/core/config/AuthenticationPrincipalConfig.java
+++ b/backend/src/main/java/botobo/core/config/AuthenticationPrincipalConfig.java
@@ -4,6 +4,8 @@
import botobo.core.ui.auth.AdminInterceptor;
import botobo.core.ui.auth.AuthenticationPrincipalArgumentResolver;
import botobo.core.ui.auth.AuthorizationInterceptor;
+import botobo.core.ui.auth.PathMatcherInterceptor;
+import botobo.core.ui.auth.PathMethod;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
@@ -43,11 +45,28 @@ public void addArgumentResolvers(List resolvers)
@Override
public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(authorizationInterceptor())
- .addPathPatterns("/api/**")
- .excludePathPatterns("/api/login", "/api/quizzes/guest", "/api/docs/**", "/api/workbooks", "/api/workbooks/public/**");
+ registry.addInterceptor(authPathMatcherInterceptor());
+ registry.addInterceptor(adminPathMatcherInterceptor());
+ }
- registry.addInterceptor(adminInterceptor())
- .addPathPatterns("/api/admin/**");
+ @Bean
+ public PathMatcherInterceptor authPathMatcherInterceptor() {
+ return new PathMatcherInterceptor(authorizationInterceptor())
+ .addPathPatterns("/api/**", PathMethod.ANY)
+ .excludePathPatterns("/api/**", PathMethod.OPTIONS)
+ .excludePathPatterns("/api/workbooks", PathMethod.GET)
+ .excludePathPatterns("/api/quizzes/**", PathMethod.GET)
+ .excludePathPatterns("/api/login/**", PathMethod.POST)
+ .excludePathPatterns("/api/docs/**", PathMethod.GET)
+ .excludePathPatterns("/api/workbooks/public/**", PathMethod.GET)
+ .excludePathPatterns("/api/search/**", PathMethod.GET);
+ }
+
+ @Bean
+ public PathMatcherInterceptor adminPathMatcherInterceptor() {
+ return new PathMatcherInterceptor(adminInterceptor())
+ .addPathPatterns("/api/admin/workbooks", PathMethod.POST)
+ .addPathPatterns("/api/admin/cards", PathMethod.POST)
+ .excludePathPatterns("/api/**", PathMethod.OPTIONS);
}
}
diff --git a/backend/src/main/java/botobo/core/config/S3Config.java b/backend/src/main/java/botobo/core/config/S3Config.java
new file mode 100644
index 00000000..7e187557
--- /dev/null
+++ b/backend/src/main/java/botobo/core/config/S3Config.java
@@ -0,0 +1,24 @@
+package botobo.core.config;
+
+import botobo.core.infrastructure.YamlPropertySourceFactory;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3ClientBuilder;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+
+@Configuration
+@PropertySource(value = "classpath:aws-s3.yml", factory = YamlPropertySourceFactory.class)
+public class S3Config {
+ @Value("${cloud.aws.region.static}")
+ private String region;
+
+ @Bean
+ public AmazonS3 amazonS3() {
+ return AmazonS3ClientBuilder
+ .standard()
+ .withRegion(region)
+ .build();
+ }
+}
diff --git a/backend/src/main/java/botobo/core/config/WebConfig.java b/backend/src/main/java/botobo/core/config/WebConfig.java
index b2c8084d..dae0e84e 100644
--- a/backend/src/main/java/botobo/core/config/WebConfig.java
+++ b/backend/src/main/java/botobo/core/config/WebConfig.java
@@ -14,7 +14,7 @@ public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
- .allowedOrigins("*")
+ .allowedOrigins("http://localhost:3000", "https://botobo.r-e.kr", "https://botobo.kro.kr")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.maxAge(3000);
}
diff --git a/backend/src/main/java/botobo/core/domain/card/Card.java b/backend/src/main/java/botobo/core/domain/card/Card.java
index 5c7a2a18..bfab06c7 100644
--- a/backend/src/main/java/botobo/core/domain/card/Card.java
+++ b/backend/src/main/java/botobo/core/domain/card/Card.java
@@ -3,6 +3,8 @@
import botobo.core.domain.BaseEntity;
import botobo.core.domain.user.User;
import botobo.core.domain.workbook.Workbook;
+import botobo.core.exception.card.CardAnswerNullException;
+import botobo.core.exception.card.CardQuestionNullException;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -63,10 +65,10 @@ private Card(Long id, String question, String answer, Workbook workbook, int enc
private void validateNull(String question, String answer) {
if (Objects.isNull(question)) {
- throw new IllegalArgumentException("Card의 question에는 null이 들어갈 수 없습니다.");
+ throw new CardQuestionNullException();
}
if (Objects.isNull(answer)) {
- throw new IllegalArgumentException("Card의 answer에는 null이 들어갈 수 없습니다.");
+ throw new CardAnswerNullException();
}
}
diff --git a/backend/src/main/java/botobo/core/domain/card/Quiz.java b/backend/src/main/java/botobo/core/domain/card/Quiz.java
index 69264be5..48e63afc 100644
--- a/backend/src/main/java/botobo/core/domain/card/Quiz.java
+++ b/backend/src/main/java/botobo/core/domain/card/Quiz.java
@@ -8,7 +8,6 @@ public class Quiz {
private final Cards cards;
private final int targetCounts;
-
public Quiz(List cards, int targetCounts) {
this.cards = new Cards(cards);
this.targetCounts = targetCounts;
diff --git a/backend/src/main/java/botobo/core/domain/heart/Heart.java b/backend/src/main/java/botobo/core/domain/heart/Heart.java
new file mode 100644
index 00000000..6172dcb5
--- /dev/null
+++ b/backend/src/main/java/botobo/core/domain/heart/Heart.java
@@ -0,0 +1,51 @@
+package botobo.core.domain.heart;
+
+import botobo.core.domain.BaseEntity;
+import botobo.core.domain.workbook.Workbook;
+import botobo.core.exception.heart.HeartCreationFailureException;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.ForeignKey;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import java.util.Objects;
+
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+@Entity
+public class Heart extends BaseEntity {
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "workbook_id", nullable = false, foreignKey = @ForeignKey(name = "FK_like_to_workbook"))
+ private Workbook workbook;
+
+ @Column(nullable = false)
+ private Long userId;
+
+ @Builder
+ public Heart(Long id, Workbook workbook, Long userId) {
+ validateNotNull(workbook, userId);
+ this.id = id;
+ this.workbook = workbook;
+ this.userId = userId;
+ }
+
+ private void validateNotNull(Workbook workbook, Long userId) {
+ if (Objects.isNull(workbook)) {
+ throw new HeartCreationFailureException("문제집 필요");
+ }
+ if (Objects.isNull(userId)) {
+ throw new HeartCreationFailureException("유저 아이디 필요");
+ }
+ }
+
+ public boolean ownedBy(Long userId) {
+ return this.userId.equals(userId);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/domain/heart/HeartRepository.java b/backend/src/main/java/botobo/core/domain/heart/HeartRepository.java
new file mode 100644
index 00000000..b55f6e6a
--- /dev/null
+++ b/backend/src/main/java/botobo/core/domain/heart/HeartRepository.java
@@ -0,0 +1,6 @@
+package botobo.core.domain.heart;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface HeartRepository extends JpaRepository {
+}
diff --git a/backend/src/main/java/botobo/core/domain/heart/Hearts.java b/backend/src/main/java/botobo/core/domain/heart/Hearts.java
new file mode 100644
index 00000000..0042f52b
--- /dev/null
+++ b/backend/src/main/java/botobo/core/domain/heart/Hearts.java
@@ -0,0 +1,98 @@
+package botobo.core.domain.heart;
+
+import botobo.core.domain.workbook.Workbook;
+import botobo.core.exception.heart.HeartAdditionFailureException;
+import botobo.core.exception.heart.HeartRemovalFailureException;
+import botobo.core.exception.heart.HeartsCreationFailureException;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Embeddable;
+import javax.persistence.OneToMany;
+import java.util.ArrayList;
+import java.util.List;
+
+@Getter
+@NoArgsConstructor
+@Embeddable
+public class Hearts {
+
+ @OneToMany(mappedBy = "workbook", cascade = CascadeType.PERSIST, orphanRemoval = true)
+ private final List hearts = new ArrayList<>();
+
+ private Hearts(List hearts) {
+ addHeartList(hearts);
+ }
+
+ public static Hearts of(List hearts) {
+ return new Hearts(hearts);
+ }
+
+ private void addHeartList(List hearts) {
+ for (Heart heart : hearts) {
+ addHeart(heart);
+ }
+ }
+
+ private void addHeart(Heart heart) {
+ validateSameWorkbook(heart.getWorkbook());
+ validateNonExistentUserId(heart.getUserId());
+ if (contains(heart)) {
+ throw new HeartAdditionFailureException();
+ }
+ hearts.add(heart);
+ }
+
+ private void validateSameWorkbook(Workbook workbook) {
+ if (hearts.isEmpty()) {
+ return;
+ }
+ Workbook workbookOfFirstHeart = hearts.get(0).getWorkbook();
+ if (!workbook.equals(workbookOfFirstHeart)) {
+ throw new HeartsCreationFailureException("같은 문제집의 하트만 추가할 수 있습니다");
+ }
+ }
+
+ private void validateNonExistentUserId(Long userId) {
+ if (contains(userId)) {
+ throw new HeartsCreationFailureException("하나의 유저 아이디를 여러번 추가할 수 없습니다");
+ }
+ }
+
+ private boolean contains(Heart heart) {
+ Long userId = heart.getUserId();
+ return contains(userId);
+ }
+
+ public boolean contains(Long userId) {
+ return hearts.parallelStream()
+ .anyMatch(h -> h.ownedBy(userId));
+ }
+
+ public boolean toggleHeart(Heart heart) {
+ if (contains(heart)) {
+ removeHeart(heart);
+ return false;
+ }
+ hearts.add(heart);
+ return true;
+ }
+
+ private void removeHeart(Heart heart) {
+ Long userId = heart.getUserId();
+ Heart removalTarget = hearts.parallelStream()
+ .filter(h -> h.ownedBy(userId))
+ .findAny()
+ .orElseThrow(HeartRemovalFailureException::new);
+ hearts.remove(removalTarget);
+ }
+
+ public void addHearts(Hearts hearts) {
+ addHeartList(hearts.hearts);
+ }
+
+ public int size() {
+ return hearts.size();
+ }
+}
diff --git a/backend/src/main/java/botobo/core/domain/tag/Tag.java b/backend/src/main/java/botobo/core/domain/tag/Tag.java
index 897b848b..9c0dd98b 100644
--- a/backend/src/main/java/botobo/core/domain/tag/Tag.java
+++ b/backend/src/main/java/botobo/core/domain/tag/Tag.java
@@ -2,7 +2,7 @@
import botobo.core.domain.BaseEntity;
import botobo.core.domain.workbooktag.WorkbookTag;
-import botobo.core.exception.tag.TagCreationFailureException;
+import botobo.core.exception.tag.TagNullException;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -32,7 +32,7 @@ private Tag(TagName tagName) {
private void validateNotNull(TagName tagName) {
if (Objects.isNull(tagName)) {
- throw new TagCreationFailureException("tagName은 null이 될 수 없습니다.");
+ throw new TagNullException();
}
}
diff --git a/backend/src/main/java/botobo/core/domain/tag/TagName.java b/backend/src/main/java/botobo/core/domain/tag/TagName.java
index 91814199..657fda49 100644
--- a/backend/src/main/java/botobo/core/domain/tag/TagName.java
+++ b/backend/src/main/java/botobo/core/domain/tag/TagName.java
@@ -1,7 +1,8 @@
package botobo.core.domain.tag;
-import botobo.core.exception.tag.InvalidTagNameException;
+import botobo.core.exception.tag.TagNameLengthException;
+import botobo.core.exception.tag.TagNameNullException;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@@ -35,21 +36,19 @@ public static TagName of(String value) {
private void validateNotNull(String value) {
if (Objects.isNull(value)) {
- throw new InvalidTagNameException("null이 될 수 없습니다");
+ throw new TagNameNullException();
}
}
private void validateNotBlank(String value) {
if (value.isBlank()) {
- throw new InvalidTagNameException("비어있거나 공백 문자열이 될 수 없습니다");
+ throw new TagNameNullException();
}
}
private void validateLength(String value) {
if (value.length() > MAX_LENGTH) {
- throw new InvalidTagNameException(
- String.format("%s자 이하여야 합니다", MAX_LENGTH)
- );
+ throw new TagNameLengthException();
}
}
}
diff --git a/backend/src/main/java/botobo/core/domain/tag/TagRepository.java b/backend/src/main/java/botobo/core/domain/tag/TagRepository.java
index 0c7d8e01..b836fc15 100644
--- a/backend/src/main/java/botobo/core/domain/tag/TagRepository.java
+++ b/backend/src/main/java/botobo/core/domain/tag/TagRepository.java
@@ -1,11 +1,18 @@
package botobo.core.domain.tag;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
+import java.util.List;
import java.util.Optional;
@Repository
public interface TagRepository extends JpaRepository {
+
Optional findByTagName(TagName tagName);
+
+ @Query(value = "SELECT * FROM tag t WHERE t.NAME LIKE %:keyword% LIMIT 10", nativeQuery = true)
+ List findByKeyword(@Param("keyword") String keyword);
}
diff --git a/backend/src/main/java/botobo/core/domain/user/AppUser.java b/backend/src/main/java/botobo/core/domain/user/AppUser.java
index fa3173b1..b00d54ba 100644
--- a/backend/src/main/java/botobo/core/domain/user/AppUser.java
+++ b/backend/src/main/java/botobo/core/domain/user/AppUser.java
@@ -1,5 +1,6 @@
package botobo.core.domain.user;
+import botobo.core.exception.user.AnonymousHasNotIdException;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
@@ -37,7 +38,7 @@ public boolean isAnonymous() {
public long getId() {
if (isAnonymous()) {
- throw new IllegalStateException("비회원의 ID는 조회할 수 없습니다.");
+ throw new AnonymousHasNotIdException("비회원의 ID는 조회할 수 없습니다.");
}
return id;
}
diff --git a/backend/src/main/java/botobo/core/domain/user/SocialType.java b/backend/src/main/java/botobo/core/domain/user/SocialType.java
new file mode 100644
index 00000000..808ad91f
--- /dev/null
+++ b/backend/src/main/java/botobo/core/domain/user/SocialType.java
@@ -0,0 +1,16 @@
+package botobo.core.domain.user;
+
+import botobo.core.exception.user.SocialTypeNotFoundException;
+
+import java.util.Arrays;
+
+public enum SocialType {
+ GITHUB, GOOGLE;
+
+ public static SocialType of(String input) {
+ return Arrays.stream(values())
+ .filter(socialType -> socialType.name().equals(input.toUpperCase()))
+ .findFirst()
+ .orElseThrow(SocialTypeNotFoundException::new);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/domain/user/User.java b/backend/src/main/java/botobo/core/domain/user/User.java
index 8cc6afa6..45c18ed7 100644
--- a/backend/src/main/java/botobo/core/domain/user/User.java
+++ b/backend/src/main/java/botobo/core/domain/user/User.java
@@ -2,6 +2,7 @@
import botobo.core.domain.BaseEntity;
import botobo.core.domain.workbook.Workbook;
+import botobo.core.exception.user.ProfileUpdateNotAllowedException;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -22,7 +23,7 @@
public class User extends BaseEntity {
@Column(nullable = false)
- private Long githubId;
+ private String socialId;
@Column(nullable = false)
private String userName;
@@ -30,25 +31,36 @@ public class User extends BaseEntity {
@Column(nullable = false)
private String profileUrl;
+ @Column(nullable = false)
+ private String bio = "";
+
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
+ @Enumerated(EnumType.STRING)
+ private SocialType socialType;
+
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List workbooks = new ArrayList<>();
@Builder
- public User(Long id, Long githubId, String userName, String profileUrl, Role role, List workbooks) {
+ public User(Long id, String socialId, String userName, String profileUrl, String bio,
+ Role role, List workbooks, SocialType socialType) {
this.id = id;
- this.githubId = githubId;
+ this.socialId = socialId;
this.userName = userName;
this.profileUrl = profileUrl;
+ if (Objects.nonNull(bio)) {
+ this.bio = bio;
+ }
if (Objects.nonNull(role)) {
this.role = role;
}
if (Objects.nonNull(workbooks)) {
this.workbooks = workbooks;
}
+ this.socialType = socialType;
}
public AppUser toAppUser() {
@@ -65,4 +77,28 @@ public boolean isAdmin() {
public boolean isUser() {
return role.isUser();
}
+
+ public void updateProfileUrl(String profileUrl) {
+ this.profileUrl = profileUrl;
+ }
+
+ public void update(User other) {
+ validateProfileUrl(other.getProfileUrl());
+ this.userName = other.getUserName();
+ this.bio = other.getBio();
+ }
+
+ private void validateProfileUrl(String profileUrl) {
+ if (!Objects.equals(this.profileUrl, profileUrl)) {
+ throw new ProfileUpdateNotAllowedException();
+ }
+ }
+
+ public boolean isSameId(Long id) {
+ return Objects.equals(this.id, id);
+ }
+
+ public boolean isSameName(User other) {
+ return Objects.equals(this.userName, other.getUserName());
+ }
}
diff --git a/backend/src/main/java/botobo/core/domain/user/UserRepository.java b/backend/src/main/java/botobo/core/domain/user/UserRepository.java
index cb0c4ed3..7c30c738 100644
--- a/backend/src/main/java/botobo/core/domain/user/UserRepository.java
+++ b/backend/src/main/java/botobo/core/domain/user/UserRepository.java
@@ -1,11 +1,20 @@
package botobo.core.domain.user;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import java.util.List;
import java.util.Optional;
public interface UserRepository extends JpaRepository {
- Optional findByGithubId(Long githubId);
+
+ Optional findByUserName(String userName);
+
+ @Query(value = "SELECT * FROM user u WHERE u.user_name LIKE %:keyword% LIMIT 10", nativeQuery = true)
+ List findByKeyword(@Param("keyword") String keyword);
boolean existsByIdAndRole(Long id, Role role);
+
+ Optional findBySocialIdAndSocialType(String socialId, SocialType socialType);
}
diff --git a/backend/src/main/java/botobo/core/domain/user/s3/ImageExtension.java b/backend/src/main/java/botobo/core/domain/user/s3/ImageExtension.java
new file mode 100644
index 00000000..548f8aef
--- /dev/null
+++ b/backend/src/main/java/botobo/core/domain/user/s3/ImageExtension.java
@@ -0,0 +1,22 @@
+package botobo.core.domain.user.s3;
+
+import java.util.Arrays;
+
+public enum ImageExtension {
+ JPG("jpg"),
+ JPEG("jpeg"),
+ PNG("png"),
+ BMP("bmp");
+
+ private final String extension;
+
+ ImageExtension(String extension) {
+ this.extension = extension;
+ }
+
+ public static boolean isAllowedExtension(String ext) {
+ return Arrays.stream(values())
+ .anyMatch(imageExtension -> ext.equalsIgnoreCase(imageExtension.extension));
+
+ }
+}
diff --git a/backend/src/main/java/botobo/core/domain/workbook/Workbook.java b/backend/src/main/java/botobo/core/domain/workbook/Workbook.java
index 13484cf6..566104b9 100644
--- a/backend/src/main/java/botobo/core/domain/workbook/Workbook.java
+++ b/backend/src/main/java/botobo/core/domain/workbook/Workbook.java
@@ -3,9 +3,14 @@
import botobo.core.domain.BaseEntity;
import botobo.core.domain.card.Card;
import botobo.core.domain.card.Cards;
+import botobo.core.domain.heart.Heart;
+import botobo.core.domain.heart.Hearts;
import botobo.core.domain.tag.Tags;
+import botobo.core.domain.user.AppUser;
import botobo.core.domain.user.User;
import botobo.core.domain.workbooktag.WorkbookTag;
+import botobo.core.exception.workbook.WorkbookNameLengthException;
+import botobo.core.exception.workbook.WorkbookNameNullException;
import botobo.core.exception.workbook.WorkbookTagLimitException;
import lombok.Builder;
import lombok.Getter;
@@ -33,7 +38,7 @@
public class Workbook extends BaseEntity {
private static final int MAX_NAME_LENGTH = 30;
- private static final int MAX_TAG_SIZE = 3;
+ private static final int MAX_TAG_SIZE = 5;
@Column(nullable = false, length = MAX_NAME_LENGTH)
private String name;
@@ -54,6 +59,9 @@ public class Workbook extends BaseEntity {
@OneToMany(mappedBy = "workbook", cascade = CascadeType.PERSIST, orphanRemoval = true)
private final List workbookTags = new ArrayList<>();
+ @Embedded
+ private final Hearts hearts = new Hearts();
+
@Builder
public Workbook(Long id, String name, boolean opened, boolean deleted, Cards cards, User user, Tags tags) {
validateName(name);
@@ -72,15 +80,13 @@ public Workbook(Long id, String name, boolean opened, boolean deleted, Cards car
private void validateName(String name) {
if (Objects.isNull(name)) {
- throw new IllegalArgumentException("Workbook의 Name에는 null이 들어갈 수 없습니다.");
+ throw new WorkbookNameNullException();
}
if (name.isBlank()) {
- throw new IllegalArgumentException("Workbook의 Name에는 비어있거나 공백 문자열이 들어갈 수 없습니다.");
+ throw new WorkbookNameNullException();
}
if (name.length() > MAX_NAME_LENGTH) {
- throw new IllegalArgumentException(
- String.format("Workbook의 Name %d자 이하여야 합니다.", MAX_NAME_LENGTH)
- );
+ throw new WorkbookNameLengthException();
}
}
@@ -97,7 +103,7 @@ private void validateTagSize(Tags tags) {
final int notHasTagsCount = tags.size() - alreadyHasTagsSize;
if (currentTags.size() + notHasTagsCount > MAX_TAG_SIZE) {
- throw new WorkbookTagLimitException(MAX_TAG_SIZE);
+ throw new WorkbookTagLimitException();
}
}
@@ -185,4 +191,19 @@ public void addCard(Card card) {
card.addWorkbook(this);
cards.addCard(card);
}
+
+ public boolean toggleHeart(Heart heart) {
+ return hearts.toggleHeart(heart);
+ }
+
+ public boolean existsHeartByAppUser(AppUser appUser) {
+ if (appUser.isAnonymous()) {
+ return false;
+ }
+ return hearts.contains(appUser.getId());
+ }
+
+ public int heartCount() {
+ return hearts.size();
+ }
}
diff --git a/backend/src/main/java/botobo/core/domain/workbook/WorkbookFinder.java b/backend/src/main/java/botobo/core/domain/workbook/WorkbookFinder.java
deleted file mode 100644
index 6e0ced8e..00000000
--- a/backend/src/main/java/botobo/core/domain/workbook/WorkbookFinder.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package botobo.core.domain.workbook;
-
-import botobo.core.domain.workbook.criteria.WorkbookCriteria;
-import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-import java.util.List;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
-
-@Getter
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor(access = AccessLevel.PRIVATE)
-public class WorkbookFinder {
-
- private List workbooks;
-
- public List apply(WorkbookCriteria workbookCriteria) {
- return workbooks.stream()
- .filter(filterAccessType(workbookCriteria))
- .filter(filterSearchKeyword(workbookCriteria))
- .collect(Collectors.toList());
- }
-
- private Predicate filterAccessType(WorkbookCriteria workbookCriteria) {
- return workbook -> (workbookCriteria.isAllAccess() ||
- (workbook.isOpened() && workbookCriteria.isPublicAccess()) ||
- (workbook.isPrivate() && workbookCriteria.isPrivateAccess())
- );
- }
-
- private Predicate filterSearchKeyword(WorkbookCriteria workbookCriteria) {
- final String keyword = workbookCriteria.getSearchKeywordValue();
- return workbook -> !keyword.isEmpty() && workbook.containsWord(keyword);
- }
-}
diff --git a/backend/src/main/java/botobo/core/domain/workbook/WorkbookRepository.java b/backend/src/main/java/botobo/core/domain/workbook/WorkbookRepository.java
index 45d93358..6345d4c2 100644
--- a/backend/src/main/java/botobo/core/domain/workbook/WorkbookRepository.java
+++ b/backend/src/main/java/botobo/core/domain/workbook/WorkbookRepository.java
@@ -1,20 +1,27 @@
package botobo.core.domain.workbook;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
-public interface WorkbookRepository extends JpaRepository {
- boolean existsByIdAndOpenedTrue(Long id);
-
- boolean existsById(Long id);
+public interface WorkbookRepository extends JpaRepository, JpaSpecificationExecutor {
@Query("select w from Workbook w where w.user.id = :userId order by w.createdAt desc")
List findAllByUserId(@Param("userId") Long userId);
@Query("select w from Workbook w left join fetch w.cards.cards c where w.id = :id order by c.createdAt desc")
Optional findByIdAndOrderCardByNew(@Param("id") Long id);
+
+ Page findAll(Specification spec, Pageable pageable);
+
+ boolean existsByIdAndOpenedTrue(Long id);
+
+ boolean existsById(Long id);
}
diff --git a/backend/src/main/java/botobo/core/domain/workbook/criteria/AccessType.java b/backend/src/main/java/botobo/core/domain/workbook/criteria/AccessType.java
deleted file mode 100644
index 4e316a1e..00000000
--- a/backend/src/main/java/botobo/core/domain/workbook/criteria/AccessType.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package botobo.core.domain.workbook.criteria;
-
-import com.fasterxml.jackson.annotation.JsonValue;
-
-import java.util.Arrays;
-import java.util.Objects;
-
-public enum AccessType {
- ALL("all"), PUBLIC("public"), PRIVATE("private");
-
- private final String value;
-
- AccessType(String value) {
- this.value = value;
- }
-
- public static AccessType from(String value) {
- if (Objects.isNull(value)) {
- return PUBLIC;
- }
- return Arrays.stream(values())
- .filter(accessType -> value.equalsIgnoreCase(accessType.value))
- .findAny()
- .orElse(PUBLIC);
- }
-
- public boolean isAll() {
- return this.equals(ALL);
- }
-
- public boolean isPublic() {
- return this.equals(PUBLIC);
- }
-
- public boolean isPrivate() {
- return this.equals(PRIVATE);
- }
-
- @JsonValue
- public String getValue() {
- return value;
- }
-}
diff --git a/backend/src/main/java/botobo/core/domain/workbook/criteria/WorkbookCriteria.java b/backend/src/main/java/botobo/core/domain/workbook/criteria/WorkbookCriteria.java
deleted file mode 100644
index 2702df77..00000000
--- a/backend/src/main/java/botobo/core/domain/workbook/criteria/WorkbookCriteria.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package botobo.core.domain.workbook.criteria;
-
-import lombok.Builder;
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-import java.util.Objects;
-
-@EqualsAndHashCode
-@Getter
-@NoArgsConstructor
-public class WorkbookCriteria {
-
- private SearchKeyword searchKeyword = SearchKeyword.from("");
- private AccessType accessType = AccessType.PUBLIC;
-
- @Builder
- private WorkbookCriteria(SearchKeyword searchKeyword, AccessType accessType) {
- if (Objects.nonNull(searchKeyword)) {
- this.searchKeyword = searchKeyword;
- }
- if (Objects.nonNull(accessType)) {
- this.accessType = accessType;
- }
- }
-
- public boolean isPublicAccess() {
- return accessType.isPublic();
- }
-
- public boolean isPrivateAccess() {
- return accessType.isPrivate();
- }
-
- public boolean isAllAccess() {
- return accessType.isAll();
- }
-
- public String getSearchKeywordValue() {
- return searchKeyword.getValue();
- }
-}
diff --git a/backend/src/main/java/botobo/core/dto/admin/AdminCardRequest.java b/backend/src/main/java/botobo/core/dto/admin/AdminCardRequest.java
index aad30d09..35c58a8e 100644
--- a/backend/src/main/java/botobo/core/dto/admin/AdminCardRequest.java
+++ b/backend/src/main/java/botobo/core/dto/admin/AdminCardRequest.java
@@ -10,13 +10,13 @@
@NoArgsConstructor
public class AdminCardRequest {
- @NotBlank(message = "질문은 필수 입력값입니다.")
+ @NotBlank(message = "C002")
private String question;
- @NotBlank(message = "답변은 필수 입력값입니다.")
+ @NotBlank(message = "C004")
private String answer;
- @NotNull(message = "카드가 포함될 문제집 아이디는 필수 입력값입니다.")
+ @NotNull(message = "C006")
private Long workbookId;
diff --git a/backend/src/main/java/botobo/core/dto/admin/AdminWorkbookRequest.java b/backend/src/main/java/botobo/core/dto/admin/AdminWorkbookRequest.java
index 9ac0b861..45305fa3 100644
--- a/backend/src/main/java/botobo/core/dto/admin/AdminWorkbookRequest.java
+++ b/backend/src/main/java/botobo/core/dto/admin/AdminWorkbookRequest.java
@@ -12,8 +12,8 @@
@NoArgsConstructor
public class AdminWorkbookRequest {
- @NotBlank(message = "문제집명은 필수 입력값입니다.")
- @Length(max = 30, message = "문제집명은 최소 1글자, 최대 30글자만 가능합니다.")
+ @NotBlank(message = "W002")
+ @Length(max = 30, message = "W001")
private String name;
private boolean opened = true;
diff --git a/backend/src/main/java/botobo/core/dto/auth/GithubUserInfoResponse.java b/backend/src/main/java/botobo/core/dto/auth/GithubUserInfoResponse.java
index cf361dbf..f06dbffc 100644
--- a/backend/src/main/java/botobo/core/dto/auth/GithubUserInfoResponse.java
+++ b/backend/src/main/java/botobo/core/dto/auth/GithubUserInfoResponse.java
@@ -1,6 +1,7 @@
package botobo.core.dto.auth;
import botobo.core.domain.user.Role;
+import botobo.core.domain.user.SocialType;
import botobo.core.domain.user.User;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
@@ -12,22 +13,22 @@
@NoArgsConstructor
@AllArgsConstructor
@Builder
-public class GithubUserInfoResponse {
+public class GithubUserInfoResponse implements UserInfoResponse {
@JsonProperty("login")
private String userName;
@JsonProperty("id")
- private Long githubId;
+ private String socialId;
@JsonProperty("avatar_url")
private String profileUrl;
public User toUser() {
return User.builder()
- .githubId(githubId)
+ .socialId(socialId)
.userName(userName)
.profileUrl(profileUrl)
.role(Role.USER)
+ .socialType(SocialType.GITHUB)
.build();
}
-
}
diff --git a/backend/src/main/java/botobo/core/dto/auth/GoogleTokenRequest.java b/backend/src/main/java/botobo/core/dto/auth/GoogleTokenRequest.java
new file mode 100644
index 00000000..f336efde
--- /dev/null
+++ b/backend/src/main/java/botobo/core/dto/auth/GoogleTokenRequest.java
@@ -0,0 +1,18 @@
+package botobo.core.dto.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class GoogleTokenRequest {
+ private String code;
+ private String client_id;
+ private String client_secret;
+ private String redirect_uri;
+ private String grant_type;
+}
diff --git a/backend/src/main/java/botobo/core/dto/auth/GoogleUserInfoResponse.java b/backend/src/main/java/botobo/core/dto/auth/GoogleUserInfoResponse.java
new file mode 100644
index 00000000..93eec23f
--- /dev/null
+++ b/backend/src/main/java/botobo/core/dto/auth/GoogleUserInfoResponse.java
@@ -0,0 +1,35 @@
+package botobo.core.dto.auth;
+
+import botobo.core.domain.user.Role;
+import botobo.core.domain.user.SocialType;
+import botobo.core.domain.user.User;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class GoogleUserInfoResponse implements UserInfoResponse {
+
+ @JsonProperty("name")
+ private String userName;
+ @JsonProperty("sub")
+ private String socialId;
+ @JsonProperty("picture")
+ private String profileUrl;
+
+ @Override
+ public User toUser() {
+ return User.builder()
+ .socialId(socialId)
+ .userName(userName)
+ .profileUrl(profileUrl)
+ .role(Role.USER)
+ .socialType(SocialType.GOOGLE)
+ .build();
+ }
+}
diff --git a/backend/src/main/java/botobo/core/dto/auth/OauthTokenRequest.java b/backend/src/main/java/botobo/core/dto/auth/OauthTokenRequest.java
new file mode 100644
index 00000000..50faf763
--- /dev/null
+++ b/backend/src/main/java/botobo/core/dto/auth/OauthTokenRequest.java
@@ -0,0 +1,18 @@
+package botobo.core.dto.auth;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class OauthTokenRequest {
+
+ private String code;
+ private String client_id;
+ private String client_secret;
+}
diff --git a/backend/src/main/java/botobo/core/dto/auth/OauthTokenResponse.java b/backend/src/main/java/botobo/core/dto/auth/OauthTokenResponse.java
new file mode 100644
index 00000000..6a179b13
--- /dev/null
+++ b/backend/src/main/java/botobo/core/dto/auth/OauthTokenResponse.java
@@ -0,0 +1,20 @@
+package botobo.core.dto.auth;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class OauthTokenResponse {
+
+ @JsonProperty("access_token")
+ private String accessToken;
+ @JsonProperty("token_type")
+ private String tokenType;
+ private String scope;
+}
diff --git a/backend/src/main/java/botobo/core/dto/auth/UserInfoResponse.java b/backend/src/main/java/botobo/core/dto/auth/UserInfoResponse.java
new file mode 100644
index 00000000..3e973775
--- /dev/null
+++ b/backend/src/main/java/botobo/core/dto/auth/UserInfoResponse.java
@@ -0,0 +1,7 @@
+package botobo.core.dto.auth;
+
+import botobo.core.domain.user.User;
+
+public interface UserInfoResponse {
+ User toUser();
+}
diff --git a/backend/src/main/java/botobo/core/dto/card/CardRequest.java b/backend/src/main/java/botobo/core/dto/card/CardRequest.java
index 3cc74c2c..50dc394e 100644
--- a/backend/src/main/java/botobo/core/dto/card/CardRequest.java
+++ b/backend/src/main/java/botobo/core/dto/card/CardRequest.java
@@ -11,6 +11,7 @@
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Positive;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@@ -18,15 +19,16 @@
@Builder
public class CardRequest {
- @NotBlank(message = "질문은 필수 입력값입니다.")
- @Length(max = 2000, message = "질문은 최대 2000자까지 입력 가능합니다.")
+ @NotBlank(message = "C002")
+ @Length(max = 2000, message = "C003")
private String question;
- @NotBlank(message = "답변은 필수 입력값입니다.")
- @Length(max = 2000, message = "답변은 최대 2000자까지 입력 가능합니다.")
+ @NotBlank(message = "C004")
+ @Length(max = 2000, message = "C005")
private String answer;
- @NotNull(message = "카드가 포함될 문제집 아이디는 필수 입력값입니다.")
+ @NotNull(message = "C006")
+ @Positive(message = "C007")
private Long workbookId;
public Card toCardWithWorkbook(Workbook workbook) {
diff --git a/backend/src/main/java/botobo/core/dto/card/CardUpdateRequest.java b/backend/src/main/java/botobo/core/dto/card/CardUpdateRequest.java
index b51d6d74..2a5c86a0 100644
--- a/backend/src/main/java/botobo/core/dto/card/CardUpdateRequest.java
+++ b/backend/src/main/java/botobo/core/dto/card/CardUpdateRequest.java
@@ -11,6 +11,7 @@
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
+import javax.validation.constraints.PositiveOrZero;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@@ -18,25 +19,26 @@
@Builder
public class CardUpdateRequest {
- @NotBlank(message = "카드를 업데이트하기 위해서는 질문이 필요합니다.")
- @Length(max = 2000, message = "질문은 최대 2000자까지 입력 가능합니다.")
+ @NotBlank(message = "C002")
+ @Length(max = 2000, message = "C003")
private String question;
- @NotBlank(message = "카드를 업데이트하기 위해서는 답변이 필요합니다.")
- @Length(max = 2000, message = "답변은 최대 2000자까지 입력 가능합니다.")
+ @NotBlank(message = "C004")
+ @Length(max = 2000, message = "C005")
private String answer;
- @NotNull
- @Positive
+ @NotNull(message = "C006")
+ @Positive(message = "C007")
private Long workbookId;
- @NotNull
+ @NotNull(message = "C008")
+ @PositiveOrZero(message = "C009")
private Integer encounterCount;
- @NotNull(message = "카드를 업데이트하기 위해서는 북마크 정보가 필요합니다.")
+ @NotNull(message = "C010")
private Boolean bookmark;
- @NotNull(message = "카드를 업데이트하기 위해서는 또 보기 정보가 필요합니다.")
+ @NotNull(message = "C011")
private Boolean nextQuiz;
public Card toCard() {
diff --git a/backend/src/main/java/botobo/core/dto/card/NextQuizCardsRequest.java b/backend/src/main/java/botobo/core/dto/card/NextQuizCardsRequest.java
index 4cbbccfe..370e3e11 100644
--- a/backend/src/main/java/botobo/core/dto/card/NextQuizCardsRequest.java
+++ b/backend/src/main/java/botobo/core/dto/card/NextQuizCardsRequest.java
@@ -14,6 +14,6 @@
@AllArgsConstructor
public class NextQuizCardsRequest {
- @NotNull(message = "유효하지 않은 또 보기 카드 등록 요청입니다.")
+ @NotNull(message = "C012")
private List cardIds;
}
diff --git a/backend/src/main/java/botobo/core/dto/card/QuizRequest.java b/backend/src/main/java/botobo/core/dto/card/QuizRequest.java
index 1a7687a8..98718822 100644
--- a/backend/src/main/java/botobo/core/dto/card/QuizRequest.java
+++ b/backend/src/main/java/botobo/core/dto/card/QuizRequest.java
@@ -1,7 +1,9 @@
package botobo.core.dto.card;
+import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
+import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.NotEmpty;
import java.util.List;
@@ -10,10 +12,15 @@
@NoArgsConstructor
public class QuizRequest {
- @NotEmpty(message = "퀴즈를 진행하려면 문제집 아이디가 필요합니다.")
+ @NotEmpty(message = "Q001")
private List workbookIds;
- public QuizRequest(List workbookIds) {
+ @Range(min = 10, max = 30, message = "Q002")
+ private int count;
+
+ @Builder
+ public QuizRequest(List workbookIds, int count) {
this.workbookIds = workbookIds;
+ this.count = count;
}
}
diff --git a/backend/src/main/java/botobo/core/dto/card/ScrapCardRequest.java b/backend/src/main/java/botobo/core/dto/card/ScrapCardRequest.java
index eee5c9a3..277c3177 100644
--- a/backend/src/main/java/botobo/core/dto/card/ScrapCardRequest.java
+++ b/backend/src/main/java/botobo/core/dto/card/ScrapCardRequest.java
@@ -15,7 +15,7 @@
@AllArgsConstructor
public class ScrapCardRequest {
- @NotEmpty(message = "카드를 내 문제집으로 옮기려면 카드 아이디가 필요합니다.")
+ @NotEmpty(message = "W016")
private List cardIds;
public List distinctCardIds() {
diff --git a/backend/src/main/java/botobo/core/dto/heart/HeartResponse.java b/backend/src/main/java/botobo/core/dto/heart/HeartResponse.java
new file mode 100644
index 00000000..e792b44d
--- /dev/null
+++ b/backend/src/main/java/botobo/core/dto/heart/HeartResponse.java
@@ -0,0 +1,16 @@
+package botobo.core.dto.heart;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class HeartResponse {
+ private boolean heart;
+
+ public static HeartResponse of(boolean heart) {
+ return new HeartResponse(heart);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/dto/tag/TagRequest.java b/backend/src/main/java/botobo/core/dto/tag/TagRequest.java
index 6a77d615..ff131fbb 100644
--- a/backend/src/main/java/botobo/core/dto/tag/TagRequest.java
+++ b/backend/src/main/java/botobo/core/dto/tag/TagRequest.java
@@ -18,11 +18,11 @@
@Builder
public class TagRequest {
- @NotNull(message = "태그 아이디는 필수 입력값입니다.")
- @PositiveOrZero(message = "태그 아이디는 0이상의 숫자입니다.")
+ @NotNull(message = "W003")
+ @PositiveOrZero(message = "W004")
private Long id;
- @NotBlank(message = "이름은 필수 입력값입니다.")
- @Length(max = 20, message = "태그는 최대 20자까지 입력 가능합니다.")
+ @NotBlank(message = "W005")
+ @Length(max = 20, message = "W006")
private String name;
}
diff --git a/backend/src/main/java/botobo/core/dto/user/ProfileResponse.java b/backend/src/main/java/botobo/core/dto/user/ProfileResponse.java
new file mode 100644
index 00000000..a90c2123
--- /dev/null
+++ b/backend/src/main/java/botobo/core/dto/user/ProfileResponse.java
@@ -0,0 +1,14 @@
+package botobo.core.dto.user;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class ProfileResponse {
+ private String profileUrl;
+}
diff --git a/backend/src/main/java/botobo/core/dto/user/SimpleUserResponse.java b/backend/src/main/java/botobo/core/dto/user/SimpleUserResponse.java
new file mode 100644
index 00000000..4c20d8e2
--- /dev/null
+++ b/backend/src/main/java/botobo/core/dto/user/SimpleUserResponse.java
@@ -0,0 +1,33 @@
+package botobo.core.dto.user;
+
+import botobo.core.domain.user.User;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class SimpleUserResponse {
+
+ private Long id;
+ private String name;
+
+ public static List listOf(List users) {
+ return users.stream()
+ .map(SimpleUserResponse::of)
+ .collect(Collectors.toList());
+ }
+
+ public static SimpleUserResponse of(User user) {
+ return SimpleUserResponse.builder()
+ .id(user.getId())
+ .name(user.getUserName())
+ .build();
+ }
+}
diff --git a/backend/src/main/java/botobo/core/dto/user/UserNameRequest.java b/backend/src/main/java/botobo/core/dto/user/UserNameRequest.java
new file mode 100644
index 00000000..0abd66f5
--- /dev/null
+++ b/backend/src/main/java/botobo/core/dto/user/UserNameRequest.java
@@ -0,0 +1,17 @@
+package botobo.core.dto.user;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@AllArgsConstructor(access = AccessLevel.PUBLIC)
+@Builder
+public class UserNameRequest {
+
+ @ValidUserName
+ private String userName;
+}
diff --git a/backend/src/main/java/botobo/core/dto/user/UserNameValidator.java b/backend/src/main/java/botobo/core/dto/user/UserNameValidator.java
new file mode 100644
index 00000000..bd00b6e2
--- /dev/null
+++ b/backend/src/main/java/botobo/core/dto/user/UserNameValidator.java
@@ -0,0 +1,51 @@
+package botobo.core.dto.user;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.util.Objects;
+
+public class UserNameValidator implements ConstraintValidator {
+ private static final int MIN_LENGTH = 1;
+ private static final int MAX_LENGTH = 20;
+
+ @Override
+ public boolean isValid(String userName, ConstraintValidatorContext context) {
+ if (isNull(userName)) {
+ addConstraintViolation(context, "U004");
+ return false;
+ }
+ if (!hasProperLength(userName)) {
+ addConstraintViolation(context, String.format("U005", MIN_LENGTH, MAX_LENGTH));
+ return false;
+ }
+ if (hasWhiteSpace(userName)) {
+ addConstraintViolation(context, "U006");
+ return false;
+ }
+ return true;
+ }
+
+ private boolean hasWhiteSpace(String userName) {
+ return !userName.matches("^\\S*$");
+ }
+
+ private boolean hasProperLength(String userName) {
+ int length = userName.length();
+ return length >= MIN_LENGTH && length <= MAX_LENGTH;
+ }
+
+ private boolean isNull(String userName) {
+ return Objects.isNull(userName);
+ }
+
+ private void addConstraintViolation(ConstraintValidatorContext context, String errorMessage) {
+ context.disableDefaultConstraintViolation();
+ context.buildConstraintViolationWithTemplate(errorMessage)
+ .addConstraintViolation();
+ }
+
+ @Override
+ public void initialize(ValidUserName validUserName) {
+ ConstraintValidator.super.initialize(validUserName);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/dto/user/UserResponse.java b/backend/src/main/java/botobo/core/dto/user/UserResponse.java
index 46de4e27..593c0a5c 100644
--- a/backend/src/main/java/botobo/core/dto/user/UserResponse.java
+++ b/backend/src/main/java/botobo/core/dto/user/UserResponse.java
@@ -1,5 +1,6 @@
package botobo.core.dto.user;
+import botobo.core.domain.user.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@@ -10,8 +11,17 @@
@AllArgsConstructor
@Builder
public class UserResponse {
-
private Long id;
private String userName;
+ private String bio;
private String profileUrl;
+
+ public static UserResponse of(User user) {
+ return UserResponse.builder()
+ .id(user.getId())
+ .userName(user.getUserName())
+ .bio(user.getBio())
+ .profileUrl(user.getProfileUrl())
+ .build();
+ }
}
diff --git a/backend/src/main/java/botobo/core/dto/user/UserUpdateRequest.java b/backend/src/main/java/botobo/core/dto/user/UserUpdateRequest.java
new file mode 100644
index 00000000..32b09be7
--- /dev/null
+++ b/backend/src/main/java/botobo/core/dto/user/UserUpdateRequest.java
@@ -0,0 +1,37 @@
+package botobo.core.dto.user;
+
+import botobo.core.domain.user.User;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@AllArgsConstructor(access = AccessLevel.PUBLIC)
+@Builder
+public class UserUpdateRequest {
+
+ @ValidUserName
+ private String userName;
+
+ @NotBlank(message = "U007")
+ private String profileUrl;
+
+ @NotNull(message = "U008")
+ @Length(max = 255, message = "U009")
+ private String bio;
+
+ public User toUser() {
+ return User.builder()
+ .userName(userName)
+ .profileUrl(profileUrl)
+ .bio(bio)
+ .build();
+ }
+}
diff --git a/backend/src/main/java/botobo/core/dto/user/ValidUserName.java b/backend/src/main/java/botobo/core/dto/user/ValidUserName.java
new file mode 100644
index 00000000..056c1046
--- /dev/null
+++ b/backend/src/main/java/botobo/core/dto/user/ValidUserName.java
@@ -0,0 +1,19 @@
+package botobo.core.dto.user;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Constraint(validatedBy = UserNameValidator.class)
+@Target({ElementType.METHOD, ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ValidUserName {
+ String message() default "회원명이 규칙에 알맞지 않습니다.";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+}
diff --git a/backend/src/main/java/botobo/core/dto/workbook/WorkbookCardResponse.java b/backend/src/main/java/botobo/core/dto/workbook/WorkbookCardResponse.java
index d8e70e1b..763e0b92 100644
--- a/backend/src/main/java/botobo/core/dto/workbook/WorkbookCardResponse.java
+++ b/backend/src/main/java/botobo/core/dto/workbook/WorkbookCardResponse.java
@@ -23,6 +23,10 @@ public class WorkbookCardResponse {
@JsonInclude(JsonInclude.Include.NON_NULL)
private Integer cardCount;
@JsonInclude(JsonInclude.Include.NON_NULL)
+ private Integer heartCount;
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private Boolean heart;
+ @JsonInclude(JsonInclude.Include.NON_NULL)
private List tags;
private List cards;
@@ -35,13 +39,15 @@ public static WorkbookCardResponse ofUserWorkbook(Workbook workbook) {
.build();
}
- public static WorkbookCardResponse ofOpenedWorkbook(Workbook workbook) {
+ public static WorkbookCardResponse ofOpenedWorkbook(Workbook workbook, boolean heart) {
List cardResponses = CardResponse.listOfSimple(workbook.getCards());
List tagResponses = TagResponse.listOf(workbook.tags());
return WorkbookCardResponse.builder()
.workbookId(workbook.getId())
.workbookName(workbook.getName())
.cardCount(workbook.cardCount())
+ .heartCount(workbook.heartCount())
+ .heart(heart)
.tags(tagResponses)
.cards(cardResponses)
.build();
diff --git a/backend/src/main/java/botobo/core/dto/workbook/WorkbookRequest.java b/backend/src/main/java/botobo/core/dto/workbook/WorkbookRequest.java
index 21580a6c..89b66f7c 100644
--- a/backend/src/main/java/botobo/core/dto/workbook/WorkbookRequest.java
+++ b/backend/src/main/java/botobo/core/dto/workbook/WorkbookRequest.java
@@ -20,12 +20,13 @@
@Builder
public class WorkbookRequest {
- @NotBlank(message = "이름은 필수 입력값입니다.")
- @Length(max = 30, message = "이름은 최대 30자까지 입력 가능합니다.")
+ @NotBlank(message = "W002")
+ @Length(max = 30, message = "W001")
private String name;
private boolean opened;
+ @Builder.Default
@Valid
private List tags = new ArrayList<>();
diff --git a/backend/src/main/java/botobo/core/dto/workbook/WorkbookResponse.java b/backend/src/main/java/botobo/core/dto/workbook/WorkbookResponse.java
index b21fb76b..30ff0a9f 100644
--- a/backend/src/main/java/botobo/core/dto/workbook/WorkbookResponse.java
+++ b/backend/src/main/java/botobo/core/dto/workbook/WorkbookResponse.java
@@ -20,6 +20,7 @@ public class WorkbookResponse {
private Long id;
private String name;
private int cardCount;
+ private int heartCount;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String author;
@JsonInclude(JsonInclude.Include.NON_NULL)
@@ -31,6 +32,7 @@ public static WorkbookResponse of(Workbook workbook) {
.id(workbook.getId())
.name(workbook.getName())
.cardCount(workbook.cardCount())
+ .heartCount(workbook.heartCount())
.author(workbook.author())
.opened(workbook.isOpened())
.tags(TagResponse.listOf(workbook.tags()))
@@ -54,6 +56,7 @@ public static WorkbookResponse authorOf(Workbook workbook) {
.id(workbook.getId())
.name(workbook.getName())
.cardCount(workbook.cardCount())
+ .heartCount(workbook.heartCount())
.opened(workbook.isOpened())
.tags(TagResponse.listOf(workbook.tags()))
.build();
@@ -70,6 +73,7 @@ private static WorkbookResponse openedOf(Workbook workbook) {
.id(workbook.getId())
.name(workbook.getName())
.cardCount(workbook.cardCount())
+ .heartCount(workbook.heartCount())
.author(workbook.author())
.tags(TagResponse.listOf(workbook.tags()))
.build();
diff --git a/backend/src/main/java/botobo/core/dto/workbook/WorkbookUpdateRequest.java b/backend/src/main/java/botobo/core/dto/workbook/WorkbookUpdateRequest.java
index c0c479c4..51d01ec3 100644
--- a/backend/src/main/java/botobo/core/dto/workbook/WorkbookUpdateRequest.java
+++ b/backend/src/main/java/botobo/core/dto/workbook/WorkbookUpdateRequest.java
@@ -24,17 +24,22 @@
@Builder
public class WorkbookUpdateRequest {
- @NotBlank(message = "이름은 필수 입력값입니다.")
- @Length(max = 30, message = "이름은 최대 30자까지 입력 가능합니다.")
+ @NotBlank(message = "W002")
+ @Length(max = 30, message = "W001")
private String name;
- @NotNull(message = "문제집 공개여부는 필수 입력값입니다.")
+ @NotNull(message = "W009")
private Boolean opened;
- @PositiveOrZero(message = "카드 개수는 0이상 입니다.")
- private int cardCount;
+ @NotNull(message = "W010")
+ @PositiveOrZero(message = "W011")
+ private Integer cardCount;
- @NotNull(message = "문제집를 수정하려면 태그가 필요합니다.")
+ @NotNull(message = "W012")
+ @PositiveOrZero(message = "W013")
+ private Integer heartCount;
+
+ @NotNull(message = "W008")
@Valid
private List tags;
diff --git a/backend/src/main/java/botobo/core/exception/BadRequestException.java b/backend/src/main/java/botobo/core/exception/BadRequestException.java
deleted file mode 100644
index 6d6f56e0..00000000
--- a/backend/src/main/java/botobo/core/exception/BadRequestException.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package botobo.core.exception;
-
-public class BadRequestException extends RuntimeException {
- public BadRequestException() {
- }
-
- public BadRequestException(String message) {
- super(message);
- }
-}
diff --git a/backend/src/main/java/botobo/core/exception/BotoboException.java b/backend/src/main/java/botobo/core/exception/BotoboException.java
new file mode 100644
index 00000000..ee8787e9
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/BotoboException.java
@@ -0,0 +1,13 @@
+package botobo.core.exception;
+
+import botobo.core.exception.common.ErrorType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@AllArgsConstructor
+public class BotoboException extends RuntimeException {
+ private final HttpStatus httpStatus;
+ private final ErrorType errorType = ErrorType.of(this.getClass());
+}
diff --git a/backend/src/main/java/botobo/core/exception/ErrorResponse.java b/backend/src/main/java/botobo/core/exception/ErrorResponse.java
deleted file mode 100644
index eeddfe5d..00000000
--- a/backend/src/main/java/botobo/core/exception/ErrorResponse.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package botobo.core.exception;
-
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-@Getter
-@NoArgsConstructor
-public class ErrorResponse {
- private String message;
-
- private ErrorResponse(String message) {
- this.message = message;
- }
-
- public static ErrorResponse of(String message) {
- return new ErrorResponse(message);
- }
-}
\ No newline at end of file
diff --git a/backend/src/main/java/botobo/core/exception/ExternalException.java b/backend/src/main/java/botobo/core/exception/ExternalException.java
new file mode 100644
index 00000000..19f942e3
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/ExternalException.java
@@ -0,0 +1,9 @@
+package botobo.core.exception;
+
+import org.springframework.http.HttpStatus;
+
+public class ExternalException extends BotoboException {
+ public ExternalException() {
+ super(HttpStatus.BAD_REQUEST);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/exception/ForbiddenException.java b/backend/src/main/java/botobo/core/exception/ForbiddenException.java
deleted file mode 100644
index 29173ce3..00000000
--- a/backend/src/main/java/botobo/core/exception/ForbiddenException.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package botobo.core.exception;
-
-public class ForbiddenException extends RuntimeException {
-
- public ForbiddenException() {
- super();
- }
-
- public ForbiddenException(String message) {
- super(message);
- }
-}
diff --git a/backend/src/main/java/botobo/core/exception/GlobalControllerAdvice.java b/backend/src/main/java/botobo/core/exception/GlobalControllerAdvice.java
index 65571eb5..3456aea8 100644
--- a/backend/src/main/java/botobo/core/exception/GlobalControllerAdvice.java
+++ b/backend/src/main/java/botobo/core/exception/GlobalControllerAdvice.java
@@ -1,65 +1,69 @@
package botobo.core.exception;
+import botobo.core.exception.common.ErrorResponse;
+import botobo.core.exception.common.ErrorType;
+import botobo.core.exception.http.InternalServerErrorException;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.context.support.DefaultMessageSourceResolvable;
+import org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
+import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.multipart.MaxUploadSizeExceededException;
-import java.util.stream.Collectors;
+import java.util.Objects;
@RestControllerAdvice
@Slf4j
public class GlobalControllerAdvice {
- @ExceptionHandler(NotFoundException.class)
- public ResponseEntity handleNotFoundException(NotFoundException e) {
- log.info("NotFoundException", e);
- return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.of(e.getMessage()));
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity handleBindingException(MethodArgumentNotValidException e) {
+ FieldError fieldError = e.getFieldError();
+ Objects.requireNonNull(fieldError);
+
+ log.info(String.format("MethodArgumentNotValidException %s", e.getMessage()), e);
+ ErrorType errorType = ErrorType.of(fieldError.getDefaultMessage());
+ return ResponseEntity.badRequest().body(ErrorResponse.of(errorType));
}
- @ExceptionHandler(UnauthorizedException.class)
- public ResponseEntity handleUnauthorizedException(UnauthorizedException e) {
- log.info("UnauthorizedException", e);
- return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ErrorResponse.of(e.getMessage()));
+ @ExceptionHandler(InternalServerErrorException.class)
+ public ResponseEntity handleInternalServerErrorException(InternalServerErrorException e) {
+ log.info(String.format("InternalServerErrorException %s", e.getServerMessage()), e);
+ return ResponseEntity.status(e.getHttpStatus()).body(ErrorResponse.of(e.getErrorType()));
}
- @ExceptionHandler(MethodArgumentNotValidException.class)
- public ResponseEntity handleBindingException(MethodArgumentNotValidException e) {
- String message = e.getFieldErrors()
- .stream()
- .map(DefaultMessageSourceResolvable::getDefaultMessage)
- .collect(Collectors.joining(System.lineSeparator()));
- log.info("MethodArgumentNotValidException", e);
- return ResponseEntity.badRequest().body(ErrorResponse.of(message));
+ @ExceptionHandler(BotoboException.class)
+ public ResponseEntity handleBotoboException(BotoboException e) {
+ log.info(String.format("%s %s", e.getClass().getName(), e.getErrorType().getMessage()), e);
+ return ResponseEntity.status(e.getHttpStatus()).body(ErrorResponse.of(e.getErrorType()));
}
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity handleMissingParams(MissingServletRequestParameterException e) {
+ log.info(String.format("MissingServletRequestParameterException %s", e.getMessage()), e);
String message = String.format("파라미터를 입력해야 합니다.(%s)", e.getParameterName());
- log.info("MethodArgumentNotValidException", e);
- return ResponseEntity.badRequest().body(ErrorResponse.of(message));
+ return ResponseEntity.badRequest().body(ErrorResponse.of("E001", message));
}
- @ExceptionHandler(BadRequestException.class)
- public ResponseEntity handleBadRequestException(BadRequestException e) {
- log.info("BadRequestException", e);
- return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.of(e.getMessage()));
+ @ExceptionHandler(MaxUploadSizeExceededException.class)
+ public ResponseEntity handleMaxUploadSizeExceedException(MaxUploadSizeExceededException e) {
+ log.info(String.format("MaxUploadSizeExceededException %s", e.getMessage()), e);
+ return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(ErrorResponse.of(ErrorType.U010));
}
- @ExceptionHandler(ForbiddenException.class)
- public ResponseEntity handleForbiddenException(ForbiddenException e) {
- log.info("ForbiddenException", e);
- return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ErrorResponse.of(e.getMessage()));
+ @ExceptionHandler(SizeLimitExceededException.class)
+ public ResponseEntity handleSizeLimitExceededException(SizeLimitExceededException e) {
+ log.info(String.format("SizeLimitExceededException %s", e.getMessage()), e);
+ return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(ErrorResponse.of(ErrorType.U011));
}
-
@ExceptionHandler(Exception.class)
public ResponseEntity handleException(Exception e) {
- log.info("Exception", e);
- return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorResponse.of("알 수 없는 예외가 발생했습니다."));
+ log.info(String.format("Exception %s", e.getMessage()), e);
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorResponse.of(ErrorType.X001));
}
}
diff --git a/backend/src/main/java/botobo/core/exception/NotAuthorException.java b/backend/src/main/java/botobo/core/exception/NotAuthorException.java
deleted file mode 100644
index e2db3b23..00000000
--- a/backend/src/main/java/botobo/core/exception/NotAuthorException.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package botobo.core.exception;
-
-public class NotAuthorException extends ForbiddenException {
- public NotAuthorException() {
- super("작성자가 아니므로 권한이 없습니다.");
- }
-}
diff --git a/backend/src/main/java/botobo/core/exception/NotFoundException.java b/backend/src/main/java/botobo/core/exception/NotFoundException.java
deleted file mode 100644
index 7f477224..00000000
--- a/backend/src/main/java/botobo/core/exception/NotFoundException.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package botobo.core.exception;
-
-public class NotFoundException extends RuntimeException {
- public NotFoundException() {
- super();
- }
-
- public NotFoundException(String message) {
- super(message);
- }
-}
diff --git a/backend/src/main/java/botobo/core/exception/UnauthorizedException.java b/backend/src/main/java/botobo/core/exception/UnauthorizedException.java
deleted file mode 100644
index 64e9c902..00000000
--- a/backend/src/main/java/botobo/core/exception/UnauthorizedException.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package botobo.core.exception;
-
-public class UnauthorizedException extends RuntimeException {
-
- public UnauthorizedException() {
- }
-
- public UnauthorizedException(String message) {
- super(message);
- }
-}
diff --git a/backend/src/main/java/botobo/core/exception/auth/GithubApiFailedException.java b/backend/src/main/java/botobo/core/exception/auth/GithubApiFailedException.java
deleted file mode 100644
index 7a490a10..00000000
--- a/backend/src/main/java/botobo/core/exception/auth/GithubApiFailedException.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package botobo.core.exception.auth;
-
-import botobo.core.exception.UnauthorizedException;
-
-public class GithubApiFailedException extends UnauthorizedException {
-
- public GithubApiFailedException() {
- super("GithubAccessToken을 받아오는데 실패했습니다.");
- }
-}
diff --git a/backend/src/main/java/botobo/core/exception/auth/NotAdminException.java b/backend/src/main/java/botobo/core/exception/auth/NotAdminException.java
index a3c839ba..31f51fc6 100644
--- a/backend/src/main/java/botobo/core/exception/auth/NotAdminException.java
+++ b/backend/src/main/java/botobo/core/exception/auth/NotAdminException.java
@@ -1,9 +1,6 @@
package botobo.core.exception.auth;
-import botobo.core.exception.ForbiddenException;
+import botobo.core.exception.http.ForbiddenException;
public class NotAdminException extends ForbiddenException {
- public NotAdminException() {
- super("Admin 권한이 아니기에 접근할 수 없습니다.");
- }
}
diff --git a/backend/src/main/java/botobo/core/exception/auth/OauthApiFailedException.java b/backend/src/main/java/botobo/core/exception/auth/OauthApiFailedException.java
new file mode 100644
index 00000000..3c87aa90
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/auth/OauthApiFailedException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.auth;
+
+import botobo.core.exception.http.UnAuthorizedException;
+
+public class OauthApiFailedException extends UnAuthorizedException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/auth/TokenExpirationException.java b/backend/src/main/java/botobo/core/exception/auth/TokenExpirationException.java
new file mode 100644
index 00000000..004c0960
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/auth/TokenExpirationException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.auth;
+
+import botobo.core.exception.http.UnAuthorizedException;
+
+public class TokenExpirationException extends UnAuthorizedException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/auth/TokenNotValidException.java b/backend/src/main/java/botobo/core/exception/auth/TokenNotValidException.java
index 16ffcc89..c7e91954 100644
--- a/backend/src/main/java/botobo/core/exception/auth/TokenNotValidException.java
+++ b/backend/src/main/java/botobo/core/exception/auth/TokenNotValidException.java
@@ -1,9 +1,6 @@
package botobo.core.exception.auth;
-import botobo.core.exception.UnauthorizedException;
+import botobo.core.exception.http.UnAuthorizedException;
-public class TokenNotValidException extends UnauthorizedException {
- public TokenNotValidException() {
- super("토큰이 유효하지 않습니다.");
- }
+public class TokenNotValidException extends UnAuthorizedException {
}
diff --git a/backend/src/main/java/botobo/core/exception/auth/UserProfileLoadFailedException.java b/backend/src/main/java/botobo/core/exception/auth/UserProfileLoadFailedException.java
index e34e73e9..1f917997 100644
--- a/backend/src/main/java/botobo/core/exception/auth/UserProfileLoadFailedException.java
+++ b/backend/src/main/java/botobo/core/exception/auth/UserProfileLoadFailedException.java
@@ -1,10 +1,6 @@
package botobo.core.exception.auth;
-import botobo.core.exception.UnauthorizedException;
+import botobo.core.exception.http.UnAuthorizedException;
-public class UserProfileLoadFailedException extends UnauthorizedException {
-
- public UserProfileLoadFailedException() {
- super("Github에서 유저정보를 불러오는데 실패했습니다.");
- }
+public class UserProfileLoadFailedException extends UnAuthorizedException {
}
diff --git a/backend/src/main/java/botobo/core/exception/card/CardAnswerNullException.java b/backend/src/main/java/botobo/core/exception/card/CardAnswerNullException.java
new file mode 100644
index 00000000..e39c7d7c
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/card/CardAnswerNullException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.card;
+
+import botobo.core.exception.http.BadRequestException;
+
+public class CardAnswerNullException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/card/CardNotFoundException.java b/backend/src/main/java/botobo/core/exception/card/CardNotFoundException.java
index 0929c504..a8ffb512 100644
--- a/backend/src/main/java/botobo/core/exception/card/CardNotFoundException.java
+++ b/backend/src/main/java/botobo/core/exception/card/CardNotFoundException.java
@@ -1,13 +1,6 @@
package botobo.core.exception.card;
-import botobo.core.exception.NotFoundException;
+import botobo.core.exception.http.NotFoundException;
public class CardNotFoundException extends NotFoundException {
- public CardNotFoundException() {
- super("해당 카드를 찾을 수 없습니다.");
- }
-
- public CardNotFoundException(String message) {
- super(message);
- }
}
diff --git a/backend/src/main/java/botobo/core/exception/card/CardQuestionNullException.java b/backend/src/main/java/botobo/core/exception/card/CardQuestionNullException.java
new file mode 100644
index 00000000..d663d500
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/card/CardQuestionNullException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.card;
+
+import botobo.core.exception.http.BadRequestException;
+
+public class CardQuestionNullException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/card/QuizEmptyException.java b/backend/src/main/java/botobo/core/exception/card/QuizEmptyException.java
index dd16b87b..3861f256 100644
--- a/backend/src/main/java/botobo/core/exception/card/QuizEmptyException.java
+++ b/backend/src/main/java/botobo/core/exception/card/QuizEmptyException.java
@@ -1,13 +1,6 @@
package botobo.core.exception.card;
-import botobo.core.exception.BadRequestException;
+import botobo.core.exception.http.BadRequestException;
public class QuizEmptyException extends BadRequestException {
- public QuizEmptyException() {
- super("퀴즈에 문제가 존재하지 않습니다.");
- }
-
- public QuizEmptyException(String message) {
- super(message);
- }
}
diff --git a/backend/src/main/java/botobo/core/exception/common/ErrorResponse.java b/backend/src/main/java/botobo/core/exception/common/ErrorResponse.java
new file mode 100644
index 00000000..2e928df9
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/common/ErrorResponse.java
@@ -0,0 +1,22 @@
+package botobo.core.exception.common;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public class ErrorResponse {
+ private String code;
+ private String message;
+
+ public static ErrorResponse of(String code, String message) {
+ return new ErrorResponse(code, message);
+ }
+
+ public static ErrorResponse of(ErrorType errorType) {
+ return new ErrorResponse(errorType.getCode(), errorType.getMessage());
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/botobo/core/exception/common/ErrorType.java b/backend/src/main/java/botobo/core/exception/common/ErrorType.java
new file mode 100644
index 00000000..fda8bb21
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/common/ErrorType.java
@@ -0,0 +1,151 @@
+package botobo.core.exception.common;
+
+import botobo.core.exception.BotoboException;
+import botobo.core.exception.ExternalException;
+import botobo.core.exception.auth.NotAdminException;
+import botobo.core.exception.auth.OauthApiFailedException;
+import botobo.core.exception.auth.TokenExpirationException;
+import botobo.core.exception.auth.TokenNotValidException;
+import botobo.core.exception.auth.UserProfileLoadFailedException;
+import botobo.core.exception.card.CardAnswerNullException;
+import botobo.core.exception.card.CardNotFoundException;
+import botobo.core.exception.card.CardQuestionNullException;
+import botobo.core.exception.card.QuizEmptyException;
+import botobo.core.exception.http.InternalServerErrorException;
+import botobo.core.exception.search.InvalidPageSizeException;
+import botobo.core.exception.search.InvalidPageStartException;
+import botobo.core.exception.search.InvalidSearchCriteriaException;
+import botobo.core.exception.search.InvalidSearchOrderException;
+import botobo.core.exception.search.InvalidSearchTypeException;
+import botobo.core.exception.search.LongSearchKeywordException;
+import botobo.core.exception.search.SearchKeywordNullException;
+import botobo.core.exception.search.ShortSearchKeywordException;
+import botobo.core.exception.tag.TagNameLengthException;
+import botobo.core.exception.tag.TagNameNullException;
+import botobo.core.exception.tag.TagNullException;
+import botobo.core.exception.user.NotAuthorException;
+import botobo.core.exception.user.ProfileUpdateNotAllowedException;
+import botobo.core.exception.user.SocialTypeNotFoundException;
+import botobo.core.exception.user.UserNameDuplicatedException;
+import botobo.core.exception.user.UserNotFoundException;
+import botobo.core.exception.user.s3.ImageExtensionNotAllowedException;
+import botobo.core.exception.workbook.NotOpenedWorkbookException;
+import botobo.core.exception.workbook.WorkbookNameLengthException;
+import botobo.core.exception.workbook.WorkbookNameNullException;
+import botobo.core.exception.workbook.WorkbookNotFoundException;
+import botobo.core.exception.workbook.WorkbookTagLimitException;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+@Getter
+@AllArgsConstructor
+public enum ErrorType {
+ A001("A001", "토큰이 유효하지 않습니다.", TokenNotValidException.class),
+ A002("A002", "만료된 토큰입니다.", TokenExpirationException.class),
+ A003("A003", "작성자가 아니므로 권한이 없습니다.", NotAuthorException.class),
+ A004("A004", "AccessToken을 받아오는데 실패했습니다.", OauthApiFailedException.class),
+ A005("A005", "유저정보를 불러오는데 실패했습니다.", UserProfileLoadFailedException.class),
+ A006("A006", "Admin 권한이 아니기에 접근할 수 없습니다.", NotAdminException.class),
+ A007("A007", "존재하지 않는 소셜 로그인 방식입니다.", SocialTypeNotFoundException.class),
+
+ U001("U001", "해당 유저를 찾을 수 없습니다.", UserNotFoundException.class),
+ U002("U002", "프로필 이미지 수정은 불가합니다.", ProfileUpdateNotAllowedException.class),
+ U003("U003", "이미 존재하는 회원 이름입니다.", UserNameDuplicatedException.class),
+ U004("U004", "회원명은 필수 입력값입니다.", ExternalException.class),
+ U005("U005", "이름은 최소 1자 이상, 최대 20자까지 입력 가능합니다.", ExternalException.class),
+ U006("U006", "회원명에 공백은 포함될 수 없습니다.", ExternalException.class),
+ U007("U007", "회원 정보를 수정하기 위해서는 프로필 사진이 필요합니다.", ExternalException.class),
+ U008("U008", "회원 정보를 수정하기 위해서는 소개글은 최소 0자 이상이 필요합니다.", ExternalException.class),
+ U009("U009", "소개글은 최대 255자까지 가능합니다.", ExternalException.class),
+ U010("U010", "10MB 이하의 파일만 업로드할 수 있습니다.", ExternalException.class),
+ U011("U011", "요청할 수 있는 최대 파일 크기는 100MB 입니다.", ExternalException.class),
+ U012("U012", "허용되지 않는 파일 확장자입니다.", ImageExtensionNotAllowedException.class),
+
+ W001("W001", "문제집 이름은 30자 이하여야 합니다.", WorkbookNameLengthException.class),
+ W002("W002", "문제집 이름은 필수 입력값입니다.", WorkbookNameNullException.class),
+ W003("W003", "태그 아이디는 필수 입력값입니다.", ExternalException.class),
+ W004("W004", "태그 아이디는 0이상의 숫자입니다.", ExternalException.class),
+ W005("W005", "태그 이름은 필수 입력값입니다.", TagNameNullException.class),
+ W006("W006", "태그는 20자 이하여야 합니다.", TagNameLengthException.class),
+ W007("W007", "문제집이 가질 수 있는 태그수는 최대 3개 입니다.", WorkbookTagLimitException.class),
+ W008("W008", "문제집을 수정하려면 태그가 필요합니다.", TagNullException.class),
+ W009("W009", "문제집의 공개 여부는 필수 입력값입니다.", ExternalException.class),
+ W010("W010", "카드 개수는 필수 입력값입니다..", ExternalException.class),
+ W011("W011", "카드 개수는 0이상 입니다.", ExternalException.class),
+ W012("W012", "좋아요 개수는 필수 입력값입니다.", ExternalException.class),
+ W013("W013", "좋아요 개수는 0이상 입니다.", ExternalException.class),
+ W014("W014", "해당 문제집을 찾을 수 없습니다.", WorkbookNotFoundException.class),
+ W015("W015", "공개 문제집이 아닙니다.", NotOpenedWorkbookException.class),
+ W016("W016", "카드를 내 문제집으로 옮기려면 카드 아이디가 필요합니다.", ExternalException.class),
+
+ C001("C001", "해당 카드를 찾을 수 없습니다.", CardNotFoundException.class),
+ C002("C002", "질문은 필수 입력값입니다.", CardQuestionNullException.class),
+ C003("C003", "질문은 최대 2000자까지 입력 가능합니다.", ExternalException.class),
+ C004("C004", "답변은 필수 입력값입니다.", CardAnswerNullException.class),
+ C005("C005", "답변은 최대 2000자까지 입력 가능합니다.", ExternalException.class),
+ C006("C006", "문제집 아이디는 필수 입력값입니다.", ExternalException.class),
+ C007("C007", "문제집 아이디는 0이상의 숫자입니다.", ExternalException.class),
+ C008("C008", "마주친 횟수는 필수 입력값입니다.", ExternalException.class),
+ C009("C009", "마주친 횟수는 0이상 입니다.", ExternalException.class),
+ C010("C010", "카드를 수정하기 위해서는 북마크 정보가 필요합니다.", ExternalException.class),
+ C011("C011", "카드를 수정 위해서는 또 보기 정보가 필요합니다.", ExternalException.class),
+ C012("C012", "유효하지 않은 또 보기 카드 등록 요청입니다.", ExternalException.class),
+
+ Q001("Q001", "퀴즈를 진행하려면 문제집 아이디가 필요합니다.", ExternalException.class),
+ Q002("Q002", "퀴즈의 개수는 10 ~ 30 사이의 수만 가능합니다.", ExternalException.class),
+ Q003("Q003", "퀴즈에 문제가 존재하지 않습니다.", QuizEmptyException.class),
+
+ S001("S001", "페이지의 시작 값은 음수가 될 수 없습니다.", InvalidPageStartException.class),
+ S002("S002", "유효하지 않은 페이지 크기입니다. 유효한 크기 : 1 ~ 100", InvalidPageSizeException.class),
+ S003("S003", "유효하지 않은 정렬 조건입니다. 유효한 정렬 조건 : date, name, count, heart", InvalidSearchCriteriaException.class),
+ S004("S004", "유효하지 않은 정렬 방향입니다. 유효한 정렬 방식 : ASC, DESC", InvalidSearchOrderException.class),
+ S005("S005", "유효하지 않은 검색 타입입니다. 유효한 검색 타임 : name, tag, user", InvalidSearchTypeException.class),
+ S006("S006", "검색어는 null일 수 없습니다.", SearchKeywordNullException.class),
+ S007("S007", "검색어는 30자 이하여야 합니다.", LongSearchKeywordException.class),
+ S008("S008", "검색어는 1자 이상이어야 합니다.", ShortSearchKeywordException.class),
+ S009("S009", "금지어를 입력했습니다.", ShortSearchKeywordException.class),
+
+ E001("E001", "서버에러", InternalServerErrorException.class),
+ E002("E002", "파라미터를 입력해야 합니다.", ExternalException.class),
+ X001("X001", "정의되지 않은 에러", UndefinedException.class);
+
+ private final String code;
+ private final String message;
+ private final Class extends BotoboException> classType;
+
+ private static final Map, ErrorType> codeMap = new HashMap<>();
+
+ static {
+ Arrays.stream(values())
+ .filter(ErrorType::isNotExternalException)
+ .forEach(errorType -> codeMap.put(errorType.classType, errorType));
+ }
+
+ public static ErrorType of(Class extends BotoboException> classType) {
+ if (classType.equals(ExternalException.class)) {
+ throw new UnsupportedOperationException("클래스로 ErrorType을 생성할 수 없는 예외입니다.");
+ }
+ return codeMap.getOrDefault(classType, ErrorType.X001);
+ }
+
+ public static ErrorType of(String code) {
+ return Arrays.stream(values())
+ .parallel()
+ .filter(errorType -> errorType.hasSameCode(code))
+ .findAny()
+ .orElse(ErrorType.X001);
+ }
+
+ public boolean isNotExternalException() {
+ return !classType.equals(ExternalException.class);
+ }
+
+ public boolean hasSameCode(String code) {
+ return Objects.equals(this.code, code);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/exception/common/UndefinedException.java b/backend/src/main/java/botobo/core/exception/common/UndefinedException.java
new file mode 100644
index 00000000..4ea8d0f6
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/common/UndefinedException.java
@@ -0,0 +1,9 @@
+package botobo.core.exception.common;
+
+import botobo.core.exception.http.InternalServerErrorException;
+
+public class UndefinedException extends InternalServerErrorException {
+ public UndefinedException(String serverMessage) {
+ super(serverMessage);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/exception/heart/HeartAdditionFailureException.java b/backend/src/main/java/botobo/core/exception/heart/HeartAdditionFailureException.java
new file mode 100644
index 00000000..cba93135
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/heart/HeartAdditionFailureException.java
@@ -0,0 +1,10 @@
+package botobo.core.exception.heart;
+
+
+import botobo.core.exception.http.InternalServerErrorException;
+
+public class HeartAdditionFailureException extends InternalServerErrorException {
+ public HeartAdditionFailureException() {
+ super("하트가 이미 존재하여 추가에 실패했습니다.");
+ }
+}
diff --git a/backend/src/main/java/botobo/core/exception/heart/HeartCreationFailureException.java b/backend/src/main/java/botobo/core/exception/heart/HeartCreationFailureException.java
new file mode 100644
index 00000000..6450412d
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/heart/HeartCreationFailureException.java
@@ -0,0 +1,10 @@
+package botobo.core.exception.heart;
+
+
+import botobo.core.exception.http.InternalServerErrorException;
+
+public class HeartCreationFailureException extends InternalServerErrorException {
+ public HeartCreationFailureException(String reason) {
+ super(String.format("Heart 생성에 실패했습니다. (%s)", reason));
+ }
+}
diff --git a/backend/src/main/java/botobo/core/exception/heart/HeartRemovalFailureException.java b/backend/src/main/java/botobo/core/exception/heart/HeartRemovalFailureException.java
new file mode 100644
index 00000000..88fefa94
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/heart/HeartRemovalFailureException.java
@@ -0,0 +1,10 @@
+package botobo.core.exception.heart;
+
+
+import botobo.core.exception.http.InternalServerErrorException;
+
+public class HeartRemovalFailureException extends InternalServerErrorException {
+ public HeartRemovalFailureException() {
+ super("제거할 하트를 찾지 못했습니다.");
+ }
+}
diff --git a/backend/src/main/java/botobo/core/exception/heart/HeartsCreationFailureException.java b/backend/src/main/java/botobo/core/exception/heart/HeartsCreationFailureException.java
new file mode 100644
index 00000000..45227c41
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/heart/HeartsCreationFailureException.java
@@ -0,0 +1,10 @@
+package botobo.core.exception.heart;
+
+
+import botobo.core.exception.http.InternalServerErrorException;
+
+public class HeartsCreationFailureException extends InternalServerErrorException {
+ public HeartsCreationFailureException(String reason) {
+ super(String.format("Hearts 생성에 실패했습니다. (%s)", reason));
+ }
+}
diff --git a/backend/src/main/java/botobo/core/exception/http/BadRequestException.java b/backend/src/main/java/botobo/core/exception/http/BadRequestException.java
new file mode 100644
index 00000000..d3cd2ccd
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/http/BadRequestException.java
@@ -0,0 +1,10 @@
+package botobo.core.exception.http;
+
+import botobo.core.exception.BotoboException;
+import org.springframework.http.HttpStatus;
+
+public class BadRequestException extends BotoboException {
+ public BadRequestException() {
+ super(HttpStatus.BAD_REQUEST);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/exception/http/ConflictException.java b/backend/src/main/java/botobo/core/exception/http/ConflictException.java
new file mode 100644
index 00000000..1a97acb3
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/http/ConflictException.java
@@ -0,0 +1,10 @@
+package botobo.core.exception.http;
+
+import botobo.core.exception.BotoboException;
+import org.springframework.http.HttpStatus;
+
+public class ConflictException extends BotoboException {
+ public ConflictException() {
+ super(HttpStatus.CONFLICT);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/exception/http/ForbiddenException.java b/backend/src/main/java/botobo/core/exception/http/ForbiddenException.java
new file mode 100644
index 00000000..7215e6cb
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/http/ForbiddenException.java
@@ -0,0 +1,10 @@
+package botobo.core.exception.http;
+
+import botobo.core.exception.BotoboException;
+import org.springframework.http.HttpStatus;
+
+public class ForbiddenException extends BotoboException {
+ public ForbiddenException() {
+ super(HttpStatus.FORBIDDEN);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/exception/http/InternalServerErrorException.java b/backend/src/main/java/botobo/core/exception/http/InternalServerErrorException.java
new file mode 100644
index 00000000..b6965124
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/http/InternalServerErrorException.java
@@ -0,0 +1,16 @@
+package botobo.core.exception.http;
+
+import botobo.core.exception.BotoboException;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+public class InternalServerErrorException extends BotoboException {
+
+ private final String serverMessage;
+
+ public InternalServerErrorException(String serverMessage) {
+ super(HttpStatus.INTERNAL_SERVER_ERROR);
+ this.serverMessage = serverMessage;
+ }
+}
diff --git a/backend/src/main/java/botobo/core/exception/http/NotFoundException.java b/backend/src/main/java/botobo/core/exception/http/NotFoundException.java
new file mode 100644
index 00000000..292a2e12
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/http/NotFoundException.java
@@ -0,0 +1,10 @@
+package botobo.core.exception.http;
+
+import botobo.core.exception.BotoboException;
+import org.springframework.http.HttpStatus;
+
+public class NotFoundException extends BotoboException {
+ public NotFoundException() {
+ super(HttpStatus.NOT_FOUND);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/exception/http/UnAuthorizedException.java b/backend/src/main/java/botobo/core/exception/http/UnAuthorizedException.java
new file mode 100644
index 00000000..4bbe2522
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/http/UnAuthorizedException.java
@@ -0,0 +1,10 @@
+package botobo.core.exception.http;
+
+import botobo.core.exception.BotoboException;
+import org.springframework.http.HttpStatus;
+
+public class UnAuthorizedException extends BotoboException {
+ public UnAuthorizedException() {
+ super(HttpStatus.UNAUTHORIZED);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/exception/search/ForbiddenSearchKeywordException.java b/backend/src/main/java/botobo/core/exception/search/ForbiddenSearchKeywordException.java
new file mode 100644
index 00000000..ee54ca77
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/search/ForbiddenSearchKeywordException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.search;
+
+import botobo.core.exception.http.BadRequestException;
+
+public class ForbiddenSearchKeywordException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/search/InvalidPageSizeException.java b/backend/src/main/java/botobo/core/exception/search/InvalidPageSizeException.java
new file mode 100644
index 00000000..2d790b8e
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/search/InvalidPageSizeException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.search;
+
+import botobo.core.exception.http.BadRequestException;
+
+public class InvalidPageSizeException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/search/InvalidPageStartException.java b/backend/src/main/java/botobo/core/exception/search/InvalidPageStartException.java
new file mode 100644
index 00000000..200d54ef
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/search/InvalidPageStartException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.search;
+
+import botobo.core.exception.http.BadRequestException;
+
+public class InvalidPageStartException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/search/InvalidSearchCriteriaException.java b/backend/src/main/java/botobo/core/exception/search/InvalidSearchCriteriaException.java
new file mode 100644
index 00000000..e4ec3f05
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/search/InvalidSearchCriteriaException.java
@@ -0,0 +1,7 @@
+package botobo.core.exception.search;
+
+
+import botobo.core.exception.http.BadRequestException;
+
+public class InvalidSearchCriteriaException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/search/InvalidSearchOrderException.java b/backend/src/main/java/botobo/core/exception/search/InvalidSearchOrderException.java
new file mode 100644
index 00000000..d34229d8
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/search/InvalidSearchOrderException.java
@@ -0,0 +1,7 @@
+package botobo.core.exception.search;
+
+
+import botobo.core.exception.http.BadRequestException;
+
+public class InvalidSearchOrderException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/search/InvalidSearchTypeException.java b/backend/src/main/java/botobo/core/exception/search/InvalidSearchTypeException.java
new file mode 100644
index 00000000..bc285b77
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/search/InvalidSearchTypeException.java
@@ -0,0 +1,7 @@
+package botobo.core.exception.search;
+
+
+import botobo.core.exception.http.BadRequestException;
+
+public class InvalidSearchTypeException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/search/LongSearchKeywordException.java b/backend/src/main/java/botobo/core/exception/search/LongSearchKeywordException.java
new file mode 100644
index 00000000..a7556c8a
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/search/LongSearchKeywordException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.search;
+
+import botobo.core.exception.http.BadRequestException;
+
+public class LongSearchKeywordException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/search/SearchKeywordNullException.java b/backend/src/main/java/botobo/core/exception/search/SearchKeywordNullException.java
new file mode 100644
index 00000000..feeb449a
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/search/SearchKeywordNullException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.search;
+
+import botobo.core.exception.http.BadRequestException;
+
+public class SearchKeywordNullException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/search/ShortSearchKeywordException.java b/backend/src/main/java/botobo/core/exception/search/ShortSearchKeywordException.java
new file mode 100644
index 00000000..1a5f144b
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/search/ShortSearchKeywordException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.search;
+
+import botobo.core.exception.http.BadRequestException;
+
+public class ShortSearchKeywordException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/tag/InvalidTagNameException.java b/backend/src/main/java/botobo/core/exception/tag/InvalidTagNameException.java
deleted file mode 100644
index 13a68852..00000000
--- a/backend/src/main/java/botobo/core/exception/tag/InvalidTagNameException.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package botobo.core.exception.tag;
-
-import botobo.core.exception.BadRequestException;
-
-public class InvalidTagNameException extends BadRequestException {
- public InvalidTagNameException(String reason) {
- super(String.format("적절하지 않은 태그입니다. (%s)", reason));
- }
-}
diff --git a/backend/src/main/java/botobo/core/exception/tag/TagCreationFailureException.java b/backend/src/main/java/botobo/core/exception/tag/TagCreationFailureException.java
deleted file mode 100644
index e869ea2b..00000000
--- a/backend/src/main/java/botobo/core/exception/tag/TagCreationFailureException.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package botobo.core.exception.tag;
-
-import botobo.core.exception.BadRequestException;
-
-public class TagCreationFailureException extends BadRequestException {
- public TagCreationFailureException(String reason) {
- super(String.format("Tag객체 생성에 실패했습니다. (%s)", reason));
- }
-}
diff --git a/backend/src/main/java/botobo/core/exception/tag/TagNameLengthException.java b/backend/src/main/java/botobo/core/exception/tag/TagNameLengthException.java
new file mode 100644
index 00000000..fa01025a
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/tag/TagNameLengthException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.tag;
+
+import botobo.core.exception.http.BadRequestException;
+
+public class TagNameLengthException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/tag/TagNameNullException.java b/backend/src/main/java/botobo/core/exception/tag/TagNameNullException.java
new file mode 100644
index 00000000..599f8a2d
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/tag/TagNameNullException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.tag;
+
+import botobo.core.exception.http.BadRequestException;
+
+public class TagNameNullException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/tag/TagNamesCreationFailureException.java b/backend/src/main/java/botobo/core/exception/tag/TagNamesCreationFailureException.java
deleted file mode 100644
index 894e6e8f..00000000
--- a/backend/src/main/java/botobo/core/exception/tag/TagNamesCreationFailureException.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package botobo.core.exception.tag;
-
-import botobo.core.exception.BadRequestException;
-
-public class TagNamesCreationFailureException extends BadRequestException {
- public TagNamesCreationFailureException(String reason) {
- super(String.format("TagNames객체 생성에 실패했습니다. (%s)", reason));
- }
-}
diff --git a/backend/src/main/java/botobo/core/exception/tag/TagNullException.java b/backend/src/main/java/botobo/core/exception/tag/TagNullException.java
new file mode 100644
index 00000000..da04b473
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/tag/TagNullException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.tag;
+
+import botobo.core.exception.http.BadRequestException;
+
+public class TagNullException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/tag/TagsCreationFailureException.java b/backend/src/main/java/botobo/core/exception/tag/TagsCreationFailureException.java
index 2f80caaf..3a8cef9e 100644
--- a/backend/src/main/java/botobo/core/exception/tag/TagsCreationFailureException.java
+++ b/backend/src/main/java/botobo/core/exception/tag/TagsCreationFailureException.java
@@ -1,8 +1,8 @@
package botobo.core.exception.tag;
-import botobo.core.exception.BadRequestException;
+import botobo.core.exception.http.InternalServerErrorException;
-public class TagsCreationFailureException extends BadRequestException {
+public class TagsCreationFailureException extends InternalServerErrorException {
public TagsCreationFailureException(String reason) {
super(String.format("Tags객체 생성에 실패했습니다. (%s)", reason));
}
diff --git a/backend/src/main/java/botobo/core/exception/user/AnonymousHasNotIdException.java b/backend/src/main/java/botobo/core/exception/user/AnonymousHasNotIdException.java
new file mode 100644
index 00000000..ed283dac
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/user/AnonymousHasNotIdException.java
@@ -0,0 +1,9 @@
+package botobo.core.exception.user;
+
+import botobo.core.exception.http.InternalServerErrorException;
+
+public class AnonymousHasNotIdException extends InternalServerErrorException {
+ public AnonymousHasNotIdException(String message) {
+ super(message);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/exception/user/NotAuthorException.java b/backend/src/main/java/botobo/core/exception/user/NotAuthorException.java
new file mode 100644
index 00000000..7bffa7c2
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/user/NotAuthorException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.user;
+
+import botobo.core.exception.http.ForbiddenException;
+
+public class NotAuthorException extends ForbiddenException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/user/ProfileUpdateNotAllowedException.java b/backend/src/main/java/botobo/core/exception/user/ProfileUpdateNotAllowedException.java
new file mode 100644
index 00000000..92f12e3a
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/user/ProfileUpdateNotAllowedException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.user;
+
+import botobo.core.exception.http.ForbiddenException;
+
+public class ProfileUpdateNotAllowedException extends ForbiddenException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/user/SocialTypeNotFoundException.java b/backend/src/main/java/botobo/core/exception/user/SocialTypeNotFoundException.java
new file mode 100644
index 00000000..3a6b75f8
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/user/SocialTypeNotFoundException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.user;
+
+import botobo.core.exception.http.NotFoundException;
+
+public class SocialTypeNotFoundException extends NotFoundException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/user/UserNameDuplicatedException.java b/backend/src/main/java/botobo/core/exception/user/UserNameDuplicatedException.java
new file mode 100644
index 00000000..911747a6
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/user/UserNameDuplicatedException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.user;
+
+import botobo.core.exception.http.ConflictException;
+
+public class UserNameDuplicatedException extends ConflictException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/user/UserNotFoundException.java b/backend/src/main/java/botobo/core/exception/user/UserNotFoundException.java
index 84fc6ac6..913e7045 100644
--- a/backend/src/main/java/botobo/core/exception/user/UserNotFoundException.java
+++ b/backend/src/main/java/botobo/core/exception/user/UserNotFoundException.java
@@ -1,13 +1,6 @@
package botobo.core.exception.user;
-import botobo.core.exception.NotFoundException;
+import botobo.core.exception.http.NotFoundException;
public class UserNotFoundException extends NotFoundException {
- public UserNotFoundException() {
- super("해당 유저를 찾을 수 없습니다.");
- }
-
- public UserNotFoundException(String message) {
- super(message);
- }
}
diff --git a/backend/src/main/java/botobo/core/exception/user/s3/FileConvertFailedException.java b/backend/src/main/java/botobo/core/exception/user/s3/FileConvertFailedException.java
new file mode 100644
index 00000000..f87c04d5
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/user/s3/FileConvertFailedException.java
@@ -0,0 +1,9 @@
+package botobo.core.exception.user.s3;
+
+import botobo.core.exception.http.InternalServerErrorException;
+
+public class FileConvertFailedException extends InternalServerErrorException {
+ public FileConvertFailedException() {
+ super("파일을 변환할 수 없습니다.");
+ }
+}
diff --git a/backend/src/main/java/botobo/core/exception/user/s3/ImageExtensionNotAllowedException.java b/backend/src/main/java/botobo/core/exception/user/s3/ImageExtensionNotAllowedException.java
new file mode 100644
index 00000000..b929a17e
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/user/s3/ImageExtensionNotAllowedException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.user.s3;
+
+import botobo.core.exception.http.BadRequestException;
+
+public class ImageExtensionNotAllowedException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/workbook/NotOpenedWorkbookException.java b/backend/src/main/java/botobo/core/exception/workbook/NotOpenedWorkbookException.java
new file mode 100644
index 00000000..93dca3f8
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/workbook/NotOpenedWorkbookException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.workbook;
+
+import botobo.core.exception.http.BadRequestException;
+
+public class NotOpenedWorkbookException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/workbook/SearchKeywordCreationFailureException.java b/backend/src/main/java/botobo/core/exception/workbook/SearchKeywordCreationFailureException.java
deleted file mode 100644
index a810edf9..00000000
--- a/backend/src/main/java/botobo/core/exception/workbook/SearchKeywordCreationFailureException.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package botobo.core.exception.workbook;
-
-import botobo.core.exception.BadRequestException;
-
-public class SearchKeywordCreationFailureException extends BadRequestException {
- public SearchKeywordCreationFailureException(String message) {
- super(message);
- }
-}
diff --git a/backend/src/main/java/botobo/core/exception/workbook/WorkbookNameLengthException.java b/backend/src/main/java/botobo/core/exception/workbook/WorkbookNameLengthException.java
new file mode 100644
index 00000000..1fcd4681
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/workbook/WorkbookNameLengthException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.workbook;
+
+import botobo.core.exception.http.BadRequestException;
+
+public class WorkbookNameLengthException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/workbook/WorkbookNameNullException.java b/backend/src/main/java/botobo/core/exception/workbook/WorkbookNameNullException.java
new file mode 100644
index 00000000..fd649c92
--- /dev/null
+++ b/backend/src/main/java/botobo/core/exception/workbook/WorkbookNameNullException.java
@@ -0,0 +1,6 @@
+package botobo.core.exception.workbook;
+
+import botobo.core.exception.http.BadRequestException;
+
+public class WorkbookNameNullException extends BadRequestException {
+}
diff --git a/backend/src/main/java/botobo/core/exception/workbook/WorkbookNotFoundException.java b/backend/src/main/java/botobo/core/exception/workbook/WorkbookNotFoundException.java
index 746707d1..a8c35353 100644
--- a/backend/src/main/java/botobo/core/exception/workbook/WorkbookNotFoundException.java
+++ b/backend/src/main/java/botobo/core/exception/workbook/WorkbookNotFoundException.java
@@ -1,13 +1,6 @@
package botobo.core.exception.workbook;
-import botobo.core.exception.NotFoundException;
+import botobo.core.exception.http.NotFoundException;
public class WorkbookNotFoundException extends NotFoundException {
- public WorkbookNotFoundException() {
- super("해당 문제집을 찾을 수 없습니다.");
- }
-
- public WorkbookNotFoundException(String message) {
- super(message);
- }
}
diff --git a/backend/src/main/java/botobo/core/exception/workbook/WorkbookTagLimitException.java b/backend/src/main/java/botobo/core/exception/workbook/WorkbookTagLimitException.java
index 75e2a21b..b271f85d 100644
--- a/backend/src/main/java/botobo/core/exception/workbook/WorkbookTagLimitException.java
+++ b/backend/src/main/java/botobo/core/exception/workbook/WorkbookTagLimitException.java
@@ -1,10 +1,6 @@
package botobo.core.exception.workbook;
-import botobo.core.exception.BadRequestException;
+import botobo.core.exception.http.BadRequestException;
public class WorkbookTagLimitException extends BadRequestException {
-
- public WorkbookTagLimitException(int maxTagSize) {
- super(String.format("문제집이 가질 수 있는 태그수는 최대 %s개 입니다.", maxTagSize));
- }
}
diff --git a/backend/src/main/java/botobo/core/exception/workbooktag/WorkbookTagCreationFailureException.java b/backend/src/main/java/botobo/core/exception/workbooktag/WorkbookTagCreationFailureException.java
index 0dbf7261..cd8170ca 100644
--- a/backend/src/main/java/botobo/core/exception/workbooktag/WorkbookTagCreationFailureException.java
+++ b/backend/src/main/java/botobo/core/exception/workbooktag/WorkbookTagCreationFailureException.java
@@ -1,8 +1,8 @@
package botobo.core.exception.workbooktag;
-import botobo.core.exception.BadRequestException;
+import botobo.core.exception.http.InternalServerErrorException;
-public class WorkbookTagCreationFailureException extends BadRequestException {
+public class WorkbookTagCreationFailureException extends InternalServerErrorException {
public WorkbookTagCreationFailureException(String message) {
super(message);
}
diff --git a/backend/src/main/java/botobo/core/infrastructure/FileNameGenerator.java b/backend/src/main/java/botobo/core/infrastructure/FileNameGenerator.java
new file mode 100644
index 00000000..8e600401
--- /dev/null
+++ b/backend/src/main/java/botobo/core/infrastructure/FileNameGenerator.java
@@ -0,0 +1,47 @@
+package botobo.core.infrastructure;
+
+import botobo.core.domain.user.s3.ImageExtension;
+import botobo.core.exception.user.s3.ImageExtensionNotAllowedException;
+import org.apache.commons.io.FilenameUtils;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.UUID;
+
+@Component
+public class FileNameGenerator {
+ private static final String BASE_DIR = "users/";
+ private static final String SLASH = "/";
+
+ public String generateFileName(MultipartFile multipartFile, String userName) {
+ return makeNewFileName(replaceWhiteSpace(userName)) + extension(multipartFile);
+ }
+
+ private String replaceWhiteSpace(String userName) {
+ return userName.replaceAll("\\s+", "_");
+ }
+
+ private String makeNewFileName(String userName) {
+ String newlyCreatedFileName = UUID.randomUUID() + "_"
+ + LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE);
+ return insertDirectory(newlyCreatedFileName, userName);
+ }
+
+ private String insertDirectory(String fileName, String userName) {
+ return BASE_DIR + userName + SLASH + fileName;
+ }
+
+ private String extension(MultipartFile multipartFile) {
+ String extension = FilenameUtils.getExtension(multipartFile.getOriginalFilename());
+ if (!ImageExtension.isAllowedExtension(extension)) {
+ throw new ImageExtensionNotAllowedException();
+ }
+ return insertDot(extension);
+ }
+
+ private String insertDot(String extension) {
+ return "." + extension;
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/botobo/core/infrastructure/GithubOauthManager.java b/backend/src/main/java/botobo/core/infrastructure/GithubOauthManager.java
index dbed4aeb..e2b60514 100644
--- a/backend/src/main/java/botobo/core/infrastructure/GithubOauthManager.java
+++ b/backend/src/main/java/botobo/core/infrastructure/GithubOauthManager.java
@@ -1,10 +1,12 @@
package botobo.core.infrastructure;
+import botobo.core.domain.user.SocialType;
+import botobo.core.domain.user.User;
import botobo.core.dto.auth.GithubTokenRequest;
-import botobo.core.dto.auth.GithubTokenResponse;
import botobo.core.dto.auth.GithubUserInfoResponse;
-import botobo.core.dto.auth.LoginRequest;
-import botobo.core.exception.auth.GithubApiFailedException;
+import botobo.core.dto.auth.OauthTokenResponse;
+import botobo.core.dto.auth.UserInfoResponse;
+import botobo.core.exception.auth.OauthApiFailedException;
import botobo.core.exception.auth.UserProfileLoadFailedException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
@@ -16,7 +18,7 @@
import org.springframework.web.client.RestTemplate;
@Component
-public class GithubOauthManager {
+public class GithubOauthManager implements OauthManager {
@Value("${github.client.id}")
private String clientId;
@@ -27,27 +29,35 @@ public class GithubOauthManager {
@Value("${github.url.profile}")
private String profileUrl;
- public GithubUserInfoResponse getUserInfoFromGithub(LoginRequest loginRequest) {
- GithubTokenResponse githubTokenResponse = getAccessTokenFromGithub(loginRequest.getCode());
+ @Override
+ public User getUserInfo(String code) {
+ OauthTokenResponse githubTokenResponse = getAccessToken(code);
final String accessToken = githubTokenResponse.getAccessToken();
if (accessToken == null) {
- throw new GithubApiFailedException();
+ throw new OauthApiFailedException();
}
HttpHeaders headers = new HttpHeaders();
- headers.add("Authorization", "token " + accessToken);
+ headers.add("Authorization", githubTokenResponse.getTokenType() + " " + accessToken);
HttpEntity httpEntity = new HttpEntity(headers);
RestTemplate restTemplate = new RestTemplate();
try {
- return restTemplate
+ UserInfoResponse githubUserInfoResponse = restTemplate
.exchange(profileUrl, HttpMethod.GET, httpEntity, GithubUserInfoResponse.class)
.getBody();
+ return githubUserInfoResponse.toUser();
} catch (RestClientException e) {
throw new UserProfileLoadFailedException();
}
}
- private GithubTokenResponse getAccessTokenFromGithub(String code) {
+ @Override
+ public boolean isSameSocialType(SocialType socialType) {
+ return socialType == SocialType.GITHUB;
+ }
+
+ @Override
+ public OauthTokenResponse getAccessToken(String code) {
RestTemplate restTemplate = new RestTemplate();
GithubTokenRequest githubTokenRequest = GithubTokenRequest.builder()
.code(code)
@@ -57,6 +67,6 @@ private GithubTokenResponse getAccessTokenFromGithub(String code) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Accept", MediaType.APPLICATION_JSON_VALUE);
HttpEntity httpEntity = new HttpEntity<>(githubTokenRequest, httpHeaders);
- return restTemplate.exchange(url, HttpMethod.POST, httpEntity, GithubTokenResponse.class).getBody();
+ return restTemplate.exchange(url, HttpMethod.POST, httpEntity, OauthTokenResponse.class).getBody();
}
}
diff --git a/backend/src/main/java/botobo/core/infrastructure/GoogleOauthManager.java b/backend/src/main/java/botobo/core/infrastructure/GoogleOauthManager.java
new file mode 100644
index 00000000..b4e82565
--- /dev/null
+++ b/backend/src/main/java/botobo/core/infrastructure/GoogleOauthManager.java
@@ -0,0 +1,78 @@
+package botobo.core.infrastructure;
+
+import botobo.core.domain.user.SocialType;
+import botobo.core.domain.user.User;
+import botobo.core.dto.auth.GoogleTokenRequest;
+import botobo.core.dto.auth.GoogleUserInfoResponse;
+import botobo.core.dto.auth.OauthTokenResponse;
+import botobo.core.dto.auth.UserInfoResponse;
+import botobo.core.exception.auth.OauthApiFailedException;
+import botobo.core.exception.auth.UserProfileLoadFailedException;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+
+@Component
+public class GoogleOauthManager implements OauthManager {
+
+ @Value("${google.client.id}")
+ private String clientId;
+ @Value("${google.client.secret}")
+ private String clientSecret;
+ @Value("${google.client.redirect-uri}")
+ private String redirectUri;
+ @Value("${google.client.grant-type}")
+ private String grantType;
+ @Value("${google.url.access-token}")
+ private String url;
+ @Value("${google.url.profile}")
+ private String profileUrl;
+
+ @Override
+ public User getUserInfo(String code) {
+ OauthTokenResponse googleTokenResponse = getAccessToken(code);
+ final String accessToken = googleTokenResponse.getAccessToken();
+ if (accessToken == null) {
+ throw new OauthApiFailedException();
+ }
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.add("Authorization", googleTokenResponse.getTokenType() + " " + accessToken);
+ HttpEntity httpEntity = new HttpEntity(headers);
+ RestTemplate restTemplate = new RestTemplate();
+ try {
+ UserInfoResponse googleUserInfoResponse = restTemplate
+ .exchange(profileUrl, HttpMethod.GET, httpEntity, GoogleUserInfoResponse.class)
+ .getBody();
+ return googleUserInfoResponse.toUser();
+ } catch (RestClientException e) {
+ throw new UserProfileLoadFailedException();
+ }
+ }
+
+ @Override
+ public boolean isSameSocialType(SocialType socialType) {
+ return socialType == SocialType.GOOGLE;
+ }
+
+ @Override
+ public OauthTokenResponse getAccessToken(String code) {
+ RestTemplate restTemplate = new RestTemplate();
+ GoogleTokenRequest googleTokenRequest = GoogleTokenRequest.builder()
+ .code(code)
+ .client_id(clientId)
+ .client_secret(clientSecret)
+ .redirect_uri(redirectUri)
+ .grant_type(grantType)
+ .build();
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.add("Accept", MediaType.APPLICATION_JSON_VALUE);
+ HttpEntity httpEntity = new HttpEntity<>(googleTokenRequest, httpHeaders);
+ return restTemplate.exchange(url, HttpMethod.POST, httpEntity, OauthTokenResponse.class).getBody();
+ }
+}
diff --git a/backend/src/main/java/botobo/core/infrastructure/JwtTokenProvider.java b/backend/src/main/java/botobo/core/infrastructure/JwtTokenProvider.java
index 43a3a023..6f309699 100644
--- a/backend/src/main/java/botobo/core/infrastructure/JwtTokenProvider.java
+++ b/backend/src/main/java/botobo/core/infrastructure/JwtTokenProvider.java
@@ -1,7 +1,8 @@
package botobo.core.infrastructure;
+import botobo.core.exception.auth.TokenExpirationException;
import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@@ -41,11 +42,12 @@ public Long getIdFromPayLoad(String token) {
public boolean isValidToken(String token) {
try {
- Jws claims = Jwts
- .parser()
+ Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);
- return !claims.getBody().getExpiration().before(new Date());
+ return true;
+ } catch (ExpiredJwtException e) {
+ throw new TokenExpirationException();
} catch (JwtException | IllegalArgumentException e) {
return false;
}
diff --git a/backend/src/main/java/botobo/core/infrastructure/OauthManager.java b/backend/src/main/java/botobo/core/infrastructure/OauthManager.java
new file mode 100644
index 00000000..b52f6284
--- /dev/null
+++ b/backend/src/main/java/botobo/core/infrastructure/OauthManager.java
@@ -0,0 +1,13 @@
+package botobo.core.infrastructure;
+
+import botobo.core.domain.user.SocialType;
+import botobo.core.domain.user.User;
+import botobo.core.dto.auth.OauthTokenResponse;
+
+public interface OauthManager {
+ User getUserInfo(String code);
+
+ boolean isSameSocialType(SocialType socialType);
+
+ OauthTokenResponse getAccessToken(String code);
+}
diff --git a/backend/src/main/java/botobo/core/infrastructure/OauthManagerFactory.java b/backend/src/main/java/botobo/core/infrastructure/OauthManagerFactory.java
new file mode 100644
index 00000000..31660f2c
--- /dev/null
+++ b/backend/src/main/java/botobo/core/infrastructure/OauthManagerFactory.java
@@ -0,0 +1,24 @@
+package botobo.core.infrastructure;
+
+import botobo.core.domain.user.SocialType;
+import botobo.core.exception.user.SocialTypeNotFoundException;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class OauthManagerFactory {
+
+ private final List oauthManagers;
+
+ public OauthManagerFactory(List oauthManagers) {
+ this.oauthManagers = oauthManagers;
+ }
+
+ public OauthManager findOauthMangerBySocialType(SocialType socialType) {
+ return oauthManagers.stream()
+ .filter(oauthManager -> oauthManager.isSameSocialType(socialType))
+ .findFirst()
+ .orElseThrow(SocialTypeNotFoundException::new);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/infrastructure/S3Uploader.java b/backend/src/main/java/botobo/core/infrastructure/S3Uploader.java
new file mode 100644
index 00000000..b045ee9c
--- /dev/null
+++ b/backend/src/main/java/botobo/core/infrastructure/S3Uploader.java
@@ -0,0 +1,101 @@
+package botobo.core.infrastructure;
+
+import botobo.core.exception.user.s3.FileConvertFailedException;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.model.DeleteObjectRequest;
+import com.amazonaws.services.s3.model.PutObjectRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Objects;
+import java.util.Optional;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class S3Uploader {
+ private final AmazonS3 amazonS3Client;
+ private final FileNameGenerator fileNameGenerator;
+
+ @Value("${aws.cloudfront.url-format}")
+ private String cloudfrontUrlFormat;
+
+ @Value("${aws.s3.bucket}")
+ private String bucket;
+
+ @Value("${aws.user-default-image}")
+ private String userDefaultImageName;
+
+ public String upload(MultipartFile multipartFile, String userName) throws IOException {
+ if (isEmpty(multipartFile)) {
+ return makeCloudFrontUrl(userDefaultImageName);
+ }
+
+ String generatedFileName = fileNameGenerator.generateFileName(multipartFile, userName);
+ uploadImageToS3(
+ makeUploadImageFile(multipartFile),
+ generatedFileName
+ );
+
+ return makeCloudFrontUrl(generatedFileName);
+ }
+
+ private File makeUploadImageFile(MultipartFile multipartFile) throws IOException {
+ return convert(multipartFile)
+ .orElseThrow(FileConvertFailedException::new);
+ }
+
+ public void deleteFromS3(String oldImageUrl) {
+ String oldImageName = oldImageUrl.replace(
+ cloudfrontUrl(),
+ ""
+ );
+ if (!Objects.equals(oldImageName, userDefaultImageName) && amazonS3Client.doesObjectExist(bucket, oldImageName)) {
+ log.info(String.format("S3Uploader, S3에서 이미지(이미지명: %s)를 삭제했습니다.", oldImageName));
+ amazonS3Client.deleteObject(new DeleteObjectRequest(bucket, oldImageName));
+ }
+ }
+
+ private String cloudfrontUrl() {
+ return cloudfrontUrlFormat.replace("%s", "");
+ }
+
+ private boolean isEmpty(MultipartFile multipartFile) {
+ return Objects.isNull(multipartFile) || multipartFile.isEmpty();
+ }
+
+ private String makeCloudFrontUrl(String uploadImageUrl) {
+ return String.format(cloudfrontUrlFormat, uploadImageUrl);
+ }
+
+ private Optional convert(MultipartFile multipartFile) throws IOException {
+ File convertFile = new File(Objects.requireNonNull(multipartFile.getOriginalFilename()));
+ if (convertFile.createNewFile()) {
+ try (FileOutputStream fos = new FileOutputStream(convertFile)) {
+ fos.write(multipartFile.getBytes());
+ }
+ return Optional.of(convertFile);
+ }
+ return Optional.empty();
+ }
+
+ private void uploadImageToS3(File uploadImage, String fileName) {
+ try {
+ amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, uploadImage));
+ } finally {
+ deleteTemporaryFile(uploadImage);
+ }
+ }
+
+ private void deleteTemporaryFile(File uploadImage) {
+ if (!uploadImage.delete()) {
+ log.info("업로드용 파일이 제대로 삭제되지 않았습니다.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/botobo/core/infrastructure/YamlPropertySourceFactory.java b/backend/src/main/java/botobo/core/infrastructure/YamlPropertySourceFactory.java
new file mode 100644
index 00000000..f4e8caaf
--- /dev/null
+++ b/backend/src/main/java/botobo/core/infrastructure/YamlPropertySourceFactory.java
@@ -0,0 +1,24 @@
+package botobo.core.infrastructure;
+
+import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
+import org.springframework.core.env.PropertiesPropertySource;
+import org.springframework.core.env.PropertySource;
+import org.springframework.core.io.support.EncodedResource;
+import org.springframework.core.io.support.PropertySourceFactory;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.Properties;
+
+public class YamlPropertySourceFactory implements PropertySourceFactory {
+ @Override
+ public PropertySource> createPropertySource(String name, EncodedResource encodedResource)
+ throws IOException {
+ YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
+ factory.setResources(encodedResource.getResource());
+
+ Properties properties = factory.getObject();
+
+ return new PropertiesPropertySource(Objects.requireNonNull(encodedResource.getResource().getFilename()), properties);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/ui/CardController.java b/backend/src/main/java/botobo/core/ui/CardController.java
index 5f0c0ca7..73dfe895 100644
--- a/backend/src/main/java/botobo/core/ui/CardController.java
+++ b/backend/src/main/java/botobo/core/ui/CardController.java
@@ -52,8 +52,9 @@ public ResponseEntity deleteCard(@PathVariable Long id, @AuthenticationPri
}
@PutMapping("/next-quiz")
- public ResponseEntity selectNextQuizCards(@Valid @RequestBody NextQuizCardsRequest nextQuizCardsRequest) {
- cardService.selectNextQuizCards(nextQuizCardsRequest);
+ public ResponseEntity selectNextQuizCards(@Valid @RequestBody NextQuizCardsRequest nextQuizCardsRequest,
+ @AuthenticationPrincipal AppUser appUser) {
+ cardService.selectNextQuizCards(nextQuizCardsRequest, appUser);
return ResponseEntity.noContent().build();
}
}
diff --git a/backend/src/main/java/botobo/core/ui/QuizController.java b/backend/src/main/java/botobo/core/ui/QuizController.java
index 0e77686e..0b5d4582 100644
--- a/backend/src/main/java/botobo/core/ui/QuizController.java
+++ b/backend/src/main/java/botobo/core/ui/QuizController.java
@@ -1,8 +1,10 @@
package botobo.core.ui;
import botobo.core.application.QuizService;
+import botobo.core.domain.user.AppUser;
import botobo.core.dto.card.QuizRequest;
import botobo.core.dto.card.QuizResponse;
+import botobo.core.ui.auth.AuthenticationPrincipal;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -25,8 +27,9 @@ public QuizController(QuizService quizService) {
}
@PostMapping
- public ResponseEntity> createQuiz(@Valid @RequestBody QuizRequest quizRequest) {
- List quizResponses = quizService.createQuiz(quizRequest.getWorkbookIds());
+ public ResponseEntity> createQuiz(@Valid @RequestBody QuizRequest quizRequest,
+ @AuthenticationPrincipal AppUser appUser) {
+ List quizResponses = quizService.createQuiz(quizRequest, appUser);
return ResponseEntity.ok(quizResponses);
}
diff --git a/backend/src/main/java/botobo/core/ui/UserController.java b/backend/src/main/java/botobo/core/ui/UserController.java
index ed3deef5..3d9ca9ae 100644
--- a/backend/src/main/java/botobo/core/ui/UserController.java
+++ b/backend/src/main/java/botobo/core/ui/UserController.java
@@ -2,12 +2,23 @@
import botobo.core.application.UserService;
import botobo.core.domain.user.AppUser;
+import botobo.core.dto.user.ProfileResponse;
+import botobo.core.dto.user.UserNameRequest;
import botobo.core.dto.user.UserResponse;
+import botobo.core.dto.user.UserUpdateRequest;
import botobo.core.ui.auth.AuthenticationPrincipal;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.validation.Valid;
+import java.io.IOException;
@RestController
@RequestMapping("/api/users")
@@ -21,7 +32,31 @@ public UserController(UserService userService) {
@GetMapping("/me")
public ResponseEntity findUserOfMine(@AuthenticationPrincipal AppUser appUser) {
- UserResponse userResponse = userService.findById(appUser.getId());
+ UserResponse userResponse = userService.findById(appUser);
+ return ResponseEntity.ok(userResponse);
+ }
+
+ @PostMapping(value = "/profile")
+ public ResponseEntity updateProfileImage(
+ @RequestParam(value = "profile", required = false) MultipartFile multipartFile,
+ @AuthenticationPrincipal AppUser appUser
+ )
+ throws IOException {
+ ProfileResponse profileResponse = userService.updateProfile(multipartFile, appUser);
+ return ResponseEntity.ok(profileResponse);
+ }
+
+ @PutMapping("/me")
+ public ResponseEntity update(@Valid @RequestBody UserUpdateRequest userUpdateRequest,
+ @AuthenticationPrincipal AppUser appUser) {
+ UserResponse userResponse = userService.update(userUpdateRequest, appUser);
return ResponseEntity.ok(userResponse);
}
+
+ @PostMapping("/name-check")
+ public ResponseEntity checkDuplicateUserName(@Valid @RequestBody UserNameRequest userNameRequest,
+ @AuthenticationPrincipal AppUser appUser) {
+ userService.checkDuplicatedUserName(userNameRequest, appUser);
+ return ResponseEntity.ok().build();
+ }
}
diff --git a/backend/src/main/java/botobo/core/ui/WorkbookController.java b/backend/src/main/java/botobo/core/ui/WorkbookController.java
index ef91d491..4d0d456d 100644
--- a/backend/src/main/java/botobo/core/ui/WorkbookController.java
+++ b/backend/src/main/java/botobo/core/ui/WorkbookController.java
@@ -3,6 +3,7 @@
import botobo.core.application.WorkbookService;
import botobo.core.domain.user.AppUser;
import botobo.core.dto.card.ScrapCardRequest;
+import botobo.core.dto.heart.HeartResponse;
import botobo.core.dto.workbook.WorkbookCardResponse;
import botobo.core.dto.workbook.WorkbookRequest;
import botobo.core.dto.workbook.WorkbookResponse;
@@ -16,7 +17,6 @@
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@@ -56,24 +56,19 @@ public ResponseEntity updateWorkbook(@PathVariable Long id,
);
}
- @GetMapping("/public")
- public ResponseEntity> findPublicWorkbooksBySearch(@RequestParam String search) {
- return ResponseEntity.ok(
- workbookService.findPublicWorkbooksBySearch(search)
- );
- }
-
@GetMapping("/public/{id}")
- public ResponseEntity findPublicWorkbookById(@PathVariable Long id) {
+ public ResponseEntity findPublicWorkbookById(@PathVariable Long id,
+ @AuthenticationPrincipal AppUser appUser) {
return ResponseEntity.ok(
- workbookService.findPublicWorkbookById(id)
+ workbookService.findPublicWorkbookById(id, appUser)
);
}
@GetMapping("/{id}/cards")
- public ResponseEntity findWorkbookCardsById(@PathVariable Long id) {
+ public ResponseEntity findWorkbookCardsById(@PathVariable Long id,
+ @AuthenticationPrincipal AppUser appUser) {
return ResponseEntity.ok(
- workbookService.findWorkbookCardsById(id)
+ workbookService.findWorkbookCardsById(id, appUser)
);
}
@@ -91,4 +86,11 @@ public ResponseEntity scrapSelectedCardsToWorkbook(@PathVariable Long work
workbookService.scrapSelectedCardsToWorkbook(workbookId, scrapCardRequest, appUser);
return ResponseEntity.created(URI.create(String.format("/api/workbooks/%d/cards", workbookId))).build();
}
+
+ @PutMapping("/{workbookId}/hearts")
+ public ResponseEntity toggleHeart(@PathVariable Long workbookId, @AuthenticationPrincipal AppUser appUser) {
+ return ResponseEntity.ok(
+ workbookService.toggleHeart(workbookId, appUser)
+ );
+ }
}
diff --git a/backend/src/main/java/botobo/core/ui/auth/AdminInterceptor.java b/backend/src/main/java/botobo/core/ui/auth/AdminInterceptor.java
index 0b86cf14..21d4929f 100644
--- a/backend/src/main/java/botobo/core/ui/auth/AdminInterceptor.java
+++ b/backend/src/main/java/botobo/core/ui/auth/AdminInterceptor.java
@@ -2,7 +2,6 @@
import botobo.core.application.AuthService;
import botobo.core.infrastructure.AuthorizationExtractor;
-import org.springframework.web.cors.CorsUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
@@ -18,9 +17,6 @@ public AdminInterceptor(AuthService authService) {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- if (CorsUtils.isPreFlightRequest(request)) {
- return true;
- }
String credentials = AuthorizationExtractor.extract(request);
authService.validateAdmin(credentials);
return true;
diff --git a/backend/src/main/java/botobo/core/ui/auth/AuthController.java b/backend/src/main/java/botobo/core/ui/auth/AuthController.java
index 3e64136d..8b0feb4f 100644
--- a/backend/src/main/java/botobo/core/ui/auth/AuthController.java
+++ b/backend/src/main/java/botobo/core/ui/auth/AuthController.java
@@ -1,10 +1,10 @@
package botobo.core.ui.auth;
-
import botobo.core.application.AuthService;
import botobo.core.dto.auth.LoginRequest;
import botobo.core.dto.auth.TokenResponse;
import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -20,9 +20,9 @@ public AuthController(AuthService authService) {
this.authService = authService;
}
- @PostMapping
- public ResponseEntity login(@RequestBody LoginRequest loginRequest) {
- TokenResponse tokenResponse = authService.createToken(loginRequest);
+ @PostMapping("/{socialType}")
+ public ResponseEntity login(@PathVariable String socialType, @RequestBody LoginRequest loginRequest) {
+ TokenResponse tokenResponse = authService.createToken(socialType, loginRequest);
return ResponseEntity.ok(tokenResponse);
}
}
\ No newline at end of file
diff --git a/backend/src/main/java/botobo/core/ui/auth/AuthenticationPrincipalArgumentResolver.java b/backend/src/main/java/botobo/core/ui/auth/AuthenticationPrincipalArgumentResolver.java
index a75d1948..704562c6 100644
--- a/backend/src/main/java/botobo/core/ui/auth/AuthenticationPrincipalArgumentResolver.java
+++ b/backend/src/main/java/botobo/core/ui/auth/AuthenticationPrincipalArgumentResolver.java
@@ -9,6 +9,7 @@
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpServletRequest;
+import java.util.Objects;
public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
@@ -25,7 +26,7 @@ public boolean supportsParameter(MethodParameter parameter) {
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
- String credentials = AuthorizationExtractor.extract(webRequest.getNativeRequest(HttpServletRequest.class));
+ String credentials = AuthorizationExtractor.extract(Objects.requireNonNull(webRequest.getNativeRequest(HttpServletRequest.class)));
return authService.findAppUserByToken(credentials);
}
}
diff --git a/backend/src/main/java/botobo/core/ui/auth/AuthorizationInterceptor.java b/backend/src/main/java/botobo/core/ui/auth/AuthorizationInterceptor.java
index fa5dfb85..795c7eaf 100644
--- a/backend/src/main/java/botobo/core/ui/auth/AuthorizationInterceptor.java
+++ b/backend/src/main/java/botobo/core/ui/auth/AuthorizationInterceptor.java
@@ -2,7 +2,6 @@
import botobo.core.application.AuthService;
import botobo.core.infrastructure.AuthorizationExtractor;
-import org.springframework.web.cors.CorsUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
@@ -18,9 +17,6 @@ public AuthorizationInterceptor(AuthService authService) {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- if (CorsUtils.isPreFlightRequest(request)) {
- return true;
- }
String credentials = AuthorizationExtractor.extract(request);
authService.validateToken(credentials);
return true;
diff --git a/backend/src/main/java/botobo/core/ui/auth/PathMatcherInterceptor.java b/backend/src/main/java/botobo/core/ui/auth/PathMatcherInterceptor.java
new file mode 100644
index 00000000..49390012
--- /dev/null
+++ b/backend/src/main/java/botobo/core/ui/auth/PathMatcherInterceptor.java
@@ -0,0 +1,47 @@
+package botobo.core.ui.auth;
+
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class PathMatcherInterceptor implements HandlerInterceptor {
+ private final HandlerInterceptor handlerInterceptor;
+ private final PathPatterns pathPatterns;
+
+ public PathMatcherInterceptor(HandlerInterceptor handlerInterceptor) {
+ this.handlerInterceptor = handlerInterceptor;
+ this.pathPatterns = new PathPatterns();
+ }
+
+ public PathMatcherInterceptor addPathPatterns(String pathPattern, PathMethod... pathMethods) {
+ pathPatterns.addPathPatterns(pathPattern, pathMethods);
+ return this;
+ }
+
+ public PathMatcherInterceptor excludePathPatterns(String pathPattern, PathMethod... pathMethods) {
+ pathPatterns.excludePathPatterns(pathPattern, pathMethods);
+ return this;
+ }
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ String uri = request.getRequestURI();
+ String httpMethod = request.getMethod();
+ if (pathPatterns.isExcludedPath(uri, PathMethod.of(httpMethod))) {
+ return true;
+ }
+ return handlerInterceptor.preHandle(request, response, handler);
+ }
+
+ @Override
+ public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
+ handlerInterceptor.postHandle(request, response, handler, modelAndView);
+ }
+
+ @Override
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
+ handlerInterceptor.afterCompletion(request, response, handler, ex);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/ui/auth/PathMethod.java b/backend/src/main/java/botobo/core/ui/auth/PathMethod.java
new file mode 100644
index 00000000..cf58e470
--- /dev/null
+++ b/backend/src/main/java/botobo/core/ui/auth/PathMethod.java
@@ -0,0 +1,18 @@
+package botobo.core.ui.auth;
+
+import java.util.Arrays;
+
+public enum PathMethod {
+ GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, ANY;
+
+ public static PathMethod of(String httpMethod) {
+ return Arrays.stream(values())
+ .filter(pathMethod -> pathMethod.match(httpMethod))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 PathMethod 입니다."));
+ }
+
+ private boolean match(String httpMethod) {
+ return this.name().equals(httpMethod);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/ui/auth/PathMethods.java b/backend/src/main/java/botobo/core/ui/auth/PathMethods.java
new file mode 100644
index 00000000..f33adebf
--- /dev/null
+++ b/backend/src/main/java/botobo/core/ui/auth/PathMethods.java
@@ -0,0 +1,18 @@
+package botobo.core.ui.auth;
+
+import java.util.Set;
+
+public class PathMethods {
+ private final Set pathMethods;
+
+ public PathMethods(Set pathMethods) {
+ this.pathMethods = pathMethods;
+ }
+
+ public boolean contains(PathMethod pathMethod) {
+ if (pathMethods.contains(PathMethod.ANY)) {
+ return true;
+ }
+ return pathMethods.contains(pathMethod);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/ui/auth/PathPatterns.java b/backend/src/main/java/botobo/core/ui/auth/PathPatterns.java
new file mode 100644
index 00000000..e881704f
--- /dev/null
+++ b/backend/src/main/java/botobo/core/ui/auth/PathPatterns.java
@@ -0,0 +1,38 @@
+package botobo.core.ui.auth;
+
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.PathMatcher;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class PathPatterns {
+ private final Map includePatterns;
+ private final Map excludePatterns;
+ private final PathMatcher pathMatcher;
+
+ public PathPatterns() {
+ this.includePatterns = new HashMap<>();
+ this.excludePatterns = new HashMap<>();
+ this.pathMatcher = new AntPathMatcher();
+ }
+
+ public void addPathPatterns(String pathPattern, PathMethod... pathMethods) {
+ includePatterns.put(pathPattern, new PathMethods(Set.of(pathMethods)));
+ }
+
+ public void excludePathPatterns(String pathPattern, PathMethod... pathMethods) {
+ excludePatterns.put(pathPattern, new PathMethods(Set.of(pathMethods)));
+ }
+
+ public boolean isExcludedPath(String uri, PathMethod pathMethod) {
+ return matchPatterns(excludePatterns, uri, pathMethod) || !matchPatterns(includePatterns, uri, pathMethod);
+ }
+
+ private boolean matchPatterns(Map patterns, String uri, PathMethod pathMethod) {
+ return patterns.keySet().stream()
+ .filter(pattern -> pathMatcher.match(pattern, uri))
+ .anyMatch(pattern -> patterns.get(pattern).contains(pathMethod));
+ }
+}
diff --git a/backend/src/main/java/botobo/core/ui/search/SearchArgumentResolver.java b/backend/src/main/java/botobo/core/ui/search/SearchArgumentResolver.java
new file mode 100644
index 00000000..92e67088
--- /dev/null
+++ b/backend/src/main/java/botobo/core/ui/search/SearchArgumentResolver.java
@@ -0,0 +1,45 @@
+package botobo.core.ui.search;
+
+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;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+public class SearchArgumentResolver implements HandlerMethodArgumentResolver {
+
+ @Override
+ public boolean supportsParameter(MethodParameter parameter) {
+ return parameter.hasParameterAnnotation(SearchParams.class);
+ }
+
+ @Override
+ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
+ HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
+ Map parameterMap = extractMap(request);
+
+ return WorkbookSearchParameter.builder()
+ .searchType(parameterMap.getOrDefault("type", null))
+ .searchKeyword(parameterMap.getOrDefault("keyword", null))
+ .searchCriteria(parameterMap.getOrDefault("criteria", null))
+ .searchOrder(parameterMap.getOrDefault("order", null))
+ .start(parameterMap.getOrDefault("start", null))
+ .size(parameterMap.getOrDefault("size", null))
+ .build();
+ }
+
+ private Map extractMap(HttpServletRequest request) {
+ Map parameterMap = new HashMap<>();
+ Enumeration parameterNames = request.getParameterNames();
+ while (parameterNames.hasMoreElements()) {
+ String name = parameterNames.nextElement();
+ parameterMap.put(name, request.getParameter(name));
+ }
+ return parameterMap;
+ }
+}
diff --git a/backend/src/main/java/botobo/core/ui/search/SearchController.java b/backend/src/main/java/botobo/core/ui/search/SearchController.java
new file mode 100644
index 00000000..7e424b3f
--- /dev/null
+++ b/backend/src/main/java/botobo/core/ui/search/SearchController.java
@@ -0,0 +1,44 @@
+package botobo.core.ui.search;
+
+import botobo.core.application.SearchService;
+import botobo.core.dto.tag.TagResponse;
+import botobo.core.dto.user.SimpleUserResponse;
+import botobo.core.dto.workbook.WorkbookResponse;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/search")
+public class SearchController {
+
+ private final SearchService searchService;
+
+ public SearchController(SearchService searchService) {
+ this.searchService = searchService;
+ }
+
+ @GetMapping("/workbooks")
+ public ResponseEntity> searchWorkbooks(@SearchParams WorkbookSearchParameter workbookSearchParameter) {
+ List workbookResponses = searchService.searchWorkbooks(workbookSearchParameter);
+ return ResponseEntity.ok(workbookResponses);
+ }
+
+ @GetMapping("/tags")
+ public ResponseEntity> searchTags(@RequestParam String keyword) {
+ SearchKeyword searchKeyword = SearchKeyword.of(keyword);
+ List tagResponses = searchService.searchTags(searchKeyword);
+ return ResponseEntity.ok(tagResponses);
+ }
+
+ @GetMapping("/users")
+ public ResponseEntity> searchUsers(@RequestParam String keyword) {
+ SearchKeyword searchKeyword = SearchKeyword.of(keyword);
+ List userResponses = searchService.searchUsers(searchKeyword);
+ return ResponseEntity.ok(userResponses);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/ui/search/SearchCriteria.java b/backend/src/main/java/botobo/core/ui/search/SearchCriteria.java
new file mode 100644
index 00000000..2e21dc38
--- /dev/null
+++ b/backend/src/main/java/botobo/core/ui/search/SearchCriteria.java
@@ -0,0 +1,48 @@
+package botobo.core.ui.search;
+
+import botobo.core.domain.workbook.Workbook;
+import botobo.core.exception.search.InvalidSearchCriteriaException;
+import lombok.Getter;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.Expression;
+import javax.persistence.criteria.Root;
+import java.util.Arrays;
+import java.util.Objects;
+
+@Getter
+public enum SearchCriteria {
+ DATE("date"),
+ NAME("name"),
+ COUNT("count"),
+ HEART("heart");
+
+ private String value;
+
+ SearchCriteria(String value) {
+ this.value = value;
+ }
+
+ public static SearchCriteria of(String value) {
+ if (Objects.isNull(value)) {
+ return DATE;
+ }
+ return Arrays.stream(values())
+ .filter(searchCriteria -> searchCriteria.value.equalsIgnoreCase(value))
+ .findFirst()
+ .orElseThrow(InvalidSearchCriteriaException::new);
+ }
+
+ public Expression> toExpression(CriteriaBuilder builder, Root root) {
+ if (DATE.equals(this)) {
+ return root.get("createdAt");
+ }
+ if (NAME.equals(this)) {
+ return root.get("name");
+ }
+ if (COUNT.equals(this)) {
+ return builder.size(root.get("cards").get("cards"));
+ }
+ return builder.size(root.get("hearts").get("hearts"));
+ }
+}
diff --git a/backend/src/main/java/botobo/core/domain/workbook/criteria/SearchKeyword.java b/backend/src/main/java/botobo/core/ui/search/SearchKeyword.java
similarity index 52%
rename from backend/src/main/java/botobo/core/domain/workbook/criteria/SearchKeyword.java
rename to backend/src/main/java/botobo/core/ui/search/SearchKeyword.java
index 1718f87e..40d9ed99 100644
--- a/backend/src/main/java/botobo/core/domain/workbook/criteria/SearchKeyword.java
+++ b/backend/src/main/java/botobo/core/ui/search/SearchKeyword.java
@@ -1,6 +1,9 @@
-package botobo.core.domain.workbook.criteria;
+package botobo.core.ui.search;
-import botobo.core.exception.workbook.SearchKeywordCreationFailureException;
+import botobo.core.exception.search.ForbiddenSearchKeywordException;
+import botobo.core.exception.search.LongSearchKeywordException;
+import botobo.core.exception.search.SearchKeywordNullException;
+import botobo.core.exception.search.ShortSearchKeywordException;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@@ -10,6 +13,7 @@
@Getter
public class SearchKeyword {
+ private static final int KEYWORD_MIN_LENGTH = 1;
private static final int KEYWORD_MAX_LENGTH = 30;
private final String value;
@@ -24,33 +28,34 @@ private SearchKeyword(String value) {
private void validateNonNull(String value) {
if (Objects.isNull(value)) {
- throw new SearchKeywordCreationFailureException("검색어는 null일 수 없습니다.");
+ throw new SearchKeywordNullException();
}
}
private String refineValue(String value) {
- return value.trim()
- .replaceAll("(\\t|\r\n|\r|\n|\n\r)", " ")
- .replaceAll("[ ]+", " ");
+ return value.replaceAll("(\\t|\r\n|\r|\n|\n\r)", " ");
}
private void validateLength(String value) {
if (value.length() > KEYWORD_MAX_LENGTH) {
- throw new SearchKeywordCreationFailureException(
- String.format("검색어는 %d자 이하여야 합니다.", KEYWORD_MAX_LENGTH)
- );
+ throw new LongSearchKeywordException();
+ }
+ if (value.length() < KEYWORD_MIN_LENGTH) {
+ throw new ShortSearchKeywordException();
}
}
private void validateNotForbidden(String value) {
if (value.contains("바보")) {
- throw new SearchKeywordCreationFailureException(
- String.format("금지어를 입력했습니다. (%s)", value)
- );
+ throw new ForbiddenSearchKeywordException();
}
}
- public static SearchKeyword from(String value) {
+ public static SearchKeyword of(String value) {
return new SearchKeyword(value);
}
+
+ public String toLowercase() {
+ return value.toLowerCase();
+ }
}
diff --git a/backend/src/main/java/botobo/core/ui/search/SearchOrder.java b/backend/src/main/java/botobo/core/ui/search/SearchOrder.java
new file mode 100644
index 00000000..b8db27d2
--- /dev/null
+++ b/backend/src/main/java/botobo/core/ui/search/SearchOrder.java
@@ -0,0 +1,41 @@
+package botobo.core.ui.search;
+
+import botobo.core.domain.workbook.Workbook;
+import botobo.core.exception.search.InvalidSearchOrderException;
+import lombok.Getter;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.Order;
+import javax.persistence.criteria.Root;
+import java.util.Arrays;
+import java.util.Objects;
+
+@Getter
+public enum SearchOrder {
+
+ ASC("asc"),
+ DESC("desc");
+
+ private String value;
+
+ SearchOrder(String value) {
+ this.value = value;
+ }
+
+ public static SearchOrder of(String value) {
+ if (Objects.isNull(value)) {
+ return DESC;
+ }
+ return Arrays.stream(values())
+ .filter(searchOrder -> searchOrder.value.equalsIgnoreCase(value))
+ .findFirst()
+ .orElseThrow(InvalidSearchOrderException::new);
+ }
+
+ public Order toOrder(CriteriaBuilder builder, Root root, SearchCriteria searchCriteria) {
+ if (this.equals(ASC)) {
+ return builder.asc(searchCriteria.toExpression(builder, root));
+ }
+ return builder.desc(searchCriteria.toExpression(builder, root));
+ }
+}
diff --git a/backend/src/main/java/botobo/core/ui/search/SearchParams.java b/backend/src/main/java/botobo/core/ui/search/SearchParams.java
new file mode 100644
index 00000000..d21dad50
--- /dev/null
+++ b/backend/src/main/java/botobo/core/ui/search/SearchParams.java
@@ -0,0 +1,11 @@
+package botobo.core.ui.search;
+
+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 SearchParams {
+}
diff --git a/backend/src/main/java/botobo/core/ui/search/SearchType.java b/backend/src/main/java/botobo/core/ui/search/SearchType.java
new file mode 100644
index 00000000..d8fc2ddd
--- /dev/null
+++ b/backend/src/main/java/botobo/core/ui/search/SearchType.java
@@ -0,0 +1,62 @@
+package botobo.core.ui.search;
+
+import botobo.core.domain.workbook.Workbook;
+import botobo.core.exception.search.InvalidSearchTypeException;
+import lombok.Getter;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.Expression;
+import javax.persistence.criteria.Root;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.function.Function;
+
+@Getter
+public enum SearchType {
+ NAME(
+ "name",
+ root -> root.get("name"),
+ keyword -> "%" + keyword.toLowerCase() + "%"
+ ),
+ TAG(
+ "tag",
+ root -> root.join("workbookTags").get("tag").get("tagName").get("value"),
+ String::toLowerCase
+ ),
+ USER(
+ "user",
+ root -> root.get("user").get("userName"),
+ keyword -> keyword
+ );
+
+ private final String value;
+ private final Function, Expression> target;
+ private final Function pattern;
+
+ SearchType(String value, Function, Expression> target, Function pattern) {
+ this.value = value;
+ this.target = target;
+ this.pattern = pattern;
+ }
+
+ public static SearchType of(String value) {
+ if (Objects.isNull(value)) {
+ return NAME;
+ }
+ return Arrays.stream(values())
+ .filter(searchType -> searchType.value.equalsIgnoreCase(value))
+ .findFirst()
+ .orElseThrow(InvalidSearchTypeException::new);
+ }
+
+ public Expression toExpression(Root root, CriteriaBuilder builder) {
+ if (this.equals(NAME)) {
+ return builder.lower(target.apply(root));
+ }
+ return target.apply(root);
+ }
+
+ public String toPattern(String value) {
+ return pattern.apply(value);
+ }
+}
diff --git a/backend/src/main/java/botobo/core/ui/search/WorkbookSearchParameter.java b/backend/src/main/java/botobo/core/ui/search/WorkbookSearchParameter.java
new file mode 100644
index 00000000..14f7f2b5
--- /dev/null
+++ b/backend/src/main/java/botobo/core/ui/search/WorkbookSearchParameter.java
@@ -0,0 +1,113 @@
+package botobo.core.ui.search;
+
+import botobo.core.domain.workbook.Workbook;
+import botobo.core.exception.search.InvalidPageSizeException;
+import botobo.core.exception.search.InvalidPageStartException;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.jpa.domain.Specification;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.Expression;
+import javax.persistence.criteria.Order;
+import javax.persistence.criteria.Root;
+
+@EqualsAndHashCode
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public class WorkbookSearchParameter {
+
+ private static final int MINIMUM_START_PAGE = 0;
+ private static final int DEFAULT_START_PAGE = 0;
+ private static final int MINIMUM_PAGE_SIZE = 1;
+ private static final int MAXIMUM_PAGE_SIZE = 100;
+ private static final int DEFAULT_PAGE_SIZE = 20;
+
+ private SearchType searchType;
+ private SearchKeyword searchKeyword;
+ private SearchCriteria searchCriteria;
+ private SearchOrder searchOrder;
+ private int start;
+ private int size;
+
+ @Builder
+ private WorkbookSearchParameter(String searchType, String searchCriteria, String searchOrder, String searchKeyword, String start, String size) {
+ this.start = initializeStartValue(start);
+ this.size = initializeSizeValue(size);
+ this.searchType = SearchType.of(searchType);
+ this.searchKeyword = SearchKeyword.of(searchKeyword);
+ this.searchCriteria = SearchCriteria.of(searchCriteria);
+ this.searchOrder = SearchOrder.of(searchOrder);
+ }
+
+ private int initializeStartValue(String start) {
+ try {
+ int value = Integer.parseInt(start);
+ if (value < MINIMUM_START_PAGE) {
+ throw new InvalidPageStartException();
+ }
+ return value;
+ } catch (NumberFormatException e) {
+ return DEFAULT_START_PAGE;
+ }
+ }
+
+ private int initializeSizeValue(String size) {
+ try {
+ int value = Integer.parseInt(size);
+ if (value < MINIMUM_PAGE_SIZE || value > MAXIMUM_PAGE_SIZE) {
+ throw new InvalidPageSizeException();
+ }
+ return value;
+ } catch (NumberFormatException e) {
+ return DEFAULT_PAGE_SIZE;
+ }
+ }
+
+ public Specification toSpecification() {
+ return Specification.where(matchesKeyword())
+ .and(orderByCriteria())
+ .and(containsAtLeastOneCard());
+ }
+
+ private Specification matchesKeyword() {
+ return (root, query, builder) -> builder.like(
+ expression(root, builder),
+ pattern()
+ );
+ }
+
+ private Expression expression(Root root, CriteriaBuilder builder) {
+ return searchType.toExpression(root, builder);
+ }
+
+ private String pattern() {
+ return searchType.toPattern(searchKeyword.getValue());
+ }
+
+ private Specification orderByCriteria() {
+ return (root, query, builder) -> {
+ Order order = searchOrder.toOrder(builder, root, searchCriteria);
+ query.orderBy(order);
+ return null;
+ };
+ }
+
+ private Specification containsAtLeastOneCard() {
+ return (root, query, builder) -> builder
+ .greaterThan(
+ builder.size(root.get("cards").get("cards")),
+ 0
+ );
+ }
+
+ public PageRequest toPageRequest() {
+ return PageRequest.of(start, size);
+ }
+}
diff --git a/backend/src/main/resources/application-test.yml b/backend/src/main/resources/application-test.yml
index 1427b8ab..285f5af5 100644
--- a/backend/src/main/resources/application-test.yml
+++ b/backend/src/main/resources/application-test.yml
@@ -13,17 +13,50 @@ spring:
config:
activate:
on-profile: test
+ flyway:
+ enabled: false
+
+logging:
+ level:
+ com:
+ amazonaws:
+ util:
+ EC2MetadataUtils: error
github:
client:
- id: a
- secret: a
+ id: githubId
+ secret: githubSecret
url:
access-token: https://github.com/login/oauth/access_token
profile: https://api.github.com/user
+google:
+ client:
+ id: googleId
+ secret: googleSecret
+ redirect-uri: googleRedirectUri
+ grant-type: authorization_code
+ url:
+ access-token: https://oauth2.googleapis.com/token
+ profile: https://openidconnect.googleapis.com/v1/userinfo
+
security:
jwt:
token:
secret-key: testsecretkey
- expire-length: 3600000
\ No newline at end of file
+ expire-length: 3600000
+
+aws:
+ cloudfront:
+ url-format: https://d1mlkr1uzdb8as.cloudfront.net/%s
+ s3:
+ bucket: s3-botobo-storage
+ user-default-image: botobo-default-profile.png
+
+cloud:
+ aws:
+ region:
+ static: ap-northeast-2
+ stack:
+ auto: false
\ No newline at end of file
diff --git a/backend/src/main/resources/db/migration/V1__init.sql b/backend/src/main/resources/db/migration/V1__init.sql
new file mode 100644
index 00000000..e77d6f51
--- /dev/null
+++ b/backend/src/main/resources/db/migration/V1__init.sql
@@ -0,0 +1,52 @@
+CREATE TABLE user(
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ github_id BIGINT NOT NULL,
+ user_name VARCHAR(255) NOT NULL,
+ profile_url VARCHAR(255) NOT NULL,
+ role VARCHAR(255) NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+);
+
+CREATE TABLE workbook(
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(30) NOT NULL,
+ opened TINYINT(1) NOT NULL,
+ deleted TINYINT(1) NOT NULL,
+ user_id BIGINT not null,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY(user_id) REFERENCES user(id) ON UPDATE CASCADE ON DELETE RESTRICT
+);
+
+CREATE TABLE card(
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ question BLOB NOT NULL,
+ answer BLOB NOT NULL,
+ encounter_count INT NOT NULL,
+ bookmark TINYINT(1) not null,
+ next_quiz TINYINT(1) not null,
+ workbook_id BIGINT not null,
+ deleted TINYINT(1) not null,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY(workbook_id) REFERENCES workbook(id) ON UPDATE CASCADE ON DELETE RESTRICT
+);
+
+CREATE TABLE tag(
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(30) UNIQUE NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+);
+
+CREATE TABLE workbook_tag(
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ workbook_id BIGINT not null,
+ tag_id BIGINT not null,
+ deleted TINYINT(1) not null,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY(workbook_id) REFERENCES workbook(id) ON UPDATE CASCADE ON DELETE RESTRICT,
+ FOREIGN KEY(tag_id) REFERENCES tag(id) ON UPDATE CASCADE ON DELETE RESTRICT
+);
\ No newline at end of file
diff --git a/backend/src/main/resources/db/migration/V2__change_user_column.sql b/backend/src/main/resources/db/migration/V2__change_user_column.sql
new file mode 100644
index 00000000..e17e5754
--- /dev/null
+++ b/backend/src/main/resources/db/migration/V2__change_user_column.sql
@@ -0,0 +1,3 @@
+alter table user change github_id social_id varchar(255) not null;
+alter table user add column bio varchar(255) not null default '';
+alter table user add column social_type varchar(255);
\ No newline at end of file
diff --git a/backend/src/main/resources/db/migration/V3__add_heart_table.sql b/backend/src/main/resources/db/migration/V3__add_heart_table.sql
new file mode 100644
index 00000000..ddc69c48
--- /dev/null
+++ b/backend/src/main/resources/db/migration/V3__add_heart_table.sql
@@ -0,0 +1,8 @@
+CREATE TABLE heart(
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ workbook_id BIGINT not null,
+ user_id BIGINT not null,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY(workbook_id) REFERENCES workbook(id) ON UPDATE CASCADE ON DELETE RESTRICT
+);
\ No newline at end of file
diff --git a/backend/src/main/resources/import.sql b/backend/src/main/resources/import.sql
deleted file mode 100644
index b51d055d..00000000
--- a/backend/src/main/resources/import.sql
+++ /dev/null
@@ -1,72 +0,0 @@
--- -- INSERT INTO category(name, is_deleted, logo_url, description) VALUES ('JAVA', false, 'logo1', 'java입니다.');
--- -- INSERT INTO category(name, is_deleted, logo_url, description) VALUES ('Javascript', false, 'logo2', 'javascript 입니다');
--- INSERT INTO category(name, is_deleted, logo_url, description) VALUES ('네트워크', false, 'logo3', '네트워크 입니다.');
--- --
--- INSERT INTO card(question, is_deleted, category_id) VALUES ('오즈 질문 1', false,31);
--- INSERT INTO card(question, is_deleted, category_id) VALUES ('오즈 질문 2', false,32);
--- INSERT INTO card(question, is_deleted, category_id) VALUES ('오즈 질문 3', false,33);
--- -- INSERT INTO card(question, is_deleted, category_id) VALUES ('Java 질문 2', false,1);
--- -- INSERT INTO card(question, is_deleted, category_id) VALUES ('Java 질문 3', false,1);
--- -- INSERT INTO card(question, is_deleted, category_id) VALUES ('Java 질문 4', false,1);
--- -- INSERT INTO card(question, is_deleted, category_id) VALUES ('Java 질문 5', false,1);
--- -- INSERT INTO card(question, is_deleted, category_id) VALUES ('Java 질문 6', false,1);
--- -- INSERT INTO card(question, is_deleted, category_id) VALUES ('Java 질문 7', false,1);
--- -- INSERT INTO card(question, is_deleted, category_id) VALUES ('Java 질문 8', false,1);
--- -- INSERT INTO card(question, is_deleted, category_id) VALUES ('Java 질문 9', false,1);
--- -- INSERT INTO card(question, is_deleted, category_id) VALUES ('Java 질문 10',false, 1);
--- --
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Javascript 질문 1', false,2);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Javascript 질문 2', false,2);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Javascript 질문 3', false,2);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Javascript 질문 4', false,2);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Javascript 질문 5', false,2);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Javascript 질문 6', false,2);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Javascript 질문 7', false,2);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Javascript 질문 8', false,2);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Javascript 질문 9', false,2);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Javascript 질문 10',false, 2);
--- --
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Spring 질문 1', false,3);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Spring 질문 2', false,3);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Spring 질문 3', false,3);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Spring 질문 4', false,3);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Spring 질문 5', false,3);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Spring 질문 6', false,3);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Spring 질문 7', false,3);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Spring 질문 8', false,3);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Spring 질문 9', false,3);
--- -- INSERT INTO card(question, is_deleted,category_id) VALUES ('Spring 질문 10',false, 3);
--- --
--- INSERT INTO answer(content,is_deleted, card_id) VALUES ('오즈 답 1', false,31);
--- INSERT INTO answer(content,is_deleted, card_id) VALUES ('오즈 답 2', false,32);
--- INSERT INTO answer(content,is_deleted, card_id) VALUES ('오즈 답 3', false,33);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Java 답 4', false,4);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Java 답 5', false,5);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Java 답 6', false,6);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Java 답 7', false,7);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Java 답 8', false,8);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Java 답 9', false,9);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Java 답 10',false, 10);
--- --
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Javascript 답 1', false,11);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Javascript 답 2', false,12);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Javascript 답 3', false,13);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Javascript 답 4', false,14);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Javascript 답 5', false,15);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Javascript 답 6', false,16);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Javascript 답 7', false,17);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Javascript 답 8', false,18);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Javascript 답 9', false,19);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Javascript 답 10',false,20);
--- --
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Spring 답 1', false,21);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Spring 답 2', false,22);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Spring 답 3', false,23);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Spring 답 4', false,24);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Spring 답 5', false,25);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Spring 답 6', false,26);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Spring 답 7', false,27);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Spring 답 8', false,28);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Spring 답 9', false,29);
--- -- INSERT INTO answer(content,is_deleted, card_id) VALUES ('Spring 답 10',false, 30);
--- --
diff --git a/backend/src/test/java/botobo/core/AcceptanceTest.java b/backend/src/test/java/botobo/core/AcceptanceTest.java
deleted file mode 100644
index 78056536..00000000
--- a/backend/src/test/java/botobo/core/AcceptanceTest.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package botobo.core;
-
-import io.restassured.RestAssured;
-import org.junit.jupiter.api.BeforeEach;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.web.server.LocalServerPort;
-import org.springframework.test.annotation.DirtiesContext;
-import org.springframework.test.context.ActiveProfiles;
-
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
-@ActiveProfiles("test")
-@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
-public class AcceptanceTest {
- @LocalServerPort
- int port;
-
- @BeforeEach
- public void setUp() {
- RestAssured.port = port;
- }
-}
diff --git a/backend/src/test/java/botobo/core/acceptance/AcceptanceTest.java b/backend/src/test/java/botobo/core/acceptance/AcceptanceTest.java
index b0b9dad7..7273c0d0 100644
--- a/backend/src/test/java/botobo/core/acceptance/AcceptanceTest.java
+++ b/backend/src/test/java/botobo/core/acceptance/AcceptanceTest.java
@@ -8,24 +8,31 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
-import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
+import static io.restassured.RestAssured.UNDEFINED_PORT;
+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
-@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
@ActiveProfiles("test")
public class AcceptanceTest {
@LocalServerPort
private int port;
+ @Autowired
+ private DatabaseCleaner databaseCleaner;
+
private RequestBuilder requestBuilder;
@Autowired
- private JwtTokenProvider jwtTokenProvider;
+ protected JwtTokenProvider jwtTokenProvider;
@BeforeEach
- public void setUp() {
- RestAssured.port = port;
+ protected void setUp() {
+ if (RestAssured.port == UNDEFINED_PORT) {
+ RestAssured.port = port;
+ databaseCleaner.afterPropertiesSet();
+ }
+ databaseCleaner.execute();
String defaultToken = jwtTokenProvider.createToken(100L);
requestBuilder = new RequestBuilder(defaultToken);
}
@@ -36,13 +43,18 @@ public void setUp() {
* request()
* .get(path, params) http method type
* .queryParam(name, value) optional
- * .auth() default: false
+ * .auth(createToken(1L)) default: false
* .log() default: false
* .build();
* <로그인이 필요하지 않은 경우>
* request()
* .post(path, body) http method type
* .build();
+ * <등록되지 않은 유저가 필요한 경우>
+ * request()
+ * .post(path, body) http method type
+ * .failAuth()
+ * .build();
*/
protected HttpFunction request() {
return requestBuilder.build();
diff --git a/backend/src/test/java/botobo/core/acceptance/AuthAcceptanceTest.java b/backend/src/test/java/botobo/core/acceptance/AuthAcceptanceTest.java
index 32fdc7b1..1d72697c 100644
--- a/backend/src/test/java/botobo/core/acceptance/AuthAcceptanceTest.java
+++ b/backend/src/test/java/botobo/core/acceptance/AuthAcceptanceTest.java
@@ -1,50 +1,70 @@
package botobo.core.acceptance;
-import botobo.core.dto.auth.GithubUserInfoResponse;
+import botobo.core.acceptance.utils.RequestBuilder;
+import botobo.core.domain.user.SocialType;
import botobo.core.dto.auth.LoginRequest;
-import botobo.core.dto.auth.TokenResponse;
-import botobo.core.infrastructure.GithubOauthManager;
-import io.restassured.response.ExtractableResponse;
-import io.restassured.response.Response;
+import botobo.core.exception.common.ErrorResponse;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.HttpStatus;
+import static botobo.core.utils.Fixture.oz;
+import static botobo.core.utils.Fixture.pk;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
@DisplayName("Auth Acceptance 테스트")
-public class AuthAcceptanceTest extends AcceptanceTest {
+public class AuthAcceptanceTest extends DomainAcceptanceTest {
- @MockBean
- private GithubOauthManager githubOauthManager;
+ @Test
+ @DisplayName("깃헙 로그인을 한다 - 성공")
+ void loginWithGithub() {
+ // given, when
+ final String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
- protected TokenResponse 로그인되어_있음() {
- ExtractableResponse response = 로그인_요청();
- return response.as(TokenResponse.class);
+ // then
+ assertThat(accessToken).isNotNull();
+ then(githubOauthManager)
+ .should(times(1))
+ .getUserInfo(any());
+ then(googleOauthManager)
+ .should(never())
+ .getUserInfo(any());
}
- private ExtractableResponse 로그인_요청() {
- LoginRequest loginRequest = new LoginRequest("githubCode");
- GithubUserInfoResponse githubUserInfoResponse = GithubUserInfoResponse.builder()
- .userName("githubUser")
- .githubId(2L)
- .profileUrl("github.io")
- .build();
-
- given(githubOauthManager.getUserInfoFromGithub(any())).willReturn(githubUserInfoResponse);
+ @Test
+ @DisplayName("구글 로그인을 한다 - 성공")
+ void loginWithGoogle() {
+ // given, when
+ final String accessToken = 소셜_로그인되어_있음(oz, SocialType.GOOGLE);
- return request()
- .post("/api/login", loginRequest)
- .build()
- .extract();
+ // then
+ assertThat(accessToken).isNotNull();
+ then(githubOauthManager)
+ .should(never())
+ .getUserInfo(any());
+ then(googleOauthManager)
+ .should(times(1))
+ .getUserInfo(any());
}
@Test
- @DisplayName("로그인을 한다 - 성공")
- void login() {
- final TokenResponse tokenResponse = 로그인되어_있음();
- assertThat(tokenResponse.getAccessToken()).isNotNull();
+ @DisplayName("소셜 로그인을 한다 - 실패, 존재하지 않는 SocialType일 경우")
+ void loginWithSocial() {
+ // given
+ LoginRequest loginRequest = new LoginRequest("code");
+
+ // when
+ final RequestBuilder.HttpResponse response = request()
+ .post("/api/login/{socialType}", loginRequest, "kakao")
+ .build();
+
+ // then
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+ assertThat(errorResponse.getMessage()).isEqualTo("존재하지 않는 소셜 로그인 방식입니다.");
}
}
diff --git a/backend/src/test/java/botobo/core/acceptance/DatabaseCleaner.java b/backend/src/test/java/botobo/core/acceptance/DatabaseCleaner.java
new file mode 100644
index 00000000..ea7743de
--- /dev/null
+++ b/backend/src/test/java/botobo/core/acceptance/DatabaseCleaner.java
@@ -0,0 +1,48 @@
+package botobo.core.acceptance;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.testcontainers.shaded.com.google.common.base.CaseFormat;
+
+import javax.persistence.Entity;
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Component
+@Profile("test")
+public class DatabaseCleaner implements InitializingBean {
+
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ private List tableNames;
+
+ @Override
+ public void afterPropertiesSet() {
+ tableNames = entityManager.getMetamodel().getEntities().stream()
+ .filter(entityType -> Objects.nonNull(entityType.getJavaType().getAnnotation(Entity.class)))
+ .map(entityType -> CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, entityType.getName()))
+ .collect(Collectors.toList());
+ }
+
+ @Transactional
+ public void execute() {
+ entityManager.flush();
+ entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();
+ tableNames.forEach(
+ tableName -> executeQueryWithTable(tableName)
+ );
+ entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
+ }
+
+ private void executeQueryWithTable(String tableName) {
+ entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
+ entityManager.createNativeQuery("ALTER TABLE " + tableName + " ALTER COLUMN "
+ + "ID RESTART WITH 1").executeUpdate();
+ }
+}
diff --git a/backend/src/test/java/botobo/core/acceptance/DomainAcceptanceTest.java b/backend/src/test/java/botobo/core/acceptance/DomainAcceptanceTest.java
index bfa17634..2966938b 100644
--- a/backend/src/test/java/botobo/core/acceptance/DomainAcceptanceTest.java
+++ b/backend/src/test/java/botobo/core/acceptance/DomainAcceptanceTest.java
@@ -1,49 +1,68 @@
package botobo.core.acceptance;
-import botobo.core.acceptance.utils.RequestBuilder;
-import botobo.core.domain.card.CardRepository;
+import botobo.core.acceptance.utils.RequestBuilder.HttpResponse;
import botobo.core.domain.user.Role;
+import botobo.core.domain.user.SocialType;
import botobo.core.domain.user.User;
import botobo.core.domain.user.UserRepository;
import botobo.core.dto.admin.AdminCardRequest;
import botobo.core.dto.admin.AdminWorkbookRequest;
+import botobo.core.dto.auth.LoginRequest;
+import botobo.core.dto.auth.TokenResponse;
+import botobo.core.dto.auth.UserInfoResponse;
import botobo.core.dto.card.CardRequest;
import botobo.core.dto.card.CardResponse;
-import botobo.core.infrastructure.JwtTokenProvider;
+import botobo.core.dto.tag.TagRequest;
+import botobo.core.dto.workbook.WorkbookCardResponse;
+import botobo.core.dto.workbook.WorkbookRequest;
+import botobo.core.dto.workbook.WorkbookResponse;
+import botobo.core.infrastructure.GithubOauthManager;
+import botobo.core.infrastructure.GoogleOauthManager;
+import botobo.core.infrastructure.OauthManagerFactory;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import java.util.Collections;
import java.util.List;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+
public class DomainAcceptanceTest extends AcceptanceTest {
- @Autowired
- private UserRepository userRepository;
+ @MockBean
+ protected GithubOauthManager githubOauthManager;
- @Autowired
- private CardRepository cardRepository;
+ @MockBean
+ protected GoogleOauthManager googleOauthManager;
@Autowired
- protected JwtTokenProvider jwtTokenProvider;
+ protected UserRepository userRepository;
+
+ @MockBean
+ private OauthManagerFactory oauthManagerFactory;
- protected User user;
+ protected User admin;
+ @Override
@BeforeEach
- void setUser() {
- user = User.builder()
- .githubId(1L)
+ protected void setUp() {
+ super.setUp();
+ admin = User.builder()
+ .socialId("1")
.userName("admin")
.profileUrl("github.io")
.role(Role.ADMIN)
.build();
- userRepository.save(user);
+ userRepository.save(admin);
}
protected User anyUser() {
User anyUser = User.builder()
- .githubId(1L)
+ .socialId("2")
.userName("joanne")
.profileUrl("github.io")
.role(Role.USER)
@@ -54,7 +73,7 @@ protected User anyUser() {
public ExtractableResponse 문제집_생성_요청(AdminWorkbookRequest adminWorkbookRequest) {
return request()
.post("/api/admin/workbooks", adminWorkbookRequest)
- .auth(jwtTokenProvider.createToken(user.getId()))
+ .auth(jwtTokenProvider.createToken(admin.getId()))
.build()
.extract();
}
@@ -69,34 +88,97 @@ protected User anyUser() {
for (AdminCardRequest adminCardRequest : adminCardRequests) {
request()
.post("/api/admin/cards", adminCardRequest)
- .auth(jwtTokenProvider.createToken(user.getId()))
+ .auth(jwtTokenProvider.createToken(admin.getId()))
.build()
.extract();
}
}
- // TODO 카드 문서화 테스트 추가 이슈에서 카드 테스트 리팩토링 전체까지 진행할 예정!
- public CardResponse 카드_등록되어_있음(String question, String answer, Long workbookId, Long userId) {
+ public CardResponse 유저_카드_등록되어_있음(String question, String answer, Long workbookId, String accessToken) {
CardRequest cardRequest = CardRequest.builder()
.question(question)
.answer(answer)
.workbookId(workbookId)
.build();
- return 카드_등록되어있음(cardRequest, userId);
+ return 유저_카드_등록되어_있음(cardRequest, accessToken);
}
- private CardResponse 카드_등록되어있음(CardRequest cardRequest, Long userId) {
- return 카드_생성_요청(cardRequest, userId).convertBody(CardResponse.class);
+ private CardResponse 유저_카드_등록되어_있음(CardRequest cardRequest, String accessToken) {
+ return 유저_카드_생성_요청(cardRequest, accessToken).convertBody(CardResponse.class);
}
- private RequestBuilder.HttpResponse 카드_생성_요청(CardRequest cardRequest, Long userId) {
+ public HttpResponse 유저_카드_생성_요청(CardRequest cardRequest, String accessToken) {
return request()
.post("/api/cards", cardRequest)
- .auth(createToken(userId))
+ .auth(accessToken)
+ .build();
+ }
+
+ protected WorkbookResponse 유저_문제집_등록되어_있음(String name, boolean opened, List tags, String accessToken) {
+ WorkbookRequest workbookRequest = WorkbookRequest.builder()
+ .name(name)
+ .opened(opened)
+ .tags(tags)
+ .build();
+ return 유저_문제집_등록되어_있음(workbookRequest, accessToken);
+ }
+
+ protected WorkbookResponse 유저_문제집_등록되어_있음(WorkbookRequest workbookRequest, String accessToken) {
+ return 유저_문제집_생성_요청(workbookRequest, accessToken).convertBody(WorkbookResponse.class);
+ }
+
+ protected HttpResponse 유저_문제집_생성_요청(WorkbookRequest workbookRequest, String accessToken) {
+ return request()
+ .post("/api/workbooks", workbookRequest)
+ .auth(accessToken)
+ .build();
+ }
+
+ protected WorkbookResponse 유저_태그_포함_문제집_등록되어_있음(String name, boolean opened, String accessToken) {
+ List tagRequests = Collections.singletonList(
+ TagRequest.builder().id(1L).name("자바").build()
+ );
+ return 유저_문제집_등록되어_있음(name, opened, tagRequests, accessToken);
+ }
+
+ protected WorkbookCardResponse 문제집의_카드_모아보기(Long workbookId) {
+ HttpResponse response = request()
+ .get("/api/workbooks/{id}/cards", workbookId)
+ .auth(createToken(1L))
.build();
+ return response.convertBody(WorkbookCardResponse.class);
}
protected String createToken(Long id) {
return jwtTokenProvider.createToken(id);
}
+
+ protected HttpResponse 하트_토글_요청(Long workbookId, String accessToken) {
+ return request()
+ .putWithoutBody("/api/workbooks/{workbookId}/hearts", workbookId)
+ .auth(accessToken)
+ .build();
+ }
+
+ protected String 소셜_로그인되어_있음(UserInfoResponse userInfo, SocialType socialType) {
+ ExtractableResponse response = 소셜_로그인_요청(userInfo, socialType);
+ return response.as(TokenResponse.class).getAccessToken();
+ }
+
+ protected ExtractableResponse 소셜_로그인_요청(UserInfoResponse userInfo, SocialType socialType) {
+ LoginRequest loginRequest = new LoginRequest("code");
+
+ if (socialType == SocialType.GITHUB) {
+ given(oauthManagerFactory.findOauthMangerBySocialType(socialType)).willReturn(githubOauthManager);
+ given(githubOauthManager.getUserInfo(any())).willReturn(userInfo.toUser());
+ } else {
+ given(oauthManagerFactory.findOauthMangerBySocialType(socialType)).willReturn(googleOauthManager);
+ given(googleOauthManager.getUserInfo(any())).willReturn(userInfo.toUser());
+ }
+
+ return request()
+ .post("/api/login/{socialType}", loginRequest, socialType)
+ .build()
+ .extract();
+ }
}
diff --git a/backend/src/test/java/botobo/core/acceptance/admin/AdminAcceptanceTest.java b/backend/src/test/java/botobo/core/acceptance/admin/AdminAcceptanceTest.java
index c9c066b8..731a8ba3 100644
--- a/backend/src/test/java/botobo/core/acceptance/admin/AdminAcceptanceTest.java
+++ b/backend/src/test/java/botobo/core/acceptance/admin/AdminAcceptanceTest.java
@@ -1,6 +1,6 @@
package botobo.core.acceptance.admin;
-import botobo.core.acceptance.AcceptanceTest;
+import botobo.core.acceptance.DomainAcceptanceTest;
import botobo.core.domain.user.Role;
import botobo.core.domain.user.User;
import botobo.core.domain.user.UserRepository;
@@ -8,11 +8,10 @@
import botobo.core.dto.admin.AdminCardResponse;
import botobo.core.dto.admin.AdminWorkbookRequest;
import botobo.core.dto.admin.AdminWorkbookResponse;
-import botobo.core.exception.ErrorResponse;
+import botobo.core.exception.common.ErrorResponse;
import botobo.core.infrastructure.JwtTokenProvider;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -23,7 +22,7 @@
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("Admin 인수 테스트")
-public class AdminAcceptanceTest extends AcceptanceTest {
+public class AdminAcceptanceTest extends DomainAcceptanceTest {
private static final AdminWorkbookRequest ADMIN_WORKBOOK_REQUEST =
new AdminWorkbookRequest("관리자의 문제집");
@@ -34,25 +33,10 @@ public class AdminAcceptanceTest extends AcceptanceTest {
@Autowired
private JwtTokenProvider jwtTokenProvider;
- private User admin;
-
- @BeforeEach
- void setUser() {
- admin = User.builder()
- .githubId(1L)
- .userName("admin")
- .profileUrl("github.io")
- .role(Role.ADMIN)
- .build();
- userRepository.save(admin);
- }
-
-
@Test
@DisplayName("관리자 문제집 생성 - 성공")
void createWorkbook() {
// given
-
final ExtractableResponse response = 문제집_생성_요청(ADMIN_WORKBOOK_REQUEST, admin);
// when
@@ -76,7 +60,7 @@ void createWorkbookWithNullName() {
//then
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value());
- assertThat(errorResponse.getMessage()).isEqualTo("문제집명은 필수 입력값입니다.");
+ assertThat(errorResponse.getMessage()).isEqualTo("문제집 이름은 필수 입력값입니다.");
}
@@ -92,7 +76,7 @@ void createWorkbookWithInvalidLengthWithZero() {
//then
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value());
- assertThat(errorResponse.getMessage()).isEqualTo("문제집명은 필수 입력값입니다.");
+ assertThat(errorResponse.getMessage()).isEqualTo("문제집 이름은 필수 입력값입니다.");
}
@Test
@@ -107,7 +91,7 @@ void createWorkbookWithOnlyWhiteSpace() {
//then
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value());
- assertThat(errorResponse.getMessage()).isEqualTo("문제집명은 필수 입력값입니다.");
+ assertThat(errorResponse.getMessage()).isEqualTo("문제집 이름은 필수 입력값입니다.");
}
@Test
@@ -123,7 +107,7 @@ void createWorkbookWithInvalidLengthWith31() {
//then
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value());
- assertThat(errorResponse.getMessage()).isEqualTo("문제집명은 최소 1글자, 최대 30글자만 가능합니다.");
+ assertThat(errorResponse.getMessage()).isEqualTo("문제집 이름은 30자 이하여야 합니다.");
}
@Test
@@ -131,7 +115,7 @@ void createWorkbookWithInvalidLengthWith31() {
void createWorkbookWithNotAdmin() {
//given
User newUser = User.builder()
- .githubId(2L)
+ .socialId("2")
.userName("user")
.profileUrl("github.io")
.role(Role.USER)
@@ -213,7 +197,7 @@ void createCardWithNullWorkbookId() {
//then
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value());
- assertThat(errorResponse.getMessage()).isEqualTo("카드가 포함될 문제집 아이디는 필수 입력값입니다.");
+ assertThat(errorResponse.getMessage()).isEqualTo("문제집 아이디는 필수 입력값입니다.");
}
@Test
@@ -224,7 +208,7 @@ void createCardWithNotAdmin() {
final Long workbookId = extractId(workbookResponse);
User newUser = User.builder()
- .githubId(2L)
+ .socialId("2")
.userName("user")
.profileUrl("github.io")
.role(Role.USER)
diff --git a/backend/src/test/java/botobo/core/acceptance/card/CardAcceptanceTest.java b/backend/src/test/java/botobo/core/acceptance/card/CardAcceptanceTest.java
index 909e2c48..8be19079 100644
--- a/backend/src/test/java/botobo/core/acceptance/card/CardAcceptanceTest.java
+++ b/backend/src/test/java/botobo/core/acceptance/card/CardAcceptanceTest.java
@@ -2,12 +2,13 @@
import botobo.core.acceptance.DomainAcceptanceTest;
import botobo.core.acceptance.utils.RequestBuilder.HttpResponse;
+import botobo.core.domain.user.SocialType;
import botobo.core.dto.card.CardRequest;
import botobo.core.dto.card.CardResponse;
import botobo.core.dto.card.CardUpdateRequest;
import botobo.core.dto.card.CardUpdateResponse;
import botobo.core.dto.card.NextQuizCardsRequest;
-import botobo.core.exception.ErrorResponse;
+import botobo.core.exception.common.ErrorResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -16,27 +17,25 @@
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.http.HttpStatus;
-import java.util.Arrays;
import java.util.List;
-import static botobo.core.utils.Fixture.CARD_REQUEST_1;
-import static botobo.core.utils.Fixture.CARD_REQUEST_2;
-import static botobo.core.utils.Fixture.CARD_REQUEST_3;
-import static botobo.core.utils.Fixture.WORKBOOK_REQUEST_1;
+import static botobo.core.utils.Fixture.ditto;
+import static botobo.core.utils.Fixture.joanne;
+import static botobo.core.utils.Fixture.kyle;
import static botobo.core.utils.TestUtils.stringGenerator;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("카드 인수 테스트")
public class CardAcceptanceTest extends DomainAcceptanceTest {
+ private String joanneToken;
+
+ private Long workbookId;
+
@BeforeEach
void setFixture() {
- 문제집_생성_요청(WORKBOOK_REQUEST_1);
- 여러개_카드_생성_요청(Arrays.asList(CARD_REQUEST_1, CARD_REQUEST_2, CARD_REQUEST_3));
- }
-
- protected String createToken(Long id) {
- return jwtTokenProvider.createToken(id);
+ joanneToken = 소셜_로그인되어_있음(joanne, SocialType.GOOGLE);
+ workbookId = 유저_태그_포함_문제집_등록되어_있음("문제집", true, joanneToken).getId();
}
@Test
@@ -46,14 +45,11 @@ void createCard() {
CardRequest cardRequest = CardRequest.builder()
.question("question")
.answer("answer")
- .workbookId(1L)
+ .workbookId(workbookId)
.build();
// when
- final HttpResponse response = request()
- .post("/api/cards", cardRequest)
- .auth(createToken(1L))
- .build();
+ final HttpResponse response = 유저_카드_생성_요청(cardRequest, joanneToken);
// then
CardResponse cardResponse = response.convertBody(CardResponse.class);
@@ -72,14 +68,11 @@ void createCardWithInvalidQuestion(String question) {
CardRequest cardRequest = CardRequest.builder()
.question(question)
.answer("answer")
- .workbookId(1L)
+ .workbookId(workbookId)
.build();
// when
- final HttpResponse response = request()
- .post("/api/cards", cardRequest)
- .auth(createToken(1L))
- .build();
+ final HttpResponse response = 유저_카드_생성_요청(cardRequest, joanneToken);
// then
ErrorResponse errorResponse = response.convertToErrorResponse();
@@ -94,14 +87,11 @@ void createCardWithLongQuestion() {
CardRequest cardRequest = CardRequest.builder()
.question(stringGenerator(2001))
.answer("answer")
- .workbookId(1L)
+ .workbookId(workbookId)
.build();
// when
- final HttpResponse response = request()
- .post("/api/cards", cardRequest)
- .auth(createToken(1L))
- .build();
+ final HttpResponse response = 유저_카드_생성_요청(cardRequest, joanneToken);
// then
ErrorResponse errorResponse = response.convertToErrorResponse();
@@ -118,14 +108,12 @@ void createCardWithInvalidAnswer(String answer) {
CardRequest cardRequest = CardRequest.builder()
.question("question")
.answer(answer)
- .workbookId(1L)
+ .workbookId(workbookId)
.build();
// when
- final HttpResponse response = request()
- .post("/api/cards", cardRequest)
- .auth(createToken(1L))
- .build();
+ final HttpResponse response = 유저_카드_생성_요청(cardRequest, joanneToken);
+
// then
ErrorResponse errorResponse = response.convertToErrorResponse();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
@@ -139,14 +127,11 @@ void createCardWithLongAnswer() {
CardRequest cardRequest = CardRequest.builder()
.question("question")
.answer(stringGenerator(2001))
- .workbookId(1L)
+ .workbookId(workbookId)
.build();
// when
- final HttpResponse response = request()
- .post("/api/cards", cardRequest)
- .auth(createToken(1L))
- .build();
+ final HttpResponse response = 유저_카드_생성_요청(cardRequest, joanneToken);
// then
ErrorResponse errorResponse = response.convertToErrorResponse();
@@ -165,15 +150,12 @@ void createCardWithInvalidWorkbookId() {
.build();
// when
- final HttpResponse response = request()
- .post("/api/cards", cardRequest)
- .auth(createToken(1L))
- .build();
+ final HttpResponse response = 유저_카드_생성_요청(cardRequest, joanneToken);
// then
ErrorResponse errorResponse = response.convertToErrorResponse();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(errorResponse).extracting("message").isEqualTo("카드가 포함될 문제집 아이디는 필수 입력값입니다.");
+ assertThat(errorResponse).extracting("message").isEqualTo("문제집 아이디는 필수 입력값입니다.");
}
@@ -189,10 +171,7 @@ void createCardWithNoneExistingWorkbookId() {
.build();
// when
- final HttpResponse response = request()
- .post("/api/cards", cardRequest)
- .auth(createToken(1L))
- .build();
+ final HttpResponse response = 유저_카드_생성_요청(cardRequest, joanneToken);
// then
ErrorResponse errorResponse = response.convertToErrorResponse();
@@ -207,13 +186,13 @@ void createCardWithNotExistUser() {
CardRequest cardRequest = CardRequest.builder()
.question("question")
.answer("answer")
- .workbookId(1L)
+ .workbookId(workbookId)
.build();
// when
final HttpResponse response = request()
.post("/api/cards", cardRequest)
- .auth() // 100L
+ .failAuth() // 100L
.build();
// then
@@ -229,14 +208,12 @@ void createCardWithNotSameUser() {
CardRequest cardRequest = CardRequest.builder()
.question("question")
.answer("answer")
- .workbookId(1L)
+ .workbookId(workbookId)
.build();
+ final String anotherToken = 소셜_로그인되어_있음(kyle, SocialType.GOOGLE);
// when
- final HttpResponse response = request()
- .post("/api/cards", cardRequest)
- .auth(createToken(anyUser().getId()))
- .build();
+ final HttpResponse response = 유저_카드_생성_요청(cardRequest, anotherToken);
// then
ErrorResponse errorResponse = response.convertToErrorResponse();
@@ -248,20 +225,19 @@ void createCardWithNotSameUser() {
@DisplayName("카드 수정 - 성공")
void updateCard() {
// given
+ final CardResponse cardResponse = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken);
+
CardUpdateRequest cardUpdateRequest = CardUpdateRequest.builder()
.question("question")
.answer("answer")
- .workbookId(1L)
+ .workbookId(workbookId)
.encounterCount(0)
.bookmark(true)
.nextQuiz(true)
.build();
// when
- final HttpResponse response = request()
- .put("/api/cards/1", cardUpdateRequest)
- .auth(createToken(1L))
- .build();
+ final HttpResponse response = 유저_카드_수정_요청(cardUpdateRequest, cardResponse, joanneToken);
// then
CardUpdateResponse cardUpdateResponse = response.convertBody(CardUpdateResponse.class);
@@ -280,45 +256,43 @@ void updateCard() {
@DisplayName("카드 수정 - 실패, 유효하지 않은 question")
void updateCardWithInvalidQuestion(String question) {
// given
+ final CardResponse cardResponse = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken);
+
CardUpdateRequest cardUpdateRequest = CardUpdateRequest.builder()
.question(question)
.answer("answer")
- .workbookId(1L)
+ .workbookId(workbookId)
.encounterCount(0)
.bookmark(true)
.nextQuiz(true)
.build();
// when
- final HttpResponse response = request()
- .put("/api/cards/1", cardUpdateRequest)
- .auth(createToken(1L))
- .build();
+ final HttpResponse response = 유저_카드_수정_요청(cardUpdateRequest, cardResponse, joanneToken);
// then
ErrorResponse errorResponse = response.convertToErrorResponse();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(errorResponse).extracting("message").isEqualTo("카드를 업데이트하기 위해서는 질문이 필요합니다.");
+ assertThat(errorResponse).extracting("message").isEqualTo("질문은 필수 입력값입니다.");
}
@Test
@DisplayName("카드 수정 - 실패, 2000자를 넘긴 question")
void updateCardWithLongQuestion() {
// given
+ final CardResponse cardResponse = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken);
+
CardUpdateRequest cardUpdateRequest = CardUpdateRequest.builder()
.question(stringGenerator(2001))
.answer("answer")
- .workbookId(1L)
+ .workbookId(workbookId)
.encounterCount(0)
.bookmark(true)
.nextQuiz(true)
.build();
// when
- final HttpResponse response = request()
- .put("/api/cards/1", cardUpdateRequest)
- .auth(createToken(1L))
- .build();
+ final HttpResponse response = 유저_카드_수정_요청(cardUpdateRequest, cardResponse, joanneToken);
// then
ErrorResponse errorResponse = response.convertToErrorResponse();
@@ -332,45 +306,44 @@ void updateCardWithLongQuestion() {
@DisplayName("카드 수정 - 실패, 유효하지 않은 answer")
void updateCardWithInvalidAnswer(String answer) {
// given
+ final CardResponse cardResponse = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken);
+
CardUpdateRequest cardUpdateRequest = CardUpdateRequest.builder()
.question("question")
.answer(answer)
- .workbookId(1L)
+ .workbookId(workbookId)
.encounterCount(0)
.bookmark(true)
.nextQuiz(true)
.build();
// when
- final HttpResponse response = request()
- .put("/api/cards/1", cardUpdateRequest)
- .auth(createToken(1L))
- .build();
+ final HttpResponse response = 유저_카드_수정_요청(cardUpdateRequest, cardResponse, joanneToken);
// then
ErrorResponse errorResponse = response.convertToErrorResponse();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(errorResponse).extracting("message").isEqualTo("카드를 업데이트하기 위해서는 답변이 필요합니다.");
+ assertThat(errorResponse).extracting("message").isEqualTo("답변은 필수 입력값입니다.");
}
@Test
@DisplayName("카드 수정 - 실패, 2000자를 넘긴 answer")
void updateCardWithLongAnswer() {
// given
+ final CardResponse cardResponse = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken);
+
CardUpdateRequest cardUpdateRequest = CardUpdateRequest.builder()
.question("question")
.answer(stringGenerator(2001))
- .workbookId(1L)
+ .workbookId(workbookId)
.encounterCount(0)
.bookmark(true)
.nextQuiz(true)
.build();
// when
- final HttpResponse response = request()
- .put("/api/cards/1", cardUpdateRequest)
- .auth(createToken(1L))
- .build();
+ final HttpResponse response = 유저_카드_수정_요청(cardUpdateRequest, cardResponse, joanneToken);
+
// then
ErrorResponse errorResponse = response.convertToErrorResponse();
@@ -382,10 +355,12 @@ void updateCardWithLongAnswer() {
@DisplayName("카드 수정 - 실패, 존재하지 않는 유저")
void updateCardWithNotExistUser() {
// given
+ final CardResponse cardResponse = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken);
+
CardUpdateRequest cardUpdateRequest = CardUpdateRequest.builder()
.question("question")
.answer("answer")
- .workbookId(1L)
+ .workbookId(workbookId)
.encounterCount(0)
.bookmark(true)
.nextQuiz(true)
@@ -393,8 +368,8 @@ void updateCardWithNotExistUser() {
// when
final HttpResponse response = request()
- .put("/api/cards/1", cardUpdateRequest)
- .auth()
+ .put("/api/cards/{id}", cardUpdateRequest, cardResponse.getId())
+ .failAuth() // 100L
.build();
// then
@@ -407,20 +382,21 @@ void updateCardWithNotExistUser() {
@DisplayName("카드 수정 - 실패, 작성자가 아닌 유저")
void updateCardWithNotAuthor() {
// given
+ final CardResponse cardResponse = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken);
+
CardUpdateRequest cardUpdateRequest = CardUpdateRequest.builder()
.question("question")
.answer("answer")
- .workbookId(1L)
+ .workbookId(workbookId)
.encounterCount(0)
.bookmark(true)
.nextQuiz(true)
.build();
+ final String anotherToken = 소셜_로그인되어_있음(ditto, SocialType.GOOGLE);
+
// when
- final HttpResponse response = request()
- .put("/api/cards/1", cardUpdateRequest)
- .auth(createToken(anyUser().getId()))
- .build();
+ final HttpResponse response = 유저_카드_수정_요청(cardUpdateRequest, cardResponse, anotherToken);
// then
ErrorResponse errorResponse = response.convertToErrorResponse();
@@ -432,13 +408,10 @@ void updateCardWithNotAuthor() {
@DisplayName("카드 삭제 - 성공")
void deleteCard() {
// given
- long cardId = 1L;
+ final CardResponse cardResponse = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken);
// when
- final HttpResponse response = request()
- .delete("/api/cards/" + cardId)
- .auth(createToken(1L))
- .build();
+ final HttpResponse response = 유저_카드_삭제_요청(cardResponse, joanneToken);
// then
assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT);
@@ -448,12 +421,12 @@ void deleteCard() {
@DisplayName("카드 삭제 - 실패, 존재하지 않는 유저")
void deleteCardWithNotExistUser() {
// given
- long cardId = 1L;
+ final CardResponse cardResponse = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken);
// when
final HttpResponse response = request()
- .delete("/api/cards/" + cardId)
- .auth()
+ .delete("/api/cards/" + cardResponse.getId())
+ .failAuth()
.build();
// then
@@ -466,13 +439,11 @@ void deleteCardWithNotExistUser() {
@DisplayName("카드 삭제 - 실패, 작성자가 아닌 유저")
void deleteCardWithNotAuthor() {
// given
- long cardId = 1L;
+ final CardResponse cardResponse = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken);
+ final String anotherToken = 소셜_로그인되어_있음(ditto, SocialType.GOOGLE);
// when
- final HttpResponse response = request()
- .delete("/api/cards/" + cardId)
- .auth(createToken(anyUser().getId()))
- .build();
+ final HttpResponse response = 유저_카드_삭제_요청(cardResponse, anotherToken);
// then
ErrorResponse errorResponse = response.convertToErrorResponse();
@@ -484,15 +455,17 @@ void deleteCardWithNotAuthor() {
@DisplayName("또 보기 원하는 카드 선택 - 성공")
void selectNextQuizCards() {
// given
+ final Long card1 = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken).getId();
+ final Long card2 = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken).getId();
+ final Long card3 = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken).getId();
+
NextQuizCardsRequest request = NextQuizCardsRequest.builder()
- .cardIds(List.of(1L, 2L, 3L))
+ .cardIds(List.of(card1, card2, card3))
.build();
// when
- final HttpResponse response = request()
- .put("/api/cards/next-quiz", request)
- .auth()
- .build();
+ final HttpResponse response = 카드_또_보기_요청(request, joanneToken);
+ ;
// then
assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT);
@@ -509,7 +482,7 @@ void selectNextQuizCardsWithNullList() {
// when
final HttpResponse response = request()
.put("/api/cards/next-quiz", request)
- .auth()
+ .failAuth()
.build();
// then
@@ -517,4 +490,97 @@ void selectNextQuizCardsWithNullList() {
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(errorResponse).extracting("message").isEqualTo("유효하지 않은 또 보기 카드 등록 요청입니다.");
}
+
+ @Test
+ @DisplayName("또 보기 원하는 카드 선택 - 실패, 로그인하지 않은 경우")
+ void selectNextQuizCardsWhenUserNotLogin() {
+ // given
+ final Long card1 = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken).getId();
+ final Long card2 = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken).getId();
+ final Long card3 = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken).getId();
+
+ NextQuizCardsRequest request = NextQuizCardsRequest.builder()
+ .cardIds(List.of(card1, card2, card3))
+ .build();
+
+ // when
+ final HttpResponse response = request()
+ .put("/api/cards/next-quiz", request)
+ .build();
+
+ // then
+ ErrorResponse errorResponse = response.convertToErrorResponse();
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
+ assertThat(errorResponse).extracting("message").isEqualTo("토큰이 유효하지 않습니다.");
+ }
+
+ @Test
+ @DisplayName("또 보기 원하는 카드 선택 - 실패, 유저가 존재하지 않는 경우")
+ void selectNextQuizCardsWhenUserNotFound() {
+ // given
+ final Long card1 = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken).getId();
+ final Long card2 = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken).getId();
+ final Long card3 = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken).getId();
+
+ NextQuizCardsRequest request = NextQuizCardsRequest.builder()
+ .cardIds(List.of(card1, card2, card3))
+ .build();
+
+ // when
+ final HttpResponse response = request()
+ .put("/api/cards/next-quiz", request)
+ .failAuth() // 100L
+ .build();
+
+ // then
+ ErrorResponse errorResponse = response.convertToErrorResponse();
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+ assertThat(errorResponse).extracting("message").isEqualTo("해당 유저를 찾을 수 없습니다.");
+ }
+
+ @Test
+ @DisplayName("또 보기 원하는 카드 선택 - 실패, author가 아닌 경우")
+ void selectNextQuizCardsWhenUserIsNotAuthor() {
+ // given
+ final Long card1 = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken).getId();
+ final Long card2 = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken).getId();
+ final Long card3 = 유저_카드_등록되어_있음("question", "answer", workbookId, joanneToken).getId();
+
+ final String anotherToken = 소셜_로그인되어_있음(kyle, SocialType.GOOGLE);
+
+ NextQuizCardsRequest request = NextQuizCardsRequest.builder()
+ .cardIds(List.of(card1, card2, card3))
+ .build();
+
+ // when
+ final HttpResponse response = 카드_또_보기_요청(request, anotherToken);
+
+ // then
+ ErrorResponse errorResponse = response.convertToErrorResponse();
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.FORBIDDEN);
+ assertThat(errorResponse).extracting("message").isEqualTo("작성자가 아니므로 권한이 없습니다.");
+ }
+
+ private HttpResponse 유저_카드_수정_요청(CardUpdateRequest cardUpdateRequest,
+ CardResponse cardResponse,
+ String accessToken) {
+ return request()
+ .put("/api/cards/{id}", cardUpdateRequest, cardResponse.getId())
+ .auth(accessToken)
+ .build();
+ }
+
+ private HttpResponse 유저_카드_삭제_요청(CardResponse cardResponse, String accessToken) {
+ return request()
+ .delete("/api/cards/{id}", cardResponse.getId())
+ .auth(accessToken)
+ .build();
+ }
+
+ private HttpResponse 카드_또_보기_요청(NextQuizCardsRequest request, String accessToken) {
+ return request()
+ .put("/api/cards/next-quiz", request)
+ .auth(accessToken)
+ .build();
+ }
}
diff --git a/backend/src/test/java/botobo/core/acceptance/card/QuizAcceptanceTest.java b/backend/src/test/java/botobo/core/acceptance/card/QuizAcceptanceTest.java
index 179698b6..b437d295 100644
--- a/backend/src/test/java/botobo/core/acceptance/card/QuizAcceptanceTest.java
+++ b/backend/src/test/java/botobo/core/acceptance/card/QuizAcceptanceTest.java
@@ -2,15 +2,19 @@
import botobo.core.acceptance.DomainAcceptanceTest;
import botobo.core.acceptance.utils.RequestBuilder.HttpResponse;
+import botobo.core.domain.user.User;
+import botobo.core.dto.card.CardResponse;
import botobo.core.dto.card.NextQuizCardsRequest;
import botobo.core.dto.card.QuizRequest;
import botobo.core.dto.card.QuizResponse;
-import botobo.core.exception.ErrorResponse;
+import botobo.core.exception.common.ErrorResponse;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.http.HttpStatus;
import java.util.Arrays;
@@ -18,52 +22,32 @@
import java.util.List;
import java.util.stream.Collectors;
-import static botobo.core.utils.Fixture.CARD_REQUEST_1;
-import static botobo.core.utils.Fixture.CARD_REQUEST_10;
-import static botobo.core.utils.Fixture.CARD_REQUEST_11;
-import static botobo.core.utils.Fixture.CARD_REQUEST_12;
-import static botobo.core.utils.Fixture.CARD_REQUEST_13;
-import static botobo.core.utils.Fixture.CARD_REQUEST_14;
-import static botobo.core.utils.Fixture.CARD_REQUEST_15;
-import static botobo.core.utils.Fixture.CARD_REQUEST_2;
-import static botobo.core.utils.Fixture.CARD_REQUEST_3;
-import static botobo.core.utils.Fixture.CARD_REQUEST_4;
-import static botobo.core.utils.Fixture.CARD_REQUEST_5;
-import static botobo.core.utils.Fixture.CARD_REQUEST_6;
-import static botobo.core.utils.Fixture.CARD_REQUEST_7;
-import static botobo.core.utils.Fixture.CARD_REQUEST_8;
-import static botobo.core.utils.Fixture.CARD_REQUEST_9;
-import static botobo.core.utils.Fixture.WORKBOOK_REQUEST_1;
-import static botobo.core.utils.Fixture.WORKBOOK_REQUEST_2;
-import static botobo.core.utils.Fixture.WORKBOOK_REQUEST_3;
-import static botobo.core.utils.Fixture.WORKBOOK_REQUEST_4;
-import static botobo.core.utils.Fixture.WORKBOOK_REQUEST_5;
+import static botobo.core.utils.Fixture.ADMIN_CARD_REQUESTS_OF_30_CARDS;
+import static botobo.core.utils.Fixture.ADMIN_WORKBOOK_REQUESTS;
import static org.assertj.core.api.Assertions.assertThat;
-
@DisplayName("Quiz 인수 테스트")
public class QuizAcceptanceTest extends DomainAcceptanceTest {
@BeforeEach
void setFixture() {
- 여러개_문제집_생성_요청(Arrays.asList(WORKBOOK_REQUEST_1, WORKBOOK_REQUEST_2, WORKBOOK_REQUEST_3,
- WORKBOOK_REQUEST_4, WORKBOOK_REQUEST_5));
- 여러개_카드_생성_요청(Arrays.asList(CARD_REQUEST_1, CARD_REQUEST_2, CARD_REQUEST_3, CARD_REQUEST_4,
- CARD_REQUEST_5, CARD_REQUEST_6, CARD_REQUEST_7, CARD_REQUEST_8, CARD_REQUEST_9, CARD_REQUEST_10,
- CARD_REQUEST_11, CARD_REQUEST_12, CARD_REQUEST_13, CARD_REQUEST_14, CARD_REQUEST_15));
+ 여러개_문제집_생성_요청(ADMIN_WORKBOOK_REQUESTS);
+ 여러개_카드_생성_요청(ADMIN_CARD_REQUESTS_OF_30_CARDS);
}
@Test
- @DisplayName("문제집 id(Long)를 이용해서 퀴즈 생성 - 성공")
+ @DisplayName("퀴즈 생성 - 성공")
void createQuiz() {
// given
List ids = Arrays.asList(1L, 2L, 3L);
- QuizRequest quizRequest =
- new QuizRequest(ids);
+ QuizRequest quizRequest = QuizRequest.builder()
+ .workbookIds(ids)
+ .count(10)
+ .build();
final HttpResponse response = request()
.post("/api/quizzes", quizRequest)
- .auth()
+ .auth(createToken(1L))
.build();
// when
@@ -74,17 +58,162 @@ void createQuiz() {
assertThat(quizResponses.size()).isEqualTo(10);
}
+ @Test
+ @DisplayName("퀴즈 생성 - 실패, 비로그인은 회원용 퀴즈 생성을 이용할 수 없다.")
+ void createQuizWhenGuest() {
+ // given
+ List ids = Arrays.asList(1L, 2L, 3L);
+ QuizRequest quizRequest = QuizRequest.builder()
+ .workbookIds(ids)
+ .count(10)
+ .build();
+
+ final HttpResponse response = request()
+ .post("/api/quizzes", quizRequest)
+ .build();
+
+ // when
+ ErrorResponse errorResponse = response.convertToErrorResponse();
+
+ // then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
+ assertThat(errorResponse.getMessage()).isEqualTo("토큰이 유효하지 않습니다.");
+ }
+
+ @Test
+ @DisplayName("퀴즈 생성 - 실패, 회원을 찾을 수 없음.")
+ void createQuizWhenUserNotFound() {
+ // given
+ List ids = Arrays.asList(1L, 2L, 3L);
+ QuizRequest quizRequest = QuizRequest.builder()
+ .workbookIds(ids)
+ .count(10)
+ .build();
+
+ final HttpResponse response = request()
+ .post("/api/quizzes", quizRequest)
+ .auth(createToken(100L))
+ .build();
+
+ // when
+ ErrorResponse errorResponse = response.convertToErrorResponse();
+
+ // then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+ assertThat(errorResponse.getMessage()).isEqualTo("해당 유저를 찾을 수 없습니다.");
+ }
+
+ @Test
+ @DisplayName("퀴즈 생성 - 실패, 내 문제집이 아님.")
+ void createQuizWhenNotAuthor() {
+ // given
+ List ids = Arrays.asList(1L, 2L, 3L);
+ QuizRequest quizRequest = QuizRequest.builder()
+ .workbookIds(ids)
+ .count(10)
+ .build();
+
+ User anyUser = anyUser();
+ final HttpResponse response = request()
+ .post("/api/quizzes", quizRequest)
+ .auth(createToken(anyUser.getId()))
+ .build();
+
+ // when
+ ErrorResponse errorResponse = response.convertToErrorResponse();
+
+ // then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.FORBIDDEN);
+ assertThat(errorResponse.getMessage()).isEqualTo("작성자가 아니므로 권한이 없습니다.");
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {10, 15, 20, 25, 30})
+ @DisplayName("퀴즈 생성 - 성공, 퀴즈 개수는 10 ~ 30사이의 수만 가능하다.")
+ void createQuizWhenCountIsCorrect(int count) {
+ // given
+ List ids = Arrays.asList(1L, 2L, 3L);
+ QuizRequest quizRequest = QuizRequest.builder()
+ .workbookIds(ids)
+ .count(count)
+ .build();
+
+ final HttpResponse response = request()
+ .post("/api/quizzes", quizRequest)
+ .auth(createToken(1L))
+ .build();
+
+ // when
+ final List quizResponses = response.convertBodyToList(QuizResponse.class);
+
+ // then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(quizResponses.size()).isEqualTo(count);
+ }
+
+ @Test
+ @DisplayName("퀴즈 생성 - 성공, 요청 개수가 가진 퀴즈 개수보다 많을 때, min 값을 취한다.")
+ void createQuizWhenCountIsMoreThanWeHave() {
+ // given
+ final int requestCount = 11;
+ final int existCardCount = 10;
+
+ List ids = List.of(1L); // 10개의 카드 존재
+ QuizRequest quizRequest = QuizRequest.builder()
+ .workbookIds(ids)
+ .count(requestCount)
+ .build();
+
+ final HttpResponse response = request()
+ .post("/api/quizzes", quizRequest)
+ .auth(createToken(1L))
+ .build();
+
+ // when
+ final List quizResponses = response.convertBodyToList(QuizResponse.class);
+
+ // then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(quizResponses.size()).isEqualTo(existCardCount);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {-1, 0, 9, 31})
+ @DisplayName("퀴즈 생성 - 실패, 퀴즈 개수는 10 ~ 30사이의 수만 가능하다.")
+ void createQuizWhenQuizCountIsInvalid(int count) {
+ // given
+ List ids = Arrays.asList(1L, 2L, 3L);
+ QuizRequest quizRequest = QuizRequest.builder()
+ .workbookIds(ids)
+ .count(count)
+ .build();
+
+ final HttpResponse response = request()
+ .post("/api/quizzes", quizRequest)
+ .auth(createToken(1L))
+ .build();
+
+ // when
+ ErrorResponse errorResponse = response.convertToErrorResponse();
+
+ // then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).isEqualTo("퀴즈의 개수는 10 ~ 30 사이의 수만 가능합니다.");
+ }
+
@Test
@DisplayName("문제집 id(Long)를 이용해서 퀴즈 생성 시 encounterCount가 1 증가한다. - 성공")
void checkEncounterCount() {
// given
List ids = Arrays.asList(1L, 2L, 3L);
- QuizRequest quizRequest =
- new QuizRequest(ids);
+ QuizRequest quizRequest = QuizRequest.builder()
+ .workbookIds(ids)
+ .count(10)
+ .build();
final HttpResponse response = request()
.post("/api/quizzes", quizRequest)
- .auth()
+ .auth(createToken(1L))
.build();
// when
@@ -100,15 +229,17 @@ void checkEncounterCount() {
@Test
@DisplayName("문제집 id(Long)를 이용해서 퀴즈 생성 - 실패, 문제집 id가 비어있음")
- void createQuizWithEmptyCategoryIdList() {
+ void createQuizWithEmptyWorkbookIdList() {
// given
List ids = Collections.emptyList();
- QuizRequest quizRequest =
- new QuizRequest(ids);
+ QuizRequest quizRequest = QuizRequest.builder()
+ .workbookIds(ids)
+ .count(10)
+ .build();
final HttpResponse response = request()
.post("/api/quizzes", quizRequest)
- .auth()
+ .auth(createToken(1L))
.build();
// when, then
@@ -122,12 +253,14 @@ void createQuizWithEmptyCategoryIdList() {
void createQuizWithNotExistId() {
// given
List ids = Arrays.asList(1000L, 1001L, 1002L);
- QuizRequest quizRequest =
- new QuizRequest(ids);
+ QuizRequest quizRequest = QuizRequest.builder()
+ .workbookIds(ids)
+ .count(10)
+ .build();
final HttpResponse response = request()
.post("/api/quizzes", quizRequest)
- .auth()
+ .auth(createToken(1L))
.build();
// when, then
@@ -141,11 +274,14 @@ void createQuizWithNotExistId() {
void createQuizWithEmptyCards() {
// given
List ids = Collections.singletonList(4L);
- QuizRequest quizRequest = new QuizRequest(ids);
+ QuizRequest quizRequest = QuizRequest.builder()
+ .workbookIds(ids)
+ .count(10)
+ .build();
final HttpResponse response = request()
.post("/api/quizzes", quizRequest)
- .auth()
+ .auth(createToken(1L))
.build();
// when, then
@@ -158,14 +294,21 @@ void createQuizWithEmptyCards() {
@DisplayName("다음에 또 보기가 포함된 카드를 퀴즈에 포함한다. - 성공")
void createQuizIncludeNextQuizOption() {
// given
- 다음에_또_보기(List.of(1L, 2L, 3L));
+ List cards = 문제집의_카드_모아보기(1L).getCards();
+ final List cardIds = cards.stream()
+ .map(CardResponse::getId)
+ .collect(Collectors.toList());
+
+ 다음에_또_보기(cardIds);
List ids = Arrays.asList(1L, 2L, 3L);
- QuizRequest quizRequest =
- new QuizRequest(ids);
+ QuizRequest quizRequest = QuizRequest.builder()
+ .workbookIds(ids)
+ .count(10)
+ .build();
final HttpResponse response = request()
.post("/api/quizzes", quizRequest)
- .auth()
+ .auth(createToken(1L))
.build();
// when
@@ -175,9 +318,9 @@ void createQuizIncludeNextQuizOption() {
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
assertThat(quizResponses.size()).isEqualTo(10);
assertThat(quizResponses.stream()
- .map(QuizResponse::getQuestion)
+ .map(QuizResponse::getId)
.collect(Collectors.toList()))
- .containsAll(List.of("1", "2", "3"));
+ .containsAll(cardIds);
}
public ExtractableResponse 다음에_또_보기(List cardIds) {
@@ -187,7 +330,7 @@ void createQuizIncludeNextQuizOption() {
return request()
.put("/api/cards/next-quiz", request)
- .auth()
+ .auth(createToken(admin.getId()))
.build()
.extract();
}
@@ -235,7 +378,7 @@ void createQuizFromWorkbook() {
// 1번 문제집에는 5개의 카드가 존재한다.
final HttpResponse response = request()
.get("/api/quizzes/{workbookId}", 1L)
- .auth()
+ .failAuth()
.build();
// when
@@ -243,7 +386,7 @@ void createQuizFromWorkbook() {
// then
assertThat(quizResponses).isNotEmpty();
- assertThat(quizResponses.size()).isEqualTo(5);
+ assertThat(quizResponses.size()).isEqualTo(10);
}
@Test
@@ -253,7 +396,7 @@ void createQuizFromWorkbookWithNotExistId() {
// 1번 문제집에는 5개의 카드가 존재한다.
final HttpResponse response = request()
.get("/api/quizzes/{workbookId}", 100L)
- .auth()
+ .failAuth()
.build();
// when
@@ -271,7 +414,7 @@ void createQuizFromWorkbookFailedWhenWorkbookIsNotPublic() {
// 1번 문제집에는 5개의 카드가 존재한다.
final HttpResponse response = request()
.get("/api/quizzes/{workbookId}", 5L)
- .auth()
+ .failAuth()
.build();
// when
@@ -290,7 +433,7 @@ void createQuizFromWorkbookWithEmptyCards() {
// 4번 문제집은 isPublic = true
final HttpResponse response = request()
.get("/api/quizzes/{workbookId}", 4L)
- .auth()
+ .failAuth()
.build();
// when
@@ -300,4 +443,4 @@ void createQuizFromWorkbookWithEmptyCards() {
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(errorResponse.getMessage()).isEqualTo("퀴즈에 문제가 존재하지 않습니다.");
}
-}
+}
\ No newline at end of file
diff --git a/backend/src/test/java/botobo/core/acceptance/search/SearchAcceptanceTest.java b/backend/src/test/java/botobo/core/acceptance/search/SearchAcceptanceTest.java
new file mode 100644
index 00000000..ae30a237
--- /dev/null
+++ b/backend/src/test/java/botobo/core/acceptance/search/SearchAcceptanceTest.java
@@ -0,0 +1,658 @@
+package botobo.core.acceptance.search;
+
+import botobo.core.acceptance.DomainAcceptanceTest;
+import botobo.core.acceptance.utils.RequestBuilder.HttpResponse;
+import botobo.core.domain.user.SocialType;
+import botobo.core.dto.tag.TagRequest;
+import botobo.core.dto.tag.TagResponse;
+import botobo.core.dto.user.SimpleUserResponse;
+import botobo.core.dto.workbook.WorkbookResponse;
+import botobo.core.exception.common.ErrorResponse;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.springframework.http.HttpStatus;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.IntStream;
+
+import static botobo.core.utils.Fixture.bear;
+import static botobo.core.utils.Fixture.pk;
+import static botobo.core.utils.TestUtils.stringGenerator;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SearchAcceptanceTest extends DomainAcceptanceTest {
+
+ private String pkToken, bearToken;
+
+ @Override
+ @BeforeEach
+ protected void setUp() {
+ super.setUp();
+ initializeUsers();
+ initializeWorkbooks();
+ }
+
+ private void initializeUsers() {
+ pkToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
+ bearToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ }
+
+ private void initializeWorkbooks() {
+ // pk의 문제집 생성
+ 카드도_함께_등록(
+ 유저_문제집_등록되어_있음("피케이의 자바 기초 문제집", true, makeJavaTags(), pkToken),
+ 2,
+ pkToken
+ );
+ 카드도_함께_등록(
+ 유저_문제집_등록되어_있음("피케이의 자바 중급 문제집", true, makeJavaTags(), pkToken),
+ 5,
+ pkToken
+ );
+ 카드도_함께_등록(
+ 유저_문제집_등록되어_있음("피케이의 자바 고급 문제집", true, makeJavaTags(), pkToken),
+ 10,
+ pkToken
+ );
+ 카드도_함께_등록(
+ 유저_문제집_등록되어_있음("네트워크는 나의 것", true, makeNetworkTags(), pkToken),
+ 1,
+ pkToken
+ );
+
+ // 중간곰의 문제집 생성
+ 카드와_좋아요도_함께_등록(
+ 유저_문제집_등록되어_있음("중간곰의 자바 기초 문제집", true, makeJavaTags(), bearToken),
+ 1,
+ bearToken,
+ List.of(pkToken)
+ );
+ 카드와_좋아요도_함께_등록(
+ 유저_문제집_등록되어_있음("중간곰의 자바 중급 문제집", true, makeJavaTags(), bearToken),
+ 1,
+ bearToken,
+ List.of(pkToken, bearToken)
+ );
+ 카드와_좋아요도_함께_등록(
+ 유저_문제집_등록되어_있음("중간곰의 자바 고급 문제집", true, makeJavaTags(), bearToken),
+ 1,
+ bearToken,
+ List.of()
+ );
+ 유저_문제집_등록되어_있음("너도 자바 할 수 있어", true, makeJavaTags(), bearToken);
+ }
+
+ private List makeJavaTags() {
+ return Arrays.asList(
+ TagRequest.builder().id(0L).name("java").build(),
+ TagRequest.builder().id(0L).name("자바").build()
+ );
+ }
+
+ private List makeNetworkTags() {
+ return Collections.singletonList(
+ TagRequest.builder().id(0L).name("network").build()
+ );
+ }
+
+ private void 카드와_좋아요도_함께_등록(WorkbookResponse workbookResponse, int cardCount, String accessToken, List heartUserTokens) {
+ 카드도_함께_등록(workbookResponse, cardCount, accessToken);
+ 좋아요도_함께_등록(workbookResponse, heartUserTokens);
+ }
+
+ private void 카드도_함께_등록(WorkbookResponse workbookResponse, int cardCount, String accessToken) {
+ Long workbookId = workbookResponse.getId();
+ IntStream.rangeClosed(1, cardCount)
+ .forEach(number -> 유저_카드_등록되어_있음("질문", "정답", workbookId, accessToken));
+ }
+
+ private void 좋아요도_함께_등록(WorkbookResponse workbookResponse, List heartUserTokens) {
+ Long workbookId = workbookResponse.getId();
+ heartUserTokens
+ .forEach(token -> 하트_토글_요청(workbookId, token));
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 성공, 검색어를 제외한 다른 인자는 생략 가능")
+ void searchWithDefault() {
+ // given
+ Map parameters = Map.of("keyword", "java");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 성공, 문제집 이름으로 검색")
+ void searchFromNameType() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "기초");
+ parameters.put("type", "name");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ List workbookResponses = response.convertBodyToList(WorkbookResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(workbookResponses).hasSize(2);
+ assertThat(workbookResponses)
+ .extracting(WorkbookResponse::getName)
+ .allMatch(name -> name.contains("기초"));
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 성공, 태그 이름으로 검색")
+ void searchFromTagType() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "java");
+ parameters.put("type", "tag");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ List workbookResponses = response.convertBodyToList(WorkbookResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(workbookResponses).hasSize(6);
+ workbookResponses.forEach(workbookResponse ->
+ assertThat(workbookResponse.getTags()).extracting(TagResponse::getName).contains("java")
+ );
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 성공, 유저 이름으로 검색")
+ void searchFromUserType() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "pk");
+ parameters.put("type", "user");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ List workbookResponses = response.convertBodyToList(WorkbookResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(workbookResponses).hasSize(4);
+ assertThat(workbookResponses).extracting(WorkbookResponse::getAuthor)
+ .allMatch(userName -> userName.equals("pk"));
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 성공, 시간 기준 최신 순 정렬")
+ void searchFromDateDesc() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "pk");
+ parameters.put("type", "user");
+ parameters.put("criteria", "date");
+ parameters.put("order", "desc");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ List workbookResponses = response.convertBodyToList(WorkbookResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(workbookResponses).hasSize(4);
+ assertThat(workbookResponses)
+ .extracting(WorkbookResponse::getName)
+ .containsExactly(
+ "네트워크는 나의 것",
+ "피케이의 자바 고급 문제집",
+ "피케이의 자바 중급 문제집",
+ "피케이의 자바 기초 문제집"
+ );
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 성공, 시간 기준 오랜 순 정렬")
+ void searchFromDateAsc() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "pk");
+ parameters.put("type", "user");
+ parameters.put("criteria", "date");
+ parameters.put("order", "asc");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ List workbookResponses = response.convertBodyToList(WorkbookResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(workbookResponses).hasSize(4);
+ assertThat(workbookResponses)
+ .extracting(WorkbookResponse::getName)
+ .containsExactly(
+ "피케이의 자바 기초 문제집",
+ "피케이의 자바 중급 문제집",
+ "피케이의 자바 고급 문제집",
+ "네트워크는 나의 것"
+ );
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 성공, 사전 역순 정렬")
+ void searchFromNameDesc() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "pk");
+ parameters.put("type", "user");
+ parameters.put("criteria", "name");
+ parameters.put("order", "desc");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ List workbookResponses = response.convertBodyToList(WorkbookResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(workbookResponses).hasSize(4);
+ assertThat(workbookResponses)
+ .extracting(WorkbookResponse::getName)
+ .containsExactly(
+ "피케이의 자바 중급 문제집",
+ "피케이의 자바 기초 문제집",
+ "피케이의 자바 고급 문제집",
+ "네트워크는 나의 것"
+ );
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 성공, 사전 순 정렬")
+ void searchFromNameAsc() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "pk");
+ parameters.put("type", "user");
+ parameters.put("criteria", "name");
+ parameters.put("order", "asc");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ List workbookResponses = response.convertBodyToList(WorkbookResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(workbookResponses).hasSize(4);
+ assertThat(workbookResponses)
+ .extracting(WorkbookResponse::getName)
+ .containsExactly(
+ "네트워크는 나의 것",
+ "피케이의 자바 고급 문제집",
+ "피케이의 자바 기초 문제집",
+ "피케이의 자바 중급 문제집"
+ );
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 성공, 좋아요 많은 순 정렬")
+ void searchFromHeartDesc() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "bear");
+ parameters.put("type", "user");
+ parameters.put("criteria", "heart");
+ parameters.put("order", "desc");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ List workbookResponses = response.convertBodyToList(WorkbookResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(workbookResponses).hasSize(3);
+ assertThat(workbookResponses)
+ .extracting(WorkbookResponse::getName)
+ .containsExactly(
+ "중간곰의 자바 중급 문제집",
+ "중간곰의 자바 기초 문제집",
+ "중간곰의 자바 고급 문제집"
+ );
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 성공, 좋아요 적은 순 정렬")
+ void searchFromHeartAsc() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "bear");
+ parameters.put("type", "user");
+ parameters.put("criteria", "heart");
+ parameters.put("order", "asc");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ List workbookResponses = response.convertBodyToList(WorkbookResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(workbookResponses).hasSize(3);
+ assertThat(workbookResponses)
+ .extracting(WorkbookResponse::getName)
+ .containsExactly(
+ "중간곰의 자바 고급 문제집",
+ "중간곰의 자바 기초 문제집",
+ "중간곰의 자바 중급 문제집"
+ );
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 성공, 카드 많은 순 정렬")
+ void searchFromCountDesc() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "pk");
+ parameters.put("type", "user");
+ parameters.put("criteria", "count");
+ parameters.put("order", "desc");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ List workbookResponses = response.convertBodyToList(WorkbookResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(workbookResponses).hasSize(4);
+ assertThat(workbookResponses)
+ .extracting(WorkbookResponse::getName)
+ .containsExactly(
+ "피케이의 자바 고급 문제집",
+ "피케이의 자바 중급 문제집",
+ "피케이의 자바 기초 문제집",
+ "네트워크는 나의 것"
+ );
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 성공, 카드 적은 순 정렬")
+ void searchFromCountAsc() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "pk");
+ parameters.put("type", "user");
+ parameters.put("criteria", "count");
+ parameters.put("order", "asc");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ List workbookResponses = response.convertBodyToList(WorkbookResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(workbookResponses).hasSize(4);
+ assertThat(workbookResponses)
+ .extracting(WorkbookResponse::getName)
+ .containsExactly(
+ "네트워크는 나의 것",
+ "피케이의 자바 기초 문제집",
+ "피케이의 자바 중급 문제집",
+ "피케이의 자바 고급 문제집"
+ );
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 성공, 요청한 크기보다 문제집을 조금 가진 경우 가지고 있는 문제집만 보여준다.")
+ void searchWithBiggerSizeThanStored() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "pk");
+ parameters.put("type", "user");
+ parameters.put("criteria", "date");
+ parameters.put("order", "desc");
+ parameters.put("size", "50");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ List workbookResponses = response.convertBodyToList(WorkbookResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(workbookResponses).hasSize(4);
+ assertThat(workbookResponses)
+ .extracting(WorkbookResponse::getName)
+ .containsExactly(
+ "네트워크는 나의 것",
+ "피케이의 자바 고급 문제집",
+ "피케이의 자바 중급 문제집",
+ "피케이의 자바 기초 문제집"
+ );
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 성공, 페이징 이용")
+ void searchWithPaging() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "pk");
+ parameters.put("type", "user");
+ parameters.put("criteria", "date");
+ parameters.put("order", "desc");
+ parameters.put("start", "1");
+ parameters.put("size", "2");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ List workbookResponses = response.convertBodyToList(WorkbookResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(workbookResponses).hasSize(2);
+ assertThat(workbookResponses)
+ .extracting(WorkbookResponse::getName)
+ .containsExactly(
+ "피케이의 자바 중급 문제집",
+ "피케이의 자바 기초 문제집"
+ );
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 성공, 카드의 수가 0개인 문제집은 조회되지 않음")
+ void searchExceptZeroCards() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "bear");
+ parameters.put("type", "user");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ List workbookResponses = response.convertBodyToList(WorkbookResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(workbookResponses).hasSize(3);
+ assertThat(workbookResponses)
+ .extracting(WorkbookResponse::getCardCount)
+ .allMatch(cardCount -> cardCount > 0);
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 실패, 검색어 없음")
+ void searchWithNoKeyword() {
+ // given
+ Map parameters = Map.of("type", "name");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).contains("검색어는 null일 수 없습니다");
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 실패, 30자를 초과하는 검색어")
+ void searchWithLongKeyword() {
+ // given
+ Map parameters = Map.of("keyword", stringGenerator(31));
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).contains("검색어는 30자 이하여야 합니다");
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 실패, 지원하지 않는 검색 타입")
+ void searchWithInvalidType() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "java");
+ parameters.put("type", "alphabet");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).contains("유효하지 않은 검색 타입입니다. 유효한 검색 타임 : name, tag, user");
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 실패, 지원하지 않는 정렬 기준")
+ void searchWithInvalidCriteria() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "java");
+ parameters.put("criteria", "random");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).contains("유효하지 않은 정렬 조건입니다. 유효한 정렬 조건 : date, name, count, heart");
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 실패, 지원하지 않는 정렬 방법")
+ void searchWithInvalidOrder() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "java");
+ parameters.put("order", "center");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).contains("유효하지 않은 정렬 방향입니다. 유효한 정렬 방식 : ASC, DESC");
+ }
+
+ @Test
+ @DisplayName("문제집 검색 - 올바르지 않은 시작 페이지")
+ void searchWithInvalidStart() {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "java");
+ parameters.put("start", "-1");
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).contains("페이지의 시작 값은 음수가 될 수 없습니다");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"0", "-1", "101"})
+ @DisplayName("문제집 검색 - 올바르지 않은 페이지 크기")
+ void searchWithInvalidSize(String size) {
+ // given
+ Map parameters = new HashMap<>();
+ parameters.put("keyword", "java");
+ parameters.put("size", size);
+
+ // when
+ HttpResponse response = 문제집_검색_요청(parameters);
+
+ // then
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).contains("유효하지 않은 페이지 크기입니다. 유효한 크기 : 1 ~ 100");
+ }
+
+ @Test
+ @DisplayName("태그 검색 - 성공")
+ void searchTagsWithKeyword() {
+ // given
+ String keyword = "ava";
+
+ // when
+ HttpResponse response = 태그_검색_요청(keyword);
+
+ // then
+ List tagResponses = response.convertBodyToList(TagResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(tagResponses).hasSize(1);
+ assertThat(tagResponses)
+ .extracting(TagResponse::getName)
+ .allMatch(name -> name.contains(keyword));
+ }
+
+ @Test
+ @DisplayName("유저 검색 - 성공")
+ void searchUsersWithKeyword() {
+ // given
+ String keyword = "ear";
+
+ // when
+ HttpResponse response = 유저_검색_요청(keyword);
+
+ // then
+ List searchUserResponse = response.convertBodyToList(SimpleUserResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(searchUserResponse).hasSize(1);
+ assertThat(searchUserResponse)
+ .extracting(SimpleUserResponse::getName)
+ .allMatch(name -> name.contains(keyword));
+ }
+
+ private HttpResponse 문제집_검색_요청(Map parameters) {
+ return request()
+ .get("/api/search/workbooks")
+ .queryParams(parameters)
+ .auth(createToken(admin.getId()))
+ .build();
+ }
+
+ private HttpResponse 태그_검색_요청(String keyword) {
+ return request()
+ .get("/api/search/tags")
+ .queryParam("keyword", keyword)
+ .auth(createToken(admin.getId()))
+ .build();
+ }
+
+ private HttpResponse 유저_검색_요청(String keyword) {
+ return request()
+ .get("/api/search/users")
+ .queryParam("keyword", keyword)
+ .auth(createToken(admin.getId()))
+ .build();
+ }
+}
diff --git a/backend/src/test/java/botobo/core/acceptance/user/UserAcceptanceTest.java b/backend/src/test/java/botobo/core/acceptance/user/UserAcceptanceTest.java
index 66255ec1..bd3ca66f 100644
--- a/backend/src/test/java/botobo/core/acceptance/user/UserAcceptanceTest.java
+++ b/backend/src/test/java/botobo/core/acceptance/user/UserAcceptanceTest.java
@@ -1,32 +1,51 @@
package botobo.core.acceptance.user;
-import botobo.core.acceptance.AuthAcceptanceTest;
+import botobo.core.acceptance.DomainAcceptanceTest;
import botobo.core.acceptance.utils.RequestBuilder.HttpResponse;
+import botobo.core.domain.user.SocialType;
+import botobo.core.dto.auth.GithubUserInfoResponse;
+import botobo.core.dto.auth.UserInfoResponse;
+import botobo.core.dto.user.ProfileResponse;
+import botobo.core.dto.user.UserNameRequest;
import botobo.core.dto.user.UserResponse;
-import botobo.core.exception.ErrorResponse;
+import botobo.core.dto.user.UserUpdateRequest;
+import botobo.core.exception.common.ErrorResponse;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
+import org.springframework.mock.web.MockMultipartFile;
+import static botobo.core.utils.Fixture.pk;
+import static botobo.core.utils.TestUtils.stringGenerator;
import static org.assertj.core.api.Assertions.assertThat;
-public class UserAcceptanceTest extends AuthAcceptanceTest {
+public class UserAcceptanceTest extends DomainAcceptanceTest {
+
+ @Value("${aws.user-default-image}")
+ private String userDefaultImage;
+
+ @Value("${aws.cloudfront.url-format}")
+ private String cloudfrontUrlFormat;
@Test
@DisplayName("로그인 한 유저의 정보를 가져온다. - 성공")
void findByUserOfMine() {
- //when
+ // given, when
final HttpResponse response = request()
.get("/api/users/me")
- .auth(로그인되어_있음().getAccessToken())
+ .auth(소셜_로그인되어_있음(pk, SocialType.GITHUB))
.build();
UserResponse userResponse = response.convertBody(UserResponse.class);
//then
assertThat(userResponse.getId()).isNotNull();
- assertThat(userResponse.getUserName()).isEqualTo("githubUser");
- assertThat(userResponse.getProfileUrl()).isEqualTo("github.io");
+ assertThat(userResponse.getUserName()).isEqualTo("pk");
+ assertThat(userResponse.getProfileUrl()).isEqualTo("pk.profile");
+ assertThat(userResponse.getBio()).isEqualTo("");
}
@Test
@@ -42,4 +61,465 @@ void findByUserOfMineWhenNotExistToken() {
assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
assertThat(errorResponse.getMessage()).isEqualTo("토큰이 유효하지 않습니다.");
}
+
+ @Test
+ @DisplayName("프로필 이미지를 수정한다. - 성공, multipartFile이 비어있는 경우 디폴트 유저 이미지로 대체")
+ void updateWhenDefaultProfile() {
+ //given
+ String defaultUserImageUrl = String.format(cloudfrontUrlFormat, userDefaultImage);
+ UserInfoResponse userInfoResponse = GithubUserInfoResponse.builder()
+ .userName("socialUser")
+ .socialId("2")
+ .profileUrl(defaultUserImageUrl)
+ .build();
+ MockMultipartFile mockMultipartFile = null;
+
+ //when
+ final HttpResponse response = request()
+ .post("/api/users/profile", mockMultipartFile)
+ .auth(소셜_로그인되어_있음(userInfoResponse, SocialType.GITHUB))
+ .build();
+
+ ProfileResponse profileResponse = response.convertBody(ProfileResponse.class);
+
+ //then
+ assertThat(profileResponse.getProfileUrl()).isNotNull();
+ assertThat(profileResponse.getProfileUrl()).isEqualTo(defaultUserImageUrl);
+ }
+
+ @Test
+ @DisplayName("로그인 한 유저의 정보를 수정한다. - 성공")
+ void update() {
+ //given
+ UserUpdateRequest userUpdateRequest = UserUpdateRequest.builder()
+ .userName("new조앤")
+ .profileUrl("github.io")
+ .bio("new 소개글")
+ .build();
+ //when
+ final HttpResponse response = request()
+ .put("/api/users/me", userUpdateRequest)
+ .auth(createToken(1L))
+ .build();
+
+ UserResponse userResponse = response.convertBody(UserResponse.class);
+
+ //then
+ assertThat(userResponse.getId()).isNotNull();
+ assertThat(userResponse.getUserName()).isEqualTo("new조앤");
+ assertThat(userResponse.getProfileUrl()).isEqualTo("github.io");
+ assertThat(userResponse.getBio()).isEqualTo("new 소개글");
+ }
+
+ @Test
+ @DisplayName("로그인 한 유저의 정보를 수정한다. - 실패, profileUrl이 다름.")
+ void updateFailedDifferentProfileUrl() {
+ //given
+ UserUpdateRequest userUpdateRequest = UserUpdateRequest.builder()
+ .userName("new조앤")
+ .profileUrl("another.profile.url")
+ .bio("new 소개글")
+ .build();
+ //when
+ final HttpResponse response = request()
+ .put("/api/users/me", userUpdateRequest)
+ .auth(createToken(1L))
+ .build();
+
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.FORBIDDEN);
+ assertThat(errorResponse.getMessage()).isEqualTo("프로필 이미지 수정은 불가합니다.");
+ }
+
+ @Test
+ @DisplayName("로그인 한 유저의 정보를 수정한다. - 실패, userName은 null이 될 수 없다.")
+ void updateFailedUserNameIsNull() {
+ //given
+ UserUpdateRequest userUpdateRequest = UserUpdateRequest.builder()
+ .userName(null)
+ .profileUrl("github.io")
+ .bio("new 소개글")
+ .build();
+ //when
+ final HttpResponse response = request()
+ .put("/api/users/me", userUpdateRequest)
+ .auth(createToken(1L))
+ .build();
+
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).isEqualTo("회원명은 필수 입력값입니다.");
+ }
+
+ @Test
+ @DisplayName("로그인 한 유저의 정보를 수정한다. - 실패, userName은 최소 한 글자 이상")
+ void updateFailedEmptyUserName() {
+ //given
+ UserUpdateRequest userUpdateRequest = UserUpdateRequest.builder()
+ .userName("")
+ .profileUrl("github.io")
+ .bio("new 소개글")
+ .build();
+ //when
+ final HttpResponse response = request()
+ .put("/api/users/me", userUpdateRequest)
+ .auth(createToken(1L))
+ .build();
+
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).isEqualTo("이름은 최소 1자 이상, 최대 20자까지 입력 가능합니다.");
+ }
+
+ @Test
+ @DisplayName("로그인 한 유저의 정보를 수정한다. - 실패, userName은 최대 20자")
+ void updateFailedLongUserName() {
+ //given
+ UserUpdateRequest userUpdateRequest = UserUpdateRequest.builder()
+ .userName(stringGenerator(21))
+ .profileUrl("github.io")
+ .bio("new 소개글")
+ .build();
+ //when
+ final HttpResponse response = request()
+ .put("/api/users/me", userUpdateRequest)
+ .auth(createToken(1L))
+ .build();
+
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).isEqualTo("이름은 최소 1자 이상, 최대 20자까지 입력 가능합니다.");
+ }
+
+ @Test
+ @DisplayName("로그인 한 유저의 정보를 수정한다. - 실패, 프로필 사진 필드는 null이 될 수 없다.")
+ void updateFailedProfileUrlIsNull() {
+ //given
+ UserUpdateRequest userUpdateRequest = UserUpdateRequest.builder()
+ .userName("new조앤")
+ .profileUrl(null)
+ .bio("new 소개글")
+ .build();
+ //when
+ final HttpResponse response = request()
+ .put("/api/users/me", userUpdateRequest)
+ .auth(createToken(1L))
+ .build();
+
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).isEqualTo("회원 정보를 수정하기 위해서는 프로필 사진이 필요합니다.");
+ }
+
+ @Test
+ @DisplayName("로그인 한 유저의 정보를 수정한다. - 실패, 프로필 사진 필드는 ''이 될 수 없다.")
+ void updateFailedProfileUrlIsEmpty() {
+ //given
+ UserUpdateRequest userUpdateRequest = UserUpdateRequest.builder()
+ .userName("new조앤")
+ .profileUrl("")
+ .bio("new 소개글")
+ .build();
+ //when
+ final HttpResponse response = request()
+ .put("/api/users/me", userUpdateRequest)
+ .auth(createToken(1L))
+ .build();
+
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).isEqualTo("회원 정보를 수정하기 위해서는 프로필 사진이 필요합니다.");
+ }
+
+ @Test
+ @DisplayName("로그인 한 유저의 정보를 수정한다. - 실패, 소개글은 null이 될 수 없다.")
+ void updateFailedBioIsNull() {
+ //given
+ UserUpdateRequest userUpdateRequest = UserUpdateRequest.builder()
+ .userName("new조앤")
+ .profileUrl("github.io")
+ .bio(null)
+ .build();
+ //when
+ final HttpResponse response = request()
+ .put("/api/users/me", userUpdateRequest)
+ .auth(createToken(1L))
+ .build();
+
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).isEqualTo("회원 정보를 수정하기 위해서는 소개글은 최소 0자 이상이 필요합니다.");
+ }
+
+ @Test
+ @DisplayName("로그인 한 유저의 정보를 수정한다. - 실패, 소개글은 최대 255자까지만 가능하다.")
+ void updateFailedLongBio() {
+ //given
+ UserUpdateRequest userUpdateRequest = UserUpdateRequest.builder()
+ .userName("new조앤")
+ .profileUrl("github.io")
+ .bio(stringGenerator(256))
+ .build();
+ //when
+ final HttpResponse response = request()
+ .put("/api/users/me", userUpdateRequest)
+ .auth(createToken(1L))
+ .build();
+
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).isEqualTo("소개글은 최대 255자까지 가능합니다.");
+ }
+
+ @Test
+ @DisplayName("로그인 한 유저의 정보를 수정한다. - 실패, 회원명은 중복될 수 없다.")
+ void updateFailedDuplicateUserName() {
+ //given
+ Long id = anyUser().getId();// 유저 2번을 save한다. {이름 : "joanne"}
+ UserUpdateRequest userUpdateRequest = UserUpdateRequest.builder()
+ .userName("admin") // 기존에 존재하는 1번 유저의 admin 이름으로 변경한다.
+ .profileUrl("github.io")
+ .bio("안녕하세요~")
+ .build();
+ //when
+ final HttpResponse response = request()
+ .put("/api/users/me", userUpdateRequest)
+ .auth(createToken(id))
+ .build();
+
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.CONFLICT);
+ assertThat(errorResponse.getMessage()).isEqualTo("이미 존재하는 회원 이름입니다.");
+ }
+
+ @Test
+ @DisplayName("로그인 한 유저의 정보를 수정한다. - 성공, 요청으로 들어온 이름이 기존 이름인 경우에는 수정 가능")
+ void updateFailedDuplicateUserNameButItsMe() {
+ //given
+ UserUpdateRequest userUpdateRequest = UserUpdateRequest.builder()
+ .userName("admin") // 기존에 존재하는 1번 유저가 이름 변경없이 admin 이름으로 변경한다.
+ .profileUrl("github.io")
+ .bio("안녕하세요~")
+ .build();
+ //when
+ final HttpResponse response = request()
+ .put("/api/users/me", userUpdateRequest)
+ .auth(createToken(1L))
+ .build();
+
+ // then
+ UserResponse userResponse = response.convertBody(UserResponse.class);
+
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(userResponse.getId()).isNotNull();
+ assertThat(userResponse.getUserName()).isEqualTo("admin");
+ assertThat(userResponse.getProfileUrl()).isEqualTo("github.io");
+ assertThat(userResponse.getBio()).isEqualTo("안녕하세요~");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {" ", "카일 안녕", "나는 조앤 하이", " ", " ", "\t", "\n", "\r\n", "\r"})
+ @DisplayName("로그인 한 유저의 정보를 수정한다. - 실패, 회원명에는 공백이 포함될 수 없다.")
+ void updateFailedWhiteSpaceUserName(String name) {
+ //given
+ UserUpdateRequest userUpdateRequest = UserUpdateRequest.builder()
+ .userName(name)
+ .profileUrl("github.io")
+ .bio("안녕하세요~")
+ .build();
+ //when
+ final HttpResponse response = request()
+ .put("/api/users/me", userUpdateRequest)
+ .auth(createToken(1L))
+ .build();
+
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).isEqualTo("회원명에 공백은 포함될 수 없습니다.");
+ }
+
+ @Test
+ @DisplayName("로그인 한 유저의 정보를 수정한다. - 성공, 변경사항이 없어도 요청에서는 실패하지 않는다.")
+ void updateWhenRequestIsSame() {
+ //given
+ UserUpdateRequest userUpdateRequest = UserUpdateRequest.builder()
+ .userName("admin") // 기존에 존재하는 1번 유저의 admin 이름으로 변경한다.
+ .profileUrl("github.io")
+ .bio("")
+ .build();
+ //when
+ final HttpResponse response = request()
+ .put("/api/users/me", userUpdateRequest)
+ .auth(createToken(1L))
+ .build();
+
+ UserResponse userResponse = response.convertBody(UserResponse.class);
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(userResponse.getId()).isNotNull();
+ assertThat(userResponse.getUserName()).isEqualTo("admin");
+ assertThat(userResponse.getProfileUrl()).isEqualTo("github.io");
+ assertThat(userResponse.getBio()).isEqualTo("");
+ }
+
+ @Test
+ @DisplayName("회원명을 중복 조회한다. - 성공, 중복되지 않은 이름")
+ void checkSameUserNameAlreadyExist() {
+ //given
+ UserNameRequest userNameRequest = UserNameRequest.builder()
+ .userName("클러치박")
+ .build();
+ //when
+ final HttpResponse response = request()
+ .post("/api/users/name-check", userNameRequest)
+ .auth(createToken(1L))
+ .build();
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ }
+
+ @Test
+ @DisplayName("회원명을 중복 조회한다. - 성공, 현재 로그인한 회원의 이름과 동일할 때에도 OK를 보낸다.")
+ void checkSameUserNameAlreadyExistWhenSameUser() {
+ //given
+ UserNameRequest userNameRequest = UserNameRequest.builder()
+ .userName("admin")
+ .build();
+ //when
+ final HttpResponse response = request()
+ .post("/api/users/name-check", userNameRequest)
+ .auth(createToken(1L))
+ .build();
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
+ }
+
+ @Test
+ @DisplayName("회원명을 중복 조회한다. - 실패, 중복된 이름")
+ void checkSameUserNameAlreadyExistFailed() {
+ //given
+ Long id = anyUser().getId();// 유저 2번을 save한다. {이름 : "joanne"}
+ UserNameRequest userNameRequest = UserNameRequest.builder()
+ .userName("admin")
+ .build();
+ //when
+ final HttpResponse response = request()
+ .post("/api/users/name-check", userNameRequest)
+ .auth(createToken(id))
+ .build();
+
+ ErrorResponse errorResponse = response.convertToErrorResponse();
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.CONFLICT);
+ assertThat(errorResponse.getMessage()).isEqualTo("이미 존재하는 회원 이름입니다.");
+ }
+
+ @Test
+ @DisplayName("회원명을 중복 조회한다. - 실패, 요청 회원 명은 null이 될 수 없다.")
+ void checkSameUserNameAlreadyExistFailedWhenNameIsNull() {
+ //given
+ UserNameRequest userNameRequest = UserNameRequest.builder()
+ .userName(null)
+ .build();
+ //when
+ final HttpResponse response = request()
+ .post("/api/users/name-check", userNameRequest)
+ .auth(createToken(1L))
+ .build();
+
+ //then
+ ErrorResponse errorResponse = response.convertToErrorResponse();
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).isEqualTo("회원명은 필수 입력값입니다.");
+ }
+
+ @Test
+ @DisplayName("회원명을 중복 조회한다. - 실패, 요청 회원 명은 최소 한글자 이상이어야한다.")
+ void checkSameUserNameAlreadyExistFailedWhenNameIsEmpty() {
+ //given
+ UserNameRequest userNameRequest = UserNameRequest.builder()
+ .userName("")
+ .build();
+ //when
+ final HttpResponse response = request()
+ .post("/api/users/name-check", userNameRequest)
+ .auth(createToken(1L))
+ .build();
+
+ //then
+ ErrorResponse errorResponse = response.convertToErrorResponse();
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).isEqualTo("이름은 최소 1자 이상, 최대 20자까지 입력 가능합니다.");
+ }
+
+ @Test
+ @DisplayName("회원명을 중복 조회한다. - 실패, 요청 회원 명은 최대 20글자까지 가능하다.")
+ void checkSameUserNameAlreadyExistFailedWhenNameIsLong() {
+ //given
+ UserNameRequest userNameRequest = UserNameRequest.builder()
+ .userName(stringGenerator(21))
+ .build();
+ //when
+ final HttpResponse response = request()
+ .post("/api/users/name-check", userNameRequest)
+ .auth(createToken(1L))
+ .build();
+
+ //then
+ ErrorResponse errorResponse = response.convertToErrorResponse();
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).isEqualTo("이름은 최소 1자 이상, 최대 20자까지 입력 가능합니다.");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {" ", "카일 안녕", "나는 조앤 하이", " ", " ", "\t", "\n", "\r\n", "\r"})
+ @DisplayName("로그인 한 유저의 정보를 수정한다. - 실패, userName에는 공백이 포함될 수 없다.")
+ void updateFailedWithWhiteSpace(String name) {
+ //given
+ UserNameRequest userNameRequest = UserNameRequest.builder()
+ .userName(name)
+ .build();
+ //when
+ final HttpResponse response = request()
+ .post("/api/users/name-check", userNameRequest)
+ .auth(createToken(1L))
+ .build();
+
+ //then
+ ErrorResponse errorResponse = response.convertToErrorResponse();
+
+ //then
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).isEqualTo("회원명에 공백은 포함될 수 없습니다.");
+ }
}
diff --git a/backend/src/test/java/botobo/core/acceptance/utils/RequestBuilder.java b/backend/src/test/java/botobo/core/acceptance/utils/RequestBuilder.java
index 0ebbfaba..5dcc1693 100644
--- a/backend/src/test/java/botobo/core/acceptance/utils/RequestBuilder.java
+++ b/backend/src/test/java/botobo/core/acceptance/utils/RequestBuilder.java
@@ -1,6 +1,6 @@
package botobo.core.acceptance.utils;
-import botobo.core.exception.ErrorResponse;
+import botobo.core.exception.common.ErrorResponse;
import io.restassured.RestAssured;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
@@ -13,13 +13,13 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
public class RequestBuilder {
- private static String defaultToken;
private static String accessToken;
- public RequestBuilder(String defaultToken) {
- RequestBuilder.defaultToken = defaultToken;
+ public RequestBuilder(String accessToken) {
+ RequestBuilder.accessToken = accessToken;
}
public HttpFunction build() {
@@ -39,21 +39,24 @@ public Options put(String path, T body, Object... params) {
return new Options(new PutRequest<>(path, body, params));
}
+ public Options putWithoutBody(String path, Object... params) {
+ return new Options(new PutRequest<>(path, params));
+ }
+
public Options delete(String path, Object... params) {
return new Options(new DeleteRequest(path, params));
}
}
public static class Options {
+ private final List> queryParams;
private final RestAssuredRequest request;
- private RequestSpecification requestSpecification;
+ private String customAccessToken;
private boolean loginFlag;
private boolean logFlag;
- private final List> queryParams;
public Options(RestAssuredRequest request) {
this.request = request;
- this.requestSpecification = RestAssured.given();
this.loginFlag = false;
this.logFlag = false;
this.queryParams = new ArrayList<>();
@@ -61,18 +64,17 @@ public Options(RestAssuredRequest request) {
public Options log() {
this.logFlag = true;
- this.requestSpecification = requestSpecification.log().all();
return this;
}
- public Options auth() {
+ public Options failAuth() {
this.loginFlag = true;
return this;
}
public Options auth(String token) {
- RequestBuilder.accessToken = token;
this.loginFlag = true;
+ this.customAccessToken = token;
return this;
}
@@ -81,18 +83,19 @@ public Options queryParam(String key, String value) {
return this;
}
+ public Options queryParams(Map parameters) {
+ this.queryParams.addAll(parameters.entrySet());
+ return this;
+ }
+
public HttpResponse build() {
+ RequestSpecification requestSpecification = RestAssured.given();
if (loginFlag) {
- if (accessToken != null && !defaultToken.equals(accessToken)) {
- requestSpecification = requestSpecification.header("Authorization", "Bearer " + accessToken);
- accessToken = null;
- } else {
- requestSpecification = requestSpecification.header("Authorization", "Bearer " + defaultToken);
- }
+ requestSpecification = addAuthHeader(requestSpecification, getToken());
+ }
+ for (Map.Entry param : queryParams) {
+ requestSpecification = addParams(requestSpecification, param);
}
- queryParams.forEach(entry -> {
- requestSpecification.queryParam(entry.getKey(), entry.getValue());
- });
ValidatableResponse response = request.action(requestSpecification);
if (logFlag) {
response = response.log().all();
@@ -100,6 +103,22 @@ public HttpResponse build() {
return new HttpResponse(response.extract());
}
+ private String getToken() {
+ if (Objects.isNull(customAccessToken)) {
+ return accessToken;
+ }
+ final String token = customAccessToken;
+ customAccessToken = null;
+ return token;
+ }
+
+ private RequestSpecification addParams(RequestSpecification requestSpecification, Map.Entry param) {
+ return requestSpecification.queryParam(param.getKey(), param.getValue());
+ }
+
+ private RequestSpecification addAuthHeader(RequestSpecification requestSpecification, String token) {
+ return requestSpecification.header("Authorization", "Bearer " + token);
+ }
}
public static class HttpResponse {
@@ -167,6 +186,11 @@ public PostRequest(String path, T body, Object[] params) {
@Override
public ValidatableResponse action(RequestSpecification specification) {
+ if (Objects.isNull(body)) {
+ return specification
+ .post(path, params)
+ .then();
+ }
return specification.body(body)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.post(path, params)
@@ -179,6 +203,10 @@ private static class PutRequest implements RestAssuredRequest {
private final T body;
private final Object[] params;
+ public PutRequest(String path, Object[] params) {
+ this(path, null, params);
+ }
+
public PutRequest(String path, T body, Object[] params) {
this.path = path;
this.body = body;
@@ -187,7 +215,10 @@ public PutRequest(String path, T body, Object[] params) {
@Override
public ValidatableResponse action(RequestSpecification specification) {
- return specification.body(body)
+ if (Objects.nonNull(body)) {
+ specification.body(body);
+ }
+ return specification
.contentType(MediaType.APPLICATION_JSON_VALUE)
.put(path, params)
.then();
diff --git a/backend/src/test/java/botobo/core/acceptance/workbook/WorkbookAcceptanceTest.java b/backend/src/test/java/botobo/core/acceptance/workbook/WorkbookAcceptanceTest.java
index 03cdbf2b..8ee0eca6 100644
--- a/backend/src/test/java/botobo/core/acceptance/workbook/WorkbookAcceptanceTest.java
+++ b/backend/src/test/java/botobo/core/acceptance/workbook/WorkbookAcceptanceTest.java
@@ -2,27 +2,21 @@
import botobo.core.acceptance.DomainAcceptanceTest;
import botobo.core.acceptance.utils.RequestBuilder.HttpResponse;
-import botobo.core.dto.auth.GithubUserInfoResponse;
-import botobo.core.dto.auth.LoginRequest;
-import botobo.core.dto.auth.TokenResponse;
+import botobo.core.domain.user.SocialType;
import botobo.core.dto.card.CardResponse;
import botobo.core.dto.card.ScrapCardRequest;
+import botobo.core.dto.heart.HeartResponse;
import botobo.core.dto.tag.TagRequest;
import botobo.core.dto.workbook.WorkbookCardResponse;
import botobo.core.dto.workbook.WorkbookRequest;
import botobo.core.dto.workbook.WorkbookResponse;
import botobo.core.dto.workbook.WorkbookUpdateRequest;
-import botobo.core.exception.ErrorResponse;
-import botobo.core.infrastructure.GithubOauthManager;
-import io.restassured.response.ExtractableResponse;
-import io.restassured.response.Response;
-import org.junit.jupiter.api.BeforeEach;
+import botobo.core.exception.common.ErrorResponse;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
-import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import java.util.ArrayList;
@@ -30,45 +24,20 @@
import java.util.Collections;
import java.util.List;
-import static botobo.core.utils.Fixture.CARD_REQUEST_1;
-import static botobo.core.utils.Fixture.CARD_REQUEST_2;
-import static botobo.core.utils.Fixture.CARD_REQUEST_3;
-import static botobo.core.utils.Fixture.WORKBOOK_REQUEST_1;
-import static botobo.core.utils.Fixture.WORKBOOK_REQUEST_2;
-import static botobo.core.utils.Fixture.WORKBOOK_REQUEST_3;
+import static botobo.core.utils.Fixture.bear;
+import static botobo.core.utils.Fixture.oz;
+import static botobo.core.utils.Fixture.pk;
import static botobo.core.utils.TestUtils.stringGenerator;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.BDDMockito.given;
@DisplayName("Workbook 인수 테스트")
public class WorkbookAcceptanceTest extends DomainAcceptanceTest {
- @MockBean
- private GithubOauthManager githubOauthManager;
-
- private GithubUserInfoResponse userInfo, anotherUserInfo;
-
- @BeforeEach
- void setFixture() {
- 여러개_문제집_생성_요청(Arrays.asList(WORKBOOK_REQUEST_1, WORKBOOK_REQUEST_2, WORKBOOK_REQUEST_3));
- 여러개_카드_생성_요청(Arrays.asList(CARD_REQUEST_1, CARD_REQUEST_2, CARD_REQUEST_3));
- userInfo = GithubUserInfoResponse.builder()
- .userName("githubUser")
- .githubId(2L)
- .profileUrl("github.io")
- .build();
- anotherUserInfo = GithubUserInfoResponse.builder()
- .userName("anotherUser")
- .githubId(3L)
- .profileUrl("github.io")
- .build();
- }
-
@Test
@DisplayName("유저가 문제집 추가 - 성공")
void createWorkbookByUser() {
// given
+ final String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
TagRequest tagRequest = TagRequest.builder().id(0L).name("자바").build();
WorkbookRequest workbookRequest = WorkbookRequest.builder()
.name("Java 문제집")
@@ -77,7 +46,7 @@ void createWorkbookByUser() {
.build();
// when
- final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, userInfo);
+ final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, accessToken);
// then
WorkbookResponse workbookResponse = response.convertBody(WorkbookResponse.class);
@@ -89,19 +58,21 @@ void createWorkbookByUser() {
assertThat(workbookResponse.getTags()).hasSize(1);
assertThat(workbookResponse.getTags().get(0).getId()).isNotZero();
assertThat(workbookResponse.getTags().get(0).getName()).isEqualTo("자바");
+ assertThat(workbookResponse.getHeartCount()).isEqualTo(0);
}
@Test
- @DisplayName("유저 문제집 추가시 opened와 tags는 필수가 아니다 - 기본값 (opened = false, tags = empty list)")
+ @DisplayName("문제집 추가 요청 - 성공, opened와 tags는 필수가 아니다 - 기본값 (opened = false, tags = empty list)")
void createWorkbookByUserWithTags() {
// given
+ final String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
WorkbookRequest workbookRequest = WorkbookRequest.builder()
.name("Java 문제집")
.tags(new ArrayList<>())
.build();
// when
- final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, userInfo);
+ final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, accessToken);
// then
WorkbookResponse workbookResponse = response.convertBody(WorkbookResponse.class);
@@ -116,42 +87,45 @@ void createWorkbookByUserWithTags() {
@DisplayName("유저가 문제집 추가 - 실패, name이 없을 때")
void createWorkbookByUserWhenNameNotExist(String name) {
// given
+ final String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
WorkbookRequest workbookRequest = WorkbookRequest.builder()
.name(name)
.opened(true)
.build();
// when
- final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, userInfo);
+ final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, accessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(errorResponse.getMessage()).isEqualTo("이름은 필수 입력값입니다.");
+ assertThat(errorResponse.getMessage()).isEqualTo("문제집 이름은 필수 입력값입니다.");
}
@Test
@DisplayName("유저가 문제집 추가 - 실패, name이 30자 초과")
void createWorkbookByUserWhenNameLengthOver30() {
// given
+ final String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
WorkbookRequest workbookRequest = WorkbookRequest.builder()
.name(stringGenerator(31))
.opened(true)
.build();
// when
- final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, userInfo);
+ final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, accessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(errorResponse.getMessage()).isEqualTo("이름은 최대 30자까지 입력 가능합니다.");
+ assertThat(errorResponse.getMessage()).isEqualTo("문제집 이름은 30자 이하여야 합니다.");
}
@Test
@DisplayName("유저가 문제집 추가 - 실패, Tag 아이디 없음")
void createWorkbookByUserWhenTagIdNull() {
// given
+ final String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
TagRequest tagRequest = TagRequest.builder().name("자바").build();
WorkbookRequest workbookRequest = WorkbookRequest.builder()
.name("자바 문제집")
@@ -160,7 +134,7 @@ void createWorkbookByUserWhenTagIdNull() {
.build();
// when
- final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, userInfo);
+ final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, accessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
@@ -172,6 +146,7 @@ void createWorkbookByUserWhenTagIdNull() {
@DisplayName("유저가 문제집 추가 - 실패, Tag 아이디 음수")
void createWorkbookByUserWhenTagIdNegative() {
// given
+ final String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
TagRequest tagRequest = TagRequest.builder().id(-1L).name("자바").build();
WorkbookRequest workbookRequest = WorkbookRequest.builder()
.name("자바 문제집")
@@ -180,7 +155,7 @@ void createWorkbookByUserWhenTagIdNegative() {
.build();
// when
- final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, userInfo);
+ final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, accessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
@@ -192,6 +167,7 @@ void createWorkbookByUserWhenTagIdNegative() {
@DisplayName("유저가 문제집 추가 - 실패, Tag 이름 없음")
void createWorkbookByUserWhenTagNameNull() {
// given
+ final String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
TagRequest tagRequest = TagRequest.builder().id(0L).build();
WorkbookRequest workbookRequest = WorkbookRequest.builder()
.name("자바 문제집")
@@ -200,7 +176,7 @@ void createWorkbookByUserWhenTagNameNull() {
.build();
// when
- final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, userInfo);
+ final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, accessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
@@ -212,6 +188,7 @@ void createWorkbookByUserWhenTagNameNull() {
@DisplayName("유저가 문제집 추가 - 실패, 20자를 초과하는 Tag 이름")
void createWorkbookByUserWhenTagNameLong() {
// given
+ final String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
TagRequest tagRequest = TagRequest.builder().id(0L).name(stringGenerator(21)).build();
WorkbookRequest workbookRequest = WorkbookRequest.builder()
.name("자바 문제집")
@@ -220,21 +197,27 @@ void createWorkbookByUserWhenTagNameLong() {
.build();
// when
- final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, userInfo);
+ final HttpResponse response = 유저_문제집_생성_요청(workbookRequest, accessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(errorResponse.getMessage()).contains("태그는 최대 20자까지 입력 가능합니다");
+ assertThat(errorResponse.getMessage()).contains("태그는 20자 이하여야 합니다.");
}
@Test
@DisplayName("문제집 전체 조회 - 성공")
void findWorkbooksByUser() {
+ // given
+ final String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
+ 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
+ 유저_태그_포함_문제집_등록되어_있음("Spring 문제집", true, accessToken);
+ 유저_태그_포함_문제집_등록되어_있음("Database 문제집", true, accessToken);
+
// when
final HttpResponse response = request()
.get("/api/workbooks")
- .auth(jwtTokenProvider.createToken(user.getId()))
+ .auth(accessToken)
.build();
// then
@@ -259,75 +242,99 @@ void findWorkbooksByAnonymous() {
@Test
@DisplayName("문제집의 카드 모아보기 (카드 존재) - 성공")
void findCategoryCardsById() {
+ // given
+ final String accessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
+ 유저_카드_등록되어_있음("question", "answer", workbookResponse.getId(), accessToken);
+ 유저_카드_등록되어_있음("question", "answer", workbookResponse.getId(), accessToken);
+ 유저_카드_등록되어_있음("question", "answer", workbookResponse.getId(), accessToken);
+
// when
final HttpResponse response = request()
- .get("/api/workbooks/{id}/cards", 1L)
- .auth()
+ .get("/api/workbooks/{id}/cards", workbookResponse.getId())
+ .auth(accessToken)
.build();
+
// then
final WorkbookCardResponse workbookCardResponse = response.convertBody(WorkbookCardResponse.class);
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
- assertThat(workbookCardResponse.getWorkbookName()).isEqualTo(WORKBOOK_REQUEST_1.getName());
+ assertThat(workbookCardResponse.getWorkbookName()).isEqualTo(workbookResponse.getName());
assertThat(workbookCardResponse.getCards()).hasSize(3);
}
@Test
@DisplayName("문제집의 카드 모아보기 (카드 0개) - 성공")
void findWorkbookCardsByIdWithNotExistsCard() {
+ // given
+ final String accessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
+
// when
- HttpResponse response = request()
- .get("/api/workbooks/{id}/cards", 2L)
- .auth()
+ final HttpResponse response = request()
+ .get("/api/workbooks/{id}/cards", workbookResponse.getId())
+ .auth(accessToken)
.build();
// then
final WorkbookCardResponse workbookCardResponse = response.convertBody(WorkbookCardResponse.class);
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
- assertThat(workbookCardResponse.getWorkbookName()).isEqualTo(WORKBOOK_REQUEST_2.getName());
+ assertThat(workbookCardResponse.getWorkbookName()).isEqualTo(workbookResponse.getName());
assertThat(workbookCardResponse.getCards()).isEmpty();
}
@Test
- @DisplayName("공유 문제집 검색 - 성공")
- void findPublicWorkbooksBySearch() {
+ @DisplayName("문제집의 카드 모아보기 - 실패, 자신의 문제집이 아닌 경우")
+ void findWorkbookCardsByIdWithOtherUser() {
+ // given
+ final String accessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
+
+ final String otherAccessToken = 소셜_로그인되어_있음(oz, SocialType.GITHUB);
+
// when
final HttpResponse response = request()
- .get("/api/workbooks/public")
- .queryParam("search", "1")
- .auth(jwtTokenProvider.createToken(user.getId()))
+ .get("/api/workbooks/{id}/cards", workbookResponse.getId())
+ .auth(otherAccessToken)
.build();
// then
- List workbookResponses = response.convertBodyToList(WorkbookResponse.class);
- assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
- assertThat(workbookResponses).hasSize(1);
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.FORBIDDEN);
+ assertThat(errorResponse.getMessage()).isEqualTo("작성자가 아니므로 권한이 없습니다.");
}
@Test
@DisplayName("공유 문제집 조회 - 성공")
void findPublicWorkbookById() {
+ final String accessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
+ 유저_카드_등록되어_있음("question", "answer", workbookResponse.getId(), accessToken);
+ 유저_카드_등록되어_있음("question", "answer", workbookResponse.getId(), accessToken);
+ 유저_카드_등록되어_있음("question", "answer", workbookResponse.getId(), accessToken);
+
// when
final HttpResponse response = request()
- .get("/api/workbooks/public/{id}", 1L)
- .auth()
+ .get("/api/workbooks/public/{id}", workbookResponse.getId())
+ .auth(accessToken)
.build();
// then
final WorkbookCardResponse workbookCardResponse = response.convertBody(WorkbookCardResponse.class);
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
- assertThat(workbookCardResponse.getWorkbookName()).isEqualTo(WORKBOOK_REQUEST_1.getName());
+ assertThat(workbookCardResponse.getWorkbookName()).isEqualTo(workbookResponse.getName());
assertThat(workbookCardResponse.getCards()).hasSize(3);
+ assertThat(workbookCardResponse.getHeart()).isNotNull();
}
@Test
@DisplayName("유저가 문제집 수정 - 성공")
void updateWorkbook() {
// given
+ final String accessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
List tagRequests = Collections.singletonList(
TagRequest.builder().id(0L).name("잡아").build()
);
- WorkbookResponse workbookResponse = 유저_문제집_등록되어_있음("Java 문제집", true, tagRequests, userInfo);
-
+ final WorkbookResponse workbookResponse = 유저_문제집_등록되어_있음("Java 문제집", true, tagRequests, accessToken);
List updatedTagRequests = Collections.singletonList(
TagRequest.builder().id(1L).name("자바").build()
);
@@ -335,11 +342,12 @@ void updateWorkbook() {
.name("Java 문제집 비공개버전")
.opened(false)
.cardCount(0)
+ .heartCount(0)
.tags(updatedTagRequests)
.build();
// when
- final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, userInfo);
+ final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, accessToken);
// then
WorkbookResponse updateResponse = response.convertBody(WorkbookResponse.class);
@@ -358,78 +366,86 @@ void updateWorkbook() {
@DisplayName("유저가 문제집 수정 - 실패, name이 없을 때")
void updateWorkbookWhenNameNotExist(String name) {
// given
- WorkbookResponse workbookResponse = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo);
+ final String accessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
WorkbookUpdateRequest workbookUpdateRequest = WorkbookUpdateRequest.builder()
.name(name)
.opened(true)
.cardCount(0)
+ .heartCount(0)
.tags(Collections.emptyList())
.build();
// when
- final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, userInfo);
+ final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, accessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(errorResponse.getMessage()).isEqualTo("이름은 필수 입력값입니다.");
+ assertThat(errorResponse.getMessage()).isEqualTo("문제집 이름은 필수 입력값입니다.");
}
@Test
@DisplayName("유저가 문제집 수정 - 실패, name이 30자 초과")
void updateWorkbookWhenNameLengthOver30() {
// given
- WorkbookResponse workbookResponse = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo);
+ final String accessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
WorkbookUpdateRequest workbookUpdateRequest = WorkbookUpdateRequest.builder()
.name(stringGenerator(31))
.opened(true)
.cardCount(0)
+ .heartCount(0)
.tags(Collections.emptyList())
.build();
// when
- final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, userInfo);
+ final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, accessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(errorResponse.getMessage()).isEqualTo("이름은 최대 30자까지 입력 가능합니다.");
+ assertThat(errorResponse.getMessage()).isEqualTo("문제집 이름은 30자 이하여야 합니다.");
}
@Test
@DisplayName("유저가 문제집 수정 - 실패, opened가 없을 때")
void updateWorkbookWhenOpenedNotExist() {
// given
- WorkbookResponse workbookResponse = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo);
+ final String accessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
WorkbookUpdateRequest workbookUpdateRequest = WorkbookUpdateRequest.builder()
- .name(stringGenerator(31))
+ .name(stringGenerator(30))
.cardCount(0)
+ .heartCount(0)
.tags(Collections.emptyList())
.build();
// when
- final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, userInfo);
+ final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, accessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(errorResponse.getMessage()).contains("문제집 공개여부는 필수 입력값입니다");
+ assertThat(errorResponse.getMessage()).contains("문제집의 공개 여부는 필수 입력값입니다.");
}
@Test
@DisplayName("유저가 문제집 수정 - 실패, cardCount가 음수")
void updateWorkbookWhenCardCountNegative() {
// given
- WorkbookResponse workbookResponse = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo);
+ final String accessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
WorkbookUpdateRequest workbookUpdateRequest = WorkbookUpdateRequest.builder()
.name("Java 문제집 비공개버전")
.opened(true)
.cardCount(-5)
+ .heartCount(0)
.tags(Collections.emptyList())
.build();
// when
- final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, userInfo);
+ final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, accessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
@@ -437,32 +453,58 @@ void updateWorkbookWhenCardCountNegative() {
assertThat(errorResponse.getMessage()).isEqualTo("카드 개수는 0이상 입니다.");
}
+ @Test
+ @DisplayName("유저가 문제집 수정 - 실패, heartCount가 음수")
+ void updateWorkbookWhenHeartCountNegative() {
+ // given
+ final String accessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
+ WorkbookUpdateRequest workbookUpdateRequest = WorkbookUpdateRequest.builder()
+ .name("Java 문제집 비공개버전")
+ .opened(true)
+ .cardCount(0)
+ .heartCount(-5)
+ .tags(Collections.emptyList())
+ .build();
+
+ // when
+ final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, accessToken);
+
+ // then
+ ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
+ assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(errorResponse.getMessage()).isEqualTo("좋아요 개수는 0이상 입니다.");
+ }
+
@Test
@DisplayName("유저가 문제집 수정 - 실패, Tag 없음")
void updateWorkbookByUserWhenTagNull() {
// given
- WorkbookResponse workbookResponse = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo);
+ final String accessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
WorkbookUpdateRequest workbookUpdateRequest = WorkbookUpdateRequest.builder()
.name("Java 문제집 비공개버전")
.opened(false)
.cardCount(0)
+ .heartCount(0)
.build();
// when
- final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, userInfo);
+ final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, accessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(errorResponse.getMessage()).contains("문제집를 수정하려면 태그가 필요합니다");
+ assertThat(errorResponse.getMessage()).contains("문제집을 수정하려면 태그가 필요합니다");
}
@Test
@DisplayName("유저가 문제집 수정 - 실패, Tag 아이디 없음")
void updateWorkbookByUserWhenTagIdNull() {
// given
- WorkbookResponse workbookResponse = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo);
+ final String accessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
List updatedTagRequests = Collections.singletonList(
TagRequest.builder().name("자바").build()
);
@@ -470,11 +512,12 @@ void updateWorkbookByUserWhenTagIdNull() {
.name("Java 문제집 비공개버전")
.opened(false)
.cardCount(0)
+ .heartCount(0)
.tags(updatedTagRequests)
.build();
// when
- final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, userInfo);
+ final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, accessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
@@ -486,7 +529,8 @@ void updateWorkbookByUserWhenTagIdNull() {
@DisplayName("유저가 문제집 수정 - 실패, Tag 아이디 음수")
void updateWorkbookByUserWhenTagIdNegative() {
// given
- WorkbookResponse workbookResponse = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo);
+ final String accessToken = 소셜_로그인되어_있음(oz, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
List updatedTagRequests = Collections.singletonList(
TagRequest.builder().id(-1L).name("자바").build()
);
@@ -494,11 +538,12 @@ void updateWorkbookByUserWhenTagIdNegative() {
.name("Java 문제집 비공개버전")
.opened(false)
.cardCount(0)
+ .heartCount(0)
.tags(updatedTagRequests)
.build();
// when
- final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, userInfo);
+ final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, accessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
@@ -510,7 +555,8 @@ void updateWorkbookByUserWhenTagIdNegative() {
@DisplayName("유저가 문제집 수정 - 실패, Tag 이름 없음")
void updateWorkbookByUserWhenTagNameNull() {
// given
- WorkbookResponse workbookResponse = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo);
+ final String accessToken = 소셜_로그인되어_있음(oz, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
List updatedTagRequests = Collections.singletonList(
TagRequest.builder().id(1L).build()
);
@@ -518,11 +564,12 @@ void updateWorkbookByUserWhenTagNameNull() {
.name("Java 문제집 비공개버전")
.opened(false)
.cardCount(0)
+ .heartCount(0)
.tags(updatedTagRequests)
.build();
// when
- final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, userInfo);
+ final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, accessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
@@ -534,7 +581,8 @@ void updateWorkbookByUserWhenTagNameNull() {
@DisplayName("유저가 문제집 수정 - 실패, 20자를 초과하는 Tag 이름")
void updateWorkbookByUserWhenTagNameLong() {
// given
- WorkbookResponse workbookResponse = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo);
+ final String accessToken = 소셜_로그인되어_있음(oz, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
List updatedTagRequests = Collections.singletonList(
TagRequest.builder().id(1L).name(stringGenerator(21)).build()
);
@@ -542,37 +590,37 @@ void updateWorkbookByUserWhenTagNameLong() {
.name("Java 문제집 비공개버전")
.opened(false)
.cardCount(0)
+ .heartCount(0)
.tags(updatedTagRequests)
.build();
// when
- final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, userInfo);
+ final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, accessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(errorResponse.getMessage()).contains("태그는 최대 20자까지 입력 가능합니다");
+ assertThat(errorResponse.getMessage()).contains("태그는 20자 이하여야 합니다.");
}
@Test
@DisplayName("유저가 문제집 수정 - 실패, 다른 유저가 수정을 시도할 때")
void updateWorkbookWithOtherUser() {
// given
- WorkbookResponse workbookResponse = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo);
+ final String accessToken = 소셜_로그인되어_있음(oz, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
+
+ String otherAccessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
WorkbookUpdateRequest workbookUpdateRequest = WorkbookUpdateRequest.builder()
.name("Java 문제집 비공개버전")
.opened(false)
.cardCount(0)
+ .heartCount(0)
.tags(Collections.emptyList())
.build();
- GithubUserInfoResponse otherUserInfo = GithubUserInfoResponse.builder()
- .userName("otherUser")
- .githubId(3L)
- .profileUrl("github.io")
- .build();
// when
- final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, otherUserInfo);
+ final HttpResponse response = 유저_문제집_수정_요청(workbookUpdateRequest, workbookResponse, otherAccessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
@@ -584,10 +632,11 @@ void updateWorkbookWithOtherUser() {
@DisplayName("유저가 문제집 삭제 - 성공")
void deleteWorkbook() {
// given
- WorkbookResponse workbookResponse = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo);
+ final String accessToken = 소셜_로그인되어_있음(oz, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
// when
- final HttpResponse response = 유저_문제집_삭제_요청(workbookResponse, userInfo);
+ final HttpResponse response = 유저_문제집_삭제_요청(workbookResponse, accessToken);
// then
assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT);
@@ -597,15 +646,13 @@ void deleteWorkbook() {
@DisplayName("유저가 문제집 삭제 - 실패, 다른 유저가 삭제를 시도할 때")
void deleteWorkbookWithOtherUser() {
// given
- WorkbookResponse workbookResponse = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo);
- GithubUserInfoResponse otherUserInfo = GithubUserInfoResponse.builder()
- .userName("otherUser")
- .githubId(3L)
- .profileUrl("github.io")
- .build();
+ final String accessToken = 소셜_로그인되어_있음(oz, SocialType.GITHUB);
+ final WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken);
+
+ final String otherAccessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
// when
- final HttpResponse response = 유저_문제집_삭제_요청(workbookResponse, otherUserInfo);
+ final HttpResponse response = 유저_문제집_삭제_요청(workbookResponse, otherAccessToken);
// then
ErrorResponse errorResponse = response.convertBody(ErrorResponse.class);
@@ -617,11 +664,13 @@ void deleteWorkbookWithOtherUser() {
@DisplayName("문제집으로 카드 가져오기 - 성공")
void scrapSelectedCardsToWorkbook() {
// given
- final Long workbookId = 유저_문제집_등록되어_있음("Spring 문제집", true, userInfo).getId();
+ final String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
+ final Long workbookId = 유저_태그_포함_문제집_등록되어_있음("Spring 문제집", true, accessToken).getId();
- 유저_문제집_등록되어_있음("Java 문제집", true, userInfo);
- CardResponse response1 = 카드_등록되어_있음("question", "answer", 1L, 1L);
- CardResponse response2 = 카드_등록되어_있음("question", "answer", 1L, 1L);
+ final String otherAccessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ final Long otherWorkbookId = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, otherAccessToken).getId();
+ CardResponse response1 = 유저_카드_등록되어_있음("question", "answer", otherWorkbookId, otherAccessToken);
+ CardResponse response2 = 유저_카드_등록되어_있음("question", "answer", otherWorkbookId, otherAccessToken);
final ScrapCardRequest scrapCardRequest = ScrapCardRequest.builder()
.cardIds(Arrays.asList(response1.getId(), response2.getId()))
@@ -630,30 +679,7 @@ void scrapSelectedCardsToWorkbook() {
// when
final HttpResponse response = request()
.post("/api/workbooks/{id}/cards", scrapCardRequest, workbookId)
- .auth(로그인되어_있음(userInfo).getAccessToken())
- .build();
-
- // then
- assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED);
- assertThat(response.header("Location")).isEqualTo(String.format("/api/workbooks/%d/cards", workbookId));
- }
-
- @Test
- @DisplayName("문제집으로 카드 가져오기 - 성공, 중복되는 카드가 존재할 때 중복을 제거하고 추가한다.")
- void scrapSelectedCardsToWorkbookFailedWhenDuplicate() {
- // given
- 유저_문제집_등록되어_있음("Spring 문제집", true, anotherUserInfo);
- final Long workbookId = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo).getId();
- CardResponse response1 = 카드_등록되어_있음("question", "answer", 1L, 1L);
-
- final ScrapCardRequest scrapCardRequest = ScrapCardRequest.builder()
- .cardIds(Arrays.asList(response1.getId(), response1.getId()))
- .build();
-
- // when
- final HttpResponse response = request()
- .post("/api/workbooks/{id}/cards", scrapCardRequest, workbookId)
- .auth(로그인되어_있음(userInfo).getAccessToken())
+ .auth(accessToken)
.build();
// then
@@ -666,9 +692,11 @@ void scrapSelectedCardsToWorkbookFailedWhenDuplicate() {
void scrapSelectedCardsToWorkbookFailedWhenWorkbookNotExist() {
// given
final Long workbookId = 100L;
- 유저_문제집_등록되어_있음("Java 문제집", true, userInfo);
- CardResponse response1 = 카드_등록되어_있음("question", "answer", 1L, 1L);
- CardResponse response2 = 카드_등록되어_있음("question", "answer", 1L, 1L);
+
+ final String otherAccessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
+ final Long otherWorkbookId = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, otherAccessToken).getId();
+ CardResponse response1 = 유저_카드_등록되어_있음("question", "answer", otherWorkbookId, otherAccessToken);
+ CardResponse response2 = 유저_카드_등록되어_있음("question", "answer", otherWorkbookId, otherAccessToken);
final ScrapCardRequest scrapCardRequest = ScrapCardRequest.builder()
.cardIds(Arrays.asList(response1.getId(), response2.getId()))
@@ -677,7 +705,7 @@ void scrapSelectedCardsToWorkbookFailedWhenWorkbookNotExist() {
// when
final HttpResponse response = request()
.post("/api/workbooks/{id}/cards", scrapCardRequest, workbookId)
- .auth(로그인되어_있음(userInfo).getAccessToken())
+ .auth(소셜_로그인되어_있음(oz, SocialType.GITHUB))
.build();
// then
@@ -699,7 +727,7 @@ void scrapSelectedCardsToWorkbookFailedWhenRequestIsEmpty() {
// when
final HttpResponse response = request()
.post("/api/workbooks/{id}/cards", scrapCardRequest, workbookId)
- .auth(로그인되어_있음(userInfo).getAccessToken())
+ .auth(소셜_로그인되어_있음(pk, SocialType.GITHUB))
.build();
// then
@@ -712,10 +740,13 @@ void scrapSelectedCardsToWorkbookFailedWhenRequestIsEmpty() {
@DisplayName("문제집으로 카드 가져오기 - 실패, 유저가 존재하지 않음.")
void scrapSelectedCardsToWorkbookFailedWhenUserNotFound() {
// given
- 유저_문제집_등록되어_있음("유저가 존재하지 않는 문제집", true, anotherUserInfo);
- final Long workbookId = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo).getId();
- CardResponse response1 = 카드_등록되어_있음("question", "answer", 1L, 1L);
- CardResponse response2 = 카드_등록되어_있음("question", "answer", 1L, 1L);
+ final String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
+ final Long workbookId = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken).getId();
+
+ final String otherAccessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ final Long otherWorkbookId = 유저_태그_포함_문제집_등록되어_있음("유저가 존재하지 않는 문제집", true, otherAccessToken).getId();
+ CardResponse response1 = 유저_카드_등록되어_있음("question", "answer", otherWorkbookId, otherAccessToken);
+ CardResponse response2 = 유저_카드_등록되어_있음("question", "answer", otherWorkbookId, otherAccessToken);
final ScrapCardRequest scrapCardRequest = ScrapCardRequest.builder()
.cardIds(Arrays.asList(response1.getId(), response2.getId()))
@@ -724,7 +755,7 @@ void scrapSelectedCardsToWorkbookFailedWhenUserNotFound() {
// when
final HttpResponse response = request()
.post("/api/workbooks/{id}/cards", scrapCardRequest, workbookId)
- .auth()
+ .failAuth()
.build();
// then
@@ -737,10 +768,13 @@ void scrapSelectedCardsToWorkbookFailedWhenUserNotFound() {
@DisplayName("문제집으로 카드 가져오기 - 실패, 문제집의 작성자가 아닌 유저")
void scrapSelectedCardsToWorkbookFailedWhenNotAuthor() {
// given
- 유저_문제집_등록되어_있음("Spring 문제집", true, userInfo);
- final Long workbookId = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo).getId();
- CardResponse response1 = 카드_등록되어_있음("question", "answer", 1L, 1L);
- CardResponse response2 = 카드_등록되어_있음("question", "answer", 1L, 1L);
+ final String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
+ final Long workbookId = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken).getId();
+
+ final String otherAccessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ final Long otherWorkbookId = 유저_태그_포함_문제집_등록되어_있음("Spring 문제집", true, otherAccessToken).getId();
+ CardResponse response1 = 유저_카드_등록되어_있음("question", "answer", otherWorkbookId, otherAccessToken);
+ CardResponse response2 = 유저_카드_등록되어_있음("question", "answer", otherWorkbookId, otherAccessToken);
final ScrapCardRequest scrapCardRequest = ScrapCardRequest.builder()
.cardIds(Arrays.asList(response1.getId(), response2.getId()))
@@ -749,7 +783,7 @@ void scrapSelectedCardsToWorkbookFailedWhenNotAuthor() {
// when
final HttpResponse response = request()
.post("/api/workbooks/{id}/cards", scrapCardRequest, workbookId)
- .auth(로그인되어_있음(anotherUserInfo).getAccessToken())
+ .auth(otherAccessToken)
.build();
// then
@@ -770,7 +804,7 @@ void scrapSelectedCardsToWorkbookFailedWhenEmptyCardIds() {
// when
final HttpResponse response = request()
.post("/api/workbooks/{id}/cards", scrapCardRequest, workbookId)
- .auth(로그인되어_있음(anotherUserInfo).getAccessToken())
+ .auth(소셜_로그인되어_있음(pk, SocialType.GITHUB))
.build();
// then
@@ -791,7 +825,7 @@ void scrapSelectedCardsToWorkbookFailedWhenNullCardIds() {
// when
final HttpResponse response = request()
.post("/api/workbooks/{id}/cards", scrapCardRequest, workbookId)
- .auth()
+ .failAuth()
.build();
// then
@@ -804,8 +838,8 @@ void scrapSelectedCardsToWorkbookFailedWhenNullCardIds() {
@DisplayName("문제집으로 카드 가져오기 - 실패, Card Id는 요청으로 들어왔으나 해당 ID의 카드가 모두 존재하지 않음.")
void scrapSelectedCardsToWorkbookFailedWhenCardNotFound() {
// given
- 유저_문제집_등록되어_있음("Spring 문제집", true, anotherUserInfo);
- final Long workbookId = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo).getId();
+ final String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
+ final Long workbookId = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken).getId();
final ScrapCardRequest scrapCardRequest = ScrapCardRequest.builder()
.cardIds(Arrays.asList(100L, 101L))
@@ -814,7 +848,7 @@ void scrapSelectedCardsToWorkbookFailedWhenCardNotFound() {
// when
final HttpResponse response = request()
.post("/api/workbooks/{id}/cards", scrapCardRequest, workbookId)
- .auth(로그인되어_있음(userInfo).getAccessToken())
+ .auth(accessToken)
.build();
// then
@@ -827,10 +861,13 @@ void scrapSelectedCardsToWorkbookFailedWhenCardNotFound() {
@DisplayName("문제집으로 카드 가져오기 - 실패, Card Id는 요청으로 들어왔으나 해당 ID의 카드가 일부 존재하지 않음.")
void scrapSelectedCardsToWorkbookFailedWhenPartOfCardNotFound() {
// given
- 유저_문제집_등록되어_있음("Spring 문제집", true, anotherUserInfo);
- final Long workbookId = 유저_문제집_등록되어_있음("Java 문제집", true, userInfo).getId();
- CardResponse response1 = 카드_등록되어_있음("question", "answer", 1L, 1L);
- CardResponse response2 = 카드_등록되어_있음("question", "answer", 1L, 1L);
+ final String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
+ final Long workbookId = 유저_태그_포함_문제집_등록되어_있음("Java 문제집", true, accessToken).getId();
+
+ final String otherAccessToken = 소셜_로그인되어_있음(bear, SocialType.GITHUB);
+ final Long otherWorkbookId = 유저_태그_포함_문제집_등록되어_있음("Spring 문제집", true, otherAccessToken).getId();
+ CardResponse response1 = 유저_카드_등록되어_있음("question", "answer", otherWorkbookId, otherAccessToken);
+ CardResponse response2 = 유저_카드_등록되어_있음("question", "answer", otherWorkbookId, otherAccessToken);
final ScrapCardRequest scrapCardRequest = ScrapCardRequest.builder()
.cardIds(Arrays.asList(response1.getId(), response2.getId(), 100L))
@@ -839,7 +876,7 @@ void scrapSelectedCardsToWorkbookFailedWhenPartOfCardNotFound() {
// when
final HttpResponse response = request()
.post("/api/workbooks/{id}/cards", scrapCardRequest, workbookId)
- .auth(로그인되어_있음(userInfo).getAccessToken())
+ .auth(accessToken)
.build();
// then
@@ -848,67 +885,41 @@ void scrapSelectedCardsToWorkbookFailedWhenPartOfCardNotFound() {
assertThat(errorResponse.getMessage()).isEqualTo("해당 카드를 찾을 수 없습니다.");
}
- private WorkbookResponse 유저_문제집_등록되어_있음(String name, boolean opened, GithubUserInfoResponse userInfo) {
- List tagRequests = Collections.singletonList(
- TagRequest.builder().id(1L).name("자바").build()
- );
- WorkbookRequest workbookRequest = WorkbookRequest.builder()
- .name(name)
- .opened(opened)
- .tags(tagRequests)
- .build();
- return 유저_문제집_등록되어_있음(workbookRequest, userInfo);
- }
+ @Test
+ @DisplayName("유저가 하트를 토글 - 성공")
+ void toggleOnHeart() {
+ // given
+ String accessToken = 소셜_로그인되어_있음(pk, SocialType.GITHUB);
+ WorkbookResponse workbookResponse = 유저_태그_포함_문제집_등록되어_있음("자바 문제집", true, accessToken);
- private WorkbookResponse 유저_문제집_등록되어_있음(String name, boolean opened, List tags, GithubUserInfoResponse userInfo) {
- WorkbookRequest workbookRequest = WorkbookRequest.builder()
- .name(name)
- .opened(opened)
- .tags(tags)
- .build();
- return 유저_문제집_등록되어_있음(workbookRequest, userInfo);
- }
+ Long workbookId = workbookResponse.getId();
+ String anotherToken = 소셜_로그인되어_있음(oz, SocialType.GITHUB);
- private WorkbookResponse 유저_문제집_등록되어_있음(WorkbookRequest workbookRequest, GithubUserInfoResponse userInfo) {
- return 유저_문제집_생성_요청(workbookRequest, userInfo).convertBody(WorkbookResponse.class);
- }
+ // when, then
+ HttpResponse httpResponse = 하트_토글_요청(workbookId, anotherToken);
+ HeartResponse heartResponse = httpResponse.convertBody(HeartResponse.class);
+ assertThat(httpResponse.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(heartResponse.isHeart()).isTrue();
- private HttpResponse 유저_문제집_생성_요청(WorkbookRequest workbookRequest, GithubUserInfoResponse userInfo) {
- return request()
- .post("/api/workbooks", workbookRequest)
- .auth(로그인되어_있음(userInfo).getAccessToken())
- .build();
+ httpResponse = 하트_토글_요청(workbookId, anotherToken);
+ heartResponse = httpResponse.convertBody(HeartResponse.class);
+ assertThat(httpResponse.statusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(heartResponse.isHeart()).isFalse();
}
private HttpResponse 유저_문제집_수정_요청(WorkbookUpdateRequest workbookUpdateRequest,
WorkbookResponse workbookResponse,
- GithubUserInfoResponse userInfo) {
+ String accessToken) {
return request()
.put("/api/workbooks/{id}", workbookUpdateRequest, workbookResponse.getId())
- .auth(로그인되어_있음(userInfo).getAccessToken())
+ .auth(accessToken)
.build();
}
- private HttpResponse 유저_문제집_삭제_요청(WorkbookResponse workbookResponse, GithubUserInfoResponse userInfo) {
+ private HttpResponse 유저_문제집_삭제_요청(WorkbookResponse workbookResponse, String accessToken) {
return request()
.delete("/api/workbooks/{id}", workbookResponse.getId())
- .auth(로그인되어_있음(userInfo).getAccessToken())
+ .auth(accessToken)
.build();
}
-
- private TokenResponse 로그인되어_있음(GithubUserInfoResponse userInfo) {
- ExtractableResponse response = 로그인_요청(userInfo);
- return response.as(TokenResponse.class);
- }
-
- private ExtractableResponse 로그인_요청(GithubUserInfoResponse userInfo) {
- LoginRequest loginRequest = new LoginRequest("githubCode");
-
- given(githubOauthManager.getUserInfoFromGithub(any())).willReturn(userInfo);
-
- return request()
- .post("/api/login", loginRequest)
- .build()
- .extract();
- }
}
diff --git a/backend/src/test/java/botobo/core/application/AuthServiceTest.java b/backend/src/test/java/botobo/core/application/AuthServiceTest.java
index 0e0adc45..4aad0fd1 100644
--- a/backend/src/test/java/botobo/core/application/AuthServiceTest.java
+++ b/backend/src/test/java/botobo/core/application/AuthServiceTest.java
@@ -1,11 +1,15 @@
package botobo.core.application;
+import botobo.core.domain.user.SocialType;
import botobo.core.domain.user.User;
import botobo.core.domain.user.UserRepository;
import botobo.core.dto.auth.GithubUserInfoResponse;
+import botobo.core.dto.auth.LoginRequest;
import botobo.core.dto.auth.TokenResponse;
+import botobo.core.dto.auth.UserInfoResponse;
import botobo.core.infrastructure.GithubOauthManager;
import botobo.core.infrastructure.JwtTokenProvider;
+import botobo.core.infrastructure.OauthManagerFactory;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
@@ -25,6 +29,9 @@
@MockitoSettings
class AuthServiceTest {
+ @Mock
+ private OauthManagerFactory oauthManagerFactory;
+
@Mock
private GithubOauthManager githubOauthManager;
@@ -41,36 +48,42 @@ class AuthServiceTest {
@DisplayName("토큰을 만든다. - 성공, 유저가 이미 존재하는 경우")
void createToken() {
// given
- GithubUserInfoResponse githubUserInfoResponse = GithubUserInfoResponse.builder()
- .githubId(1L)
+ UserInfoResponse githubUserInfoResponse = GithubUserInfoResponse.builder()
+ .socialId("1")
.userName("user")
.profileUrl("user.io")
.build();
User user = User.builder()
.id(1L)
- .githubId(1L)
+ .socialId("1")
.userName("user")
.profileUrl("user.io")
+ .socialType(SocialType.GITHUB)
.build();
+ LoginRequest loginRequest = new LoginRequest("code");
String accessToken = "토큰입니다";
- given(githubOauthManager.getUserInfoFromGithub(any())).willReturn(githubUserInfoResponse);
- given(userRepository.findByGithubId(anyLong())).willReturn(Optional.of(user));
+ given(oauthManagerFactory.findOauthMangerBySocialType(any())).willReturn(githubOauthManager);
+ given(githubOauthManager.getUserInfo(any())).willReturn(githubUserInfoResponse.toUser());
+ given(userRepository.findBySocialIdAndSocialType(any(), any())).willReturn(Optional.of(user));
given(jwtTokenProvider.createToken(user.getId())).willReturn(accessToken);
// when
- TokenResponse tokenResponse = authService.createToken(any());
+ TokenResponse tokenResponse = authService.createToken("github", loginRequest);
// then
assertThat(tokenResponse.getAccessToken()).isEqualTo(accessToken);
+ then(oauthManagerFactory)
+ .should(times(1))
+ .findOauthMangerBySocialType(any());
then(githubOauthManager)
.should(times(1))
- .getUserInfoFromGithub(any());
+ .getUserInfo(any());
then(userRepository)
.should(times(1))
- .findByGithubId(anyLong());
+ .findBySocialIdAndSocialType(any(), any());
then(jwtTokenProvider)
.should(times(1))
.createToken(anyLong());
@@ -80,37 +93,42 @@ void createToken() {
@DisplayName("토큰을 만든다. - 성공, 새로운 유저인 경우")
void createTokenWithNewUser() {
// given
- GithubUserInfoResponse githubUserInfoResponse = GithubUserInfoResponse.builder()
- .githubId(1L)
+ UserInfoResponse githubUserInfoResponse = GithubUserInfoResponse.builder()
+ .socialId("1")
.userName("user")
.profileUrl("user.io")
.build();
User user = User.builder()
.id(1L)
- .githubId(1L)
+ .socialId("1")
.userName("user")
.profileUrl("user.io")
.build();
+ LoginRequest loginRequest = new LoginRequest("code");
String accessToken = "토큰입니다";
- given(githubOauthManager.getUserInfoFromGithub(any())).willReturn(githubUserInfoResponse);
- given(userRepository.findByGithubId(anyLong())).willReturn(Optional.empty());
+ given(oauthManagerFactory.findOauthMangerBySocialType(any())).willReturn(githubOauthManager);
+ given(githubOauthManager.getUserInfo(any())).willReturn(githubUserInfoResponse.toUser());
+ given(userRepository.findBySocialIdAndSocialType(any(), any())).willReturn(Optional.empty());
given(userRepository.save(any(User.class))).willReturn(user);
given(jwtTokenProvider.createToken(user.getId())).willReturn(accessToken);
// when
- TokenResponse tokenResponse = authService.createToken(any());
+ TokenResponse tokenResponse = authService.createToken("github", loginRequest);
// then
assertThat(tokenResponse.getAccessToken()).isEqualTo(accessToken);
+ then(oauthManagerFactory)
+ .should(times(1))
+ .findOauthMangerBySocialType(any());
then(githubOauthManager)
.should(times(1))
- .getUserInfoFromGithub(any());
+ .getUserInfo(any());
then(userRepository)
.should(times(1))
- .findByGithubId(anyLong());
+ .findBySocialIdAndSocialType(any(), any());
then(userRepository)
.should(times(1))
.save(any(User.class));
diff --git a/backend/src/test/java/botobo/core/application/CardServiceTest.java b/backend/src/test/java/botobo/core/application/CardServiceTest.java
index 9a21d366..6f9d0d28 100644
--- a/backend/src/test/java/botobo/core/application/CardServiceTest.java
+++ b/backend/src/test/java/botobo/core/application/CardServiceTest.java
@@ -10,7 +10,7 @@
import botobo.core.dto.card.CardRequest;
import botobo.core.dto.card.CardUpdateRequest;
import botobo.core.dto.card.NextQuizCardsRequest;
-import botobo.core.exception.NotAuthorException;
+import botobo.core.exception.user.NotAuthorException;
import botobo.core.exception.user.UserNotFoundException;
import botobo.core.exception.workbook.WorkbookNotFoundException;
import org.junit.jupiter.api.BeforeEach;
@@ -308,14 +308,62 @@ void selectNextQuizCards() {
// given
NextQuizCardsRequest requestWithThreeIds = nextQuizCardsRequestWithThreeIds();
List threeCards = listOfThreeCards();
+
+ given(userRepository.findById(appUser.getId())).willReturn(Optional.of(user));
given(cardRepository.findByIdIn(requestWithThreeIds.getCardIds())).willReturn(threeCards);
// when
- cardService.selectNextQuizCards(requestWithThreeIds);
+ cardService.selectNextQuizCards(requestWithThreeIds, appUser);
// then
assertThat(threeCards).extracting("nextQuiz")
.containsExactly(true, true, true);
+ then(userRepository)
+ .should(times(1))
+ .findById(appUser.getId());
+ then(cardRepository)
+ .should(times(1))
+ .findByIdIn(anyList());
+ }
+
+ @Test
+ @DisplayName("다음에 또 보는 카드 선택 - 실패, 존재하지 않는 유저")
+ void selectNextQuizCardsWhenUserNotFound() {
+ // given
+ NextQuizCardsRequest requestWithThreeIds = nextQuizCardsRequestWithThreeIds();
+ List threeCards = listOfThreeCards();
+
+ given(userRepository.findById(appUser.getId())).willThrow(UserNotFoundException.class);
+
+ // when - then
+ assertThatThrownBy(() -> cardService.selectNextQuizCards(requestWithThreeIds, appUser))
+ .isInstanceOf(UserNotFoundException.class);
+
+ then(userRepository)
+ .should(times(1))
+ .findById(appUser.getId());
+ then(cardRepository)
+ .should(never())
+ .findByIdIn(anyList());
+ }
+
+ @Test
+ @DisplayName("다음에 또 보는 카드 선택 - 실패, author가 아닌 유저")
+ void selectNextQuizCardsWhenUserNotAuthor() {
+ // given
+ NextQuizCardsRequest requestWithThreeIds = nextQuizCardsRequestWithThreeIds();
+ List threeCards = listOfThreeCards();
+
+ given(userRepository.findById(appUser.getId())).willReturn(Optional.of(anotherUser));
+ given(cardRepository.findByIdIn(requestWithThreeIds.getCardIds())).willReturn(threeCards);
+
+ // when - then
+ assertThatThrownBy(() -> cardService.selectNextQuizCards(requestWithThreeIds, appUser))
+ .isInstanceOf(NotAuthorException.class);
+
+ then(userRepository)
+ .should(times(1))
+ .findById(appUser.getId());
then(cardRepository)
.should(times(1))
.findByIdIn(anyList());
diff --git a/backend/src/test/java/botobo/core/application/QuizServiceTest.java b/backend/src/test/java/botobo/core/application/QuizServiceTest.java
index 8a49f47e..c1907efe 100644
--- a/backend/src/test/java/botobo/core/application/QuizServiceTest.java
+++ b/backend/src/test/java/botobo/core/application/QuizServiceTest.java
@@ -2,10 +2,17 @@
import botobo.core.domain.card.Card;
import botobo.core.domain.card.CardRepository;
+import botobo.core.domain.user.AppUser;
+import botobo.core.domain.user.Role;
+import botobo.core.domain.user.User;
+import botobo.core.domain.user.UserRepository;
import botobo.core.domain.workbook.Workbook;
import botobo.core.domain.workbook.WorkbookRepository;
+import botobo.core.dto.card.QuizRequest;
import botobo.core.dto.card.QuizResponse;
import botobo.core.exception.card.QuizEmptyException;
+import botobo.core.exception.user.NotAuthorException;
+import botobo.core.exception.user.UserNotFoundException;
import botobo.core.exception.workbook.WorkbookNotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
@@ -18,6 +25,9 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -25,11 +35,15 @@
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.times;
+import static org.mockito.Mockito.never;
@DisplayName("퀴즈 서비스 테스트")
@MockitoSettings
class QuizServiceTest {
+ @Mock
+ private UserRepository userRepository;
+
@Mock
private WorkbookRepository workbookRepository;
@@ -41,19 +55,30 @@ class QuizServiceTest {
private Workbook workbook;
private Workbook workbookWithOneCard;
+
private List cards;
private List twoCards;
+ private AppUser appUser;
+ private User user;
+
@BeforeEach
void setUp() {
+ user = User.builder()
+ .id(1L)
+ .role(Role.ADMIN)
+ .build();
+
workbook = Workbook.builder()
.name("name")
.deleted(false)
+ .user(user)
.build();
workbookWithOneCard = Workbook.builder()
.name("name")
.deleted(false)
+ .user(user)
.build();
Card card1 = Card.builder()
@@ -72,6 +97,7 @@ void setUp() {
cards = Arrays.asList(card1, card1, card1, card1, card1, card1, card1, card1, card1, card1);
twoCards = Arrays.asList(card1, card2);
+ appUser = user.toAppUser();
}
@Test
@@ -79,18 +105,90 @@ void setUp() {
void createQuiz() {
// given
List ids = Collections.singletonList(1L);
- given(workbookRepository.existsById(any())).willReturn(true);
- given(cardRepository.findCardsByWorkbookIds(any())).willReturn(cards);
+ QuizRequest quizRequest = makeQuizRequest(ids, 10);
+
+ given(userRepository.findById(appUser.getId())).willReturn(Optional.of(user));
+ given(workbookRepository.findById(1L)).willReturn(Optional.of(workbook));
+ given(cardRepository.findCardsByWorkbookIds(ids)).willReturn(cards);
// when
- List quizResponses = quizService.createQuiz(ids);
+ List quizResponses = quizService.createQuiz(quizRequest, appUser);
// then
assertThat(quizResponses.size()).isEqualTo(10);
then(cardRepository)
.should(times(1))
- .findCardsByWorkbookIds(any());
+ .findCardsByWorkbookIds(ids);
+ then(userRepository)
+ .should(times(1))
+ .findById(appUser.getId());
+ then(workbookRepository)
+ .should(times(1))
+ .findById(1L);
+ }
+
+ private QuizRequest makeQuizRequest(List ids, int count) {
+ return QuizRequest.builder()
+ .workbookIds(ids)
+ .count(count)
+ .build();
+ }
+
+ @Test
+ @DisplayName("문제집 id(Long)를 이용해서 10개의 카드가 담긴 퀴즈 생성 - 실패, 회원 정보가 다름.")
+ void createQuizWhenGuest() {
+ // given
+ List ids = Collections.singletonList(1L);
+ QuizRequest quizRequest = makeQuizRequest(ids, 10);
+
+ given(userRepository.findById(appUser.getId())).willThrow(UserNotFoundException.class);
+
+ // when
+ assertThatThrownBy(() -> quizService.createQuiz(quizRequest, appUser))
+ .isInstanceOf(UserNotFoundException.class);
+
+ // then
+ then(userRepository)
+ .should(times(1))
+ .findById(appUser.getId());
+ then(cardRepository)
+ .should(never())
+ .findCardsByWorkbookIds(ids);
+ then(workbookRepository)
+ .should(never())
+ .findById(1L);
+ }
+
+ @DisplayName("문제집 id(Long)를 이용해서 10개의 카드가 담긴 퀴즈 생성 - 실패, 내 문제집이 아님")
+ @Test
+ void createQuizWhenNotAuthor() {
+ // given
+ List ids = Collections.singletonList(1L);
+ QuizRequest quizRequest = makeQuizRequest(ids, 10);
+ User notAuthorUser = User.builder()
+ .id(100L)
+ .userName("내 문제집이 아닌 유저")
+ .build();
+ AppUser notAuthorAppUser = user.toAppUser();
+
+ given(userRepository.findById(notAuthorAppUser.getId())).willReturn(Optional.of(notAuthorUser));
+ given(workbookRepository.findById(1L)).willReturn(Optional.of(workbook));
+
+ // when
+ assertThatThrownBy(() -> quizService.createQuiz(quizRequest, notAuthorAppUser))
+ .isInstanceOf(NotAuthorException.class);
+
+ // then
+ then(userRepository)
+ .should(times(1))
+ .findById(notAuthorAppUser.getId());
+ then(workbookRepository)
+ .should(times(1))
+ .findById(1L);
+ then(cardRepository)
+ .should(never())
+ .findCardsByWorkbookIds(ids);
}
@Test
@@ -98,11 +196,14 @@ void createQuiz() {
void createQuizIncrementEncounterCount() {
// given
List ids = Collections.singletonList(1L);
- given(workbookRepository.existsById(any())).willReturn(true);
+ QuizRequest quizRequest = makeQuizRequest(ids, 10);
+
+ given(userRepository.findById(appUser.getId())).willReturn(Optional.of(user));
+ given(workbookRepository.findById(1L)).willReturn(Optional.of(workbook));
given(cardRepository.findCardsByWorkbookIds(any())).willReturn(twoCards);
// when
- List quizResponses = quizService.createQuiz(ids);
+ List quizResponses = quizService.createQuiz(quizRequest, appUser);
// then
assertThat(quizResponses.size()).isEqualTo(2);
@@ -112,7 +213,13 @@ void createQuizIncrementEncounterCount() {
then(cardRepository)
.should(times(1))
- .findCardsByWorkbookIds(any());
+ .findCardsByWorkbookIds(ids);
+ then(userRepository)
+ .should(times(1))
+ .findById(appUser.getId());
+ then(workbookRepository)
+ .should(times(1))
+ .findById(1L);
}
@Test
@@ -120,15 +227,24 @@ void createQuizIncrementEncounterCount() {
void createQuizFailedWhenIdNotFound() {
// given
List ids = Collections.singletonList(100L);
- given(workbookRepository.existsById(any())).willReturn(false);
+ QuizRequest quizRequest = makeQuizRequest(ids, 10);
+
+ given(userRepository.findById(appUser.getId())).willReturn(Optional.of(user));
+ given(workbookRepository.findById(100L)).willReturn(Optional.empty());
// when
- assertThatThrownBy(() -> quizService.createQuiz(ids))
+ assertThatThrownBy(() -> quizService.createQuiz(quizRequest, appUser))
.isInstanceOf(WorkbookNotFoundException.class);
then(cardRepository)
- .should(times(0))
- .findCardsByWorkbookIds(any());
+ .should(never())
+ .findCardsByWorkbookIds(ids);
+ then(userRepository)
+ .should(times(1))
+ .findById(appUser.getId());
+ then(workbookRepository)
+ .should(times(1))
+ .findById(100L);
}
@Test
@@ -136,36 +252,74 @@ void createQuizFailedWhenIdNotFound() {
void createQuizFailedWhenQuizIsEmpty() {
// given
List ids = Collections.singletonList(100L);
+ QuizRequest quizRequest = makeQuizRequest(ids, 10);
List emptyCards = new ArrayList<>();
- given(workbookRepository.existsById(any())).willReturn(true);
+
+ given(userRepository.findById(appUser.getId())).willReturn(Optional.of(user));
+ given(workbookRepository.findById(100L)).willReturn(Optional.of(workbook));
given(cardRepository.findCardsByWorkbookIds(ids)).willReturn(emptyCards);
// when
- assertThatThrownBy(() -> quizService.createQuiz(ids))
+ assertThatThrownBy(() -> quizService.createQuiz(quizRequest, appUser))
.isInstanceOf(QuizEmptyException.class);
then(cardRepository)
.should(times(1))
- .findCardsByWorkbookIds(any());
+ .findCardsByWorkbookIds(ids);
+ then(userRepository)
+ .should(times(1))
+ .findById(appUser.getId());
+ then(workbookRepository)
+ .should(times(1))
+ .findById(100L);
}
@Test
@DisplayName("문제집 id(Long) 및 다음에 또보기 카드 10개를 포함한 퀴즈 생성 - 성공")
- void createQuizWithOneCards() {
+ void createQuizWithNextQuizCards() {
// given
List ids = Collections.singletonList(1L);
- given(workbookRepository.existsById(any())).willReturn(true);
+ List cards = listOfNextQuizCards(5, true);
+ cards.addAll(listOfNextQuizCards(5, false));
+
+ QuizRequest quizRequest = makeQuizRequest(ids, 10);
+ given(userRepository.findById(appUser.getId())).willReturn(Optional.of(user));
+ given(workbookRepository.findById(1L)).willReturn(Optional.of(workbook));
given(cardRepository.findCardsByWorkbookIds(any())).willReturn(cards);
// when
- List quizResponses = quizService.createQuiz(ids);
+ List quizResponses = quizService.createQuiz(quizRequest, appUser);
// then
+ assertThat(cards).extracting("nextQuiz").doesNotContain(true);
assertThat(quizResponses.size()).isEqualTo(10);
-
then(cardRepository)
.should(times(1))
- .findCardsByWorkbookIds(any());
+ .findCardsByWorkbookIds(ids);
+ then(userRepository)
+ .should(times(1))
+ .findById(appUser.getId());
+ then(workbookRepository)
+ .should(times(1))
+ .findById(1L);
+ }
+
+ private List listOfNextQuizCards(int quantity, boolean nextQuiz) {
+ return IntStream.range(0, quantity)
+ .mapToObj(num -> card(nextQuiz))
+ .collect(Collectors.toList());
+ }
+
+ private Card card(boolean nextQuiz) {
+ Workbook workbook = Workbook.builder()
+ .name("workbook")
+ .build();
+ return Card.builder()
+ .question("question")
+ .answer("answer")
+ .workbook(workbook)
+ .nextQuiz(nextQuiz)
+ .build();
}
@Test
@@ -194,7 +348,10 @@ void createQuizFromWorkbook() {
then(cardRepository)
.should(times(1))
- .findCardsByWorkbookId(any());
+ .findCardsByWorkbookId(workbookId);
+ then(workbookRepository)
+ .should(times(1))
+ .existsByIdAndOpenedTrue(workbookId);
}
@Test
@@ -216,7 +373,10 @@ void createQuizFromWorkbookNotIncrementEncounterCount() {
then(cardRepository)
.should(times(1))
- .findCardsByWorkbookId(any());
+ .findCardsByWorkbookId(workbookId);
+ then(workbookRepository)
+ .should(times(1))
+ .existsByIdAndOpenedTrue(workbookId);
}
@Test
@@ -231,8 +391,11 @@ void createQuizFromWorkbookFailed() {
.isInstanceOf(WorkbookNotFoundException.class);
then(cardRepository)
- .should(times(0))
- .findCardsByWorkbookId(any());
+ .should(never())
+ .findCardsByWorkbookId(workbookId);
+ then(workbookRepository)
+ .should(times(1))
+ .existsByIdAndOpenedTrue(workbookId);
}
@Test
@@ -247,8 +410,11 @@ void createQuizFromWorkbookFailedWhenWorkbookIsNotPublic() {
.isInstanceOf(WorkbookNotFoundException.class);
then(cardRepository)
- .should(times(0))
- .findCardsByWorkbookId(any());
+ .should(never())
+ .findCardsByWorkbookId(workbookId);
+ then(workbookRepository)
+ .should(times(1))
+ .existsByIdAndOpenedTrue(workbookId);
}
@Test
@@ -266,6 +432,9 @@ void createQuizFromWorkbookFailedWhenQuizIsEmpty() {
then(cardRepository)
.should(times(1))
- .findCardsByWorkbookId(any());
+ .findCardsByWorkbookId(workbookId);
+ then(workbookRepository)
+ .should(times(1))
+ .existsByIdAndOpenedTrue(workbookId);
}
}
diff --git a/backend/src/test/java/botobo/core/application/SearchServiceTest.java b/backend/src/test/java/botobo/core/application/SearchServiceTest.java
new file mode 100644
index 00000000..dac4201f
--- /dev/null
+++ b/backend/src/test/java/botobo/core/application/SearchServiceTest.java
@@ -0,0 +1,98 @@
+package botobo.core.application;
+
+import botobo.core.domain.tag.Tag;
+import botobo.core.domain.tag.TagRepository;
+import botobo.core.domain.user.User;
+import botobo.core.domain.user.UserRepository;
+import botobo.core.dto.tag.TagResponse;
+import botobo.core.dto.user.SimpleUserResponse;
+import botobo.core.ui.search.SearchKeyword;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoSettings;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.times;
+
+@DisplayName("검색 서비스 테스트")
+@MockitoSettings
+class SearchServiceTest {
+
+ @Mock
+ private TagRepository tagRepository;
+
+ @Mock
+ private UserRepository userRepository;
+
+ @InjectMocks
+ private SearchService searchService;
+
+ @Test
+ @DisplayName("태그 검색 - 성공, 이름에 키워드가 들어가면 결과에 포함된다")
+ void searchTags() {
+ // given
+ String java = "java";
+ String javascript = "javascript";
+ List