diff --git a/.github/workflows/docker-push-and-aws-run.yml b/.github/workflows/docker-push-and-aws-run.yml
index 2f88782..e9fa9d0 100644
--- a/.github/workflows/docker-push-and-aws-run.yml
+++ b/.github/workflows/docker-push-and-aws-run.yml
@@ -9,7 +9,7 @@ permissions:
contents: read
jobs:
- build:
+ deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@@ -23,7 +23,7 @@ jobs:
run: sudo docker run -d -p 3306:3306 -e MYSQL_DATABASE="${{ secrets.MYSQL_DATABASE }}" -e MYSQL_ROOT_PASSWORD="${{ secrets.MYSQL_ROOT_PASSWORD }}" mysql:8.0.23
- name: Create db config file
- run: echo "${{ secrets.DB_PROPERTIES }}" > ./.env
+ run: echo "${{ secrets.ENV_PROPERTIES }}" > ./.env
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
@@ -42,10 +42,5 @@ jobs:
host: ${{ secrets.AWS_HOST }}
username: ${{ secrets.AWS_USERNAME }}
key: ${{ secrets.AWS_KEY }}
- script: |
- sudo docker-compose down
- sudo docker rmi fortune00/prolog
- sudo docker pull fortune00/prolog
- echo "${{ secrets.DOCKER_COMPOSE }}" > ./docker-compose.yml
- echo "${{ secrets.DOCKER_COMPOSE_ENV }}" > ./.env
- sudo docker-compose up -d
\ No newline at end of file
+ script: #docker-compose.yml ./.env ./nginx/default.conf ./deploy.sh 서버에 초기화
+ bash deploy.sh
\ No newline at end of file
diff --git a/.github/workflows/gradle-build.yml b/.github/workflows/gradle-build.yml
index 8c90903..bc9e541 100644
--- a/.github/workflows/gradle-build.yml
+++ b/.github/workflows/gradle-build.yml
@@ -3,7 +3,7 @@ name: Java CI with Gradle
on:
pull_request:
branches:
- - "develop"
+ - develop
permissions:
contents: read
@@ -13,19 +13,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
+
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- - uses: actions/checkout@v3
- - run: echo "${{ secrets.DB_PROPERTIES }}" > ./.env
-
- name: Create mysql docker container
run: sudo docker run -d -p 3306:3306 --env MYSQL_DATABASE="${{ secrets.MYSQL_DATABASE }}" --env MYSQL_ROOT_PASSWORD="${{ secrets.MYSQL_ROOT_PASSWORD }}" mysql:8.0.23
+ - uses: actions/checkout@v3
+ - run: echo "${{ secrets.ENV_PROPERTIES }}" > ./.env
+
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
- arguments: build
\ No newline at end of file
+ arguments: build
diff --git a/Dockerfile b/Dockerfile
index 226239c..2f3cdfe 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,12 +1,14 @@
FROM openjdk:17.0.2
-ARG JAR_FILE=build/libs/prolog-1.0.0.jar
-ENV MYSQL_URL=${SPRING_DATASOURCE_URL} \
-MYSQL_USERNAME=${SPRING_DATASOURCE_USERNAME} \
-MYSQL_ROOT_PASSWORD=${SPRING_DATASOURCE_PASSWORD} \
+ARG JAR_FILE=build/libs/prolog-1.0.2.jar
+ENV SPRING_DATASOURCE_URL=${SPRING_DATASOURCE_URL} \
+SPRING_DATASOURCE_USERNAME=${SPRING_DATASOURCE_USERNAME} \
+SPRING_DATASOURCE_PASSWORD=${SPRING_DATASOURCE_PASSWORD} \
JWT_ISSUER=${JWT_ISSUER} \
JWT_SECRET_KEY=${JWT_SECRET_KEY} \
CLIENT_ID=${CLIENT_ID} \
CLIENT_SECRET=${CLIENT_SECRET} \
-REDIRECT_URI=${REDIRECT_URI}
+REDIRECT_URI=${REDIRECT_URI} \
+AWS_ACCESS_KEY=${AWS_ACCESS_KEY} \
+AWS_SECRET_KEY=${AWS_SECRET_KEY}
COPY ${JAR_FILE} prolog.jar
ENTRYPOINT ["java", "-jar", "/prolog.jar"]
diff --git a/README.md b/README.md
index 0c2a8a8..bbe40dc 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,9 @@
+
# 🥚 Prolog
-백엔드 알팀 velog 클론코딩 프로젝트
+ 백엔드 알팀 velog 클론코딩 프로젝트
-## 🍑 프로젝트 목표
+## :peach: 프로젝트 목표
프로그래머스 데브코스만의 기술 블로그를 만들어서 지식을 공유해보자
@@ -15,37 +16,168 @@
## 🍊 개발 언어 및 활용기술
+
### Tech
-
+
### Deploy
-
+
### Tool
-
+
+
## 🍎 설계 및 문서
-### ERD
+### 프로젝트 구조
+(예정)
-
+### ERD
+(예정)
### [Prolog API](https://www.notion.so/backend-devcourse/API-1-3785ae03912441e7a87e253fd069c200)
-### 인프라 구조
-(예정)
-
## 🍉 주요 기능
(예정)
+## 🍒 배포 주소
+### [Docker Hub의 prolog](https://hub.docker.com/repository/docker/fortune00/prolog/general)
+
+### [현재 접근 가능한 IP](43.200.173.123)
+
+## 🍇 프로젝트 실행 방법
+
+- 프로젝트 실행 전 database(mysql)가 실행되고 있어야 하며(docker compose 제외), kakao OAuth를 서비스를 사용하고 있어야 합니다
+- 아래의 실행 과정은 .env 파일을 사용하는 방식으로 설명합니다
+
+### using Github Project
+
+1. github에서 프로젝트를 다운받는다
+
+ ```git clone https://github.com/prgrms-be-devcourse/BE-03-Prolog```
+
+2. 프로젝트 root 경로에 .env 파일을 생성한다
+
+ ```
+ #datasource
+ SPRING_DATASOURCE_USERNAME=
+ SPRING_DATASOURCE_PASSWORD=
+ SPRING_DATASOURCE_URL=
+
+ #security
+ JWT_ISSUER=
+ JWT_SECRET_KEY=
+ CLIENT_ID=
+ CLIENT_SECRET=
+ REDIRECT_URI=
+ ```
+
+3. build 후, jar 파일을 실행한다
+
+ ```
+ ./gradlew clean build
+ java -jar build/libs/prolog-1.0.0.jar
+ ```
+
+### using Docker Image
+
+1. docker를 설치한다
+2. docker hub에서 docker image를 다운받는다, 자세한 경로는 [여기](https://hub.docker.com/repository/docker/fortune00/prolog/general)
+
+ ```docker pull fortune00/prolog```
+
+3. .env 파일을 생성한다
+
+ ```
+ #datasource
+ SPRING_DATASOURCE_USERNAME=
+ SPRING_DATASOURCE_PASSWORD=
+ SPRING_DATASOURCE_URL=
+
+ #security
+ JWT_ISSUER=
+ JWT_SECRET_KEY=
+ CLIENT_ID=
+ CLIENT_SECRET=
+ REDIRECT_URI=
+ ```
+
+4. .env 파일을 지정해, 컨테이너를 실행한다
+
+ ```docker run --env-file=.env -d fortune00/prolog```
+
+### using Docker-Compose
+
+1. docker-compose를 설치한다
+2. docker-compose.yml 파일을 생성한다
+
+ ```yml
+ version : "3"
+ services:
+ db:
+ container_name: prolog-db
+ image: mysql
+ environment:
+ MYSQL_DATABASE: ${MYSQL_DATABASE}
+ MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
+ ports:
+ - "3306:3306"
+ volumes:
+ - ./mysqldata:/var/lib/mysql
+ restart: always
+ app:
+ container_name: prolog-app
+ image: fortune00/prolog
+ ports:
+ - "8080:8080"
+ working_dir: /app
+ depends_on:
+ - db
+ restart: always
+ environment:
+ SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
+ SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
+ SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL} #IP 값으로 "prolog-db"를 넣어주세요
+ JWT_ISSUER: ${JWT_ISSUER}
+ JWT_SECRET_KEY: ${JWT_SECRET_KEY}
+ CLIENT_ID: ${CLIENT_ID}
+ CLIENT_SECRET: ${CLIENT_SECRET}
+ REDIRECT_URI: ${REDIRECT_URI}
+ ```
+
+3. docker-compose.yml과 같은 경로에 .env 파일을 만든다
+
+ ```
+ # database
+ MYSQL_ROOT_PASSWORD=
+ MYSQL_DATABASE=
+
+ #datasource
+ SPRING_DATASOURCE_USERNAME=
+ SPRING_DATASOURCE_PASSWORD=
+ SPRING_DATASOURCE_URL=
+
+ #security
+ JWT_ISSUER=
+ JWT_SECRET_KEY=
+ CLIENT_ID=
+ CLIENT_SECRET=
+ REDIRECT_URI=
+ ```
+
+4. docker-compose를 실행한다
+
+ ```docker-compose -d up```
+
+
## 🫐 프로젝트 페이지
### [프로젝트 문서](https://www.notion.so/backend-devcourse/Prolog-a038a633c3fc496ba0489beb2b15ef6c)
### [그라운드 룰](https://www.notion.so/backend-devcourse/7063f14625f147e291f45f371092d84a)
-### [회고](https://www.notion.so/backend-devcourse/6a625fcd1af340b197cd24fba38f3c90)
+### [프로젝트 회고](https://www.notion.so/backend-devcourse/6a625fcd1af340b197cd24fba38f3c90)
diff --git a/build.gradle b/build.gradle
index 67762de..3632af3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,16 +5,18 @@ plugins {
id 'java'
id 'org.springframework.boot' version '2.7.7'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
+ id 'org.asciidoctor.jvm.convert' version '3.3.2'
id 'org.hidetake.swagger.generator' version '2.18.2'
id 'com.epages.restdocs-api-spec' version '0.16.2'
id 'jacoco'
}
group = 'com.prgrms'
-version = '1.0.0'
+version = '1.0.2'
sourceCompatibility = '17'
configurations {
+ asciidoctorExt
compileOnly {
extendsFrom annotationProcessor
}
@@ -31,6 +33,7 @@ swaggerSources {
}
ext {
+ set('snippetsDir', file("build/generated-snippets"))
set('testcontainersVersion', "1.17.6")
}
@@ -41,7 +44,12 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // OAuth2-Client dependency
+ implementation 'org.springframework.boot:spring-boot-starter-actuator' // Actuator dependency
+ implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' // AWS S3 dependency
+
testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation 'org.springframework.security:spring-security-test'
+ annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
// RestDocs API SPEC
testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.16.2'
@@ -49,9 +57,6 @@ dependencies {
// Swagger UI
swaggerUI 'org.webjars:swagger-ui:4.11.1'
- testImplementation 'org.springframework.security:spring-security-test'
- annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
-
//Lombok dependency
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
@@ -59,18 +64,18 @@ dependencies {
// MySQL Driver
runtimeOnly 'com.mysql:mysql-connector-j'
- //testcontainers dependency
+ //Testcontainers dependency
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:mysql'
// Flyway dependency
- // https://mvnrepository.com/artifact/org.flywaydb/flyway-core
implementation 'org.flywaydb:flyway-core:6.4.2'
// JWT dependency
- // https://mvnrepository.com/artifact/com.auth0/java-jwt
implementation group: 'com.auth0', name: 'java-jwt', version: '4.2.1'
+ // Log4jdbc
+ implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
}
dependencyManagement {
@@ -88,6 +93,7 @@ openapi3 {
}
tasks.named('test') {
+ outputs.dir snippetsDir
useJUnitPlatform()
finalizedBy jacocoTestReport
}
@@ -96,6 +102,10 @@ tasks.withType(GenerateSwaggerUI).configureEach {
dependsOn 'openapi3'
}
+asciidoctor.doFirst {
+ delete file('src/main/resources/static/docs')
+}
+
tasks.register('copySwaggerUI', Copy) {
dependsOn 'generateSwaggerUISample'
@@ -105,6 +115,12 @@ tasks.register('copySwaggerUI', Copy) {
into("${project.buildDir}/resources/main/static/docs")
}
+task copyDocument(type: Copy) {
+ dependsOn asciidoctor
+ from file("build/docs/asciidoc")
+ into file("src/main/resources/static/docs")
+}
+
tasks.withType(BootJar).configureEach {
dependsOn 'copySwaggerUI'
}
@@ -138,7 +154,8 @@ jacocoTestCoverageVerification {
excludes = [
'*.global*',
- '*.service*',
+ '*.series*',
+ '*.comment*',
'*.dto*'
]
diff --git a/deploy.sh b/deploy.sh
new file mode 100644
index 0000000..4bcc016
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,31 @@
+RUNNING_CONTAINER=$(docker ps | grep blue)
+DEFAULT_CONF="nginx/default.conf"
+
+if [ -z "$RUNNING_CONTAINER" ]; then
+ TARGET_SERVICE="blue"
+ OTHER_SERVICE="green"
+else
+ TARGET_SERVICE="green"
+ OTHER_SERVICE="blue"
+fi
+
+echo "$TARGET_SERVICE Deploy..."
+docker-compose pull $TARGET_SERVICE
+docker-compose up -d $TARGET_SERVICE
+
+# Wait for the target service to be healthy before proceeding
+while true; do
+ echo "$TARGET_SERVICE health check...."
+ HEALTH=$(docker-compose exec nginx curl http://$TARGET_SERVICE:8080)
+ if [ -n "$HEALTH" ]; then
+ break
+ fi
+ sleep 3
+done
+
+# Update the nginx config and reload
+sed -i "" "s/$OTHER_SERVICE/$TARGET_SERVICE/g" $DEFAULT_CONF
+docker-compose exec nginx service nginx reload
+
+# Stop the other service
+docker-compose stop $OTHER_SERVICE
diff --git a/docker-compose.yml b/docker-compose.yml
index ea2f319..66d5192 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,13 @@
version : "3"
services:
+ nginx:
+ container_name: nginx
+ image: nginx
+ restart: always
+ ports:
+ - "80:80"
+ volumes:
+ - ./nginx/:/etc/nginx/conf.d/
db:
container_name: prolog-db
image: mysql
@@ -11,23 +19,43 @@ services:
volumes:
- ./mysqldata:/var/lib/mysql
restart: always
- app:
- build:
- context: "."
- dockerfile: "Dockerfile"
- container_name: prolog-app
- ports:
- - "8080:8080"
+ blue:
+ container_name: blue
+ image: fortune00/prolog
+ expose:
+ - "8080"
working_dir: /app
depends_on:
- db
restart: always
environment:
+ SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL}
SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
+ REDIRECT_URI: ${REDIRECT_URI}
+ JWT_ISSUER: ${JWT_ISSUER}
+ JWT_SECRET_KEY: ${JWT_SECRET_KEY}
+ CLIENT_ID: ${CLIENT_ID}
+ CLIENT_SECRET: ${CLIENT_SECRET}
+ AWS_ACCESS_KEY: ${AWS_ACCESS_KEY}
+ AWS_SECRET_KEY: ${AWS_SECRET_KEY}
+ green:
+ container_name: green
+ image: fortune00/prolog
+ expose:
+ - "8080"
+ working_dir: /app
+ depends_on:
+ - db
+ restart: always
+ environment:
SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL}
+ SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
+ SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
REDIRECT_URI: ${REDIRECT_URI}
JWT_ISSUER: ${JWT_ISSUER}
JWT_SECRET_KEY: ${JWT_SECRET_KEY}
CLIENT_ID: ${CLIENT_ID}
CLIENT_SECRET: ${CLIENT_SECRET}
+ AWS_ACCESS_KEY: ${AWS_ACCESS_KEY}
+ AWS_SECRET_KEY: ${AWS_SECRET_KEY}
diff --git a/nginx/default.conf b/nginx/default.conf
new file mode 100644
index 0000000..5f3eaa9
--- /dev/null
+++ b/nginx/default.conf
@@ -0,0 +1,11 @@
+server {
+ listen 80;
+ listen [::]:80;
+
+ location / {
+ proxy_pass http://green:8080;
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/prgrms/prolog/domain/comment/api/CommentController.java b/src/main/java/com/prgrms/prolog/domain/comment/api/CommentController.java
index 76c3857..d51bbd8 100644
--- a/src/main/java/com/prgrms/prolog/domain/comment/api/CommentController.java
+++ b/src/main/java/com/prgrms/prolog/domain/comment/api/CommentController.java
@@ -30,10 +30,9 @@ public class CommentController {
public ResponseEntity save(
@PathVariable(name = "post_id") Long postId,
@Valid @RequestBody CreateCommentRequest request,
- @AuthenticationPrincipal JwtAuthentication jwt
+ @AuthenticationPrincipal JwtAuthentication user
) {
- String userEmail = jwt.userEmail();
- commentService.save(request, userEmail, postId);
+ commentService.save(request, user.id(), postId);
return ResponseEntity.status(CREATED).build();
}
@@ -42,10 +41,9 @@ public ResponseEntity update(
@PathVariable(name = "post_id") Long postId,
@PathVariable(name = "id") Long commentId,
@Valid @RequestBody UpdateCommentRequest request,
- @AuthenticationPrincipal JwtAuthentication jwt
+ @AuthenticationPrincipal JwtAuthentication user
) {
- String userEmail = jwt.userEmail();
- commentService.update(request, userEmail, commentId);
+ commentService.update(request, user.id(), commentId);
return ResponseEntity.ok().build();
}
}
diff --git a/src/main/java/com/prgrms/prolog/domain/comment/model/Comment.java b/src/main/java/com/prgrms/prolog/domain/comment/model/Comment.java
index 4d7d0b7..59e867d 100644
--- a/src/main/java/com/prgrms/prolog/domain/comment/model/Comment.java
+++ b/src/main/java/com/prgrms/prolog/domain/comment/model/Comment.java
@@ -7,7 +7,6 @@
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
diff --git a/src/main/java/com/prgrms/prolog/domain/comment/service/CommentService.java b/src/main/java/com/prgrms/prolog/domain/comment/service/CommentService.java
index 235d855..a9871dc 100644
--- a/src/main/java/com/prgrms/prolog/domain/comment/service/CommentService.java
+++ b/src/main/java/com/prgrms/prolog/domain/comment/service/CommentService.java
@@ -3,6 +3,6 @@
import static com.prgrms.prolog.domain.comment.dto.CommentDto.*;
public interface CommentService {
- Long save(CreateCommentRequest request, String email, Long postId);
- Long update(UpdateCommentRequest request, String email, Long commentId);
+ Long save(CreateCommentRequest request, Long userId, Long postId);
+ Long update(UpdateCommentRequest request, Long userId, Long commentId);
}
diff --git a/src/main/java/com/prgrms/prolog/domain/comment/service/CommentServiceImpl.java b/src/main/java/com/prgrms/prolog/domain/comment/service/CommentServiceImpl.java
index 35c5d1a..3a9d426 100644
--- a/src/main/java/com/prgrms/prolog/domain/comment/service/CommentServiceImpl.java
+++ b/src/main/java/com/prgrms/prolog/domain/comment/service/CommentServiceImpl.java
@@ -26,19 +26,18 @@ public class CommentServiceImpl implements CommentService {
@Override
@Transactional
- public Long save(CreateCommentRequest request, String email, Long postId) {
+ public Long save(CreateCommentRequest request, Long userId, Long postId) {
Post findPost = getFindPostBy(postId);
- User findUser = getFindUserBy(email);
+ User findUser = getFindUserBy(userId);
Comment comment = buildComment(request, findPost, findUser);
return commentRepository.save(comment).getId();
}
@Override
@Transactional
- public Long update(UpdateCommentRequest request, String email, Long commentId) {
+ public Long update(UpdateCommentRequest request, Long userId, Long commentId) {
Comment findComment = commentRepository.joinUserByCommentId(commentId);
validateCommentNotNull(findComment);
- validateCommentOwnerNotSameEmail(email, findComment);
findComment.changeContent(request.content());
return findComment.getId();
}
@@ -51,8 +50,8 @@ private Comment buildComment(CreateCommentRequest request, Post findPost, User f
.build();
}
- private User getFindUserBy(String email) {
- return userRepository.findByEmail(email)
+ private User getFindUserBy(Long userId) {
+ return userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("exception.user.notExists"));
}
@@ -61,12 +60,6 @@ private Post getFindPostBy(Long postId) {
.orElseThrow(() -> new IllegalArgumentException("exception.post.notExists"));
}
- private void validateCommentOwnerNotSameEmail(String email, Comment comment) {
- if (! comment.checkUserEmail(email)) {
- throw new IllegalArgumentException("exception.user.email.notSame");
- }
- }
-
private void validateCommentNotNull(Comment comment) {
Assert.notNull(comment, "exception.comment.notExists");
}
diff --git a/src/main/java/com/prgrms/prolog/domain/like/api/LikeController.java b/src/main/java/com/prgrms/prolog/domain/like/api/LikeController.java
new file mode 100644
index 0000000..5a878e9
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/like/api/LikeController.java
@@ -0,0 +1,44 @@
+package com.prgrms.prolog.domain.like.api;
+
+import javax.validation.Valid;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.DeleteMapping;
+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;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.prgrms.prolog.domain.like.dto.LikeDto;
+import com.prgrms.prolog.domain.like.dto.LikeDto.likeRequest;
+import com.prgrms.prolog.domain.like.service.LikeServiceImpl;
+import com.prgrms.prolog.global.jwt.JwtAuthentication;
+
+import lombok.RequiredArgsConstructor;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/like")
+public class LikeController {
+
+ private final LikeServiceImpl likeService;
+
+ @PostMapping(value = "/{postId}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void insert(
+ @PathVariable Long postId,
+ @AuthenticationPrincipal JwtAuthentication user
+ ) {
+ LikeDto.likeRequest request = new likeRequest(user.id(), postId);
+ likeService.save(request);
+ }
+
+ @DeleteMapping
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void delete(@RequestBody @Valid likeRequest likeRequest) {
+ likeService.cancel(likeRequest);
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/like/dto/LikeDto.java b/src/main/java/com/prgrms/prolog/domain/like/dto/LikeDto.java
new file mode 100644
index 0000000..86a0807
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/like/dto/LikeDto.java
@@ -0,0 +1,10 @@
+package com.prgrms.prolog.domain.like.dto;
+
+import javax.validation.constraints.NotNull;
+
+public class LikeDto {
+
+ public record likeRequest(@NotNull Long userId,
+ @NotNull Long postId) {
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/like/model/Like.java b/src/main/java/com/prgrms/prolog/domain/like/model/Like.java
new file mode 100644
index 0000000..0d3aaa8
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/like/model/Like.java
@@ -0,0 +1,44 @@
+package com.prgrms.prolog.domain.like.model;
+
+import static javax.persistence.FetchType.*;
+import static javax.persistence.GenerationType.*;
+import static lombok.AccessLevel.*;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+
+import com.prgrms.prolog.domain.post.model.Post;
+import com.prgrms.prolog.domain.user.model.User;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Table(name = "likes")
+@Entity
+@Getter
+@NoArgsConstructor(access = PROTECTED)
+public class Like {
+
+ @Id
+ @GeneratedValue(strategy = IDENTITY)
+ private Long id;
+
+ @ManyToOne(fetch = LAZY)
+ @JoinColumn(name = "user_id")
+ private User user;
+
+ @ManyToOne(fetch = LAZY)
+ @JoinColumn(name = "post_id")
+ private Post post;
+
+ @Builder
+ public Like(User user, Post post) {
+ this.user = user;
+ this.post = post;
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/like/repository/LikeRepository.java b/src/main/java/com/prgrms/prolog/domain/like/repository/LikeRepository.java
new file mode 100644
index 0000000..9d4bfc2
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/like/repository/LikeRepository.java
@@ -0,0 +1,15 @@
+package com.prgrms.prolog.domain.like.repository;
+
+import java.util.Optional;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import com.prgrms.prolog.domain.like.model.Like;
+import com.prgrms.prolog.domain.post.model.Post;
+import com.prgrms.prolog.domain.user.model.User;
+
+@Repository
+public interface LikeRepository extends JpaRepository {
+ Optional findByUserAndPost(User user, Post post);
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/like/service/LikeService.java b/src/main/java/com/prgrms/prolog/domain/like/service/LikeService.java
new file mode 100644
index 0000000..5f6503a
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/like/service/LikeService.java
@@ -0,0 +1,9 @@
+package com.prgrms.prolog.domain.like.service;
+
+import com.prgrms.prolog.domain.like.dto.LikeDto;
+
+public interface LikeService {
+ Long save(LikeDto.likeRequest likeRequest);
+
+ void cancel(LikeDto.likeRequest likeRequest);
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/like/service/LikeServiceImpl.java b/src/main/java/com/prgrms/prolog/domain/like/service/LikeServiceImpl.java
new file mode 100644
index 0000000..04e8244
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/like/service/LikeServiceImpl.java
@@ -0,0 +1,73 @@
+package com.prgrms.prolog.domain.like.service;
+
+import javax.persistence.EntityNotFoundException;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.prgrms.prolog.domain.like.dto.LikeDto.likeRequest;
+import com.prgrms.prolog.domain.like.model.Like;
+import com.prgrms.prolog.domain.like.repository.LikeRepository;
+import com.prgrms.prolog.domain.post.model.Post;
+import com.prgrms.prolog.domain.post.repository.PostRepository;
+import com.prgrms.prolog.domain.user.model.User;
+import com.prgrms.prolog.domain.user.repository.UserRepository;
+
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Transactional
+@Service
+public class LikeServiceImpl implements LikeService {
+
+ private final LikeRepository likeRepository;
+ private final UserRepository userRepository;
+ private final PostRepository postRepository;
+
+ @Override
+ public Long save(likeRequest likeRequest) {
+
+ User user = getFindUserBy(likeRequest.userId());
+ Post post = getFindPostBy(likeRequest.postId());
+
+ //TODO 이미 좋아요 되어있으면 에러 반환 -> 409 Conflict 오류로 변환
+ if (likeRepository.findByUserAndPost(user, post).isPresent()) {
+ throw new EntityNotFoundException("exception.like.alreadyExist");
+ }
+
+ Like like = likeRepository.save(saveLike(user, post));
+
+ postRepository.addLikeCount(post.getId());
+ return like.getId();
+ }
+
+ @Override
+ public void cancel(likeRequest likeRequest) {
+
+ User user = getFindUserBy(likeRequest.userId());
+ Post post = getFindPostBy(likeRequest.postId());
+
+ Like like = likeRepository.findByUserAndPost(user, post)
+ .orElseThrow(() -> new EntityNotFoundException("exception.like.notExist"));
+
+ likeRepository.delete(like);
+ postRepository.subLikeCount(post.getId());
+ }
+
+ private Like saveLike(User user, Post post) {
+ return Like.builder()
+ .user(user)
+ .post(post)
+ .build();
+ }
+
+ private User getFindUserBy(Long userId) {
+ return userRepository.findById(userId)
+ .orElseThrow(() -> new IllegalArgumentException("exception.user.notExists"));
+ }
+
+ private Post getFindPostBy(Long postId) {
+ return postRepository.findById(postId)
+ .orElseThrow(() -> new IllegalArgumentException("exception.post.notExists"));
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/post/api/PostController.java b/src/main/java/com/prgrms/prolog/domain/post/api/PostController.java
index e0f2a40..f6eeb3d 100644
--- a/src/main/java/com/prgrms/prolog/domain/post/api/PostController.java
+++ b/src/main/java/com/prgrms/prolog/domain/post/api/PostController.java
@@ -7,6 +7,8 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -22,46 +24,49 @@
import com.prgrms.prolog.domain.post.dto.PostRequest.CreateRequest;
import com.prgrms.prolog.domain.post.dto.PostRequest.UpdateRequest;
import com.prgrms.prolog.domain.post.dto.PostResponse;
-import com.prgrms.prolog.domain.post.service.PostService;
+import com.prgrms.prolog.domain.post.service.PostServiceImpl;
import com.prgrms.prolog.global.jwt.JwtAuthentication;
@RestController
@RequestMapping("/api/v1/posts")
public class PostController {
- private final PostService postService;
+ private final PostServiceImpl postService;
- public PostController(PostService postService) {
+ public PostController(PostServiceImpl postService) {
this.postService = postService;
}
@PostMapping()
public ResponseEntity save(
@Valid @RequestBody CreateRequest create,
- @AuthenticationPrincipal JwtAuthentication jwt
+ @AuthenticationPrincipal JwtAuthentication user
) {
- String userEmail = jwt.userEmail();
- Long savePostId = postService.save(create, userEmail);
+ Long savePostId = postService.save(create, user.id());
URI location = UriComponentsBuilder.fromUriString("/api/v1/posts/" + savePostId).build().toUri();
return ResponseEntity.created(location).build();
}
@GetMapping("/{id}")
- public ResponseEntity findById(@PathVariable Long id) {
+ public ResponseEntity findById(@PathVariable Long id) { // 비공개 처리는?
PostResponse findPost = postService.findById(id);
return ResponseEntity.ok(findPost);
}
@GetMapping()
- public ResponseEntity> findAll(Pageable pageable) {
+ public ResponseEntity> findAll(
+ @PageableDefault(size=10, page=0, sort="updatedAt", direction= Sort.Direction.DESC) Pageable pageable
+ ) {
Page allPost = postService.findAll(pageable);
return ResponseEntity.ok(allPost.getContent());
}
@PatchMapping("/{id}")
- public ResponseEntity update(@PathVariable Long id,
+ public ResponseEntity update(
+ @PathVariable Long id,
+ @AuthenticationPrincipal JwtAuthentication user,
@Valid @RequestBody UpdateRequest postRequest) {
- PostResponse update = postService.update(id, postRequest);
+ PostResponse update = postService.update(postRequest, user.id(), id);
return ResponseEntity.ok(update);
}
diff --git a/src/main/java/com/prgrms/prolog/domain/post/dto/PostInfo.java b/src/main/java/com/prgrms/prolog/domain/post/dto/PostInfo.java
new file mode 100644
index 0000000..846e1b1
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/post/dto/PostInfo.java
@@ -0,0 +1,14 @@
+package com.prgrms.prolog.domain.post.dto;
+
+import com.prgrms.prolog.domain.post.model.Post;
+
+public record PostInfo(
+ Long id,
+ String title
+) {
+ public static PostInfo toPostInfo(Post post) {
+ return new PostInfo(
+ post.getId(),
+ post.getTitle()
+ );
+ }}
diff --git a/src/main/java/com/prgrms/prolog/domain/post/dto/PostRequest.java b/src/main/java/com/prgrms/prolog/domain/post/dto/PostRequest.java
index 78595db..2062bcd 100644
--- a/src/main/java/com/prgrms/prolog/domain/post/dto/PostRequest.java
+++ b/src/main/java/com/prgrms/prolog/domain/post/dto/PostRequest.java
@@ -3,13 +3,17 @@
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
+import org.springframework.lang.Nullable;
+
import com.prgrms.prolog.domain.post.model.Post;
import com.prgrms.prolog.domain.user.model.User;
public class PostRequest {
public record CreateRequest(@NotBlank @Size(max = 200) String title,
@NotBlank String content,
- boolean openStatus) {
+ @Nullable String tagText,
+ boolean openStatus,
+ @Nullable String seriesTitle) {
public static Post toEntity(CreateRequest create, User user) {
return Post.builder()
.title(create.title)
@@ -22,7 +26,7 @@ public static Post toEntity(CreateRequest create, User user) {
public record UpdateRequest(@NotBlank @Size(max = 200) String title,
@NotBlank String content,
+ @Nullable String tagText,
boolean openStatus) {
-
}
}
diff --git a/src/main/java/com/prgrms/prolog/domain/post/dto/PostResponse.java b/src/main/java/com/prgrms/prolog/domain/post/dto/PostResponse.java
index 37b36af..0c2ab9c 100644
--- a/src/main/java/com/prgrms/prolog/domain/post/dto/PostResponse.java
+++ b/src/main/java/com/prgrms/prolog/domain/post/dto/PostResponse.java
@@ -1,20 +1,35 @@
package com.prgrms.prolog.domain.post.dto;
+import static com.prgrms.prolog.domain.posttag.dto.PostTagDto.*;
+import static com.prgrms.prolog.domain.user.dto.UserDto.UserProfile.*;
+
import java.util.List;
+import java.util.Set;
import com.prgrms.prolog.domain.comment.model.Comment;
import com.prgrms.prolog.domain.post.model.Post;
-import com.prgrms.prolog.domain.user.dto.UserResponse;
+import com.prgrms.prolog.domain.series.dto.SeriesResponse;
+import com.prgrms.prolog.domain.user.dto.UserDto.UserProfile;
public record PostResponse(String title,
String content,
boolean openStatus,
- UserResponse.findResponse user,
+ UserProfile user,
+ Set tags,
+ SeriesResponse seriesResponse,
List comment,
- int commentCount) {
+ int commentCount,
+ int likeCount) {
public static PostResponse toPostResponse(Post post) {
- return new PostResponse(post.getTitle(), post.getContent(), post.isOpenStatus(),
- UserResponse.findResponse.toUserResponse(post.getUser()), post.getComments(), post.getComments().size());
+ return new PostResponse(post.getTitle(),
+ post.getContent(),
+ post.isOpenStatus(),
+ toUserProfile(post.getUser()),
+ PostTagsResponse.from(post.getPostTags()).tagNames(),
+ SeriesResponse.toSeriesResponse(post.getSeries()),
+ post.getComments(),
+ post.getComments().size(),
+ post.getLikeCount());
}
}
diff --git a/src/main/java/com/prgrms/prolog/domain/post/model/Post.java b/src/main/java/com/prgrms/prolog/domain/post/model/Post.java
index 647b685..7c617cf 100644
--- a/src/main/java/com/prgrms/prolog/domain/post/model/Post.java
+++ b/src/main/java/com/prgrms/prolog/domain/post/model/Post.java
@@ -4,22 +4,27 @@
import static javax.persistence.GenerationType.*;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
+import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
+import org.hibernate.annotations.ColumnDefault;
import org.springframework.util.Assert;
import com.prgrms.prolog.domain.comment.model.Comment;
import com.prgrms.prolog.domain.post.dto.PostRequest.UpdateRequest;
+import com.prgrms.prolog.domain.posttag.model.PostTag;
+import com.prgrms.prolog.domain.series.model.Series;
import com.prgrms.prolog.domain.user.model.User;
import com.prgrms.prolog.global.common.BaseEntity;
@@ -36,10 +41,6 @@ public class Post extends BaseEntity {
private static final int TITLE_MAX_SIZE = 50;
private static final int CONTENT_MAX_SIZE = 65535;
- private static final String USER_INFO_NEED_MESSAGE = "게시글은 작성자 정보가 필요합니다.";
- private static final String NOT_NULL_DATA_MESSAGE = "빈 값일 수 없는 데이터입니다.";
- private static final String OVER_LENGTH_MESSAGE = "입력할 수 있는 범위를 초과하였습니다.";
-
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
@@ -58,12 +59,24 @@ public class Post extends BaseEntity {
@OneToMany(mappedBy = "post")
private final List comments = new ArrayList<>();
+ @OneToMany(mappedBy = "post")
+ private final Set postTags = new HashSet<>();
+
+ @ManyToOne(fetch = LAZY)
+ @JoinColumn(name = "series_id")
+ private Series series;
+
+ @ColumnDefault("0")
+ @Column(name = "like_count")
+ private int likeCount;
+
@Builder
- public Post(String title, String content, boolean openStatus, User user) {
+ public Post(String title, String content, boolean openStatus, User user, Series series) {
this.title = validateTitle(title);
this.content = validateContent(content);
this.openStatus = openStatus;
this.user = Objects.requireNonNull(user, "exception.comment.user.require");
+ this.series = series;
}
public void setUser(User user) {
@@ -95,6 +108,10 @@ public void changePost(UpdateRequest updateRequest) {
this.openStatus = updateRequest.openStatus();
}
+ public void addPostTagsFrom(Set postTags) {
+ this.postTags.addAll(postTags);
+ }
+
private String validateTitle(String title) {
checkText(title);
checkOverLength(title, TITLE_MAX_SIZE);
@@ -116,4 +133,12 @@ private void checkOverLength(String text, int length) {
throw new IllegalArgumentException("exception.post.text.overLength");
}
}
+
+ public void setSeries(Series series) {
+ if (this.series != null) {
+ this.series.getPosts().remove(this);
+ }
+ this.series = series;
+ series.getPosts().add(this);
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/prgrms/prolog/domain/post/repository/PostRepository.java b/src/main/java/com/prgrms/prolog/domain/post/repository/PostRepository.java
index 5d6b3f8..70c54fb 100644
--- a/src/main/java/com/prgrms/prolog/domain/post/repository/PostRepository.java
+++ b/src/main/java/com/prgrms/prolog/domain/post/repository/PostRepository.java
@@ -1,8 +1,39 @@
package com.prgrms.prolog.domain.post.repository;
+import java.util.Optional;
+
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
import com.prgrms.prolog.domain.post.model.Post;
+@Repository
public interface PostRepository extends JpaRepository {
+
+ @Query("""
+ SELECT p
+ FROM Post p
+ LEFT JOIN FETCH p.comments c
+ where p.id = :postId
+ """)
+ Optional joinCommentFindById(@Param(value = "postId") Long postId);
+
+ @Query("""
+ SELECT p
+ FROM Post p
+ LEFT JOIN FETCH p.user
+ WHERE p.id = :postId
+ """)
+ Optional joinUserFindById(@Param(value = "postId") Long postId);
+
+ @Modifying
+ @Query("UPDATE Post p SET p.likeCount = p.likeCount + 1 WHERE p.id = :postId")
+ int addLikeCount(@Param(value = "postId") Long postId);
+
+ @Modifying
+ @Query("UPDATE Post p SET p.likeCount = p.likeCount - 1 WHERE p.id = :postId")
+ int subLikeCount(@Param(value = "postId") Long postId);
}
diff --git a/src/main/java/com/prgrms/prolog/domain/post/service/PostService.java b/src/main/java/com/prgrms/prolog/domain/post/service/PostService.java
index 24f2f52..33f36e7 100644
--- a/src/main/java/com/prgrms/prolog/domain/post/service/PostService.java
+++ b/src/main/java/com/prgrms/prolog/domain/post/service/PostService.java
@@ -2,62 +2,18 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-import com.prgrms.prolog.domain.post.dto.PostRequest.CreateRequest;
-import com.prgrms.prolog.domain.post.dto.PostRequest.UpdateRequest;
+import com.prgrms.prolog.domain.post.dto.PostRequest;
import com.prgrms.prolog.domain.post.dto.PostResponse;
-import com.prgrms.prolog.domain.post.model.Post;
-import com.prgrms.prolog.domain.post.repository.PostRepository;
-import com.prgrms.prolog.domain.user.model.User;
-import com.prgrms.prolog.domain.user.repository.UserRepository;
-@Service
-@Transactional
-public class PostService {
+public interface PostService {
+ Long save(PostRequest.CreateRequest request, Long userId);
- private static final String POST_NOT_EXIST_MESSAGE = "존재하지 않는 게시물입니다.";
- private static final String USER_NOT_EXIST_MESSAGE = "존재하지 않는 사용자입니다.";
+ PostResponse findById(Long postId);
- private final PostRepository postRepository;
- private final UserRepository userRepository;
+ Page findAll(Pageable pageable);
- public PostService(PostRepository postRepository, UserRepository userRepository) {
- this.postRepository = postRepository;
- this.userRepository = userRepository;
- }
+ PostResponse update(PostRequest.UpdateRequest update, Long userId, Long postId);
- public Long save(CreateRequest create, String userEmail) {
- User user = userRepository.findByEmail(userEmail)
- .orElseThrow(() -> new IllegalArgumentException(USER_NOT_EXIST_MESSAGE));
- Post post = postRepository.save(CreateRequest.toEntity(create, user));
- return post.getId();
- }
-
- @Transactional(readOnly = true)
- public PostResponse findById(Long id) {
- return postRepository.findById(id)
- .map(PostResponse::toPostResponse)
- .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE));
- }
-
- @Transactional(readOnly = true)
- public Page findAll(Pageable pageable) {
- return postRepository.findAll(pageable)
- .map(PostResponse::toPostResponse);
- }
-
- public PostResponse update(Long id, UpdateRequest update) {
- Post post = postRepository.findById(id)
- .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE));
- post.changePost(update);
- return PostResponse.toPostResponse(post);
- }
-
- public void delete(Long id) {
- Post findPost = postRepository.findById(id)
- .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE));
- postRepository.delete(findPost);
- }
-}
\ No newline at end of file
+ void delete(Long id);
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java b/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java
new file mode 100644
index 0000000..26ddf9a
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java
@@ -0,0 +1,242 @@
+package com.prgrms.prolog.domain.post.service;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.prgrms.prolog.domain.post.dto.PostRequest.CreateRequest;
+import com.prgrms.prolog.domain.post.dto.PostRequest.UpdateRequest;
+import com.prgrms.prolog.domain.post.dto.PostResponse;
+import com.prgrms.prolog.domain.post.model.Post;
+import com.prgrms.prolog.domain.post.repository.PostRepository;
+import com.prgrms.prolog.domain.posttag.model.PostTag;
+import com.prgrms.prolog.domain.posttag.repository.PostTagRepository;
+import com.prgrms.prolog.domain.roottag.model.RootTag;
+import com.prgrms.prolog.domain.roottag.repository.RootTagRepository;
+import com.prgrms.prolog.domain.roottag.util.TagConverter;
+import com.prgrms.prolog.domain.series.model.Series;
+import com.prgrms.prolog.domain.series.repository.SeriesRepository;
+import com.prgrms.prolog.domain.user.model.User;
+import com.prgrms.prolog.domain.user.repository.UserRepository;
+import com.prgrms.prolog.domain.usertag.model.UserTag;
+import com.prgrms.prolog.domain.usertag.repository.UserTagRepository;
+
+import lombok.RequiredArgsConstructor;
+
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class PostServiceImpl implements PostService {
+
+ private static final String POST_NOT_EXIST_MESSAGE = "존재하지 않는 게시물입니다.";
+ private static final String USER_NOT_EXIST_MESSAGE = "존재하지 않는 사용자입니다.";
+
+ private final SeriesRepository seriesRepository;
+ private final PostRepository postRepository;
+ private final UserRepository userRepository;
+ private final RootTagRepository rootTagRepository;
+ private final PostTagRepository postTagRepository;
+ private final UserTagRepository userTagRepository;
+
+
+ @Override
+ @Transactional
+ public Long save(CreateRequest request, Long userId) {
+ User findUser = userRepository.joinUserTagFindByUserId(userId);
+ Post createdPost = CreateRequest.toEntity(request, findUser);
+ Post savedPost = postRepository.save(createdPost);
+ updateNewPostAndUserIfTagExists(request.tagText(), savedPost, findUser);
+ registerSeries(request, savedPost, findUser);
+ return savedPost.getId();
+ }
+
+ private void registerSeries(CreateRequest request, Post post, User owner) {
+ String seriesTitle = request.seriesTitle();
+ if (seriesTitle == null || seriesTitle.isBlank()) {
+ seriesTitle = "시리즈 없음";
+ }
+ final String finalSeriesTitle = seriesTitle;
+ Series series = seriesRepository
+ .findByIdAndTitle(owner.getId(), seriesTitle)
+ .orElseGet(() -> seriesRepository.save(
+ Series.builder()
+ .title(finalSeriesTitle)
+ .user(owner)
+ .build()
+ )
+ );
+ post.setSeries(series);
+ }
+
+ @Override
+ public PostResponse findById(Long postId) {
+ Post post = postRepository.joinCommentFindById(postId)
+ .orElseThrow(() -> new IllegalArgumentException("exception.post.notExists"));
+ Set findPostTags = postTagRepository.joinRootTagFindByPostId(postId);
+ post.addPostTagsFrom(findPostTags);
+ return PostResponse.toPostResponse(post);
+ }
+
+ @Override
+ public Page findAll(Pageable pageable) {
+ return postRepository.findAll(pageable)
+ .map(PostResponse::toPostResponse);
+ }
+
+ @Override
+ @Transactional
+ public PostResponse update(UpdateRequest update, Long userId, Long postId) {
+ Post findPost = postRepository.findById(postId)
+ .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE));
+
+ if (!findPost.getUser().checkSameUserId(userId)) {
+ throw new IllegalArgumentException("exception.post.not.owner");
+ }
+
+ findPost.changePost(update);
+ updatePostAndUserIfTagChanged(update.tagText(), findPost);
+
+ Set findPostTags = postTagRepository.joinRootTagFindByPostId(findPost.getId());
+ findPost.addPostTagsFrom(findPostTags);
+ return PostResponse.toPostResponse(findPost);
+ }
+
+ @Override
+ @Transactional
+ public void delete(Long postId) {
+ Post findPost = postRepository.findById(postId)
+ .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE));
+ Set findRootTags = postTagRepository.joinRootTagFindByPostId(findPost.getId())
+ .stream()
+ .map(PostTag::getRootTag)
+ .collect(Collectors.toSet());
+ removeOrDecreaseUserTags(findPost.getUser(), findRootTags);
+ postTagRepository.deleteByPostId(postId);
+ postRepository.delete(findPost);
+ }
+
+ private void updatePostAndUserIfTagChanged(String tagText, Post findPost) {
+ Set tagNames = TagConverter.convertFrom(tagText);
+ Set currentRootTags = rootTagRepository.findByTagNamesIn(tagNames);
+ Set newTagNames = distinguishNewTagNames(tagNames, currentRootTags);
+ Set savedNewRootTags = saveNewRootTags(newTagNames);
+ saveNewPostTags(findPost, savedNewRootTags);
+ saveOrIncreaseUserTags(findPost.getUser(), savedNewRootTags);
+
+ Set findPostTags = postTagRepository.joinRootTagFindByPostId(findPost.getId());
+ Set oldRootTags = distinguishOldRootTags(tagNames, findPostTags);
+ removeOldPostTags(findPost, oldRootTags);
+ removeOrDecreaseUserTags(findPost.getUser(), oldRootTags);
+ }
+
+ private void removeOldPostTags(Post post, Set oldRootTags) {
+ if (oldRootTags.isEmpty()) {
+ return;
+ }
+ Set rootTagIds = oldRootTags.stream()
+ .map(RootTag::getId)
+ .collect(Collectors.toSet());
+ postTagRepository.deleteByPostIdAndRootTagIds(post.getId(), rootTagIds);
+ }
+
+ private void removeOrDecreaseUserTags(User user, Set oldRootTags) {
+ Map userTagMap = getFindUserTagMap(user, oldRootTags);
+ for (RootTag rootTag : oldRootTags) {
+ if (!userTagMap.containsKey(rootTag.getId())) {
+ continue;
+ }
+
+ UserTag currentUserTag = userTagMap.get(rootTag.getId());
+ currentUserTag.decreaseCount(1);
+ if (currentUserTag.isCountZero()) {
+ userTagRepository.deleteById(currentUserTag.getId());
+ }
+ }
+ }
+
+ private Set distinguishOldRootTags(Set tagNames, Set postTags) {
+ Set oldRootTags = new HashSet<>();
+ for (PostTag postTag : postTags) {
+ String postTagName = postTag.getRootTag().getName();
+ boolean isPostTagRemoved = !tagNames.contains(postTagName);
+ if (isPostTagRemoved) {
+ oldRootTags.add(postTag.getRootTag());
+ }
+ }
+ return oldRootTags;
+ }
+
+ private void updateNewPostAndUserIfTagExists(String tagText, Post savedPost, User findUser) {
+ Set tagNames = TagConverter.convertFrom(tagText);
+ if (tagNames.isEmpty()) {
+ return;
+ }
+
+ Set currentRootTags = rootTagRepository.findByTagNamesIn(tagNames);
+ Set newTagNames = distinguishNewTagNames(tagNames, currentRootTags);
+ Set savedNewRootTags = saveNewRootTags(newTagNames);
+ currentRootTags.addAll(savedNewRootTags);
+ saveNewPostTags(savedPost, currentRootTags);
+ saveOrIncreaseUserTags(findUser, currentRootTags);
+ }
+
+ private void saveOrIncreaseUserTags(User user, Set rootTags) {
+ Map userTagMap = getFindUserTagMap(user, rootTags);
+ for (RootTag rootTag : rootTags) {
+ boolean isUserTagExists = userTagMap.containsKey(rootTag.getId());
+ if (isUserTagExists) {
+ userTagMap.get(rootTag.getId()).increaseCount(1);
+ }
+ if (!isUserTagExists) {
+ userTagRepository.save(UserTag.builder()
+ .user(user)
+ .rootTag(rootTag)
+ .count(1)
+ .build());
+ }
+ }
+ }
+
+ private Map getFindUserTagMap(User user, Set rootTags) {
+ List rootTagIds = rootTags.stream()
+ .map(RootTag::getId)
+ .toList();
+ return userTagRepository.findByUserIdAndInRootTagIds(user.getId(), rootTagIds)
+ .stream()
+ .collect(Collectors.toMap(userTag -> userTag.getRootTag().getId(), userTag -> userTag));
+ }
+
+ private void saveNewPostTags(Post post, Set rootTags) {
+ rootTags.forEach(rootTag -> postTagRepository.save(
+ PostTag.builder()
+ .rootTag(rootTag)
+ .post(post)
+ .build()));
+ }
+
+ private Set distinguishNewTagNames(Set tagNames, Set rootTags) {
+ Set newTagNames = new HashSet<>(tagNames);
+ for (RootTag rootTag : rootTags) {
+ newTagNames.remove(rootTag.getName());
+ }
+ return newTagNames;
+ }
+
+ private Set saveNewRootTags(Set newTagNames) {
+ if (newTagNames.isEmpty()) {
+ return Collections.emptySet();
+ }
+ return newTagNames.stream()
+ .map(RootTag::new)
+ .map(rootTagRepository::save)
+ .collect(Collectors.toSet());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/prgrms/prolog/domain/posttag/dto/PostTagDto.java b/src/main/java/com/prgrms/prolog/domain/posttag/dto/PostTagDto.java
new file mode 100644
index 0000000..f302461
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/posttag/dto/PostTagDto.java
@@ -0,0 +1,18 @@
+package com.prgrms.prolog.domain.posttag.dto;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.prgrms.prolog.domain.posttag.model.PostTag;
+
+public class PostTagDto {
+
+ public record PostTagsResponse(Set tagNames) {
+ public static PostTagsResponse from(Set postTags) {
+ Set postTagNames = postTags.stream()
+ .map(postTag -> postTag.getRootTag().getName())
+ .collect(Collectors.toSet());
+ return new PostTagsResponse(postTagNames);
+ }
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/posttag/model/PostTag.java b/src/main/java/com/prgrms/prolog/domain/posttag/model/PostTag.java
new file mode 100644
index 0000000..4272988
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/posttag/model/PostTag.java
@@ -0,0 +1,54 @@
+package com.prgrms.prolog.domain.posttag.model;
+
+import static javax.persistence.FetchType.*;
+import static javax.persistence.GenerationType.*;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+
+import org.springframework.util.Assert;
+
+import com.prgrms.prolog.domain.post.model.Post;
+import com.prgrms.prolog.domain.roottag.model.RootTag;
+
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class PostTag {
+
+ @Id
+ @GeneratedValue(strategy = IDENTITY)
+ private Long id;
+
+ @ManyToOne(fetch = LAZY)
+ @JoinColumn(name = "post_id")
+ private Post post;
+
+ @ManyToOne(fetch = LAZY)
+ @JoinColumn(name = "root_tag_id")
+ private RootTag rootTag;
+
+ @Builder
+ public PostTag(Post post, RootTag rootTag) {
+ this.post = validatePost(post);
+ this.rootTag = validateRootTag(rootTag);
+ }
+
+ private RootTag validateRootTag(RootTag rootTag) {
+ Assert.notNull(rootTag, "exception.postTag.rootTag.null");
+ return rootTag;
+ }
+
+ private Post validatePost(Post post) {
+ Assert.notNull(post, "exception.postTag.post.null");
+ return post;
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/posttag/repository/PostTagRepository.java b/src/main/java/com/prgrms/prolog/domain/posttag/repository/PostTagRepository.java
new file mode 100644
index 0000000..2b79d56
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/posttag/repository/PostTagRepository.java
@@ -0,0 +1,41 @@
+package com.prgrms.prolog.domain.posttag.repository;
+
+import java.util.Set;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import com.prgrms.prolog.domain.posttag.model.PostTag;
+
+public interface PostTagRepository extends JpaRepository {
+
+ @Modifying
+ @Query("""
+ DELETE
+ FROM PostTag pt
+ WHERE pt.post.id = :postId
+ AND pt.rootTag.id IN :rootTagIds
+ """)
+ void deleteByPostIdAndRootTagIds(
+ @Param(value = "postId") Long postId,
+ @Param(value = "rootTagIds") Set rootTagIds
+ );
+
+ @Query("""
+ SELECT pt
+ FROM PostTag pt
+ LEFT JOIN FETCH pt.rootTag
+ WHERE pt.post.id = :postId
+ """)
+ Set joinRootTagFindByPostId(@Param(value = "postId") Long postId);
+
+ @Modifying
+ @Query("""
+ DELETE
+ FROM PostTag pt
+ WHERE pt.post.id = :postId
+ """)
+ void deleteByPostId(@Param(value = "postId") Long postId);
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/roottag/model/RootTag.java b/src/main/java/com/prgrms/prolog/domain/roottag/model/RootTag.java
new file mode 100644
index 0000000..600cd06
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/roottag/model/RootTag.java
@@ -0,0 +1,54 @@
+package com.prgrms.prolog.domain.roottag.model;
+
+import static javax.persistence.GenerationType.*;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.OneToMany;
+import javax.validation.constraints.NotNull;
+
+import org.springframework.util.Assert;
+
+import com.prgrms.prolog.domain.posttag.model.PostTag;
+import com.prgrms.prolog.domain.usertag.model.UserTag;
+
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class RootTag {
+
+ private static final int NAME_MAX_LENGTH = 100;
+
+ @Id
+ @GeneratedValue(strategy = IDENTITY)
+ private Long id;
+
+ @NotNull
+ private String name;
+
+ @OneToMany(mappedBy = "rootTag")
+ private final Set postTags = new HashSet<>();
+
+ @OneToMany(mappedBy = "rootTag")
+ private final Set userTag = new HashSet<>();
+
+ @Builder
+ public RootTag(String name) {
+ this.name = validateRootTagName(name);
+ }
+
+ private String validateRootTagName(String name) {
+ Assert.hasText(name, "exception.rootTag.name.text");
+ Assert.isTrue(name.length() <= NAME_MAX_LENGTH, "exception.rootTag.name.length");
+ return name;
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/roottag/repository/RootTagRepository.java b/src/main/java/com/prgrms/prolog/domain/roottag/repository/RootTagRepository.java
new file mode 100644
index 0000000..a8202a5
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/roottag/repository/RootTagRepository.java
@@ -0,0 +1,19 @@
+package com.prgrms.prolog.domain.roottag.repository;
+
+import java.util.Set;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import com.prgrms.prolog.domain.roottag.model.RootTag;
+
+public interface RootTagRepository extends JpaRepository {
+
+ @Query("""
+ SELECT rt
+ FROM RootTag rt
+ WHERE rt.name IN :tagNames
+ """)
+ Set findByTagNamesIn(@Param(value = "tagNames") Set tagNames);
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/roottag/util/TagConverter.java b/src/main/java/com/prgrms/prolog/domain/roottag/util/TagConverter.java
new file mode 100644
index 0000000..e668390
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/roottag/util/TagConverter.java
@@ -0,0 +1,24 @@
+package com.prgrms.prolog.domain.roottag.util;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class TagConverter {
+
+ private static final String TAG_EXPRESSION = "#";
+
+ private TagConverter() {
+ }
+
+ public static Set convertFrom(String tagNames) {
+ if (tagNames == null || tagNames.isBlank()) {
+ return Collections.emptySet();
+ }
+
+ return Arrays.stream(tagNames.split(TAG_EXPRESSION))
+ .filter(tagName -> !tagName.isBlank())
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/series/api/SeriesController.java b/src/main/java/com/prgrms/prolog/domain/series/api/SeriesController.java
new file mode 100644
index 0000000..de6c972
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/series/api/SeriesController.java
@@ -0,0 +1,32 @@
+package com.prgrms.prolog.domain.series.api;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+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 com.prgrms.prolog.domain.series.dto.SeriesResponse;
+import com.prgrms.prolog.domain.series.service.SeriesService;
+import com.prgrms.prolog.global.jwt.JwtAuthentication;
+
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/series")
+@RestController
+public class SeriesController {
+
+ private final SeriesService seriesService;
+
+ @GetMapping()
+ ResponseEntity findSeriesByTitle(
+ @RequestParam String title,
+ @AuthenticationPrincipal JwtAuthentication user
+ ) {
+ return ResponseEntity.ok(
+ seriesService.findByTitle(user.id(), title)
+ );
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/series/dto/CreateSeriesRequest.java b/src/main/java/com/prgrms/prolog/domain/series/dto/CreateSeriesRequest.java
new file mode 100644
index 0000000..a197ae2
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/series/dto/CreateSeriesRequest.java
@@ -0,0 +1,11 @@
+package com.prgrms.prolog.domain.series.dto;
+
+import javax.validation.constraints.NotBlank;
+
+public record CreateSeriesRequest(
+ @NotBlank String title
+) {
+ public static CreateSeriesRequest of(String title) {
+ return new CreateSeriesRequest(title);
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/series/dto/SeriesResponse.java b/src/main/java/com/prgrms/prolog/domain/series/dto/SeriesResponse.java
new file mode 100644
index 0000000..0d82e01
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/series/dto/SeriesResponse.java
@@ -0,0 +1,24 @@
+package com.prgrms.prolog.domain.series.dto;
+
+import java.util.List;
+
+import com.prgrms.prolog.domain.post.dto.PostInfo;
+import com.prgrms.prolog.domain.post.model.Post;
+import com.prgrms.prolog.domain.series.model.Series;
+
+public record SeriesResponse(
+ String title,
+ List posts,
+ int count
+) {
+ public static SeriesResponse toSeriesResponse(Series series) {
+ List posts = series.getPosts();
+ return new SeriesResponse(
+ series.getTitle(),
+ posts.stream()
+ .map(PostInfo::toPostInfo).toList(),
+ posts.size()
+ );
+ }
+
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/series/model/Series.java b/src/main/java/com/prgrms/prolog/domain/series/model/Series.java
new file mode 100644
index 0000000..63f0335
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/series/model/Series.java
@@ -0,0 +1,87 @@
+package com.prgrms.prolog.domain.series.model;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.OneToMany;
+
+import org.springframework.util.Assert;
+
+import com.prgrms.prolog.domain.post.model.Post;
+import com.prgrms.prolog.domain.user.model.User;
+
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class Series {
+
+ private static final int TITLE_MAX_SIZE = 50;
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private String title;
+
+ @OneToMany(mappedBy = "series")
+ private final List posts = new ArrayList<>();
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id")
+ private User user;
+
+ @Builder
+ public Series(String title, User user, Post post) {
+ this.title = validateTitle(title);
+ this.user = Objects.requireNonNull(user, "exception.comment.user.require");
+ addPost(post);
+ }
+
+ private void addPost(Post post) {
+ if (post == null) {
+ return;
+ }
+ post.setSeries(this);
+ }
+
+ public void setUser(User user) {
+ if (this.user != null) {
+ this.user.getSeries().remove(this);
+ }
+ this.user = user;
+ user.getSeries().add(this);
+ }
+
+ public void changeTitle(String title) {
+ this.title = validateTitle(title);
+ }
+
+ private String validateTitle(String title) {
+ checkText(title);
+ checkOverLength(title, TITLE_MAX_SIZE);
+ return title;
+ }
+
+ private void checkText(String text) {
+ Assert.hasText(text, "exception.comment.text");
+ }
+
+ private void checkOverLength(String text, int length) {
+ if (text.length() > length) {
+ throw new IllegalArgumentException("exception.post.text.overLength");
+ }
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/series/repository/SeriesRepository.java b/src/main/java/com/prgrms/prolog/domain/series/repository/SeriesRepository.java
new file mode 100644
index 0000000..8400079
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/series/repository/SeriesRepository.java
@@ -0,0 +1,21 @@
+package com.prgrms.prolog.domain.series.repository;
+
+import java.util.Optional;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import com.prgrms.prolog.domain.series.model.Series;
+
+public interface SeriesRepository extends JpaRepository {
+
+ @Query("""
+ SELECT s
+ FROM Series s
+ WHERE s.user.id = :userId
+ and s.title = :title
+ """)
+ Optional findByIdAndTitle(@Param(value = "userId") Long userId, @Param(value = "title") String title);
+
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/series/service/SeriesService.java b/src/main/java/com/prgrms/prolog/domain/series/service/SeriesService.java
new file mode 100644
index 0000000..f87a057
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/series/service/SeriesService.java
@@ -0,0 +1,15 @@
+package com.prgrms.prolog.domain.series.service;
+
+import javax.validation.Valid;
+
+import com.prgrms.prolog.domain.series.dto.CreateSeriesRequest;
+import com.prgrms.prolog.domain.series.dto.SeriesResponse;
+import com.prgrms.prolog.global.common.IdResponse;
+
+public interface SeriesService {
+
+ IdResponse create(@Valid CreateSeriesRequest request, Long userId);
+
+ SeriesResponse findByTitle(Long userId, String title);
+
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/series/service/SeriesServiceImpl.java b/src/main/java/com/prgrms/prolog/domain/series/service/SeriesServiceImpl.java
new file mode 100644
index 0000000..48b3514
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/series/service/SeriesServiceImpl.java
@@ -0,0 +1,52 @@
+package com.prgrms.prolog.domain.series.service;
+
+import javax.validation.Valid;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.prgrms.prolog.domain.series.dto.CreateSeriesRequest;
+import com.prgrms.prolog.domain.series.dto.SeriesResponse;
+import com.prgrms.prolog.domain.series.model.Series;
+import com.prgrms.prolog.domain.series.repository.SeriesRepository;
+import com.prgrms.prolog.domain.user.model.User;
+import com.prgrms.prolog.domain.user.repository.UserRepository;
+import com.prgrms.prolog.global.common.IdResponse;
+
+import lombok.RequiredArgsConstructor;
+
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+@Service
+public class SeriesServiceImpl implements SeriesService {
+
+ private final SeriesRepository seriesRepository;
+ private final UserRepository userRepository;
+
+ @Override
+ @Transactional
+ public IdResponse create(@Valid CreateSeriesRequest request, Long userId) {
+ User findUser = getFindUserBy(userId);
+ Series series = buildSeries(request.title(), findUser);
+ return new IdResponse(seriesRepository.save(series).getId());
+ }
+
+ @Override
+ public SeriesResponse findByTitle(Long userId, String title) {
+ return seriesRepository.findByIdAndTitle(userId, title)
+ .map(SeriesResponse::toSeriesResponse)
+ .orElseThrow(() -> new IllegalArgumentException("exception.user.notExists"));
+ }
+
+ private Series buildSeries(String title, User user) {
+ return Series.builder()
+ .title(title)
+ .user(user)
+ .build();
+ }
+
+ private User getFindUserBy(Long userId) {
+ return userRepository.findById(userId)
+ .orElseThrow(() -> new IllegalArgumentException("exception.user.notExists"));
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/user/api/UserController.java b/src/main/java/com/prgrms/prolog/domain/user/api/UserController.java
index 469c2d5..dad2f1b 100644
--- a/src/main/java/com/prgrms/prolog/domain/user/api/UserController.java
+++ b/src/main/java/com/prgrms/prolog/domain/user/api/UserController.java
@@ -14,15 +14,19 @@
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
-@RequestMapping("/api/v1/users")
+@RequestMapping("/api/v1/user")
@RestController
public class UserController {
private final UserService userService;
@GetMapping("/me")
- ResponseEntity myPage(@AuthenticationPrincipal JwtAuthentication user) {
- return ResponseEntity.ok(userService.findByEmail(user.userEmail()));
+ ResponseEntity getMyProfile(
+ @AuthenticationPrincipal JwtAuthentication user
+ ) {
+ return ResponseEntity.ok(
+ userService.findUserProfileByUserId(user.id())
+ );
}
}
diff --git a/src/main/java/com/prgrms/prolog/domain/user/dto/UserDto.java b/src/main/java/com/prgrms/prolog/domain/user/dto/UserDto.java
index 7dcc34b..09753bd 100644
--- a/src/main/java/com/prgrms/prolog/domain/user/dto/UserDto.java
+++ b/src/main/java/com/prgrms/prolog/domain/user/dto/UserDto.java
@@ -6,31 +6,54 @@
public class UserDto {
- @Builder
- public record UserInfo(
+ public record UserProfile(
+ Long id,
String email,
String nickName,
String introduce,
- String prologName
+ String prologName,
+ String profileImgUrl
) {
- public UserInfo(User user) {
- this(
+ @Builder
+ public UserProfile(Long id, String email, String nickName, String introduce, String prologName,
+ String profileImgUrl) {
+ this.id = id;
+ this.email = email;
+ this.nickName = nickName;
+ this.introduce = introduce;
+ this.prologName = prologName;
+ this.profileImgUrl = profileImgUrl;
+ }
+
+ public static UserProfile toUserProfile(User user) {
+ return new UserProfile(
+ user.getId(),
user.getEmail(),
user.getNickName(),
user.getIntroduce(),
- user.getPrologName()
+ user.getPrologName(),
+ user.getProfileImgUrl()
);
}
}
- @Builder
- public record UserProfile(
+ public record UserInfo(
String email,
String nickName,
String provider,
- String oauthId
+ String oauthId,
+ String profileImgUrl
) {
-
+ @Builder
+ public UserInfo(String email, String nickName, String provider, String oauthId, String profileImgUrl) {
+ this.email = email;
+ this.nickName = nickName;
+ this.provider = provider;
+ this.oauthId = oauthId;
+ this.profileImgUrl = profileImgUrl;
+ }
}
+ public record IdResponse(Long id) {
+ }
}
diff --git a/src/main/java/com/prgrms/prolog/domain/user/model/User.java b/src/main/java/com/prgrms/prolog/domain/user/model/User.java
index 9fc85de..fb77a12 100644
--- a/src/main/java/com/prgrms/prolog/domain/user/model/User.java
+++ b/src/main/java/com/prgrms/prolog/domain/user/model/User.java
@@ -1,12 +1,15 @@
package com.prgrms.prolog.domain.user.model;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.persistence.Entity;
+import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@@ -18,6 +21,8 @@
import com.prgrms.prolog.domain.comment.model.Comment;
import com.prgrms.prolog.domain.post.model.Post;
+import com.prgrms.prolog.domain.series.model.Series;
+import com.prgrms.prolog.domain.usertag.model.UserTag;
import com.prgrms.prolog.global.common.BaseEntity;
import lombok.AccessLevel;
@@ -49,11 +54,17 @@ public class User extends BaseEntity {
private final List posts = new ArrayList<>();
@OneToMany(mappedBy = "user")
private final List comments = new ArrayList<>();
+ @OneToMany(mappedBy = "user")
+ private final Set userTags = new HashSet<>();
+ @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
+ private List series = new ArrayList<>();
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Size(max = 100)
private String email;
+ @Size(max = 255)
+ private String profileImgUrl;
@Size(max = 100)
private String nickName;
@Size(max = 100)
@@ -67,13 +78,14 @@ public class User extends BaseEntity {
@Builder
public User(String email, String nickName, String introduce,
- String prologName, String provider, String oauthId) {
+ String prologName, String provider, String oauthId, String profileImgUrl) {
this.email = validateEmail(email);
this.nickName = validateNickName(nickName);
this.introduce = validateIntroduce(introduce);
this.prologName = validatePrologName(prologName);
this.provider = Objects.requireNonNull(provider, "provider" + NULL_VALUE_MESSAGE);
this.oauthId = Objects.requireNonNull(oauthId, "oauthId" + NULL_VALUE_MESSAGE);
+ this.profileImgUrl = profileImgUrl;
}
private String validatePrologName(String prologName) {
@@ -135,6 +147,14 @@ public boolean checkSameEmail(String email) {
return this.email.equals(email);
}
+ public boolean checkSameUserId(Long userId) {
+ return Objects.equals(this.id, userId);
+ }
+
+ public void changeProfileImgUrl(String profileImgUrl) {
+ Objects.requireNonNull(profileImgUrl, "profileImgUrl" + NULL_VALUE_MESSAGE);
+ }
+
@Override
public String toString() {
return "User{"
diff --git a/src/main/java/com/prgrms/prolog/domain/user/repository/UserRepository.java b/src/main/java/com/prgrms/prolog/domain/user/repository/UserRepository.java
index cf9a540..04bd078 100644
--- a/src/main/java/com/prgrms/prolog/domain/user/repository/UserRepository.java
+++ b/src/main/java/com/prgrms/prolog/domain/user/repository/UserRepository.java
@@ -3,6 +3,8 @@
import java.util.Optional;
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 com.prgrms.prolog.domain.user.model.User;
@@ -10,5 +12,19 @@
@Repository
public interface UserRepository extends JpaRepository {
- Optional findByEmail(String email);
+ @Query("""
+ SELECT u
+ FROM User u
+ WHERE u.provider = :provider
+ and u.oauthId = :oauthId
+ """)
+ Optional findByProviderAndOauthId(String provider, String oauthId);
+
+ @Query("""
+ SELECT u
+ FROM User u
+ LEFT JOIN FETCH u.userTags
+ WHERE u.id = :userId
+ """)
+ User joinUserTagFindByUserId(@Param(value = "userId") Long userId);
}
diff --git a/src/main/java/com/prgrms/prolog/domain/user/service/UserService.java b/src/main/java/com/prgrms/prolog/domain/user/service/UserService.java
index 6927531..8f6a274 100644
--- a/src/main/java/com/prgrms/prolog/domain/user/service/UserService.java
+++ b/src/main/java/com/prgrms/prolog/domain/user/service/UserService.java
@@ -4,11 +4,11 @@
public interface UserService {
- /* 사용자 로그인 */
- UserInfo login(UserProfile userProfile);
+ /* 사용자 회원 가입 */
+ IdResponse signUp(UserInfo userInfo);
- /* 이메일로 사용자 조회 */
- UserInfo findByEmail(String email);
+ /* 사용자 조회 */
+ UserProfile findUserProfileByUserId(Long userId);
}
diff --git a/src/main/java/com/prgrms/prolog/domain/user/service/UserServiceImpl.java b/src/main/java/com/prgrms/prolog/domain/user/service/UserServiceImpl.java
index cbb553d..df4787a 100644
--- a/src/main/java/com/prgrms/prolog/domain/user/service/UserServiceImpl.java
+++ b/src/main/java/com/prgrms/prolog/domain/user/service/UserServiceImpl.java
@@ -19,35 +19,37 @@ public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
- /* [사용자 조회] 이메일 값으로 등록된 유저 정보 찾아서 제공 */
+ /* [사용자 조회] 사용자 ID를 통해 등록된 유저 정보 찾아서 제공 */
@Override
- public UserInfo findByEmail(String email) {
- return userRepository.findByEmail(email)
- .map(UserInfo::new)
- .orElseThrow(IllegalArgumentException::new);
+ public UserProfile findUserProfileByUserId(Long userId) {
+ return userRepository.findById(userId)
+ .map(UserProfile::toUserProfile)
+ .orElseThrow(() -> new IllegalArgumentException("유저를 찾을 수 없습니다."));
}
- /* [로그인] 등록된 사용자인지 확인해서 맞는 경우 정보 제공, 아닌 경우 등록 진행 */
+ /* [회원 가입] 등록된 사용자 인지 확인해서 맞는 경우 유저ID 제공, 아닌 경우 사용자 등록 */
@Transactional
- public UserInfo login(UserProfile userProfile) {
- return userRepository.findByEmail(userProfile.email())
- .map(UserInfo::new)
- .orElseGet(() -> register(userProfile));
+ public IdResponse signUp(UserInfo userInfo) {
+ return new IdResponse(
+ userRepository
+ .findByProviderAndOauthId(userInfo.provider(), userInfo.oauthId())
+ .map(User::getId)
+ .orElseGet(() -> register(userInfo).getId())
+ );
}
/* [사용자 등록] 디폴트 설정 값으로 회원가입 진행 */
- private UserInfo register(UserProfile userProfile) {
- return new UserInfo(
- userRepository.save(
+ private User register(UserInfo userInfo) {
+ return userRepository.save(
User.builder()
- .email(userProfile.email())
- .nickName(userProfile.nickName())
+ .email(userInfo.email())
+ .nickName(userInfo.nickName())
.introduce(DEFAULT_INTRODUCE)
- .prologName(userProfile.nickName() + "의 prolog")
- .provider(userProfile.provider())
- .oauthId(userProfile.oauthId())
+ .prologName(userInfo.nickName() + "의 prolog")
+ .provider(userInfo.provider())
+ .oauthId(userInfo.oauthId())
+ .profileImgUrl(userInfo.profileImgUrl())
.build()
- )
- );
+ );
}
}
diff --git a/src/main/java/com/prgrms/prolog/domain/usertag/model/UserTag.java b/src/main/java/com/prgrms/prolog/domain/usertag/model/UserTag.java
new file mode 100644
index 0000000..e3121fe
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/usertag/model/UserTag.java
@@ -0,0 +1,83 @@
+package com.prgrms.prolog.domain.usertag.model;
+
+import static javax.persistence.FetchType.*;
+import static javax.persistence.GenerationType.*;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.validation.constraints.PositiveOrZero;
+
+import org.springframework.util.Assert;
+
+import com.prgrms.prolog.domain.roottag.model.RootTag;
+import com.prgrms.prolog.domain.user.model.User;
+
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class UserTag {
+
+ @Id
+ @GeneratedValue(strategy = IDENTITY)
+ private Long id;
+
+ @PositiveOrZero
+ private Integer count;
+
+ @ManyToOne(fetch = LAZY)
+ @JoinColumn(name = "user_id")
+ private User user;
+
+ @ManyToOne(fetch = LAZY)
+ @JoinColumn(name = "root_tag_id")
+ private RootTag rootTag;
+
+ public UserTag(User user, RootTag rootTag) {
+ this.user = validateUser(user);
+ this.rootTag = validateRootTag(rootTag);
+ }
+
+ @Builder
+ public UserTag(Integer count, User user, RootTag rootTag) {
+ this.user = validateUser(user);
+ this.rootTag = validateRootTag(rootTag);
+ this.count = validateCount(count);
+ }
+
+ public void increaseCount(int count) {
+ Assert.isTrue(count >= 0, "exception.userTag.count.positive");
+ this.count += count;
+ }
+
+ public void decreaseCount(int count) {
+ Assert.isTrue(count >= 0, "exception.userTag.count.positive");
+ this.count -= count;
+ }
+
+ private RootTag validateRootTag(RootTag rootTag) {
+ Assert.notNull(rootTag, "exception.userTag.rootTag.null");
+ return rootTag;
+ }
+
+ private User validateUser(User user) {
+ Assert.notNull(user, "exception.userTag.user.null");
+ return user;
+ }
+
+ private Integer validateCount(Integer count) {
+ Assert.isTrue(count >= 0, "exception.userTag.count.positiveOrZero");
+ return count;
+ }
+
+ public boolean isCountZero() {
+ return this.count == 0;
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/domain/usertag/repository/UserTagRepository.java b/src/main/java/com/prgrms/prolog/domain/usertag/repository/UserTagRepository.java
new file mode 100644
index 0000000..2b2fd49
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/domain/usertag/repository/UserTagRepository.java
@@ -0,0 +1,24 @@
+package com.prgrms.prolog.domain.usertag.repository;
+
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import com.prgrms.prolog.domain.usertag.model.UserTag;
+
+public interface UserTagRepository extends JpaRepository {
+
+ @Query("""
+ SELECT ut
+ FROM UserTag ut
+ WHERE ut.user.id = :userId
+ AND ut.rootTag.id IN :rootTagIds
+ """)
+ Set findByUserIdAndInRootTagIds(
+ @Param(value = "userId") Long userId,
+ @Param(value = "rootTagIds") List rootTagIds
+ );
+}
diff --git a/src/main/java/com/prgrms/prolog/global/common/IdResponse.java b/src/main/java/com/prgrms/prolog/global/common/IdResponse.java
new file mode 100644
index 0000000..2258248
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/global/common/IdResponse.java
@@ -0,0 +1,4 @@
+package com.prgrms.prolog.global.common;
+
+public record IdResponse(Long id) {
+}
\ No newline at end of file
diff --git a/src/main/java/com/prgrms/prolog/global/config/DatabaseConfig.java b/src/main/java/com/prgrms/prolog/global/config/DatabaseConfig.java
deleted file mode 100644
index 73189b0..0000000
--- a/src/main/java/com/prgrms/prolog/global/config/DatabaseConfig.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.prgrms.prolog.global.config;
-
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Profile;
-import org.springframework.context.annotation.PropertySource;
-
-@Configuration
-@Profile("local")
-@PropertySource("classpath:db/db.properties") // env(db).properties 파일 소스 등록
-public class DatabaseConfig {
-
-}
diff --git a/src/main/java/com/prgrms/prolog/global/config/JpaConfig.java b/src/main/java/com/prgrms/prolog/global/config/JpaConfig.java
index 597c859..3f1b134 100644
--- a/src/main/java/com/prgrms/prolog/global/config/JpaConfig.java
+++ b/src/main/java/com/prgrms/prolog/global/config/JpaConfig.java
@@ -25,13 +25,13 @@ public class JpaConfig {
public AuditorAware auditorAwareProvider() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
- .map(this::getUser);
+ .map(this::getCreatorInfo);
}
- private String getUser(Authentication authentication) { //TODO: 메소드명 바꾸기
+ private String getCreatorInfo(Authentication authentication) {
if (isValidAuthentication(authentication)) {
JwtAuthentication user = (JwtAuthentication)authentication.getPrincipal();
- return user.userEmail();
+ return user.id().toString();
}
return null;
}
diff --git a/src/main/java/com/prgrms/prolog/global/config/SecurityConfig.java b/src/main/java/com/prgrms/prolog/global/config/SecurityConfig.java
index da7c02c..9c7f933 100644
--- a/src/main/java/com/prgrms/prolog/global/config/SecurityConfig.java
+++ b/src/main/java/com/prgrms/prolog/global/config/SecurityConfig.java
@@ -1,14 +1,12 @@
package com.prgrms.prolog.global.config;
import org.springframework.context.annotation.Configuration;
-import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
-import org.springframework.security.web.SecurityFilterChain;
import com.prgrms.prolog.global.jwt.JwtAuthenticationEntryPoint;
import com.prgrms.prolog.global.jwt.JwtAuthenticationFilter;
@@ -36,6 +34,7 @@ protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/docs/**").permitAll()
+ .antMatchers("/actuator/**").hasRole("USER")
.anyRequest().authenticated()
.and()
// REST API 기반이기 때문에 사용 X
diff --git a/src/main/java/com/prgrms/prolog/global/exception/GlobalExceptionHandler.java b/src/main/java/com/prgrms/prolog/global/exception/GlobalExceptionHandler.java
index 923891d..2f81e4c 100644
--- a/src/main/java/com/prgrms/prolog/global/exception/GlobalExceptionHandler.java
+++ b/src/main/java/com/prgrms/prolog/global/exception/GlobalExceptionHandler.java
@@ -68,11 +68,11 @@ public ErrorResponse handleIllegalArgumentException(IllegalArgumentException e)
return ErrorResponse.of(BAD_REQUEST.name(), MessageUtil.getMessage(e.getMessage()));
}
- @ResponseStatus(BAD_REQUEST)
+ @ResponseStatus(INTERNAL_SERVER_ERROR)
@ExceptionHandler(IllegalStateException.class)
public ErrorResponse handleIllegalStateException(IllegalStateException e) {
logWarn(e);
- return ErrorResponse.of(BAD_REQUEST.name(), MessageUtil.getMessage(e.getMessage()));
+ return ErrorResponse.of(INTERNAL_SERVER_ERROR.name(), MessageUtil.getMessage(e.getMessage()));
}
@ResponseStatus(INTERNAL_SERVER_ERROR)
@@ -87,13 +87,11 @@ private void logDebug(HttpServletRequest request, Exception e) {
log.debug("[EXCEPTION] HTTP_METHOD_TYPE -----> [{}]", request.getMethod());
log.debug("[EXCEPTION] EXCEPTION_TYPE -----> [{}]", e.getClass().getSimpleName());
log.debug("[EXCEPTION] EXCEPTION_MESSAGE -----> [{}]", MessageUtil.getMessage(e.getMessage()));
- log.debug("[EXCEPTION] -----> ", e);
}
private void logWarn(Exception e) {
log.warn("[EXCEPTION] EXCEPTION_TYPE -----> [{}]", e.getClass().getSimpleName());
log.warn("[EXCEPTION] EXCEPTION_MESSAGE -----> [{}]", MessageUtil.getMessage(e.getMessage()));
- log.warn("[EXCEPTION] -----> ", e);
}
private void logError(Exception e) {
diff --git a/src/main/java/com/prgrms/prolog/global/image/api/ImageController.java b/src/main/java/com/prgrms/prolog/global/image/api/ImageController.java
new file mode 100644
index 0000000..18500c5
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/global/image/api/ImageController.java
@@ -0,0 +1,42 @@
+package com.prgrms.prolog.global.image.api;
+
+import java.io.File;
+import java.util.UUID;
+
+import org.springframework.context.annotation.Profile;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.prgrms.prolog.global.image.dto.UploadFileResponse;
+import com.prgrms.prolog.global.image.util.FileManager;
+import com.prgrms.prolog.global.image.util.UploadFileToS3;
+
+import lombok.RequiredArgsConstructor;
+
+@Profile("!test")
+@RestController
+@RequiredArgsConstructor
+public class ImageController {
+
+ private static final String FILE_PATH = "posts";
+ private final FileManager fileManager;
+ private final UploadFileToS3 uploadFileToS3;
+
+ @PostMapping("/file")
+ public UploadFileResponse uploadFile(
+ @RequestPart(value = "file") MultipartFile multipartFile
+ ) {
+ String id = UUID.randomUUID().toString();
+ String originalFilename = multipartFile.getOriginalFilename();
+ String fileName = (originalFilename + id).replace(" ", "");
+
+ File tempTargetFile = fileManager.convertMultipartFileToFile(multipartFile)
+ .orElseThrow(() -> new IllegalArgumentException("exception.file.convert"));
+ String fileUrl = uploadFileToS3.upload(tempTargetFile, FILE_PATH, fileName);
+
+ fileManager.removeFile(tempTargetFile);
+ return UploadFileResponse.toUploadFileResponse(originalFilename, fileUrl);
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/global/image/dto/UploadFileResponse.java b/src/main/java/com/prgrms/prolog/global/image/dto/UploadFileResponse.java
new file mode 100644
index 0000000..a6f0e1b
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/global/image/dto/UploadFileResponse.java
@@ -0,0 +1,10 @@
+package com.prgrms.prolog.global.image.dto;
+
+public record UploadFileResponse(
+ String originalFileName,
+ String uploadFilePath) {
+
+ public static UploadFileResponse toUploadFileResponse(String originalFileName, String uploadFilePath) {
+ return new UploadFileResponse(originalFileName, uploadFilePath);
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/global/image/util/FileManager.java b/src/main/java/com/prgrms/prolog/global/image/util/FileManager.java
new file mode 100644
index 0000000..a20544b
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/global/image/util/FileManager.java
@@ -0,0 +1,43 @@
+package com.prgrms.prolog.global.image.util;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Optional;
+
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Profile("!test")
+@Slf4j
+@Component
+public class FileManager {
+
+ public Optional convertMultipartFileToFile(MultipartFile multipartFile) {
+ File newFile = new File(multipartFile.getOriginalFilename());
+ try {
+ if (newFile.createNewFile()) {
+ log.debug(newFile.getName() + " : 임시 파일을 생성했습니다");
+ try (FileOutputStream fos = new FileOutputStream(newFile)) {
+ fos.write(multipartFile.getBytes());
+ }
+ return Optional.of(newFile);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("exception.file.io");
+ }
+ return Optional.empty();
+ }
+
+ public void removeFile(File targetFile) {
+ String fileName = targetFile.getName();
+ if (targetFile.delete()) {
+ log.debug(fileName + " : 임시 파일를 삭제했습니다");
+ } else {
+ log.debug(fileName + " : 임시 파일를 삭제하지 못했습니다.");
+ }
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/global/image/util/UploadFileToS3.java b/src/main/java/com/prgrms/prolog/global/image/util/UploadFileToS3.java
new file mode 100644
index 0000000..92f08a3
--- /dev/null
+++ b/src/main/java/com/prgrms/prolog/global/image/util/UploadFileToS3.java
@@ -0,0 +1,29 @@
+package com.prgrms.prolog.global.image.util;
+
+import java.io.File;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+
+import com.amazonaws.services.s3.AmazonS3Client;
+import com.amazonaws.services.s3.model.PutObjectRequest;
+
+import lombok.RequiredArgsConstructor;
+
+@Profile("!test")
+@Component
+@RequiredArgsConstructor
+public class UploadFileToS3 {
+
+ private final AmazonS3Client amazonS3Client;
+
+ @Value("${cloud.aws.s3.bucket}")
+ private String bucket;
+
+ public String upload(File uploadFile, String dirName, String fileName) {
+ String savedFileName = dirName + "/" + fileName;
+ amazonS3Client.putObject(new PutObjectRequest(bucket, savedFileName, uploadFile));
+ return amazonS3Client.getUrl(bucket, savedFileName).toString();
+ }
+}
diff --git a/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthentication.java b/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthentication.java
index d9c02df..f5dd644 100644
--- a/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthentication.java
+++ b/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthentication.java
@@ -2,30 +2,31 @@
import java.util.Objects;
-public record JwtAuthentication(String token, String userEmail) {
+public record JwtAuthentication(String token, Long id) {
public JwtAuthentication {
validateToken(token);
- validateUserEmail(userEmail);
+ validateId(id);
}
+
private void validateToken(String token) {
if (Objects.isNull(token) || token.isBlank()) {
throw new IllegalArgumentException("토큰이 없습니다.");
}
}
- private void validateUserEmail(String userEmail) {
- if (Objects.isNull(userEmail) || userEmail.isBlank()) {
- throw new IllegalArgumentException("유저 이메일이 없습니다.");
+ private void validateId(Long userId) {
+ if (Objects.isNull(userId) || userId <= 0L) {
+ throw new IllegalArgumentException("유저의 ID가 없습니다.");
}
}
@Override
public String toString() {
- return "JwtAuthentication{"
- + "token='" + token + '\''
- + ", userEmail='" + userEmail + '\''
- + '}';
+ return "JwtAuthentication{" +
+ "token='" + token + '\'' +
+ ", id='" + id + '\'' +
+ '}';
}
}
diff --git a/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthenticationEntryPoint.java b/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthenticationEntryPoint.java
index 9db9f20..188afcd 100644
--- a/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthenticationEntryPoint.java
+++ b/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthenticationEntryPoint.java
@@ -1,13 +1,19 @@
package com.prgrms.prolog.global.jwt;
+import static org.springframework.http.HttpStatus.*;
+
+import java.io.IOException;
+
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.prgrms.prolog.global.dto.ErrorResponse;
+
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -18,15 +24,18 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { /
private static final String ERROR_LOG_MESSAGE = "[ERROR] {} : {}";
- // private final ObjectMapper objectMapper;
+ private final ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
- AuthenticationException authException) {
+ AuthenticationException authException) throws IOException {
log.info(ERROR_LOG_MESSAGE, authException.getClass().getSimpleName(), authException.getMessage());
- response.setStatus(HttpStatus.UNAUTHORIZED.value());
+ response.setStatus(UNAUTHORIZED.value());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
- // response.getWriter().write(objectMapper.writeValueAsString(ERROR_MESSAGE));
+ response.getWriter()
+ .write(objectMapper.writeValueAsString(
+ ErrorResponse.of(UNAUTHORIZED.name(), authException.getMessage()))
+ );
}
}
diff --git a/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthenticationFilter.java b/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthenticationFilter.java
index cfe3c15..b006e54 100644
--- a/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthenticationFilter.java
+++ b/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthenticationFilter.java
@@ -1,5 +1,7 @@
package com.prgrms.prolog.global.jwt;
+import static org.springframework.http.HttpHeaders.*;
+
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
@@ -19,25 +21,27 @@
import com.prgrms.prolog.global.jwt.JwtTokenProvider.Claims;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+@Slf4j
@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
- private static final String headerKey = "token";
+ private static final String BEARER_TYPE = "Bearer";
private final JwtTokenProvider jwtTokenProvider;
@Override
- protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain)
- throws ServletException, IOException {
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
- String token = getToken(req);
+ String token = getToken(request);
if (token != null) {
JwtAuthenticationToken authentication = createAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
- filterChain.doFilter(req, res);
+ filterChain.doFilter(request, response);
}
@Override
@@ -47,21 +51,31 @@ protected boolean shouldNotFilter(HttpServletRequest request) {
}
private String getToken(HttpServletRequest request) {
- String token = request.getHeader(headerKey);
+ String token = extractToken(request);
if (token != null) {
return URLDecoder.decode(token, StandardCharsets.UTF_8);
}
return null;
}
+ private String extractToken(HttpServletRequest request) {
+ String headerValue = request.getHeader(AUTHORIZATION);
+ if (headerValue != null) {
+ return headerValue.split(BEARER_TYPE)[1].trim();
+ }
+ return null;
+ }
+
private JwtAuthenticationToken createAuthentication(String token) {
Claims claims = jwtTokenProvider.getClaims(token);
- return new JwtAuthenticationToken(
- new JwtAuthentication(token, claims.getEmail()), getAuthorities(claims.getRole())
- );
+ JwtAuthentication principal
+ = new JwtAuthentication(token, claims.getUserId());
+
+ return new JwtAuthenticationToken(principal, getAuthorities(claims.getRole()));
}
private List getAuthorities(String role) {
return List.of(new SimpleGrantedAuthority(role));
}
+
}
diff --git a/src/main/java/com/prgrms/prolog/global/jwt/JwtTokenProvider.java b/src/main/java/com/prgrms/prolog/global/jwt/JwtTokenProvider.java
index 02387b8..6f14f74 100644
--- a/src/main/java/com/prgrms/prolog/global/jwt/JwtTokenProvider.java
+++ b/src/main/java/com/prgrms/prolog/global/jwt/JwtTokenProvider.java
@@ -16,6 +16,7 @@
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
+import lombok.ToString;
@Component
public final class JwtTokenProvider {
@@ -46,7 +47,7 @@ public String createAccessToken(Claims claims) {
.withIssuer(issuer)
.withIssuedAt(now)
.withExpiresAt(new Date(now.getTime() + expirySeconds * 1_000L))
- .withClaim("email", claims.getEmail())
+ .withClaim("userId", claims.getUserId())
.withClaim("role", claims.getRole())
.sign(algorithm);
}
@@ -55,19 +56,20 @@ public Claims getClaims(String token) throws JWTVerificationException { // 기
return new Claims(jwtVerifier.verify(token));
}
+ @ToString
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Claims {
- private String email;
+ private Long userId;
private String role;
private Date iat;
private Date exp;
protected Claims(DecodedJWT decodedJwt) {
- Claim email = decodedJwt.getClaim("email");
- if (!email.isNull()) {
- this.email = email.asString();
+ Claim id = decodedJwt.getClaim("userId");
+ if (!id.isNull()) {
+ this.userId = id.asLong();
}
Claim role = decodedJwt.getClaim("role");
if (!role.isNull()) {
@@ -77,21 +79,11 @@ protected Claims(DecodedJWT decodedJwt) {
this.exp = decodedJwt.getExpiresAt();
}
- public static Claims from(String email, String role) {
+ public static Claims from(Long userId, String role) {
Claims claims = new Claims();
- claims.email = email;
+ claims.userId = userId;
claims.role = role;
return claims;
}
-
- @Override
- public String toString() {
- return "Claims{"
- + "email='" + email + '\''
- + ", role='" + role + '\''
- + ", iat=" + iat
- + ", exp=" + exp
- + '}';
- }
}
}
diff --git a/src/main/java/com/prgrms/prolog/global/oauth/OAuthAuthenticationSuccessHandler.java b/src/main/java/com/prgrms/prolog/global/oauth/OAuthAuthenticationSuccessHandler.java
index 167bdec..382a194 100644
--- a/src/main/java/com/prgrms/prolog/global/oauth/OAuthAuthenticationSuccessHandler.java
+++ b/src/main/java/com/prgrms/prolog/global/oauth/OAuthAuthenticationSuccessHandler.java
@@ -12,7 +12,7 @@
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.prgrms.prolog.domain.user.dto.UserDto.UserProfile;
+import com.prgrms.prolog.domain.user.dto.UserDto.UserInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -36,8 +36,8 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
Object principal = authentication.getPrincipal();
if (principal instanceof OAuth2User oauth2User) {
- UserProfile userProfile = OAuthProvider.toUserProfile(oauth2User, providerName);
- String accessToken = oauthService.login(userProfile);
+ UserInfo userInfo = OAuthProvider.toUserProfile(oauth2User, providerName);
+ String accessToken = oauthService.login(userInfo);
setResponse(response, accessToken); // TODO: 헤더에 넣기
}
}
diff --git a/src/main/java/com/prgrms/prolog/global/oauth/OAuthProvider.java b/src/main/java/com/prgrms/prolog/global/oauth/OAuthProvider.java
index 1e277a0..b863694 100644
--- a/src/main/java/com/prgrms/prolog/global/oauth/OAuthProvider.java
+++ b/src/main/java/com/prgrms/prolog/global/oauth/OAuthProvider.java
@@ -12,16 +12,17 @@
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class OAuthProvider {
- public static UserProfile toUserProfile(OAuth2User oauth, String providerName) {
+ public static UserInfo toUserProfile(OAuth2User oauth, String providerName) {
Map response = oauth.getAttributes();
Map properties = oauth.getAttribute("properties");
Map account = oauth.getAttribute("kakao_account");
- return UserProfile.builder()
+ return UserInfo.builder()
.email(String.valueOf(account.get("email")))
.nickName(String.valueOf(properties.get("nickname")))
.oauthId(String.valueOf(response.get("id")))
.provider(providerName)
+ .profileImgUrl(String.valueOf(properties.get("profile_image")))
.build();
}
}
diff --git a/src/main/java/com/prgrms/prolog/global/oauth/OAuthService.java b/src/main/java/com/prgrms/prolog/global/oauth/OAuthService.java
index b9fb844..11c4e63 100644
--- a/src/main/java/com/prgrms/prolog/global/oauth/OAuthService.java
+++ b/src/main/java/com/prgrms/prolog/global/oauth/OAuthService.java
@@ -20,8 +20,9 @@ public class OAuthService {
private final UserService userService;
@Transactional
- public String login(UserProfile userProfile) {
- UserInfo user = userService.login(userProfile);
- return jwtTokenProvider.createAccessToken(Claims.from(user.email(), "ROLE_USER"));
+ public String login(UserInfo userInfo) {
+ Long userId = userService.signUp(userInfo).id();
+ return jwtTokenProvider.createAccessToken(
+ Claims.from(userId,"ROLE_USER"));
}
}
diff --git a/src/main/resources/appender/console-appender.xml b/src/main/resources/appender/console-appender.xml
new file mode 100644
index 0000000..4dc444b
--- /dev/null
+++ b/src/main/resources/appender/console-appender.xml
@@ -0,0 +1,7 @@
+
+
+
+ ${CONSOLE_LOG_PATTERN}
+
+
+
diff --git a/src/main/resources/appender/error-file-appender.xml b/src/main/resources/appender/error-file-appender.xml
new file mode 100644
index 0000000..04abd05
--- /dev/null
+++ b/src/main/resources/appender/error-file-appender.xml
@@ -0,0 +1,22 @@
+
+
+ ${LOG_PATH}/error/error.txt
+
+
+ ERROR
+ ACCEPT
+ DENY
+
+
+
+ ${LOG_PATTERN}
+
+
+
+ ${LOG_PATH}/error/error.%d{yyyy-MM-dd}.%i.txt
+ 100MB
+ 10
+ 1GB
+
+
+
diff --git a/src/main/resources/appender/info-file-appender.xml b/src/main/resources/appender/info-file-appender.xml
new file mode 100644
index 0000000..9c7b849
--- /dev/null
+++ b/src/main/resources/appender/info-file-appender.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ ${LOG_PATTERN}
+
+
+ ${LOG_PATH}/info/info.txt
+
+
+ ${LOG_PATH}/info/info.%d{yyyy-MM-dd}.%i.txt
+ 100MB
+ 10
+ 1GB
+
+
+
diff --git a/src/main/resources/appender/sql-file-appender.xml b/src/main/resources/appender/sql-file-appender.xml
new file mode 100644
index 0000000..f2bca72
--- /dev/null
+++ b/src/main/resources/appender/sql-file-appender.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ ${LOG_PATH}/db/sql.txt
+
+
+ ${SQL_LOG_PATTERN}
+
+
+
+ ${LOG_PATH}/db/sql.%d{yyyy-MM-dd}.%i.txt
+ 100MB
+ 10
+ 1GB
+
+
+
diff --git a/src/main/resources/appender/warn-file-appender.xml b/src/main/resources/appender/warn-file-appender.xml
new file mode 100644
index 0000000..8f618af
--- /dev/null
+++ b/src/main/resources/appender/warn-file-appender.xml
@@ -0,0 +1,22 @@
+
+
+ ${LOG_PATH}/warn/warn.txt
+
+
+ WARN
+ ACCEPT
+ DENY
+
+
+
+ ${LOG_PATTERN}
+
+
+
+ ${LOG_PATH}/warn/warn.%d{yyyy-MM-dd}.%i.txt
+ 100MB
+ 10
+ 1GB
+
+
+
diff --git a/src/main/resources/application-aws.yml b/src/main/resources/application-aws.yml
new file mode 100644
index 0000000..a35d90c
--- /dev/null
+++ b/src/main/resources/application-aws.yml
@@ -0,0 +1,12 @@
+cloud:
+ aws:
+ s3:
+ bucket: prolog-storage
+ credentials:
+ access-key: ${AWS_ACCESS_KEY}
+ secret-key: ${AWS_SECRET_KEY}
+ region:
+ static: ap-northeast-2
+ auto: false
+ stack:
+ auto: false
\ No newline at end of file
diff --git a/src/main/resources/application-db.yml b/src/main/resources/application-db.yml
index 021918d..865f609 100644
--- a/src/main/resources/application-db.yml
+++ b/src/main/resources/application-db.yml
@@ -1,8 +1,12 @@
# default
spring:
+ # database 설정
datasource:
- driver-class-name: com.mysql.cj.jdbc.Driver
+ driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
+ url: ${SPRING_DATASOURCE_URL}
+ username: ${SPRING_DATASOURCE_USERNAME}
+ password: ${SPRING_DATASOURCE_PASSWORD}
# 커넥션 풀 설정
hikari:
@@ -22,27 +26,23 @@ spring:
dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true
+ # flyway 설정
flyway:
- enabled: false
+ enabled: true
+ baseline-on-migrate: true
+ # 커넥션 풀 설정
messages:
encoding: UTF-8
basename: messages/exceptions/exception, messages/logs/log-form
+# SQL 로그 설정
+
--- # local
spring:
- config.activate.on-profile: "db-local"
-
- datasource:
- url: ${db.datasource.url}
- username: ${db.datasource.username}
- password: ${db.datasource.password}
-
-# SQL 로그 설정
-logging:
- level:
- org.hibernate.SQL: debug
- org.hibernate.type: trace # 파라미터 값
+ config:
+ activate.on-profile: "db-local"
+ import: optional:file:.env[.properties]
--- # prod
spring:
@@ -50,12 +50,6 @@ spring:
activate.on-profile: "db-prod"
import: optional:file:.env[.properties]
- datasource:
- url: ${MYSQL_URL}
- username: ${MYSQL_USERNAME}
- password: ${MYSQL_ROOT_PASSWORD}
-
- # Flyway 설정
flyway:
enabled: true
baseline-on-migrate: true
\ No newline at end of file
diff --git a/src/main/resources/application-security.yml b/src/main/resources/application-security.yml
index 48ccb8b..0a97d0c 100644
--- a/src/main/resources/application-security.yml
+++ b/src/main/resources/application-security.yml
@@ -2,7 +2,7 @@
jwt:
issuer: ${JWT_ISSUER}
secret-key: ${JWT_SECRET_KEY}
- expiry-seconds: 60
+ expiry-seconds: 1200
spring:
security:
@@ -13,7 +13,7 @@ spring:
client-name: kakao
client-id: ${CLIENT_ID}
client-secret: ${CLIENT_SECRET}
- scope: profile_nickname, account_email
+ scope: profile_nickname,profile_image,account_email
redirect-uri: ${REDIRECT_URI}
authorization-grant-type: authorization_code
client-authentication-method: POST
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 154a7c6..b722a5b 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -11,4 +11,11 @@ spring:
include:
- db
- exception
- - security
\ No newline at end of file
+ - security
+ - aws
+
+management:
+ endpoints:
+ web:
+ exposure:
+ include: '*'
diff --git a/src/main/resources/db/migration/V2.1__add_tag_table.sql b/src/main/resources/db/migration/V2.1__add_tag_table.sql
new file mode 100644
index 0000000..b46e414
--- /dev/null
+++ b/src/main/resources/db/migration/V2.1__add_tag_table.sql
@@ -0,0 +1,28 @@
+DROP TABLE IF EXISTS user_tag;
+DROP TABLE IF EXISTS post_tag;
+DROP TABLE IF EXISTS root_tag;
+
+CREATE TABLE root_tag
+(
+ id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ name varchar(100) NOT NULL UNIQUE
+);
+
+CREATE TABLE post_tag
+(
+ id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ post_id bigint NOT NULL,
+ root_tag_id bigint NOT NULL,
+ FOREIGN KEY fk_post_tag_post_id (post_id) REFERENCES post (id),
+ FOREIGN KEY fk_post_tag_root_tag_id (root_tag_id) REFERENCES root_tag (id)
+);
+
+CREATE TABLE user_tag
+(
+ id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ count int NOT NULL default 0,
+ user_id bigint NOT NULL,
+ root_tag_id bigint NOT NULL,
+ FOREIGN KEY fk_user_tag_user_id (user_id) REFERENCES users (id),
+ FOREIGN KEY fk_user_tag_root_tag_id (root_tag_id) REFERENCES root_tag (id)
+)
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V2.2__add_like_table.sql b/src/main/resources/db/migration/V2.2__add_like_table.sql
new file mode 100644
index 0000000..dd768c7
--- /dev/null
+++ b/src/main/resources/db/migration/V2.2__add_like_table.sql
@@ -0,0 +1,12 @@
+DROP TABLE IF EXISTS likes;
+
+CREATE TABLE likes
+(
+ id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ user_id bigint NOT NULL,
+ post_id bigint NOT NULL,
+ FOREIGN KEY fk_likes_user_id (user_id) REFERENCES users (id),
+ FOREIGN KEY fk_likes_post_id (post_id) REFERENCES post (id)
+);
+
+ALTER TABLE post ADD like_count INT DEFAULT 0;
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V2__add_profile_img_url.sql b/src/main/resources/db/migration/V2__add_profile_img_url.sql
new file mode 100644
index 0000000..640f4f8
--- /dev/null
+++ b/src/main/resources/db/migration/V2__add_profile_img_url.sql
@@ -0,0 +1 @@
+ALTER TABLE users ADD profile_img_url varchar(255) NULL AFTER email;
\ No newline at end of file
diff --git a/src/main/resources/log4jdbc.log4j2.properties b/src/main/resources/log4jdbc.log4j2.properties
new file mode 100644
index 0000000..8f18407
--- /dev/null
+++ b/src/main/resources/log4jdbc.log4j2.properties
@@ -0,0 +1,2 @@
+log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
+log4jdbc.dump.sql.maxlinelength=0
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index d9f564c..b2c35d6 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -1,18 +1,50 @@
-
-
+
+
-
+
+
-
-
- ${CONSOLE_LOG_PATTERN}
-
-
+
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/messages/exceptions/exception.properties b/src/main/resources/messages/exceptions/exception.properties
index a35f3cf..51ec01b 100644
--- a/src/main/resources/messages/exceptions/exception.properties
+++ b/src/main/resources/messages/exceptions/exception.properties
@@ -2,20 +2,17 @@
exception.user.notExists=유저가 존재하지 않습니다.
exception.user.email.notSame=해당 유저의 이메일과 일치하지 않습니다.
exception.user.require=해당하는 유저가 필요합니다.
-
## POST ##
exception.post.notExists=존재하지 않는 포스트입니다.
exception.post.content.overLength=게시글 내용의 최대 글자 수를 초과하였습나다.
exception.post.require=해당하는 게시글이 필요합니다.
exception.post.text=데이터는 빈 값일 수 없습니다.
exception.post.text.overLength=입력된 문자열이 최대 범위를 초과하였습니다.
-
## COMMENT ##
exception.comment.content.overLength=댓글 내용의 최대 글자 수를 초과하였습나다.
exception.comment.content.empty=댓글 내용은 빈 값일 수 없습니다.
exception.comment.notExists=존재하지 않는 댓글입니다.
exception.comment.user.require=게시글은 작성자 정보가 필요합니다.
-
## VALIDATION ##
javax.validation.constraints.AssertFalse.message={}는 false 이어야만 합니다.
javax.validation.constraints.AssertTrue.message={}는 true 이어야만 합니다.
@@ -32,8 +29,14 @@ javax.validation.constraints.Pattern.message={regexp} 정규 표현식에 일치
javax.validation.constraints.Positive.message=0 보다 커야합니다.
javax.validation.constraints.PositiveOrZero.message=0 보다 크거나 같아야 합니다.
javax.validation.constraints.Size.message={min} 과 {max} 사이의 값이어야 합니다.
-
## SECURITY ##
exception.jwtAuthentication.token.notExists=토큰이 존재하지 않습니다.
exception.jwtAuthentication.user.email.notExists=유저 이메일이 존재하지 않습니다.
exception.jwtAuthenticationToken.isAuthenticated=인증 정보를 확인할 수 없는 메서드 주입은 지원하지 않습니다. 생성자를 통해 생성해야 합니다.
+## LIKE ##
+exception.like.notExist=좋아요를 누르지않아 취소할 수 없습니다.
+exception.like.alreadyExist=이미 좋아요를 누른 게시물에는 좋아요를 할 수 없습니다.
+## FILE ##
+exception.file.convert=\uD30C\uC77C\uC744 \uBCC0\uD658\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
+exception.file.io=\uD30C\uC77C \uC0DD\uC131\uAD00\uB828 \uC624\uB958
+
diff --git a/src/test/java/com/prgrms/prolog/config/JwtConfig.java b/src/test/java/com/prgrms/prolog/config/JwtConfig.java
new file mode 100644
index 0000000..4d36121
--- /dev/null
+++ b/src/test/java/com/prgrms/prolog/config/JwtConfig.java
@@ -0,0 +1,20 @@
+package com.prgrms.prolog.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
+
+import com.prgrms.prolog.global.jwt.JwtTokenProvider;
+
+@TestConfiguration
+public class JwtConfig {
+
+ @Bean
+ public JwtTokenProvider jwtTokenProvider(
+ @Value("${jwt.issuer}") String issuer,
+ @Value("${jwt.secret-key}") String secretKey,
+ @Value("${jwt.expiry-seconds}") int expirySeconds
+ ) {
+ return new JwtTokenProvider(issuer,secretKey,expirySeconds);
+ }
+}
diff --git a/src/test/java/com/prgrms/prolog/config/TestContainerConfig.java b/src/test/java/com/prgrms/prolog/config/TestContainerConfig.java
new file mode 100644
index 0000000..da20bd4
--- /dev/null
+++ b/src/test/java/com/prgrms/prolog/config/TestContainerConfig.java
@@ -0,0 +1,25 @@
+package com.prgrms.prolog.config;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.testcontainers.containers.MySQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@Testcontainers
+public class TestContainerConfig {
+
+ @Container
+ public static MySQLContainer> MY_SQL_CONTAINER = new MySQLContainer("mysql:8")
+ .withDatabaseName("test");
+
+ @BeforeAll
+ static void beforeAll() {
+ MY_SQL_CONTAINER.start();
+ }
+
+ @AfterAll
+ static void afterAll() {
+ MY_SQL_CONTAINER.stop();
+ }
+}
diff --git a/src/test/java/com/prgrms/prolog/domain/comment/api/CommentControllerTest.java b/src/test/java/com/prgrms/prolog/domain/comment/api/CommentControllerTest.java
index bd3a811..ff9666a 100644
--- a/src/test/java/com/prgrms/prolog/domain/comment/api/CommentControllerTest.java
+++ b/src/test/java/com/prgrms/prolog/domain/comment/api/CommentControllerTest.java
@@ -14,6 +14,7 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
@@ -21,23 +22,24 @@
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.prgrms.prolog.config.RestDocsConfig;
import com.prgrms.prolog.domain.comment.dto.CommentDto;
import com.prgrms.prolog.domain.comment.service.CommentService;
-import com.prgrms.prolog.domain.user.dto.UserDto;
+import com.prgrms.prolog.domain.user.repository.UserRepository;
import com.prgrms.prolog.global.jwt.JwtTokenProvider;
+import com.prgrms.prolog.global.jwt.JwtTokenProvider.Claims;
import com.prgrms.prolog.utils.TestUtils;
@SpringBootTest
@ExtendWith(RestDocumentationExtension.class)
@Import(RestDocsConfig.class)
+@Transactional
class CommentControllerTest {
- private static final JwtTokenProvider jwtTokenProvider = JWT_TOKEN_PROVIDER;
-
@Autowired
RestDocumentationResultHandler restDocs;
@@ -49,6 +51,14 @@ class CommentControllerTest {
@Autowired
ObjectMapper objectMapper;
+ @Autowired
+ UserRepository userRepository;
+
+ @Autowired
+ private JwtTokenProvider jwtTokenProvider;
+
+ Long savedUserId ;
+
@BeforeEach
void setUpRestDocs(WebApplicationContext webApplicationContext,
RestDocumentationContextProvider restDocumentation) {
@@ -61,16 +71,16 @@ void setUpRestDocs(WebApplicationContext webApplicationContext,
@Test
void commentSaveApiTest() throws Exception {
- UserDto.UserInfo userInfo = getUserInfo();
- JwtTokenProvider.Claims claims = JwtTokenProvider.Claims.from(userInfo.email(), USER_ROLE);
+ savedUserId = userRepository.save(USER).getId();
+ Claims claims = Claims.from(savedUserId, USER_ROLE);
CommentDto.CreateCommentRequest createCommentRequest = new CommentDto.CreateCommentRequest(
TestUtils.getComment().getContent());
- when(commentService.save(createCommentRequest, userInfo.email(), 1L))
+ when(commentService.save(createCommentRequest, savedUserId, 1L))
.thenReturn(1L);
mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts/{post_id}/comments", 1L)
- .header("token", jwtTokenProvider.createAccessToken(claims))
+ .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createCommentRequest)))
.andExpect(status().isCreated())
@@ -84,18 +94,17 @@ void commentSaveApiTest() throws Exception {
@Test
void commentUpdateApiTest() throws Exception {
- UserDto.UserInfo userInfo = getUserInfo();
- JwtTokenProvider.Claims claims = JwtTokenProvider.Claims.from(userInfo.email(), USER_ROLE);
-
+ savedUserId = userRepository.save(USER).getId();
+ Claims claims = Claims.from(savedUserId, USER_ROLE);
CommentDto.UpdateCommentRequest updateCommentRequest = new CommentDto.UpdateCommentRequest(
TestUtils.getComment().getContent() + "updated");
- when(commentService.update(updateCommentRequest, userInfo.email(), 1L))
+ when(commentService.update(updateCommentRequest, savedUserId, 1L))
.thenReturn(1L);
// when
mockMvc.perform(RestDocumentationRequestBuilders.patch("/api/v1/posts/{post_id}/comments/{id}", 1, 1)
- .header("token", jwtTokenProvider.createAccessToken(claims))
+ .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updateCommentRequest)))
.andExpect(status().isOk())
diff --git a/src/test/java/com/prgrms/prolog/domain/comment/repository/CommentRepositoryTest.java b/src/test/java/com/prgrms/prolog/domain/comment/repository/CommentRepositoryTest.java
index cfd79ae..a2e9565 100644
--- a/src/test/java/com/prgrms/prolog/domain/comment/repository/CommentRepositoryTest.java
+++ b/src/test/java/com/prgrms/prolog/domain/comment/repository/CommentRepositoryTest.java
@@ -16,7 +16,6 @@
import com.prgrms.prolog.domain.post.repository.PostRepository;
import com.prgrms.prolog.domain.user.model.User;
import com.prgrms.prolog.domain.user.repository.UserRepository;
-import com.prgrms.prolog.global.config.DatabaseConfig;
import com.prgrms.prolog.global.config.JpaConfig;
@DataJpaTest
@@ -38,10 +37,12 @@ class CommentRepositoryTest {
void joinUserByCommentIdTest() {
// given
User user = userRepository.save(USER);
- Post post = postRepository.save(POST);
+ Post post = getPost();
+ post.setUser(user);
+ Post savedPost = postRepository.save(post);
Comment comment = Comment.builder()
.user(user)
- .post(post)
+ .post(savedPost)
.content("댓글 내용")
.build();
Comment savedComment = commentRepository.save(comment);
diff --git a/src/test/java/com/prgrms/prolog/domain/comment/service/CommentServiceImplTest.java b/src/test/java/com/prgrms/prolog/domain/comment/service/CommentServiceImplTest.java
index d7a3362..d72ca03 100644
--- a/src/test/java/com/prgrms/prolog/domain/comment/service/CommentServiceImplTest.java
+++ b/src/test/java/com/prgrms/prolog/domain/comment/service/CommentServiceImplTest.java
@@ -15,7 +15,7 @@
class CommentServiceImplTest {
@Mock
- CommentService commentService;
+ CommentServiceImpl commentService;
final CreateCommentRequest CREATE_COMMENT_REQUEST = new CreateCommentRequest(COMMENT.getContent());
final UpdateCommentRequest UPDATE_COMMENT_REQUEST = new UpdateCommentRequest(COMMENT.getContent() + "updated");
@@ -24,9 +24,9 @@ class CommentServiceImplTest {
@DisplayName("댓글 저장에 성공한다.")
void saveTest() {
// given
- when(commentService.save(any(), anyString(), anyLong())).thenReturn(1L);
+ when(commentService.save(any(), anyLong(), anyLong())).thenReturn(1L);
// when
- Long commentId = commentService.save(CREATE_COMMENT_REQUEST, USER.getEmail(), 1L);
+ Long commentId = commentService.save(CREATE_COMMENT_REQUEST, USER_ID, 1L);
// then
assertThat(commentId).isEqualTo(1L);
}
@@ -34,8 +34,8 @@ void saveTest() {
@Test
@DisplayName("댓글 수정에 성공한다.")
void updateTest() {
- when(commentService.update(any(), anyString(), anyLong())).thenReturn(1L);
- Long commentId = commentService.update(UPDATE_COMMENT_REQUEST, USER.getEmail(), 1L);
+ when(commentService.update(any(), anyLong(), anyLong())).thenReturn(1L);
+ Long commentId = commentService.update(UPDATE_COMMENT_REQUEST, USER_ID, 1L);
assertThat(commentId).isEqualTo(1L);
}
@@ -43,10 +43,10 @@ void updateTest() {
@DisplayName("존재하지 않는 댓글을 수정하면 예외가 발생한다.")
void updateNotExistsCommentThrowExceptionTest() {
// given
- when(commentService.update(UPDATE_COMMENT_REQUEST, USER.getEmail(), 0L)).thenThrow(
+ when(commentService.update(UPDATE_COMMENT_REQUEST, USER_ID, 0L)).thenThrow(
new IllegalArgumentException());
// when & then
- assertThatThrownBy(() -> commentService.update(UPDATE_COMMENT_REQUEST, USER.getEmail(), 0L)).isInstanceOf(
+ assertThatThrownBy(() -> commentService.update(UPDATE_COMMENT_REQUEST, USER_ID, 0L)).isInstanceOf(
IllegalArgumentException.class);
}
@@ -54,11 +54,12 @@ void updateNotExistsCommentThrowExceptionTest() {
@DisplayName("존재하지 않는 회원이 댓글을 저장하면 예외가 발생한다.")
void updateCommentByNotExistsUserThrowExceptionTest() {
// given
+
final UpdateCommentRequest updateCommentRequest = new UpdateCommentRequest("댓글 내용");
- when(commentService.update(updateCommentRequest, "존재하지않는이메일@test.com", 1L)).thenThrow(
+ when(commentService.update(updateCommentRequest, UNSAVED_USER_ID, 1L)).thenThrow(
new IllegalArgumentException());
// when & then
- assertThatThrownBy(() -> commentService.update(updateCommentRequest, "존재하지않는이메일@test.com", 1L)).isInstanceOf(
+ assertThatThrownBy(() -> commentService.update(updateCommentRequest, UNSAVED_USER_ID, 1L)).isInstanceOf(
IllegalArgumentException.class);
}
@@ -66,10 +67,10 @@ void updateCommentByNotExistsUserThrowExceptionTest() {
@DisplayName("존재하지 않는 게시글에 댓글을 저장하면 예외가 발생한다.")
void saveCommentNotExistsPostThrowExceptionTest() {
// given
- when(commentService.save(CREATE_COMMENT_REQUEST, USER.getEmail(), 0L)).thenThrow(
+ when(commentService.save(CREATE_COMMENT_REQUEST, USER_ID, 0L)).thenThrow(
new IllegalArgumentException());
// when & then
- assertThatThrownBy(() -> commentService.save(CREATE_COMMENT_REQUEST, USER.getEmail(), 0L)).isInstanceOf(
+ assertThatThrownBy(() -> commentService.save(CREATE_COMMENT_REQUEST, USER_ID, 0L)).isInstanceOf(
IllegalArgumentException.class);
}
diff --git a/src/test/java/com/prgrms/prolog/domain/like/api/LikeControllerTest.java b/src/test/java/com/prgrms/prolog/domain/like/api/LikeControllerTest.java
new file mode 100644
index 0000000..3199688
--- /dev/null
+++ b/src/test/java/com/prgrms/prolog/domain/like/api/LikeControllerTest.java
@@ -0,0 +1,118 @@
+package com.prgrms.prolog.domain.like.api;
+
+import static com.prgrms.prolog.utils.TestUtils.*;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*;
+import static org.springframework.restdocs.payload.PayloadDocumentation.*;
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.RestDocumentationContextProvider;
+import org.springframework.restdocs.RestDocumentationExtension;
+import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
+import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.context.WebApplicationContext;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.prgrms.prolog.config.RestDocsConfig;
+import com.prgrms.prolog.domain.like.dto.LikeDto;
+import com.prgrms.prolog.domain.like.service.LikeService;
+import com.prgrms.prolog.domain.post.model.Post;
+import com.prgrms.prolog.domain.post.repository.PostRepository;
+import com.prgrms.prolog.domain.user.model.User;
+import com.prgrms.prolog.domain.user.repository.UserRepository;
+import com.prgrms.prolog.global.jwt.JwtTokenProvider;
+import com.prgrms.prolog.global.jwt.JwtTokenProvider.Claims;
+
+@SpringBootTest
+@ExtendWith(RestDocumentationExtension.class)
+@Import(RestDocsConfig.class)
+@Transactional
+class LikeControllerTest {
+
+ @Autowired
+ RestDocumentationResultHandler restDocs;
+
+ @Autowired
+ LikeService likeService;
+
+ @Autowired
+ ObjectMapper objectMapper;
+
+ @Autowired
+ private JwtTokenProvider jwtTokenProvider;
+
+ @Autowired
+ UserRepository userRepository;
+
+ @Autowired
+ PostRepository postRepository;
+
+ MockMvc mockMvc;
+
+ @BeforeEach
+ void setUpRestDocs(WebApplicationContext webApplicationContext,
+ RestDocumentationContextProvider restDocumentation) {
+ this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
+ .apply(documentationConfiguration(restDocumentation))
+ .alwaysDo(restDocs)
+ .apply(springSecurity())
+ .build();
+ }
+
+ @Test
+ void likeSaveApiTest() throws Exception {
+ User savedUser = userRepository.save(USER);
+ Post post = getPost();
+ post.setUser(savedUser);
+ Post savedPost = postRepository.save(post);
+ Claims claims = Claims.from(savedUser.getId(), USER_ROLE);
+
+ LikeDto.likeRequest likeRequest = new LikeDto.likeRequest(savedUser.getId(), savedPost.getId());
+
+ mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/like/{postId}", savedPost.getId())
+ .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims))
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(likeRequest)))
+ .andExpect(status().isNoContent())
+ .andDo(restDocs.document(
+ responseBody()
+ ));
+ }
+
+ @Test
+ void likeCancelApiTest() throws Exception {
+ User savedUser = userRepository.save(USER);
+ Post post = getPost();
+ post.setUser(savedUser);
+ Post savedPost = postRepository.save(post);
+
+ Claims claims = Claims.from(savedUser.getId(), USER_ROLE);
+
+ LikeDto.likeRequest likeRequest = new LikeDto.likeRequest(savedUser.getId(), savedPost.getId());
+ likeService.save(likeRequest);
+
+ mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/like")
+ .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims))
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(likeRequest)))
+ .andExpect(status().isNoContent())
+ .andDo(restDocs.document(
+ requestFields(
+ fieldWithPath("userId").description("사용자 아이디"),
+ fieldWithPath("postId").description("게시물 아이디")
+ ),
+ responseBody()
+ ));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/prgrms/prolog/domain/like/repository/LikeRepositoryTest.java b/src/test/java/com/prgrms/prolog/domain/like/repository/LikeRepositoryTest.java
new file mode 100644
index 0000000..bf6e2b3
--- /dev/null
+++ b/src/test/java/com/prgrms/prolog/domain/like/repository/LikeRepositoryTest.java
@@ -0,0 +1,63 @@
+package com.prgrms.prolog.domain.like.repository;
+
+import static com.prgrms.prolog.utils.TestUtils.*;
+import static org.assertj.core.api.AssertionsForClassTypes.*;
+import static org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.*;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.context.annotation.Import;
+
+import com.prgrms.prolog.domain.like.model.Like;
+import com.prgrms.prolog.domain.post.model.Post;
+import com.prgrms.prolog.domain.post.repository.PostRepository;
+import com.prgrms.prolog.domain.user.model.User;
+import com.prgrms.prolog.domain.user.repository.UserRepository;
+import com.prgrms.prolog.global.config.JpaConfig;
+
+@DataJpaTest
+@AutoConfigureTestDatabase(replace = NONE)
+@Import({JpaConfig.class})
+class LikeRepositoryTest {
+
+ @Autowired
+ LikeRepository likeRepository;
+
+ @Autowired
+ PostRepository postRepository;
+
+ @Autowired
+ UserRepository userRepository;
+
+ @Test
+ @DisplayName("존재하는 사용자가 존재하는 게시물을 좋아요를 할 때 좋아요가 생긴다.")
+ void findByUserAndPostTest() {
+ // given
+ User savedUser = userRepository.save(USER);
+ Post post = Post.builder()
+ .title(TITLE)
+ .content(CONTENT)
+ .openStatus(true)
+ .user(savedUser)
+ .build();
+ Post savedPost = postRepository.save(post);
+
+ Like like = Like.builder()
+ .user(savedUser)
+ .post(savedPost)
+ .build();
+ Like savedLike = likeRepository.save(like);
+
+ // when
+ Optional actual = likeRepository.findByUserAndPost(savedUser, savedPost);
+
+ // then
+ assertThat(actual)
+ .hasValueSatisfying(l -> assertThat(l.getId()).isEqualTo(savedLike.getId()));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/prgrms/prolog/domain/like/service/LikeServiceTest.java b/src/test/java/com/prgrms/prolog/domain/like/service/LikeServiceTest.java
new file mode 100644
index 0000000..d070300
--- /dev/null
+++ b/src/test/java/com/prgrms/prolog/domain/like/service/LikeServiceTest.java
@@ -0,0 +1,133 @@
+package com.prgrms.prolog.domain.like.service;
+
+import static com.prgrms.prolog.utils.TestUtils.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.BDDMockito.*;
+
+import java.util.Optional;
+
+import javax.persistence.EntityNotFoundException;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import com.prgrms.prolog.domain.like.dto.LikeDto.likeRequest;
+import com.prgrms.prolog.domain.like.model.Like;
+import com.prgrms.prolog.domain.like.repository.LikeRepository;
+import com.prgrms.prolog.domain.post.repository.PostRepository;
+import com.prgrms.prolog.domain.user.repository.UserRepository;
+
+@ExtendWith(MockitoExtension.class)
+class LikeServiceTest {
+
+ @InjectMocks
+ private LikeServiceImpl likeService;
+
+ @Mock
+ private LikeRepository likeRepository;
+
+ @Mock
+ private PostRepository postRepository;
+
+ @Mock
+ private UserRepository userRepository;
+
+ @Mock
+ private Like like;
+
+ likeRequest likeRequest = new likeRequest(USER_ID, POST_ID);
+
+ @Test
+ @DisplayName("게시물에 좋아요를 누를 수 있다.")
+ void insertLikeTest() {
+ // given
+ given(userRepository.findById(USER_ID)).willReturn(Optional.of(USER));
+ given(postRepository.findById(POST_ID)).willReturn(Optional.of(POST));
+ given(likeRepository.findByUserAndPost(USER, POST)).willReturn(Optional.empty());
+ given(likeRepository.save(any(Like.class))).willReturn(like);
+ given(like.getId()).willReturn(1L);
+
+ // when
+ Long likeId = likeService.save(likeRequest);
+
+ // then
+ then(likeRepository).should().save(any(Like.class)); // 행위 검증
+ assertThat(likeId).isEqualTo(1L); // 상태 검증
+ }
+
+ @Test
+ @DisplayName("좋아요한 게시물을 좋아요 취소할 수 있다.")
+ void cancelLikeTest() {
+ // given
+ given(userRepository.findById(USER_ID)).willReturn(Optional.of(USER));
+ given(postRepository.findById(POST_ID)).willReturn(Optional.of(POST));
+ given(likeRepository.findByUserAndPost(USER, POST)).willReturn(Optional.of(like));
+
+ // when
+ likeService.cancel(likeRequest);
+
+ // then
+ then(likeRepository).should().delete(any(Like.class));
+ }
+
+ @Test
+ @DisplayName("좋아요한 게시물에 또 좋아요를 할 수 없다.")
+ void insertDuplicateLikeTest() {
+ given(userRepository.findById(USER_ID)).willReturn(Optional.of(USER));
+ given(postRepository.findById(POST_ID)).willReturn(Optional.of(POST));
+ given(likeRepository.findByUserAndPost(USER, POST)).willReturn(Optional.of(LIKE));
+
+ assertThatThrownBy(() -> likeService.save(likeRequest)).isInstanceOf(EntityNotFoundException.class);
+ }
+
+ @Test
+ @DisplayName("좋아요를 하지 않은 게시물에는 좋아요를 취소할 수 없다.")
+ void cancelDuplicateLikeTest() {
+ given(userRepository.findById(USER_ID)).willReturn(Optional.of(USER));
+ given(postRepository.findById(POST_ID)).willReturn(Optional.of(POST));
+ given(likeRepository.findByUserAndPost(USER, POST)).willThrow(EntityNotFoundException.class);
+
+ assertThatThrownBy(() -> likeService.save(likeRequest)).isInstanceOf(EntityNotFoundException.class);
+ }
+
+ @Test
+ @DisplayName("좋아요를 누르면 게시물의 총 좋아요의 개수가 1씩 증가한다.")
+ void addLikeCountTest() {
+ // given
+ given(userRepository.findById(USER_ID)).willReturn(Optional.of(USER));
+ given(postRepository.findById(POST_ID)).willReturn(Optional.of(POST));
+ given(likeRepository.findByUserAndPost(USER, POST)).willReturn(Optional.empty());
+ given(likeRepository.save(any(Like.class))).willReturn(like);
+ given(postRepository.addLikeCount(any())).willReturn(1);
+ given(like.getId()).willReturn(1L);
+
+ // when
+ likeService.save(likeRequest);
+
+ // then
+ then(postRepository).should().addLikeCount(any()); // 행위 검증
+ assertThat(postRepository.addLikeCount(POST_ID)).isEqualTo(1);
+ }
+
+ @Test
+ @DisplayName("좋아요를 게시물의 총 좋아요의 개수가 1씩 증가한다.")
+ void cancelLikeCountTest() {
+ // given
+ given(userRepository.findById(USER_ID)).willReturn(Optional.of(USER));
+ given(postRepository.findById(POST_ID)).willReturn(Optional.of(POST));
+ given(likeRepository.findByUserAndPost(USER, POST)).willReturn(Optional.of(LIKE));
+ willDoNothing().given(likeRepository).delete(any(Like.class));
+ given(postRepository.subLikeCount(any())).willReturn(1);
+
+ // when
+ likeService.cancel(likeRequest);
+
+ // then
+ then(postRepository).should().subLikeCount(any());
+ assertThat(postRepository.subLikeCount(POST_ID)).isEqualTo(1);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/prgrms/prolog/domain/post/api/PostControllerTest.java b/src/test/java/com/prgrms/prolog/domain/post/api/PostControllerTest.java
index 5e4445f..fc41fc4 100644
--- a/src/test/java/com/prgrms/prolog/domain/post/api/PostControllerTest.java
+++ b/src/test/java/com/prgrms/prolog/domain/post/api/PostControllerTest.java
@@ -3,69 +3,96 @@
import static com.prgrms.prolog.utils.TestUtils.*;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
-import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
+import org.springframework.restdocs.RestDocumentationContextProvider;
+import org.springframework.restdocs.RestDocumentationExtension;
+import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
+import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.context.WebApplicationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.prgrms.prolog.config.RestDocsConfig;
+import com.prgrms.prolog.config.TestContainerConfig;
import com.prgrms.prolog.domain.post.dto.PostRequest.CreateRequest;
import com.prgrms.prolog.domain.post.dto.PostRequest.UpdateRequest;
import com.prgrms.prolog.domain.post.service.PostService;
+import com.prgrms.prolog.domain.series.repository.SeriesRepository;
import com.prgrms.prolog.domain.user.repository.UserRepository;
+import com.prgrms.prolog.global.jwt.JwtTokenProvider;
import com.prgrms.prolog.global.jwt.JwtTokenProvider.Claims;
-@AutoConfigureRestDocs
+@ExtendWith(RestDocumentationExtension.class)
+@Import({RestDocsConfig.class, TestContainerConfig.class})
@SpringBootTest
-@AutoConfigureMockMvc
@Transactional
class PostControllerTest {
@Autowired
- private MockMvc mockMvc;
+ private JwtTokenProvider jwtTokenProvider;
+ @Autowired
+ private RestDocumentationResultHandler restDocs;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private PostService postService;
@Autowired
private UserRepository userRepository;
+ @Autowired
+ private SeriesRepository seriesRepository;
- static Claims claims = Claims.from(USER_EMAIL, "ROLE_USER");
- Long postId;
+ private MockMvc mockMvc;
+ private Long userId;
+ private Claims claims;
+ private Long postId;
@BeforeEach
- void setUp() {
- userRepository.save(USER);
- CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", false);
- postId = postService.save(createRequest, USER_EMAIL);
+ void setUp(WebApplicationContext webApplicationContext,
+ RestDocumentationContextProvider restDocumentation) {
+
+ userId = userRepository.save(USER).getId();
+ claims = Claims.from(userId, "ROLE_USER");
+ CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", "#tag", true, SERIES_TITLE);
+ postId = postService.save(createRequest, userId);
+ this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
+ .apply(documentationConfiguration(restDocumentation))
+ .alwaysDo(restDocs)
+ .apply(springSecurity())
+ .build();
}
@Test
@DisplayName("게시물을 등록할 수 있다.")
void save() throws Exception {
- CreateRequest request = new CreateRequest("생성된 테스트 제목", "생성된 테스트 내용", true);
+ CreateRequest request = new CreateRequest("생성된 테스트 제목", "생성된 테스트 내용", "tag", true, SERIES_TITLE);
- mockMvc.perform(post("/api/v1/posts")
- .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims))
+ mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts")
+ .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
).andExpect(status().isCreated())
- .andDo(document("post-save",
+ .andDo(restDocs.document(
requestFields(
fieldWithPath("title").type(JsonFieldType.STRING).description("title"),
fieldWithPath("content").type(JsonFieldType.STRING).description("content"),
- fieldWithPath("openStatus").type(JsonFieldType.BOOLEAN).description("openStatus")
+ fieldWithPath("tagText").type(JsonFieldType.STRING).description("tagText"),
+ fieldWithPath("openStatus").type(JsonFieldType.BOOLEAN).description("openStatus"),
+ fieldWithPath("seriesTitle").type(JsonFieldType.STRING).description("seriesTitle")
),
responseBody()
));
@@ -74,14 +101,14 @@ void save() throws Exception {
@Test
@DisplayName("게시물을 전체 조회할 수 있다.")
void findAll() throws Exception {
- mockMvc.perform(get("/api/v1/posts")
+ mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/posts")
.param("page", "0")
.param("size", "10")
.contentType(MediaType.APPLICATION_JSON)
- .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims)))
+ .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)))
.andExpect(status().isOk())
.andDo(print())
- .andDo(document("post-findAll",
+ .andDo(restDocs.document(
responseFields(
fieldWithPath("[].title").type(JsonFieldType.STRING).description("title"),
fieldWithPath("[].content").type(JsonFieldType.STRING).description("content"),
@@ -91,19 +118,31 @@ void findAll() throws Exception {
fieldWithPath("[].user.nickName").type(JsonFieldType.STRING).description("nickName"),
fieldWithPath("[].user.introduce").type(JsonFieldType.STRING).description("introduce"),
fieldWithPath("[].user.prologName").type(JsonFieldType.STRING).description("prologName"),
+ fieldWithPath("[].user.profileImgUrl").type(JsonFieldType.STRING).description("profileImgUrl"),
+ fieldWithPath("[].tags").type(JsonFieldType.ARRAY).description("tags"),
fieldWithPath("[].comment").type(JsonFieldType.ARRAY).description("comment"),
- fieldWithPath("[].commentCount").type(JsonFieldType.NUMBER).description("commentCount")
+ fieldWithPath("[].commentCount").type(JsonFieldType.NUMBER).description("commentCount"),
+ fieldWithPath("[].seriesResponse").type(JsonFieldType.OBJECT).description("series"),
+ fieldWithPath("[].seriesResponse.title").type(JsonFieldType.STRING).description("seriesTitle"),
+ fieldWithPath("[].seriesResponse.posts").type(JsonFieldType.ARRAY).description("postInSeries"),
+ fieldWithPath("[].seriesResponse.posts.[].id").type(JsonFieldType.NUMBER)
+ .description("postIdInSeries"),
+ fieldWithPath("[].seriesResponse.posts.[].title").type(JsonFieldType.STRING)
+ .description("postTitleInSeries"),
+ fieldWithPath("[].seriesResponse.count").type(JsonFieldType.NUMBER).description("seriesCount"),
+ fieldWithPath("[].likeCount").type(JsonFieldType.NUMBER).description("likeCount")
+
)));
}
@Test
@DisplayName("게시물 아이디로 게시물을 단건 조회할 수 있다.")
void findById() throws Exception {
- mockMvc.perform(get("/api/v1/posts/{id}", postId)
+ mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/posts/{id}", postId)
.contentType(MediaType.APPLICATION_JSON)
- .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims)))
+ .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)))
.andExpect(status().isOk())
- .andDo(document("post-findById",
+ .andDo(restDocs.document(
responseFields(
fieldWithPath("title").type(JsonFieldType.STRING).description("title"),
fieldWithPath("content").type(JsonFieldType.STRING).description("content"),
@@ -113,25 +152,38 @@ void findById() throws Exception {
fieldWithPath("user.nickName").type(JsonFieldType.STRING).description("nickName"),
fieldWithPath("user.introduce").type(JsonFieldType.STRING).description("introduce"),
fieldWithPath("user.prologName").type(JsonFieldType.STRING).description("prologName"),
+ fieldWithPath("user.profileImgUrl").type(JsonFieldType.STRING).description("profileImgUrl"),
+ fieldWithPath("tags").type(JsonFieldType.ARRAY).description("tags"),
fieldWithPath("comment").type(JsonFieldType.ARRAY).description("comment"),
- fieldWithPath("commentCount").type(JsonFieldType.NUMBER).description("commentCount")
+ fieldWithPath("commentCount").type(JsonFieldType.NUMBER).description("commentCount"),
+ fieldWithPath("seriesResponse").type(JsonFieldType.OBJECT).description("series"),
+ fieldWithPath("seriesResponse.title").type(JsonFieldType.STRING).description("seriesTitle"),
+ fieldWithPath("seriesResponse.posts").type(JsonFieldType.ARRAY).description("postInSeries"),
+ fieldWithPath("seriesResponse.posts.[].id").type(JsonFieldType.NUMBER)
+ .description("postIdInSeries"),
+ fieldWithPath("seriesResponse.posts.[].title").type(JsonFieldType.STRING)
+ .description("postTitleInSeries"),
+ fieldWithPath("seriesResponse.count").type(JsonFieldType.NUMBER).description("seriesCount"),
+ fieldWithPath("likeCount").type(JsonFieldType.NUMBER).description("likeCount")
)));
}
@Test
@DisplayName("게시물 아이디로 게시물을 수정할 수 있다.")
void update() throws Exception {
- UpdateRequest request = new UpdateRequest("수정된 테스트 제목", "수정된 테스트 내용", true);
+ UpdateRequest update = new UpdateRequest(UPDATE_TITLE, UPDATE_CONTENT, "", false);
+ postService.update(update, userId, postId);
- mockMvc.perform(patch("/api/v1/posts/{id}", postId)
- .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims))
+ mockMvc.perform(RestDocumentationRequestBuilders.patch("/api/v1/posts/{id}", postId)
+ .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims))
.contentType(MediaType.APPLICATION_JSON)
- .content(objectMapper.writeValueAsString(request))
+ .content(objectMapper.writeValueAsString(update))
).andExpect(status().isOk())
- .andDo(document("post-update",
+ .andDo(restDocs.document(
requestFields(
fieldWithPath("title").type(JsonFieldType.STRING).description("title"),
fieldWithPath("content").type(JsonFieldType.STRING).description("content"),
+ fieldWithPath("tagText").type(JsonFieldType.STRING).description("tagText"),
fieldWithPath("openStatus").type(JsonFieldType.BOOLEAN).description("openStatus")
),
responseFields(
@@ -143,8 +195,19 @@ void update() throws Exception {
fieldWithPath("user.nickName").type(JsonFieldType.STRING).description("nickName"),
fieldWithPath("user.introduce").type(JsonFieldType.STRING).description("introduce"),
fieldWithPath("user.prologName").type(JsonFieldType.STRING).description("prologName"),
+ fieldWithPath("user.profileImgUrl").type(JsonFieldType.STRING).description("profileImgUrl"),
+ fieldWithPath("tags").type(JsonFieldType.ARRAY).description("tags"),
fieldWithPath("comment").type(JsonFieldType.ARRAY).description("comment"),
- fieldWithPath("commentCount").type(JsonFieldType.NUMBER).description("commentCount")
+ fieldWithPath("commentCount").type(JsonFieldType.NUMBER).description("commentCount"),
+ fieldWithPath("seriesResponse").type(JsonFieldType.OBJECT).description("series"),
+ fieldWithPath("seriesResponse.title").type(JsonFieldType.STRING).description("seriesTitle"),
+ fieldWithPath("seriesResponse.posts").type(JsonFieldType.ARRAY).description("postInSeries"),
+ fieldWithPath("seriesResponse.posts.[].id").type(JsonFieldType.NUMBER)
+ .description("postIdInSeries"),
+ fieldWithPath("seriesResponse.posts.[].title").type(JsonFieldType.STRING)
+ .description("postTitleInSeries"),
+ fieldWithPath("seriesResponse.count").type(JsonFieldType.NUMBER).description("seriesCount"),
+ fieldWithPath("likeCount").type(JsonFieldType.NUMBER).description("likeCount")
)
));
}
@@ -152,8 +215,8 @@ void update() throws Exception {
@Test
@DisplayName("게시물 아이디로 게시물을 삭제할 수 있다.")
void remove() throws Exception {
- mockMvc.perform(delete("/api/v1/posts/{id}", postId)
- .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims))
+ mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/posts/{id}", postId)
+ .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims))
.contentType(MediaType.APPLICATION_JSON)
).andExpect(status().isNoContent())
.andDo(document("post-delete"));
@@ -161,13 +224,13 @@ void remove() throws Exception {
@Test
@DisplayName("게시물 작성 중 제목이 공백인 경우 에러가 발생해야한다.")
- void isValidateTitleNull() throws Exception {
- CreateRequest createRequest = new CreateRequest("", "테스트 게시물 내용", true);
+ void validateTitleNull() throws Exception {
+ CreateRequest createRequest = new CreateRequest("", "테스트 게시물 내용", "#tag", true, SERIES_TITLE);
String requestJsonString = objectMapper.writeValueAsString(createRequest);
- mockMvc.perform(post("/api/v1/posts")
- .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims))
+ mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts")
+ .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims))
.contentType(MediaType.APPLICATION_JSON)
.content(requestJsonString))
.andExpect(status().isBadRequest());
@@ -175,13 +238,12 @@ void isValidateTitleNull() throws Exception {
@Test
@DisplayName("게시물 작성 중 내용이 빈칸인 경우 에러가 발생해야한다.")
- void isValidateContentEmpty() throws Exception {
- CreateRequest createRequest = new CreateRequest("테스트 게시물 제목", " ", true);
-
+ void validateContentEmpty() throws Exception {
+ CreateRequest createRequest = new CreateRequest("테스트 게시물 제목", " ", "#tag", true, SERIES_TITLE);
String requestJsonString = objectMapper.writeValueAsString(createRequest);
- mockMvc.perform(post("/api/v1/posts")
- .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims))
+ mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts")
+ .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims))
.contentType(MediaType.APPLICATION_JSON)
.content(requestJsonString))
.andExpect(status().isBadRequest());
@@ -189,18 +251,35 @@ void isValidateContentEmpty() throws Exception {
@Test
@DisplayName("게시물 작성 중 게시물 제목이 50이상인 경우 에러가 발생해야한다.")
- void isValidateTitleSizeOver() throws Exception {
+ void validateTitleSizeOver() throws Exception {
CreateRequest createRequest = new CreateRequest(
"안녕하세요. 여기는 프로그래머스 기술 블로그 prolog입니다. 이곳에 글을 작성하기 위해서는 제목은 50글자 미만이어야합니다.",
- "null 게시물 내용",
- true);
+ "null 게시물 내용", "#tag",
+ true, SERIES_TITLE);
String requestJsonString = objectMapper.writeValueAsString(createRequest);
- mockMvc.perform(post("/api/v1/posts")
- .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims))
+ mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts")
+ .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims))
.contentType(MediaType.APPLICATION_JSON)
.content(requestJsonString))
.andExpect(status().isBadRequest());
}
+
+ @Test
+ @DisplayName("포스트 생성시 시리즈도 만들어진다.")
+ void createSeries() throws Exception {
+ CreateRequest createRequest = new CreateRequest(
+ "안녕하세요. 여기는 프로그래머스 기술 블로그 prolog입니다",
+ "null 게시물 내용", "#tag",
+ true, "테스트 중");
+
+ String requestJsonString = objectMapper.writeValueAsString(createRequest);
+
+ mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts")
+ .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims))
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(requestJsonString))
+ .andExpect(status().isCreated());
+ }
}
\ No newline at end of file
diff --git a/src/test/java/com/prgrms/prolog/domain/post/model/PostTest.java b/src/test/java/com/prgrms/prolog/domain/post/model/PostTest.java
index 5711e63..bc11656 100644
--- a/src/test/java/com/prgrms/prolog/domain/post/model/PostTest.java
+++ b/src/test/java/com/prgrms/prolog/domain/post/model/PostTest.java
@@ -54,7 +54,7 @@ void updatePostTest() {
@DisplayName("게시글을 생성하기 위해서는 사용자가 필요하다.")
void createFailByUserNullTest() {
//given & when & then
- assertThatThrownBy(() -> new Post(title, content, true, null))
+ assertThatThrownBy(() -> new Post(title, content, true, null,null))
.isInstanceOf(NullPointerException.class);
}
@@ -62,7 +62,7 @@ void createFailByUserNullTest() {
@DisplayName("게시글 제목은 50자를 넘을 수 없다.")
void validateTitleTest() {
//given & when & then
- assertThatThrownBy(() -> new Post(OVER_SIZE_50, content, true, USER))
+ assertThatThrownBy(() -> new Post(OVER_SIZE_50, content, true, USER, null))
.isInstanceOf(IllegalArgumentException.class);
}
@@ -71,7 +71,7 @@ void validateTitleTest() {
@DisplayName("게시글 제목은 빈 값,null일 수 없다.")
void validateTitleTest2(String inputTitle) {
//given & when & then
- assertThatThrownBy(() -> new Post(inputTitle, content, true, USER))
+ assertThatThrownBy(() -> new Post(inputTitle, content, true, USER, null))
.isInstanceOf(IllegalArgumentException.class);
}
@@ -79,7 +79,7 @@ void validateTitleTest2(String inputTitle) {
@DisplayName("게시글 내용은 65535자를 넘을 수 없다.")
void validateContentTest() {
//given & when & then
- assertThatThrownBy(() -> new Post(title, OVER_SIZE_65535, true, USER))
+ assertThatThrownBy(() -> new Post(title, OVER_SIZE_65535, true, USER, null))
.isInstanceOf(IllegalArgumentException.class);
}
@@ -88,7 +88,7 @@ void validateContentTest() {
@DisplayName("게시글 내용은 빈 값,null일 수 없다.")
void validateContentTest2(String inputContent) {
//given & when & then
- assertThatThrownBy(() -> new Post(title, inputContent, true, USER))
+ assertThatThrownBy(() -> new Post(title, inputContent, true, USER, null))
.isInstanceOf(IllegalArgumentException.class);
}
diff --git a/src/test/java/com/prgrms/prolog/domain/post/repository/PostRepositoryTest.java b/src/test/java/com/prgrms/prolog/domain/post/repository/PostRepositoryTest.java
index 52599a8..3e308c8 100644
--- a/src/test/java/com/prgrms/prolog/domain/post/repository/PostRepositoryTest.java
+++ b/src/test/java/com/prgrms/prolog/domain/post/repository/PostRepositoryTest.java
@@ -18,7 +18,6 @@
import com.prgrms.prolog.domain.post.model.Post;
import com.prgrms.prolog.domain.user.model.User;
import com.prgrms.prolog.domain.user.repository.UserRepository;
-import com.prgrms.prolog.global.config.DatabaseConfig;
import com.prgrms.prolog.global.config.JpaConfig;
@DataJpaTest
@@ -39,22 +38,21 @@ class PostRepositoryTest {
@BeforeEach
void setUp() {
user = userRepository.save(USER);
- post = Post.builder()
- .title("테스트 제목")
- .content("테스트 내용")
+ Post p = Post.builder()
+ .title(TITLE)
+ .content(CONTENT)
.openStatus(true)
.user(user)
.build();
-
- postRepository.save(post);
+ post = postRepository.save(p);
}
@Test
@DisplayName("게시물을 등록할 수 있다.")
void save() {
Post newPost = Post.builder()
- .title("새로운 테스트 제목")
- .content("새로운 테스트 내용")
+ .title("새로 저장한 제목")
+ .content("새로 저장한 내용")
.openStatus(false)
.user(user)
.build();
@@ -79,7 +77,7 @@ void findById() {
void findAll() {
List all = postRepository.findAll();
- assertThat(all).hasSize(1);
+ assertThat(all).isNotEmpty();
}
@Test
diff --git a/src/test/java/com/prgrms/prolog/domain/post/service/PostServiceTest.java b/src/test/java/com/prgrms/prolog/domain/post/service/PostServiceTest.java
index 1cd05ed..b991639 100644
--- a/src/test/java/com/prgrms/prolog/domain/post/service/PostServiceTest.java
+++ b/src/test/java/com/prgrms/prolog/domain/post/service/PostServiceTest.java
@@ -1,9 +1,12 @@
package com.prgrms.prolog.domain.post.service;
+import static com.prgrms.prolog.domain.series.dto.SeriesResponse.*;
import static com.prgrms.prolog.utils.TestUtils.*;
import static org.assertj.core.api.Assertions.*;
+import java.util.List;
import java.util.Optional;
+import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
@@ -11,7 +14,6 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.context.annotation.Import;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.transaction.annotation.Transactional;
@@ -21,18 +23,18 @@
import com.prgrms.prolog.domain.post.dto.PostResponse;
import com.prgrms.prolog.domain.post.model.Post;
import com.prgrms.prolog.domain.post.repository.PostRepository;
+import com.prgrms.prolog.domain.series.model.Series;
+import com.prgrms.prolog.domain.series.repository.SeriesRepository;
import com.prgrms.prolog.domain.user.model.User;
import com.prgrms.prolog.domain.user.repository.UserRepository;
-import com.prgrms.prolog.global.config.DatabaseConfig;
-@Import(DatabaseConfig.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@SpringBootTest
@Transactional
class PostServiceTest {
@Autowired
- private PostService postService;
+ PostService postService;
@Autowired
UserRepository userRepository;
@@ -40,38 +42,109 @@ class PostServiceTest {
@Autowired
PostRepository postRepository;
+ @Autowired
+ SeriesRepository seriesRepository;
+
User user;
Post post;
+ Series savedSeries;
@BeforeEach
void setData() {
user = userRepository.save(USER);
+ Series series = Series.builder().title(SERIES_TITLE).user(user).build();
+ savedSeries = seriesRepository.save(series);
post = Post.builder()
.title("테스트 게시물")
.content("테스트 내용")
.openStatus(true)
.user(user)
.build();
+ post.setSeries(savedSeries);
postRepository.save(post);
}
@Test
@DisplayName("게시물을 등록할 수 있다.")
void save_success() {
- CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", true);
- Long savePostId = postService.save(postRequest, USER_EMAIL);
+ final CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", "#테스트", true, SERIES_TITLE);
+ Long savePostId = postService.save(postRequest, user.getId());
assertThat(savePostId).isNotNull();
}
+ @Test
+ @DisplayName("게시글에 태그 없이 등록할 수 있다.")
+ void savePostAndWithOutAnyTagTest() {
+ // given
+ final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", null, true, SERIES_TITLE);
+
+ // when
+ Long savedPostId = postService.save(request, user.getId());
+ PostResponse findPostResponse = postService.findById(savedPostId);
+ Set findTags = findPostResponse.tags();
+
+ // then
+ assertThat(findTags).isEmpty();
+ }
+
+ @Test
+ @DisplayName("게시글에 태그가 공백이거나 빈 칸이라면 태그는 무시된다.")
+ void savePostWithBlankTagTest() {
+ // given
+ final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", "# #", true, SERIES_TITLE);
+
+ // when
+ Long savedPostId = postService.save(request, user.getId());
+ PostResponse findPostResponse = postService.findById(savedPostId);
+ Set findTags = findPostResponse.tags();
+
+ // then
+ assertThat(findTags).isEmpty();
+ }
+
+ @Test
+ @DisplayName("게시글에 복수의 태그를 등록할 수 있다.")
+ void savePostAndTagsTest() {
+ // given
+ final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", "#테스트#test#test1#테 스트", true, SERIES_TITLE);
+ final List expectedTags = List.of("테스트", "test", "test1", "테 스트");
+
+ // when
+ Long savedPostId = postService.save(request, user.getId());
+ PostResponse findPostResponse = postService.findById(savedPostId);
+ Set findTags = findPostResponse.tags();
+
+ // then
+ assertThat(findTags)
+ .containsExactlyInAnyOrderElementsOf(expectedTags);
+ }
+
+ @Test
+ @DisplayName("게시물과 태그를 조회할 수 있다.")
+ void findPostAndTagsTest() {
+ // given
+ final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", "#테스트", true, SERIES_TITLE);
+
+ // when
+ Long savedPostId = postService.save(request, user.getId());
+ PostResponse findPost = postService.findById(savedPostId);
+
+ // then
+ assertThat(findPost)
+ .hasFieldOrPropertyWithValue("title", request.title())
+ .hasFieldOrPropertyWithValue("content", request.content())
+ .hasFieldOrPropertyWithValue("openStatus", request.openStatus())
+ .hasFieldOrPropertyWithValue("tags", Set.of("테스트"))
+ .hasFieldOrPropertyWithValue("seriesResponse", toSeriesResponse(savedSeries));
+ }
+
@Test
@DisplayName("존재하지 않는 사용자(비회원)의 이메일로 게시물을 등록할 수 없다.")
void save_fail() {
- String notExistEmail = "no_email@test.com";
+ CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", "#테스트", true, SERIES_TITLE);
- CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", true);
-
- assertThatThrownBy(() -> postService.save(postRequest, notExistEmail))
- .isInstanceOf(IllegalArgumentException.class);
+ assertThatThrownBy(() -> postService.save(postRequest, UNSAVED_USER_ID))
+ .isInstanceOf(NullPointerException.class);
}
@Test
@@ -93,22 +166,26 @@ void findById_fail() {
}
@Test
- @DisplayName("존재하는 게시물의 아이디로 게시물을 수정할 수 있다.")
+ @DisplayName("존재하는 게시물의 아이디로 게시물의 제목, 내용, 태그, 공개범위를 수정할 수 있다.")
void update_success() {
- UpdateRequest updateRequest = new UpdateRequest("수정된 테스트", "수정된 테스트 내용", true);
+ final CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", "#테스트#test#test1#테 스트", true,SERIES_TITLE);
+ Long savedPost = postService.save(createRequest, user.getId());
- PostResponse update = postService.update(post.getId(), updateRequest);
+ final UpdateRequest updateRequest = new UpdateRequest("수정된 테스트", "수정된 테스트 내용", "#테스트#수정된 태그", true);
+ PostResponse updatedPostResponse = postService.update(updateRequest, user.getId(), savedPost);
- assertThat(update.title()).isEqualTo("수정된 테스트");
- assertThat(update.content()).isEqualTo("수정된 테스트 내용");
+ assertThat(updatedPostResponse)
+ .hasFieldOrPropertyWithValue("title", updateRequest.title())
+ .hasFieldOrPropertyWithValue("content", updateRequest.content())
+ .hasFieldOrPropertyWithValue("tags", Set.of("테스트", "수정된 태그"));
}
@Test
@DisplayName("존재하지 않는 게시물의 아이디로 게시물을 수정할 수 없다.")
void update_fail() {
- UpdateRequest updateRequest = new UpdateRequest("수정된 테스트", "수정된 테스트 내용", true);
+ UpdateRequest updateRequest = new UpdateRequest("수정된 테스트", "수정된 테스트 내용", "", true);
- assertThatThrownBy(() -> postService.update(0L, updateRequest))
+ assertThatThrownBy(() -> postService.update(updateRequest, user.getId(), 0L))
.isInstanceOf(IllegalArgumentException.class);
}
@@ -119,7 +196,7 @@ void findAll_success() {
Page all = postService.findAll(page);
- assertThat(all).hasSize(1);
+ assertThat(all).isNotNull();
}
@Test
diff --git a/src/test/java/com/prgrms/prolog/domain/posttag/model/PostTagTest.java b/src/test/java/com/prgrms/prolog/domain/posttag/model/PostTagTest.java
new file mode 100644
index 0000000..0ec8d73
--- /dev/null
+++ b/src/test/java/com/prgrms/prolog/domain/posttag/model/PostTagTest.java
@@ -0,0 +1,39 @@
+package com.prgrms.prolog.domain.posttag.model;
+
+import static com.prgrms.prolog.utils.TestUtils.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class PostTagTest {
+
+ @Test
+ @DisplayName("게시글 태그 생성")
+ void createPostTagTest() {
+ // given
+ PostTag postTag = PostTag.builder()
+ .rootTag(ROOT_TAG)
+ .post(POST)
+ .build();
+ // when & then
+ assertThat(postTag)
+ .hasFieldOrPropertyWithValue("rootTag", ROOT_TAG)
+ .hasFieldOrPropertyWithValue("post", POST);
+ }
+
+ @Test
+ @DisplayName("게시글 태그에는 게시글과 루트 태그가 null일 수 없다.")
+ void validatePostTagNullTest() {
+ assertAll(
+ () -> assertThatThrownBy(() -> new PostTag(null, ROOT_TAG))
+ .isInstanceOf(IllegalArgumentException.class),
+ () -> assertThatThrownBy(() -> new PostTag(POST, null))
+ .isInstanceOf(IllegalArgumentException.class),
+ () -> assertThatThrownBy(() -> new PostTag(null, null))
+ .isInstanceOf(IllegalArgumentException.class)
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/prgrms/prolog/domain/roottag/model/RootTagTest.java b/src/test/java/com/prgrms/prolog/domain/roottag/model/RootTagTest.java
new file mode 100644
index 0000000..e67f6db
--- /dev/null
+++ b/src/test/java/com/prgrms/prolog/domain/roottag/model/RootTagTest.java
@@ -0,0 +1,45 @@
+package com.prgrms.prolog.domain.roottag.model;
+
+import static com.prgrms.prolog.utils.TestUtils.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+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;
+
+class RootTagTest {
+
+ @Test
+ @DisplayName("태그 생성")
+ void createRootTagTest() {
+ // given
+ RootTag rootTag = RootTag.builder()
+ .name(ROOT_TAG_NAME)
+ .build();
+ // when & then
+ assertThat(rootTag).hasFieldOrPropertyWithValue("name", ROOT_TAG_NAME);
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @DisplayName("태그 이름은 null, 빈 값일 수 없다.")
+ void validateRootTagNameTextTest(String name) {
+ // given & when & then
+ assertAll(
+ () -> assertThatThrownBy(() -> RootTag.builder().name(name).build())
+ .isInstanceOf(IllegalArgumentException.class),
+ () -> assertThatThrownBy(() -> new RootTag(name))
+ .isInstanceOf(IllegalArgumentException.class)
+ );
+ }
+
+ @Test
+ @DisplayName("태그 이름은 100글자 이내여야 한다.")
+ void validateRootTagNameLengthTest() {
+ // given & when & then
+ assertThatThrownBy(() -> new RootTag(OVER_SIZE_100))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/prgrms/prolog/domain/roottag/repository/RootTagRepositoryTest.java b/src/test/java/com/prgrms/prolog/domain/roottag/repository/RootTagRepositoryTest.java
new file mode 100644
index 0000000..6fee4a5
--- /dev/null
+++ b/src/test/java/com/prgrms/prolog/domain/roottag/repository/RootTagRepositoryTest.java
@@ -0,0 +1,42 @@
+package com.prgrms.prolog.domain.roottag.repository;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.*;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+
+import com.prgrms.prolog.domain.roottag.model.RootTag;
+
+@DataJpaTest
+@AutoConfigureTestDatabase(replace = NONE)
+class RootTagRepositoryTest {
+
+ @Autowired
+ RootTagRepository rootTagRepository;
+
+ @Test
+ @DisplayName("태그 이름들로 루트 태그들을 검색한다.")
+ void findByTagNamesInTest() {
+ // given
+ final Set tagNames = Set.of("태그1", "태그2", "태그3", "태그4", "태그5");
+ final Set tags = Set.of(
+ new RootTag("태그1"),
+ new RootTag("태그2"),
+ new RootTag("태그3"),
+ new RootTag("태그4"),
+ new RootTag("태그5"));
+ rootTagRepository.saveAll(tags);
+
+ // when
+ Set findTags = rootTagRepository.findByTagNamesIn(tagNames);
+
+ // then
+ assertThat(findTags).hasSize(5);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/prgrms/prolog/domain/series/api/SeriesControllerTest.java b/src/test/java/com/prgrms/prolog/domain/series/api/SeriesControllerTest.java
new file mode 100644
index 0000000..4a646bd
--- /dev/null
+++ b/src/test/java/com/prgrms/prolog/domain/series/api/SeriesControllerTest.java
@@ -0,0 +1,110 @@
+package com.prgrms.prolog.domain.series.api;
+
+import static com.prgrms.prolog.global.jwt.JwtTokenProvider.*;
+import static com.prgrms.prolog.utils.TestUtils.*;
+import static org.springframework.http.HttpHeaders.*;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
+import static org.springframework.restdocs.payload.JsonFieldType.*;
+import static org.springframework.restdocs.payload.PayloadDocumentation.*;
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.restdocs.RestDocumentationContextProvider;
+import org.springframework.restdocs.RestDocumentationExtension;
+import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.filter.CharacterEncodingFilter;
+
+import com.prgrms.prolog.config.RestDocsConfig;
+import com.prgrms.prolog.domain.post.model.Post;
+import com.prgrms.prolog.domain.post.repository.PostRepository;
+import com.prgrms.prolog.domain.series.model.Series;
+import com.prgrms.prolog.domain.series.repository.SeriesRepository;
+import com.prgrms.prolog.domain.user.model.User;
+import com.prgrms.prolog.domain.user.repository.UserRepository;
+import com.prgrms.prolog.global.jwt.JwtTokenProvider;
+
+@SpringBootTest
+@ExtendWith(RestDocumentationExtension.class)
+@Import(RestDocsConfig.class)
+@Transactional
+class SeriesControllerTest {
+
+ @Autowired
+ private JwtTokenProvider jwtTokenProvider;
+ @Autowired
+ private RestDocumentationResultHandler restDocs;
+ @Autowired
+ private UserRepository userRepository;
+ @Autowired
+ private PostRepository postRepository;
+ @Autowired
+ private SeriesRepository seriesRepository;
+
+ private MockMvc mockMvc;
+ private User savedUser;
+ private Post savedPost;
+ private Series savedSeries;
+
+ @BeforeEach
+ void setUp(WebApplicationContext context, RestDocumentationContextProvider provider) {
+ this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
+ .addFilter(new CharacterEncodingFilter("UTF-8", true))
+ .apply(documentationConfiguration(provider))
+ .apply(springSecurity())
+ .alwaysDo(restDocs)
+ .build();
+ savedUser = userRepository.save(USER);
+ Post post = Post.builder()
+ .title(POST_TITLE)
+ .content(POST_CONTENT)
+ .openStatus(true)
+ .user(savedUser)
+ .build();
+ savedPost = postRepository.save(post);
+ Series series = Series.builder()
+ .title(SERIES_TITLE)
+ .user(savedUser)
+ .post(savedPost)
+ .build();
+ savedSeries = seriesRepository.save(series);
+ }
+
+ @Test
+ @DisplayName("자신이 가진 시리즈 중에서 제목으로 게시글 정보를 조회할 수 있다.")
+ void findSeriesByTitleTest() throws Exception {
+ // given
+ Claims claims = Claims.from(savedUser.getId(), USER_ROLE);
+ // when
+ mockMvc.perform(get("/api/v1/series")
+ .header(AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims))
+ .param("title", SERIES_TITLE)
+ )
+ // then
+ .andExpectAll(
+ handler().methodName("findSeriesByTitle"),
+ status().isOk())
+ // docs
+ .andDo(restDocs.document(
+ responseFields(
+ fieldWithPath("title").type(STRING).description("시리즈 제목"),
+ fieldWithPath("posts").type(ARRAY).description("게시글 목록"),
+ fieldWithPath("posts.[].id").type(NUMBER).description("게시글 아이디"),
+ fieldWithPath("posts.[].title").type(STRING).description("게시글 제목"),
+ fieldWithPath("count").type(NUMBER).description("게시물 개수")
+ ))
+ );
+
+ }
+}
diff --git a/src/test/java/com/prgrms/prolog/domain/series/model/SeriesTest.java b/src/test/java/com/prgrms/prolog/domain/series/model/SeriesTest.java
new file mode 100644
index 0000000..fd64325
--- /dev/null
+++ b/src/test/java/com/prgrms/prolog/domain/series/model/SeriesTest.java
@@ -0,0 +1,68 @@
+package com.prgrms.prolog.domain.series.model;
+
+import static com.prgrms.prolog.utils.TestUtils.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.List;
+
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import com.prgrms.prolog.utils.TestUtils;
+
+class SeriesTest {
+
+ @Test
+ @DisplayName("시리즈를 생성할 수 있다.")
+ void createSuccessTest(){
+ // given & when & then
+ assertDoesNotThrow(
+ () -> Series.builder()
+ .title(TestUtils.SERIES_TITLE)
+ .user(USER)
+ .post(POST)
+ .build()
+ );
+ }
+
+ @Test
+ @DisplayName("시리즈와 연관된 엔티티를 조회할 수 있다.")
+ void readTest(){
+ // given & when & then
+ Series series = getSeries();
+ assertAll(
+ () -> assertThat(series.getTitle()).isEqualTo(TestUtils.SERIES_TITLE),
+ () -> assertThat(series.getUser()).isEqualTo(USER),
+ () -> assertThat(series.getPosts()).isEqualTo(List.of(POST))
+ );
+ }
+
+ @Test
+ @DisplayName("시리즈는 포스트 없이도 생성할 수 있다.")
+ void createSuccessDoesNotExistPostTest(){
+ // given & when & then
+ assertDoesNotThrow(
+ () -> Series.builder()
+ .title(TestUtils.SERIES_TITLE)
+ .user(USER)
+ .build()
+ );
+ }
+
+ @Test
+ @DisplayName("시리즈는 유저 없이 생성할 수 없다.")
+ void createFailTest(){
+ // given & when & then
+ assertThatThrownBy(
+ () -> Series.builder()
+ .title(TestUtils.SERIES_TITLE)
+ .post(POST)
+ .build()
+ )
+ .isInstanceOf(NullPointerException.class)
+ .hasMessageContaining("user");
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/prgrms/prolog/domain/series/repository/SeriesRepositoryTest.java b/src/test/java/com/prgrms/prolog/domain/series/repository/SeriesRepositoryTest.java
new file mode 100644
index 0000000..30df3bd
--- /dev/null
+++ b/src/test/java/com/prgrms/prolog/domain/series/repository/SeriesRepositoryTest.java
@@ -0,0 +1,65 @@
+package com.prgrms.prolog.domain.series.repository;
+
+import static com.prgrms.prolog.utils.TestUtils.*;
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.context.annotation.Import;
+
+import com.prgrms.prolog.domain.series.model.Series;
+import com.prgrms.prolog.domain.user.model.User;
+import com.prgrms.prolog.domain.user.repository.UserRepository;
+import com.prgrms.prolog.global.config.JpaConfig;
+
+
+@DataJpaTest
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+@Import({JpaConfig.class})
+public class SeriesRepositoryTest {
+
+ @Autowired
+ private SeriesRepository seriesRepository;
+
+ @Autowired
+ private UserRepository userRepository;
+
+ private Series savedSeries;
+ private User savedUser;
+
+ @BeforeEach
+ void setUp() {
+ savedUser = userRepository.save(USER);
+ Series series = Series.builder()
+ .title(SERIES_TITLE)
+ .user(savedUser)
+ .build();
+ savedSeries = seriesRepository.save(series);
+ }
+
+ @Test
+ @DisplayName("해당 유저가 가진 시리즈 중에서 찾는 제목의 시리즈를 조회한다.")
+ void findByIdAndTitleTest() {
+ // given & when
+ Optional series = seriesRepository.findByIdAndTitle(savedUser.getId(), SERIES_TITLE);
+ // then
+ assertThat(series).isPresent();
+ }
+
+ @Disabled
+ @Test
+ @DisplayName("포스트 조회시 N+1 테스트")
+ void nPlus1Test() {
+ // given & when
+ Optional series = seriesRepository.findByIdAndTitle(savedUser.getId(), SERIES_TITLE);
+ // then
+ assertThat(series).isPresent();
+ }
+}
diff --git a/src/test/java/com/prgrms/prolog/domain/series/service/SeriesServiceImplTest.java b/src/test/java/com/prgrms/prolog/domain/series/service/SeriesServiceImplTest.java
new file mode 100644
index 0000000..e85fdf9
--- /dev/null
+++ b/src/test/java/com/prgrms/prolog/domain/series/service/SeriesServiceImplTest.java
@@ -0,0 +1,106 @@
+package com.prgrms.prolog.domain.series.service;
+
+import static com.prgrms.prolog.utils.TestUtils.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.BDDMockito.*;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import com.prgrms.prolog.domain.post.dto.PostInfo;
+import com.prgrms.prolog.domain.post.model.Post;
+import com.prgrms.prolog.domain.series.dto.CreateSeriesRequest;
+import com.prgrms.prolog.domain.series.dto.SeriesResponse;
+import com.prgrms.prolog.domain.series.model.Series;
+import com.prgrms.prolog.domain.series.repository.SeriesRepository;
+import com.prgrms.prolog.domain.user.repository.UserRepository;
+import com.prgrms.prolog.global.common.IdResponse;
+
+@ExtendWith(MockitoExtension.class)
+class SeriesServiceImplTest {
+
+ @Mock
+ private SeriesRepository seriesRepository;
+
+ @Mock
+ private UserRepository userRepository;
+
+ @Mock
+ private Series series;
+
+ @Mock
+ private Post post;
+
+ @InjectMocks
+ private SeriesServiceImpl seriesService;
+
+ @Test
+ @DisplayName("시리즈를 저장하기 위해서는 등록된 유저 정보가 필요하다.")
+ void saveSuccessTest() {
+ // given
+ CreateSeriesRequest createSeriesRequest
+ = new CreateSeriesRequest(SERIES_TITLE);
+ given(seriesRepository.save(any(Series.class))).willReturn(series);
+ given(userRepository.findById(USER_ID)).willReturn(Optional.of(USER));
+ given(series.getId()).willReturn(1L);
+ // when
+ IdResponse response = seriesService.create(createSeriesRequest, USER_ID);
+ // then
+ assertThat(response.id()).isEqualTo(1L);
+ then(seriesRepository).should().save(any(Series.class));
+ then(userRepository).should().findById(USER_ID);
+ }
+
+ @Test
+ @DisplayName("등록된 유저가 없는 경우 시리즈를 만들때 예외가 발생한다.")
+ void saveFailTest() {
+ // given
+ CreateSeriesRequest createSeriesRequest
+ = new CreateSeriesRequest(SERIES_TITLE);
+ given(userRepository.findById(USER_ID)).willReturn(Optional.empty());
+ // when & then
+ assertThatThrownBy(() -> seriesService.create(createSeriesRequest, USER_ID))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("user");
+ }
+
+ @Test
+ @DisplayName("등록된 유저가 없는 경우 시리즈를 만들때 예외가 발생한다.")
+ void findByTitleSuccessTest() {
+ // given
+ given(seriesRepository.findByIdAndTitle(any(Long.class),any(String.class)))
+ .willReturn(Optional.of(series));
+ given(series.getTitle()).willReturn(SERIES_TITLE);
+ given(series.getPosts()).willReturn((List.of(post)));
+ given(post.getId()).willReturn(1L);
+ given(post.getTitle()).willReturn(POST_TITLE);
+ // when
+ SeriesResponse seriesResponse = seriesService.findByTitle(USER_ID, SERIES_TITLE);
+ // then
+ then(seriesRepository).should().findByIdAndTitle(any(Long.class),any(String.class));
+ assertThat(seriesResponse)
+ .hasFieldOrPropertyWithValue("title", SERIES_TITLE)
+ .hasFieldOrPropertyWithValue("posts", List.of(new PostInfo(1L,POST_TITLE)))
+ .hasFieldOrPropertyWithValue("count",1);
+ assertThat(seriesResponse.posts()).isNotEmpty();
+ }
+
+ @Test
+ @DisplayName("찾는 시리즈가 없으면 예외가 발생한다.")
+ void findByTitleFailTest() {
+ // given
+ given(seriesRepository.findByIdAndTitle(any(Long.class),any(String.class)))
+ .willReturn(Optional.empty());
+ // when & then
+ assertThatThrownBy(() -> seriesService.findByTitle(USER_ID, SERIES_TITLE))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("notExists");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/prgrms/prolog/domain/user/Repository/UserRepositoryTest.java b/src/test/java/com/prgrms/prolog/domain/user/Repository/UserRepositoryTest.java
index 69b4eb0..e91ec8e 100644
--- a/src/test/java/com/prgrms/prolog/domain/user/Repository/UserRepositoryTest.java
+++ b/src/test/java/com/prgrms/prolog/domain/user/Repository/UserRepositoryTest.java
@@ -2,75 +2,68 @@
import static com.prgrms.prolog.utils.TestUtils.*;
import static org.assertj.core.api.Assertions.*;
-import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.*;
import java.util.Optional;
+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;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
+import org.springframework.transaction.annotation.Transactional;
import com.prgrms.prolog.domain.user.model.User;
import com.prgrms.prolog.domain.user.repository.UserRepository;
-import com.prgrms.prolog.global.config.DatabaseConfig;
import com.prgrms.prolog.global.config.JpaConfig;
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
@Import({JpaConfig.class})
+@Transactional
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
- @Test
- @DisplayName("정상적으로 DB에 저장이 된다.")
- void saveTest() {
- // given & when & then
- assertDoesNotThrow(() -> userRepository.save(getUser()));
+ private User savedUser;
+
+ @BeforeEach
+ void setUp() {
+ savedUser = userRepository.save(getUser());
}
@Test
@DisplayName("저장된 유저 정보를 유저ID로 찾아 가져올 수 있다.")
void saveAndFindByIdTest() {
- // given
- User savedUser = userRepository.save(getUser());
- // when
+ // given & when
Optional foundUser = userRepository.findById(savedUser.getId());
// then
assertThat(foundUser).isPresent();
assertThat(foundUser.get())
.usingRecursiveComparison()
.isEqualTo(savedUser);
-
}
@Test
- @DisplayName("이메일로 저장된 유저 정보를 조회할 수 있다.")
- void findEmailTest() {
+ @DisplayName("저장되지 않은 유저는 조회할 수 없다.")
+ void findFailTest() {
// given
- User savedUser = userRepository.save(getUser());
+ Long unsavedUserId = 0L;
// when
- Optional foundUser = userRepository.findByEmail(savedUser.getEmail());
+ Optional foundUser = userRepository.findById(unsavedUserId);
// then
- assertThat(foundUser).isPresent();
- assertThat(foundUser.get())
- .usingRecursiveComparison()
- .isEqualTo(savedUser);
+ assertThat(foundUser).isNotPresent();
}
@Test
- @DisplayName("저장되지 않은 유저는 조회할 수 없다.")
- void findFailTest() {
- // given
- User notSavedUser = getUser();
- // when
- Optional foundUser = userRepository.findByEmail(notSavedUser.getEmail());
+ @DisplayName("유저와 유저 태그를 조인하여 조회할 수 있다.")
+ void joinUserTagFindByEmailTest() {
+ // given & when
+ User findUser = userRepository.joinUserTagFindByUserId(savedUser.getId());
// then
- assertThat(foundUser).isNotPresent();
+ assertThat(findUser).isNotNull();
}
}
diff --git a/src/test/java/com/prgrms/prolog/domain/user/api/UserControllerTest.java b/src/test/java/com/prgrms/prolog/domain/user/api/UserControllerTest.java
index 779bde8..2701c00 100644
--- a/src/test/java/com/prgrms/prolog/domain/user/api/UserControllerTest.java
+++ b/src/test/java/com/prgrms/prolog/domain/user/api/UserControllerTest.java
@@ -1,9 +1,7 @@
package com.prgrms.prolog.domain.user.api;
-import static com.prgrms.prolog.domain.user.dto.UserDto.*;
import static com.prgrms.prolog.global.jwt.JwtTokenProvider.*;
import static com.prgrms.prolog.utils.TestUtils.*;
-import static org.mockito.BDDMockito.*;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*;
import static org.springframework.restdocs.payload.JsonFieldType.*;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
@@ -16,35 +14,36 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
+import org.springframework.http.HttpHeaders;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import com.prgrms.prolog.config.RestDocsConfig;
-
+import com.prgrms.prolog.domain.user.model.User;
import com.prgrms.prolog.domain.user.repository.UserRepository;
import com.prgrms.prolog.domain.user.service.UserServiceImpl;
-import com.prgrms.prolog.global.config.JpaConfig;
import com.prgrms.prolog.global.jwt.JwtTokenProvider;
@SpringBootTest
@ExtendWith(RestDocumentationExtension.class)
-@Import({RestDocsConfig.class, JpaConfig.class})
+@Import(RestDocsConfig.class)
+@Transactional
class UserControllerTest {
- private static final JwtTokenProvider jwtTokenProvider = JWT_TOKEN_PROVIDER;
protected MockMvc mockMvc;
-
@Autowired
- RestDocumentationResultHandler restDocs;
- @MockBean
+ private JwtTokenProvider jwtTokenProvider;
+ @Autowired
+ private RestDocumentationResultHandler restDocs;
+ @Autowired
private UserServiceImpl userService;
@Autowired
private UserRepository userRepository;
@@ -63,26 +62,25 @@ void setUp(WebApplicationContext context, RestDocumentationContextProvider provi
@DisplayName("사용자는 자신의 프로필 정보를 확인할 수 있다")
void userPage() throws Exception {
// given
- UserInfo userInfo = getUserInfo();
- userRepository.save(USER);
- Claims claims = Claims.from(USER_EMAIL, USER_ROLE);
- given(userService.findByEmail(USER_EMAIL)).willReturn(userInfo);
+ User savedUser = userRepository.save(USER);
+ Claims claims = Claims.from(savedUser.getId(), USER_ROLE);
// when
- mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/users/me")
- .header("token", jwtTokenProvider.createAccessToken(claims))
- // .header(HttpHeaders.AUTHORIZATION, "token" + jwtTokenProvider.createAccessToken(claims))
+ mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/user/me")
+ .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims))
)
// then
.andExpectAll(
- handler().methodName("myPage"),
+ handler().methodName("getMyProfile"),
status().isOk())
// docs
.andDo(restDocs.document(
responseFields(
+ fieldWithPath("id").type(NUMBER).description("ID"),
fieldWithPath("email").type(STRING).description("이메일"),
fieldWithPath("nickName").type(STRING).description("닉네임"),
fieldWithPath("introduce").type(STRING).description("한줄 소개"),
- fieldWithPath("prologName").type(STRING).description("블로그 제목")
+ fieldWithPath("prologName").type(STRING).description("블로그 제목"),
+ fieldWithPath("profileImgUrl").type(STRING).description("프로필 이미지")
))
);
}
diff --git a/src/test/java/com/prgrms/prolog/domain/user/service/UserServiceTest.java b/src/test/java/com/prgrms/prolog/domain/user/service/UserServiceTest.java
index 0e62d61..0fc9877 100644
--- a/src/test/java/com/prgrms/prolog/domain/user/service/UserServiceTest.java
+++ b/src/test/java/com/prgrms/prolog/domain/user/service/UserServiceTest.java
@@ -12,9 +12,10 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
+import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
-import com.prgrms.prolog.domain.user.dto.UserDto.UserInfo;
+import com.prgrms.prolog.domain.user.dto.UserDto.IdResponse;
import com.prgrms.prolog.domain.user.dto.UserDto.UserProfile;
import com.prgrms.prolog.domain.user.model.User;
import com.prgrms.prolog.domain.user.repository.UserRepository;
@@ -25,81 +26,80 @@ class UserServiceTest {
@Mock
private UserRepository userRepository;
+ @Mock
+ private User userMock;
+
@InjectMocks
- private UserServiceImpl userService; // 빈으로 등록해서 주입 받고 싶으면 어떻게 해야하나요? 구현체말고 인터페이스를 주입 받고 싶습니다!
+ private UserServiceImpl userService;
@Nested
@DisplayName("사용자 조회 #10")
class SignUpAndLogin {
@Test
- @DisplayName("이메일로 사용자 정보를 조회할 수 있다")
+ @DisplayName("userId를 통해서 사용자 정보를 조회할 수 있다")
void findByEmailTest() {
- // given
- User user = getUser();
- given(userRepository.findByEmail(USER_EMAIL)).willReturn(Optional.of(user));
- // when
- UserInfo foundUser = userService.findByEmail(USER_EMAIL);
- // then
- then(userRepository).should().findByEmail(USER_EMAIL);
- assertThat(foundUser)
- .hasFieldOrPropertyWithValue("email", user.getEmail())
- .hasFieldOrPropertyWithValue("nickName", user.getNickName())
- .hasFieldOrPropertyWithValue("introduce", user.getIntroduce())
- .hasFieldOrPropertyWithValue("prologName", user.getPrologName());
+ try (MockedStatic userProfile = mockStatic(UserProfile.class)) {
+
+ //given
+ given(userRepository.findById(USER_ID)).willReturn(Optional.of(USER));
+ given(UserProfile.toUserProfile(USER)).willReturn(USER_PROFILE);
+
+ // when
+ UserProfile foundUser = userService.findUserProfileByUserId(USER_ID);
+
+ // then
+ then(userRepository).should().findById(USER_ID);
+ assertThat(foundUser)
+ .hasFieldOrPropertyWithValue("email", USER_EMAIL)
+ .hasFieldOrPropertyWithValue("nickName", USER_NICK_NAME)
+ .hasFieldOrPropertyWithValue("introduce", USER_INTRODUCE)
+ .hasFieldOrPropertyWithValue("prologName", USER_PROLOG_NAME)
+ .hasFieldOrPropertyWithValue("profileImgUrl", USER_PROFILE_IMG_URL);
+ }
}
- @DisplayName("이메일 정보에 일치하는 사용자가 없으면 NotFoundException")
+ @DisplayName("userId에 해당하는 사용자가 없으면 IllegalArgumentException")
@Test
void notFoundMatchUser() {
- given(userRepository.findByEmail(any(String.class))).willReturn(Optional.empty());
- String unsavedEmail = "unsaved@test.com";
- assertThatThrownBy(() -> userService.findByEmail(unsavedEmail))
+ //given
+ Long unsavedUserId = 100L;
+ given(userRepository.findById(any(Long.class))).willReturn(Optional.empty());
+ //when & then
+ assertThatThrownBy(() -> userService.findUserProfileByUserId(unsavedUserId))
.isInstanceOf(IllegalArgumentException.class);
}
}
@Nested
- @DisplayName("회원가입 및 로그인 #9")
- class FindUserInfo {
+ @DisplayName("회원가입 #9")
+ class FindUserProfile {
@Test
- @DisplayName("등록된 사용자라면 로그인할 수 있다.")
- void loginTest() {
+ @DisplayName("등록된 사용자는 회원 가입 절차 없이 등록된 사용자 ID를 반환 받을 수 있다.")
+ void signUpTest() {
// given
- User user = getUser();
- UserProfile savedUserProfile = getUserProfile();
- given(userRepository.findByEmail(any(String.class))).willReturn(Optional.of(user));
+ given(userRepository.findByProviderAndOauthId(PROVIDER,OAUTH_ID))
+ .willReturn(Optional.of(userMock));
+ given(userMock.getId()).willReturn(USER_ID);
// when
- UserInfo foundUserInfo = userService.login(savedUserProfile);
+ IdResponse userId = userService.signUp(USER_INFO);
// then
- then(userRepository).should().findByEmail(savedUserProfile.email());
- assertThat(foundUserInfo)
- .hasFieldOrPropertyWithValue("email", user.getEmail())
- .hasFieldOrPropertyWithValue("email", savedUserProfile.email())
- .hasFieldOrPropertyWithValue("nickName", user.getNickName())
- .hasFieldOrPropertyWithValue("nickName", savedUserProfile.nickName())
- .hasFieldOrPropertyWithValue("introduce", user.getIntroduce())
- .hasFieldOrPropertyWithValue("prologName", user.getPrologName());
+ then(userRepository).should().findByProviderAndOauthId(PROVIDER,OAUTH_ID);
}
@Test
- @DisplayName("등록되지 않은 사용자가 로그인하는 경우 자동으로 회원가입이 진행된다.")
+ @DisplayName("등록되지 않은 사용자는 자동으로 회원가입이 진행된다.")
void defaultSignUpTest() {
// given
- User user = getUser();
- UserProfile unsavedUserProfile = getUserProfile();
- given(userRepository.findByEmail(any(String.class))).willReturn(Optional.empty());
- given(userRepository.save(any(User.class))).willReturn(user);
+ given(userRepository.findByProviderAndOauthId(PROVIDER, OAUTH_ID))
+ .willReturn(Optional.empty());
+ given(userRepository.save(any(User.class))).willReturn(userMock);
+ given(userMock.getId()).willReturn(USER_ID);
// when
- UserInfo foundUserInfo = userService.login(unsavedUserProfile);
+ IdResponse userId = userService.signUp(USER_INFO);
// then
- then(userRepository).should().findByEmail(unsavedUserProfile.email());
+ then(userRepository).should().findByProviderAndOauthId(PROVIDER, OAUTH_ID);
then(userRepository).should().save(any(User.class));
- assertThat(foundUserInfo)
- .hasFieldOrPropertyWithValue("email", user.getEmail())
- .hasFieldOrPropertyWithValue("nickName", user.getNickName())
- .hasFieldOrPropertyWithValue("introduce", user.getIntroduce())
- .hasFieldOrPropertyWithValue("prologName", user.getPrologName());
}
}
}
diff --git a/src/test/java/com/prgrms/prolog/domain/usertag/model/UserTagTest.java b/src/test/java/com/prgrms/prolog/domain/usertag/model/UserTagTest.java
new file mode 100644
index 0000000..ea06aa7
--- /dev/null
+++ b/src/test/java/com/prgrms/prolog/domain/usertag/model/UserTagTest.java
@@ -0,0 +1,39 @@
+package com.prgrms.prolog.domain.usertag.model;
+
+import static com.prgrms.prolog.utils.TestUtils.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class UserTagTest {
+
+ @Test
+ @DisplayName("유저 태그 생성 성공")
+ void createUserTagTest() {
+ // given
+ UserTag userTag = UserTag.builder()
+ .user(USER)
+ .rootTag(ROOT_TAG)
+ .count(1)
+ .build();
+ // when & then
+ assertThat(userTag)
+ .hasFieldOrPropertyWithValue("user", USER)
+ .hasFieldOrPropertyWithValue("rootTag", ROOT_TAG);
+ }
+
+ @Test
+ @DisplayName("유저 태그에는 유저와 루트 태그가 null일 수 없다.")
+ void validateUserTagNulLTest() {
+ assertAll(
+ () -> assertThatThrownBy(() -> new UserTag(USER, null))
+ .isInstanceOf(IllegalArgumentException.class),
+ () -> assertThatThrownBy(() -> new UserTag(null, ROOT_TAG))
+ .isInstanceOf(IllegalArgumentException.class),
+ () -> assertThatThrownBy(() -> new UserTag(null, null))
+ .isInstanceOf(IllegalArgumentException.class)
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/prgrms/prolog/global/jwt/JwtAuthenticationTest.java b/src/test/java/com/prgrms/prolog/global/jwt/JwtAuthenticationTest.java
index 7def1dc..6566c3c 100644
--- a/src/test/java/com/prgrms/prolog/global/jwt/JwtAuthenticationTest.java
+++ b/src/test/java/com/prgrms/prolog/global/jwt/JwtAuthenticationTest.java
@@ -7,6 +7,8 @@
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.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
class JwtAuthenticationTest {
@@ -17,11 +19,11 @@ class JwtAuthenticationTest {
void token() {
// given
JwtAuthentication jwtAuthentication
- = new JwtAuthentication(token, USER_EMAIL);
+ = new JwtAuthentication(token, USER_ID);
// when & then
assertThat(jwtAuthentication)
.hasFieldOrPropertyWithValue("token", token)
- .hasFieldOrPropertyWithValue("userEmail", USER_EMAIL);
+ .hasFieldOrPropertyWithValue("id", USER_ID);
}
@ParameterizedTest
@@ -29,18 +31,19 @@ void token() {
@DisplayName("token은 null, 빈 값일 수 없다.")
void validateTokenTest(String inputToken) {
//given & when & then
- assertThatThrownBy(() -> new JwtAuthentication(inputToken, USER_EMAIL))
+ assertThatThrownBy(() -> new JwtAuthentication(inputToken, USER_ID))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("토큰");
}
@ParameterizedTest
- @NullAndEmptySource
- @DisplayName("email은 null, 빈 값일 수 없다.")
- void validateUserEmailTest(String inputUserEmail) {
+ @NullSource
+ @ValueSource(longs = {0L, -1L, -100L})
+ @DisplayName("userId는 null, 0 이하일 수 없다.")
+ void validateUserEmailTest(Long inputUserId) {
//given & when & then
- assertThatThrownBy(() -> new JwtAuthentication(token, inputUserEmail))
+ assertThatThrownBy(() -> new JwtAuthentication(token, inputUserId))
.isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("이메일");
+ .hasMessageContaining("ID");
}
}
\ No newline at end of file
diff --git a/src/test/java/com/prgrms/prolog/global/jwt/JwtTokenProviderTest.java b/src/test/java/com/prgrms/prolog/global/jwt/JwtTokenProviderTest.java
index a729869..28328e4 100644
--- a/src/test/java/com/prgrms/prolog/global/jwt/JwtTokenProviderTest.java
+++ b/src/test/java/com/prgrms/prolog/global/jwt/JwtTokenProviderTest.java
@@ -13,18 +13,23 @@
class JwtTokenProviderTest {
- private static final JwtTokenProvider jwtTokenProvider = JWT_TOKEN_PROVIDER;
+ private static final String ISSUER = "issuer";
+ private static final String SECRET_KEY = "secretKey";
+ private static final int EXPIRY_SECONDS = 2;
+
+ private static final JwtTokenProvider jwtTokenProvider
+ = new JwtTokenProvider(ISSUER,SECRET_KEY,EXPIRY_SECONDS);
@Test
@DisplayName("토큰 생성 및 추출")
void createTokenAndVerifyToken() {
// given
- String token = jwtTokenProvider.createAccessToken(Claims.from(USER_EMAIL, USER_ROLE)); // TODO: 추후에 리팩터링 고려
+ String token = jwtTokenProvider.createAccessToken(Claims.from(USER_ID, USER_ROLE));
// when
Claims claims = jwtTokenProvider.getClaims(token);
// then
assertAll(
- () -> assertThat(claims.getEmail()).isEqualTo(USER_EMAIL),
+ () -> assertThat(claims.getUserId()).isEqualTo(USER_ID),
() -> assertThat(claims.getRole()).isEqualTo(USER_ROLE)
);
}
@@ -33,9 +38,9 @@ void createTokenAndVerifyToken() {
@DisplayName("유효 시간이 지난 토큰을 사용하면 예외가 발생한다.")
void validateToken_OverTime() throws InterruptedException {
// given
- String token = jwtTokenProvider.createAccessToken(Claims.from(USER_EMAIL, USER_ROLE));
+ String token = jwtTokenProvider.createAccessToken(Claims.from(USER_ID, USER_ROLE));
// when
- sleep(EXPIRY_SECONDS * 2000);
+ sleep(EXPIRY_SECONDS * 2000L);
// then
assertThatThrownBy(() -> jwtTokenProvider.getClaims(token))
.isInstanceOf(JWTVerificationException.class);
@@ -43,9 +48,9 @@ void validateToken_OverTime() throws InterruptedException {
@Test
@DisplayName("유효하지 않은 토큰을 사용하면 예외가 발생한다.")
- void validateToken_Invalid() {
+ void validateTokenByInvalid() {
// given
- String token = "Invalid";
+ String token = "invalid";
// when & then
assertThatThrownBy(() -> jwtTokenProvider.getClaims(token))
.isInstanceOf(JWTVerificationException.class);
@@ -53,15 +58,13 @@ void validateToken_Invalid() {
@Test
@DisplayName("올바르지 않은 시그니처로 검증 시 예외를 발생한다.")
- void validateToken_WrongSign() {
+ void validateTokenByWrongSign() {
// given
- JwtTokenProvider wongTokenProvider = new JwtTokenProvider(
- ISSUER,
- "S-Team",
- 0
- );
+ String invalidSecretKey = "S-Team";
+ JwtTokenProvider wongTokenProvider
+ = new JwtTokenProvider(ISSUER, invalidSecretKey, EXPIRY_SECONDS);
//when
- String token = wongTokenProvider.createAccessToken(Claims.from(USER_EMAIL, USER_ROLE));
+ String token = wongTokenProvider.createAccessToken(Claims.from(USER_ID, USER_ROLE));
//then
assertThatThrownBy(() -> jwtTokenProvider.getClaims(token))
.isInstanceOf(JWTVerificationException.class);
diff --git a/src/test/java/com/prgrms/prolog/utils/TestUtils.java b/src/test/java/com/prgrms/prolog/utils/TestUtils.java
index 98e1044..c4ce30e 100644
--- a/src/test/java/com/prgrms/prolog/utils/TestUtils.java
+++ b/src/test/java/com/prgrms/prolog/utils/TestUtils.java
@@ -1,40 +1,68 @@
package com.prgrms.prolog.utils;
import com.prgrms.prolog.domain.comment.model.Comment;
+import com.prgrms.prolog.domain.like.model.Like;
import com.prgrms.prolog.domain.post.model.Post;
+import com.prgrms.prolog.domain.posttag.model.PostTag;
+import com.prgrms.prolog.domain.roottag.model.RootTag;
+import com.prgrms.prolog.domain.series.model.Series;
import com.prgrms.prolog.domain.user.dto.UserDto.UserInfo;
import com.prgrms.prolog.domain.user.dto.UserDto.UserProfile;
import com.prgrms.prolog.domain.user.model.User;
-import com.prgrms.prolog.global.jwt.JwtTokenProvider;
public class TestUtils {
// User Data
- public static final String USER_EMAIL = "Dev@programmers.com";
+ public static final Long USER_ID = 1L;
+ public static final User USER = getUser();
+ public static final Long UNSAVED_USER_ID = 0L;
+ public static final String USER_EMAIL = "dev@programmers.com";
public static final String USER_NICK_NAME = "머쓱이";
public static final String USER_INTRODUCE = "머쓱이에욤";
public static final String USER_PROLOG_NAME = "머쓱이의 prolog";
public static final String PROVIDER = "kakao";
public static final String OAUTH_ID = "kakao@123456789";
- public static final User USER = getUser();
+ public static final String USER_PROFILE_IMG_URL = "http://kakao/defaultImg.jpg";
+ public static final UserInfo USER_INFO = getUserInfo();
+ public static final UserProfile USER_PROFILE = getUserProfile();
+ public static final String USER_ROLE = "ROLE_USER";
+
+ // Post
+ public static final Long POST_ID = 1L;
public static final Post POST = getPost();
- public static final Comment COMMENT = getComment();
- // Post & Comment Data
public static final String TITLE = "제목을 입력해주세요";
public static final String CONTENT = "내용을 입력해주세요";
- public static final String USER_ROLE = "ROLE_USER";
+ public static final String POST_TITLE = "게시글 제목";
+ public static final String POST_CONTENT = "게시글 내용";
+ public static final String UPDATE_TITLE = "수정할 제목을 입력해주세요";
+ public static final String UPDATE_CONTENT = "수정할 내용을 입력해주세요";
+
+ // Comment
+ public static final Comment COMMENT = getComment();
+ public static final String COMMENT_CONTENT = "댓글 내용";
+
+ // Series
+ public static final Series SERIES = getSeries();
+ public static final String SERIES_TITLE = "시리즈 제목";
+
+ // Like
+ public static final Long LIKE_ID = 1L;
+ public static final Like LIKE = getLike();
+
+ // RootTag & PostTag Data
+ public static final String ROOT_TAG_NAME = "머쓱 태그";
+ public static final Integer POST_TAG_COUNT = 0;
+ public static final RootTag ROOT_TAG = getRootTag();
+ public static final PostTag POST_TAG = getPostTag();
+
// Over Size String Dummy
public static final String OVER_SIZE_50 = "0" + "1234567890".repeat(5);
public static final String OVER_SIZE_100 = "0" + "1234567890".repeat(10);
public static final String OVER_SIZE_255 = "012345" + "1234567890".repeat(25);
public static final String OVER_SIZE_65535 = "012345" + "1234567890".repeat(6553);
- // JWT
- public static final String ISSUER = "prgrms";
- public static final String SECRET_KEY = "prgrmsbackenddevrteamprologkwonj";
- public static final int EXPIRY_SECONDS = 2;
- public static final JwtTokenProvider JWT_TOKEN_PROVIDER
- = new JwtTokenProvider(ISSUER, SECRET_KEY, EXPIRY_SECONDS);
+ // Authentication
+ public static final String BEARER_TYPE = "Bearer ";
private TestUtils() {
/* no-op */
@@ -48,13 +76,14 @@ public static User getUser() {
.prologName(USER_PROLOG_NAME)
.provider(PROVIDER)
.oauthId(OAUTH_ID)
+ .profileImgUrl(USER_PROFILE_IMG_URL)
.build();
}
public static Post getPost() {
return Post.builder()
- .title("제목")
- .content("내용")
+ .title(POST_TITLE)
+ .content(POST_CONTENT)
.openStatus(true)
.user(USER)
.build();
@@ -62,23 +91,58 @@ public static Post getPost() {
public static Comment getComment() {
return Comment.builder()
- .content("내용")
+ .content(COMMENT_CONTENT)
.post(POST)
.user(USER)
.build();
}
+ public static UserInfo getUserInfo() {
+ return UserInfo.builder()
+ .email(USER_EMAIL)
+ .nickName(USER_NICK_NAME)
+ .provider(PROVIDER)
+ .oauthId(OAUTH_ID)
+ .profileImgUrl(USER_PROFILE_IMG_URL)
+ .build();
+ }
+
public static UserProfile getUserProfile() {
- return new UserProfile(
- USER_EMAIL,
- USER_NICK_NAME,
- PROVIDER,
- OAUTH_ID
- );
+ return UserProfile.builder()
+ .id(USER_ID)
+ .email(USER_EMAIL)
+ .nickName(USER_NICK_NAME)
+ .prologName(USER_PROLOG_NAME)
+ .introduce(USER_INTRODUCE)
+ .profileImgUrl(USER_PROFILE_IMG_URL)
+ .build();
}
- public static UserInfo getUserInfo() {
- return new UserInfo(USER);
+ public static RootTag getRootTag() {
+ return RootTag.builder()
+ .name(ROOT_TAG_NAME)
+ .build();
+ }
+
+ public static PostTag getPostTag() {
+ return PostTag.builder()
+ .rootTag(ROOT_TAG)
+ .post(POST)
+ .build();
+ }
+
+ public static Series getSeries() {
+ return Series.builder()
+ .title(SERIES_TITLE)
+ .user(USER)
+ .post(POST)
+ .build();
}
+ public static Like getLike() {
+ return Like.builder()
+ .user(USER)
+ .post(POST)
+ .build();
+ }
}
diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml
new file mode 100644
index 0000000..ec675f4
--- /dev/null
+++ b/src/test/resources/application.yml
@@ -0,0 +1,17 @@
+spring:
+ config:
+ import: optional:file:.env[.properties]
+ profiles:
+ include:
+ - security
+ - aws
+
+ flyway:
+ enabled: false
+
+ datasource:
+ driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
+ url: jdbc:tc:mysql:8.0.31:///test?TC_INITSCRIPT=schema.sql
+
+jwt:
+ expiry-seconds: 2
\ No newline at end of file
diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000..ca6ee9c
--- /dev/null
+++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
\ No newline at end of file
diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql
new file mode 100644
index 0000000..05c7014
--- /dev/null
+++ b/src/test/resources/schema.sql
@@ -0,0 +1,123 @@
+-- init.sql
+# create database if not exists prolog;
+# use prolog;
+DROP TABLE IF EXISTS likes;
+DROP TABLE IF EXISTS user_tag;
+DROP TABLE IF EXISTS post_tag;
+DROP TABLE IF EXISTS root_tag;
+DROP TABLE IF EXISTS social_account;
+DROP TABLE IF EXISTS comment;
+DROP TABLE IF EXISTS post;
+DROP TABLE IF EXISTS series;
+DROP TABLE IF EXISTS users;
+
+
+CREATE TABLE users
+(
+ id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ email varchar(100) NOT NULL UNIQUE,
+ profile_img_url varchar(255) NULL,
+ nick_name varchar(100) NULL UNIQUE,
+ introduce varchar(100) NULL,
+ prolog_name varchar(100) NOT NULL UNIQUE,
+ provider varchar(100) NOT NULL,
+ oauth_id varchar(100) NOT NULL,
+ created_by varchar(100) NULL,
+ created_at datetime NOT NULL DEFAULT now(),
+ updated_at datetime NOT NULL DEFAULT now(),
+ deleted_at datetime
+);
+
+CREATE TABLE series
+(
+ id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ title varchar(200) NOT NULL,
+ created_at datetime NOT NULL DEFAULT now(),
+ updated_at datetime NOT NULL DEFAULT now(),
+ deleted_at datetime,
+ user_id bigint NOT NULL,
+ FOREIGN KEY fk_series_user_id (user_id) REFERENCES users (id)
+);
+
+CREATE TABLE post
+(
+ id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ title varchar(200) NOT NULL,
+ content text NOT NULL,
+ open_status tinyint(1) NOT NULL DEFAULT 0,
+ created_by varchar(100) NULL,
+ created_at datetime NOT NULL DEFAULT now(),
+ updated_at datetime NOT NULL DEFAULT now(),
+ deleted_at datetime,
+ user_id bigint NOT NULL,
+ series_id bigint NULL,
+ FOREIGN KEY fk_post_user_id (user_id) REFERENCES users (id),
+ FOREIGN KEY fk_post_series_id (series_id) REFERENCES series (id)
+);
+
+CREATE TABLE social_account
+(
+ id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ email varchar(100),
+ facebook_id varchar(100),
+ github_id varchar(100),
+ twitter_id varchar(100),
+ blog_url varchar(100),
+ created_by varchar(100) NULL,
+ created_at datetime NOT NULL DEFAULT now(),
+ updated_at datetime NOT NULL DEFAULT now(),
+ deleted_at datetime,
+ user_id bigint NOT NULL,
+ FOREIGN KEY fk_social_account_user_id (user_id) REFERENCES users (id)
+);
+
+CREATE TABLE comment
+(
+ id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ content varchar(255) NOT NULL,
+ created_by varchar(100) NULL,
+ created_at datetime NOT NULL DEFAULT now(),
+ updated_at datetime NOT NULL DEFAULT now(),
+ deleted_at datetime,
+ post_id bigint NOT NULL,
+ user_id bigint NOT NULL,
+ FOREIGN KEY fk_comment_post_id (post_id) REFERENCES post (id),
+ FOREIGN KEY fk_comment_user_id (user_id) REFERENCES users (id)
+);
+
+CREATE TABLE root_tag
+(
+ id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ name varchar(100) NOT NULL UNIQUE
+);
+
+CREATE TABLE post_tag
+(
+ id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ post_id bigint NOT NULL,
+ root_tag_id bigint NOT NULL,
+ FOREIGN KEY fk_post_tag_post_id (post_id) REFERENCES post (id),
+ FOREIGN KEY fk_post_tag_root_tag_id (root_tag_id) REFERENCES root_tag (id)
+);
+
+CREATE TABLE user_tag
+(
+ id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ count int NOT NULL default 0,
+ user_id bigint NOT NULL,
+ root_tag_id bigint NOT NULL,
+ FOREIGN KEY fk_user_tag_user_id (user_id) REFERENCES users (id),
+ FOREIGN KEY fk_user_tag_root_tag_id (root_tag_id) REFERENCES root_tag (id)
+);
+
+CREATE TABLE likes
+(
+ id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ user_id bigint NOT NULL,
+ post_id bigint NOT NULL,
+ FOREIGN KEY fk_likes_user_id (user_id) REFERENCES users (id),
+ FOREIGN KEY fk_likes_post_id (post_id) REFERENCES post (id)
+);
+
+ALTER TABLE post
+ ADD like_count INT DEFAULT 0;
\ No newline at end of file