From 4c93a60087998df24be9623c33db1ed53aa9c204 Mon Sep 17 00:00:00 2001 From: Shiwon Park Date: Sun, 25 Aug 2024 21:21:05 +0900 Subject: [PATCH] feat: post --- .../configurations/SecurityConfiguration.kt | 2 + .../controllers/BoardController.kt | 3 +- .../controllers/PostController.kt | 74 +++++++++++++++ .../controllers/TagController.kt | 2 +- .../gistory/newbies_server_24/dto/PostDto.kt | 31 +++++++ .../newbies_server_24/entities/Post.kt | 19 +++- .../exceptions/PostNotFoundException.kt | 7 ++ .../exceptions/TagNotFoundException.kt | 7 ++ .../repositories/PostRepository.kt | 9 +- .../newbies_server_24/services/PostService.kt | 93 +++++++++++++++++++ 10 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/me/gistory/newbies_server_24/controllers/PostController.kt create mode 100644 src/main/kotlin/me/gistory/newbies_server_24/dto/PostDto.kt create mode 100644 src/main/kotlin/me/gistory/newbies_server_24/exceptions/PostNotFoundException.kt create mode 100644 src/main/kotlin/me/gistory/newbies_server_24/exceptions/TagNotFoundException.kt create mode 100644 src/main/kotlin/me/gistory/newbies_server_24/services/PostService.kt diff --git a/src/main/kotlin/me/gistory/newbies_server_24/configurations/SecurityConfiguration.kt b/src/main/kotlin/me/gistory/newbies_server_24/configurations/SecurityConfiguration.kt index 7a1adb6..15094fa 100644 --- a/src/main/kotlin/me/gistory/newbies_server_24/configurations/SecurityConfiguration.kt +++ b/src/main/kotlin/me/gistory/newbies_server_24/configurations/SecurityConfiguration.kt @@ -3,6 +3,7 @@ package me.gistory.newbies_server_24.configurations import me.gistory.newbies_server_24.providers.TokenProvider import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpMethod import org.springframework.security.config.Customizer import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity @@ -31,6 +32,7 @@ class SecurityConfiguration { session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } .authorizeHttpRequests { req -> + req.requestMatchers(HttpMethod.GET).permitAll() req.requestMatchers("/swagger-ui/**" , "v3/api-docs/**").permitAll() req.requestMatchers("/auth/login", "/auth/register").permitAll() req.requestMatchers("/error").permitAll() diff --git a/src/main/kotlin/me/gistory/newbies_server_24/controllers/BoardController.kt b/src/main/kotlin/me/gistory/newbies_server_24/controllers/BoardController.kt index 86f1a72..f9e9a19 100644 --- a/src/main/kotlin/me/gistory/newbies_server_24/controllers/BoardController.kt +++ b/src/main/kotlin/me/gistory/newbies_server_24/controllers/BoardController.kt @@ -13,7 +13,6 @@ import java.util.* @Tag(name="Board") @RestController -@SecurityRequirement(name = "Bearer Authorization") @RequestMapping("/boards") class BoardController(private val boardService: BoardService) { @@ -26,6 +25,7 @@ class BoardController(private val boardService: BoardService) { ) } + @SecurityRequirement(name = "Bearer Authorization") @PostMapping("") fun createBoard( authentication: Authentication, @@ -34,6 +34,7 @@ class BoardController(private val boardService: BoardService) { return boardService.createBoard(body, authentication.name).toSummaryDto(); } + @SecurityRequirement(name = "Bearer Authorization") @DeleteMapping("{uuid}") fun deleteBoard( authentication: Authentication, diff --git a/src/main/kotlin/me/gistory/newbies_server_24/controllers/PostController.kt b/src/main/kotlin/me/gistory/newbies_server_24/controllers/PostController.kt new file mode 100644 index 0000000..447feb3 --- /dev/null +++ b/src/main/kotlin/me/gistory/newbies_server_24/controllers/PostController.kt @@ -0,0 +1,74 @@ +package me.gistory.newbies_server_24.controllers + +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.tags.Tag +import me.gistory.newbies_server_24.dto.CreatePostDto +import me.gistory.newbies_server_24.dto.PostDto +import me.gistory.newbies_server_24.dto.PostListDto +import me.gistory.newbies_server_24.dto.UpdatePostDto +import me.gistory.newbies_server_24.entities.Post +import me.gistory.newbies_server_24.services.PostService +import org.springframework.security.core.Authentication +import org.springframework.web.bind.annotation.* +import java.util.* + +@Tag(name = "Post") +@RestController +@RequestMapping("/posts") +class PostController ( + private val postService: PostService +) { + + @GetMapping("") + fun getPosts( + @RequestParam(value = "tag", required = false) tag: String?, + @RequestParam(value = "boardUuid", required = false) boardUuid: String?, + ): PostListDto { + val boardId = boardUuid?.let { UUID.fromString(it) } + val posts = postService.getPosts(boardId = boardId, tagId = tag).toList() + return PostListDto( + count = posts.size, + list = posts.map(Post::toPostDto) + ) + } + + @GetMapping("/search") + fun searchPosts( + @RequestParam(value = "keyword") keyword: String + ): PostListDto { + val posts = postService.searchPost(keyword).toList() + return PostListDto( + count = posts.size, + list = posts.map(Post::toPostDto) + ) + } + + @SecurityRequirement(name = "Bearer Authorization") + @PostMapping("") + fun createPost( + @RequestBody post: CreatePostDto, + @RequestParam(value = "boardUuid") boardUuid: String, + authentication: Authentication + ): PostDto { + return postService.createPost(post, UUID.fromString(boardUuid), authentication.name).toPostDto() + } + + @SecurityRequirement(name = "Bearer Authorization") + @PatchMapping("/{uuid}") + fun updatePost( + @RequestBody post: UpdatePostDto, + @PathVariable("uuid") uuid: String, + authentication: Authentication + ): PostDto { + return postService.updatePost(post, UUID.fromString(uuid), authentication.name).toPostDto() + } + + @SecurityRequirement(name = "Bearer Authorization") + @DeleteMapping("/{uuid}") + fun deletePost( + @PathVariable("uuid") uuid: String, + authentication: Authentication + ): Unit { + return postService.deletePost(UUID.fromString(uuid), authentication.name) + } +} \ No newline at end of file diff --git a/src/main/kotlin/me/gistory/newbies_server_24/controllers/TagController.kt b/src/main/kotlin/me/gistory/newbies_server_24/controllers/TagController.kt index daf0e91..d966768 100644 --- a/src/main/kotlin/me/gistory/newbies_server_24/controllers/TagController.kt +++ b/src/main/kotlin/me/gistory/newbies_server_24/controllers/TagController.kt @@ -9,7 +9,6 @@ import org.springframework.web.bind.annotation.* @Tag(name = "Tag") @RestController -@SecurityRequirement(name = "Bearer Authorization") @RequestMapping("/tag") class TagController ( private val tagService: TagService, @@ -34,6 +33,7 @@ class TagController ( ) } + @SecurityRequirement(name = "Bearer Authorization") @PostMapping("") fun createTag(@RequestBody tag: TagDto): TagDto { return tagService.createTag(tag.key).toTagDto() diff --git a/src/main/kotlin/me/gistory/newbies_server_24/dto/PostDto.kt b/src/main/kotlin/me/gistory/newbies_server_24/dto/PostDto.kt new file mode 100644 index 0000000..94ee0c3 --- /dev/null +++ b/src/main/kotlin/me/gistory/newbies_server_24/dto/PostDto.kt @@ -0,0 +1,31 @@ +package me.gistory.newbies_server_24.dto + +import me.gistory.newbies_server_24.entities.Board +import java.util.* + +data class CreatePostDto( + val title: String, + val body: String, + val tags: List +) + +data class UpdatePostDto( + val title: String?, + val body: String?, + val tags: List?, +) + +data class PostDto ( + val id: UUID, + val title: String, + val body: String, + val tags: List, + val board: BoardSummaryDto, + val createdAt: Date, + val createdBy: UserDto +) + +data class PostListDto ( + val count: Int, + val list: List, +) diff --git a/src/main/kotlin/me/gistory/newbies_server_24/entities/Post.kt b/src/main/kotlin/me/gistory/newbies_server_24/entities/Post.kt index cf15e45..542cf32 100644 --- a/src/main/kotlin/me/gistory/newbies_server_24/entities/Post.kt +++ b/src/main/kotlin/me/gistory/newbies_server_24/entities/Post.kt @@ -1,14 +1,15 @@ package me.gistory.newbies_server_24.entities import jakarta.persistence.* +import me.gistory.newbies_server_24.dto.PostDto import org.hibernate.annotations.CreationTimestamp import java.util.* @Entity @Table(name = "posts") class Post( - @Column() val title: String, - @Column() val body: String, + @Column() var title: String, + @Column() var body: String, @ManyToOne @JoinColumn(name = "created_by") @@ -16,7 +17,7 @@ class Post( @ManyToMany @JoinTable(name = "post_to_tag") - val tags: MutableSet, + var tags: MutableSet, @ManyToOne @JoinColumn(name = "board_id") @@ -29,4 +30,16 @@ class Post( @Column(nullable = false) @CreationTimestamp val createdAt: Date = Date() + + fun toPostDto(): PostDto { + return PostDto( + id = id, + title = title, + body = body, + tags = tags.toList().map { it.key }, + board = board.toSummaryDto(), + createdAt = createdAt, + createdBy = createdBy.toDto() + ) + } } \ No newline at end of file diff --git a/src/main/kotlin/me/gistory/newbies_server_24/exceptions/PostNotFoundException.kt b/src/main/kotlin/me/gistory/newbies_server_24/exceptions/PostNotFoundException.kt new file mode 100644 index 0000000..1a75fc0 --- /dev/null +++ b/src/main/kotlin/me/gistory/newbies_server_24/exceptions/PostNotFoundException.kt @@ -0,0 +1,7 @@ +package me.gistory.newbies_server_24.exceptions + +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus + +@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Post Not Found") +class PostNotFoundException : RuntimeException() \ No newline at end of file diff --git a/src/main/kotlin/me/gistory/newbies_server_24/exceptions/TagNotFoundException.kt b/src/main/kotlin/me/gistory/newbies_server_24/exceptions/TagNotFoundException.kt new file mode 100644 index 0000000..9266dcd --- /dev/null +++ b/src/main/kotlin/me/gistory/newbies_server_24/exceptions/TagNotFoundException.kt @@ -0,0 +1,7 @@ +package me.gistory.newbies_server_24.exceptions + +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus + +@ResponseStatus(HttpStatus.NOT_FOUND, reason = "Tag Not Found") +class TagNotFoundException : RuntimeException() \ No newline at end of file diff --git a/src/main/kotlin/me/gistory/newbies_server_24/repositories/PostRepository.kt b/src/main/kotlin/me/gistory/newbies_server_24/repositories/PostRepository.kt index e649307..4a09988 100644 --- a/src/main/kotlin/me/gistory/newbies_server_24/repositories/PostRepository.kt +++ b/src/main/kotlin/me/gistory/newbies_server_24/repositories/PostRepository.kt @@ -1,7 +1,14 @@ package me.gistory.newbies_server_24.repositories +import me.gistory.newbies_server_24.entities.Board import me.gistory.newbies_server_24.entities.Post +import me.gistory.newbies_server_24.entities.Tag import org.springframework.data.repository.CrudRepository import java.util.UUID -interface PostRepository : CrudRepository \ No newline at end of file +interface PostRepository : CrudRepository { + fun findByTags(tag: Tag): List + fun findByBoard(board: Board): List + fun findByTagsAndBoard(tag: Tag, board: Board): List + fun findByTitleContainingIgnoreCaseOrBodyContainingIgnoreCase(titleContainingIgnoreCase: String, bodyContainingIgnoreCase: String): List +} \ No newline at end of file diff --git a/src/main/kotlin/me/gistory/newbies_server_24/services/PostService.kt b/src/main/kotlin/me/gistory/newbies_server_24/services/PostService.kt new file mode 100644 index 0000000..b56cd48 --- /dev/null +++ b/src/main/kotlin/me/gistory/newbies_server_24/services/PostService.kt @@ -0,0 +1,93 @@ +package me.gistory.newbies_server_24.services + +import jakarta.transaction.Transactional +import me.gistory.newbies_server_24.dto.CreatePostDto +import me.gistory.newbies_server_24.dto.UpdatePostDto +import me.gistory.newbies_server_24.entities.Post +import me.gistory.newbies_server_24.exceptions.* +import me.gistory.newbies_server_24.repositories.AuthRepository +import me.gistory.newbies_server_24.repositories.BoardRepository +import me.gistory.newbies_server_24.repositories.PostRepository +import me.gistory.newbies_server_24.repositories.TagRepository +import org.slf4j.LoggerFactory +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +@Transactional +class PostService ( + private val postRepository: PostRepository, + private val tagRepository: TagRepository, + private val boardRepository: BoardRepository, + private val authRepository: AuthRepository, +) { + private val logger = LoggerFactory.getLogger(AuthService::class.java) + + fun getPosts(boardId: UUID?, tagId: String?): List { + if (boardId == null && tagId == null) { + return postRepository.findAll().toList() + } else if (boardId == null && tagId != null) { + val tag = tagRepository.findByIdOrNull(tagId) ?: return postRepository.findAll().toList() + return postRepository.findByTags(tag) + } else if (tagId == null && boardId != null) { + val board = boardRepository.findByIdOrNull(boardId) ?: return postRepository.findAll().toList() + return postRepository.findByBoard(board) + } else if (tagId != null && boardId != null) { + val tag = tagRepository.findByIdOrNull(tagId) ?: return postRepository.findAll().toList() + val board = boardRepository.findByIdOrNull(boardId) ?: return postRepository.findAll().toList() + return postRepository.findByTagsAndBoard(tag, board) + } + throw ForbiddenException() + } + + fun searchPost(keyword: String): List { + return postRepository.findByTitleContainingIgnoreCaseOrBodyContainingIgnoreCase(keyword, keyword) + } + + fun createPost(dto: CreatePostDto, boardId: UUID, userEmail: String): Post { + val board = boardRepository.findByIdOrNull(boardId) ?: throw BoardNotFoundException() + val user = authRepository.findByEmail(userEmail) ?: throw UserNotFoundException() + val tags = try { + tagRepository.findAllById(dto.tags).toMutableSet() + } catch(e: Error) { + throw TagNotFoundException() + } + return postRepository.save(Post( + title = dto.title, + body = dto.body, + tags = tags, + createdBy = user, + board = board, + )) + } + + fun updatePost(dto: UpdatePostDto, postId: UUID, userEmail: String): Post { + val post = postRepository.findByIdOrNull(postId) ?: throw PostNotFoundException() + val user = authRepository.findByEmail(userEmail) ?: throw UserNotFoundException() + if (post.createdBy.id != user.id) { + throw ForbiddenException() + } + post.title = dto.title ?: post.title + post.body = dto.body ?: post.body + if (dto.tags != null) { + val tags = try { + tagRepository.findAllById(dto.tags).toMutableSet() + } catch(e: Error) { + throw TagNotFoundException() + } + post.tags = tags + } + return postRepository.save(post) + } + + fun deletePost(postId: UUID, userEmail: String): Unit { + val user = authRepository.findByEmail(userEmail) ?: throw UserNotFoundException() + val post = postRepository.findByIdOrNull(postId) + ?: throw PostNotFoundException() + if (post.createdBy.id != user.id) { + throw ForbiddenException() + } + postRepository.delete(post) + } +} \ No newline at end of file