From e87af19b71ba7e7144d33aed3a0f62284f8b2633 Mon Sep 17 00:00:00 2001 From: hyunseo <82203978+hyena0608@users.noreply.github.com> Date: Mon, 23 Jan 2023 15:15:49 +0900 Subject: [PATCH 01/19] =?UTF-8?q?[#69]=20Restdocs,asciidoctor=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RestDocs 문서화 기능 추가 --- build.gradle | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/build.gradle b/build.gradle index 67762de..8b437b8 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,7 @@ 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' @@ -15,6 +16,7 @@ version = '1.0.0' sourceCompatibility = '17' configurations { + asciidoctorExt compileOnly { extendsFrom annotationProcessor } @@ -31,6 +33,7 @@ swaggerSources { } ext { + set('snippetsDir', file("build/generated-snippets")) set('testcontainersVersion', "1.17.6") } @@ -88,6 +91,7 @@ openapi3 { } tasks.named('test') { + outputs.dir snippetsDir useJUnitPlatform() finalizedBy jacocoTestReport } @@ -96,6 +100,10 @@ tasks.withType(GenerateSwaggerUI).configureEach { dependsOn 'openapi3' } +asciidoctor.doFirst { + delete file('src/main/resources/static/docs') +} + tasks.register('copySwaggerUI', Copy) { dependsOn 'generateSwaggerUISample' @@ -105,6 +113,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' } From 2afe7ada8927fc801786efe3a277fc3454ded034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=9D=80=EB=B9=84?= <59335077+hikarigin99@users.noreply.github.com> Date: Mon, 23 Jan 2023 17:59:20 +0900 Subject: [PATCH 02/19] =?UTF-8?q?[#71]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20(#72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: test Container 적용 - 1 * refactor: test Container 적용 - 2 * refactor: test Container 적용 - 3 * refactor: test Container 적용 - 4 --- .../domain/comment/api/CommentController.java | 8 +- .../domain/user/api/UserController.java | 6 +- .../prolog/global/config/DatabaseConfig.java | 12 --- .../prolog/global/jwt/JwtAuthentication.java | 1 + src/main/resources/application-db.yml | 23 +++--- .../prolog/config/TestContainerConfig.java | 25 ++++++ .../repository/CommentRepositoryTest.java | 1 - .../domain/post/api/PostControllerTest.java | 54 ++++++++----- .../post/repository/PostRepositoryTest.java | 1 - .../domain/post/service/PostServiceTest.java | 4 +- .../user/Repository/UserRepositoryTest.java | 21 +++-- .../domain/user/api/UserControllerTest.java | 10 +-- .../com/prgrms/prolog/utils/TestUtils.java | 2 +- src/test/resources/application.yml | 28 +++++++ src/test/resources/schema.sql | 80 +++++++++++++++++++ 15 files changed, 200 insertions(+), 76 deletions(-) delete mode 100644 src/main/java/com/prgrms/prolog/global/config/DatabaseConfig.java create mode 100644 src/test/java/com/prgrms/prolog/config/TestContainerConfig.java create mode 100644 src/test/resources/application.yml create mode 100644 src/test/resources/schema.sql 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..e119cf6 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,9 +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(); + String userEmail = user.userEmail(); commentService.save(request, userEmail, postId); return ResponseEntity.status(CREATED).build(); } @@ -42,9 +42,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(); + String userEmail = user.userEmail(); commentService.update(request, userEmail, commentId); return ResponseEntity.ok().build(); } 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..0e57df4 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,14 +14,16 @@ 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) { + ResponseEntity myPage( + @AuthenticationPrincipal JwtAuthentication user + ) { return ResponseEntity.ok(userService.findByEmail(user.userEmail())); } 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/jwt/JwtAuthentication.java b/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthentication.java index d9c02df..0b1074f 100644 --- a/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthentication.java +++ b/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthentication.java @@ -7,6 +7,7 @@ public record JwtAuthentication(String token, String userEmail) { public JwtAuthentication { validateToken(token); validateUserEmail(userEmail); + } private void validateToken(String token) { diff --git a/src/main/resources/application-db.yml b/src/main/resources/application-db.yml index 021918d..ad16f4b 100644 --- a/src/main/resources/application-db.yml +++ b/src/main/resources/application-db.yml @@ -2,6 +2,9 @@ spring: datasource: + url: ${MYSQL_URL} + username: ${MYSQL_USERNAME} + password: ${MYSQL_ROOT_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver # 커넥션 풀 설정 @@ -22,21 +25,15 @@ spring: dialect: org.hibernate.dialect.MySQL8Dialect format_sql: true - flyway: - enabled: false - messages: encoding: UTF-8 basename: messages/exceptions/exception, messages/logs/log-form --- # local spring: - config.activate.on-profile: "db-local" - - datasource: - url: ${db.datasource.url} - username: ${db.datasource.username} - password: ${db.datasource.password} + config: + activate.on-profile: "db-local" + import: optional:file:.env[.properties] # SQL 로그 설정 logging: @@ -44,17 +41,15 @@ logging: org.hibernate.SQL: debug org.hibernate.type: trace # 파라미터 값 + flyway: + enabled: false + --- # prod spring: config: 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 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/repository/CommentRepositoryTest.java b/src/test/java/com/prgrms/prolog/domain/comment/repository/CommentRepositoryTest.java index cfd79ae..100309a 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 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..4efc846 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,37 +3,47 @@ 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.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.user.repository.UserRepository; 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; + + @Autowired + RestDocumentationResultHandler restDocs; @Autowired private ObjectMapper objectMapper; @Autowired @@ -45,10 +55,16 @@ class PostControllerTest { Long postId; @BeforeEach - void setUp() { + void setUp(WebApplicationContext webApplicationContext, + RestDocumentationContextProvider restDocumentation) { userRepository.save(USER); CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", false); postId = postService.save(createRequest, USER_EMAIL); + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .apply(documentationConfiguration(restDocumentation)) + .alwaysDo(restDocs) + .apply(springSecurity()) + .build(); } @Test @@ -56,12 +72,12 @@ void setUp() { void save() throws Exception { CreateRequest request = new CreateRequest("생성된 테스트 제목", "생성된 테스트 내용", true); - mockMvc.perform(post("/api/v1/posts") + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts") .header("token", JWT_TOKEN_PROVIDER.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"), @@ -74,14 +90,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))) .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"), @@ -99,11 +115,11 @@ void findAll() throws Exception { @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))) .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"), @@ -123,12 +139,12 @@ void findById() throws Exception { void update() throws Exception { UpdateRequest request = new UpdateRequest("수정된 테스트 제목", "수정된 테스트 내용", true); - mockMvc.perform(patch("/api/v1/posts/{id}", postId) + mockMvc.perform(RestDocumentationRequestBuilders.patch("/api/v1/posts/{id}", postId) .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims)) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) ).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"), @@ -152,7 +168,7 @@ void update() throws Exception { @Test @DisplayName("게시물 아이디로 게시물을 삭제할 수 있다.") void remove() throws Exception { - mockMvc.perform(delete("/api/v1/posts/{id}", postId) + mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/posts/{id}", postId) .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims)) .contentType(MediaType.APPLICATION_JSON) ).andExpect(status().isNoContent()) @@ -166,7 +182,7 @@ void isValidateTitleNull() throws Exception { String requestJsonString = objectMapper.writeValueAsString(createRequest); - mockMvc.perform(post("/api/v1/posts") + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts") .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims)) .contentType(MediaType.APPLICATION_JSON) .content(requestJsonString)) @@ -180,7 +196,7 @@ void isValidateContentEmpty() throws Exception { String requestJsonString = objectMapper.writeValueAsString(createRequest); - mockMvc.perform(post("/api/v1/posts") + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts") .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims)) .contentType(MediaType.APPLICATION_JSON) .content(requestJsonString)) @@ -197,7 +213,7 @@ void isValidateTitleSizeOver() throws Exception { String requestJsonString = objectMapper.writeValueAsString(createRequest); - mockMvc.perform(post("/api/v1/posts") + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts") .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims)) .contentType(MediaType.APPLICATION_JSON) .content(requestJsonString)) 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..a5b293c 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 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..b69f3ed 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 @@ -16,6 +16,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.transaction.annotation.Transactional; +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.dto.PostResponse; @@ -23,12 +24,11 @@ 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(DatabaseConfig.class) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @SpringBootTest @Transactional +@Import(TestContainerConfig.class) class PostServiceTest { @Autowired 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..fedf236 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,43 +2,43 @@ 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 Optional foundUser = userRepository.findById(savedUser.getId()); // then @@ -46,14 +46,12 @@ void saveAndFindByIdTest() { assertThat(foundUser.get()) .usingRecursiveComparison() .isEqualTo(savedUser); - } @Test @DisplayName("이메일로 저장된 유저 정보를 조회할 수 있다.") void findEmailTest() { // given - User savedUser = userRepository.save(getUser()); // when Optional foundUser = userRepository.findByEmail(savedUser.getEmail()); // then @@ -67,9 +65,8 @@ void findEmailTest() { @DisplayName("저장되지 않은 유저는 조회할 수 없다.") void findFailTest() { // given - User notSavedUser = getUser(); // when - Optional foundUser = userRepository.findByEmail(notSavedUser.getEmail()); + Optional foundUser = userRepository.findByEmail("unsavedUserEmail@test.com"); // then assertThat(foundUser).isNotPresent(); } 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..c4983c7 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,7 +14,6 @@ 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.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; @@ -28,7 +25,6 @@ import org.springframework.web.filter.CharacterEncodingFilter; import com.prgrms.prolog.config.RestDocsConfig; - import com.prgrms.prolog.domain.user.repository.UserRepository; import com.prgrms.prolog.domain.user.service.UserServiceImpl; import com.prgrms.prolog.global.config.JpaConfig; @@ -44,7 +40,7 @@ class UserControllerTest { @Autowired RestDocumentationResultHandler restDocs; - @MockBean + @Autowired private UserServiceImpl userService; @Autowired private UserRepository userRepository; @@ -63,12 +59,10 @@ 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); // when - mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/users/me") + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/user/me") .header("token", jwtTokenProvider.createAccessToken(claims)) // .header(HttpHeaders.AUTHORIZATION, "token" + jwtTokenProvider.createAccessToken(claims)) ) diff --git a/src/test/java/com/prgrms/prolog/utils/TestUtils.java b/src/test/java/com/prgrms/prolog/utils/TestUtils.java index 98e1044..6198b8f 100644 --- a/src/test/java/com/prgrms/prolog/utils/TestUtils.java +++ b/src/test/java/com/prgrms/prolog/utils/TestUtils.java @@ -10,7 +10,7 @@ public class TestUtils { // User Data - public static final String USER_EMAIL = "Dev@programmers.com"; + 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"; diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000..8f0451e --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,28 @@ +spring: + flyway: + enabled: false + datasource: + driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver + url: jdbc:tc:mysql:8.0.31:///test?TC_INITSCRIPT=schema.sql + security: + oauth2: + client: + registration: + kakao: + client-name: kakao + client-id: ${CLIENT_ID} + client-secret: ${CLIENT_SECRET} + scope: profile_nickname, account_email + redirect-uri: ${REDIRECT_URI} + authorization-grant-type: authorization_code + client-authentication-method: POST + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id +jwt: + issuer: prgrms + secret-key: prgrmsbackenddevrteamprologkwonj + expiry-seconds: 60 diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 0000000..e8498b3 --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,80 @@ +-- V1__init.sql +-- create database if not exists test; +-- use test; +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, + 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) +); \ No newline at end of file From 72a84ccc1719b6502b3ea402ff37c70f1c946525 Mon Sep 17 00:00:00 2001 From: Fortune00 <53924962+Sinyoung3016@users.noreply.github.com> Date: Wed, 25 Jan 2023 14:24:30 +0900 Subject: [PATCH 03/19] =?UTF-8?q?Github=20Action=20=EB=A6=AC=ED=8E=99?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20README=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20(#7?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 변수 이름 및 중복 값 정리 * docs: README에 배포 및 프로젝트 실행 방법 추가 * docs: README 양식 수정 - 이모지를 포함한 목차가 안되나봐요.. 슬퍼요 --- .github/workflows/docker-push-and-aws-run.yml | 4 +- .github/workflows/gradle-build.yml | 11 +- Dockerfile | 6 +- README.md | 154 ++++++++++++++++-- docker-compose.yml | 2 +- src/main/resources/application-db.yml | 6 +- 6 files changed, 158 insertions(+), 25 deletions(-) diff --git a/.github/workflows/docker-push-and-aws-run.yml b/.github/workflows/docker-push-and-aws-run.yml index 2f88782..fa3f0e7 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 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..76fd627 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ 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} \ +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} \ 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/docker-compose.yml b/docker-compose.yml index ea2f319..5bea43b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,9 +23,9 @@ services: - db restart: always environment: + SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL} SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME} SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} - SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL} REDIRECT_URI: ${REDIRECT_URI} JWT_ISSUER: ${JWT_ISSUER} JWT_SECRET_KEY: ${JWT_SECRET_KEY} diff --git a/src/main/resources/application-db.yml b/src/main/resources/application-db.yml index ad16f4b..eb0ad66 100644 --- a/src/main/resources/application-db.yml +++ b/src/main/resources/application-db.yml @@ -2,9 +2,9 @@ spring: datasource: - url: ${MYSQL_URL} - username: ${MYSQL_USERNAME} - password: ${MYSQL_ROOT_PASSWORD} + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver # 커넥션 풀 설정 From 78707532bc2dcce7fc317068380b4535cd193c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=EC=A3=BC=EC=84=B1?= <99165624+JoosungKwon@users.noreply.github.com> Date: Wed, 25 Jan 2023 14:29:36 +0900 Subject: [PATCH 04/19] =?UTF-8?q?[#66]=20Jwt=20&=20OAuth=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=84=B0=EB=A7=81=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Jwt Payload(Claim)를 email에서 id로 변경 * refactor: headerKey를 "token"에서 "AUTHORIZATION Bearer"로 변경 * refactor: 변경사항 의존관계 정리 * feat: Profile_Img_url 추가 * refactor: 테스트 코드 리팩터링 * jacoco: 예외 클래스 추가 * add: 엔티티 변경으로 flyway 변경 sql 추가 * update: 엔티티 변경 * update: 엔티티 변경 --- build.gradle | 3 +- .../domain/comment/api/CommentController.java | 6 +- .../comment/service/CommentService.java | 4 +- .../comment/service/CommentServiceImpl.java | 17 +--- .../domain/post/api/PostController.java | 7 +- .../prolog/domain/post/dto/PostResponse.java | 8 +- .../domain/post/service/PostService.java | 4 +- .../domain/user/api/UserController.java | 6 +- .../prolog/domain/user/dto/UserDto.java | 21 +++-- .../prgrms/prolog/domain/user/model/User.java | 5 +- .../user/repository/UserRepository.java | 9 +- .../domain/user/service/UserService.java | 8 +- .../domain/user/service/UserServiceImpl.java | 42 +++++---- .../prolog/global/config/JpaConfig.java | 6 +- .../prolog/global/jwt/JwtAuthentication.java | 20 ++-- .../jwt/JwtAuthenticationEntryPoint.java | 19 +++- .../global/jwt/JwtAuthenticationFilter.java | 32 +++++-- .../prolog/global/jwt/JwtTokenProvider.java | 26 ++---- .../OAuthAuthenticationSuccessHandler.java | 6 +- .../prolog/global/oauth/OAuthProvider.java | 5 +- .../prolog/global/oauth/OAuthService.java | 7 +- src/main/resources/application-security.yml | 2 +- .../db/migration/V2__add_profile_img_url.sql | 1 + .../com/prgrms/prolog/config/JwtConfig.java | 20 ++++ .../comment/api/CommentControllerTest.java | 33 ++++--- .../repository/CommentRepositoryTest.java | 6 +- .../service/CommentServiceImplTest.java | 21 +++-- .../domain/post/api/PostControllerTest.java | 36 +++++--- .../domain/post/service/PostServiceTest.java | 4 +- .../user/Repository/UserRepositoryTest.java | 19 +--- .../domain/user/api/UserControllerTest.java | 26 +++--- .../domain/user/service/UserServiceTest.java | 92 +++++++++---------- .../global/jwt/JwtAuthenticationTest.java | 19 ++-- .../global/jwt/JwtTokenProviderTest.java | 31 ++++--- .../com/prgrms/prolog/utils/TestUtils.java | 44 +++++---- src/test/resources/application.yml | 2 +- .../org.mockito.plugins.MockMaker | 1 + src/test/resources/schema.sql | 1 + 38 files changed, 351 insertions(+), 268 deletions(-) create mode 100644 src/main/resources/db/migration/V2__add_profile_img_url.sql create mode 100644 src/test/java/com/prgrms/prolog/config/JwtConfig.java create mode 100644 src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/build.gradle b/build.gradle index 8b437b8..1e9f0de 100644 --- a/build.gradle +++ b/build.gradle @@ -152,7 +152,8 @@ jacocoTestCoverageVerification { excludes = [ '*.global*', - '*.service*', + '*.series*', + '*.comment*', '*.dto*' ] 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 e119cf6..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 @@ -32,8 +32,7 @@ public ResponseEntity save( @Valid @RequestBody CreateCommentRequest request, @AuthenticationPrincipal JwtAuthentication user ) { - String userEmail = user.userEmail(); - commentService.save(request, userEmail, postId); + commentService.save(request, user.id(), postId); return ResponseEntity.status(CREATED).build(); } @@ -44,8 +43,7 @@ public ResponseEntity update( @Valid @RequestBody UpdateCommentRequest request, @AuthenticationPrincipal JwtAuthentication user ) { - String userEmail = user.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/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/post/api/PostController.java b/src/main/java/com/prgrms/prolog/domain/post/api/PostController.java index e0f2a40..e82627a 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 @@ -38,16 +38,15 @@ public PostController(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); } 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..96d5f93 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,22 @@ package com.prgrms.prolog.domain.post.dto; +import static com.prgrms.prolog.domain.user.dto.UserDto.UserProfile.*; + import java.util.List; 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.user.dto.UserDto.UserProfile; public record PostResponse(String title, String content, boolean openStatus, - UserResponse.findResponse user, + UserProfile user, List comment, int commentCount) { 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()); + toUserProfile(post.getUser()), post.getComments(), post.getComments().size()); } } 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..7d2d774 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 @@ -28,8 +28,8 @@ public PostService(PostRepository postRepository, UserRepository userRepository) this.userRepository = userRepository; } - public Long save(CreateRequest create, String userEmail) { - User user = userRepository.findByEmail(userEmail) + public Long save(CreateRequest create, Long userId) { + User user = userRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException(USER_NOT_EXIST_MESSAGE)); Post post = postRepository.save(CreateRequest.toEntity(create, user)); return post.getId(); 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 0e57df4..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 @@ -21,10 +21,12 @@ public class UserController { private final UserService userService; @GetMapping("/me") - ResponseEntity myPage( + ResponseEntity getMyProfile( @AuthenticationPrincipal JwtAuthentication user ) { - return ResponseEntity.ok(userService.findByEmail(user.userEmail())); + 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..97641e1 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 @@ -7,30 +7,37 @@ 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( + 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 ) { } + 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..6f4b132 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 @@ -54,6 +54,8 @@ public class User extends BaseEntity { 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 +69,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) { 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..0136ef5 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,7 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import com.prgrms.prolog.domain.user.model.User; @@ -10,5 +11,11 @@ @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); } 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/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/jwt/JwtAuthentication.java b/src/main/java/com/prgrms/prolog/global/jwt/JwtAuthentication.java index 0b1074f..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,31 +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/application-security.yml b/src/main/resources/application-security.yml index 48ccb8b..bb0d63f 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: 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/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/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 100309a..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 @@ -37,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..bc53c75 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 @@ -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/post/api/PostControllerTest.java b/src/test/java/com/prgrms/prolog/domain/post/api/PostControllerTest.java index 4efc846..873ad7e 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 @@ -14,6 +14,7 @@ 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; @@ -32,6 +33,7 @@ import com.prgrms.prolog.domain.post.dto.PostRequest.UpdateRequest; import com.prgrms.prolog.domain.post.service.PostService; import com.prgrms.prolog.domain.user.repository.UserRepository; +import com.prgrms.prolog.global.jwt.JwtTokenProvider; import com.prgrms.prolog.global.jwt.JwtTokenProvider.Claims; @ExtendWith(RestDocumentationExtension.class) @@ -42,6 +44,9 @@ class PostControllerTest { private MockMvc mockMvc; + @Autowired + private JwtTokenProvider jwtTokenProvider; + @Autowired RestDocumentationResultHandler restDocs; @Autowired @@ -50,16 +55,20 @@ class PostControllerTest { private PostService postService; @Autowired private UserRepository userRepository; - - static Claims claims = Claims.from(USER_EMAIL, "ROLE_USER"); Long postId; + Long userId; + Claims claims; + @BeforeEach void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { - userRepository.save(USER); + + userId = userRepository.save(USER).getId(); + claims = Claims.from(userId, "ROLE_USER"); CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", false); - postId = postService.save(createRequest, USER_EMAIL); + postId = postService.save(createRequest, userId); + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .apply(documentationConfiguration(restDocumentation)) .alwaysDo(restDocs) @@ -73,7 +82,7 @@ void save() throws Exception { CreateRequest request = new CreateRequest("생성된 테스트 제목", "생성된 테스트 내용", true); mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts") - .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims)) + .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) ).andExpect(status().isCreated()) @@ -94,7 +103,7 @@ void findAll() throws Exception { .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(restDocs.document( @@ -107,6 +116,7 @@ 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("[].comment").type(JsonFieldType.ARRAY).description("comment"), fieldWithPath("[].commentCount").type(JsonFieldType.NUMBER).description("commentCount") ))); @@ -117,7 +127,7 @@ void findAll() throws Exception { void findById() throws Exception { 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(restDocs.document( responseFields( @@ -129,6 +139,7 @@ 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("comment").type(JsonFieldType.ARRAY).description("comment"), fieldWithPath("commentCount").type(JsonFieldType.NUMBER).description("commentCount") ))); @@ -140,7 +151,7 @@ void update() throws Exception { UpdateRequest request = new UpdateRequest("수정된 테스트 제목", "수정된 테스트 내용", true); mockMvc.perform(RestDocumentationRequestBuilders.patch("/api/v1/posts/{id}", postId) - .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims)) + .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) ).andExpect(status().isOk()) @@ -159,6 +170,7 @@ 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("comment").type(JsonFieldType.ARRAY).description("comment"), fieldWithPath("commentCount").type(JsonFieldType.NUMBER).description("commentCount") ) @@ -169,7 +181,7 @@ void update() throws Exception { @DisplayName("게시물 아이디로 게시물을 삭제할 수 있다.") void remove() throws Exception { mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/posts/{id}", postId) - .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims)) + .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) .contentType(MediaType.APPLICATION_JSON) ).andExpect(status().isNoContent()) .andDo(document("post-delete")); @@ -183,7 +195,7 @@ void isValidateTitleNull() throws Exception { String requestJsonString = objectMapper.writeValueAsString(createRequest); mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts") - .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims)) + .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) .contentType(MediaType.APPLICATION_JSON) .content(requestJsonString)) .andExpect(status().isBadRequest()); @@ -197,7 +209,7 @@ void isValidateContentEmpty() throws Exception { String requestJsonString = objectMapper.writeValueAsString(createRequest); mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts") - .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims)) + .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) .contentType(MediaType.APPLICATION_JSON) .content(requestJsonString)) .andExpect(status().isBadRequest()); @@ -214,7 +226,7 @@ void isValidateTitleSizeOver() throws Exception { String requestJsonString = objectMapper.writeValueAsString(createRequest); mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts") - .header("token", JWT_TOKEN_PROVIDER.createAccessToken(claims)) + .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) .contentType(MediaType.APPLICATION_JSON) .content(requestJsonString)) .andExpect(status().isBadRequest()); 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 b69f3ed..56eeec0 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 @@ -59,7 +59,7 @@ void setData() { @DisplayName("게시물을 등록할 수 있다.") void save_success() { CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", true); - Long savePostId = postService.save(postRequest, USER_EMAIL); + Long savePostId = postService.save(postRequest, user.getId()); assertThat(savePostId).isNotNull(); } @@ -70,7 +70,7 @@ void save_fail() { CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", true); - assertThatThrownBy(() -> postService.save(postRequest, notExistEmail)) + assertThatThrownBy(() -> postService.save(postRequest, USER_ID)) .isInstanceOf(IllegalArgumentException.class); } 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 fedf236..a456bae 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 @@ -38,8 +38,7 @@ void setUp() { @Test @DisplayName("저장된 유저 정보를 유저ID로 찾아 가져올 수 있다.") void saveAndFindByIdTest() { - // given - // when + // given & when Optional foundUser = userRepository.findById(savedUser.getId()); // then assertThat(foundUser).isPresent(); @@ -48,25 +47,13 @@ void saveAndFindByIdTest() { .isEqualTo(savedUser); } - @Test - @DisplayName("이메일로 저장된 유저 정보를 조회할 수 있다.") - void findEmailTest() { - // given - // when - Optional foundUser = userRepository.findByEmail(savedUser.getEmail()); - // then - assertThat(foundUser).isPresent(); - assertThat(foundUser.get()) - .usingRecursiveComparison() - .isEqualTo(savedUser); - } - @Test @DisplayName("저장되지 않은 유저는 조회할 수 없다.") void findFailTest() { // given + Long unsavedUserId = 0L; // when - Optional foundUser = userRepository.findByEmail("unsavedUserEmail@test.com"); + Optional foundUser = userRepository.findById(unsavedUserId); // then assertThat(foundUser).isNotPresent(); } 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 c4983c7..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 @@ -15,31 +15,34 @@ 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.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; + private JwtTokenProvider jwtTokenProvider; + @Autowired + private RestDocumentationResultHandler restDocs; @Autowired private UserServiceImpl userService; @Autowired @@ -59,24 +62,25 @@ void setUp(WebApplicationContext context, RestDocumentationContextProvider provi @DisplayName("사용자는 자신의 프로필 정보를 확인할 수 있다") void userPage() throws Exception { // given - userRepository.save(USER); - Claims claims = Claims.from(USER_EMAIL, USER_ROLE); + User savedUser = userRepository.save(USER); + Claims claims = Claims.from(savedUser.getId(), USER_ROLE); // when mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/user/me") - .header("token", jwtTokenProvider.createAccessToken(claims)) - // .header(HttpHeaders.AUTHORIZATION, "token" + jwtTokenProvider.createAccessToken(claims)) + .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..b6707fe 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,6 +26,9 @@ class UserServiceTest { @Mock private UserRepository userRepository; + @Mock + private User userMock; + @InjectMocks private UserServiceImpl userService; // 빈으로 등록해서 주입 받고 싶으면 어떻게 해야하나요? 구현체말고 인터페이스를 주입 받고 싶습니다! @@ -33,73 +37,69 @@ class UserServiceTest { 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/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 6198b8f..e37465a 100644 --- a/src/test/java/com/prgrms/prolog/utils/TestUtils.java +++ b/src/test/java/com/prgrms/prolog/utils/TestUtils.java @@ -2,22 +2,26 @@ import com.prgrms.prolog.domain.comment.model.Comment; import com.prgrms.prolog.domain.post.model.Post; -import com.prgrms.prolog.domain.user.dto.UserDto.UserInfo; import com.prgrms.prolog.domain.user.dto.UserDto.UserProfile; +import com.prgrms.prolog.domain.user.dto.UserDto.UserInfo; import com.prgrms.prolog.domain.user.model.User; -import com.prgrms.prolog.global.jwt.JwtTokenProvider; public class TestUtils { // User Data + public static final Long USER_ID = 1L; + 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 String USER_PROFILE_IMG_URL = "http://kakao/defaultImg.jpg"; public static final User USER = getUser(); public static final Post POST = getPost(); + public static final UserInfo USER_INFO = getUserInfo(); + public static final UserProfile USER_PROFILE = getUserProfile(); public static final Comment COMMENT = getComment(); // Post & Comment Data public static final String TITLE = "제목을 입력해주세요"; @@ -28,13 +32,8 @@ public class TestUtils { 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,6 +47,7 @@ public static User getUser() { .prologName(USER_PROLOG_NAME) .provider(PROVIDER) .oauthId(OAUTH_ID) + .profileImgUrl(USER_PROFILE_IMG_URL) .build(); } @@ -68,17 +68,25 @@ public static Comment getComment() { .build(); } - public static UserProfile getUserProfile() { - return new UserProfile( - USER_EMAIL, - USER_NICK_NAME, - PROVIDER, - OAUTH_ID - ); + 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 UserInfo getUserInfo() { - return new UserInfo(USER); + public static UserProfile getUserProfile() { + 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(); } } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 8f0451e..2f53868 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -25,4 +25,4 @@ spring: jwt: issuer: prgrms secret-key: prgrmsbackenddevrteamprologkwonj - expiry-seconds: 60 + expiry-seconds: 3 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 index e8498b3..5bbba69 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -11,6 +11,7 @@ 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, From db0307d3c41ef12a84f28e94100884f6c4389620 Mon Sep 17 00:00:00 2001 From: hyunseo <82203978+hyena0608@users.noreply.github.com> Date: Wed, 25 Jan 2023 15:55:45 +0900 Subject: [PATCH 05/19] =?UTF-8?q?[#59]=20=ED=83=9C=EA=B7=B8=20API=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 태그 엔티티 추가 - PostTag 엔티티 추가 - RootTag 엔티티 추가 - UserTag 엔티티 추가 - User 연관 관계 수정 - Post 연관 관계 수정 * feat: 태그 레포지터리 추가 - UserTagRepository - PostTagRepository - RootTagRepository * feat: tag 서비스, 컨트롤러 기능 구현 - UserTag - PostTag - RootTag * test: 태그 테스트 추가 * add: V2.1 Tag 추가 --- .../prolog/domain/comment/model/Comment.java | 1 - .../domain/post/api/PostController.java | 12 +- .../prolog/domain/post/dto/PostRequest.java | 5 +- .../prolog/domain/post/dto/PostResponse.java | 12 +- .../prgrms/prolog/domain/post/model/Post.java | 12 +- .../post/repository/PostRepository.java | 20 ++ .../domain/post/service/PostService.java | 64 +----- .../domain/post/service/PostServiceImpl.java | 202 ++++++++++++++++++ .../prolog/domain/posttag/dto/PostTagDto.java | 18 ++ .../prolog/domain/posttag/model/PostTag.java | 56 +++++ .../posttag/repository/PostTagRepository.java | 31 +++ .../prolog/domain/roottag/model/RootTag.java | 56 +++++ .../roottag/repository/RootTagRepository.java | 19 ++ .../domain/roottag/util/TagConverter.java | 24 +++ .../prgrms/prolog/domain/user/model/User.java | 15 ++ .../user/repository/UserRepository.java | 9 + .../prolog/domain/usertag/model/UserTag.java | 84 ++++++++ .../usertag/repository/UserTagRepository.java | 24 +++ .../prolog/global/config/SecurityConfig.java | 2 - .../db/migration/V2.1__add_tag_table.sql | 28 +++ .../service/CommentServiceImplTest.java | 2 +- .../domain/post/api/PostControllerTest.java | 22 +- .../domain/post/service/PostServiceTest.java | 97 +++++++-- .../domain/posttag/model/PostTagTest.java | 39 ++++ .../domain/roottag/model/RootTagTest.java | 45 ++++ .../repository/RootTagRepositoryTest.java | 42 ++++ .../user/Repository/UserRepositoryTest.java | 9 + .../domain/usertag/model/UserTagTest.java | 39 ++++ .../com/prgrms/prolog/utils/TestUtils.java | 22 +- src/test/resources/schema.sql | 34 ++- 30 files changed, 946 insertions(+), 99 deletions(-) create mode 100644 src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java create mode 100644 src/main/java/com/prgrms/prolog/domain/posttag/dto/PostTagDto.java create mode 100644 src/main/java/com/prgrms/prolog/domain/posttag/model/PostTag.java create mode 100644 src/main/java/com/prgrms/prolog/domain/posttag/repository/PostTagRepository.java create mode 100644 src/main/java/com/prgrms/prolog/domain/roottag/model/RootTag.java create mode 100644 src/main/java/com/prgrms/prolog/domain/roottag/repository/RootTagRepository.java create mode 100644 src/main/java/com/prgrms/prolog/domain/roottag/util/TagConverter.java create mode 100644 src/main/java/com/prgrms/prolog/domain/usertag/model/UserTag.java create mode 100644 src/main/java/com/prgrms/prolog/domain/usertag/repository/UserTagRepository.java create mode 100644 src/main/resources/db/migration/V2.1__add_tag_table.sql create mode 100644 src/test/java/com/prgrms/prolog/domain/posttag/model/PostTagTest.java create mode 100644 src/test/java/com/prgrms/prolog/domain/roottag/model/RootTagTest.java create mode 100644 src/test/java/com/prgrms/prolog/domain/roottag/repository/RootTagRepositoryTest.java create mode 100644 src/test/java/com/prgrms/prolog/domain/usertag/model/UserTagTest.java 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/post/api/PostController.java b/src/main/java/com/prgrms/prolog/domain/post/api/PostController.java index e82627a..8b23f65 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 @@ -22,16 +22,16 @@ 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; } @@ -58,9 +58,11 @@ public ResponseEntity> findAll(Pageable pageable) { } @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/PostRequest.java b/src/main/java/com/prgrms/prolog/domain/post/dto/PostRequest.java index 78595db..5e3380e 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,12 +3,15 @@ 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, + @Nullable String tagText, boolean openStatus) { public static Post toEntity(CreateRequest create, User user) { return Post.builder() @@ -22,7 +25,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 96d5f93..2285f91 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,8 +1,10 @@ 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; @@ -12,11 +14,17 @@ public record PostResponse(String title, String content, boolean openStatus, UserProfile user, + Set tags, List comment, int commentCount) { public static PostResponse toPostResponse(Post post) { - return new PostResponse(post.getTitle(), post.getContent(), post.isOpenStatus(), - toUserProfile(post.getUser()), post.getComments(), post.getComments().size()); + return new PostResponse(post.getTitle(), + post.getContent(), + post.isOpenStatus(), + toUserProfile(post.getUser()), + PostTagsResponse.from(post.getPostTags()).tagNames(), + post.getComments(), + post.getComments().size()); } } 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..0146c26 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 @@ -1,15 +1,17 @@ package com.prgrms.prolog.domain.post.model; +import static javax.persistence.CascadeType.*; import static javax.persistence.FetchType.*; 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.Entity; import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.Lob; @@ -20,6 +22,7 @@ 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.user.model.User; import com.prgrms.prolog.global.common.BaseEntity; @@ -58,6 +61,9 @@ public class Post extends BaseEntity { @OneToMany(mappedBy = "post") private final List comments = new ArrayList<>(); + @OneToMany(mappedBy = "post", cascade = ALL) + private final Set postTags = new HashSet<>(); + @Builder public Post(String title, String content, boolean openStatus, User user) { this.title = validateTitle(title); @@ -95,6 +101,10 @@ public void changePost(UpdateRequest updateRequest) { this.openStatus = updateRequest.openStatus(); } + public void addPostTagsFrom(List postTags) { + this.postTags.addAll(postTags); + } + private String validateTitle(String title) { checkText(title); checkOverLength(title, TITLE_MAX_SIZE); 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..9cacba6 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,28 @@ package com.prgrms.prolog.domain.post.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.post.model.Post; 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); } 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 7d2d774..175c166 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,14 @@ 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 { - - private static final String POST_NOT_EXIST_MESSAGE = "존재하지 않는 게시물입니다."; - private static final String USER_NOT_EXIST_MESSAGE = "존재하지 않는 사용자입니다."; - - private final PostRepository postRepository; - private final UserRepository userRepository; - - public PostService(PostRepository postRepository, UserRepository userRepository) { - this.postRepository = postRepository; - this.userRepository = userRepository; - } - - public Long save(CreateRequest create, Long userId) { - User user = userRepository.findById(userId) - .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 +public interface PostService { + public Long save(PostRequest.CreateRequest request, Long userId); + public PostResponse findById(Long postId); + public Page findAll(Pageable pageable); + public PostResponse update(PostRequest.UpdateRequest update, Long userId, Long postId); + public 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..cf379cd --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java @@ -0,0 +1,202 @@ +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.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 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); + return savedPost.getId(); + } + + @Override + public PostResponse findById(Long postId) { + Post post = postRepository.joinCommentFindById(postId) + .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); + List findPostTags = postTagRepository.joinPostTagFindByPostId(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.joinUserFindById(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); + return PostResponse.toPostResponse(findPost); + } + + @Override + @Transactional + public void delete(Long id) { + Post findPost = postRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); + 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 oldRootTags = distinguishOldRootTags(tagNames, findPost.getPostTags()); + Set savedNewRootTags = saveNewRootTags(newTagNames); + currentRootTags.addAll(savedNewRootTags); + + removeOldPostTags(findPost, oldRootTags); + saveNewPostTags(findPost, savedNewRootTags); + removeOrDecreaseUserTags(findPost.getUser(), oldRootTags); + } + + private void removeOldPostTags(Post post, Set oldRootTags) { + if (oldRootTags.isEmpty()) { + return; + } + List rootTagIds = oldRootTags.stream() + .map(RootTag::getId) + .toList(); + 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())) { + userTagMap.get(rootTag.getId()).decreaseCount(1); + } + } + } + + 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 newTagNames, Set rootTags) { + 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..a4e8abc --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/posttag/model/PostTag.java @@ -0,0 +1,56 @@ +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.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@EqualsAndHashCode +@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..ca27fd3 --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/posttag/repository/PostTagRepository.java @@ -0,0 +1,31 @@ +package com.prgrms.prolog.domain.posttag.repository; + +import java.util.List; + +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.posttag.model.PostTag; + +public interface PostTagRepository extends JpaRepository { + + @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") List rootTagIds + ); + + @Query(""" + SELECT pt + FROM PostTag pt + LEFT JOIN FETCH pt.rootTag + WHERE pt.post.id = :postId + """) + List joinPostTagFindByPostId(@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..4243f54 --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/roottag/model/RootTag.java @@ -0,0 +1,56 @@ +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.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@EqualsAndHashCode +@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/user/model/User.java b/src/main/java/com/prgrms/prolog/domain/user/model/User.java index 6f4b132..0d1945d 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,8 +1,12 @@ package com.prgrms.prolog.domain.user.model; +import static javax.persistence.CascadeType.*; + 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; @@ -18,6 +22,7 @@ import com.prgrms.prolog.domain.comment.model.Comment; import com.prgrms.prolog.domain.post.model.Post; +import com.prgrms.prolog.domain.usertag.model.UserTag; import com.prgrms.prolog.global.common.BaseEntity; import lombok.AccessLevel; @@ -49,6 +54,8 @@ public class User extends BaseEntity { private final List posts = new ArrayList<>(); @OneToMany(mappedBy = "user") private final List comments = new ArrayList<>(); + @OneToMany(mappedBy = "user", cascade = ALL) + private final Set userTags = new HashSet<>(); @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -138,6 +145,14 @@ public boolean checkSameEmail(String email) { return this.email.equals(email); } + public void removeUserTag(UserTag userTag) { + this.userTags.remove(userTag); + } + + public boolean checkSameUserId(Long userId) { + return Objects.equals(this.id, userId); + } + @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 0136ef5..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 @@ -4,6 +4,7 @@ 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; @@ -18,4 +19,12 @@ public interface UserRepository extends JpaRepository { 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/usertag/model/UserTag.java b/src/main/java/com/prgrms/prolog/domain/usertag/model/UserTag.java new file mode 100644 index 0000000..a14bf22 --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/usertag/model/UserTag.java @@ -0,0 +1,84 @@ +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.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@EqualsAndHashCode +@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; + if (count == 0) { + this.user.removeUserTag(this); + } + } + + 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; + } +} 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/config/SecurityConfig.java b/src/main/java/com/prgrms/prolog/global/config/SecurityConfig.java index da7c02c..c830202 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; 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/test/java/com/prgrms/prolog/domain/comment/service/CommentServiceImplTest.java b/src/test/java/com/prgrms/prolog/domain/comment/service/CommentServiceImplTest.java index bc53c75..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"); 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 873ad7e..38b3b5f 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 @@ -31,7 +31,7 @@ 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.post.service.PostServiceImpl; import com.prgrms.prolog.domain.user.repository.UserRepository; import com.prgrms.prolog.global.jwt.JwtTokenProvider; import com.prgrms.prolog.global.jwt.JwtTokenProvider.Claims; @@ -52,7 +52,7 @@ class PostControllerTest { @Autowired private ObjectMapper objectMapper; @Autowired - private PostService postService; + private PostServiceImpl postService; @Autowired private UserRepository userRepository; Long postId; @@ -66,9 +66,8 @@ void setUp(WebApplicationContext webApplicationContext, userId = userRepository.save(USER).getId(); claims = Claims.from(userId, "ROLE_USER"); - CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", false); + CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", "#tag", false); postId = postService.save(createRequest, userId); - this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .apply(documentationConfiguration(restDocumentation)) .alwaysDo(restDocs) @@ -79,7 +78,7 @@ void setUp(WebApplicationContext webApplicationContext, @Test @DisplayName("게시물을 등록할 수 있다.") void save() throws Exception { - CreateRequest request = new CreateRequest("생성된 테스트 제목", "생성된 테스트 내용", true); + CreateRequest request = new CreateRequest("생성된 테스트 제목", "생성된 테스트 내용", "tag", true); mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts") .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) @@ -90,6 +89,7 @@ void save() throws Exception { 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") ), responseBody() @@ -117,6 +117,7 @@ void findAll() throws Exception { 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") ))); @@ -140,6 +141,7 @@ void findById() throws Exception { 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") ))); @@ -148,7 +150,7 @@ void findById() throws Exception { @Test @DisplayName("게시물 아이디로 게시물을 수정할 수 있다.") void update() throws Exception { - UpdateRequest request = new UpdateRequest("수정된 테스트 제목", "수정된 테스트 내용", true); + UpdateRequest request = new UpdateRequest("수정된 테스트 제목", "수정된 테스트 내용", "", true); mockMvc.perform(RestDocumentationRequestBuilders.patch("/api/v1/posts/{id}", postId) .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) @@ -159,6 +161,7 @@ void update() throws Exception { 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( @@ -171,6 +174,7 @@ void update() throws Exception { 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") ) @@ -190,7 +194,7 @@ void remove() throws Exception { @Test @DisplayName("게시물 작성 중 제목이 공백인 경우 에러가 발생해야한다.") void isValidateTitleNull() throws Exception { - CreateRequest createRequest = new CreateRequest("", "테스트 게시물 내용", true); + CreateRequest createRequest = new CreateRequest("", "테스트 게시물 내용", "#tag", true); String requestJsonString = objectMapper.writeValueAsString(createRequest); @@ -204,7 +208,7 @@ void isValidateTitleNull() throws Exception { @Test @DisplayName("게시물 작성 중 내용이 빈칸인 경우 에러가 발생해야한다.") void isValidateContentEmpty() throws Exception { - CreateRequest createRequest = new CreateRequest("테스트 게시물 제목", " ", true); + CreateRequest createRequest = new CreateRequest("테스트 게시물 제목", " ", "#tag", true); String requestJsonString = objectMapper.writeValueAsString(createRequest); @@ -220,7 +224,7 @@ void isValidateContentEmpty() throws Exception { void isValidateTitleSizeOver() throws Exception { CreateRequest createRequest = new CreateRequest( "안녕하세요. 여기는 프로그래머스 기술 블로그 prolog입니다. 이곳에 글을 작성하기 위해서는 제목은 50글자 미만이어야합니다.", - "null 게시물 내용", + "null 게시물 내용", "#tag", true); String requestJsonString = objectMapper.writeValueAsString(createRequest); 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 56eeec0..2eaea18 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 @@ -3,7 +3,9 @@ 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,12 +13,10 @@ 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; -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.dto.PostResponse; @@ -28,11 +28,10 @@ @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @SpringBootTest @Transactional -@Import(TestContainerConfig.class) class PostServiceTest { @Autowired - private PostService postService; + PostServiceImpl postService; @Autowired UserRepository userRepository; @@ -58,20 +57,83 @@ void setData() { @Test @DisplayName("게시물을 등록할 수 있다.") void save_success() { - CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", true); + final CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", "#테스트", true); Long savePostId = postService.save(postRequest, user.getId()); assertThat(savePostId).isNotNull(); } + @Test + @DisplayName("게시글에 태그 없이 등록할 수 있다.") + void savePostAndWithOutAnyTagTest() { + // given + final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", null, true); + + // 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); + + // 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); + 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); + + // 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("테스트")); + } + @Test @DisplayName("존재하지 않는 사용자(비회원)의 이메일로 게시물을 등록할 수 없다.") void save_fail() { - String notExistEmail = "no_email@test.com"; - - CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", true); + CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", "#테스트", true); - assertThatThrownBy(() -> postService.save(postRequest, USER_ID)) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> postService.save(postRequest, UNSAVED_USER_ID)) + .isInstanceOf(NullPointerException.class); } @Test @@ -93,22 +155,23 @@ void findById_fail() { } @Test - @DisplayName("존재하는 게시물의 아이디로 게시물을 수정할 수 있다.") + @DisplayName("존재하는 게시물의 아이디로 게시물의 제목, 내용, 태그, 공개범위를 수정할 수 있다.") void update_success() { - UpdateRequest updateRequest = new UpdateRequest("수정된 테스트", "수정된 테스트 내용", true); + UpdateRequest updateRequest = new UpdateRequest("수정된 테스트", "수정된 테스트 내용", "#수정된 태그", true); - PostResponse update = postService.update(post.getId(), updateRequest); + PostResponse update = postService.update(updateRequest, user.getId(), post.getId()); - assertThat(update.title()).isEqualTo("수정된 테스트"); - assertThat(update.content()).isEqualTo("수정된 테스트 내용"); + assertThat(update) + .hasFieldOrPropertyWithValue("title", updateRequest.title()) + .hasFieldOrPropertyWithValue("content", updateRequest.content()); } @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); } 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/user/Repository/UserRepositoryTest.java b/src/test/java/com/prgrms/prolog/domain/user/Repository/UserRepositoryTest.java index a456bae..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 @@ -57,4 +57,13 @@ void findFailTest() { // then assertThat(foundUser).isNotPresent(); } + + @Test + @DisplayName("유저와 유저 태그를 조인하여 조회할 수 있다.") + void joinUserTagFindByEmailTest() { + // given & when + User findUser = userRepository.joinUserTagFindByUserId(savedUser.getId()); + // then + assertThat(findUser).isNotNull(); + } } 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/utils/TestUtils.java b/src/test/java/com/prgrms/prolog/utils/TestUtils.java index e37465a..0a76da4 100644 --- a/src/test/java/com/prgrms/prolog/utils/TestUtils.java +++ b/src/test/java/com/prgrms/prolog/utils/TestUtils.java @@ -2,8 +2,10 @@ import com.prgrms.prolog.domain.comment.model.Comment; import com.prgrms.prolog.domain.post.model.Post; -import com.prgrms.prolog.domain.user.dto.UserDto.UserProfile; +import com.prgrms.prolog.domain.posttag.model.PostTag; +import com.prgrms.prolog.domain.roottag.model.RootTag; 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; public class TestUtils { @@ -27,6 +29,11 @@ public class TestUtils { public static final String TITLE = "제목을 입력해주세요"; public static final String CONTENT = "내용을 입력해주세요"; public static final String USER_ROLE = "ROLE_USER"; + // 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); @@ -89,4 +96,17 @@ public static UserProfile getUserProfile() { .build(); } + 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(); + } + } diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql index 5bbba69..62f6f7e 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -1,11 +1,14 @@ -- V1__init.sql --- create database if not exists test; --- use test; +# create database if not exists prolog; +# use prolog; 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; +DROP TABLE IF EXISTS user_tag; +DROP TABLE IF EXISTS post_tag; +DROP TABLE IF EXISTS root_tag; CREATE TABLE users ( @@ -78,4 +81,29 @@ CREATE TABLE comment 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) -); \ No newline at end of file +); + +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) +) From 594c4f412521b1c8acf6c90dff3c297e29c61028 Mon Sep 17 00:00:00 2001 From: Fortune00 <53924962+Sinyoung3016@users.noreply.github.com> Date: Thu, 26 Jan 2023 13:47:45 +0900 Subject: [PATCH 06/19] =?UTF-8?q?[#65]=20Nginx=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20=EB=AC=B4=EC=A4=91=EB=8B=A8=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20(#76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: nginx 설정 및 적용 * add: blue-green 무중단 배포 적용 - bash deploy.sh로 실행 * update: 배포 워크 플로우 수정 --- .github/workflows/docker-push-and-aws-run.yml | 9 +---- deploy.sh | 31 +++++++++++++++ docker-compose.yml | 38 +++++++++++++++---- nginx/default.conf | 11 ++++++ 4 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 deploy.sh create mode 100644 nginx/default.conf diff --git a/.github/workflows/docker-push-and-aws-run.yml b/.github/workflows/docker-push-and-aws-run.yml index fa3f0e7..e9fa9d0 100644 --- a/.github/workflows/docker-push-and-aws-run.yml +++ b/.github/workflows/docker-push-and-aws-run.yml @@ -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/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 5bea43b..51a6615 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,13 +19,29 @@ 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} + green: + container_name: green + image: fortune00/prolog + expose: + - "8080" working_dir: /app depends_on: - db 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 From cb56e4a4c444757c73591f6da7000d46c5568a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=EC=A3=BC=EC=84=B1?= <99165624+JoosungKwon@users.noreply.github.com> Date: Thu, 26 Jan 2023 13:49:13 +0900 Subject: [PATCH 07/19] =?UTF-8?q?[#77]=20=ED=83=9C=EA=B7=B8=20=EB=B0=8F=20?= =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prolog/domain/posttag/model/PostTag.java | 2 -- .../prolog/domain/roottag/model/RootTag.java | 2 -- .../prolog/domain/usertag/model/UserTag.java | 2 -- src/main/resources/application-db.yml | 18 +++++++++++++----- src/main/resources/application-security.yml | 2 +- 5 files changed, 14 insertions(+), 12 deletions(-) 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 index a4e8abc..4272988 100644 --- a/src/main/java/com/prgrms/prolog/domain/posttag/model/PostTag.java +++ b/src/main/java/com/prgrms/prolog/domain/posttag/model/PostTag.java @@ -16,12 +16,10 @@ import lombok.AccessLevel; import lombok.Builder; -import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Getter -@EqualsAndHashCode @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class PostTag { 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 index 4243f54..600cd06 100644 --- a/src/main/java/com/prgrms/prolog/domain/roottag/model/RootTag.java +++ b/src/main/java/com/prgrms/prolog/domain/roottag/model/RootTag.java @@ -18,12 +18,10 @@ import lombok.AccessLevel; import lombok.Builder; -import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Getter -@EqualsAndHashCode @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class RootTag { 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 index a14bf22..2af37cc 100644 --- a/src/main/java/com/prgrms/prolog/domain/usertag/model/UserTag.java +++ b/src/main/java/com/prgrms/prolog/domain/usertag/model/UserTag.java @@ -17,12 +17,10 @@ import lombok.AccessLevel; import lombok.Builder; -import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Getter -@EqualsAndHashCode @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class UserTag { diff --git a/src/main/resources/application-db.yml b/src/main/resources/application-db.yml index eb0ad66..26c0a52 100644 --- a/src/main/resources/application-db.yml +++ b/src/main/resources/application-db.yml @@ -1,6 +1,7 @@ # default spring: + # database 설정 datasource: url: ${SPRING_DATASOURCE_URL} username: ${SPRING_DATASOURCE_USERNAME} @@ -25,32 +26,39 @@ spring: dialect: org.hibernate.dialect.MySQL8Dialect format_sql: true + # flyway 설정 + flyway: + enabled: true + baseline-on-migrate: true + + # 커넥션 풀 설정 messages: encoding: UTF-8 basename: messages/exceptions/exception, messages/logs/log-form +# SQL 로그 설정 +logging: + level: + org.hibernate.SQL: debug + org.hibernate.type: trace # 파라미터 값 + --- # local spring: config: activate.on-profile: "db-local" import: optional:file:.env[.properties] -# SQL 로그 설정 logging: level: org.hibernate.SQL: debug org.hibernate.type: trace # 파라미터 값 - flyway: - enabled: false - --- # prod spring: config: activate.on-profile: "db-prod" import: optional:file:.env[.properties] - # 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 bb0d63f..0a97d0c 100644 --- a/src/main/resources/application-security.yml +++ b/src/main/resources/application-security.yml @@ -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 From 4e83181bed6d34acfee84537e73aadca1cbdb11c Mon Sep 17 00:00:00 2001 From: hyunseo <82203978+hyena0608@users.noreply.github.com> Date: Thu, 26 Jan 2023 13:50:59 +0900 Subject: [PATCH 08/19] =?UTF-8?q?[#79]=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=ED=83=9C=EA=B7=B8=20=EC=88=98=EC=A0=95=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20(#80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 엔티티 수정 - Post 엔티티 수정 - User 엔티티 수정 - PostTag 엔티티 수정 - RootTag 엔티티 수정 - UserTag 엔티티 수정 * refactor: 게시글 수정 메서드 리팩터링 - 게시글 수정시 태그 업데이트 메서드 리팩터링 - 새로운 RootTag 존재시 저장한다. - 없어진 PostTag를 삭제한다. - UserTag 카운트를 줄인다. - UserTag 카운트가 0일시에 삭제한다. - 새로운 PostTag를 저장한다. - UserTag를 새로 저장하거나 카운트를 증가한다. * test: 게시글 수정 테스트 업데이트 * fix: 게시글 태그 삭제 기능 어노테이션 수정 --- .../prgrms/prolog/domain/post/model/Post.java | 5 +-- .../domain/post/service/PostService.java | 10 ++--- .../domain/post/service/PostServiceImpl.java | 37 ++++++++++++------- .../posttag/repository/PostTagRepository.java | 8 ++-- .../prgrms/prolog/domain/user/model/User.java | 8 +--- .../prolog/domain/usertag/model/UserTag.java | 7 ++-- .../domain/post/service/PostServiceTest.java | 14 ++++--- 7 files changed, 50 insertions(+), 39 deletions(-) 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 0146c26..8737f15 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 @@ -1,6 +1,5 @@ package com.prgrms.prolog.domain.post.model; -import static javax.persistence.CascadeType.*; import static javax.persistence.FetchType.*; import static javax.persistence.GenerationType.*; @@ -61,7 +60,7 @@ public class Post extends BaseEntity { @OneToMany(mappedBy = "post") private final List comments = new ArrayList<>(); - @OneToMany(mappedBy = "post", cascade = ALL) + @OneToMany(mappedBy = "post") private final Set postTags = new HashSet<>(); @Builder @@ -101,7 +100,7 @@ public void changePost(UpdateRequest updateRequest) { this.openStatus = updateRequest.openStatus(); } - public void addPostTagsFrom(List postTags) { + public void addPostTagsFrom(Set postTags) { this.postTags.addAll(postTags); } 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 175c166..c7f9ff9 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 @@ -7,9 +7,9 @@ import com.prgrms.prolog.domain.post.dto.PostResponse; public interface PostService { - public Long save(PostRequest.CreateRequest request, Long userId); - public PostResponse findById(Long postId); - public Page findAll(Pageable pageable); - public PostResponse update(PostRequest.UpdateRequest update, Long userId, Long postId); - public void delete(Long id); + Long save(PostRequest.CreateRequest request, Long userId); + PostResponse findById(Long postId); + Page findAll(Pageable pageable); + PostResponse update(PostRequest.UpdateRequest update, Long userId, Long postId); + 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 index cf379cd..b1c8ae2 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java +++ b/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java @@ -57,7 +57,7 @@ public Long save(CreateRequest request, Long userId) { public PostResponse findById(Long postId) { Post post = postRepository.joinCommentFindById(postId) .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); - List findPostTags = postTagRepository.joinPostTagFindByPostId(postId); + Set findPostTags = postTagRepository.joinRootTagFindByPostId(postId); post.addPostTagsFrom(findPostTags); return PostResponse.toPostResponse(post); } @@ -73,20 +73,23 @@ public Page findAll(Pageable pageable) { public PostResponse update(UpdateRequest update, Long userId, Long postId) { Post findPost = postRepository.joinUserFindById(postId) .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); - + if (!findPost.getUser().checkSameUserId(userId)) { - throw new IllegalArgumentException("exception.post.not.owner"); + 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 id) { - Post findPost = postRepository.findById(id) + public void delete(Long postId) { + Post findPost = postRepository.findById(postId) .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); postRepository.delete(findPost); } @@ -95,12 +98,13 @@ private void updatePostAndUserIfTagChanged(String tagText, Post findPost) { Set tagNames = TagConverter.convertFrom(tagText); Set currentRootTags = rootTagRepository.findByTagNamesIn(tagNames); Set newTagNames = distinguishNewTagNames(tagNames, currentRootTags); - Set oldRootTags = distinguishOldRootTags(tagNames, findPost.getPostTags()); Set savedNewRootTags = saveNewRootTags(newTagNames); - currentRootTags.addAll(savedNewRootTags); + saveNewPostTags(findPost, savedNewRootTags); + saveOrIncreaseUserTags(findPost.getUser(), savedNewRootTags); + Set findPostTags = postTagRepository.joinRootTagFindByPostId(findPost.getId()); + Set oldRootTags = distinguishOldRootTags(tagNames, findPostTags); removeOldPostTags(findPost, oldRootTags); - saveNewPostTags(findPost, savedNewRootTags); removeOrDecreaseUserTags(findPost.getUser(), oldRootTags); } @@ -108,17 +112,23 @@ private void removeOldPostTags(Post post, Set oldRootTags) { if (oldRootTags.isEmpty()) { return; } - List rootTagIds = oldRootTags.stream() + Set rootTagIds = oldRootTags.stream() .map(RootTag::getId) - .toList(); + .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())) { - userTagMap.get(rootTag.getId()).decreaseCount(1); + if (!userTagMap.containsKey(rootTag.getId())) { + continue; + } + + UserTag currentUserTag = userTagMap.get(rootTag.getId()); + currentUserTag.decreaseCount(1); + if (currentUserTag.isCountZero()) { + userTagRepository.deleteById(currentUserTag.getId()); } } } @@ -183,7 +193,8 @@ private void saveNewPostTags(Post post, Set rootTags) { .build())); } - private Set distinguishNewTagNames(Set newTagNames, Set rootTags) { + private Set distinguishNewTagNames(Set tagNames, Set rootTags) { + Set newTagNames = new HashSet<>(tagNames); for (RootTag rootTag : rootTags) { newTagNames.remove(rootTag.getName()); } 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 index ca27fd3..c42fd74 100644 --- a/src/main/java/com/prgrms/prolog/domain/posttag/repository/PostTagRepository.java +++ b/src/main/java/com/prgrms/prolog/domain/posttag/repository/PostTagRepository.java @@ -1,8 +1,9 @@ package com.prgrms.prolog.domain.posttag.repository; -import java.util.List; +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; @@ -10,6 +11,7 @@ public interface PostTagRepository extends JpaRepository { + @Modifying @Query(""" DELETE FROM PostTag pt @@ -18,7 +20,7 @@ public interface PostTagRepository extends JpaRepository { """) void deleteByPostIdAndRootTagIds( @Param(value = "postId") Long postId, - @Param(value = "rootTagIds") List rootTagIds + @Param(value = "rootTagIds") Set rootTagIds ); @Query(""" @@ -27,5 +29,5 @@ void deleteByPostIdAndRootTagIds( LEFT JOIN FETCH pt.rootTag WHERE pt.post.id = :postId """) - List joinPostTagFindByPostId(@Param(value = "postId") Long postId); + Set joinRootTagFindByPostId(@Param(value = "postId") Long postId); } 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 0d1945d..e501982 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,7 +1,5 @@ package com.prgrms.prolog.domain.user.model; -import static javax.persistence.CascadeType.*; - import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -54,7 +52,7 @@ public class User extends BaseEntity { private final List posts = new ArrayList<>(); @OneToMany(mappedBy = "user") private final List comments = new ArrayList<>(); - @OneToMany(mappedBy = "user", cascade = ALL) + @OneToMany(mappedBy = "user") private final Set userTags = new HashSet<>(); @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -145,10 +143,6 @@ public boolean checkSameEmail(String email) { return this.email.equals(email); } - public void removeUserTag(UserTag userTag) { - this.userTags.remove(userTag); - } - public boolean checkSameUserId(Long userId) { return Objects.equals(this.id, userId); } 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 index 2af37cc..e3121fe 100644 --- a/src/main/java/com/prgrms/prolog/domain/usertag/model/UserTag.java +++ b/src/main/java/com/prgrms/prolog/domain/usertag/model/UserTag.java @@ -60,9 +60,6 @@ public void increaseCount(int count) { public void decreaseCount(int count) { Assert.isTrue(count >= 0, "exception.userTag.count.positive"); this.count -= count; - if (count == 0) { - this.user.removeUserTag(this); - } } private RootTag validateRootTag(RootTag rootTag) { @@ -79,4 +76,8 @@ 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/test/java/com/prgrms/prolog/domain/post/service/PostServiceTest.java b/src/test/java/com/prgrms/prolog/domain/post/service/PostServiceTest.java index 2eaea18..9f1a49e 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 @@ -31,7 +31,7 @@ class PostServiceTest { @Autowired - PostServiceImpl postService; + PostService postService; @Autowired UserRepository userRepository; @@ -105,6 +105,7 @@ void savePostAndTagsTest() { Set findTags = findPostResponse.tags(); // then + System.out.println(findTags); assertThat(findTags) .containsExactlyInAnyOrderElementsOf(expectedTags); } @@ -157,13 +158,16 @@ void findById_fail() { @Test @DisplayName("존재하는 게시물의 아이디로 게시물의 제목, 내용, 태그, 공개범위를 수정할 수 있다.") void update_success() { - UpdateRequest updateRequest = new UpdateRequest("수정된 테스트", "수정된 테스트 내용", "#수정된 태그", true); + final CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", "#테스트#test#test1#테 스트", true); + Long savedPost = postService.save(createRequest, user.getId()); - PostResponse update = postService.update(updateRequest, user.getId(), post.getId()); + final UpdateRequest updateRequest = new UpdateRequest("수정된 테스트", "수정된 테스트 내용", "#테스트#수정된 태그", true); + PostResponse updatedPostResponse = postService.update(updateRequest, user.getId(), savedPost); - assertThat(update) + assertThat(updatedPostResponse) .hasFieldOrPropertyWithValue("title", updateRequest.title()) - .hasFieldOrPropertyWithValue("content", updateRequest.content()); + .hasFieldOrPropertyWithValue("content", updateRequest.content()) + .hasFieldOrPropertyWithValue("tags", Set.of("테스트", "수정된 태그")); } @Test From 207f0809b5d05bf2124130c1416039ab50702bc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=EC=A3=BC=EC=84=B1?= <99165624+JoosungKwon@users.noreply.github.com> Date: Fri, 27 Jan 2023 15:17:08 +0900 Subject: [PATCH 09/19] =?UTF-8?q?[#61]=20Series=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#83)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Series 기능 추가 * update: JPQL 쿼리 변경 * update: 시리즈 기능 수정 * test: 시리즈 테스트 추가 * refactor: 시리즈 기능 리팩터링 * refactor: 시리즈 기능 테스트 수정 및 리팩터링 --- build.gradle | 11 +- .../prolog/domain/post/dto/PostInfo.java | 14 +++ .../prolog/domain/post/dto/PostRequest.java | 3 +- .../prolog/domain/post/dto/PostResponse.java | 3 + .../prgrms/prolog/domain/post/model/Post.java | 16 ++- .../post/repository/PostRepository.java | 2 + .../domain/post/service/PostServiceImpl.java | 22 ++++ .../domain/series/api/SeriesController.java | 32 +++++ .../series/dto/CreateSeriesRequest.java | 11 ++ .../domain/series/dto/SeriesResponse.java | 24 ++++ .../prolog/domain/series/model/Series.java | 87 ++++++++++++++ .../series/repository/SeriesRepository.java | 21 ++++ .../domain/series/service/SeriesService.java | 15 +++ .../series/service/SeriesServiceImpl.java | 52 +++++++++ .../prgrms/prolog/domain/user/model/User.java | 8 ++ .../prolog/global/common/IdResponse.java | 4 + .../domain/post/api/PostControllerTest.java | 70 ++++++++--- .../prolog/domain/post/model/PostTest.java | 10 +- .../post/repository/PostRepositoryTest.java | 2 +- .../domain/post/service/PostServiceTest.java | 30 +++-- .../series/api/SeriesControllerTest.java | 109 ++++++++++++++++++ .../domain/series/model/SeriesTest.java | 68 +++++++++++ .../repository/SeriesRepositoryTest.java | 65 +++++++++++ .../series/service/SeriesServiceImplTest.java | 106 +++++++++++++++++ .../domain/user/service/UserServiceTest.java | 2 +- .../com/prgrms/prolog/utils/TestUtils.java | 21 +++- 26 files changed, 762 insertions(+), 46 deletions(-) create mode 100644 src/main/java/com/prgrms/prolog/domain/post/dto/PostInfo.java create mode 100644 src/main/java/com/prgrms/prolog/domain/series/api/SeriesController.java create mode 100644 src/main/java/com/prgrms/prolog/domain/series/dto/CreateSeriesRequest.java create mode 100644 src/main/java/com/prgrms/prolog/domain/series/dto/SeriesResponse.java create mode 100644 src/main/java/com/prgrms/prolog/domain/series/model/Series.java create mode 100644 src/main/java/com/prgrms/prolog/domain/series/repository/SeriesRepository.java create mode 100644 src/main/java/com/prgrms/prolog/domain/series/service/SeriesService.java create mode 100644 src/main/java/com/prgrms/prolog/domain/series/service/SeriesServiceImpl.java create mode 100644 src/main/java/com/prgrms/prolog/global/common/IdResponse.java create mode 100644 src/test/java/com/prgrms/prolog/domain/series/api/SeriesControllerTest.java create mode 100644 src/test/java/com/prgrms/prolog/domain/series/model/SeriesTest.java create mode 100644 src/test/java/com/prgrms/prolog/domain/series/repository/SeriesRepositoryTest.java create mode 100644 src/test/java/com/prgrms/prolog/domain/series/service/SeriesServiceImplTest.java diff --git a/build.gradle b/build.gradle index 1e9f0de..61f82e2 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ plugins { } group = 'com.prgrms' -version = '1.0.0' +version = '1.0.2' sourceCompatibility = '17' configurations { @@ -45,6 +45,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // OAuth2-Client 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' @@ -52,9 +54,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' @@ -62,16 +61,14 @@ 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' } 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 5e3380e..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 @@ -12,7 +12,8 @@ public class PostRequest { public record CreateRequest(@NotBlank @Size(max = 200) String title, @NotBlank String content, @Nullable String tagText, - boolean openStatus) { + boolean openStatus, + @Nullable String seriesTitle) { public static Post toEntity(CreateRequest create, User user) { return Post.builder() .title(create.title) 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 2285f91..0a4dc6c 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 @@ -8,6 +8,7 @@ import com.prgrms.prolog.domain.comment.model.Comment; import com.prgrms.prolog.domain.post.model.Post; +import com.prgrms.prolog.domain.series.dto.SeriesResponse; import com.prgrms.prolog.domain.user.dto.UserDto.UserProfile; public record PostResponse(String title, @@ -15,6 +16,7 @@ public record PostResponse(String title, boolean openStatus, UserProfile user, Set tags, + SeriesResponse seriesResponse, List comment, int commentCount) { @@ -24,6 +26,7 @@ public static PostResponse toPostResponse(Post post) { post.isOpenStatus(), toUserProfile(post.getUser()), PostTagsResponse.from(post.getPostTags()).tagNames(), + SeriesResponse.toSeriesResponse(post.getSeries()), post.getComments(), post.getComments().size()); } 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 8737f15..3baeb4a 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 @@ -22,6 +22,7 @@ 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; @@ -63,12 +64,17 @@ public class Post extends BaseEntity { @OneToMany(mappedBy = "post") private final Set postTags = new HashSet<>(); + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "series_id") + private Series series; + @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) { @@ -125,4 +131,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 9cacba6..0778c93 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 @@ -5,9 +5,11 @@ 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.post.model.Post; +@Repository public interface PostRepository extends JpaRepository { @Query(""" 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 index b1c8ae2..b7f8bb6 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java +++ b/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java @@ -22,6 +22,8 @@ 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; @@ -37,12 +39,14 @@ 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) { @@ -50,9 +54,27 @@ public Long save(CreateRequest request, Long 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()) { + return; + } + Series series = seriesRepository + .findByIdAndTitle(owner.getId(), seriesTitle) + .orElseGet(() -> seriesRepository.save( + Series.builder() + .title(seriesTitle) + .user(owner) + .build() + ) + ); + post.setSeries(series); + } + @Override public PostResponse findById(Long postId) { Post post = postRepository.joinCommentFindById(postId) 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..395ceaa --- /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.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +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("/{title}") + ResponseEntity findSeriesByTitle( + @PathVariable 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/model/User.java b/src/main/java/com/prgrms/prolog/domain/user/model/User.java index e501982..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 @@ -9,6 +9,7 @@ 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; @@ -20,6 +21,7 @@ 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; @@ -54,6 +56,8 @@ public class User extends BaseEntity { 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; @@ -147,6 +151,10 @@ 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/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/test/java/com/prgrms/prolog/domain/post/api/PostControllerTest.java b/src/test/java/com/prgrms/prolog/domain/post/api/PostControllerTest.java index 38b3b5f..8bf7cba 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 @@ -32,6 +32,7 @@ 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.PostServiceImpl; +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; @@ -42,31 +43,30 @@ @Transactional class PostControllerTest { - private MockMvc mockMvc; - @Autowired private JwtTokenProvider jwtTokenProvider; - @Autowired - RestDocumentationResultHandler restDocs; + private RestDocumentationResultHandler restDocs; @Autowired private ObjectMapper objectMapper; @Autowired private PostServiceImpl postService; @Autowired private UserRepository userRepository; - Long postId; - Long userId; - Claims claims; + @Autowired + private SeriesRepository seriesRepository; + private MockMvc mockMvc; + private Long userId; + private Claims claims; + private Long postId; @BeforeEach void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { - userId = userRepository.save(USER).getId(); claims = Claims.from(userId, "ROLE_USER"); - CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", "#tag", false); + CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", "#tag", false, SERIES_TITLE); postId = postService.save(createRequest, userId); this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .apply(documentationConfiguration(restDocumentation)) @@ -78,7 +78,7 @@ void setUp(WebApplicationContext webApplicationContext, @Test @DisplayName("게시물을 등록할 수 있다.") void save() throws Exception { - CreateRequest request = new CreateRequest("생성된 테스트 제목", "생성된 테스트 내용", "tag", true); + CreateRequest request = new CreateRequest("생성된 테스트 제목", "생성된 테스트 내용", "tag", true, SERIES_TITLE); mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts") .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) @@ -90,7 +90,8 @@ void save() throws Exception { 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") + fieldWithPath("openStatus").type(JsonFieldType.BOOLEAN).description("openStatus"), + fieldWithPath("seriesTitle").type(JsonFieldType.STRING).description("seriesTitle") ), responseBody() )); @@ -119,7 +120,13 @@ void findAll() throws Exception { 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") ))); } @@ -143,7 +150,13 @@ void findById() throws Exception { 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") ))); } @@ -176,7 +189,13 @@ void update() throws Exception { 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") ) )); } @@ -194,7 +213,7 @@ void remove() throws Exception { @Test @DisplayName("게시물 작성 중 제목이 공백인 경우 에러가 발생해야한다.") void isValidateTitleNull() throws Exception { - CreateRequest createRequest = new CreateRequest("", "테스트 게시물 내용", "#tag", true); + CreateRequest createRequest = new CreateRequest("", "테스트 게시물 내용", "#tag", true, SERIES_TITLE); String requestJsonString = objectMapper.writeValueAsString(createRequest); @@ -208,7 +227,7 @@ void isValidateTitleNull() throws Exception { @Test @DisplayName("게시물 작성 중 내용이 빈칸인 경우 에러가 발생해야한다.") void isValidateContentEmpty() throws Exception { - CreateRequest createRequest = new CreateRequest("테스트 게시물 제목", " ", "#tag", true); + CreateRequest createRequest = new CreateRequest("테스트 게시물 제목", " ", "#tag", true, SERIES_TITLE); String requestJsonString = objectMapper.writeValueAsString(createRequest); @@ -225,7 +244,7 @@ void isValidateTitleSizeOver() throws Exception { CreateRequest createRequest = new CreateRequest( "안녕하세요. 여기는 프로그래머스 기술 블로그 prolog입니다. 이곳에 글을 작성하기 위해서는 제목은 50글자 미만이어야합니다.", "null 게시물 내용", "#tag", - true); + true, SERIES_TITLE); String requestJsonString = objectMapper.writeValueAsString(createRequest); @@ -235,4 +254,21 @@ void isValidateTitleSizeOver() throws Exception { .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 a5b293c..2a97cc3 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 @@ -78,7 +78,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 9f1a49e..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,5 +1,6 @@ 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.*; @@ -22,6 +23,8 @@ 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; @@ -39,25 +42,32 @@ 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() { - final CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", "#테스트", true); + final CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", "#테스트", true, SERIES_TITLE); Long savePostId = postService.save(postRequest, user.getId()); assertThat(savePostId).isNotNull(); } @@ -66,7 +76,7 @@ void save_success() { @DisplayName("게시글에 태그 없이 등록할 수 있다.") void savePostAndWithOutAnyTagTest() { // given - final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", null, true); + final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", null, true, SERIES_TITLE); // when Long savedPostId = postService.save(request, user.getId()); @@ -81,7 +91,7 @@ void savePostAndWithOutAnyTagTest() { @DisplayName("게시글에 태그가 공백이거나 빈 칸이라면 태그는 무시된다.") void savePostWithBlankTagTest() { // given - final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", "# #", true); + final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", "# #", true, SERIES_TITLE); // when Long savedPostId = postService.save(request, user.getId()); @@ -96,7 +106,7 @@ void savePostWithBlankTagTest() { @DisplayName("게시글에 복수의 태그를 등록할 수 있다.") void savePostAndTagsTest() { // given - final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", "#테스트#test#test1#테 스트", true); + final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", "#테스트#test#test1#테 스트", true, SERIES_TITLE); final List expectedTags = List.of("테스트", "test", "test1", "테 스트"); // when @@ -105,7 +115,6 @@ void savePostAndTagsTest() { Set findTags = findPostResponse.tags(); // then - System.out.println(findTags); assertThat(findTags) .containsExactlyInAnyOrderElementsOf(expectedTags); } @@ -114,7 +123,7 @@ void savePostAndTagsTest() { @DisplayName("게시물과 태그를 조회할 수 있다.") void findPostAndTagsTest() { // given - final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", "#테스트", true); + final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", "#테스트", true, SERIES_TITLE); // when Long savedPostId = postService.save(request, user.getId()); @@ -125,13 +134,14 @@ void findPostAndTagsTest() { .hasFieldOrPropertyWithValue("title", request.title()) .hasFieldOrPropertyWithValue("content", request.content()) .hasFieldOrPropertyWithValue("openStatus", request.openStatus()) - .hasFieldOrPropertyWithValue("tags", Set.of("테스트")); + .hasFieldOrPropertyWithValue("tags", Set.of("테스트")) + .hasFieldOrPropertyWithValue("seriesResponse", toSeriesResponse(savedSeries)); } @Test @DisplayName("존재하지 않는 사용자(비회원)의 이메일로 게시물을 등록할 수 없다.") void save_fail() { - CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", "#테스트", true); + CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", "#테스트", true, SERIES_TITLE); assertThatThrownBy(() -> postService.save(postRequest, UNSAVED_USER_ID)) .isInstanceOf(NullPointerException.class); @@ -158,7 +168,7 @@ void findById_fail() { @Test @DisplayName("존재하는 게시물의 아이디로 게시물의 제목, 내용, 태그, 공개범위를 수정할 수 있다.") void update_success() { - final CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", "#테스트#test#test1#테 스트", true); + final CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", "#테스트#test#test1#테 스트", true,SERIES_TITLE); Long savedPost = postService.save(createRequest, user.getId()); final UpdateRequest updateRequest = new UpdateRequest("수정된 테스트", "수정된 테스트 내용", "#테스트#수정된 태그", true); @@ -186,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/series/api/SeriesControllerTest.java b/src/test/java/com/prgrms/prolog/domain/series/api/SeriesControllerTest.java new file mode 100644 index 0000000..ca14c6c --- /dev/null +++ b/src/test/java/com/prgrms/prolog/domain/series/api/SeriesControllerTest.java @@ -0,0 +1,109 @@ +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/{title}", SERIES_TITLE) + .header(AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) + ) + // 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/service/UserServiceTest.java b/src/test/java/com/prgrms/prolog/domain/user/service/UserServiceTest.java index b6707fe..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 @@ -30,7 +30,7 @@ class UserServiceTest { private User userMock; @InjectMocks - private UserServiceImpl userService; // 빈으로 등록해서 주입 받고 싶으면 어떻게 해야하나요? 구현체말고 인터페이스를 주입 받고 싶습니다! + private UserServiceImpl userService; @Nested @DisplayName("사용자 조회 #10") diff --git a/src/test/java/com/prgrms/prolog/utils/TestUtils.java b/src/test/java/com/prgrms/prolog/utils/TestUtils.java index 0a76da4..8f1f27d 100644 --- a/src/test/java/com/prgrms/prolog/utils/TestUtils.java +++ b/src/test/java/com/prgrms/prolog/utils/TestUtils.java @@ -4,6 +4,7 @@ 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; @@ -25,9 +26,14 @@ public class TestUtils { public static final UserInfo USER_INFO = getUserInfo(); public static final UserProfile USER_PROFILE = getUserProfile(); public static final Comment COMMENT = getComment(); + public static final Series SERIES = getSeries(); // Post & Comment Data public static final String TITLE = "제목을 입력해주세요"; public static final String CONTENT = "내용을 입력해주세요"; + public static final String COMMENT_CONTENT = "댓글 내용"; + public static final String POST_TITLE = "게시글 제목"; + public static final String POST_CONTENT = "게시글 내용"; + public static final String SERIES_TITLE = "시리즈 제목"; public static final String USER_ROLE = "ROLE_USER"; // RootTag & PostTag Data public static final String ROOT_TAG_NAME = "머쓱 태그"; @@ -42,6 +48,7 @@ public class TestUtils { // Authentication public static final String BEARER_TYPE = "Bearer "; + private TestUtils() { /* no-op */ } @@ -60,8 +67,8 @@ public static User getUser() { public static Post getPost() { return Post.builder() - .title("제목") - .content("내용") + .title(POST_TITLE) + .content(POST_CONTENT) .openStatus(true) .user(USER) .build(); @@ -69,7 +76,7 @@ public static Post getPost() { public static Comment getComment() { return Comment.builder() - .content("내용") + .content(COMMENT_CONTENT) .post(POST) .user(USER) .build(); @@ -109,4 +116,12 @@ public static PostTag getPostTag() { .build(); } + public static Series getSeries() { + return Series.builder() + .title(SERIES_TITLE) + .user(USER) + .post(POST) + .build(); + } + } From c4e8d831346c36ac2e095d57c76a5dba7cbca4d0 Mon Sep 17 00:00:00 2001 From: hyunseo <82203978+hyena0608@users.noreply.github.com> Date: Fri, 27 Jan 2023 15:47:15 +0900 Subject: [PATCH 10/19] =?UTF-8?q?[#81]=20=EB=A1=9C=EA=B7=B8=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=84=A4=EC=A0=95,=20log4jdbc=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20(#84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: log4jdbc gradle 의존 추가 * refactor: 전역 예외 처리 로그 레벨 별 메서드 리팩터링 * add: logback, log4jdbc 설정 추가 - info file appender - warn file appender - error file appender - console appender - sql file appender - log4jdbc properties - logback --- build.gradle | 2 + .../exception/GlobalExceptionHandler.java | 6 +- .../resources/appender/console-appender.xml | 7 +++ .../appender/error-file-appender.xml | 22 ++++++++ .../resources/appender/info-file-appender.xml | 17 ++++++ .../resources/appender/sql-file-appender.xml | 18 ++++++ .../resources/appender/warn-file-appender.xml | 22 ++++++++ src/main/resources/application-db.yml | 11 +--- src/main/resources/log4jdbc.log4j2.properties | 2 + src/main/resources/logback.xml | 56 +++++++++++++++---- 10 files changed, 137 insertions(+), 26 deletions(-) create mode 100644 src/main/resources/appender/console-appender.xml create mode 100644 src/main/resources/appender/error-file-appender.xml create mode 100644 src/main/resources/appender/info-file-appender.xml create mode 100644 src/main/resources/appender/sql-file-appender.xml create mode 100644 src/main/resources/appender/warn-file-appender.xml create mode 100644 src/main/resources/log4jdbc.log4j2.properties diff --git a/build.gradle b/build.gradle index 61f82e2..bc3eb5d 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,8 @@ dependencies { // JWT dependency implementation group: 'com.auth0', name: 'java-jwt', version: '4.2.1' + // Log4jdbc + implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16' } dependencyManagement { 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/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-db.yml b/src/main/resources/application-db.yml index 26c0a52..865f609 100644 --- a/src/main/resources/application-db.yml +++ b/src/main/resources/application-db.yml @@ -3,10 +3,10 @@ spring: # database 설정 datasource: + driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy url: ${SPRING_DATASOURCE_URL} username: ${SPRING_DATASOURCE_USERNAME} password: ${SPRING_DATASOURCE_PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver # 커넥션 풀 설정 hikari: @@ -37,10 +37,6 @@ spring: basename: messages/exceptions/exception, messages/logs/log-form # SQL 로그 설정 -logging: - level: - org.hibernate.SQL: debug - org.hibernate.type: trace # 파라미터 값 --- # local spring: @@ -48,11 +44,6 @@ spring: activate.on-profile: "db-local" import: optional:file:.env[.properties] -logging: - level: - org.hibernate.SQL: debug - org.hibernate.type: trace # 파라미터 값 - --- # prod spring: config: 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..15e9e5c 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 57b383013a178f4898f4dd38740a5a06e0e1243b Mon Sep 17 00:00:00 2001 From: Fortune00 <53924962+Sinyoung3016@users.noreply.github.com> Date: Fri, 27 Jan 2023 15:48:59 +0900 Subject: [PATCH 11/19] =?UTF-8?q?[#62]=20=EC=B5=9C=EA=B7=BC=20=ED=8F=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EA=B0=9C=EB=B0=9C=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/prgrms/prolog/domain/post/api/PostController.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 8b23f65..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; @@ -52,7 +54,9 @@ public ResponseEntity findById(@PathVariable Long id) { // 비공 } @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()); } From 9071e53c099730ae2a68ce089cda086cb9402f81 Mon Sep 17 00:00:00 2001 From: Fortune00 <53924962+Sinyoung3016@users.noreply.github.com> Date: Fri, 27 Jan 2023 16:14:59 +0900 Subject: [PATCH 12/19] =?UTF-8?q?fix:=20jar=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=B2=84=EC=A0=84=20=EC=9D=B4=EB=A6=84=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 76fd627..f743bde 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM openjdk:17.0.2 -ARG JAR_FILE=build/libs/prolog-1.0.0.jar +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} \ From 08dfa720b84e21999631e9b145c84dbfd228caaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=9D=80=EB=B9=84?= <59335077+hikarigin99@users.noreply.github.com> Date: Fri, 27 Jan 2023 17:42:06 +0900 Subject: [PATCH 13/19] =?UTF-8?q?[#63]=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1/=EC=B7=A8=EC=86=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=B0=8F=20=ED=95=B4=EB=8B=B9=20=EA=B2=8C=EC=8B=9C=EB=AC=BC?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EA=B0=9C=EC=88=98=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: 좋아요 도메인 - likes 테이블 생성 - userId와 postId FK * add: 좋아요 생성 및 취소 기능 * add: 게시물 좋아요 개수 증가/감소 기능 - JPQL UPDATE 사용을 위해 @Modifying * add: Like 관련 에러 메시지 및 테스트용 entity 생성 * add: 좋아요 생성 및 취소 API, LikeRequestDto * add: testUtils 수정으로 인한 Post 데이터 값 변경 * add: 좋아요 리포지토리와 서비스 테스트 - repository : 추가한 findByUserAndPost 메서드만 진행 - service : BDDMockito 테스트 * add: 좋아요 컨트롤러 테스트 및 Rest docs 생성 * add: build를 위한 test Lombok 제거 * add: 좋아요 생성 API 수정 및 LikeDto userId 변경 - 컨트롤러 API에 postId 추가 - @AuthenticationPrincipal 처리 --- .../domain/like/api/LikeController.java | 47 +++++++ .../prolog/domain/like/dto/LikeDto.java | 10 ++ .../prgrms/prolog/domain/like/model/Like.java | 44 ++++++ .../like/repository/LikeRepository.java | 15 ++ .../domain/like/service/LikeService.java | 9 ++ .../domain/like/service/LikeServiceImpl.java | 73 ++++++++++ .../prgrms/prolog/domain/post/model/Post.java | 10 +- .../post/repository/PostRepository.java | 9 ++ .../domain/post/service/PostService.java | 4 + .../domain/post/service/PostServiceImpl.java | 13 +- .../prolog/domain/user/dto/UserDto.java | 22 ++- .../db/migration/V2.2__add_like_table.sql | 12 ++ .../messages/exceptions/exception.properties | 7 +- .../domain/like/api/LikeControllerTest.java | 122 ++++++++++++++++ .../like/repository/LikeRepositoryTest.java | 63 +++++++++ .../domain/like/service/LikeServiceTest.java | 133 ++++++++++++++++++ .../domain/post/api/PostControllerTest.java | 19 +-- .../post/repository/PostRepositoryTest.java | 13 +- .../com/prgrms/prolog/utils/TestUtils.java | 37 +++-- src/test/resources/schema.sql | 46 +++--- 20 files changed, 650 insertions(+), 58 deletions(-) create mode 100644 src/main/java/com/prgrms/prolog/domain/like/api/LikeController.java create mode 100644 src/main/java/com/prgrms/prolog/domain/like/dto/LikeDto.java create mode 100644 src/main/java/com/prgrms/prolog/domain/like/model/Like.java create mode 100644 src/main/java/com/prgrms/prolog/domain/like/repository/LikeRepository.java create mode 100644 src/main/java/com/prgrms/prolog/domain/like/service/LikeService.java create mode 100644 src/main/java/com/prgrms/prolog/domain/like/service/LikeServiceImpl.java create mode 100644 src/main/resources/db/migration/V2.2__add_like_table.sql create mode 100644 src/test/java/com/prgrms/prolog/domain/like/api/LikeControllerTest.java create mode 100644 src/test/java/com/prgrms/prolog/domain/like/repository/LikeRepositoryTest.java create mode 100644 src/test/java/com/prgrms/prolog/domain/like/service/LikeServiceTest.java 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..ee3e3ad --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/like/api/LikeController.java @@ -0,0 +1,47 @@ +package com.prgrms.prolog.domain.like.api; + +import java.net.URI; + +import javax.validation.Valid; + +import org.springframework.http.ResponseEntity; +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.RestController; +import org.springframework.web.util.UriComponentsBuilder; + +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}") + public ResponseEntity insert( + @PathVariable Long postId, + @AuthenticationPrincipal JwtAuthentication user + ) { + LikeDto.likeRequest request = new likeRequest(user.id(), postId); + Long likeId = likeService.save(request); + URI location = UriComponentsBuilder.fromUriString("/api/v1/like/" + likeId).build().toUri(); + return ResponseEntity.created(location).build(); + } + + @DeleteMapping + public ResponseEntity delete(@RequestBody @Valid likeRequest likeRequest) { + likeService.cancel(likeRequest); + return ResponseEntity.noContent().build(); + } +} 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/model/Post.java b/src/main/java/com/prgrms/prolog/domain/post/model/Post.java index 3baeb4a..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 @@ -9,6 +9,7 @@ import java.util.Objects; import java.util.Set; +import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @@ -17,6 +18,7 @@ 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; @@ -39,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; @@ -68,6 +66,10 @@ public class Post extends BaseEntity { @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, Series series) { this.title = validateTitle(title); 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 0778c93..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 @@ -3,6 +3,7 @@ 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; @@ -27,4 +28,12 @@ public interface PostRepository extends JpaRepository { 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 c7f9ff9..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 @@ -8,8 +8,12 @@ public interface PostService { Long save(PostRequest.CreateRequest request, Long userId); + PostResponse findById(Long postId); + Page findAll(Pageable pageable); + PostResponse update(PostRequest.UpdateRequest update, Long userId, Long postId); + 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 index b7f8bb6..790a1f6 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java +++ b/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java @@ -46,7 +46,6 @@ public class PostServiceImpl implements PostService { private final PostTagRepository postTagRepository; private final UserTagRepository userTagRepository; - @Override @Transactional public Long save(CreateRequest request, Long userId) { @@ -66,10 +65,10 @@ private void registerSeries(CreateRequest request, Post post, User owner) { Series series = seriesRepository .findByIdAndTitle(owner.getId(), seriesTitle) .orElseGet(() -> seriesRepository.save( - Series.builder() - .title(seriesTitle) - .user(owner) - .build() + Series.builder() + .title(seriesTitle) + .user(owner) + .build() ) ); post.setSeries(series); @@ -78,7 +77,7 @@ private void registerSeries(CreateRequest request, Post post, User owner) { @Override public PostResponse findById(Long postId) { Post post = postRepository.joinCommentFindById(postId) - .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); + .orElseThrow(() -> new IllegalArgumentException("exception.post.notExists")); Set findPostTags = postTagRepository.joinRootTagFindByPostId(postId); post.addPostTagsFrom(findPostTags); return PostResponse.toPostResponse(post); @@ -94,7 +93,7 @@ public Page findAll(Pageable pageable) { @Transactional public PostResponse update(UpdateRequest update, Long userId, Long postId) { Post findPost = postRepository.joinUserFindById(postId) - .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); + .orElseThrow(() -> new IllegalArgumentException("exception.post.notExists")); if (!findPost.getUser().checkSameUserId(userId)) { throw new IllegalArgumentException("exception.post.not.owner"); 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 97641e1..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,7 +6,6 @@ public class UserDto { - @Builder public record UserProfile( Long id, String email, @@ -15,6 +14,17 @@ public record UserProfile( String prologName, String profileImgUrl ) { + @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(), @@ -27,7 +37,6 @@ public static UserProfile toUserProfile(User user) { } } - @Builder public record UserInfo( String email, String nickName, @@ -35,7 +44,14 @@ public record UserInfo( 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/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/messages/exceptions/exception.properties b/src/main/resources/messages/exceptions/exception.properties index a35f3cf..4090df9 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,10 @@ 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=이미 좋아요를 누른 게시물에는 좋아요를 할 수 없습니다. \ No newline at end of file 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..0660115 --- /dev/null +++ b/src/test/java/com/prgrms/prolog/domain/like/api/LikeControllerTest.java @@ -0,0 +1,122 @@ +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().isCreated()) + .andDo(restDocs.document( + requestFields( + fieldWithPath("userId").description("사용자 아이디"), + fieldWithPath("postId").description("게시물 아이디") + ), + 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 8bf7cba..964def7 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 @@ -31,7 +31,7 @@ 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.PostServiceImpl; +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; @@ -50,7 +50,7 @@ class PostControllerTest { @Autowired private ObjectMapper objectMapper; @Autowired - private PostServiceImpl postService; + private PostService postService; @Autowired private UserRepository userRepository; @Autowired @@ -64,9 +64,10 @@ class PostControllerTest { @BeforeEach void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { + userId = userRepository.save(USER).getId(); claims = Claims.from(userId, "ROLE_USER"); - CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", "#tag", false, SERIES_TITLE); + CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", "#tag", true, SERIES_TITLE); postId = postService.save(createRequest, userId); this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .apply(documentationConfiguration(restDocumentation)) @@ -163,12 +164,13 @@ void findById() throws Exception { @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(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(restDocs.document( requestFields( @@ -212,7 +214,7 @@ void remove() throws Exception { @Test @DisplayName("게시물 작성 중 제목이 공백인 경우 에러가 발생해야한다.") - void isValidateTitleNull() throws Exception { + void validateTitleNull() throws Exception { CreateRequest createRequest = new CreateRequest("", "테스트 게시물 내용", "#tag", true, SERIES_TITLE); String requestJsonString = objectMapper.writeValueAsString(createRequest); @@ -226,9 +228,8 @@ void isValidateTitleNull() throws Exception { @Test @DisplayName("게시물 작성 중 내용이 빈칸인 경우 에러가 발생해야한다.") - void isValidateContentEmpty() throws Exception { + void validateContentEmpty() throws Exception { CreateRequest createRequest = new CreateRequest("테스트 게시물 제목", " ", "#tag", true, SERIES_TITLE); - String requestJsonString = objectMapper.writeValueAsString(createRequest); mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts") @@ -240,7 +241,7 @@ void isValidateContentEmpty() throws Exception { @Test @DisplayName("게시물 작성 중 게시물 제목이 50이상인 경우 에러가 발생해야한다.") - void isValidateTitleSizeOver() throws Exception { + void validateTitleSizeOver() throws Exception { CreateRequest createRequest = new CreateRequest( "안녕하세요. 여기는 프로그래머스 기술 블로그 prolog입니다. 이곳에 글을 작성하기 위해서는 제목은 50글자 미만이어야합니다.", "null 게시물 내용", "#tag", 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 2a97cc3..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 @@ -38,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(); diff --git a/src/test/java/com/prgrms/prolog/utils/TestUtils.java b/src/test/java/com/prgrms/prolog/utils/TestUtils.java index 8f1f27d..c4ce30e 100644 --- a/src/test/java/com/prgrms/prolog/utils/TestUtils.java +++ b/src/test/java/com/prgrms/prolog/utils/TestUtils.java @@ -1,6 +1,7 @@ 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; @@ -13,6 +14,7 @@ public class TestUtils { // User Data 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 = "머쓱이"; @@ -21,34 +23,47 @@ public class TestUtils { public static final String PROVIDER = "kakao"; public static final String OAUTH_ID = "kakao@123456789"; public static final String USER_PROFILE_IMG_URL = "http://kakao/defaultImg.jpg"; - public static final User USER = getUser(); - public static final Post POST = getPost(); public static final UserInfo USER_INFO = getUserInfo(); public static final UserProfile USER_PROFILE = getUserProfile(); - public static final Comment COMMENT = getComment(); - public static final Series SERIES = getSeries(); - // Post & Comment Data + 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 String TITLE = "제목을 입력해주세요"; public static final String CONTENT = "내용을 입력해주세요"; - public static final String COMMENT_CONTENT = "댓글 내용"; 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 = "시리즈 제목"; - public static final String USER_ROLE = "ROLE_USER"; + + // 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); + // Authentication public static final String BEARER_TYPE = "Bearer "; - private TestUtils() { /* no-op */ } @@ -124,4 +139,10 @@ public static Series getSeries() { .build(); } + public static Like getLike() { + return Like.builder() + .user(USER) + .post(POST) + .build(); + } } diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql index 62f6f7e..05c7014 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -1,29 +1,31 @@ --- V1__init.sql +-- 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; -DROP TABLE IF EXISTS user_tag; -DROP TABLE IF EXISTS post_tag; -DROP TABLE IF EXISTS root_tag; + CREATE TABLE users ( - id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT, - email varchar(100) NOT NULL UNIQUE, + 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 + 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 @@ -106,4 +108,16 @@ CREATE TABLE user_tag 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 From 41662a8f51bc006e5b707b313b18b1ec4742515c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=EC=A3=BC=EC=84=B1?= <99165624+JoosungKwon@users.noreply.github.com> Date: Fri, 27 Jan 2023 17:45:57 +0900 Subject: [PATCH 14/19] =?UTF-8?q?[#86]=20SpringBoot=20Actuator=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: actuator 의존성 추가 * config: 시큐리티 보안 설정 --- build.gradle | 1 + .../com/prgrms/prolog/global/config/SecurityConfig.java | 1 + src/main/resources/application.yml | 8 +++++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bc3eb5d..83966f9 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,7 @@ 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' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' 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 c830202..9c7f933 100644 --- a/src/main/java/com/prgrms/prolog/global/config/SecurityConfig.java +++ b/src/main/java/com/prgrms/prolog/global/config/SecurityConfig.java @@ -34,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/resources/application.yml b/src/main/resources/application.yml index 154a7c6..46f82d0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -11,4 +11,10 @@ spring: include: - db - exception - - security \ No newline at end of file + - security + +management: + endpoints: + web: + exposure: + include: '*' \ No newline at end of file From 0d3306c87e0d12bc8ce280c0846a1c27742a51d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=EC=A3=BC=EC=84=B1?= <99165624+JoosungKwon@users.noreply.github.com> Date: Fri, 27 Jan 2023 18:36:19 +0900 Subject: [PATCH 15/19] =?UTF-8?q?fix:=20=EC=8B=9C=EB=A6=AC=EC=A6=88=20NPE?= =?UTF-8?q?=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20(#89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 디폴트 값 삽입 --- .../domain/post/service/PostServiceImpl.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 index 790a1f6..6899017 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java +++ b/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java @@ -46,6 +46,7 @@ public class PostServiceImpl implements PostService { private final PostTagRepository postTagRepository; private final UserTagRepository userTagRepository; + @Override @Transactional public Long save(CreateRequest request, Long userId) { @@ -60,15 +61,16 @@ public Long save(CreateRequest request, Long userId) { private void registerSeries(CreateRequest request, Post post, User owner) { String seriesTitle = request.seriesTitle(); if (seriesTitle == null || seriesTitle.isBlank()) { - return; + seriesTitle = "시리즈 없음"; } + final String finalSeriesTitle = seriesTitle; Series series = seriesRepository .findByIdAndTitle(owner.getId(), seriesTitle) .orElseGet(() -> seriesRepository.save( - Series.builder() - .title(seriesTitle) - .user(owner) - .build() + Series.builder() + .title(finalSeriesTitle) + .user(owner) + .build() ) ); post.setSeries(series); @@ -77,7 +79,7 @@ private void registerSeries(CreateRequest request, Post post, User owner) { @Override public PostResponse findById(Long postId) { Post post = postRepository.joinCommentFindById(postId) - .orElseThrow(() -> new IllegalArgumentException("exception.post.notExists")); + .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); Set findPostTags = postTagRepository.joinRootTagFindByPostId(postId); post.addPostTagsFrom(findPostTags); return PostResponse.toPostResponse(post); @@ -93,7 +95,7 @@ public Page findAll(Pageable pageable) { @Transactional public PostResponse update(UpdateRequest update, Long userId, Long postId) { Post findPost = postRepository.joinUserFindById(postId) - .orElseThrow(() -> new IllegalArgumentException("exception.post.notExists")); + .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); if (!findPost.getUser().checkSameUserId(userId)) { throw new IllegalArgumentException("exception.post.not.owner"); From 45b0a6e564adcccff86000af09f19d6a0b97acff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=9D=80=EB=B9=84?= <59335077+hikarigin99@users.noreply.github.com> Date: Fri, 27 Jan 2023 21:22:33 +0900 Subject: [PATCH 16/19] =?UTF-8?q?[#91]=20=EA=B2=8C=EC=8B=9C=EB=AC=BC?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EA=B0=9C=EC=88=98=EB=A5=BC=20PostResponse=EC=97=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: 게시물 좋아요 개수 증가/감소 기능 - JPQL UPDATE 사용을 위해 @Modifying * fix: 게시물 Response 좋아요 개수 * fix: log 설정 변경 * update: 좋아요 생성 반환 값 변경 - 좋아요 생성 리턴 LikeId -> void 로 수정 - @ResponseStatus 이용하여 void로 리턴 * update: 좋아요 생성 Rest docs 상태 코드 변경 --- .../domain/like/api/LikeController.java | 17 +++++------ .../prolog/domain/post/dto/PostResponse.java | 6 ++-- .../domain/post/service/PostServiceImpl.java | 4 +-- src/main/resources/logback.xml | 4 +-- .../domain/like/api/LikeControllerTest.java | 6 +--- .../domain/post/api/PostControllerTest.java | 28 +++++++++++++------ 6 files changed, 35 insertions(+), 30 deletions(-) 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 index ee3e3ad..5a878e9 100644 --- a/src/main/java/com/prgrms/prolog/domain/like/api/LikeController.java +++ b/src/main/java/com/prgrms/prolog/domain/like/api/LikeController.java @@ -1,18 +1,16 @@ package com.prgrms.prolog.domain.like.api; -import java.net.URI; - import javax.validation.Valid; -import org.springframework.http.ResponseEntity; +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 org.springframework.web.util.UriComponentsBuilder; import com.prgrms.prolog.domain.like.dto.LikeDto; import com.prgrms.prolog.domain.like.dto.LikeDto.likeRequest; @@ -29,19 +27,18 @@ public class LikeController { private final LikeServiceImpl likeService; @PostMapping(value = "/{postId}") - public ResponseEntity insert( + @ResponseStatus(HttpStatus.NO_CONTENT) + public void insert( @PathVariable Long postId, @AuthenticationPrincipal JwtAuthentication user ) { LikeDto.likeRequest request = new likeRequest(user.id(), postId); - Long likeId = likeService.save(request); - URI location = UriComponentsBuilder.fromUriString("/api/v1/like/" + likeId).build().toUri(); - return ResponseEntity.created(location).build(); + likeService.save(request); } @DeleteMapping - public ResponseEntity delete(@RequestBody @Valid likeRequest likeRequest) { + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@RequestBody @Valid likeRequest likeRequest) { likeService.cancel(likeRequest); - return ResponseEntity.noContent().build(); } } 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 0a4dc6c..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 @@ -18,7 +18,8 @@ public record PostResponse(String title, Set tags, SeriesResponse seriesResponse, List comment, - int commentCount) { + int commentCount, + int likeCount) { public static PostResponse toPostResponse(Post post) { return new PostResponse(post.getTitle(), @@ -28,6 +29,7 @@ public static PostResponse toPostResponse(Post post) { PostTagsResponse.from(post.getPostTags()).tagNames(), SeriesResponse.toSeriesResponse(post.getSeries()), post.getComments(), - post.getComments().size()); + post.getComments().size(), + post.getLikeCount()); } } 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 index 6899017..9e0f88b 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java +++ b/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java @@ -79,7 +79,7 @@ private void registerSeries(CreateRequest request, Post post, User owner) { @Override public PostResponse findById(Long postId) { Post post = postRepository.joinCommentFindById(postId) - .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); + .orElseThrow(() -> new IllegalArgumentException("exception.post.notExists")); Set findPostTags = postTagRepository.joinRootTagFindByPostId(postId); post.addPostTagsFrom(findPostTags); return PostResponse.toPostResponse(post); @@ -95,7 +95,7 @@ public Page findAll(Pageable pageable) { @Transactional public PostResponse update(UpdateRequest update, Long userId, Long postId) { Post findPost = postRepository.joinUserFindById(postId) - .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); + .orElseThrow(() -> new IllegalArgumentException("exception.post.notExists")); if (!findPost.getUser().checkSameUserId(userId)) { throw new IllegalArgumentException("exception.post.not.owner"); diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 15e9e5c..b2c35d6 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -40,10 +40,10 @@ - + - + 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 index 0660115..3199688 100644 --- a/src/test/java/com/prgrms/prolog/domain/like/api/LikeControllerTest.java +++ b/src/test/java/com/prgrms/prolog/domain/like/api/LikeControllerTest.java @@ -84,12 +84,8 @@ void likeSaveApiTest() throws Exception { .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(likeRequest))) - .andExpect(status().isCreated()) + .andExpect(status().isNoContent()) .andDo(restDocs.document( - requestFields( - fieldWithPath("userId").description("사용자 아이디"), - fieldWithPath("postId").description("게시물 아이디") - ), responseBody() )); } 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 964def7..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 @@ -125,9 +125,13 @@ void findAll() throws Exception { 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("[].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") + ))); } @@ -155,9 +159,12 @@ void findById() throws Exception { 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("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") ))); } @@ -195,9 +202,12 @@ void update() throws Exception { 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("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") ) )); } From 1629171601cd5c1757632b19411d0e2edec08ed0 Mon Sep 17 00:00:00 2001 From: hyunseo <82203978+hyena0608@users.noreply.github.com> Date: Fri, 27 Jan 2023 21:26:07 +0900 Subject: [PATCH 17/19] =?UTF-8?q?[#92]=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=ED=83=9C=EA=B7=B8=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20(#93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 게시글 태그 삭제 기능 리팩터링 - 게시글 연관 태그 삭제 및 수정 - PostTag 삭제 - UserTag 삭제 및 수정 * refactor: 시리즈 컨트롤러 파라미터 리팩터링 * test: 시리즈 컨트롤러 조회 요청 테스트 수정 --- .../prolog/domain/post/service/PostServiceImpl.java | 10 ++++++++-- .../domain/posttag/repository/PostTagRepository.java | 8 ++++++++ .../prolog/domain/series/api/SeriesController.java | 6 +++--- .../prolog/domain/series/api/SeriesControllerTest.java | 3 ++- 4 files changed, 21 insertions(+), 6 deletions(-) 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 index 9e0f88b..26ddf9a 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java +++ b/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java @@ -94,8 +94,8 @@ public Page findAll(Pageable pageable) { @Override @Transactional public PostResponse update(UpdateRequest update, Long userId, Long postId) { - Post findPost = postRepository.joinUserFindById(postId) - .orElseThrow(() -> new IllegalArgumentException("exception.post.notExists")); + Post findPost = postRepository.findById(postId) + .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); if (!findPost.getUser().checkSameUserId(userId)) { throw new IllegalArgumentException("exception.post.not.owner"); @@ -114,6 +114,12 @@ public PostResponse update(UpdateRequest update, Long userId, Long postId) { 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); } 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 index c42fd74..2b79d56 100644 --- a/src/main/java/com/prgrms/prolog/domain/posttag/repository/PostTagRepository.java +++ b/src/main/java/com/prgrms/prolog/domain/posttag/repository/PostTagRepository.java @@ -30,4 +30,12 @@ void deleteByPostIdAndRootTagIds( 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/series/api/SeriesController.java b/src/main/java/com/prgrms/prolog/domain/series/api/SeriesController.java index 395ceaa..de6c972 100644 --- a/src/main/java/com/prgrms/prolog/domain/series/api/SeriesController.java +++ b/src/main/java/com/prgrms/prolog/domain/series/api/SeriesController.java @@ -3,8 +3,8 @@ 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.PathVariable; 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; @@ -20,9 +20,9 @@ public class SeriesController { private final SeriesService seriesService; - @GetMapping("/{title}") + @GetMapping() ResponseEntity findSeriesByTitle( - @PathVariable String title, + @RequestParam String title, @AuthenticationPrincipal JwtAuthentication user ) { return ResponseEntity.ok( 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 index ca14c6c..4a646bd 100644 --- a/src/test/java/com/prgrms/prolog/domain/series/api/SeriesControllerTest.java +++ b/src/test/java/com/prgrms/prolog/domain/series/api/SeriesControllerTest.java @@ -87,8 +87,9 @@ void findSeriesByTitleTest() throws Exception { // given Claims claims = Claims.from(savedUser.getId(), USER_ROLE); // when - mockMvc.perform(get("/api/v1/series/{title}", SERIES_TITLE) + mockMvc.perform(get("/api/v1/series") .header(AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) + .param("title", SERIES_TITLE) ) // then .andExpectAll( From b36d672c421dd0a8a4496920718a507e136b0b9e Mon Sep 17 00:00:00 2001 From: Fortune00 <53924962+Sinyoung3016@users.noreply.github.com> Date: Sun, 29 Jan 2023 20:23:23 +0900 Subject: [PATCH 18/19] =?UTF-8?q?[#60]=20s3=EC=97=90=20=ED=8C=8C=EC=9D=BC(?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80)=20=EC=97=85=EB=A1=9C=EB=93=9C?= =?UTF-8?q?=ED=95=98=EB=8A=94=20api=20=EA=B5=AC=ED=98=84=20(#85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: cloud 의존성과 설정 추가 * add: s3에 존재하는 파일을 업로드하는 유틸 생성 * add: MultipartFile을 File로 전환하는 유틸 생성 - s3는 multipart를 저장하지 못해, 파일로 전환해서 업로드 - 임시로 만들어진 파일을 삭제하는 메소드도 구현 * feat: 파일 업로드 api 구현 - 파일 이름 중복처리를 위해 이름에 UUID를 추가함 - 임시로 만들어진 파일은 이후 삭제됨 * add: 파일 업로드용 exception.properties 추가 * fix: jar 파일 버전 이름 업데이트 * update: profile 설정 변경 --------- Co-authored-by: kwonjoosung Co-authored-by: 권주성 <99165624+JoosungKwon@users.noreply.github.com> --- build.gradle | 4 +- .../global/image/api/ImageController.java | 42 ++++++++++++++++++ .../global/image/dto/UploadFileResponse.java | 10 +++++ .../prolog/global/image/util/FileManager.java | 43 +++++++++++++++++++ .../global/image/util/UploadFileToS3.java | 29 +++++++++++++ src/main/resources/application-aws.yml | 12 ++++++ src/main/resources/application.yml | 3 +- .../messages/exceptions/exception.properties | 6 ++- src/test/resources/application.yml | 31 +++++-------- 9 files changed, 156 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/prgrms/prolog/global/image/api/ImageController.java create mode 100644 src/main/java/com/prgrms/prolog/global/image/dto/UploadFileResponse.java create mode 100644 src/main/java/com/prgrms/prolog/global/image/util/FileManager.java create mode 100644 src/main/java/com/prgrms/prolog/global/image/util/UploadFileToS3.java create mode 100644 src/main/resources/application-aws.yml diff --git a/build.gradle b/build.gradle index 83966f9..3632af3 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,9 @@ 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' + 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' 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/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.yml b/src/main/resources/application.yml index 46f82d0..b722a5b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,9 +12,10 @@ spring: - db - exception - security + - aws management: endpoints: web: exposure: - include: '*' \ No newline at end of file + include: '*' diff --git a/src/main/resources/messages/exceptions/exception.properties b/src/main/resources/messages/exceptions/exception.properties index 4090df9..51ec01b 100644 --- a/src/main/resources/messages/exceptions/exception.properties +++ b/src/main/resources/messages/exceptions/exception.properties @@ -35,4 +35,8 @@ exception.jwtAuthentication.user.email.notExists=유저 이메일이 존재하 exception.jwtAuthenticationToken.isAuthenticated=인증 정보를 확인할 수 없는 메서드 주입은 지원하지 않습니다. 생성자를 통해 생성해야 합니다. ## LIKE ## exception.like.notExist=좋아요를 누르지않아 취소할 수 없습니다. -exception.like.alreadyExist=이미 좋아요를 누른 게시물에는 좋아요를 할 수 없습니다. \ No newline at end of file +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/resources/application.yml b/src/test/resources/application.yml index 2f53868..ec675f4 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -1,28 +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 - security: - oauth2: - client: - registration: - kakao: - client-name: kakao - client-id: ${CLIENT_ID} - client-secret: ${CLIENT_SECRET} - scope: profile_nickname, account_email - redirect-uri: ${REDIRECT_URI} - authorization-grant-type: authorization_code - client-authentication-method: POST - provider: - kakao: - authorization-uri: https://kauth.kakao.com/oauth/authorize - token-uri: https://kauth.kakao.com/oauth/token - user-info-uri: https://kapi.kakao.com/v2/user/me - user-name-attribute: id + jwt: - issuer: prgrms - secret-key: prgrmsbackenddevrteamprologkwonj - expiry-seconds: 3 + expiry-seconds: 2 \ No newline at end of file From 7340ab620ebbede8291744a206c432ded59735f5 Mon Sep 17 00:00:00 2001 From: Fortune00 Date: Mon, 30 Jan 2023 10:32:19 +0900 Subject: [PATCH 19/19] =?UTF-8?q?=08fix:=20dockerFile=EA=B3=BC=20compose?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=EC=97=90=20aws=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 +++- docker-compose.yml | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f743bde..2f3cdfe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,8 @@ 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/docker-compose.yml b/docker-compose.yml index 51a6615..66d5192 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,8 @@ services: 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 @@ -55,3 +57,5 @@ services: 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}