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 +### 프로젝트 구조 +(예정) -prolog 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