diff --git a/src/main/kotlin/com/example/daitssuapi/common/configuration/RequestLoggingFilter.kt b/src/main/kotlin/com/example/daitssuapi/common/configuration/RequestLoggingFilter.kt index bae38040..673202ae 100644 --- a/src/main/kotlin/com/example/daitssuapi/common/configuration/RequestLoggingFilter.kt +++ b/src/main/kotlin/com/example/daitssuapi/common/configuration/RequestLoggingFilter.kt @@ -84,4 +84,4 @@ class RequestLoggingFilter : OncePerRequestFilter() { } return jacksonObjectMapper().writeValueAsString(jsonMap) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/daitssuapi/common/configuration/SwaggerConfiguration.kt b/src/main/kotlin/com/example/daitssuapi/common/configuration/SwaggerConfiguration.kt index a9ed7bef..7fd5e8ef 100644 --- a/src/main/kotlin/com/example/daitssuapi/common/configuration/SwaggerConfiguration.kt +++ b/src/main/kotlin/com/example/daitssuapi/common/configuration/SwaggerConfiguration.kt @@ -126,4 +126,4 @@ class SwaggerConfiguration {   }'
""".trimIndent() -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/daitssuapi/common/enums/ErrorCode.kt b/src/main/kotlin/com/example/daitssuapi/common/enums/ErrorCode.kt index f27b825d..73ac2cf5 100644 --- a/src/main/kotlin/com/example/daitssuapi/common/enums/ErrorCode.kt +++ b/src/main/kotlin/com/example/daitssuapi/common/enums/ErrorCode.kt @@ -8,6 +8,8 @@ private const val SERVER_NUMBERING = 4000 enum class ErrorCode(val code: Int, val message: String) { USER_NOT_FOUND(MAIN_NUMBERING + 1, "유저를 찾을 수 없습니다"), DEPARTMENT_NOT_FOUND(MAIN_NUMBERING + 2, "학과를 찾을 수 없습니다"), + ARTICLE_NOT_FOUND(MAIN_NUMBERING + 3, "게시글을 찾을 수 없습니다."), + NICKNAME_REQUIRED(MAIN_NUMBERING + 4, "닉네임이 필요한 작업입니다."), COURSE_NOT_FOUND(COURSE_NUMBERING + 1, "과목을 찾을 수 없습니다."), diff --git a/src/main/kotlin/com/example/daitssuapi/domain/main/controller/ArticleController.kt b/src/main/kotlin/com/example/daitssuapi/domain/main/controller/ArticleController.kt new file mode 100644 index 00000000..31a87fc5 --- /dev/null +++ b/src/main/kotlin/com/example/daitssuapi/domain/main/controller/ArticleController.kt @@ -0,0 +1,49 @@ +package com.example.daitssuapi.domain.main.controller + +import com.example.daitssuapi.common.dto.Response +import com.example.daitssuapi.domain.main.dto.response.ArticleResponse +import com.example.daitssuapi.domain.main.dto.request.ArticleWriteRequest +import com.example.daitssuapi.domain.main.service.ArticleService +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/daitssu/community/article") +@Tag(name = "article", description = "커뮤니티 게시글 API") +class ArticleController( + private val articleService: ArticleService +) { + @Operation( + summary = "게시글 단일 조회", + responses = [ + ApiResponse( + responseCode = "200", + description = "OK" + ) + ] + ) + @GetMapping("/{articleId}") + fun getArticle( + @Parameter(name = "articleId", description = "게시글 id") + @PathVariable articleId: Long + ): Response + = Response(data = articleService.getArticle(articleId)) + + @Operation( + summary = "새로운 게시글 작성", + responses = [ + ApiResponse( + responseCode = "200", + description = "OK" + ) + ] + ) + @PostMapping + fun writeArticle( + @RequestBody articleWriteRequest: ArticleWriteRequest + ): Response + = Response(data = articleService.writeArticle(articleWriteRequest)) +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/daitssuapi/domain/main/dto/request/ArticleWriteRequest.kt b/src/main/kotlin/com/example/daitssuapi/domain/main/dto/request/ArticleWriteRequest.kt new file mode 100644 index 00000000..fbb73b44 --- /dev/null +++ b/src/main/kotlin/com/example/daitssuapi/domain/main/dto/request/ArticleWriteRequest.kt @@ -0,0 +1,24 @@ +package com.example.daitssuapi.domain.main.dto.request + +import com.example.daitssuapi.domain.main.enums.Topic +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "커뮤니티 게시글 작성 API request body") +data class ArticleWriteRequest( + @Schema( + description = "게시글 주제", + allowableValues = ["CHAT", "INFORMATION", "QUESTION"] + ) + val topic: Topic, + + @Schema(description = "게시글 제목") + val title: String, + + @Schema(description = "게시글 내용") + val content: String, + + @Schema(description = "작성자 닉네임") + val nickname: String? = null +// val studentId: Int, +// val images, // image 데이터 +) \ No newline at end of file diff --git a/src/main/kotlin/com/example/daitssuapi/domain/main/dto/response/ArticleResponse.kt b/src/main/kotlin/com/example/daitssuapi/domain/main/dto/response/ArticleResponse.kt new file mode 100644 index 00000000..dfca280e --- /dev/null +++ b/src/main/kotlin/com/example/daitssuapi/domain/main/dto/response/ArticleResponse.kt @@ -0,0 +1,30 @@ +package com.example.daitssuapi.domain.main.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +@Schema(description = "단일 게시글 정보") +data class ArticleResponse( + @Schema(description = "게시글 id") + val id: Long, + + @Schema( + description = "게시글 주제", + allowableValues = ["잡담", "정보", "질문"] + ) + val topic: String, + + @Schema(description = "게시글 제목") + val title: String, + + @Schema(description = "게시글 내용") + val content: String, + + @Schema(description = "작성자 닉네임") + val writerNickName: String, + + @Schema(description = "마지막 수정된 시각") + val updatedAt: LocalDateTime, + +// val images // image 데이터 +) \ No newline at end of file diff --git a/src/main/kotlin/com/example/daitssuapi/domain/main/enums/Topic.kt b/src/main/kotlin/com/example/daitssuapi/domain/main/enums/Topic.kt new file mode 100644 index 00000000..9599eac4 --- /dev/null +++ b/src/main/kotlin/com/example/daitssuapi/domain/main/enums/Topic.kt @@ -0,0 +1,7 @@ +package com.example.daitssuapi.domain.main.enums + +enum class Topic(val value: String) { + CHAT("잡담"), + QUESTION("질문"), + INFORMATION("정보"); +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/daitssuapi/domain/main/model/entity/Article.kt b/src/main/kotlin/com/example/daitssuapi/domain/main/model/entity/Article.kt new file mode 100644 index 00000000..3944f0d6 --- /dev/null +++ b/src/main/kotlin/com/example/daitssuapi/domain/main/model/entity/Article.kt @@ -0,0 +1,26 @@ +package com.example.daitssuapi.domain.main.model.entity + +import com.example.daitssuapi.common.audit.BaseEntity +import com.example.daitssuapi.domain.main.enums.Topic +import jakarta.persistence.* + +@Entity +@Table(schema = "main") +class Article( + @Enumerated(value = EnumType.STRING) + @Column(length = 16) + var topic: Topic, + + @Column(length = 256) + var title: String, + + @Column(length = 2048) + var content: String, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + var writer: User, + + @Column(length = 2048) + var imageUrl: String? = null, +): BaseEntity() \ No newline at end of file diff --git a/src/main/kotlin/com/example/daitssuapi/domain/main/model/entity/Reaction.kt b/src/main/kotlin/com/example/daitssuapi/domain/main/model/entity/Reaction.kt new file mode 100644 index 00000000..3b8e837b --- /dev/null +++ b/src/main/kotlin/com/example/daitssuapi/domain/main/model/entity/Reaction.kt @@ -0,0 +1,16 @@ +package com.example.daitssuapi.domain.main.model.entity + +import com.example.daitssuapi.common.audit.BaseEntity +import jakarta.persistence.* + +@Entity +@Table(schema = "main") +class Reaction( + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "article_id") + val article: Article, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + val user: User, +): BaseEntity() \ No newline at end of file diff --git a/src/main/kotlin/com/example/daitssuapi/domain/main/model/entity/User.kt b/src/main/kotlin/com/example/daitssuapi/domain/main/model/entity/User.kt new file mode 100644 index 00000000..eb99e374 --- /dev/null +++ b/src/main/kotlin/com/example/daitssuapi/domain/main/model/entity/User.kt @@ -0,0 +1,21 @@ +package com.example.daitssuapi.domain.main.model.entity + +import com.example.daitssuapi.common.audit.BaseEntity +import com.example.daitssuapi.domain.main.model.entity.Department +import jakarta.persistence.* + +@Entity +@Table(schema = "main", name = "users") +class User( + val studentId: Int, + + val name: String, + + val nickname: String? = null, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "department_id") + val department: Department, + + val imageUrl: String? = null +) : BaseEntity() \ No newline at end of file diff --git a/src/main/kotlin/com/example/daitssuapi/domain/main/model/repository/ArticleRepository.kt b/src/main/kotlin/com/example/daitssuapi/domain/main/model/repository/ArticleRepository.kt new file mode 100644 index 00000000..c341768a --- /dev/null +++ b/src/main/kotlin/com/example/daitssuapi/domain/main/model/repository/ArticleRepository.kt @@ -0,0 +1,7 @@ +package com.example.daitssuapi.domain.main.model.repository + +import com.example.daitssuapi.domain.main.model.entity.Article +import org.springframework.data.jpa.repository.JpaRepository + +interface ArticleRepository: JpaRepository { +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/daitssuapi/domain/main/model/repository/ReactionRepository.kt b/src/main/kotlin/com/example/daitssuapi/domain/main/model/repository/ReactionRepository.kt new file mode 100644 index 00000000..7a3213b6 --- /dev/null +++ b/src/main/kotlin/com/example/daitssuapi/domain/main/model/repository/ReactionRepository.kt @@ -0,0 +1,7 @@ +package com.example.daitssuapi.domain.main.model.repository + +import com.example.daitssuapi.domain.main.model.entity.Reaction +import org.springframework.data.jpa.repository.JpaRepository + +interface ReactionRepository: JpaRepository { +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/daitssuapi/domain/main/model/repository/UserRepository.kt b/src/main/kotlin/com/example/daitssuapi/domain/main/model/repository/UserRepository.kt new file mode 100644 index 00000000..7ce919f5 --- /dev/null +++ b/src/main/kotlin/com/example/daitssuapi/domain/main/model/repository/UserRepository.kt @@ -0,0 +1,9 @@ +package com.example.daitssuapi.domain.main.model.repository + +import com.example.daitssuapi.domain.main.model.entity.User +import org.springframework.data.jpa.repository.JpaRepository + +interface UserRepository : JpaRepository { + fun findByStudentId(studentId: Int): User? + fun findByNickname(nickname: String?): User? +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/daitssuapi/domain/main/service/ArticleService.kt b/src/main/kotlin/com/example/daitssuapi/domain/main/service/ArticleService.kt new file mode 100644 index 00000000..14bfdc2b --- /dev/null +++ b/src/main/kotlin/com/example/daitssuapi/domain/main/service/ArticleService.kt @@ -0,0 +1,61 @@ +package com.example.daitssuapi.domain.main.service + +import com.example.daitssuapi.common.enums.ErrorCode +import com.example.daitssuapi.common.exception.DefaultException +import com.example.daitssuapi.domain.main.dto.request.ArticleWriteRequest +import com.example.daitssuapi.domain.main.dto.response.ArticleResponse +import com.example.daitssuapi.domain.main.model.entity.Article +import com.example.daitssuapi.domain.main.model.repository.ArticleRepository +import com.example.daitssuapi.domain.main.model.entity.User +import com.example.daitssuapi.domain.main.model.repository.UserRepository +import jakarta.transaction.Transactional +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service + +@Service +class ArticleService( + private val articleRepository: ArticleRepository, + private val userRepository: UserRepository +) { + fun getArticle(id: Long): ArticleResponse { + val article: Article = articleRepository.findByIdOrNull(id) + ?: throw DefaultException(ErrorCode.ARTICLE_NOT_FOUND) + + return ArticleResponse( + id = article.id, + topic = article.topic.value, + title = article.title, + content = article.content, + writerNickName = article.writer.nickname!!, + updatedAt = article.updatedAt + ) + } + + @Transactional + fun writeArticle(articleWriteRequest: ArticleWriteRequest): ArticleResponse { + if (articleWriteRequest.nickname == null) { + throw DefaultException(ErrorCode.NICKNAME_REQUIRED) + } + + val user: User = userRepository.findByNickname(articleWriteRequest.nickname) + ?: throw DefaultException(ErrorCode.USER_NOT_FOUND) + + val article: Article = Article( + topic = articleWriteRequest.topic, + title = articleWriteRequest.title, + content = articleWriteRequest.content, + writer = user + ) + + val savedArticle = articleRepository.save(article) + + return ArticleResponse( + id = savedArticle.id, + topic = savedArticle.topic.value, + title = savedArticle.title, + content = savedArticle.content, + writerNickName = user.nickname!!, + updatedAt = savedArticle.updatedAt + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/example/daitssuapi/domain/main/controller/ArticleControllerTest.kt b/src/test/kotlin/com/example/daitssuapi/domain/main/controller/ArticleControllerTest.kt new file mode 100644 index 00000000..e5e1ea54 --- /dev/null +++ b/src/test/kotlin/com/example/daitssuapi/domain/main/controller/ArticleControllerTest.kt @@ -0,0 +1,138 @@ +package com.example.daitssuapi.domain.main.controller + +import com.example.daitssuapi.common.enums.ErrorCode +import com.example.daitssuapi.domain.main.dto.request.ArticleWriteRequest +import com.example.daitssuapi.domain.main.enums.Topic +import com.example.daitssuapi.domain.main.model.entity.Article +import com.example.daitssuapi.domain.main.model.entity.Department +import com.example.daitssuapi.domain.main.model.entity.User +import com.example.daitssuapi.domain.main.model.repository.ArticleRepository +import com.example.daitssuapi.domain.main.model.repository.DepartmentRepository +import com.example.daitssuapi.domain.main.model.repository.UserRepository +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import org.junit.jupiter.api.AfterEach +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.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.test.context.TestConstructor +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* + +@SpringBootTest +@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) +@AutoConfigureMockMvc +class ArticleControllerTest( + private val articleRepository: ArticleRepository, + private val userRepository: UserRepository, + private val departmentRepository: DepartmentRepository +) { + @Autowired + private lateinit var mockMvc: MockMvc + + @BeforeEach + fun setUser() { + val department = Department(name = "xx학부") + departmentRepository.save(department) + + val user = User( + studentId = 20221111, + name = "홍길동", + nickname = "의적", + department = department + ) + userRepository.save(user) + } + + @AfterEach + fun deleteDB() { + articleRepository.deleteAll() + userRepository.deleteAll() + departmentRepository.deleteAll() + } + + @Test + @DisplayName("article get controller test") + fun article_get_controller_test() { + // given + val baseUri = "/daitssu/community/article" + val user = userRepository.findAll()[0] + val article: Article = Article( + topic = Topic.CHAT, + title = "테스트 제목", + content = "테스트 내용", + writer = user + ) + val savedArticle = articleRepository.save(article) + + // when & then + mockMvc.perform( + get("$baseUri/${article.id}") + .header(HttpHeaders.AUTHORIZATION, "Bearer test") + ).andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.data.title").value(article.title)) + .andExpect(jsonPath("$.data.content").value(article.content)) + .andExpect(jsonPath("$.data.writerNickName").value(user.nickname)) + .andExpect(jsonPath("$.data.topic").value(article.topic.value)) + } + + @Test + @DisplayName("article post 성공") + fun article_post_controller_success() { + // given + val baseUri = "/daitssu/community/article" + val user = userRepository.findAll()[0] + val articleWriteRequest = ArticleWriteRequest( + topic = Topic.CHAT, + title = "테스트 제목", + content = "테스트 내용", + nickname = user.nickname + ) + + val json = jacksonObjectMapper().writeValueAsString(articleWriteRequest) + + // when & then + mockMvc.perform( + post(baseUri) + .header(HttpHeaders.AUTHORIZATION, "Bearer test") + .content(json) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.data.title").value(articleWriteRequest.title)) + .andExpect(jsonPath("$.data.content").value(articleWriteRequest.content)) + .andExpect(jsonPath("$.data.writerNickName").value(user.nickname)) + .andExpect(jsonPath("$.data.topic").value(articleWriteRequest.topic.value)) + } + + @Test + @DisplayName("article post nickname null") + fun article_post_nickname_null() { + // given + val baseUri = "/daitssu/community/article" + val articleWriteRequest = ArticleWriteRequest( + topic = Topic.CHAT, + title = "테스트 제목", + content = "테스트 내용", + // nickname = null + ) + + val json = jacksonObjectMapper().writeValueAsString(articleWriteRequest) + + // when & then + mockMvc.perform( + post(baseUri) + .header(HttpHeaders.AUTHORIZATION, "Bearer test") + .content(json) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(jsonPath("$.code").value(ErrorCode.NICKNAME_REQUIRED.code)) + .andExpect(jsonPath("$.message").value(ErrorCode.NICKNAME_REQUIRED.message)) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/example/daitssuapi/domain/main/service/ArticleServiceTest.kt b/src/test/kotlin/com/example/daitssuapi/domain/main/service/ArticleServiceTest.kt new file mode 100644 index 00000000..f5d12856 --- /dev/null +++ b/src/test/kotlin/com/example/daitssuapi/domain/main/service/ArticleServiceTest.kt @@ -0,0 +1,94 @@ +package com.example.daitssuapi.domain.main.service + +import com.example.daitssuapi.domain.main.dto.request.ArticleWriteRequest +import com.example.daitssuapi.domain.main.dto.response.ArticleResponse +import com.example.daitssuapi.domain.main.enums.Topic +import com.example.daitssuapi.domain.main.model.entity.Article +import com.example.daitssuapi.domain.main.model.entity.Department +import com.example.daitssuapi.domain.main.model.entity.User +import com.example.daitssuapi.domain.main.model.repository.ArticleRepository +import com.example.daitssuapi.domain.main.model.repository.DepartmentRepository +import com.example.daitssuapi.domain.main.model.repository.UserRepository +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.TestConstructor + +@SpringBootTest +@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) +class ArticleServiceTest( + private val articleService: ArticleService, + private val userRepository: UserRepository, + private val articleRepository: ArticleRepository, + private val departmentRepository: DepartmentRepository +) { + @BeforeEach + fun setUser() { + val department = Department(name = "xx학부") + departmentRepository.save(department) + + val user = User( + studentId = 20221111, + name = "홍길동", + nickname = "의적", + department = department + ) + userRepository.save(user) + } + + @AfterEach + fun deleteDB() { + articleRepository.deleteAll() + userRepository.deleteAll() + departmentRepository.deleteAll() + } + + @Test + @DisplayName("article 생성 테스트") + fun article_generation_test() { + // given + val user = userRepository.findAll()[0] + + // when + val articleWriteRequest = ArticleWriteRequest( + topic = Topic.CHAT, + title = "테스트 제목", + content = "테스트 내용", + nickname = user.nickname!! + ) + val articleResponse = articleService.writeArticle(articleWriteRequest) + + // then + assertEquals(articleWriteRequest.topic.value, articleResponse.topic) + assertEquals(articleWriteRequest.title, articleResponse.title) + assertEquals(articleWriteRequest.content, articleResponse.content) + assertEquals(user.nickname, articleResponse.writerNickName) + } + + @Test + @DisplayName("article 조회 테스트") + fun article_find_test() { + // given + val user = userRepository.findAll()[0] + val article = Article( + topic = Topic.CHAT, + title = "테스트 제목", + content = "테스트 내용", + writer = user + ) + val savedArticle = articleRepository.save(article) + + // when + val articleResponse: ArticleResponse = articleService.getArticle(savedArticle.id) + + // then + assertEquals(articleResponse.id, savedArticle.id) + assertEquals(articleResponse.topic, savedArticle.topic.value) + assertEquals(articleResponse.title, savedArticle.title) + assertEquals(articleResponse.content, savedArticle.content) + assertEquals(articleResponse.writerNickName, savedArticle.writer.nickname) + } +} \ No newline at end of file